XYZ Passwords — Security & Technical Specification

How the XYZ Passwords vault is encrypted: Argon2id key derivation, XChaCha20-Poly1305 authenticated encryption, the on-disk vault format, storage backends, cross-device sync, and the threat model.

Overview

This page documents the cryptographic design of XYZ Passwords for users and security researchers who want the details. The design goal is zero-knowledge: your master password is processed only on your device, the encryption key is never stored or transmitted, and xyz.am only ever holds an encrypted blob it cannot read.

Every implementation — the web app, the browser extension, and the mobile app — produces a byte-identical vault format, so the same vault can be opened and synced across all of them.

Cryptographic design

Your master password is stretched into an encryption key with a memory-hard function (Argon2id). That key seals the vault with an authenticated cipher (XChaCha20-Poly1305), which both encrypts and tamper-proofs the data.

ParameterValue
Key derivationArgon2id (RFC 9106), libsodium ALG_ARGON2ID13 (v1.3)
Memory cost65,536 KiB (64 MiB)
Time cost3 iterations
Parallelism1
Salt16 bytes, randomly generated per vault
Derived key32 bytes (256-bit)
CipherXChaCha20-Poly1305-IETF (AEAD)
Nonce24 bytes, randomly generated for every encryption
Authentication tag16 bytes (Poly1305)
Additional data (AAD)xyz-passwords-vault-v1
EncodingBase64 (standard alphabet, padded)
RandomnessOperating-system CSPRNG

A fresh random nonce is used for every save, and the AAD binds each ciphertext to this vault format so a blob can't be replayed into a different context.

Vault format

The vault is a small JSON document. Everything except the sealed ciphertext is public metadata needed to re-derive the key and decrypt — it contains no secrets:

{
  "vault_version": 1,
  "kdf": {
    "algorithm": "argon2id",
    "salt": "<base64, 16 bytes>",
    "memory": 65536,
    "iterations": 3,
    "parallelism": 1
  },
  "encryption": {
    "algorithm": "xchacha20-poly1305",
    "nonce": "<base64, 24 bytes>"
  },
  "ciphertext": "<base64: XChaCha20-Poly1305 sealed JSON>"
}

Decrypting the ciphertext yields the plaintext vault — a list of entries:

{
  "entries": [
    {
      "id": "<uuid>",
      "title": "Example",
      "website_url": "https://example.com",
      "username": "you@example.com",
      "password": "<secret>",
      "notes": "",
      "tags": [],
      "created_at": "<ISO-8601>",
      "updated_at": "<ISO-8601>"
    }
  ]
}

The plaintext JSON exists only in memory while the vault is unlocked. Only the sealed blob above is ever written to storage or transmitted.

Keys & what leaves your device

  • The master password never leaves your device.
  • The derived key is held only in memory and is erased when the vault locks.
  • The only thing written or transmitted is the encrypted blob.

Keep unlocked (optional). If you turn it on, the derived key is cached locally with an expiry you choose so you don't re-enter your master password constantly. It is stored on-device only (in the platform's secure storage on mobile) and is never sent to any server. Turn it off and the key is removed.

Storage backends

  • Local Only — the encrypted blob stays on your device. No sync, no server storage.
  • Local File — export/import the encrypted blob as a file you control.
  • Bring Your Own SQL — your own database stores the blob; it never sees plaintext or keys. See the BYO SQL guide.
  • XYZ Encrypted Sync (Premium) — xyz.am stores only the encrypted blob on Cloudflare R2 (at vault.xyz.am) and cannot decrypt it.

In every mode, only the encrypted blob is stored or transmitted — never your passwords, never your keys.

Cross-device sync

Sync operates entirely on encrypted blobs. When a device pulls a remote vault it decrypts it locally, then performs a merge: entries are unioned by id, and where the same id exists on both sides the one with the newer updated_at wins. The merge never silently drops an entry. The re-sealed blob is then pushed back.

For XYZ Encrypted Sync, the vault.xyz.am worker authenticates your account token before serving the blob. Reading your own blob is always allowed; writing requires an active Premium plan. If Premium lapses, your blob stays downloadable but stops syncing until renewed.

Threat model

What an attacker who obtains the stored blob sees: the vault version, KDF parameters, the salt, the nonce, and the ciphertext. None of these reveal your passwords. Recovering the contents requires guessing your master password and paying the full Argon2id cost for every guess.

What xyz.am can see: that an encrypted blob exists for your account (for the sync backend), its size, and when it was last updated. What xyz.am cannot see: your master password, your derived key, or any entry in your vault.

Out of scope: a compromised device. If malware controls the machine while the vault is unlocked, it can read what you can read. Keep your devices trusted and patched, and use keep unlocked conservatively on shared machines.

Recovery

If you lose your master password and have no backup, your vault cannot be recovered. That is the direct consequence of zero-knowledge — no server holds anything that can decrypt it.

Your safety net is an encrypted export you control: from Settings you can save an encrypted copy of the vault and import it on any device. Keep that export plus your master password somewhere safe.

Implementations

  • Web & browser extension — libsodium compiled to WebAssembly.
  • Mobile app — native libsodium, for fast on-device key derivation.

All three use the same libsodium primitives and the same parameters above, which is what keeps the vault format identical across platforms.