secrets-vault
AES-256-GCM encrypted key-value vault with PBKDF2 key derivation. Store API keys, tokens, and credentials in a single encrypted file instead of plaintext dotfiles.
Works as a Rust library (4 dependencies) and a CLI tool (508 KB binary).
The Problem
# Your .zshrc right now:
Any tool, script, or AI agent that reads your shell config gets every key.
The Solution
# Your .zshrc after:
One line. Zero plaintext keys. Secrets are decrypted from an AES-256-GCM vault at shell startup.
Install
CLI binary
As a library
[]
= { = "1", = false }
CLI Quick Start
# Store secrets
# Add to shell config
# Done. New shells load secrets from the encrypted vault.
Commands
secrets set KEY [VALUE] Store a secret (prompts with hidden input if no value)
secrets get KEY Retrieve a secret (stdout, no trailing newline)
secrets delete KEY Remove a secret
secrets list List all key names (sorted)
secrets env Output all as export KEY='VALUE' for eval
secrets env --json Output as JSON object
secrets import Import KEY=VALUE lines from stdin
secrets export Export all as KEY=VALUE
secrets --version Show version
Bulk import from existing shell config
|
# Then remove the plaintext exports and add:
# eval $(secrets env 2>/dev/null)
Passphrase
The vault passphrase can be provided three ways:
SECRETS_PASSPHRASEenv var — for scripts, CI, shell init- Interactive prompt — hidden input, used by default
- macOS Keychain — store the passphrase in Keychain for auto-unlock:
# Then in .zshrc:
Library Usage
use Vault;
// Create a vault and add secrets
let mut vault = new;
vault.set;
vault.set;
// Encrypt to bytes (QVLT format)
let encrypted = vault.encrypt?;
write?;
// Decrypt from bytes
let data = read?;
let vault = decrypt?;
assert_eq!;
assert_eq!;
API
// Core
new .set // returns previous value
vault.get .delete .keys // sorted
vault.iter .len .is_empty // Crypto
vault.encrypt // Output
vault.to_shell_exports // export KEY='VALUE'\n...
vault.to_json // { "KEY": "VALUE", ... }
// Helpers
is_valid_key // A-Z, 0-9, _
parse_env_lines // parse KEY=VALUE lines
Error Handling
use ;
match decrypt
Embedding in a Tauri / Desktop App
use Vault;
// Load user's vault at app startup
// Use in API calls
let vault = load_user_secrets?;
let api_key = vault.get.unwrap_or_default;
Vault File Format (QVLT)
Offset Size Description
0 4 Magic: "QVLT"
4 1 Version: 0x01
5 16 PBKDF2 salt (random)
21 12 AES-GCM nonce (random)
33 16 AES-GCM authentication tag
49 N Ciphertext
The plaintext inside the ciphertext uses a compact binary encoding:
Repeated:
[2 bytes BE] key length
[N bytes] key (UTF-8)
[4 bytes BE] value length
[N bytes] value (UTF-8)
Terminated by:
[0x00 0x00]
This format is binary-compatible with the Zig implementation — either can read the other's vault files.
Cryptography
| Component | Algorithm | Specification |
|---|---|---|
| Encryption | AES-256-GCM | NIST SP 800-38D |
| Key derivation | PBKDF2-HMAC-SHA256 | RFC 8018, 600k iterations |
| Salt | Random | 128-bit, fresh per save |
| Nonce | Random | 96-bit, fresh per save |
Security properties:
- Authenticated encryption — tampered data is rejected, not decrypted to garbage
- Fresh salt + nonce per save — identical data produces different ciphertext each time
- PBKDF2 at 600k iterations — OWASP 2023 minimum recommendation for SHA-256
- Memory zeroing —
Dropimpl overwrites all secret values before deallocation - No partial decryption — wrong passphrase = immediate GCM auth failure
Environment Variables
| Variable | Description |
|---|---|
SECRETS_PASSPHRASE |
Vault passphrase for non-interactive use |
SECRETS_DIR |
Override vault directory (default: ~/.config/secrets) |
Comparison
| Tool | Encryption | Dependencies | Size | Platforms |
|---|---|---|---|---|
| secrets-vault | AES-256-GCM + PBKDF2 | 4 (lib) | 508 KB | macOS, Linux |
| pass | GPG | gpg, bash, git, tree | ~50 MB | macOS, Linux |
| 1Password CLI | AES-256-GCM | Proprietary | ~30 MB | macOS, Linux, Windows |
| dotenvx | AES-256-GCM | Node.js | ~80 MB | macOS, Linux, Windows |
License
MIT