Document Purpose: Comprehensive security analysis answering key questions about the vault-app2 encryption, backup, and protection systems.
Last Updated: January 26, 2026
- Encryption Security
- End-to-End Encryption
- Credential Hacking/Leaking Risks
- Data Loss Scenarios
- Backup System Assessment
- Optimal Backup Strategy
- Multi-Device Usage
- Mobile Compatibility
- Google Sync with Encryption
- Multi-Device Same Vault
- Power Loss Scenarios
- Web UI Vulnerabilities
- Route Access Rules
- File Upload Validation
- XSS Protection
- Backdoor/Tampering Protection
- Offline Security
- Biggest Security Weakness
| Component | Specification | Security Level |
|---|---|---|
| Cipher | AES-256-GCM | ✅ NSA approved for TOP SECRET |
| Key Derivation (Primary) | Argon2id | ✅ Winner of Password Hashing Competition |
| Key Derivation (Fallback) | PBKDF2-SHA256 (310,000+ iterations) | ✅ NIST recommended |
| Salt | 32 bytes (256 bits) random | ✅ Unique per vault |
| Nonce/IV | 12 bytes (96 bits) random | ✅ Unique per encryption |
| Authentication | GCM Auth Tag (16 bytes) | ✅ Tamper detection |
From container.js:
// AES-GCM parameters
const AES_ALGORITHM = "aes-256-gcm";
const PBKDF2_ITERATIONS = 310000;
const PBKDF2_KEYLEN = 32; // 256-bit key
const PBKDF2_DIGEST = "sha256";Argon2id Parameters (when available):
- Memory Cost: 64 MiB
- Time Cost: 3 iterations
- Parallelism: 4 threads
- Hash Length: 32 bytes
┌────────────┬─────────┬─────┬──────┬───────┬────────────┬─────────┐
│ VLT1 │ Version │ KDF │ Salt │ Nonce │ Ciphertext │ AuthTag │
├────────────┼─────────┼─────┼──────┼───────┼────────────┼─────────┤
│ 4 bytes │ 1 byte │ 1 B │ 32 B │ 12 B │ Variable │ 16 B │
└────────────┴─────────┴─────┴──────┴───────┴────────────┴─────────┘
Tip
Brute-Force Resistance: With Argon2id requiring 64 MiB memory per attempt, GPU-based attacks are severely limited. At 1,000 attempts/second (unrealistically fast), a 12-word passphrase would take longer than the age of the universe to crack.
flowchart LR
subgraph "Your Device"
A["Plaintext Credentials"] -->|Encrypt| B["vault.enc<br/>(Encrypted)"]
B -->|Decrypt| A
end
subgraph "Storage (Optional)"
C["Cloud/Backup<br/>Only sees encrypted data"]
end
B -.->|Copy/Sync| C
style A fill:#4ade80
style B fill:#f87171
style C fill:#fbbf24
| Aspect | Status | Details |
|---|---|---|
| Encryption Location | ✅ Client/Local | All encryption/decryption happens on your device |
| Server Involvement | ✅ Same Device | Express server runs on 127.0.0.1 (localhost only) |
| Network Exposure | ✅ None | No external network connections |
| Key Storage | ✅ In-Memory Only | Passphrase never persisted, session tokens memory-only |
| Plaintext on Disk | ✅ Only When Unlocked | vault.tmp.db exists only while vault is open |
Important
True E2E Guarantee: Since both frontend and backend run locally on your machine (especially in Electron mode), there is no "man-in-the-middle" possible. The encryption key never leaves your device.
| Attack Vector | Protection | Implementation |
|---|---|---|
| Database Dump | AES-256-GCM encryption | Data unreadable without passphrase |
| Network Eavesdropping | Localhost-only binding | 127.0.0.1 prevents LAN access |
| Brute Force | Rate limiting + strong KDF | 5 attempts/15 min, 310k+ iterations |
| Credential Tampering | GCM authentication tags | Detects any modification |
| Session Theft | In-memory tokens only | Never persisted to disk |
| Memory Leakage | secure_delete pragma |
SQLite overwrites deleted data |
| Cross-Tab Interference | Single-session ownership | Only one tab can modify at a time |
| Attack Vector | Why Not Protected | Mitigation |
|---|---|---|
| Keyloggers | App cannot detect OS-level surveillance | Use trusted computer |
| Screen Recording | App cannot prevent screen capture | Work in private |
| Physical Theft (unlocked) | Vault accessible while open | Use auto-lock, lock manually |
| Malware with Root Access | Full system compromise | Use antivirus, keep OS updated |
| Social Engineering | Human factors | Never share passphrase |
| Supply Chain Attacks | npm dependency vulnerabilities | Regular updates, audit deps |
Caution
Unlocked Vulnerability: While the vault is unlocked, vault.tmp.db contains your credentials in plaintext SQLite format. Always lock the vault when not in use.
| Scenario | Recovery | Action Required |
|---|---|---|
| Forgot passphrase | ❌ Impossible | DATA LOST PERMANENTLY (no recovery mechanism) |
| Forgot PIN | ✅ Recoverable | Use "reset pin ?" link → verify passphrase → set new PIN |
Deleted vault.enc |
✅ If backed up | Restore from backup ZIP |
Corrupted vault.enc |
✅ If backed up | GCM auth will fail, restore from backup |
| Lost device | ✅ If backed up | Transfer backup to new device |
| App crash while unlocked | ✅ Automatic | vault.enc unchanged, temp files cleaned on restart |
| Power loss while unlocked | See Power Loss Scenarios |
Warning
CRITICAL: There is NO "forgot password" feature. Your passphrase IS the encryption key. If you forget it, your data is permanently unrecoverable. Write it down securely or use a password manager.
flowchart TD
A["Lost Access"] --> B{"What was lost?"}
B -->|Passphrase| C["❌ DATA PERMANENTLY LOST<br/>No recovery possible"]
B -->|PIN| D["Reset PIN via passphrase<br/>➝ /reset-pin page"]
B -->|vault.enc file| E{"Have backup?"}
E -->|Yes| F["Import vault from backup<br/>➝ Unlock screen → Import"]
E -->|No| C
B -->|Device| G{"Have backup?"}
G -->|Yes| H["Install app on new device<br/>→ Import backup"]
G -->|No| C
style C fill:#ef4444
style D fill:#22c55e
style F fill:#22c55e
style H fill:#22c55e
| Feature | Status | Location |
|---|---|---|
| Export Backup | ✅ Implemented | Settings → Backup Vault |
| ZIP with Timestamp | ✅ Implemented | vault-backup_yy-mm-dd-h-m-s.zip (UTC+7) |
| Import/Restore | ✅ Implemented | Unlock screen → "Import Vault from Backup" |
| Pre-Delete Export | ✅ Suggested | Warning modal before vault deletion |
| Pre-Import Backup | ✅ Automatic | Old vault backed up before replacement |
| Passphrase Verification | ✅ Implemented | Verify against imported vault before overwriting |
| Limitation | Impact | Recommendation |
|---|---|---|
| Manual-only | User must remember to backup | Set calendar reminder |
| No encryption layer | vault.enc already encrypted, but ZIP is not password-protected |
Store ZIP in secure location |
| Single-file | Only vault.enc included |
Consider backing up app config too |
| No versioning | Only one backup per export | Keep multiple dated backups |
| No auto-backup | No scheduled backup feature | Manual discipline required |
Note
Why Manual is Acceptable: Since vault.enc is already AES-256-GCM encrypted, the backup ZIP is safe to store in cloud storage (Google Drive, Dropbox) without additional encryption.
┌─────────────────────────────────────────────────────────────────────┐
│ 3-2-1 BACKUP STRATEGY │
├─────────────────────────────────────────────────────────────────────┤
│ 3 COPIES : Original vault.enc + 2 backup copies │
│ 2 MEDIA : Local storage + Cloud storage │
│ 1 OFFSITE : At least one backup in different physical location │
└─────────────────────────────────────────────────────────────────────┘
- Daily/Weekly Export: Settings → Backup Vault after significant changes
- Local Storage: Keep on USB drive or separate partition
- Cloud Storage: Upload to Google Drive/Dropbox/OneDrive (encrypted file is safe)
- Version Naming: Keep multiple versions (e.g., last 5 backups)
- Passphrase Backup: Store passphrase separately from vault (physical safe, password manager)
- Test Restores: Periodically verify backups can be imported
| Frequency | Action | Storage |
|---|---|---|
| Daily (if changed) | Export backup ZIP | Local folder |
| Weekly | Upload to cloud | Google Drive encrypted folder |
| Monthly | Fresh backup to USB | USB drive in safe |
| Before major changes | Manual export | All locations |
Tip
Cloud Safety: Since vault.enc uses AES-256-GCM, it's cryptographically safe to store on cloud services. An attacker with access to Google Drive cannot read your credentials without your passphrase.
sequenceDiagram
participant D1 as Device 1 (Current)
participant B as Backup File
participant D2 as Device 2 (New)
D1->>B: Export vault-backup_*.zip
Note over B: Transfer via<br/>USB/Cloud/Email
B->>D2: Copy backup file
D2->>D2: Install Secure Vault app
D2->>D2: Import backup on Unlock screen
D2->>D2: Enter original passphrase
Note over D2: Vault restored!
On Device 1 (Source):
- Settings → Backup Vault → Download
vault-backup_*.zip - Transfer ZIP to new device (USB, cloud, etc.)
On Device 2 (Destination):
- Install/Start Secure Vault app
- On unlock screen, expand "Import Vault from Backup"
- Select the backup ZIP file
- Check "Use this as primary database"
- Click "Import"
- Enter your original passphrase → Unlock
Warning
No Sync: Changes on one device do NOT automatically sync to other devices. You must manually export/import after making changes.
| Platform | Support | Notes |
|---|---|---|
| Windows | ✅ Full | Electron .exe installer available |
| macOS | Needs Electron macOS build (not configured) | |
| Linux | Needs Electron Linux build (not configured) | |
| Android | ❌ None | Requires major rewrite |
| iOS | ❌ None | Requires major rewrite |
| Web Browser | ✅ Dev only | npm run dev for local development |
To support mobile (Android/iOS), would need:
- React Native or Capacitor - Replace Vite with mobile framework
- SQLite Mobile Library - Replace
better-sqlite3(native Node.js) - Crypto Polyfills - Node.js crypto → Web Crypto API / react-native-crypto
- File System Abstraction - iOS/Android sandboxed storage
- Local Server Alternative - Replace Express with in-process API
Estimated Effort: 2-4 weeks for experienced developer
Note
Workaround: You can access the web UI from a mobile browser when running npm run dev on the same network, but this is NOT recommended for security (exposes to LAN).
| Method | Encryption Status | Implementation |
|---|---|---|
| Manual Upload | ✅ Fully Encrypted | Export ZIP → Upload to Google Drive |
| Auto-Sync | ❌ Not Implemented | Would need Google Drive API integration |
| Real-Time Sync | ❌ Not Possible | Single-device architecture |
- Export backup: Settings → Backup Vault
- Upload
vault-backup_*.zipto Google Drive - On new device: Download from Google Drive
- Import via Unlock screen
Your Vault File Journey to Google:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Credentials │ ──► │ vault.enc │ ──► │ Google │
│ (Plaintext) │ │ (AES-256) │ │ Drive │
└─────────────┘ └─────────────┘ └─────────────┘
Local Local Cloud
Encrypted Encrypted Encrypted
Google sees: Random bytes that look like noise Google cannot: Decrypt without your passphrase (impossible)
Tip
Future Enhancement: Auto-sync could be implemented via Google Drive API, storing only the encrypted vault.enc. The encryption key (your passphrase) would never touch Google's servers.
flowchart TD
subgraph D1["Device 1"]
A1["Vault.enc v1"]
end
subgraph D2["Device 2"]
A2["Vault.enc v1"]
end
subgraph Backup["Shared Storage"]
B["vault-backup_*.zip"]
end
A1 -->|"Export"| B
B -->|"Import"| A2
A2 -->|"Export (after changes)"| B
B -->|"Import"| A1
| Feature | Status | Notes |
|---|---|---|
| Use same vault on 2+ devices | ✅ Yes | Via backup/import |
| Real-time sync | ❌ No | Not implemented |
| Conflict resolution | ❌ No | Manual only |
| Last-write wins | ❌ No | User must manage versions |
| Simultaneous editing | ❌ No | Would corrupt data |
Caution
Conflict Warning: If you edit the vault on Device 1 and Device 2 independently without syncing, you'll have two different versions. Only ONE can be imported – the other's changes are lost.
- Always export before editing on another device
- Designate one device as "primary"
- Import latest backup before making changes
- Export immediately after changes
Question: What happens if I lose power while using the app (for example, sudden blackout or laptop battery drain)?
| State When Power Lost | Data Impact | Recovery Action |
|---|---|---|
| Vault Locked | ✅ Zero impact | Normal startup |
| Vault Unlocked (idle) | vault.enc has last-saved state |
|
| During Write Operation | Operation must be repeated | |
| During Lock Operation | ❌ Potential temp file issue | Crash recovery cleans up |
On Normal Shutdown:
vault.tmp.db → Checkpoint WAL → Encrypt → vault.enc (atomic write)
↓
Delete all temp files
↓
Clear session tokens
On Crash Recovery (Next Startup): From crashRecovery.js:
// On server startup:
// 1. Check for vault.tmp.db* files
// 2. Delete all found (no recovery attempt)
// 3. vault.enc remains safe and unchanged| File | Status | Notes |
|---|---|---|
vault.enc |
✅ Safe | Last encrypted state preserved |
vault.tmp.db |
Unsaved changes lost | |
vault.tmp.db-wal |
Write-ahead log discarded | |
vault.tmp.db-shm |
Shared memory cleaned |
Important
Design Philosophy: The app prioritizes data integrity over data recency. It's better to lose unsaved changes than to risk a corrupted vault. The encrypted vault.enc is the source of truth.
| Protection | Implementation | Risk Mitigated |
|---|---|---|
| Content Security Policy | Helmet.js CSP headers | XSS, code injection |
| CORS | Localhost origins only | Cross-origin attacks |
| No External Connections | 127.0.0.1 binding | Network exposure |
| Session Tokens | Memory-only, random | Session hijacking |
| Input Sanitization | Backend validation | SQL injection |
| Rate Limiting | 5-100 requests/15 min | Brute force |
From security.js:
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "fonts.googleapis.com"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "fonts.gstatic.com"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
},
}| Risk | Likelihood | Impact | Notes |
|---|---|---|---|
| XSS via dangerouslySetInnerHTML | Very Low | Medium | React escapes by default |
| Dependency vulnerabilities | Low | Varies | Regular updates needed |
| React state exposure | Very Low | Low | Memory-only, no persistence |
| Local file inclusion | Very Low | High | No user-controlled paths |
Tip
Why Low Risk: Since this runs locally and only accepts connections from 127.0.0.1, there's no network attack surface. An attacker would need to already be on your machine.
flowchart LR
A["HTTP Request"] --> B{"127.0.0.1?"}
B -->|No| C["❌ Connection Refused"]
B -->|Yes| D{"Rate Limited?"}
D -->|Yes| E["❌ 429 Too Many Requests"]
D -->|No| F{"Vault Unlocked?"}
F -->|No| G["❌ 403 Vault Locked"]
F -->|Yes| H{"Session Token Valid?"}
H -->|No| I["❌ 401 Unauthorized"]
H -->|Yes| J{"Tab Owner?"}
J -->|No| K["❌ 409 Tab Conflict"]
J -->|Yes| L["✅ Request Processed"]
| Category | Auth Required | Token Required | Owner Required | Example |
|---|---|---|---|---|
| Public | ❌ | ❌ | ❌ | /api/vault/status, /health |
| Vault Ops | ✅ Passphrase | ❌ | ❌ | /api/vault/unlock, /api/vault/create |
| PIN Ops | ✅ Vault unlocked | ❌ | ❌ | /api/settings/pin/verify |
| CRUD | ✅ Full | ✅ | ✅ | /api/credentials/*, /api/platforms/* |
| Sensitive | ✅ Full | ✅ | ✅ | /api/credentials/:id/reveal |
From rateLimiter.js:
| Limiter | Max Requests | Window | Applied To |
|---|---|---|---|
| PIN Verification | 5 | 15 min | /api/settings/pin/verify |
| General API | 100 | 15 min | All /api/* routes |
| Credential Ops | 50 | 15 min | /api/credentials/* |
Note
Multi-Tab Ownership: Only the "primary" tab (the one that entered PIN) can perform write operations. Other tabs receive 409 Conflict.
From vault-import.js:
flowchart TD
A["File Upload"] --> B{"Extension Check"}
B -->|".exe, .dll, etc"| C["❌ Invalid file type"]
B -->|".enc, .zip"| D{"Size Check"}
D -->|">50MB"| E["❌ File too large"]
D -->|"<50MB"| F{"ZIP Extraction"}
F -->|"Failed"| G["❌ Invalid ZIP"]
F -->|"Success"| H{"vault.enc found?"}
H -->|"No"| I["❌ Missing vault.enc"]
H -->|"Yes"| J{"Structure Check"}
J -->|"Invalid magic bytes"| K["❌ Not a vault file"]
J -->|"Valid VLT1 header"| L["✅ Valid import file"]
| Check | Implementation | Rejection Message |
|---|---|---|
| File type | Multer fileFilter |
"Only .enc and .zip files allowed" |
| File size | Multer limits.fileSize |
"File too large (max 50MB)" |
| ZIP structure | AdmZip extraction | "Failed to extract vault.enc" |
| vault.enc presence | ZIP entry search | "ZIP must contain vault.enc" |
| Magic bytes | Buffer check for "VLT1" | "Invalid vault file format" |
| Container version | Version byte check | "Unsupported container version" |
fileFilter(req, file, cb) {
const allowedMimes = ['application/octet-stream', 'application/zip', 'application/x-zip-compressed'];
const allowedExts = ['.enc', '.zip'];
const ext = file.originalname.toLowerCase().match(/\.[a-z0-9]+$/)?.[0];
if (!ext || !allowedExts.includes(ext)) {
return cb(new Error('Only .enc and .zip files are allowed'));
}
cb(null, true);
}Tip
Defense in Depth: Even if a malicious file passed the filter, the vault unlock would fail because decryption requires the correct passphrase. Wrong passphrase = GCM authentication failure.
| Protection | Implementation | Coverage |
|---|---|---|
| React Auto-Escaping | JSX syntax | All rendered content |
| Content Security Policy | Helmet.js | scriptSrc: ["'self'"] |
| X-XSS-Protection | Helmet.js | Browser XSS filter enabled |
No dangerouslySetInnerHTML |
Code review | Minimal use |
No innerHTML |
Code review | Avoided entirely |
| Strict Origin Policy | CSP | No inline scripts |
scriptSrc: ["'self'"]; // Only allow scripts from same originThis means:
- ❌ Cannot inject
<script>alert('xss')</script>via data - ❌ Cannot load external scripts
- ❌ Cannot use inline event handlers injected via data
- ✅ Only bundled app scripts execute
// User input is automatically escaped by React
const userInput = "<script>alert('xss')</script>";
return <div>{userInput}</div>; // Renders as text, not executedNote
Offline Advantage: Since the app doesn't connect to external servers, there's no attack vector for reflected XSS from external sources. All data originates from the encrypted local vault.
| Target | Protection | Mechanism |
|---|---|---|
| Encrypted Vault | ✅ Strong | GCM 16-byte authentication tag |
| Database Contents | ✅ Strong | Any modification = decryption failure |
| App Code (Source) | Open source, review before use | |
| App Code (Built) | Download from official release | |
| Dependencies | Regular npm audit needed |
From container.js:
try {
const decipher = createDecipheriv(AES_ALGORITHM, key, nonce);
decipher.setAuthTag(authTag); // 16-byte integrity check
// If ANY byte was modified, this throws:
// "Decryption failed: Invalid passphrase or corrupted data"
} catch (err) {
throw new Error("Decryption failed: Invalid passphrase or corrupted data");
}- ✅
vault.encfile contents → GCM tag verification fails - ✅ Salt/nonce values → Decryption produces garbage
- ✅ Ciphertext length field → Parse error
- ✅ Magic bytes → "Invalid container format" error
⚠️ package.jsonscripts → Run malicious commands⚠️ node_modules/*→ Supply chain attack⚠️ Built Electron.exe→ If downloaded from untrusted source
Warning
Supply Chain Risk: Always download releases from official GitHub repository. Verify checksums if available. Run npm audit regularly to check for vulnerable dependencies.
| Advantage | Impact |
|---|---|
| No network exposure | Eliminates remote attacks |
| No server breaches | Your data isn't on any server |
| No man-in-the-middle | No network to intercept |
| No account compromise | No login to external service |
| No provider access | Company can't read your data |
| Risk Category | Examples | Mitigation |
|---|---|---|
| Physical Access | Someone uses your unlocked computer | Auto-lock, manual lock, screen lock |
| Malware | Keylogger captures passphrase | Antivirus, OS updates |
| Memory Attacks | Cold boot attack on RAM | Power off when not in use |
| Weak Passphrase | Dictionary attack on vault.enc |
Use 12+ word passphrase |
| User Error | Passphrase in plaintext file | Secure passphrase storage |
| Backup Exposure | Unencrypted backup on shared drive | vault.enc is encrypted, but practice care |
| Factor | Secure Vault (Offline) | Cloud Password Manager |
|---|---|---|
| Network attack surface | ✅ None | ❌ Internet-exposed |
| Server breach risk | ✅ None (no server) | ❌ Provider database breach |
| Encryption key custody | ✅ You only | |
| Malware on device | ||
| Availability | ✅ Always (local) |
Tip
Threat Model: Secure Vault assumes a trusted device in an untrusted world. If your device itself is compromised (malware, physical access), additional OS-level security is needed.
┌─────────────────────────────────────────────────────────────────┐
│ ⚠️ CRITICAL: NO PASSPHRASE RECOVERY MECHANISM EXISTS ⚠️ │
│ │
│ This is BY DESIGN for security, but creates user risk: │
│ • Forget passphrase → All credentials permanently lost │
│ • No "forgot password" email │
│ • No master key recovery │
│ • No backdoor (that's the point) │
│ │
│ MITIGATION: Write down passphrase, store in physical safe │
│ or use another password manager for the passphrase │
└─────────────────────────────────────────────────────────────────┘
- No automatic backup scheduling
- User must remember to export regularly
- Single point of failure (local
vault.enc)
Mitigation: Set calendar reminders, use 3-2-1 backup strategy
- While unlocked,
vault.tmp.dbcontains plaintext data - Someone with physical access can read credentials
- Memory may contain decrypted data
Mitigation: Use auto-lock (configurable 5-60 min), lock manually before leaving
| Rank | Weakness | Severity | Notes |
|---|---|---|---|
| 4 | Session token in memory | Medium | Browser devtools could expose |
| 5 | No 2FA for passphrase | Medium | Single authentication factor |
| 6 | Supply chain dependencies | Medium | npm packages could be compromised |
| 7 | 'unsafe-inline' for styles |
Low | Slightly weaker CSP |
| 8 | No audit logging | Low | No record of access attempts |
| 9 | Single-device architecture | Low | Manual sync required |
| Category | Score | Notes |
|---|---|---|
| Encryption Strength | ⭐⭐⭐⭐⭐ | AES-256-GCM + Argon2id is state-of-the-art |
| Network Security | ⭐⭐⭐⭐⭐ | 127.0.0.1 only, no external connections |
| Data at Rest | ⭐⭐⭐⭐⭐ | Fully encrypted with authentication |
| Access Control | ⭐⭐⭐⭐ | Multi-layer (passphrase + PIN + tab ownership) |
| Backup System | ⭐⭐⭐ | Functional but manual-only |
| Recovery Options | ⭐⭐ | PIN recoverable, passphrase = no recovery |
| User Experience | ⭐⭐⭐⭐ | Clear UI, but requires discipline |
| Multi-Device | ⭐⭐ | Works but no sync |
Secure Vault is a high-security, local-first credential manager with military-grade encryption. Its security model trades convenience (password recovery, cloud sync) for maximum protection. Suitable for users who understand the responsibility of managing their own encryption keys.
| File | Purpose |
|---|---|
| container.js | AES-256-GCM encryption/decryption |
| vaultManager.js | Vault lifecycle and session management |
| security.js | Helmet, CORS, body limits |
| rateLimiter.js | Rate limiting configuration |
| vault-import.js | Backup import/validation |
| README.md | Full app documentation |