| title | Security Architecture |
|---|---|
| weight | 3 |
| toc | true |
Comprehensive security architecture, cryptographic implementation, threat model, and security guarantees for Pass-CLI.
Pass-CLI is designed with security as the primary concern. All credentials are encrypted using industry-standard cryptography and stored locally on your machine with no cloud dependencies.
- AES-256-GCM Encryption: Military-grade authenticated encryption
- PBKDF2 Key Derivation: 600,000 iterations with SHA-256 (hardened)
- BIP39 Recovery Phrase: 24-word mnemonic for vault password recovery (industry-standard)
- System Keychain Integration: Secure master password storage
- Offline-First Design: No network calls, no cloud dependencies
- Secure Memory Handling: Byte-based password handling with immediate zeroing
- Password Policy Enforcement: Complexity requirements for vault and credential passwords
- Tamper-Evident Audit Logging: HMAC-SHA256 signed audit trail (optional)
- File Permission Protection: Vault files restricted to user-only access
- Atomic Vault Operations: Rollback safety for vault updates
AES-256-GCM (Galois/Counter Mode)
- Algorithm: Advanced Encryption Standard
- Key Size: 256 bits (32 bytes)
- Mode: GCM (Galois/Counter Mode)
- Authentication: Built-in GMAC authentication tag
- Implementation: Go standard library
crypto/aesandcrypto/cipher
- NIST Approved: Recommended by NIST for classified information
- Authenticated Encryption: Prevents tampering and chosen-ciphertext attacks
- Parallelizable: Fast performance on modern hardware
- Standard: Widely used and well-audited implementation
PBKDF2-SHA256
- Algorithm: Password-Based Key Derivation Function 2
- Hash Function: SHA-256
- Iterations: 600,000 (hardened from 100,000)
- Salt Length: 32 bytes (256 bits)
- Output Length: 32 bytes (256 bits)
- Implementation:
golang.org/x/crypto/pbkdf2 - Performance: ~50-100ms on modern CPUs (2023+), 500-1000ms on older hardware
Master Key = PBKDF2(
password = user's master password,
salt = unique 32-byte random salt,
iterations = 600,000,
hash = SHA-256,
key_length = 32 bytes
)
- Computationally Expensive: 600,000 iterations significantly slow down brute-force attacks
- Salted: Unique salt prevents rainbow table attacks
- Standard: NIST recommended for password-based cryptography
- Deterministic: Same password + salt = same key
- Backward Compatibility: Vaults with 100k iterations continue to work
- Automatic Detection: Iteration count stored in vault metadata
- Migration Path: Manual migration required (export credentials, reinitialize vault, re-import)
- See:
docs/MIGRATION.mdfor detailed upgrade instructions
Pass-CLI supports two vault formats to balance security and recovery capabilities.
Architecture:
Master Password
↓
PBKDF2 (600k iterations)
↓
Encryption Key (32 bytes)
↓
AES-256-GCM Encrypt Vault Data
↓
Encrypted Vault File
Characteristics:
- Single password-derived encryption key
- Direct vault data encryption with derived key
- No recovery phrase support (recovery phrase data ignored if present)
- Supported for backward compatibility with existing vaults
Limitations:
- If master password is forgotten, vault is unrecoverable
- Recovery phrase feature does not work properly in V1
Architecture:
Master Password Recovery Phrase
↓ ↓
PBKDF2 (600k iterations) BIP39 Seed → Argon2id
↓ ↓
Password KEK (32 bytes) Recovery KEK (32 bytes)
↓ ↓
┌─────────────────────────────────┐
│ Key Wrapping Step │
├─────────────────────────────────┤
│ Generate DEK (32 bytes) │
│ Wrap with Password KEK │
│ Wrap with Recovery KEK │
└─────────────────────────────────┘
↓ ↓
Password Wrapped Recovery Wrapped
DEK (48 bytes) DEK (48 bytes)
↓ ↓
Stored in Vault Metadata
│
↓
AES-256-GCM Encrypt
Vault Data with DEK
↓
Encrypted Vault File
Characteristics:
- Two independent KEKs (Key Encryption Keys) from different sources
- Both KEKs wrap the same DEK (Data Encryption Key)
- Either password or recovery phrase can unlock the vault
- Recovery phrase support with optional 25th-word passphrase
- Atomic migration from V1 to V2 with rollback capability
Advantages:
- Vault can be unlocked with either password or recovery phrase
- Provides redundancy for vault access
- Fixes V1 recovery phrase bug (V1 didn't actually implement proper key wrapping)
The V2 vault format uses a three-layer key hierarchy with AES-256-GCM key wrapping.
Layer 1: Key Encryption Keys (KEKs)
Two independent KEKs are derived from different sources:
Password KEK:
- Source: User's master password
- Derivation: PBKDF2-SHA256 with 600,000 iterations
- Salt: 32-byte random salt (unique per vault)
- Output: 32-byte key for AES-256-GCM
Recovery KEK:
- Source: 24-word BIP39 mnemonic + optional passphrase
- Derivation: BIP39 seed → Argon2id (1 pass, 64MB, 4 threads)
- Salt: 32-byte random recovery salt
- Output: 32-byte key for AES-256-GCM
Layer 2: Data Encryption Key (DEK)
A single DEK encrypts all vault data:
- Generated: 256-bit random key via
crypto/rand - Wrapped twice: Once with Password KEK, once with Recovery KEK
- Storage: Both wrapped versions stored in vault metadata
- Never stored in plaintext on disk
- Cleared from memory after vault operations
Layer 3: Vault Data
Actual credentials encrypted with DEK:
- Encryption: AES-256-GCM with unique nonce per operation
- Format: JSON containing all credentials with metadata
Wrapping a DEK with a KEK (AES-256-GCM):
Input: DEK (32 bytes) + KEK (32 bytes)
↓
Generate nonce (12 bytes random)
↓
AES-256-GCM.Seal(plaintext=DEK, key=KEK, nonce=nonce)
↓
Output: Ciphertext (48 bytes: 32-byte DEK + 16-byte auth tag)
Nonce (12 bytes)
This is implemented in internal/crypto/keywrap.go - see WrapKey() and UnwrapKey() functions.
Via Master Password:
1. User enters master password
2. Derive Password KEK with PBKDF2 (stored salt from metadata)
3. Unwrap DEK with Password KEK
- Extract: Ciphertext (48 bytes) + Nonce (12 bytes) from metadata
- AES-256-GCM.Open(ciphertext, key=Password KEK, nonce)
- Result: DEK (32 bytes)
4. Decrypt vault data with DEK
5. Clear Password KEK and DEK from memory
Via Recovery Phrase:
1. User provides 24-word BIP39 mnemonic + optional passphrase
2. Derive Recovery KEK with Argon2id (recovery salt from metadata)
3. Unwrap DEK with Recovery KEK
- Extract: Ciphertext (48 bytes) + Nonce (12 bytes) from metadata
- AES-256-GCM.Open(ciphertext, key=Recovery KEK, nonce)
- Result: DEK (32 bytes)
4. Decrypt vault data with DEK
5. Vault is unlocked (no master password set until user changes password)
6. Clear Recovery KEK and DEK from memory
See internal/vault/vault.go - RecoverWithMnemonic() function for implementation.
When upgrading a V1 vault to V2:
1. Load V1 vault (decrypt with password-derived key)
2. Generate new DEK and recovery phrase
3. Derive both Password KEK and Recovery KEK
4. Wrap DEK with both KEKs
5. Re-encrypt vault data with DEK (instead of password key)
6. Update vault metadata with version=2, wrapped DEKs, recovery metadata
7. Atomic write with verification and rollback capability
See internal/storage/storage.go - MigrateToV2() function and internal/vault/vault.go - MigrateToV2() method.
Advantages of Dual-KEK Design:
- Redundancy: Vault access not dependent on single secret
- Flexibility: User can recover with passphrase if password forgotten
- Separation of Concerns: Password security and recovery phrase stored separately
- Future-Proof: Additional KEKs can be added without changing vault format
Security Guarantees:
- Both KEKs must be independently derived for each wrapping
- Each wrapping uses unique random nonce (prevents replay)
- Authentication tag (GCM) detects tampering with wrapped keys
- Master password + recovery phrase = complete vault recovery capability
- Loss of both = vault unrecoverable (no backdoor exists)
The encryption process differs between V1 and V2 vault formats.
-
Generate Salt (first time only)
salt = crypto/rand.Read(32 bytes) -
Derive Encryption Key
key = PBKDF2(master_password, salt, 600000, SHA256, 32) -
Generate Nonce
nonce = crypto/rand.Read(12 bytes) // Per-encryption unique -
Encrypt Data
ciphertext = AES-256-GCM.Encrypt( plaintext = JSON(credentials), key = derived_key, nonce = nonce, additional_data = nil ) -
Store in Vault Metadata
metadata = { version: 1, salt: salt, iterations: 600000 } vault_file = JSON(metadata) || ciphertext || auth_tag
-
Generate DEK and Wrap Keys (during initialization only)
dek = crypto/rand.Read(32 bytes) // Data Encryption Key // Wrap with Password KEK password_nonce = crypto/rand.Read(12 bytes) wrapped_with_password = AES-256-GCM.Seal( plaintext = dek, key = Password KEK, nonce = password_nonce ) // Wrap with Recovery KEK recovery_nonce = crypto/rand.Read(12 bytes) wrapped_with_recovery = AES-256-GCM.Seal( plaintext = dek, key = Recovery KEK, nonce = recovery_nonce ) -
Store Wrapped Keys in Metadata
metadata = { version: 2, salt: password_salt, iterations: 600000, wrapped_dek: wrapped_with_password, wrapped_dek_nonce: password_nonce } recovery_metadata = { encrypted_recovery_key: wrapped_with_recovery, nonce_recovery: recovery_nonce, salt_recovery: recovery_salt, ... } -
Generate Nonce and Encrypt Vault Data
vault_nonce = crypto/rand.Read(12 bytes) // Per-save unique ciphertext = AES-256-GCM.Encrypt( plaintext = JSON(credentials), key = dek, nonce = vault_nonce, additional_data = nil ) -
Store in Vault File
vault_file = JSON(metadata + ciphertext)
V1 (Direct Password):
- Load Master Password from system keychain
- Read Vault File and extract metadata (salt, iterations)
- Derive Key using PBKDF2 with stored salt and iterations
- Decrypt and Verify
plaintext = AES-256-GCM.Decrypt( ciphertext, key, nonce ) - Parse JSON to access credentials
V2 (DEK via Password KEK):
- Load Master Password from system keychain
- Read Vault File and extract metadata (salt, iterations, wrapped_dek, wrapped_dek_nonce)
- Derive Password KEK using PBKDF2 with stored salt and iterations
- Unwrap DEK using Password KEK
dek = AES-256-GCM.Open( ciphertext = wrapped_dek, key = Password KEK, nonce = wrapped_dek_nonce ) - Decrypt Vault Data using DEK
plaintext = AES-256-GCM.Decrypt( ciphertext = vault_data, key = dek, nonce = vault_nonce ) - Parse JSON to access credentials
V2 (DEK via Recovery KEK):
See Unlock Paths section for the recovery phrase decryption flow.
All random values use crypto/rand, which provides cryptographically secure random numbers from the operating system:
- Windows:
CryptGenRandom - macOS/Linux:
/dev/urandom
Used for:
- Salt generation
- Nonce generation
- Password generation
Pass-CLI integrates with your operating system's secure credential storage to save your master password.
- Location: Windows Credential Manager
- Storage: Encrypted by Windows using DPAPI
- Access: Protected by user's Windows login
- Implementation:
github.com/zalando/go-keyring
Viewing in Windows:
- Open Control Panel
- User Accounts → Credential Manager
- Windows Credentials
- Look for "pass-cli" entry
- Location: macOS Keychain (login keychain)
- Storage: Encrypted by macOS keychain services
- Access: Protected by user's macOS login password
- Implementation:
github.com/zalando/go-keyring
Viewing on macOS:
- Open Keychain Access app
- Search for "pass-cli"
- Double-click to view (requires password)
- Backend: GNOME Keyring, KWallet, or compatible
- Protocol: freedesktop.org Secret Service API
- Storage: Encrypted by keyring daemon
- Access: Protected by keyring password
- Implementation:
github.com/zalando/go-keyring
Viewing on Linux (GNOME):
- Open Seahorse (Passwords and Keys)
- Login keyring
- Search for "pass-cli"
Password policy enforced for both vault and credential passwords:
- Minimum Length: 12 characters (enforced)
- Uppercase Letter: At least one required
- Lowercase Letter: At least one required
- Digit: At least one required
- Special Symbol: At least one required (!@#$%^&*()-_=+[]{}|;:,.<>?)
- Recommended Length: 20+ characters for master password
- Strength Indicator: Real-time feedback in TUI mode
What Pass-CLI Does:
- [OK] Stores master password in system keychain
- [OK] Clears password from memory after use
- [OK] Never writes password to disk in plaintext
- [OK] Never logs password
What You Should Do:
- [OK] Use a unique master password (not reused elsewhere)
- [OK] Make it strong (20+ characters or passphrase)
- [OK] Store backup securely (password manager, safe place)
- [OK] Save your BIP39 recovery phrase offline (paper, safe)
- [ERROR] Don't share your master password
- [ERROR] Don't write it in plaintext files
Pass-CLI supports optional BIP39 recovery phrases to recover vault access if you forget your master password. This feature uses the industry-standard BIP39 mnemonic specification (same as hardware wallets).
Note: Recovery phrases only work with V2 vaults. V1 vaults do not support recovery phrase functionality. See Key Wrapping Architecture (V2) for technical details on how recovery phrases are implemented with dual-KEK wrapping.
During Initialization (V2 Vault):
- Generate 24-word BIP39 mnemonic phrase (256 bits of entropy)
- Generate DEK (Data Encryption Key) and wrap it with both Password KEK and Recovery KEK
- Recovery KEK derived from mnemonic using Argon2id + recovery salt
- Store both wrapped DEK versions in vault metadata
- Return mnemonic for user to write down securely
During Recovery (V2 Vault):
- User provides complete 24-word BIP39 mnemonic + optional passphrase
- System derives Recovery KEK from mnemonic using stored recovery salt
- Unwrap DEK using Recovery KEK (AES-256-GCM decryption)
- Decrypt vault data with DEK
- Vault is unlocked without master password (user can set new password)
- Challenge-Response: 6 random words = 2^66 possible combinations (~73.8 quintillion)
- Offline Storage: Recovery phrase should be written on paper, not stored digitally
- Optional Feature: Can be skipped during initialization with
--no-recoveryflag - Passphrase Protection: Optional 25th word for additional security
- No Backdoor: Recovery phrase is user-generated and user-stored only
# Initialize vault with recovery phrase (default)
pass-cli init
# Initialize vault without recovery phrase
pass-cli init --no-recovery
# Recover access if password forgotten
pass-cli change-password --recoverSecure Storage (Recommended):
- [OK] Write on paper and store in physical safe
- [OK] Safety deposit box
- [OK] Fireproof/waterproof document safe
- [OK] Split across multiple secure locations (advanced)
Insecure Storage (Avoid):
- [ERROR] Digital notes apps
- [ERROR] Cloud storage (Dropbox, Google Drive)
- [ERROR] Email or messaging apps
- [ERROR] Screenshots or photos
- [ERROR] Password managers (defeats the purpose)
Important: Anyone with your 24-word phrase can access your vault. Protect it as carefully as your master password.
For detailed recovery procedures, see Recovery Phrase Guide.
- Windows:
%USERPROFILE%\.pass-cli\vault.enc - macOS/Linux:
~/.pass-cli/vault.enc
Vault files are created with restricted permissions:
- Unix (macOS/Linux):
0600(owner read/write only) - Windows: ACL restricting to current user
+------------------+
| Salt (32 bytes) | ← PBKDF2 salt
+------------------+
| Nonce (12 bytes) | ← AES-GCM nonce
+------------------+
| Ciphertext | ← Encrypted credentials (variable length)
+------------------+
| Auth Tag | ← GCM authentication tag (16 bytes)
+------------------+
Vault updates use atomic write operations to prevent corruption:
- Write to temporary file (
.vault.enc.tmp) - Sync to disk (
fsync) - Rename to actual vault file (atomic operation)
- Delete temporary file on error
This ensures:
- No partial writes
- No corruption on crash
- Previous vault preserved on error
Automatic Backup Files (since atomic save implementation):
Before each vault save operation, pass-cli creates an N-1 backup:
- New vault data written to temporary file (
vault.enc.tmp.TIMESTAMP.RANDOM) - Temporary file verified (decryption test)
- Current vault renamed to
vault.enc.backup(N-1 generation) - Temporary file renamed to
vault.enc(becomes current) - Backup removed after next successful unlock (confirms new vault works)
Security Implications:
- [WARNING] Backup files contain unencrypted vault structure:
vault.enc.backupis AES-256-GCM encrypted (same as vault), but still sensitive - [OK] File permissions: Backup automatically inherits vault permissions (0600 - owner read/write only)
- [WARNING] Temporary files:
vault.enc.tmp.*files may remain if process crashes (cleaned up automatically on next save) - [OK] Automatic cleanup: Backup removed after successful unlock, minimizing exposure window
- [WARNING] Contains N-1 state: Backup has previous vault version (not current), may contain deleted credentials
Manual Backup Recommendations:
# Create timestamped backups (recommended)
cp ~/.pass-cli/vault.enc ~/backups/vault-$(date +%Y%m%d).enc
# Set correct permissions on manual backups
chmod 600 ~/backups/vault-*.enc
# Store backups on encrypted drive or secure location
# Do NOT store in cloud storage without additional encryptionWhat Files May Exist:
vault.enc- Current encrypted vault (always present when unlocked)vault.enc.backup- Previous vault state (present between saves, removed after unlock)vault.enc.tmp.YYYYMMDD-HHMMSS.XXXXXX- Orphaned temp files from crashes (auto-cleaned)
Tamper-evident audit trail for vault operations:
- Enabled by Default: Automatically initialized during
pass-cli init(use--no-auditto disable) - HMAC Signatures: HMAC-SHA256 signatures for tamper detection
- Key Storage: Audit HMAC keys stored in OS keychain (separate from vault)
- Events Logged: Vault unlock/lock, password changes, credential operations
- Privacy: Service names logged, passwords NEVER logged
- Rotation: Automatic log rotation at 10MB, 7-day retention
- Verification:
pass-cli verify-auditcommand to check log integrity - Graceful Degradation: Operations continue even if audit logging fails
Audit Log Location:
- Default: Same directory as vault (e.g.,
~/.pass-cli/audit.log) - Custom: Set
PASS_AUDIT_LOGenvironment variable
Audit Logging Commands:
# Initialize vault (audit logging enabled by default)
pass-cli init
# Initialize vault without audit logging
pass-cli init --no-audit
# Verify audit log integrity
pass-cli verify-auditAudit Log Entry Example:
{
"timestamp": "2025-01-13T10:30:45.123Z",
"event_type": "credential_access",
"outcome": "success",
"credential_name": "github.com",
"hmac_signature": "a1b2c3..."
}[OK] Offline Attacks
- Vault file encryption protects against offline brute-force
- PBKDF2 slows down password cracking (600,000 iterations)
- No plaintext credentials stored anywhere
[OK] File System Compromise
- Encrypted vault remains secure even if file is stolen
- File permissions prevent unauthorized local access
[OK] Process Memory Dumps
- Sensitive data cleared from memory after use
- Master password not kept in memory permanently
[OK] Accidental Disclosure
- No cloud storage = no cloud breach risk
- No network calls = no network interception
[OK] Unauthorized Local Access
- System keychain protects master password
- File permissions restrict vault access
[ERROR] Malware on Your Machine
- Keyloggers can capture master password when entered
- Memory scrapers can extract decrypted credentials
- Root/admin access bypasses file permissions
[ERROR] Physical Access Attacks
- Attacker with physical access can copy vault file
- Vault encryption is only protection (strong password essential)
[ERROR] Side-Channel Attacks
- Timing attacks, power analysis not mitigated
- Not designed for hostile multi-user systems
[ERROR] Weak Master Passwords
- PBKDF2 slows attacks but doesn't prevent them
- Short/common passwords can be brute-forced
[ERROR] Social Engineering
- Cannot protect against phishing for master password
- User education essential
[ERROR] TUI Display Security (Interactive Mode)
- Shoulder surfing: Credentials visible on screen in TUI mode
- Screen recording: TUI displays service names and details
- Password visibility toggle:
Ctrl+Pshows plaintext passwords - Shared terminals: Other users may see credential list
- Confidentiality: Credentials encrypted with AES-256-GCM
- Integrity: Authentication tag prevents tampering
- Forward Secrecy: Unique nonce per encryption
- Secure Defaults: No insecure configuration options
- Availability: Forgot password without recovery phrase = lost vault
- Zero-Knowledge: Master password accessible via keychain
- Perfect Security: Subject to implementation bugs
-
Master Password Recovery: Optional BIP39 recovery phrase
- If recovery phrase was enabled during init, you can recover access with
pass-cli change-password --recover - If recovery phrase was skipped (
--no-recovery), vault is unrecoverable without the master password - If you lose both master password AND recovery phrase, vault is unrecoverable
- No backdoor or master key exists
- If recovery phrase was enabled during init, you can recover access with
-
Keychain Dependency
- Master password security depends on OS keychain
- Compromise of OS account = compromise of master password
-
Single-User Design
- Not designed for multi-user systems
- File permissions rely on OS access controls
-
No Network Security
- Offline-only design
- No secure sharing mechanism
-
Memory Security
- Go garbage collector may leave memory traces
- Sensitive data cleared but not guaranteed wiped
- [FAIL] Cloud synchronization
- [FAIL] Multi-user support
- [FAIL] Hardware security module (HSM) integration
- [FAIL] Biometric authentication
- [FAIL] Two-factor authentication for master password