secrets-vault 2.0.0

AES-256-GCM encrypted key-value vault with PBKDF2 key derivation. Store API keys and tokens securely instead of plaintext dotfiles.
Documentation

secrets-vault

Crates.io Docs.rs License: MIT

AES-256-GCM encrypted key-value vault for API keys and tokens, instead of plaintext dotfiles. Ships a secrets CLI and an embeddable Rust library.

QVLT v2 encrypts per entry: reading one secret decrypts exactly one record, never the whole vault, and writes splice in without touching unrelated ciphertext.

Install

cargo install secrets-vault          # CLI
[dependencies]
secrets-vault = { version = "2", default-features = false }   # library only, 6 deps

CLI

secrets set STRIPE_SECRET_KEY        # prompts with hidden input
secrets get STRIPE_SECRET_KEY
set KEY [VALUE]     Store a secret (prompts if no value; pass on stdin to keep it off argv)
gen KEY             Generate a random secret — the value is never printed
get KEY             Retrieve to stdout, no trailing newline
delete KEY          Remove a secret
list                List key names (sorted)
env [--json]        Emit all as `export KEY='VALUE'` (or JSON) for eval
import / export     KEY=VALUE lines in / out
migrate             Upgrade a v1 vault to v2 per-entry encryption (keeps a .v1.bak)
rekey               Re-encrypt everything under a fresh salt (run after revoking access)

eval $(secrets env) puts the entire vault in the environment, where every child process inherits all of it — including npm/pip postinstall scripts and AI agents. On macOS, prefer the scoped commands below.

macOS: biometric vault + scoped injection

secrets unlock [--strict]   Store the master key behind Touch ID (Secure Enclave)
secrets lock                Remove it (also ends any session broker)
secrets session [MIN]       One tap starts a grant-checked, audited key broker
secrets exec P -- CMD       Run CMD with ONLY project P's secrets in its environment
secrets authorize A P       Grant agent A access to project P (Touch ID)
secrets revoke A P          Revoke it
secrets list-projects       Show projects + agent grants

Declare what a project needs in .secrets.tomlnames only, values stay in the vault (see .secrets.toml.example):

[projects.myapp]
secrets = ["DATABASE_URL", "STRIPE_SECRET_KEY"]
secrets exec myapp -- cargo run
# one Touch ID tap → decrypts only those keys → injects into the child → zeroizes

The master passphrase lives in a data-protection Keychain item in a team-prefixed access group: only this Developer-ID-signed binary can reach it, and only after a tap. --strict stores it with BiometryCurrentSet and a zero-reuse LAContext, so every read re-prompts instead of riding macOS's Touch ID grace window.

Agent identity is resolved from process ancestry and is a soft layer (spoofable by a same-user adversary); the hard boundary is the Touch ID tap.

Other backends

A project can pull from Google Secret Manager ([gsm]) or any secret manager with a CLI via a generic [backend] block — reads return on stdout and writes are piped to stdin, so values never land in argv. AWS Secrets Manager, HashiCorp Vault, Doppler and 1Password recipes are in .secrets.toml.example.

Library

use secrets_vault::Vault;

let mut vault = Vault::new();
vault.set("API_KEY", "sk-secret-123");

let encrypted = vault.encrypt("passphrase")?;
let vault = Vault::decrypt(&encrypted, "passphrase")?;
assert_eq!(vault.get("API_KEY"), Some("sk-secret-123"));

new from_map get set delete keys iter len is_empty to_map encrypt decrypt to_shell_exports to_json, plus is_valid_key, parse_env_lines, encrypt_blob/decrypt_blob and the v2 MasterSecret / is_v2 helpers.

match Vault::decrypt(&data, passphrase) {
    Err(VaultError::DecryptionFailed) => eprintln!("wrong passphrase"),
    Err(VaultError::BadMagic)         => eprintln!("not a vault file"),
    Ok(vault)                         => { /**/ }
    Err(e)                            => eprintln!("{e}"),
}

Cryptography

Component Algorithm
Encryption AES-256-GCM (NIST SP 800-38D)
Key derivation PBKDF2-HMAC-SHA256, 600k iterations (RFC 8018)
Per-entry / manifest / registry keys HKDF-SHA256
Manifest integrity HMAC-SHA256, constant-time verify
Salt / nonce 128-bit / 96-bit, random, fresh per save

Authenticated encryption — tampered data is rejected, not decrypted to garbage. Secret values are zeroized on drop. A wrong passphrase fails GCM authentication immediately.

The file format is binary-compatible with the Zig implementation.

Environment

Variable Description
SECRETS_PASSPHRASE Passphrase for non-interactive use. Convenient for CI; it is visible to anything that can read the process environment, so prefer unlock + exec on macOS. Stripped from exec children.
SECRETS_DIR Vault directory (default ~/.config/secrets)
SECRETS_GSM_ACCOUNT Override the active gcloud account for the GSM backend
SECRETS_GSM_IMPERSONATE Service account to impersonate for GSM
SECRETS_APPROVAL_DIR Approval handshake dir (default ~/.secrets/pending_approvals)
SECRETS_APPROVAL_TIMEOUT_SECS Approval wait before failing closed (default 30)

License

MIT