# Sync Encryption Methodology

[Read the essay](https://ownai.com/library/encryption) · Text licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).

How ownAI Sync encrypts an AI instance end to end so that it can be used on
several of a person's own devices, and exactly what the server can and cannot
learn from what it stores. This is the reference for every algorithm,
parameter and limit behind Sync.

> **The server stores ciphertext only.** Encryption and decryption happen in
> the app, on your devices. The server never receives your sync passphrase or
> the key that encrypts your data, and it cannot recover either. What it does
> see — sizes, counts, timestamps and an activity pattern — is listed in full
> in section 6.

---

## 1. Summary

```
sync passphrase (10 words, 129 bits)
   │  Argon2id (64 MiB, 3 passes, 1 lane, 128-bit random salt)
   ▼
wrapping key ──XChaCha20-Poly1305──▶ key envelope   ← stored on the server
                                        │
                                        ▼  (opened only on your devices)
master key (256 bits, random, one per account)
   │  HKDF-SHA256, per instance and purpose
   ├──▶ record key     encrypts database records and the instance name
   ├──▶ blob key       encrypts file contents
   └──▶ record-id key  derives opaque record ids (HMAC-SHA256)
```

A new device signs in, downloads the key envelope, and opens it locally with
the passphrase. No second device, QR code or transfer step is needed.

---

## 2. Principles

- **The key decides, not the word "encrypted".** Data a provider can decrypt
  is data a provider can read. Sync is built so that we cannot.
- **Nothing home-made.** Standard constructions, standard parameters, and
  widely used open-source implementations. We wrote the protocol around them,
  not the cryptography.
- **Precise about metadata.** "We can't read your data" is only honest next to
  a complete list of what we *can* see. Section 6 is that list.
- **Verifiable.** The app is open source; all of the cryptography described
  here runs in its code (section 11), not on our servers.

---

## 3. The passphrase

- **Generated by default.** The app draws ten words uniformly at random from
  the EFF large wordlist, minus its four hyphenated entries (7,772 words,
  12.92 bits each), joined by hyphens: **129.2 bits** of entropy.
- **Custom passphrases** are allowed, with a minimum of 16 characters. They are
  almost always far weaker than the generated one; the app recommends the
  generated passphrase and a password manager.
- **Normalisation.** Before key derivation the passphrase is trimmed and
  composed to Unicode NFC, so that an umlaut typed on one device and pasted on
  another derives the same key. Nothing else is rewritten (no case folding, no
  collapsing of inner spaces), which would shrink the key space.
- **Never transmitted.** The passphrase does not leave the device, in any
  form. There is no "forgot password" flow, because there is nothing on our
  side that could restore access.

---

## 4. Key hierarchy

### 4.1 Master key

One random 256-bit master key per account, generated on the first device from
the operating system's cryptographically secure random source. All synced data
of all instances is ultimately protected by it.

### 4.2 Key envelope

The master key is encrypted ("wrapped") with a key derived from the passphrase:

| Step | Construction |
|---|---|
| Key derivation | Argon2id (v1.3), m = 65,536 KiB (64 MiB), t = 3, p = 1, 128-bit random salt, 256-bit output |
| Wrapping | XChaCha20-Poly1305, fresh random 192-bit nonce, fixed associated data `ownai-sync-key-envelope-v1` |

The envelope stores the salt, the nonce, the ciphertext and the Argon2id
parameters. Because the parameters travel with the envelope, they can be raised
later without locking out existing envelopes.

The parameters are RFC 9106's second recommended option ("for
memory-constrained environments": 64 MiB, 3 passes) with a single lane instead
of four, and well above every configuration in the OWASP Password Storage
Cheat Sheet. The memory ceiling is set by what a mobile app may allocate.

A wrong passphrase is detected by the authentication tag failing — the
envelope simply does not open.

### 4.3 Key id

The envelope also carries a 128-bit key id, HKDF-SHA256 of the master key with
the info string `ownai-sync-key-id-v1`. It is a one-way function of a key the
server never sees and says nothing about the passphrase. Its only purpose: a
device compares it with the key it holds and notices when sync was reset
elsewhere, instead of encrypting new data under a key no other device has.

### 4.4 Per-instance subkeys

From the master key, each device derives three independent keys per instance
with HKDF-SHA256, info string `ownai-sync-v1-<purpose>:<instance id>`, where
purpose is `record`, `blob` or `record-id`. A key that leaked for one instance
or purpose opens nothing else.

### 4.5 On the device

Once unlocked, the master key is kept in the operating system's secret store
(Keychain on macOS and iOS, Credential Manager on Windows, Secret Service on
Linux) under its own entry, separate from the account credentials and provider
API keys. Android has no such store the app can use; there the key lives in
the app's private data directory, sandboxed per app and encrypted at rest by
Android's file-based encryption. The passphrase is typed on setup
and after a re-install, not on every start. Key material held in memory is
wiped when it is dropped.

---

## 5. What is encrypted, and how

### 5.1 Database records

The instance's database — conversations, messages, the AI's memory, settings,
tools and the rest — syncs **per row**, not as a database file. Each row
becomes a record whose payload (table name, row key, column values) is
encrypted with the instance's record key:

- XChaCha20-Poly1305 with a fresh random 192-bit nonce per record version.
- Associated data `ownai-sync-record-v1:<record id>` binds each ciphertext to
  its id: the server cannot move one record's ciphertext onto another record.
- Embedding vectors do not sync at all; each device recomputes them locally.

**Record ids** are HMAC-SHA256 over the table name and row key, keyed with the
instance's record-id key, truncated to 128 bits. They are deterministic, so two
devices that create the same logical row converge on one record — and they
reveal neither the table nor the key. The server cannot tell a message from a
memory entry.

The **instance's display name** is encrypted the same way, so the list of
instances is readable to your devices and to no one else.

### 5.2 Files

Workspace files and the instance's program files sync as files. The file's
description — path, content hash, size, modification time — travels as an
ordinary encrypted record. The contents are encrypted with the instance's blob
key (XChaCha20-Poly1305, fresh nonce, associated data
`ownai-sync-blob-v1:<blob key>`) and stored in S3-compatible object storage
under a **random 128-bit name**. The storage layout reveals no file name and no
path.

### 5.3 Transport

All requests go over HTTPS; live change notices arrive over a WebSocket on the
same connection. Transport encryption is in addition to, not instead of, the
end-to-end encryption above. A change notice carries only "instance X is now at
sequence number N"; the device then fetches the encrypted records.

---

## 6. What the server sees

This is a real record as it is stored on our server (identifiers shortened):

| Field | Value | What it tells us |
|---|---|---|
| `id` | `0bdc367d-…` | Nothing. A server-generated random id. |
| `instanceId` | `cmu60byq…` | Which of the account's instances the record belongs to — not its name. |
| `recordId` | `87781292…` | Nothing about the content or the table. Stable across updates of the same row. |
| `seq` | `361` | Its position in this instance's change stream. |
| `hlc` | `1790091015947:00000:98ab…` | When it was last written (milliseconds), and by which device (a random per-device id). |
| `deleted` | `false` | Whether the row was deleted. |
| `payload` | `KlF7yQRu5c6qNdUM8wST6hI8…` | Nothing. Nonce and ciphertext. |
| `bytes` | `428` | The size of the ciphertext. |
| `updatedAt` | `2026-09-22 16:49` | When it reached the server. |

**Visible to the server:**

- your ownAI account (email address) and whether you have a Sync subscription;
  payment is processed by Stripe
- the number of instances, devices, records and files, and their sizes —
  record and file sizes are exact, not padded (ciphertext = plaintext + 40
  bytes of nonce and tag)
- each device's name as registered with the account (for example
  "ownAI App (Mac)") and when it last synced
