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

Algorithms and parameters
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:

Registration fields
Field Contents
emailPlaintext, used for sign-in and as the uniqueness key
master_password_hashBase64 authKey
kdf, kdf_saltThe parameters needed to rebuild masterKey
protected_vault_keyvaultKey sealed under wrapKey
protected_private_keyThe X25519 private key sealed under vaultKey
public_keyThe 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.

What the account pages can do

Read the account's email, size, and age. List the devices signed in to it and sign any of them out. Close the account.

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, or vaultKey.
  • 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 password recovery

There is no escrow key and no reset path. This is the direct cost of the server being unable to read anything.

  • 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.