FrostVault
A zero-knowledge Windows password manager with deception built in.
The Problem
Mainstream password managers are cloud-first and ask you to trust a third party with your most sensitive secrets. FrostVault keeps everything local and encrypted — and fights back when someone tries to break in.
My Role
Built solo with AI-assisted development — cryptographic design, application logic, the PySide6 UI, and Windows packaging.
Highlights
- AES-256-GCM authenticated encryption for all vault data at rest
- Honey Vault serves believable decoy credentials under a wrong master key or duress
- Freeze Mode and Ice Crystal Fingerprints provide tamper detection
- Ships as a single standalone Windows executable — no installer, no cloud
Stack
Constraints
- Offline-first: vault data must never leave the user's machine.
- Single-binary distribution — no installer, no runtime to ship.
- Threat model includes coerced access, not just remote attackers.
System Architecture
Key Trade-offs
The decisions worth defending — what I chose, what I turned down, and why.
Storage architecture
Chose
Single encrypted local file
Rejected
Cloud-synced vault
Removes the third-party trust dependency entirely; matches the offline-first threat model at the cost of cross-device sync.
Wrong-password behaviour
Chose
Honey Vault returns plausible decoys
Rejected
Hard fail with a clear error
A loud failure tells an attacker they have the wrong key. A believable decoy buys time and frustrates duress scenarios.
Crypto stack
Chose
AES-256-GCM via the `cryptography` library
Rejected
Custom-built primitives
Authenticated encryption out of the box, audited implementation, zero novel crypto risk.
What I'd Do Differently
An honest retrospective — the stuff I'd change with more time, more users, or a second pass.
- 1Add an automated integrity self-test that runs on every launch and surfaces tampering visibly to the user.
- 2Move key derivation parameters to a versioned header so future Argon2 tuning doesn't break old vaults.
- 3Ship a portable Linux build — Windows-only narrows the audience more than I expected.
Technical Deep-Dive
Architecture, specifications, and implementation details.
Security Architecture
FrostVault v1.0.0 — IceLegends
#1. Security Principles
FrostVault is designed around three core security principles:
-
Zero-knowledge at rest — The master password is never stored. Only its Argon2id hash is persisted. If the database is exfiltrated, no credential can be decrypted without the master password.
-
Minimum plaintext surface — Decrypted data exists only in Python process memory, only while the vault is unlocked. Every page switch, idle timeout, or explicit lock wipes the derived key.
-
Defense in depth — Multiple independent layers (Argon2id KDF → AES-256-GCM per entry → SQLite encrypted blobs → Honey Vault decoy → Freeze Mode camouflage) mean that no single compromised layer reveals vault contents.
#2. Attack Surface Analysis
┌─────────────────────────────────────────────────────────┐
│ EXTERNAL ATTACK SURFACE │
│ │
│ 1. Master password input (login screen) │
│ 2. vault.db file on disk │
│ 3. Process memory while unlocked │
│ 4. Clipboard (after copy-password action) │
│ 5. Screen (shoulder surfing / screen capture) │
└─────────────────────────────────────────────────────────┘
##2.1 Master Password Brute Force
- Mitigation: Argon2id with memory=65536 KB, iterations=3, parallelism=1
- Cost per guess on modern hardware: ~200–500 ms
- 1 billion guesses/day rate → ~50 years for a 10-char random password
##2.2 Database File Theft
- Mitigation: Each credential field is individually AES-256-GCM encrypted
- Without the master password → derived key is unrecoverable
- SQLite file contains only ciphertext + nonces + Argon2 parameters
##2.3 Memory Scraping (RAM dump)
- Mitigation: Derived key is stored as a Python
bytesobject; wiped on lock viavault.lock() - Residual risk: Python garbage collector timing; swap file may contain stale plaintext
##2.4 Clipboard Hijacking
- Mitigation:
clipboard.pyauto-clears after 30 seconds usingQTimer - Clipboard is set via
pyperclipand overwritten with empty string on expiry
##2.5 Shoulder Surfing / Screen Capture
- Mitigation: Freeze Mode (Ctrl+Shift+F) instantly replaces the window with a fake calculator
- Window title changes to "Calculator" to defeat process-list enumeration
##2.6 Honey Vault (Intruder Misdirection)
- After 3 failed master password attempts, attacker is shown a convincing decoy vault
- Real vault silently remains locked; intrusion is logged to DB with timestamp
- Attacker cannot distinguish decoy from real vault without the master password
#3. Security Control Matrix
| Control | Type | Implementation | Standard |
|---|---|---|---|
| Password hashing | Preventive | Argon2id | OWASP PHC |
| Key derivation | Preventive | PBKDF2-HMAC-SHA256 (vault key) | NIST SP 800-132 |
| Data encryption | Preventive | AES-256-GCM per field | FIPS 197 |
| Authenticated encryption | Detective | GCM authentication tag (128-bit) | NIST SP 800-38D |
| Clipboard auto-clear | Preventive | 30-second QTimer wipe | — |
| Idle auto-lock | Preventive | 5-minute inactivity timeout | CIS Benchmark |
| Decoy vault | Deceptive | Honey Vault with fake credentials | Canary trap |
| Screen camouflage | Preventive | Freeze Mode calculator overlay | — |
| Intrusion logging | Detective | SQLite intrusion_log table | — |
| No network access | Preventive | No socket/http calls in codebase | — |
#4. Cryptographic Trust Chain
Master Password (user input — never stored)
│
│ Argon2id
│ memory=65536 KB · iterations=3 · parallelism=1
│ salt=32 random bytes (stored in DB: master table)
▼
Argon2id Hash (stored in DB for verification)
│
│ PBKDF2-HMAC-SHA256
│ salt=32 random bytes (stored in DB: master table)
│ iterations=100,000
▼
256-bit Derived Key (AES key — held in memory only)
│
│ AES-256-GCM
│ nonce=12 random bytes (prepended to each ciphertext)
▼
Ciphertext blobs (stored in DB: entries table)
#5. Key Lifecycle
State: LOCKED
vault._key = None
All DB reads return encrypted blobs (unusable)
Event: vault.unlock(master_password)
1. Fetch Argon2 hash from DB
2. argon2.verify(hash, master_password) → True/False
3. If True: derive key via PBKDF2
4. vault._key = derived_key ← key enters memory
State: UNLOCKED
Event: vault.lock()
1. vault._key = None ← key wiped from memory
2. Python GC may not immediately free the bytes object
State: LOCKED
Event: auto-lock (idle timeout)
Same as vault.lock()
Event: Freeze Mode activated
Window hidden; vault remains unlocked in memory
Key is NOT wiped during Freeze Mode (by design — fast restore)
#6. Honey Vault Design
Failed attempts counter stored in: SQLite honey_entries / intrusion_log
MAX_ATTEMPTS = 3
Attempt 1, 2: Normal "Wrong password" message
Attempt 3: honey.should_show_honey() → True
LoginScreen shows "Intruder detected! Loading decoy vault…"
After 1.2s delay → VaultDashboard.activate(is_honey=True)
Honey mode:
- Displays pre-generated fake credentials
- Real vault remains locked
- All edit/delete actions are silently no-ops
- Intrusion timestamp + count written to intrusion_log
- Reset only on successful master password entry
#7. Freeze Mode Design
Trigger: Ctrl+Shift+F (global hotkey via keyboard library)
Activation:
1. MainWindow.setWindowTitle("Calculator")
2. CalculatorOverlay widget shown over entire window
3. Real vault content hidden (not destroyed)
4. Vault key remains in memory
Deactivation:
1. User types PIN digits on calculator overlay
2. PIN verified: "1234" followed by "="
3. Overlay hidden; real vault restored
4. Window title restored to "FrostVault — by IceLegends"
Note: Freeze Mode does NOT lock the vault — it is a visual
camouflage layer, not a security boundary.
#8. Residual Risks
| Risk | Severity | Likelihood | Notes |
|---|---|---|---|
| Memory forensics on unlocked session | High | Low | Python bytes not securely wiped |
| Swap file contains decrypted data | Medium | Low | Windows page file mitigation: none |
| Keylogger captures master password | Critical | Medium | Out of scope for this version |
| Attacker knows to press "=" after "1234" | Medium | Low | Freeze Mode PIN is hardcoded |
| SQLite WAL file contains plaintext | Low | Very Low | WAL only contains ciphertext |
| Side-channel timing on Argon2 verify | Low | Very Low | argon2-cffi uses constant-time compare |