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.
Feature Specifications
FrostVault v1.0.0 — IceLegends
#Feature 1 — Honey Vault (Decoy Trap)
##1.1 Concept
After 3 consecutive failed master password attempts, the attacker is not shown an error — they are shown a convincing fake vault loaded with plausible dummy credentials. The real vault silently remains locked. This is a canary trap / honey pot technique used in real-world security operations.
##1.2 Components
| File | Responsibility |
|---|---|
core/honey_vault.py | Fail counter · honey entry generator · intrusion logger |
ui/login_screen.py | Shows "Intruder detected!" message and triggers honey mode |
ui/vault_dashboard.py | Renders honey entries; blocks write operations in honey mode |
##1.3 State Machine
[NORMAL MODE]
fail_count < 3
Wrong password → increment fail_count, show "X attempts remaining"
[HONEY TRIGGER]
fail_count == 3
honey.should_show_honey() → True
login_screen shows "Intruder detected! Loading decoy vault…"
After 1.2s delay: VaultDashboard.activate(is_honey=True)
[HONEY MODE]
Fake entries displayed
Edit / Delete buttons silently disabled
intrusion_log entry written (timestamp + count)
Stats label shows "· DECOY ACTIVE"
[RESET]
Successful master password entry
honey.reset_fail_count()
fail_count = 0
VaultDashboard.activate(is_honey=False)
##1.4 Honey Entry Generation
# core/honey_vault.py
FAKE_ENTRIES = [
{"title": "Gmail", "username": "john.doe@gmail.com", "password": "Summer2023!"},
{"title": "Facebook", "username": "john.doe@gmail.com", "password": "Fb#secure99"},
{"title": "Netflix", "username": "johndoe@yahoo.com", "password": "NetflixPass1"},
{"title": "Amazon", "username": "john.doe@amazon.com", "password": "Shopping@2023"},
{"title": "LinkedIn", "username": "john.doe@company.com", "password": "Work!Pass22"},
]
Entries are designed to look realistic: common services, plausible usernames, passwords that look real but follow no actual pattern.
##1.5 Intrusion Log Schema
intrusion_log: id | attempt_count | attempted_at
Accessible from VaultDashboard → Intrusion Log tab after successful unlock.
#Feature 2 — Freeze Mode (Panic Lock / Calculator Disguise)
##2.1 Concept
A global hotkey (Ctrl+Shift+F) instantly disguises the FrostVault window as a Windows Calculator. The window title changes, a fake calculator UI overlays the content, and all vault data is visually hidden. This protects against shoulder surfing, coworker glances, and physical access during active sessions.
Real-world precedent: Similar "duress mode" features are used in journalist security tools (e.g., Signal's screen security) and activist-targeted applications.
##2.2 Components
| File | Responsibility |
|---|---|
features/freeze_mode.py | Hotkey listener · CalculatorOverlay widget · PIN unlock |
main.py | Calls FreezeMode.activate() / deactivate() |
##2.3 Activation Flow
User presses Ctrl+Shift+F
│
▼
FreezeMode.toggle()
│
├─ [ACTIVATING]
│ MainWindow.setWindowTitle("Calculator")
│ CalculatorOverlay.show() (covers entire window)
│ FreezeMode._frozen = True
│
└─ [DEACTIVATING]
CalculatorOverlay.hide()
MainWindow.setWindowTitle("FrostVault — by IceLegends")
FreezeMode._frozen = False
##2.4 PIN Unlock
The calculator overlay accepts key presses. When the user types 1234=, the overlay is dismissed:
# features/freeze_mode.py
UNLOCK_PIN = "1234"
def keyPressEvent(self, event):
key = event.text()
if key.isdigit():
self._pin_buffer += key
if key == "=" and self._pin_buffer == UNLOCK_PIN:
self._pin_buffer = ""
self.hide() # triggers deactivation
##2.5 Security Boundary
Freeze Mode is a visual camouflage layer only. The vault key remains in memory during freeze. This is by design — requiring re-authentication on every freeze/unfreeze would defeat the "quick glance protection" use case.
For full security, use Lock Vault which wipes the key from memory.
##2.6 Calculator UI Layout
┌──────────────────────────────┐
│ Calculator [×] │
├──────────────────────────────┤
│ 0 │
├──────────────────────────────┤
│ MC MR MS M+ M- │
│ 7 8 9 ÷ √ │
│ 4 5 6 × % │
│ 1 2 3 − 1/x │
│ ± 0 . + = │
└──────────────────────────────┘
#Feature 3 — Ice Crystal Fingerprint
##3.1 Concept
Every password generates a unique, deterministic visual snowflake in real time. Identical passwords produce identical crystals — making password reuse visually obvious at a glance. The crystal forms live as the user types, with each character changing the pattern.
Design principle: A visual fingerprint that gives users an intuitive sense of password uniqueness without requiring them to read or compare strings.
##3.2 Components
| File | Responsibility |
|---|---|
features/ice_crystal.py | IceCrystalWidget — QPainter snowflake renderer |
##3.3 Seed Algorithm
# features/ice_crystal.py
import hashlib
def _password_to_seed(self, password: str) -> int:
digest = hashlib.sha256(password.encode('utf-8')).digest()
return int.from_bytes(digest[:8], 'big')
SHA-256 is used as a deterministic hash. The first 8 bytes are converted to an integer seed for random.Random(seed). This ensures:
- Same password → same seed → same crystal every time
- Different password → different seed → visually distinct crystal
- One character change → completely different crystal (avalanche effect)
##3.4 Rendering Algorithm
seed = SHA256(password)[0:8] → int
rng = random.Random(seed)
For each of 6 arms (0°, 60°, 120°, 180°, 240°, 300°):
arm_length = rng.uniform(0.4, 0.9) × radius
branch_count = rng.randint(2, 5)
branch_angle = rng.uniform(25, 65)°
Draw arm line from center
For each branch position along arm:
Draw symmetric side branches at ±branch_angle
Point cap at arm tip (small circle)
Colors:
Hue = (seed % 360) mapped to ice-blue range [185°, 225°]
Base = QColor.fromHsv(hue, 80, 220)
Tips = QColor.fromHsv(hue, 60, 255)
Center = QColor(255, 255, 255, 200)
Animation:
QTimer(40ms) → rotation += 0.3° per tick
Crystal slowly rotates when password is non-empty
Crystal is static (empty state) when password field is blank
##3.5 Visual Properties
| Property | Value |
|---|---|
| Symmetry | 6-fold (hexagonal, like real snowflakes) |
| Arm variation | Length, branch count, branch angle — all seed-derived |
| Color range | Ice blue (HSV 185–225°) with seed-derived hue shift |
| Animation | Continuous slow rotation (0.3°/frame at 25 fps) |
| Empty state | Static outline snowflake, muted color |
| Size | 90×90 px (entry dialog) · configurable |
#Feature 4 — Auto-Lock (Idle Timeout)
##4.1 Concept
If no user interaction is detected for 5 minutes, the vault automatically locks. This protects against unattended workstations.
##4.2 Implementation
# features/auto_lock.py
IDLE_TIMEOUT_MS = 5 * 60 * 1000 # 5 minutes
class AutoLock(QObject):
lock_triggered = Signal()
def reset(self):
self._timer.start(IDLE_TIMEOUT_MS) # restart on any activity
def _on_timeout(self):
self.lock_triggered.emit()
reset() is called on every key press and mouse click in MainWindow.eventFilter().
#Feature 5 — Password Generator
##5.1 Controls
| Control | Range / Options | Default |
|---|---|---|
| Length slider | 8 – 64 characters | 20 |
| Uppercase (A–Z) | Checkbox | ✓ |
| Lowercase (a–z) | Checkbox | ✓ |
| Digits (0–9) | Checkbox | ✓ |
| Symbols (!@#…) | Checkbox | ✓ |
##5.2 Generation Algorithm
import random
charset = build_charset(checkboxes)
password = "".join(random.SystemRandom().choice(charset) for _ in range(length))
random.SystemRandom uses os.urandom() — cryptographically secure.
##5.3 Use This → Auto-Fill
Clicking "Use This" emits password_selected(str) which opens a pre-filled EntryDialog. The password field is populated and the Ice Crystal is immediately rendered.