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.
| Parameter | Value for a new vault |
|---|---|
| Algorithm | Argon2id, version 19 (0x13) |
| Salt | 16 random bytes, per vault |
| Memory | 65,536 KiB (64 MiB) |
| Iterations | 3 |
| Parallelism | 4 lanes |
| Output | 32-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 context | Key | Encrypts |
|---|---|---|
vault-verification-v1 | Verification key | The password verifier |
entry-key-v1 | Entry key | Entry payloads |
entry-blob-key-v1 | Envelope key | Per-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.
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.
-
macOS: the unlock key sits in the data protection keychain with a
biometryCurrentSet+whenUnlockedThisDeviceOnlyaccess 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.
| Setting | Default |
|---|---|
| Lock when the computer sleeps | On |
| Lock when the window closes | On |
| Lock after inactivity | 5 minutes |
| Clear copied secrets from the clipboard | 45 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.
- Read the vault format specification. It defines the byte layout precisely enough to write an independent reader.
- Run the published test vectors against that reader. If it reproduces them, it agrees with Buddy.
-
Point buddy-rescue at your own vault.
buddy-rescue inspectprints the KDF parameters stored in the file without a password. - 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.
v1.3.0 macOS & Windows