Reference
How Grem encrypts things
Grem is zero knowledge. The sync server stores sealed blobs and the small amount of metadata needed to move them between devices. It cannot read a host name, a password, or a private key, and neither can anyone who takes a copy of its database.
This page is the reference for that claim: what is derived, what is sent, what is stored, and what an attacker holding each piece would actually get.
The primitives
| Job | Algorithm | Parameters |
|---|---|---|
| Password stretching | Argon2id | 64 MiB, t=3, p=4, 32-byte output |
| Key separation | HKDF-SHA256 | 32-byte output per label |
| Item encryption | XChaCha20-Poly1305 | 24-byte nonce, 16-byte tag |
| Account keypair | X25519 | Reserved for sharing between accounts |
| Server-side auth hash | Argon2id | Library defaults, per-user salt |
The Argon2id profile is OWASP's second recommended setting. It costs about 150 ms on a current laptop, which is the point, and it runs on a background isolate so the interface never blocks while it is paid.
Deriving keys
Everything descends from the master password and a random 16-byte salt generated at registration. The salt is not a secret and the server stores it in the clear, because a salt's job is uniqueness, not secrecy.
masterKey = Argon2id(masterPassword, kdfSalt, m=64MiB, t=3, p=4) 32 bytes
authKey = HKDF-SHA256(masterKey, info = "grem-auth-v1") 32 bytes
wrapKey = HKDF-SHA256(masterKey, info = "grem-wrap-v1") 32 bytes
vaultKey = 32 random bytes, generated once at registration
The auth path and the encryption path are separated
The client sends authKey to sign in.
wrapKey never leaves the device. They come off the same
master key through different HKDF labels, so recovering one tells
an attacker nothing about the other.
The vault key is random, not derived
Changing the master password rewraps
vaultKey under a new wrapKey and touches
nothing else. No item is re-encrypted, so a password change is one
small write rather than a re-encryption of the whole vault.
What the server receives
At registration the client sends exactly this:
| Field | Contents |
|---|---|
| Plaintext, used for sign-in and as the uniqueness key | |
| master_password_hash | Base64 authKey |
| kdf, kdf_salt | The parameters needed to rebuild masterKey |
| protected_vault_key | vaultKey sealed under wrapKey |
| protected_private_key | The X25519 private key sealed under vaultKey |
| public_key | The matching X25519 public key |
The server stores an Argon2id hash of
master_password_hash rather than the value itself, so a
database dump does not yield replayable sign-in values.
Sealing an item
Every item is one JSON object sealed under
vaultKey:
blob = version(1) || nonce(24) || ciphertext || tag(16)
The item's own id is passed as associated data. That binding is what stops a hostile server moving one item's ciphertext into another item's slot: the tag stops verifying and decryption fails loudly instead of quietly returning the wrong secret.
The leading version byte exists so the AEAD or the KDF can change later without a painful migration. A blob with an unknown version asks the person to update rather than failing as corruption.
Why these pages cannot open the vault
Signing in here sends a code to the address on the account. That proves someone reads the mailbox. It does not prove knowledge of the master password, and the master password is what the vault is encrypted with.
So the session these pages get is a narrower one. The server marks it, and refuses it on every route that touches vault data, including the route that returns items in their sealed form. That refusal is enforced on the server, not by the browser choosing not to ask.
Mail access is enough to manage an account. It is never enough to read what is in it, and no amount of it ever becomes enough.
What the server actually knows
Stored in the clear
- The account's email address.
- The KDF parameters and salt.
- How many items an account has.
- When each item was created and last changed, to the millisecond.
- The size of each sealed blob, which bounds the size of its plaintext.
- Device names, platforms, and last-seen times for the session list.
- Connecting IP addresses, transiently, for rate limiting.
Never stored, and never transmitted
- The master password.
masterKey,wrapKey, orvaultKey.- Any item's type, name, hostname, username, password, key, tag, or note.
Deliberately, there is no type column on an item. Adding one would make partial sync slightly cheaper and would tell the server how many SSH keys an account holds. The client downloads the whole vault anyway, so the trade was not worth making.
An attacker with the full database can see that an account exists, roughly how much is in it, and when it is used. To read any of it they need the master password, and their only route to that is Argon2id at 64 MiB per guess.
Storage on the device
The offline cache holds the same sealed blobs the server holds, so a stolen laptop yields no more than a stolen database. The key that opens them is held in memory only and dropped when the vault locks.
Quick unlock is the one exception, and it is opt-in.
Turning it on writes vaultKey to the platform keychain
so a biometric prompt can release it. On a device with no keychain
the option is disabled and the interface says so rather than
falling back to something weaker.
Host keys
Host keys are pinned on first use and stored as ordinary vault items, which means they are encrypted like everything else and they sync. Trust established on a laptop does not have to be re-established, unverified, on a phone.
A changed host key stops the connection and asks a person. Both fingerprints are shown, selectable, with the accept button styled as destructive, because the common reason for a changed key is a rebuild and the uncommon reason is interception.
What this design does not do
- No sharing yet. Every account carries an X25519 keypair for it, and the private half is already sealed under the vault key, but nothing uses them.
- No protection against a malicious client build. Zero knowledge protects the vault from the server and from anyone who takes the database. Code running on the device with the vault open can read the vault.
- Metadata is not hidden. Timing and size are visible to the server, as listed above. Hiding them would need padding and cover traffic.