Skip to content
Return to Projects
Case Study

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

Python 3.12PySide6AES-256-GCMCryptographyPyInstaller

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

Interface
PySide6 Desktop UI
Application
Vault Manager
Honey Vault
Freeze Mode
Cryptography
AES-256-GCM
Key Derivation
Ice Crystal Fingerprints
Storage
Encrypted local vault file

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.

  1. 1Add an automated integrity self-test that runs on every launch and surfaces tampering visibly to the user.
  2. 2Move key derivation parameters to a versioned header so future Argon2 tuning doesn't break old vaults.
  3. 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

FileResponsibility
core/honey_vault.pyFail counter · honey entry generator · intrusion logger
ui/login_screen.pyShows "Intruder detected!" message and triggers honey mode
ui/vault_dashboard.pyRenders 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

FileResponsibility
features/freeze_mode.pyHotkey listener · CalculatorOverlay widget · PIN unlock
main.pyCalls 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

FileResponsibility
features/ice_crystal.pyIceCrystalWidget — 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

PropertyValue
Symmetry6-fold (hexagonal, like real snowflakes)
Arm variationLength, branch count, branch angle — all seed-derived
Color rangeIce blue (HSV 185–225°) with seed-derived hue shift
AnimationContinuous slow rotation (0.3°/frame at 25 fps)
Empty stateStatic outline snowflake, muted color
Size90×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

ControlRange / OptionsDefault
Length slider8 – 64 characters20
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.

~ End of Document ~