- for every record: when it was last written, by which device, how often it
  has changed (via the sequence numbers and the stable record id), and whether
  it was deleted
- when a device is online (its live connection) and, as with any HTTPS
  service, the IP address it connects from

Taken together, this is an **activity pattern**: when your devices are active
and how much changes at once. Not *what* changes.

**Not visible to the server:** any content — messages, memory, settings,
files; file names and paths; instance names; which table a record belongs to,
and therefore whether a record is a message, a memory entry or a setting; the
passphrase; the master key or any key derived from it.

---

## 7. Threat model

**An attacker with full access to our database and object storage** — an
intruder, an insider, a legal order, a future owner — obtains ciphertext, the
metadata in section 6, and the key envelopes. The envelope is the one thing
worth attacking: each passphrase guess costs one Argon2id evaluation at the
parameters above. Against a generated 129-bit passphrase this is infeasible.
A weak custom passphrase is the realistic risk, which is why the app
generates one.

**A malicious or compromised server** cannot read, forge or alter content, and
cannot swap one record's ciphertext for another's; every such attempt fails
authentication on the device. It can, however, affect *availability and
ordering*:

- withhold records or files, or stop serving them altogether
- serve an older version of a record (rollback)
- change the plaintext clock of a record, which decides last-write-wins
- mark a record as deleted — deletion markers carry no ciphertext

None of this discloses data, and every device keeps its own complete copy, but
the server is trusted for delivery, not only for storage.

**Out of scope:**

- **The language model.** Sync protects the path between your devices, not the
  path to a model. Whatever you send to a cloud model is visible to its
  provider. With a local model, nothing readable leaves your devices.
