secrets-vault 2.4.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](https://img.shields.io/crates/v/secrets-vault.svg)](https://crates.io/crates/secrets-vault)
[![Docs.rs](https://docs.rs/secrets-vault/badge.svg)](https://docs.rs/secrets-vault)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

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

```bash
cargo install secrets-vault          # CLI
```

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

## CLI

```bash
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

```bash
secrets unlock [--strict]   Store the master key behind Touch ID (Secure Enclave)
secrets lock                Remove it (also ends any session broker + revokes leases)
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 lease create P      One tap → project P's declared keys in a TTL-bound
                            encrypted lease; reads need no tap until expiry
secrets lease get P KEY     Read from the lease (no tap; also transparent in get/exec)
secrets lease status        Live leases (purges expired); lease revoke P destroys now
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.toml` — **names only**, values stay in the
vault (see `.secrets.toml.example`):

```toml
[projects.myapp]
secrets = ["DATABASE_URL", "STRIPE_SECRET_KEY"]
```

```bash
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.

A **lease** trades that tap away knowingly, once, for a bounded window: its key sits
in a *non-biometric* Keychain item in the same entitlement-gated access group — tap-free
for this signed binary, unreachable for anything else — and the authoritative expiry
lives beside the key (not in the agent-writable filesystem), enforced on every read.
Tap-free means tap-free for any same-user caller of the CLI: the lease narrows a
compromise to one project's declared keys, until expiry, one audited read at a time.

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

```rust
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.

```rust
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](https://github.com/quantum-encoding/quantum-zig-forge).

## 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