Cryptography Details¶
Deep dive into the cryptographic primitives and implementation in vaultix.
Overview¶
Vaultix uses standard, well-audited cryptographic algorithms from Go's crypto library. No custom cryptography is implemented.
Algorithm Selection¶
Encryption: AES-256-GCM¶
Why AES-256?
- ✅ Industry standard (NIST FIPS 197)
- ✅ Hardware acceleration on modern CPUs (AES-NI)
- ✅ Proven security (no practical attacks)
- ✅ 256-bit keys provide excellent security margin
Why GCM mode?
- ✅ Authenticated encryption (AEAD)
- ✅ Provides both confidentiality and integrity
- ✅ Detects tampering automatically
- ✅ Fast (hardware accelerated)
- ✅ Well-studied and standardized
Alternatives considered and rejected:
- ❌ AES-CBC: No authentication, vulnerable to padding oracle attacks
- ❌ AES-CTR: No authentication by itself
- ❌ ChaCha20-Poly1305: Good algorithm but less hardware support
Key Derivation: Argon2id¶
Why Argon2id?
- ✅ Winner of Password Hashing Competition (2015)
- ✅ Resistant to GPU/ASIC attacks (memory-hard)
- ✅ Configurable time/memory cost
- ✅ Combined data-dependent and data-independent mixing
- ✅ Side-channel resistant
Parameters used:
const (
ArgonTime = 3 // Number of iterations
ArgonMemory = 64*1024 // 64 MB memory
ArgonParallelism = 4 // 4 threads
ArgonKeyLength = 32 // 32 bytes (256 bits)
)
Why these parameters?
- Time=3: ~500ms on modern CPU (usable but not instant)
- Memory=64MB: Expensive for attackers, reasonable for users
- Parallelism=4: Good for multi-core CPUs
- Key=32 bytes: AES-256 requirement
Alternatives considered and rejected:
- ❌ PBKDF2: Too fast, weak against GPU attacks
- ❌ bcrypt: Password length limit (72 bytes), not memory-hard
- ❌ scrypt: Good but Argon2 is newer and better
Random Number Generation: crypto/rand¶
Why crypto/rand?
- ✅ Cryptographically secure (CSPRNG)
- ✅ Uses OS entropy source (/dev/urandom, CryptGenRandom, etc.)
- ✅ Non-deterministic
- ✅ Tested and audited
Used for:
- Salt generation
- Nonce generation
- File ID generation
Never use:
- ❌ math/rand: Deterministic, not cryptographically secure
- ❌ Time-based seeds: Predictable
Detailed Cryptographic Operations¶
Key Derivation Process¶
// 1. Generate random 32-byte salt (only once during init)
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return err
}
// 2. Derive key from password + salt using Argon2id
key := argon2.IDKey(
[]byte(password), // User's password
salt, // Random salt
3, // Time cost (iterations)
64*1024, // Memory cost (64 MB)
4, // Parallelism (threads)
32, // Key length (256 bits)
)
// 3. Key is now ready for AES-256
Why salt?
- Prevents rainbow table attacks
- Makes each vault's derived key unique
- Stored unencrypted (not secret data)
Salt properties:
- 32 bytes (256 bits)
- Randomly generated
- Unique per vault
- Stored in
.vaultix/salt
Encryption Process¶
// 1. Create AES cipher with derived key
block, err := aes.NewCipher(key) // key must be 32 bytes
// 2. Create GCM mode wrapper
gcm, err := cipher.NewGCM(block)
// 3. Generate random nonce (12 bytes for GCM)
nonce := make([]byte, gcm.NonceSize()) // 12 bytes
rand.Read(nonce)
// 4. Encrypt plaintext
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
// Result: [nonce || encrypted_data || auth_tag]
// 5. Write ciphertext to disk
Output format:
Why include nonce in output?
- Nonce must be unique for each encryption
- Needed for decryption
- Not secret (but must not be reused!)
Decryption Process¶
// 1. Read ciphertext from disk
ciphertext, err := os.ReadFile(path)
// 2. Create AES cipher with derived key
block, err := aes.NewCipher(key)
// 3. Create GCM mode wrapper
gcm, err := cipher.NewGCM(block)
// 4. Split nonce from ciphertext
nonceSize := gcm.NonceSize() // 12
nonce := ciphertext[:nonceSize]
encrypted := ciphertext[nonceSize:]
// 5. Decrypt and verify
plaintext, err := gcm.Open(nil, nonce, encrypted, nil)
if err != nil {
// Wrong password OR corrupted data OR tampered data
return ErrDecryptionFailed
}
// 6. Return plaintext
GCM authentication:
- Verifies data hasn't been modified
- Detects bitflips, truncation, etc.
- Fails decryption if auth tag doesn't match
Metadata Encryption¶
// 1. Marshal metadata to JSON
jsonData, err := json.Marshal(metadata)
// 2. Encrypt JSON using same key
encryptedMeta := gcm.Seal(nonce, nonce, jsonData, nil)
// 3. Write to .vaultix/meta
Why encrypt metadata?
- Protects filenames (could be sensitive)
- Protects file sizes
- Protects modification times
- Verifies password correctness
Security Properties¶
Confidentiality¶
What's protected:
- ✅ File contents
- ✅ Filenames
- ✅ File sizes
- ✅ Modification times
- ✅ Number of files (hidden in metadata)
What's NOT protected:
- ❌ Vault existence (
.vaultix/is visible) - ❌ Approximate vault size
- ❌ When vault was last modified
- ❌ Number of encrypted file objects
Integrity¶
GCM provides:
- ✅ Authentication of ciphertext
- ✅ Detection of any modification
- ✅ Detection of truncation
- ✅ Prevention of ciphertext manipulation
Example:
// If attacker modifies even one bit:
ciphertext[100] ^= 0x01
// Decryption will fail:
plaintext, err := gcm.Open(...)
// err != nil (authentication failed)
Forward Secrecy¶
❌ Not provided.
If password is compromised:
- All past encrypted data can be decrypted
- All future encrypted data can be decrypted
Mitigation:
- Use strong passwords
- Change password if compromise suspected
- Consider separate vaults for time-sensitive data
Attack Resistance¶
Brute Force Attacks¶
Scenario: Attacker has vault and tries passwords.
Protection: Argon2id makes each attempt expensive
- Time: ~500ms per attempt
- Memory: 64 MB per attempt
- Parallelization limited by memory bandwidth
Comparison:
PBKDF2 (old standard):
- 1 billion attempts/second on GPU
- Weak 8-char password cracked: instantly
Argon2id (vaultix):
- ~2000 attempts/second on high-end GPU
- Weak 8-char password cracked: hours to days
- Strong 16-char password: infeasible
Recommendation: Use 16+ character passwords
Dictionary Attacks¶
Scenario: Attacker tries common passwords.
Protection: Argon2id slows down attempts
Mitigation:
- Don't use dictionary words
- Use password manager generated passwords
- Use passphrases: "correct horse battery staple"
Rainbow Table Attacks¶
Scenario: Attacker pre-computes password hashes.
Protection: Random salt makes pre-computation useless
- Each vault has unique salt
- Must compute from scratch for each vault
Side-Channel Attacks¶
Timing attacks:
- ❌ Partially vulnerable: String comparison for filenames
- ✅ GCM is constant-time authenticated decryption
- ✅ Argon2id is side-channel resistant
Power analysis:
- ❓ Not applicable (requires physical access to hardware)
Cache-timing:
- ✅ AES-NI resistant
- ✅ Argon2id resistant (memory-hard)
Known-Plaintext Attacks¶
Scenario: Attacker knows some plaintext/ciphertext pairs.
Protection: AES-256-GCM is resistant
- Knowing plaintext doesn't help recover key
- Each file has unique nonce (no pattern reuse)
Chosen-Ciphertext Attacks¶
Scenario: Attacker can get files decrypted.
Protection: GCM authentication prevents manipulation
- Can't create valid ciphertexts without key
- Tampering detected before plaintext revealed
Quantum Computing¶
Current status:
- AES-256: ~128-bit post-quantum security (Grover's algorithm)
- Still very strong (2^128 operations infeasible)
Future-proofing:
- May need post-quantum key derivation eventually
- AES-256 should remain secure for decades
Implementation Details¶
Memory Handling¶
// Clear sensitive data after use
defer func() {
// Overwrite password
for i := range password {
password[i] = 0
}
// Overwrite key
for i := range key {
key[i] = 0
}
}()
Why?
- Prevents recovery from memory dumps
- Reduces window for memory-reading malware
- Defense in depth
Limitations:
- Go garbage collector may have copies
- Swapped memory may persist
- Not perfect but better than nothing
Nonce Management¶
Critical: Never reuse nonces!
With GCM, reusing nonce with same key:
- ❌ Breaks confidentiality
- ❌ Breaks authentication
- ❌ Allows key recovery
How we avoid this:
- Random nonce for each file
- 12 bytes = 96 bits
- Collision probability: negligible
Error Handling¶
// Don't leak information in errors
if err := gcm.Open(...); err != nil {
// Good: Generic error
return errors.New("decryption failed")
// Bad: Leaks information
// return fmt.Errorf("wrong password for %s", filename)
}
Why?
- Prevents information leakage
- Doesn't reveal vault contents
- Constant-time-ish failure
Comparison with Alternatives¶
vs. GPG¶
| Feature | Vaultix | GPG |
|---|---|---|
| Encryption | AES-256-GCM | AES, 3DES, etc. |
| Key derivation | Argon2id | S2K (weaker) |
| Public key | No | Yes |
| Ease of use | High | Low |
| File-level | Yes | Yes |
Use vaultix: Simple password-based encryption Use GPG: Public key crypto, digital signatures
vs. VeraCrypt¶
| Feature | Vaultix | VeraCrypt |
|---|---|---|
| Encryption | AES-256-GCM | AES, Serpent, Twofish |
| Key derivation | Argon2id | PBKDF2/SHA-512 |
| Container | Directory | File container |
| Mounting | No | Yes |
| Plausible deniability | No | Yes |
Use vaultix: File-level encryption, CLI workflow Use VeraCrypt: Container-based, GUI, hidden volumes
vs. age¶
| Feature | Vaultix | age |
|---|---|---|
| Encryption | AES-256-GCM | ChaCha20-Poly1305 |
| Key derivation | Argon2id | scrypt |
| Public key | No | Yes |
| Directory | Yes | No (file-by-file) |
Use vaultix: Directory-based workflow Use age: Single-file encryption, public key crypto
Best Practices¶
Choosing Passwords¶
Good password characteristics:
- ✅ Length: 16+ characters
- ✅ Entropy: Random characters or diceware
- ✅ Uniqueness: Don't reuse
- ✅ Storage: Password manager
Examples:
Weak (DON'T USE):
- password123
- qwerty
- letmein
- Password1!
Strong (GOOD):
- Generated: 7wT$9#xK2mP@5nL&8qR
- Passphrase: correct-horse-battery-staple-purple-elephant
- Diceware: ware-klaxon-pride-ache-agony-brunt
Key Rotation¶
When to rotate:
- Password potentially compromised
- Regular schedule (e.g., yearly)
- After employee leaves (for work vaults)
How to rotate:
# 1. Extract all files
vaultix extract
# 2. Remove vault
rm -rf .vaultix
# 3. Re-initialize with new password
vaultix init
# (Enter new password)
Multiple Vaults¶
Consider separate vaults for:
- Different sensitivity levels
- Different time periods
- Different teams/projects
- Different backup schedules
Example structure:
~/vaults/
├── personal/ # Password A
├── work/ # Password B
├── financial/ # Password C (strongest)
└── archive/ # Password A (same as personal)
Threat Model¶
What Vaultix Protects Against¶
✅ Protects against:
- Stolen laptop (with strong password)
- Unauthorized physical access
- Cloud storage providers reading data
- Network sniffing (of vault files)
- Malware reading encrypted files on disk
❌ Does NOT protect against:
- Keyloggers (captures password)
- Screen recorders (sees decrypted files)
- Memory dumps while files decrypted
- Malware with root/admin access
- $5 wrench attack (coercion)
- Quantum computers (in far future)
Trust Assumptions¶
You must trust:
- Your OS and kernel
- Go's crypto library
- The computer's CPU (no hardware backdoors)
- Your password manager (if used)
- Yourself (not to forget password)
You don't need to trust:
- Cloud storage providers
- Network infrastructure
- Backup services
- Other users on same system
Cryptographic Verification¶
How to Verify Implementation¶
# 1. Check Go crypto usage
grep -r "crypto/" *.go
# Should see:
# - crypto/aes
# - crypto/cipher
# - crypto/rand
# - golang.org/x/crypto/argon2
# 2. Check for weak crypto
grep -r "math/rand" *.go # Should return nothing
# 3. Run tests
go test -v ./crypto
Audit Checklist¶
- [ ] AES-256 used (not AES-128)
- [ ] GCM mode used (authenticated encryption)
- [ ] Argon2id for key derivation (not PBKDF2/bcrypt)
- [ ] crypto/rand for all randomness
- [ ] Unique nonce per encryption
- [ ] Salt stored and used correctly
- [ ] No custom crypto implementations
- [ ] Sensitive data cleared from memory
- [ ] No password logging
Bottom line: Vaultix uses battle-tested, industry-standard cryptography. No novel algorithms, no custom implementations, just proven security.