- **The device itself.** Sync does not add encryption to data at rest on the
  device. Someone with access to an unlocked device has access to the instance,
  and malware on a device sees what the app sees.

---

## 8. Reset, cancellation, deletion

- **Forgotten passphrase.** Sync can be reset. This deletes, immediately and
  entirely, the account's synced instances, records, files and key envelope on
  the server — the data is encrypted under the old master key, so keeping it
  would only keep bytes nobody can read. You then choose a new passphrase and
  set up Sync again on your devices. Data on your devices is not touched.
- **Only when the passphrase *and* the last device are lost** is the data gone.
- **Cancellation.** Syncing stops; the server copy stays readable to your
  devices for 30 days and is then deleted. Your devices keep their copies.
- **Deleted records** leave a deletion marker (no content) that is purged after
  90 days.
- **Account deletion** removes all sync data with the account.
- Encrypted data may persist in backups until they rotate out.

---

## 9. Limits

| Limit | Value |
|---|---|
| Storage per account | 2 GB |
| Largest file | 100 MiB (larger files are skipped and logged) |
| Largest single record | 1 MiB |

---

## 10. Limitations

- **Metadata is not hidden.** Sizes are exact and timestamps are in the clear,
  because last-write-wins has to be decided somewhere. Section 6 lists all of
  it; we would rather be precise than reassuring.
- **Delivery is trusted.** See section 7: a malicious server can withhold,
  roll back or delete, though not read or forge.
- **No independent audit yet.** The primitives and their implementations are
  widely reviewed; the Sync protocol built on them has not been audited by a
  third party.
- **A custom passphrase is only as strong as its author.**

---

## 11. Implementation

All of the above runs in the app, which is open source:
[codeberg.org/ownAI/ownAI](https://codeberg.org/ownAI/ownAI).

| Where | What |
|---|---|
| `src-tauri/src/sync/crypto.rs` | Key hierarchy, envelope, record and blob encryption, record ids |
| `src-tauri/src/sync/passphrase.rs` | Passphrase generation and the custom-passphrase minimum |
| `src-tauri/src/sync/keys.rs` | Keychain storage, device id, reset detection |
| `src-tauri/src/sync/records.rs` | What a database row turns into, and which columns travel |
| `src-tauri/src/sync/files.rs` | File sync, conflict copies |
| `src-tauri/src/sync/hlc.rs` | The hybrid logical clock |

Libraries (Rust, from the RustCrypto project unless noted): `chacha20poly1305`
(XChaCha20-Poly1305), `argon2` (Argon2id), `hkdf` and `hmac` over `sha2`
(HKDF-SHA256, HMAC-SHA256), `zeroize` (wiping key material), and `rand`
(randomness, seeded from the operating system).

---

## 12. Sources

- Argon2: A. Biryukov, D. Dinu, D. Khovratovich, J.-P. Aumasson, S. Josefsson,
  *RFC 9106: Argon2 Memory-Hard Function for Password Hashing and
  Proof-of-Work Applications*, 2021. https://www.rfc-editor.org/rfc/rfc9106
- Password Hashing Competition, winner Argon2 (2015).
  https://www.password-hashing.net
- OWASP, *Password Storage Cheat Sheet*.
  https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- ChaCha20-Poly1305: Y. Nir, A. Langley, *RFC 8439: ChaCha20 and Poly1305 for
  IETF Protocols*, 2018. https://www.rfc-editor.org/rfc/rfc8439
- XChaCha20: S. Arciszewski, *XChaCha: eXtended-nonce ChaCha and
  AEAD_XChaCha20_Poly1305*, IETF draft.
  https://datatracker.ietf.org/doc/draft-irtf-cfrg-xchacha/
- ChaCha20-Poly1305 in TLS 1.3: E. Rescorla, *RFC 8446*, 2018.
  https://www.rfc-editor.org/rfc/rfc8446 — and in WireGuard: J. A. Donenfeld,
  *WireGuard: Next Generation Kernel Network Tunnel*, 2017.
  https://www.wireguard.com/papers/wireguard.pdf
- HKDF: H. Krawczyk, P. Eronen, *RFC 5869*, 2010.
  https://www.rfc-editor.org/rfc/rfc5869
- HMAC: H. Krawczyk, M. Bellare, R. Canetti, *RFC 2104*, 1997.
  https://www.rfc-editor.org/rfc/rfc2104
- EFF large wordlist: J. Bonneau, *Deep Dive: EFF's New Wordlists for Random
  Passphrases*, 2016. https://www.eff.org/dice
- Hybrid logical clocks: S. Kulkarni, M. Demirbas et al., *Logical Physical
  Clocks and Consistent Snapshots in Globally Distributed Databases*, 2014.
