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.

UI Architecture

FrostVault v1.0.0 — IceLegends


#1. Widget Hierarchy

QApplication
└── MainWindow (QMainWindow)
    └── QStackedWidget
        ├── page 0: LoginScreen (QWidget)
        │   ├── LoginBackground (QWidget — custom paintEvent)
        │   └── login_card (QFrame#login_card)
        │       ├── MascotWidget          ← Flurry polar bear
        │       ├── QLabel "FrostVault"
        │       ├── QLabel "by IceLegends"
        │       ├── badge label (AES-256 · Argon2id)
        │       ├── QLineEdit (master password)
        │       ├── QPushButton (eye toggle)
        │       ├── QLineEdit (confirm — setup mode only)
        │       ├── QLabel (status)
        │       └── QPushButton (UNLOCK / CREATE VAULT)
        │
        └── page 1: VaultDashboard (QWidget)
            ├── sidebar (QFrame)
            │   ├── logo_area (QWidget)
            │   ├── NavButton × 3
            │   ├── MascotWidget          ← Flurry in dashboard
            │   └── lock_btn (QPushButton)
            └── main (QWidget)
                ├── topbar (QWidget)
                │   ├── QLineEdit (search)
                │   ├── QComboBox (category filter)
                │   ├── QLabel (stats)
                │   └── QPushButton "+ New Entry"
                └── content (stacked panels)
                    ├── QScrollArea → cards_container
                    │   └── EntryCard × n (QFrame#card)
                    │       ├── icon badge (QLabel)
                    │       ├── title (QLabel)
                    │       ├── username (QLabel)
                    │       ├── StrengthBar (QWidget — custom paintEvent)
                    │       └── action buttons × 3
                    ├── gen_wrap → GeneratorWidget (QWidget)
                    │   ├── IceCrystalWidget (QWidget — custom paintEvent)
                    │   ├── QLineEdit (password display)
                    │   ├── QSlider (length)
                    │   ├── QCheckBox × 4
                    │   └── QPushButton × 3
                    └── log_wrap → QTextEdit (intrusion log)

#2. Custom Painted Widgets

WidgetFilepaintEvent purpose
LoginBackgroundlogin_screen.pyDark gradient + 48px grid lines + center glow
MascotWidget (FlurryCanvas)mascot_widget.pyFull polar bear character with expressions
IceCrystalWidgetfeatures/ice_crystal.pyDeterministic snowflake from password seed
StrengthBarvault_dashboard.py4-segment strength indicator

All custom painters use QPainter.Antialiasing and QPainter.SmoothPixmapTransform render hints.


#3. Theme System

##3.1 PALETTE Constants

# ui/theme.py

PALETTE = {
    "bg":           "#0B1426",   # Main background
    "bg-dark":      "#07101C",   # Darkest (topbar, input bg)
    "sidebar":      "#0F1C35",   # Sidebar background
    "card":         "#162040",   # Card / panel background
    "card-hover":   "#1D2D55",   # Card hover state
    "primary":      "#4FC3F7",   # Sky blue — primary actions
    "primary-dark": "#0288D1",   # Primary pressed state
    "accent":       "#00E5CC",   # Teal — accent / copy button
    "accent-dark":  "#00B8A4",   # Accent pressed state
    "text":         "#E8F4FD",   # Primary text
    "text-muted":   "#7BAFC8",   # Secondary text
    "text-dim":     "#3D6B8A",   # Hint / placeholder text
    "border":       "#1E3A5F",   # Subtle borders
    "border-light": "#2A4E72",   # Visible borders
    "danger":       "#FF5252",   # Error / delete / alert
    "success":      "#00E676",   # Success state
    "warning":      "#FFD740",   # Warning state
    "white":        "#FFFFFF",   # Pure white
}

##3.2 Global QSS vs Inline Styles

FrostVault uses a two-tier styling approach:

TierScopeUsed For
Global QSS (STYLESHEET)Applied to QApplicationBase widget styles (QLabel, QScrollBar, QComboBox, QCheckBox)
Inline setStyleSheet()Applied to individual widgetsButtons, panels, cards — anything inside a parent with its own stylesheet

Why inline for key elements?
When a parent widget has setStyleSheet(), Qt creates a scoped style context. Child widgets may not inherit application-level stylesheet rules for type-based selectors. Inline styles on critical widgets (New Entry button, nav buttons, generator buttons) guarantee they render correctly regardless of parent context.

##3.3 Button Variants

Object name / StyleColorUsed For
Default QPushButtonprimary (#4FC3F7)General actions
_ADD_BTN inlineaccent (#00E5CC)New Entry button
_BTN_COPY inlineaccent (#00E5CC)Copy password
_BTN_USE inlineprimary (#4FC3F7)Use generated password
_BTN_REGEN inlinecard-hover bgRegenerate
danger_btntransparent + danger borderCancel / Lock
icon_btntransparentEye toggle, generator open

#4. Mascot State Machine

States: idle · happy · alert · frozen

idle   → Calm face, ice crown, slow bob, periodic blink
happy  → Squinting eyes, grin + teeth, blush, sparkle stars
alert  → Wide scared eyes, O-mouth, red pulse glow, shake animation
frozen → Sleepy half-closed eyes, flat mouth, ice-blue tint, crown

Transitions (VaultDashboard):
  activate(is_honey=True)         → alert
  activate(is_honey=False)        → idle
  _on_add_entry (save success)    → happy
  _update_mascot_health:
    0 weak passwords              → happy
    >50% weak passwords           → alert
    otherwise                     → idle

Transitions (LoginScreen):
  unlock success                  → happy (then login_success signal)
  wrong password                  → alert
  honey triggered                 → alert

#5. Responsive Behavior

FrostVault uses fixed-width sidebar (220 px) with the main content area taking remaining space. The QScrollArea in the vault view allows vertical scrolling for large credential lists. The login card is fixed-width (420 px) centered on the background.

The application has a minimum window size of 860 × 580 px set in main.py.


#6. Accessibility Notes

  • All interactive elements have setToolTip() set
  • Password fields use QLineEdit.Password echo mode by default; eye-button toggles to Normal
  • Color is never the only indicator of state (strength labels include text)
  • Focus order follows natural top-to-bottom layout order
~ End of Document ~