Buddy security model

Buddy keeps your vault in a SQLite database on your own disk, encrypted with keys derived from your master password. The app has no code path that sends vault data anywhere.

Reviewed against Buddy 1.2.4 · · Vault format v1

Storage

A vault is a SQLite database plus a directory of encrypted attachment files, both in the operating system's application data folder (~/Library/Application Support/com.brandnewbyte.buddy on macOS, %APPDATA%\com.brandnewbyte.buddy on Windows):

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

The database holds one encrypted payload per entry, the password verifier for each vault, and the wrapped key for each attachment. Attachment contents live beside it because they can be large, so a copy of buddy.sqlite alone recovers every entry but no attachment contents. Buddy's own backups are zip archives that carry both.

Stored in cleartext

Format v1 leaves this metadata readable inside the database:

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

Someone holding the file learns how many entries exist, when each was created and last used, and what the vaults are named. Titles, usernames, passwords, URLs, notes, TOTP secrets and attachment filenames are inside the encrypted payload. Timestamps and counters are not bound to the ciphertext in v1, so someone with write access can change them without failing authentication. They cannot alter, move or forge encrypted contents.

Cryptography

Implemented in buddy-crypto-core and specified in the vault format document. Where this page and the specification disagree, the specification is right.

Key derivation

The master password's exact UTF-8 bytes go into Argon2id, with no normalization, trimming, case folding or pre-hashing. Each of those would shrink the input space.

ParameterValue for a new vault
AlgorithmArgon2id, version 19 (0x13)
Salt16 random bytes, per vault
Memory65,536 KiB (64 MiB)
Iterations3
Parallelism4 lanes
Output32-byte master key

The parameters are stored in each vault's row rather than compiled into the app. A release that raises them for new vaults leaves existing vaults readable, because a reader uses the parameters the file carries.

Key separation

The master key never encrypts anything directly. HKDF-SHA-256 derives three 32-byte keys from it, each bound to a context string:

HKDF contextKeyEncrypts
vault-verification-v1Verification keyThe password verifier
entry-key-v1Entry keyEntry payloads
entry-blob-key-v1Envelope keyPer-attachment data keys

A weakness in one usage stays confined to its key. The context strings are protocol constants: a different spelling, capitalization or version suffix derives a different key.

Key hierarchy: the master password goes through Argon2id to a 32-byte master key. HKDF-SHA-256 derives three keys from it, one per context string: a verification key that seals the password verifier, an entry key that encrypts each entry with XChaCha20-Poly1305, and an envelope key that wraps a random key for each attachment. The derived keys live in memory only while the vault is unlocked; their outputs are stored in buddy.sqlite. Held in memory while unlocked, wiped on lock Stored in buddy.sqlite Master password UTF-8, as typed Argon2id 64 MiB · t=3 · p=4 16-byte salt per vault Master key 32 bytes HKDF SHA-256 vault-verification-v1 Verification key 32 bytes Password verifier 32 random bytes, sealed at creation entry-key-v1 Entry key 32 bytes Entry payloads XChaCha20-Poly1305, fresh nonce per entry entry-blob-key-v1 Envelope key 32 bytes Attachment keys one random key per file, wrapped by this key

Entries and attachments

Each entry is serialized to JSON and sealed with XChaCha20-Poly1305 under a fresh random 24-byte nonce. At 192 bits, random nonces are safe without a counter, which removes a class of implementation mistakes.

The associated data binds each ciphertext to where it belongs: the format identifier buddy-vault-v1, the record type, the vault ID and the entry ID, each prefixed with its length as a 64-bit big-endian integer. Moving a ciphertext to another row or vault, or replaying it under another ID, fails authentication.

Attachments use envelope encryption. Each file gets a random 32-byte data key, and that key is encrypted under the envelope key. Changing the master password rewraps the 32-byte keys instead of re-encrypting every file.

Unlocking

Master password

At vault creation Buddy encrypts 32 random bytes with the verification key. To unlock, it derives a candidate key from what you typed and tries to authenticate that ciphertext. A valid tag means the password is correct, and the decrypted bytes are discarded unread. A wrong password and a corrupted record fail the same way, and no code path returns unauthenticated plaintext.

The master password is never written anywhere, and there is no reset. Without it, or a biometric unlock already set up on that device, the vault cannot be decrypted by you, by us or by anyone else.

Touch ID and Windows Hello

Biometric unlock does not store the master password either. The operating system holds a random 32-byte unlock key, and Buddy's database holds the master key encrypted under it with XChaCha20-Poly1305. Neither half is useful alone.

Biometric unlock: a fresh Touch ID or Windows Hello check makes the operating system release a random 32-byte unlock key. That key unwraps a copy of the master key stored in Buddy's database using XChaCha20-Poly1305, and the master key then derives the vault keys as usual. The master password is never stored. Biometric check Touch ID or Windows Hello fresh match every unlock OS credential store Unlock key 32 random bytes Keychain on macOS, TPM-wrapped on Windows Buddy's database Wrapped master key nonce + ciphertext Unwrap XChaCha20-Poly1305 Master key 32 bytes Into HKDF, as in the diagram above
  • macOS: the unlock key sits in the data protection keychain with a biometryCurrentSet + whenUnlockedThisDeviceOnly access control. The OS releases it only after a fresh Touch ID match on that Mac, and adding or removing a fingerprint invalidates it.
  • Windows: the unlock key is encrypted under a key derived from a signature by a TPM-held Windows Hello credential, which Windows produces only after a fresh Hello check (face, fingerprint or PIN). Credential Manager stores the challenge, nonce and ciphertext; reading the entry gets nothing without the signature. Resetting Windows Hello invalidates it.

