Security Architecture
Comprehensive security architecture, cryptographic implementation, threat model, and security guarantees for Pass-CLI.
Security Overview
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.
Key Security Features
- 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
Cryptographic Implementation
Encryption Algorithm
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
Why AES-256-GCM?
- 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
Key Derivation
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
Key Derivation Process
Master Key = PBKDF2(
password = user's master password,
salt = unique 32-byte random salt,
iterations = 600,000,
hash = SHA-256,
key_length = 32 bytes
)Why PBKDF2?
- 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
Migration From 100k to 600k Iterations
- 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
Vault Format Versions
Pass-CLI supports two vault formats to balance security and recovery capabilities.
V1 Format (Legacy)
Architecture:
Master Password
↓
PBKDF2 (600k iterations)
↓
Encryption Key (32 bytes)
↓
AES-256-GCM Encrypt Vault Data
↓
Encrypted Vault FileCharacteristics:
- 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
V2 Format (Current - with DEK and Dual-KEK Wrapping)
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 FileCharacteristics:
- 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)
Key Wrapping Architecture (V2)
The V2 vault format uses a three-layer key hierarchy with AES-256-GCM key wrapping.
Three-Layer Key Hierarchy
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
Key Wrapping Process
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.
Unlock Paths
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 memoryVia 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 memorySee internal/vault/vault.go - RecoverWithMnemonic() function for implementation.
V1 to V2 Migration
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 capabilitySee internal/storage/storage.go - MigrateToV2() function and internal/vault/vault.go - MigrateToV2() method.
Security Properties
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)
Encryption Process
The encryption process differs between V1 and V2 vault formats.
V1: Direct Password-Derived Encryption
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 uniqueEncrypt 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
V2: DEK with Dual-KEK Wrapping
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)
Decrypting Credentials
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.
Random Number Generation
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
Master Password Management
System Keychain Integration
Pass-CLI integrates with your operating system’s secure credential storage to save your master password.
Windows - Credential Manager
- 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
macOS - Keychain
- 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)
Linux - Secret Service
- 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”
Master Password Requirements
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
Master Password Security
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
BIP39 Recovery Phrase
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.
How It Works
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)
Security Properties
- 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
Commands
# 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 --recoverStorage Recommendations
Secure 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.
Data Storage Security
Vault File Location
- Windows:
%USERPROFILE%\.pass-cli\vault.enc - macOS/Linux:
~/.pass-cli/vault.enc
File Permissions
Vault files are created with restricted permissions:
- Unix (macOS/Linux):
0600(owner read/write only) - Windows: ACL restricting to current user
Vault File Structure
+------------------+
| Salt (32 bytes) | ← PBKDF2 salt
+------------------+
| Nonce (12 bytes) | ← AES-GCM nonce
+------------------+
| Ciphertext | ← Encrypted credentials (variable length)
+------------------+
| Auth Tag | ← GCM authentication tag (16 bytes)
+------------------+Atomic Writes
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
Backup Strategy
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)
Audit Logging (Enabled by Default)
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..."
}Threat Model
What Pass-CLI Protects Against
[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
What Pass-CLI Does NOT Protect Against
[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
Security Guarantees
What We Guarantee
- Confidentiality: Credentials encrypted with AES-256-GCM
- Integrity: Authentication tag prevents tampering
- Forward Secrecy: Unique nonce per encryption
- Secure Defaults: No insecure configuration options
What We Cannot Guarantee
- Availability: Forgot password without recovery phrase = lost vault
- Zero-Knowledge: Master password accessible via keychain
- Perfect Security: Subject to implementation bugs
Limitations
Known Limitations
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
Out of Scope
- [FAIL] Cloud synchronization
- [FAIL] Multi-user support
- [FAIL] Hardware security module (HSM) integration
- [FAIL] Biometric authentication
- [FAIL] Two-factor authentication for master password