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.
Data Models
FrostVault v1.0.0 — IceLegends
#1. Database Overview
- Engine: SQLite 3 (Python stdlib
sqlite3) - Location:
%APPDATA%\FrostVault\vault.db - Access: Single-process, WAL journal mode
- Encryption: Application-layer (ciphertext stored in BLOB columns)
#2. Schema
##2.1 master table
Stores vault identity and cryptographic parameters. Always contains exactly one row.
CREATE TABLE IF NOT EXISTS master (
id INTEGER PRIMARY KEY,
argon2_hash TEXT NOT NULL, -- Argon2id PHC string
argon2_salt TEXT NOT NULL, -- hex-encoded 32-byte salt
kdf_salt TEXT NOT NULL, -- hex-encoded 32-byte PBKDF2 salt
created_at TEXT NOT NULL -- ISO-8601 UTC timestamp
);
| Column | Type | Notes |
|---|---|---|
argon2_hash | TEXT | Full Argon2id PHC string (includes params + salt) |
argon2_salt | TEXT | Separate hex salt for Argon2id |
kdf_salt | TEXT | Separate hex salt for PBKDF2 key derivation |
created_at | TEXT | Vault creation timestamp |
##2.2 entries table
Stores encrypted credential fields. Each field is independently encrypted.
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title BLOB NOT NULL, -- AES-256-GCM ciphertext
username BLOB NOT NULL, -- AES-256-GCM ciphertext
password BLOB NOT NULL, -- AES-256-GCM ciphertext
url BLOB, -- AES-256-GCM ciphertext (nullable)
notes BLOB, -- AES-256-GCM ciphertext (nullable)
category TEXT NOT NULL DEFAULT 'General',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
Note: category is stored in plaintext — it is low-sensitivity metadata used only for UI filtering.
##2.3 honey_entries table
Pre-generated fake credentials shown during honey vault mode.
CREATE TABLE IF NOT EXISTS honey_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
url TEXT,
category TEXT NOT NULL DEFAULT 'General'
);
Note: Honey entries are stored in plaintext — they are intentional decoys and contain no real data.
##2.4 intrusion_log table
Records failed authentication attempts for forensic review.
CREATE TABLE IF NOT EXISTS intrusion_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
attempt_count INTEGER NOT NULL,
attempted_at TEXT NOT NULL -- ISO-8601 UTC timestamp
);
##2.5 fail_count table
Tracks the current consecutive failure count in memory across sessions.
CREATE TABLE IF NOT EXISTS fail_count (
id INTEGER PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0
);
#3. Entity Relationship Diagram
master (1 row)
│ id, argon2_hash, argon2_salt, kdf_salt, created_at
│
│ (conceptual relationship — master key decrypts entries)
▼
entries (0..n rows)
│ id, title(enc), username(enc), password(enc), url(enc), notes(enc), category, timestamps
│
│ (independent tables — no FK constraints)
honey_entries (static seed data)
│ id, title, username, password, url, category
intrusion_log (0..n rows)
│ id, attempt_count, attempted_at
fail_count (1 row)
id, count
#4. Ciphertext Blob Layout (per BLOB column)
Byte offset Length Content
─────────── ──────── ─────────────────────────────
0 12 GCM nonce (random, per encrypt call)
12 N AES-256-GCM ciphertext
12+N 16 GCM authentication tag (appended by library)
Total: N + 28 bytes (where N = len(plaintext_utf8))
#5. Vault Object Model (Python)
# Decrypted entry as returned by vault.list_entries() / vault.get_entry()
entry: dict = {
"id": int,
"title": str,
"username": str,
"password": str,
"url": str,
"notes": str,
"category": str,
"created_at": str, # ISO-8601
"updated_at": str, # ISO-8601
}
Entries exist as plaintext dicts only while the vault is unlocked and only within the calling function's scope. They are never cached.
#6. Database File Location
# core/database.py
import os
db_path = os.path.join(os.environ["APPDATA"], "FrostVault", "vault.db")
The %APPDATA%\FrostVault\ directory is created on first launch if it does not exist. This location is user-writable without elevated privileges and is excluded from the application install directory so that uninstalling FrostVault does not delete the user's vault.
#7. Data Migration
Version 1.0.0 has no migration system. The schema is created fresh on first launch. Future versions should:
- Add a
schema_versionrow to themastertable - Apply
ALTER TABLEmigrations indatabase.pyon startup - Backup
vault.dbbefore any destructive migration