When the unlock key is invalidated, Buddy asks for the master password.

While a vault is unlocked

Any password manager has to hold its keys in memory while a vault is unlocked, since that is what unlocked means. Buddy also decrypts the vault's entries, password history included, into memory at unlock, which lets search and autofill answer without touching the disk. Masked fields are masked in the interface only. Locking closes every open vault at once and drops the keys and decrypted entries; key material and plaintext serialization buffers in the Rust core are zeroized when dropped. The interface is a webview, and strings in its JavaScript heap cannot be reliably wiped, so those are released on lock and left to the garbage collector.

SettingDefault
Lock when the computer sleepsOn
Lock when the window closesOn
Lock after inactivity5 minutes
Clear copied secrets from the clipboard45 seconds

All four are configurable. The clipboard is cleared only if it still holds what Buddy put there, so something you copied afterwards is left alone.

Browser extension

The extension holds no vault data. It reaches the desktop app through the browser's native messaging host, a small binary installed alongside Buddy, which relays requests over a local socket: a Unix socket in a 0700 directory owned by your user on macOS, and the named pipe \\.\pipe\buddy on Windows.

Before any request is read, the app checks who is on the other end:

  • The peer's executable must be the native host installed next to the app (both platforms).
  • The peer must run as your OS user (macOS).

Release builds refuse a connection they cannot verify.

The first request from a new extension install prompts you in the app. Approving it issues a secret, stored in the OS keyring, that every later request must present. A new major version of the extension pairs again.

Matching the current page returns titles, usernames and URLs. A password, card number or TOTP code leaves the app only when you fill, only for the one section you picked, and only while the vault is unlocked.

These checks stop opportunistic local software. They do not stop a targeted attacker running code as you, who can launch the native host directly, and the extension's identity is self-reported. Against that attacker the boundary is the vault being locked. The extension is open source under GPL-3.0 at github.com/brandnewbyte/buddy-extension.

Network requests

These are all the requests the app makes. They go to pwbuddy.com over HTTPS (rustls). The interface's Content Security Policy is connect-src 'self' ipc:, so the webview cannot make requests of its own.

Request When Sends
Trial start Starting the trial App version, device ID
License activation Entering a license key App version, device ID, license key
License check At most once every 24 hours App version, device ID, activation ID, platform, CPU architecture, OS name and version, current license signature
Update check On launch and daily if automatic update checks are on, or from the menu App version, activation ID (when licensed), platform, CPU architecture

The device ID is a SHA-256 hash of the operating system's machine ID. The OS string excludes the hostname and edition. No request carries vault contents, entry counts or usage data.

After activation the license is verified locally against its signature, so a failed or blocked check changes nothing and the app works offline. Downloading an update fetches the installer from a short-lived signed URL on our release storage. Site icons shown next to entries are captured by the extension from the page you are filling, not fetched by the app.

Updates

Update packages are signed with an Ed25519 key (minisign). The public key is compiled into the app, and the updater checks the signature before installing anything. On top of that, macOS builds are notarized by Apple and Windows installers are Authenticode-signed.

When an update changes the database schema, Buddy writes a backup before migrating. Restoring a backup also backs up the current vault first, and a backup is validated before it replaces anything.

Threat model

Encryption at rest protects the file. It does not protect the machine while you are using it.

Defends against

  • A lost or stolen computer, powered off or with the vault locked
  • A copied vault file or backup, including one synced somewhere you did not intend
  • Tampering with encrypted records in the database
  • An update package that was not signed by us
  • Local software probing for the extension's socket

Does not defend against

  • Malware running as you while a vault is unlocked
  • A keylogger capturing the master password
  • Someone using your computer while a vault is unlocked
  • A master password weak enough to guess, whatever the Argon2id cost
  • A targeted attacker who already runs code as your user

A local vault removes the risks of a vault stored on someone else's server and leaves the risks on your own machine where they were. That trade-off is worked through in local vs cloud password managers.

Checking our work

The cryptography core, the vault specification and its test vectors are public, which makes the implementation inspectable. That is not an audit. None has been done yet, and the report will be published here when one is. The desktop app's interface, updater and licensing code are closed; the open-source page lists exactly what is published.

  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 it reproduces them, it agrees with Buddy.
  3. Point buddy-rescue at your own vault. buddy-rescue inspect prints the KDF parameters stored in the file without a password.
  4. Watch the app with a network monitor such as Little Snitch or Wireshark and compare its traffic with the table above.

Reporting a vulnerability

Email support@pwbuddy.com with "Security" in the subject. Include the Buddy version, your OS and the steps to reproduce, and please give us the chance to ship a fix before publishing details. The same contact is in /.well-known/security.txt.

Everything above applies to the 30-day trial, which needs no account.

Download Buddy

v1.3.0 macOS & Windows