A local-first, offline-capable credential vault with military-grade encryption. Your data is stored in an encrypted container file (vault.enc) that only exists when unlocked. Features 2-layer security (passphrase + PIN), intelligent multi-tab session management, and secure vault import/restore capabilities.
- 2-Layer Authentication: Passphrase (Layer 1) + PIN (Layer 2)
- AES-256-GCM Encryption: Military-grade authenticated encryption
- Argon2id/PBKDF2: Password-based key derivation (310k+ iterations)
- Zero-Knowledge: No plaintext data ever written to disk
- Localhost-Only: Binds to 127.0.0.1 (no network exposure)
- Session Tokens: In-memory only (never persisted)
- Auto-Lock: Configurable inactivity timer (5-60 minutes)
- Crash Recovery: Automatic cleanup of temporary files
- Single Active Session: Only one tab can modify data at a time
- Instant Takeover: Seamlessly switch ownership between tabs
- BroadcastChannel API: Real-time cross-tab notifications
- Smart Refresh Detection: Primary tab refresh doesn't trigger takeover
- Red Warning Modal: Clear visual indication when session is stolen
- Secure Import: Restore from backup with passphrase verification before database replacement
- Export Date Detection: Automatically extracts and displays vault export timestamps
- Safe Replacement: Verify passphrase against imported vault in-memory before overwriting
- Visual Feedback: Green field + toast notification + "Vault unlocked" label on success
- ZIP Support: Import from
vault.encfiles orvault-backup_*.zippackages - Primary Database Control: Choose whether imported vault becomes your primary database
- PIN Rate Limiting: 5 incorrect attempts → 15-minute lockout
- Passphrase Rate Limiting: 5 failed unlock attempts → 15-minute lockout
- File Change Detection: Warns if
vault.encmodified externally - Secure Memory: SQLite secure_delete pragma enabled
- CORS Protection: Strict same-origin policy
- Helmet.js: HTTP security headers
Lost passphrase = Lost data (NO RECOVERY)
Your passphrase is the encryption key. There is no "forgot password" feature. Write it down securely or use a password manager.
| Component | Technology |
|---|---|
| Frontend | React 18 + Vite + Tailwind CSS |
| Backend | Express.js + Node.js |
| Database | SQLite (better-sqlite3) |
| Encryption | AES-256-GCM + Argon2id/PBKDF2 |
| Auth | Session tokens + PIN (in-memory) |
| Multi-Tab | BroadcastChannel API |
| File Upload | Multer + ADM-ZIP |
- Node.js 18+ (LTS recommended)
- npm or yarn
- Modern browser (Chrome, Firefox, Edge, Safari)
# Clone repository
git clone https://github.com/deyman12/vault-app2.git
cd vault-app2
# Install dependencies (both server and client)
npm run install:all
# (Optional) Migrate existing vault.db to vault.enc
npm run migrate# Run both server and client with hot reload
npm run dev- Frontend: http://localhost:5173
- Backend: http://localhost:5000
# Build frontend for production
npm run build
# Start production server
npm startAccess at: http://127.0.0.1:5000
Build a standalone Windows executable:
# Build frontend, copy icon, and package to .exe
npm run makeOutput:
- Windows Installer:
out/make/squirrel.windows/x64/SecureVaultSetup.exe - Portable ZIP:
out/make/zip/win32/x64/secure-vault-win32-x64-2.0.0.zip
Electron Development Mode:
# Run in Electron window with hot reload
npm run devNote
Icon Source: The app icon is client/public/favicon.ico. During build, it's automatically copied to electron/icon.ico and used for the Windows .exe, installer, and BrowserWindow.
Data Location:
- Development:
server/data/vault.enc - Production (Packaged):
%APPDATA%/secure-vault/data/vault.enc
- Start the app:
npm run dev - Open browser: http://localhost:5173
- You'll see "Create Vault" screen
- Enter a strong passphrase (12+ characters recommended)
- Click "Create Vault"
- After vault creation, you'll see "PIN Setup" screen
- Enter a 6-digit PIN (e.g.,
123456) - Confirm the same PIN
- Click "Complete Setup"
- Dashboard: View all credentials
- Add Credential: Click "+" button
- Add Platform: Organize by website/service
- Reveal Passwords: Click eye icon (requires PIN)
- Lock Vault: Click lock icon in header
Purpose: Decrypt the vault.enc container file
When Required:
- Initial app load
- After page refresh
- After auto-lock timeout
- After manual lock
Security:
- Argon2id or PBKDF2 key derivation
- 32-byte salt (unique per vault)
- 5 failed attempts → 15-minute lockout
Purpose: Authorize sensitive operations (reveal passwords, edit, delete)
When Required:
- First access after vault unlock
- Revealing credential passwords
- Editing/deleting credentials
- Session takeover (multi-tab)
Security:
- 6-digit PIN
- bcrypt hashing
- 5 incorrect attempts → 15-minute lockout
- Session tokens tied to PIN verification
The import feature uses a separated verification flow to ensure you can verify your passphrase before any destructive changes occur.
- Navigate to "Forgot Passphrase" page or Setup Vault screen
- Click "recover here" link (if on Setup page)
- Expand "Import Vault from Backup" section
- Click "Select Vault File"
- Choose
vault.encorvault-backup_*.zipfile - File is validated automatically (checks encryption signature)
- If ZIP: Export date is extracted from filename (e.g.,
vault-backup_26-01-26-13-45-30.zip→ "January 26, 2026, 01:45 PM")
- (Optional) Check "Use this as primary database?" checkbox
- If checked: Shows orange warning about database replacement
- Warning displays export date if available
- Click "Import" button
- File is loaded into memory (NO disk write yet)
- Import button becomes disabled
- Blue info box appears: "Import file processed - Enter your passphrase above to verify"
- If "Use as primary database" was checked:
- Yellow warning appears above passphrase field
- Shows: "Primary Database Replacement Warning"
- Displays export date and warns about database replacement
- Enter the imported vault's passphrase (not current vault's)
- Click "Unlock Vault"
- System verifies passphrase against in-memory vault (no disk changes!)
- On success:
- Passphrase field turns green
- Label appears: "Vault unlocked" (with green checkmark)
- Toast notification: "Vault unlocked successfully."
- On failure:
- Error message shown
- No changes to disk
- If checkbox was checked and passphrase verified:
- Click "Unlock Vault" again
- System writes imported vault to disk (replaces
vault.enc) - Old vault is backed up temporarily
- Vault metadata refreshed (prevents conflict detection)
- Page transitions to Layer 2 PIN screen
- If checkbox was NOT checked:
- Import process stops after verification
- No database replacement occurs
- vault.enc: Raw encrypted vault file
- vault-backup_yy-mm-dd-h-m-s.zip: ZIP containing
vault.encwith timestamp
ZIP filenames are automatically parsed to show when the vault was exported:
| ZIP Filename | Extracted Date |
|---|---|
vault-backup_26-01-26-13-45-30.zip |
January 26, 2026, 01:45 PM |
vault-backup_25-12-25-09-30-15.zip |
December 25, 2025, 09:30 AM |
If file is raw vault.enc, file modification time is used as fallback.
- ✅ In-memory verification: Passphrase checked before any disk writes
- ✅ Automatic backup: Old vault backed up before replacement
- ✅ Auto-restore on failure: Backup restored if import fails
- ✅ Explicit confirmation: Must check "use as primary" checkbox
- ✅ Visual warnings: Yellow warning above passphrase + orange destructive notice
- ✅ Export date display: See when backup was created
- ✅ Metadata refresh: Prevents false conflict detection after import
When you delete your vault from the "Forgot Passphrase" page:
- Confirmation modal appears
- (Optional) Export backup before deletion
- Click "Delete Vault"
- ✨ Toast notification appears (top-right): "Vault permanently deleted."
- ✨ Cooldown card replaces the form:
- Green checkmark icon
- "Vault Deleted Successfully" heading
- Countdown timer: "Redirecting to homepage in 5 seconds..."
- Number decrements: 5 → 4 → 3 → 2 → 1 → 0
- ✨ Auto-redirect to homepage (no manual refresh required)
- ✨ Setup Vault page appears (not unlock screen)
No more manual refresh needed! The system automatically detects vault deletion and routes you to the setup page.
- Tab 1 unlocks vault and enters PIN → Becomes primary owner
- Tab 2 opens → Detects existing session → Shows takeover screen
- Tab 2 can either:
- Take Over: Enter PIN → Become new owner (Tab 1 shows red modal)
- Close Tab: Exit without affecting Tab 1
- BroadcastChannel: Instant cross-tab notifications (no polling)
- localStorage Tracking: Remembers last primary tab for refresh detection
- Stolen Tab Modal: Red overlay with "Take Over Again" or "Close Tab"
- Primary Tab Refresh: Goes to PIN screen (not takeover screen)
- Stolen Tab Refresh: Shows takeover screen
Tab 1 (Primary) Tab 2 (New)
│ │
├── In Dashboard ────────┤
│ Shows Takeover Screen
│ ↓
│ "Take Over This Session"
│ ↓
│ Enters PIN
│ ↓
│◄─── BroadcastChannel ──┤
Red Modal Becomes Primary
"Session Taken Over"
vault-app2/
├── client/ # React frontend
│ ├── src/
│ │ ├── pages/ # UnlockScreen, Dashboard, Settings, Platforms
│ │ ├── components/ # TakeoverScreen, StolenTabModal, PINSetup
│ │ ├── hooks/ # useAutoLock
│ │ ├── context/ # AuthContext, ThemeContext, ToastContext
│ │ └── utils/ # API client, validators
│ └── dist/ # Production build (generated)
│
├── server/ # Express backend
│ ├── src/
│ │ ├── crypto/ # container.js (encryption), pinManager.js
│ │ ├── vault/ # vaultManager.js, crashRecovery.js, importedVaultCache.js
│ │ ├── routes/ # vault.js, credentials.js, platforms.js, settings.js, vault-import.js
│ │ ├── middleware/ # authSession.js, security.js, rateLimiter.js
│ │ ├── models/ # Settings.js (PIN + lockout logic)
│ │ ├── database/ # db.js, schema.sql
│ │ └── scripts/ # migrateToContainer.js
│ └── data/
│ └── vault.enc # Encrypted container (created on first unlock)
│
└── package.json # Root scripts
┌────────────┬─────────┬─────┬──────┬───────┬────────────┬─────────┐
│ VLT1 │ Version │ KDF │ Salt │ Nonce │ Ciphertext │ AuthTag │
├────────────┼─────────┼─────┼──────┼───────┼────────────┼─────────┤
│ 4 bytes │ 1 byte │ 1 B │ 32 B │ 12 B │ Variable │ 16 B │
└────────────┴─────────┴─────┴──────┴───────┴────────────┴─────────┘
Components:
- Magic Bytes:
VLT1(format version identifier) - Version:
0x01(container format version) - KDF:
0x01= PBKDF2,0x02= Argon2id - Salt: Random 32-byte salt (unique per vault)
- Nonce: Random 12-byte nonce (GCM IV)
- Ciphertext: Encrypted SQLite database
- AuthTag: 16-byte authentication tag (tamper detection)
Encryption: AES-256-GCM (authenticated encryption with associated data)
vault.enc (encrypted) → Decrypt with passphrase → vault.tmp.db (SQLite)
↓
App performs database operations
↓
PIN required for sensitive ops
Active Files:
vault.enc(monitored for external changes)vault.tmp.db(temporary database)vault.tmp.db-wal(write-ahead log)vault.tmp.db-shm(shared memory)
vault.tmp.db → Checkpoint WAL → Encrypt → vault.enc (atomic write)
↓
Delete all temp files (vault.tmp.db, -wal, -shm, -journal)
↓
Clear session tokens from memory
Persisted Files:
vault.enc(encrypted container only)
On server startup:
- Check for
vault.tmp.db*files - Delete all found (no recovery attempt)
- vault.enc remains safe and unchanged
| Command | Description |
|---|---|
npm run dev |
Start dev server (hot reload) |
npm run build |
Build frontend for production |
npm start |
Run production server |
npm run copy-icon |
Copy favicon to electron directory |
npm run make |
Build Windows .exe (Electron package) |
npm run install:all |
Install all dependencies |
npm run migrate |
Migrate old vault.db → vault.enc |
npm run server |
Run backend only |
npm run client |
Run frontend only |
# Navigate to project root
cd vault-app2
# Install both server and client dependencies
npm run install:all
# Or install separately
cd server && npm install
cd ../client && npm install# Build frontend optimized bundle
npm run build
# This creates client/dist/ with minified assets
# Server automatically serves from dist/ in production# Start production server (serves built frontend)
npm start
# Access at: http://127.0.0.1:5000# Start both backend and frontend with hot reload
npm run dev
# Frontend: http://localhost:5173 (Vite dev server)
# Backend: http://localhost:5000 (Express API)# Build for Windows (.exe installer + portable ZIP)
npm run make
# Output in: out/make/cd server
# Development with auto-restart
npm run dev
# Production
npm start
# Run migration script
npm run migratecd client
# Development with hot reload
npm run dev
# Production build
npm run build
# Preview production build
npm run preview- ✅ Use strong passphrase (12+ characters, mixed case, numbers, symbols)
- ✅ Write down passphrase (store in physical safe or password manager)
- ✅ Backup vault.enc regularly (encrypted, safe to store in cloud)
- ✅ Export backups before major changes (use Settings → Backup Vault)
- ✅ Test imported vaults (verify passphrase before setting as primary)
- ✅ Enable auto-lock (minimize exposure time)
- ✅ Lock vault before leaving (manual lock button)
- ✅ Use unique PIN (different from other PINs)
- ✅ Close old tabs (avoid session takeover confusion)
- ✅ Verify export dates (check when backup was created during import)
- ❌ Share passphrase/PIN (not even with IT/support)
- ❌ Store passphrase in plaintext (not in notes, emails, or code)
- ❌ Reuse passphrases (use unique passphrase for this vault)
- ❌ Edit vault.enc manually (will corrupt data)
- ❌ Import without verification (always check passphrase works first)
- ❌ Sync .env file (contains configs, not encryption key)
- ❌ Rely on recovery (there is none)
- ❌ Use weak PIN (avoid
111111,123456) - ❌ Skip backup before import (always export current vault first)
- ✅ Database dump attacks
- ✅ Network eavesdropping (localhost-only)
- ✅ Casual data breaches (encrypted at rest)
- ✅ Brute-force attacks (rate limiting + strong KDF)
- ✅ Credential tampering (GCM authentication tags)
- ✅ Memory leakage (secure_delete pragma)
- ✅ Cross-tab interference (session management)
- ✅ Accidental database overwrites (passphrase verification before import)
- ❌ Server compromise (if attacker gains root/admin access)
- ❌ Keyloggers (physical or software)
- ❌ Screen recording malware
- ❌ Social engineering for passphrase/PIN
- ❌ Physical theft of unlocked computer
- ❌ Supply chain attacks on dependencies
Threat Model: Trusted server environment, untrusted storage, untrusted network.
Cause: vault.enc was modified while vault was unlocked (e.g., cloud sync).
Solution:
- Local changes saved to
vault.local.enc - Manually merge or choose one version
- Rename chosen file to
vault.enc - Restart server
Cause: 5 failed passphrase attempts in 15 minutes.
Solution: Wait for countdown timer (displayed on screen).
Cause: 5 incorrect PIN entries in 15 minutes.
Solution: Wait 15 minutes. Correct PIN entries don't count toward limit.
Cause: File is not a properly encrypted vault or ZIP doesn't contain vault.enc.
Solution:
- Verify you selected the correct file
- For ZIP: Ensure it contains
vault.enc(not nested in folders) - Try importing the
vault.encfile directly
Cause: Entered passphrase doesn't match the imported vault.
Solution:
- Verify you're using the imported vault's passphrase (not current vault's)
- Check for typos (passphrase is case-sensitive)
- If forgotten, import cannot proceed (no recovery)
Cause: Native binding compilation issues (Windows/macOS/ARM).
Solution: App automatically falls back to PBKDF2 (still secure, slightly slower).
Expected behavior. Session tokens are memory-only (not localStorage). This is a security feature.
Solution:
- Refreshing the primary tab → Returns to PIN screen (not full re-unlock)
- Refreshing a stolen tab → Shows takeover screen
Cause: window.close() only works for tabs opened via window.open().
Solution: Manually close tab with Ctrl+W (Windows/Linux) or Cmd+W (Mac).
Cause: Old version of app without deletion flow improvements.
Solution: This feature is in v2.0.0+. Update to latest version.
| Operation | Time (Typical) |
|---|---|
| Vault unlock (PBKDF2) | ~500ms |
| Vault unlock (Argon2id) | ~1-2s |
| Vault lock | ~300ms |
| PIN verification | ~100ms |
| CRUD operations | <10ms |
| Cross-tab notification | <5ms |
| Import file validation | <100ms |
| Import passphrase verify | ~500ms-2s |
- Open Tab 1: Unlock vault → Enter PIN → Go to Dashboard
- Open Tab 2: Navigate to app → See takeover screen
- Tab 2 takes over: Enter PIN → Become primary
- Check Tab 1: Should show red "Session Taken Over" modal instantly
- Refresh primary tab: Should go to PIN screen (not takeover)
- Refresh stolen tab: Should show takeover screen
- Export backup: Settings → Backup Vault → Save
vault-backup_*.zip - Delete vault: Forgot Passphrase → Delete Vault → Wait for auto-redirect
- Import flow:
- On Setup page, click "recover here"
- Expand "Import Vault from Backup"
- Select exported ZIP file
- Check "Use this as primary database?"
- Click "Import" → Wait for blue confirmation
- Enter exported vault's passphrase
- Click "Unlock Vault" → See green field + toast
- Click "Unlock Vault" again → Finalize import
- Enter PIN → Access restored vault
Security-related PRs: Please contact maintainers first.
This project is licensed under the MIT License - see the LICENSE file for details.
- better-sqlite3 - Fast synchronous SQLite library
- Argon2 - Password hashing competition winner
- React - UI library
- Express - Web framework
- bcrypt - PIN hashing
- Vite - Frontend build tool
- Multer - File upload middleware
- ADM-ZIP - ZIP file manipulation
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Security: Report vulnerabilities to Mochino
Argon2id (preferred):
- Memory: 64 MB
- Iterations: 3
- Parallelism: 4
- Output: 32 bytes
PBKDF2 (fallback):
- Hash: SHA-256
- Iterations: 310,000+
- Salt: 32 bytes
- Output: 32 bytes
- Hashing: bcrypt (cost factor 10)
- No plaintext: PIN never stored unencrypted
- No recovery: Lost PIN = Must re-enter passphrase
Made with 🍵 by Mochino