Skip to content

Architecture & cryptography

Everything below is optional reading. Blackbook is safe without you understanding any of it — but it's open source precisely so you can verify the claims. This is a faithful summary of what the code actually does.

The layered defense

  • In transit: TLS 1.3 only, modern AEAD ciphers, with compression and renegotiation disabled. Every request must present both a CA-issued client certificate (its Common Name and SHA3-256 fingerprint pinned to the client's row) and a matching bearer token — and both must resolve to the same client. There is no cert-only or token-only path.
  • Credentials at rest (on your machine): each profile is sealed with Argon2id(passphrase, salt) (64 MiB, t=3, p=1) → key → AES-256-GCM over the bundle. The passphrase is never stored; a stolen ~/.bbk is inert without it. Each profile also carries a rotation-stable Client Master Key, so client-side-encrypted data survives credential rotation.
  • Secrets at rest (on the server): string secrets use a two-layer AES-256-GCM envelope (scrypt-derived, domain-separated keys); files use a random per-file key, AES-256-GCM, with the plaintext's SHA3-256 stored separately and verified on retrieval.
  • Metadata at rest: the database encrypts not just values but every user-supplied identifier — secret names, client names, domain names, permission patterns, file metadata, and audit messages — at the column level. Stable lookups go through domain-separated HMAC index columns. An attacker with full SQL access sees only opaque IDs, ciphertext, and timestamps.
  • The master key: derived at every boot from a provider you supply — a keyfile (kept off the data volume as a Docker secret) or a passphrase. The plaintext data-encryption key is never written to disk; only a salt or a wrapped blob is persisted. A copy of the data volume alone is not decryptable.
  • The audit log: append-only, and every row carries a keyed SHA3-256 hash chained over the previous row, so altering, deleting, or reordering any past entry is detectable (audit --verify). The MAC key is derived from the master key and never stored in the database. Old entries can be exported to compressed, encrypted, independently verifiable archives (audit --archive).

The key hierarchy

One PrimaryKey (pure CSPRNG secret material) is the root. From it, six named SecondaryKeys are derived, each bound to a distinct, immutable domain string (secret/layer1/v1, secret/layer2/v1, file/dek-kek/v1, index/v1, hmac/v1, mfa/secret-kek/v1), so two call sites can never accidentally share a key. Wrapping uses 30 iterations of RFC 3394 AES-256 key wrap, each iteration re-deriving its KEK. The whole bundle is self-encrypted and carries an Ed25519 identity key. See src/blackbook_core.rs.

Client-side ("external") encryption (advanced)

Normally the server encrypts your data (and can therefore serve it back). With external storage, your machine encrypts it first, and the server holds only an opaque envelope it can't open. Two independent axes:

  • Key source. --external-key (-e) wraps the data key under your profile's managed Client Master Key — no extra passphrase to remember. --external-passphrase wraps it under Argon2id(passphrase, salt) instead — portable to any machine that knows the passphrase, and requested on every read (never cached).
  • Data location (files only). By default the ciphertext still lives in Blackbook (opaque to it). --external-data (-E) keeps the ciphertext resident on your machine, and the server holds only a manifest plus its half of a split key — true mutual custody, where neither side can decrypt alone.

--external is the shorthand for "managed key, and (for files) resident data." Reading still requires passing every permission gate and holding the client-side key — both factors, neither sufficient alone. (--rotate-on-read is refused for external items, since the server can't re-key what it can't read.)

Managed keys and the crypto toolkit

A managed key is a named row in a domain with one or more versions. Each version's 32 bytes of material are sealed with AES-256-GCM under a KEK derived from the master key (file/dek-kek/v1 with the info tag managed-keys/v1), with the key's id and version number as associated data, so material copied between rows or versions in the database fails to decrypt rather than working silently. The key's name is column-encrypted and HMAC-indexed like every other identifier; only Ed25519 public keys are stored in the clear. Material is generated server-side (or imported once) and is never returned by any endpoint.

Output from a managed key is tagged with the version that made it (bbk:v3:…), so rotate adds a version for new output while every older version keeps decrypting and verifying. Every operation passes the usual gates (mTLS + token, domain membership, the ACL on the key name, optional MFA) and writes an audit row. With -k, crypto encrypt uses envelope encryption: the server issues a one-time data key wrapped under the managed key, the CLI encrypts locally, and only the wrapped 32-byte data key is ever sent back for decryption.

Prefix What it is
bbk:v<N>: Ciphertext, signature, or MAC from managed-key version N (base64(nonce ‖ ct ‖ tag) for ciphertext).
bbkd1: Envelope: the managed-key-wrapped data key plus the locally sealed payload.
bbkx1: Passphrase envelope: Argon2id(passphrase, salt) → AES-256-GCM (same format as the MCP bbk_encrypt tool).
bbkc1: Profile envelope: an HKDF-SHA256 subkey of your client master key → AES-256-GCM.
bbkk1: Raw-key envelope: standard AES-256-GCM, nonce(12) ‖ ct ‖ tag, openable by any AES-GCM library.

Tokens are standard compact JWTs: EdDSA (Ed25519, RFC 8037) or HS256, with kid set to <key>:v<N>. iat, exp, and jti are always set by the issuer and can't be overridden by caller claims. Verification accepts only the algorithm that matches the key (never none), and checks exp / nbf with 60 seconds of clock-skew allowance plus the audience when one is given. Ed25519 verification uses strict mode, which rejects malleable signatures and small-order keys. Password hashes are Argon2id PHC strings (64 MiB, t=3, p=1). See src/crypto_ops.rs.

Architecture and data model

The server authenticates each connection's client certificate (Common Name + fingerprint) and bearer token, resolves the target domain and checks membership, evaluates permission rules (admin? domain-admin? a direct or group rule that's in-window and under its use cap?), enforces per-resource policy flags and K-of-N approvals, and audits every operation, denial, approval, and admin action. It stores encrypted rows in PostgreSQL and encrypted file blobs on the data volume.

Full data model (click to expand)
Table Holds
blackbook_domains namespaces + groups (name_enc, name_id, description_enc)
blackbook_domain_members (domain, client, role-in-domain)
blackbook_clients identities: name_enc + name_id + token_hash + cert_fingerprint + role + totp_secret_enc + expires_at
blackbook_acl permission rules: subject (client or group) + pattern_enc + action bits + expires_at/not_before/max_uses/use_count
blackbook_secrets encrypted secrets + name_id + flags + read_count + access_policy + exhausted_at + optional external envelope
blackbook_pages file metadata + wrapped_dek + flags + external/resident key material
blackbook_contents references to encrypted blobs on the data volume
blackbook_access_requests K-of-N requests: signatories + approvers + consumed_at
blackbook_access_grants advance (pre-)approvals
blackbook_audit append-only event log with a prev_hash/row_hash tamper-evidence chain
blackbook_schema_migrations applied schema migrations

Permission action bits: create=1, read=2, update=4, delete=8.

For the full cryptographic details, the persistence design, and the exact envelope formats, read the source — it's heavily commented. Deployment and operations (backups, hardening, scaling, Postgres TLS) live in the deployment guide.