Skip to content

Cryptography

kovra rolls no cryptography of its own. Every primitive below is a vetted, widely-used Rust implementation — the RustCrypto family, age, and BLAKE3. kovra’s job is to compose them correctly: the right primitive for each task, the right parameters, and no plaintext left where it shouldn’t be.

This page documents exactly what runs, with the real parameters from the source.

Purpose Algorithm Key / parameters Library
Encryption at rest ChaCha20-Poly1305 (AEAD) 256-bit key, 96-bit random nonce per write chacha20poly1305
Master-key custody OS keyring — (macOS Keychain) keyring
Headless key derivation Argon2id 19 MiB memory, 2 passes, 1 lane → 256-bit key argon2
Value fingerprint keyed BLAKE3 (truncated) key from the master key + the coordinate; first 4 bytes → 8 hex chars; none for critical values blake3
Master-key check ChaCha20-Poly1305 (AEAD) fixed value sealed under a BLAKE3 subkey of the master key (keycheck.sec) chacha20poly1305, blake3
Coordinate addressing BLAKE3 full digest of the canonical path blake3
Master-key backup age scrypt (passphrase) ASCII-armored age
Signing keypairs ed25519 / RSA-3072 (PKCS#1 v1.5 + SHA-2) OpenSSH format ssh-key, rsa
Asymmetric encryption X25519 (via age, ed25519 keys only) — age, ssh-key
TOTP codes RFC-6238 HMAC-SHA1 (SHA-256/512 optional) 6 digits, 30-second period hmac, sha1/sha2
Randomness OS CSPRNG — getrandom / OsRng
In-memory hygiene zeroize + secrecy — zeroize, secrecy

Every vault record — and the metadata index — is independently sealed with ChaCha20-Poly1305, an AEAD (authenticated encryption with associated data) cipher, under the vault’s 256-bit master key. Each write generates a fresh random 96-bit nonce, so two seals of the same record always differ and a nonce is never reused. The metadata and the value are sealed together, so neither the secret nor even its coordinate appears as plaintext on disk. The authentication tag (Poly1305) means a tampered record fails to open rather than returning garbage, and decryption failures are opaque — a wrong key, a corrupt ciphertext, and a malformed nonce are indistinguishable, so the error can’t act as an oracle. The transient plaintext buffer is zeroized after use.

Why ChaCha20-Poly1305. It’s a modern, constant-time AEAD that is fast in pure software without special CPU instructions (unlike AES, which leans on AES-NI for both speed and side-channel resistance) — the right default for a tool that runs on whatever laptop you have. AEAD gives confidentiality and integrity in one pass, and the per-record random nonce sidesteps the catastrophic nonce-reuse failure mode.

The 256-bit master key is never typed, displayed, or written to a project file. By default it lives in the OS keyring — the macOS Keychain — and kovra loads it only to seal and open records.

For headless use (CI, containers, no keyring), kovra derives the key instead with Argon2id, the memory-hard password KDF, from a passphrase plus a stable per-vault salt. It runs at the library’s defaults — 19 MiB of memory, 2 passes, 1 lane — producing the 256-bit key deterministically, so the same vault unlocks across runs with nothing secret stored on disk (only the non-secret salt).

Why Argon2id. Memory-hardness makes brute-forcing a stolen passphrase expensive on GPUs and custom hardware; Argon2id is the current standard (and the winner of the Password Hashing Competition). The OS keyring is preferred when present because it binds the key to the user’s login session and the platform’s own protections; Argon2id is the portable fallback that needs nothing but a passphrase.

The key check. On macOS any process running as you can rewrite the Keychain item with no prompt, so kovra does not trust whatever key it finds there. The vault holds a key check — keycheck.sec, a fixed value sealed under a subkey derived from the master key — and kovra refuses to read or seal anything with a key that does not open it. A vault written before the check existed adopts its key only if it opens one of the vault’s records, and writes the check then. kovra init creates a key only when the keyring has none (a failed read is an error, never “no key”), and kovra key import restores only a key that opens the vault’s records.

kovra uses BLAKE3 in two places:

  • Value fingerprints. kovra list and doctor show a truncated fingerprint — the first 4 bytes of a keyed BLAKE3 digest, as 8 lowercase hex characters. The key comes from the vault’s master key and the secret’s coordinate, so it answers “did this secret’s value change?” without showing the value — and nobody can compute it outside kovra to confirm a guess (a PIN, say). The same value under two coordinates shows two different fingerprints. It is never the full hash. Critical values show none: high, inject-only and every prod value show —, and the index stores no fingerprint for them — the key does not cover the vault, so a decoy record in another vault could otherwise confirm a guess. Only low/medium values outside prod, which kovra already shows without a dialog, keep one.
  • Coordinate addressing. A record’s on-disk identifier is the BLAKE3 digest of its canonical env/component/key path, so coordinates aren’t leaked as plaintext filenames.

Why BLAKE3. It’s fast, modern, and has a clean, hard-to-misuse API. The fingerprint’s security comes from truncation plus determinism: long enough to detect a change, short enough that it reveals essentially nothing about the value.

kovra key export writes a disaster-recovery backup of the master key as an ASCII-armored age scrypt blob — encrypted under a recovery passphrase you choose, decryptable by any age implementation in an emergency. The transient plaintext is wiped after the call; only the encrypted blob is ever returned. The export is approved with biometrics only when the Mac has Touch ID — the login password does not approve it — and the dialog says it exports the key that opens every secret.

Custodied keypairs are stored in OpenSSH format and used only through kovra:

  • ed25519 — signing (Edwards-curve) and asymmetric encryption (X25519, via the age path above).
  • RSA-3072 — signing and SSH only, using PKCS#1 v1.5 with SHA-2. Deliberately no RSA encryption — when you need to encrypt to a key, use ed25519.

Signatures are made under a fixed SSH signature namespace, so a signature kovra produces verifies with standard ssh-keygen -Y verify. The private half is generated inside the vault, sealed under the master key with the same ChaCha20-Poly1305 path as every other secret, and never written to disk or printed.

Why ed25519 first. Small keys, fast signatures, no parameter foot-guns, and a clean bridge to encryption through X25519. RSA-3072 (≈128-bit security) is kept for interoperability with systems that still require RSA.

A TOTP enrollment custodies the shared seed and computes codes per RFC-6238: HMAC-SHA1 by default (the RFC default), with SHA-256 and SHA-512 available, 6 digits, 30-second period. The HMAC is built on the hmac + sha1/sha2 crates. The seed is sealed like any other secret and never revealed — only the derived, time-limited code is ever produced.

All nonces, generated secrets (kovra generate), and freshly created keypairs draw from the operating system’s CSPRNG (getrandom / OsRng) — never a userspace PRNG seeded from a guessable source.

Secret-bearing types are wrapped in secrecy and implement zeroize: their Debug/Display are redacted (a value can’t leak into a log line or a panic message), and the underlying bytes are wiped from memory when dropped. Transient plaintext buffers — the decrypted record, the serialized payload before sealing — are zeroized explicitly as soon as they’re no longer needed.

  • No home-rolled cryptography. Every primitive is a vetted, widely-reviewed library; kovra only composes them.
  • No nonce reuse. Every AEAD seal uses a fresh random nonce.
  • No value oracle. Fingerprints are truncated, critical values show none, and errors are opaque.
  • No protection past delivery. Once a value is handed to the process that needs it, it lives in that process’s memory under that program’s rules — kovra’s cryptography secures custody and delivery, not what a program does with a value after it receives it.