This document describes the cryptographic design, key lifecycle, threat model, and known limitations of Vault.
Vault uses Argon2id (via the argon2 crate) with default parameters to derive a 256-bit encryption key from the user's master password and a random salt.
| Property | Value |
|---|---|
| Algorithm | Argon2id (hybrid of Argon2i and Argon2d) |
| Output length | 32 bytes (256 bits) |
| Salt length | 16 bytes (128 bits), generated by OsRng |
| Parameters | Argon2::default() (m=19456 KiB, t=2, p=1) |
Argon2id is memory-hard, meaning brute-force attacks are significantly more expensive than with bcrypt or PBKDF2. The salt is unique per vault and stored in the vault file alongside the ciphertext.
Vault encrypts the serialized entries map using AES-256-GCM (via the aes-gcm crate).
| Property | Value |
|---|---|
| Algorithm | AES-256-GCM (AEAD) |
| Key length | 256 bits |
| Nonce length | 96 bits (12 bytes) |
| Nonce source | OsRng — fresh random nonce on every commit |
| Authentication tag | 128 bits (GCM default) |
The AEAD tag provides integrity and authenticity guarantees: any tampering with the ciphertext or nonce will cause decryption to fail deterministically. A failed decrypt is treated as an invalid password (the two cases are indistinguishable from the ciphertext alone).
create/unlock
│
▼ Argon2id(password, salt) → key: [u8; 32]
│ stored in AesGcmCrypto.key (Option<[u8;32]>)
│
├──► commit: key used in encrypt(), nonce refreshed each time
│
└──► lock / process exit:
ZeroizeOnDrop fires → key bytes overwritten with 0x00 in memory
entries: BTreeMap also zeroized via Zeroize derive on Entry
- The raw key never leaves the process — it is not written to disk, logged, or transmitted.
AesGcmCryptoderivesZeroizeOnDrop— the key is overwritten when the struct is dropped.reset()explicitly zeroizes the key without dropping the struct (used whenunlockfails, to avoid leaving a derived-but-wrong key in memory).- Entries derive
Zeroize— onlock(), every entry is explicitly zeroized before the map is cleared. - Passwords read from stdin via
rpasswordare held inStringand zeroized (viastr.zeroize()) immediately after use in the CLI layer.
Each .vault file is a wincode-serialized VaultState:
VaultState {
salt: [u8; 16], // Argon2id salt — plaintext
nonce: [u8; 12], // AES-GCM nonce — plaintext
cipher: Vec<u8>, // AES-256-GCM ciphertext + 16-byte auth tag
}
The salt and nonce are stored in plaintext — this is standard and safe; neither is secret. The cipher field is the concatenation of the encrypted payload and the GCM authentication tag.
A fresh random nonce is generated by OsRng on every commit. Nonce reuse under AES-GCM with the same key would be catastrophic (it leaks the authentication key and reveals plaintext XOR). Because the nonce is random and 96 bits wide, the probability of collision is negligible for any reasonable number of commits (birthday bound: ~2^48 commits for 1% collision probability).
Before every write, the storage adapter:
- Copies the existing vault file to
<name>.bkp - SHA-256 hashes both files and compares them
- Aborts if they differ (corruption during copy)
- Writes the new data and calls
fsync - Restores from
.bkpif the write fails
This ensures that a crash or power loss during a write does not leave the vault in an unrecoverable state.
Note: The SHA-256 hash here is used for integrity verification of the backup copy, not for authentication of the vault contents. Vault authentication is provided by the AES-GCM tag.
| Threat | Mitigation |
|---|---|
| Offline brute force on stolen vault file | Argon2id key derivation makes each attempt expensive |
| Ciphertext tampering | AES-GCM authentication tag — decryption fails on any modification |
| Secrets lingering in RAM after lock | ZeroizeOnDrop on key, Zeroize on entries, explicit zeroize on passwords |
| Vault file corruption on crash | Backup + fsync write protocol |
| Accidental vault overwrite | create_vault now checks for existing file before proceeding |
| Threat | Status |
|---|---|
| Keylogger / clipboard snooping | Not mitigated — OS-level threat |
| Root/kernel-level memory access | Not mitigated — memory zeroization is best-effort in most OS environments |
| Side-channel attacks (timing, cache) | Not specifically hardened |
| Swap / hibernation leaking secrets | Not mitigated — use encrypted swap / disable hibernation |
.bkp file left on disk |
Backup is not deleted after a successful write — contains previous vault version |
| Crate | Purpose |
|---|---|
aes-gcm |
AES-256-GCM AEAD encryption |
argon2 |
Argon2id key derivation |
zeroize |
Memory zeroization of secrets |
rpassword |
Password input without terminal echo |
wincode |
Binary serialization |
serde |
Derive serialize/deserialize for models |
sha2 |
SHA-256 for backup integrity check |
rustyline |
Line editing, history, tab-completion |
anyhow |
Error propagation in the CLI layer |
thiserror |
Typed error definitions |
chrono |
Timestamps on entries |
dirs-2 |
Home directory resolution |