This document describes the security model, cryptographic design, and threat analysis for Passwordless Vault.
- Security Goals
- Cryptographic Design
- Threat Model
- Security Controls
- Known Limitations
- Responsible Disclosure
- Confidentiality: Vault contents are only accessible with a registered passkey
- Integrity: Any tampering with encrypted data is detected
- Authentication: Only the legitimate user can unlock the vault
- No Password Storage: No passwords or password-derived keys are ever stored
- Local-Only: Sensitive data never leaves the user's device
| Purpose | Algorithm | Rationale |
|---|---|---|
| Vault Encryption | AES-256-GCM | NIST-approved, authenticated encryption |
| Key Derivation | HKDF-SHA256 | NIST SP 800-56C, extracts entropy from PRF |
| Key Wrapping | AES-KW | NIST SP 800-38F, RFC 3394 |
| Random Generation | crypto.getRandomValues() |
CSPRNG from browser |
Key Size: 256 bits
IV Size: 96 bits (12 bytes) - NIST recommended
Tag Size: 128 bits (default)
IV Generation: Each encryption operation generates a fresh random IV using crypto.getRandomValues(). IVs are prepended to ciphertext for storage.
Hash: SHA-256
Salt: Credential-specific (16 bytes random + prefix)
Info: "vault-kek-v1" (domain separation)
Output: 256 bits
The PRF output from the authenticator is treated as Input Keying Material (IKM). HKDF expands this to derive the KEK.
DEKs are wrapped using AES-KW (RFC 3394) with the KEK. This provides:
- Confidentiality of the DEK
- Integrity protection (built into AES-KW)
- Deterministic output (same input = same output)
- Vault Data: Passwords, secrets, notes stored by the user
- DEK: Data Encryption Key that protects vault data
- Credentials: WebAuthn credential metadata
| Actor | Capability | Motivation |
|---|---|---|
| Remote Attacker | Network access, web exploits | Steal credentials/secrets |
| Local Attacker | Physical device access | Access vault data |
| Malicious Software | Code execution | Exfiltrate data |
| Attack | Mitigation |
|---|---|
| Man-in-the-Middle | All operations are client-side; no network requests for vault data |
| Server Compromise | No server; data stored locally only |
| API Attacks | No backend API to attack |
| Attack | Mitigation |
|---|---|
| XSS | Strict CSP, no innerHTML, Svelte auto-escaping |
| DOM Clobbering | Using crypto.randomUUID() for IDs |
| Prototype Pollution | Careful object handling, schema validation |
| Timing Attacks | Using constant-time crypto operations (Web Crypto API) |
| Attack | Mitigation |
|---|---|
| IndexedDB Inspection | All vault data is AES-256-GCM encrypted |
| Backup Extraction | DEK is wrapped; requires passkey to unwrap |
| Cross-Origin Access | Browser same-origin policy |
| Attack | Mitigation |
|---|---|
| Brute Force KEK | 256-bit key space; infeasible |
| IV Reuse | Fresh random IV per encryption |
| Key Recovery | Keys never stored in plaintext |
| Chosen Plaintext | AES-GCM is CPA-secure |
- Unlocked Vault Access: If the vault is unlocked and an attacker has device access, they can read data
- Malicious Browser Extensions: Extensions with permission can read page content
- Compromised OS: Kernel-level malware can access any process memory
- Physical Authenticator Theft: If someone steals your authenticator AND knows your PIN
- Rubber Hose Cryptanalysis: User coercion is out of scope
{
'default-src': ['self'],
'script-src': ['self'],
'style-src': ['self', 'unsafe-inline'], // Required for Svelte
'img-src': ['self', 'data:', 'blob:'],
'connect-src': ['self'],
'frame-ancestors': ['none'],
'form-action': ['self'],
'base-uri': ['self'],
'object-src': ['none'],
}All data is validated using Zod schemas before processing:
const VaultItemSchema = z.object({
id: z.string().uuid(),
type: z.enum(['note', 'password', 'secret']),
title: z.string().min(1).max(256),
content: z.string().max(65536),
// ...
});Password generation uses rejection sampling to avoid modulo bias:
function generateSecurePassword(length: number): string {
const charset = '...';
const maxValidByte = Math.floor(256 / charset.length) * charset.length;
while (password.length < length) {
const randomBytes = crypto.getRandomValues(new Uint8Array(length * 2));
for (const byte of randomBytes) {
// Reject bytes that would cause bias
if (byte < maxValidByte) {
password += charset[byte % charset.length];
}
}
}
}function lockVault(): void {
currentDEK = null; // Releases reference
state.vault = null;
}The actual memory containing key material is managed by the browser's garbage collector. CryptoKey objects are handled by the Web Crypto implementation, which may use OS-level secure memory.
Each passkey has a unique PRF salt, providing domain separation:
Salt = "vault-prf-v1-" + random(16 bytes, base64url)
This ensures that:
- Different credentials derive different KEKs
- The same authenticator produces different outputs for different vaults
- PRF outputs cannot be correlated across services
IndexedDB storage limits vary by browser and device. Large vaults may encounter storage quota issues.
Not all authenticators support the PRF extension:
- Platform authenticators: Generally supported on recent OS versions
- Security keys: Requires CTAP 2.1 with
hmac-secretextension
Currently no export functionality. If all passkeys are lost, vault data is unrecoverable by design.
No audit trail of access attempts is maintained.
If you discover a security vulnerability, please report it responsibly:
- Do not open a public GitHub issue
- Use GitHub's private vulnerability reporting feature or open a security advisory
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Allow 90 days for remediation before public disclosure
- NIST SP 800-38D — GCM Mode
- NIST SP 800-56C — Key Derivation
- NIST SP 800-57 — Key Management
- RFC 3394 — AES Key Wrap
- WebAuthn Level 3 — PRF Extension