How a Buddy vault is encrypted, step by step

The exact parameters, the key hierarchy, what the format leaves in cleartext, and how to check all of it yourself.

By Robert, who builds Buddy 9 minute read

Most password managers describe their encryption in one paragraph of adjectives. That paragraph is impossible to check. This page describes Buddy's instead: the algorithms, the exact cost parameters, the key hierarchy, the parts of the file that stay in cleartext, and the published test vectors you can run against your own implementation.

Everything below is implemented in buddy-crypto-core and specified in the vault format document. If this page and the specification ever disagree, the specification is right.

What a vault is on disk

A Buddy vault is a SQLite database plus a directory of encrypted attachment files:

buddy.sqlite
attachments/
└── <vault_id>/
    └── <blob_id>        # raw encrypted attachment bytes

The database holds the vault's key material for verification, one encrypted payload per entry, and the wrapped keys for attachments. Attachment contents live outside the database because they can be large. A copy of buddy.sqlite alone can recover every entry, but it cannot recover attachment contents. A complete backup needs both.

Step 1: your password becomes a key

Buddy takes the exact UTF-8 bytes of your master password and runs them through Argon2id, the winner of the Password Hashing Competition. No normalization, no trimming, no case changes, and no pre-hashing, because any of those would quietly shrink the input space.

InputValue for a new vault
AlgorithmArgon2id
Salt16 random bytes, per vault
Memory cost65,536 KiB (64 MiB)
Time cost3 iterations
Parallelism4 lanes
Argon2 version19 (0x13)
Output32 bytes

The point of a memory-hard function here is economics. An attacker holding a stolen vault file can guess offline as fast as their hardware allows, and 64 MiB per guess makes the parallel hardware that breaks fast hash functions far more expensive to build.

Those four numbers are stored in the vault row itself, not compiled into the app. That detail matters more than it sounds: raising the recommended cost for new vaults in a future release cannot make your existing vault unreadable, because a reader uses the parameters the file carries rather than the ones it prefers.

Step 2: one key becomes three

The 32-byte master key is never used to encrypt anything directly. It is fed into HKDF-SHA-256 to derive three independent 32-byte keys, each bound to a fixed context string:

PurposeHKDF context
Password verifiervault-verification-v1
Entry payloadsentry-key-v1
Attachment key envelopesentry-blob-key-v1

Key separation is cheap insurance. If one usage pattern turns out to have a weakness, that weakness stays confined to the key that was used for it. The context strings are protocol constants: change the spelling, the capitalization, or the version suffix and you derive a different key, which is exactly why they are written down in the specification.

Step 3: checking your password without storing it

Buddy needs to tell a correct master password from a wrong one, and it needs to do that without keeping anything that could confirm a guess more cheaply than deriving the key would.

At vault creation, Buddy generates 32 random bytes with no meaning at all and encrypts them with the verification key. To unlock later, it derives a candidate key from the password you typed and tries to authenticate that stored ciphertext. If the tag verifies, the password was right. The decrypted bytes are thrown away without being read.

A failed unlock is indistinguishable from a corrupt record on purpose, and no code path returns unauthenticated plaintext.

Step 4: entries are encrypted one at a time

Every entry is serialized to JSON and sealed with XChaCha20-Poly1305 using a fresh random 24-byte nonce. XChaCha20's nonce is large enough that random generation is safe without tracking a counter, which removes a class of implementation mistakes.

Each entry's ciphertext also authenticates a record context built from four values: the format identifier buddy-vault-v1, the record type entry, the vault ID, and the entry ID. Each is length-prefixed with a 64-bit big-endian length before being concatenated, so the framing is unambiguous.

This is what stops a rearrangement attack. Someone with write access to your vault file cannot move the ciphertext of one entry into another row, swap a record between two vaults, or replay an old value under a different ID. The identifiers are cleartext in the database, so any substitution changes the associated data and authentication fails.

Step 5: attachments use envelope encryption

Each attachment gets its own random 32-byte data key. The file bytes are encrypted with that key, and the key itself is then encrypted with the attachment envelope key derived in step 2.

The reason is practical. When you change your master password, every derived key changes, and every wrapped data key has to be rewrapped. Rewrapping a 32-byte key is instant. Re-encrypting a 200 MB scanned document is not. The envelope keeps a password change cheap no matter how much you have stored.

What the format does not encrypt

This is the part usually left out. Profile v1 stores some data in cleartext inside the database:

  • Vault names, colors, and password hints
  • Vault and entry IDs
  • Created, updated, last-used and trashed timestamps
  • Per-entry use counters
  • The KDF parameters and salt, which have to be readable to derive a key at all

So someone holding your vault file learns how many entries exist, when you created and last used each of them, and what you named your vaults. They do not learn any titles, usernames, passwords, URLs, notes, TOTP secrets, or attachment filenames, all of which live inside the encrypted entry payload.

Timestamps and usage counters are also not cryptographically bound to the ciphertext in v1, so an attacker with write access to the file could alter them without breaking authentication. They cannot alter, move, or forge the encrypted contents.

What this protects you from, and what it does not

Encryption at rest defends the file. It defends nothing about the machine the file is on while you are using it.

It helps against: a stolen or lost laptop, a copied backup, a cloud folder that syncs your encrypted backup somewhere you did not expect, anyone reading the disk after the fact, and tampering with records inside the database.

It does not help against: malware running as you while the vault is unlocked, a keylogger capturing the master password as you type it, someone watching an unlocked screen, or a master password weak enough to guess. Argon2id raises the cost per guess; it cannot rescue a password that appears in a breach list.

This is also the honest reason a local vault is not automatically safer than a hosted one. It removes an entire category of risk that has nothing to do with your machine, and it leaves every risk that lives on your machine exactly where it was. That tradeoff is worked through in local vs cloud password managers.

How to check any of this yourself

  1. Read the vault format specification. It defines the byte layout precisely enough to write an independent reader.
  2. Run the published test vectors against that reader. If your implementation reproduces them, it agrees with Buddy's.
  3. Point the standalone buddy-rescue tool at your own vault. Its inspect command prints the KDF parameters actually stored in your file, with no password required.

One thing published code does not do is replace an audit. Buddy has not completed an independent third-party review, and nothing on this page should be read as a substitute for one. When that changes, the report goes up here.

The full architecture, including biometric unlock, auto-lock and update signing, is on the security model page.

Try it on your own machine

Buddy is $49 once, with a 30-day trial that needs no account and no card.