Skip to content

Security: vortex2jm/vault

Security

docs/SECURITY.md

Security Model

This document describes the cryptographic design, key lifecycle, threat model, and known limitations of Vault.


Cryptographic Primitives

Key Derivation — Argon2id

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.

Symmetric Encryption — AES-256-GCM

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).


Key Lifecycle

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

Key Material Handling

  • The raw key never leaves the process — it is not written to disk, logged, or transmitted.
  • AesGcmCrypto derives ZeroizeOnDrop — the key is overwritten when the struct is dropped.
  • reset() explicitly zeroizes the key without dropping the struct (used when unlock fails, to avoid leaving a derived-but-wrong key in memory).
  • Entries derive Zeroize — on lock(), every entry is explicitly zeroized before the map is cleared.
  • Passwords read from stdin via rpassword are held in String and zeroized (via str.zeroize()) immediately after use in the CLI layer.

On-Disk Format

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.

Nonce Reuse Prevention

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).


Backup & Integrity

Before every write, the storage adapter:

  1. Copies the existing vault file to <name>.bkp
  2. SHA-256 hashes both files and compares them
  3. Aborts if they differ (corruption during copy)
  4. Writes the new data and calls fsync
  5. Restores from .bkp if 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 Model

Protected Against

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

Out of Scope / Limitations

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

Dependencies

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

There aren't any published security advisories