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