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.
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
| Widget | File | paintEvent purpose |
|---|---|---|
LoginBackground | login_screen.py | Dark gradient + 48px grid lines + center glow |
MascotWidget (FlurryCanvas) | mascot_widget.py | Full polar bear character with expressions |
IceCrystalWidget | features/ice_crystal.py | Deterministic snowflake from password seed |
StrengthBar | vault_dashboard.py | 4-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:
| Tier | Scope | Used For |
|---|---|---|
Global QSS (STYLESHEET) | Applied to QApplication | Base widget styles (QLabel, QScrollBar, QComboBox, QCheckBox) |
Inline setStyleSheet() | Applied to individual widgets | Buttons, 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 / Style | Color | Used For |
|---|---|---|
| Default QPushButton | primary (#4FC3F7) | General actions |
_ADD_BTN inline | accent (#00E5CC) | New Entry button |
_BTN_COPY inline | accent (#00E5CC) | Copy password |
_BTN_USE inline | primary (#4FC3F7) | Use generated password |
_BTN_REGEN inline | card-hover bg | Regenerate |
danger_btn | transparent + danger border | Cancel / Lock |
icon_btn | transparent | Eye 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.Passwordecho mode by default; eye-button toggles toNormal - Color is never the only indicator of state (strength labels include text)
- Focus order follows natural top-to-bottom layout order