SecureVault implements a zero-knowledge architecture with client-side encryption using industry-standard cryptographic algorithms.
Encryption Overview
All sensitive data is encrypted using AES-256-GCM before leaving the client.
AES-256-GCM
Algorithm AES-256-GCM (Galois/Counter Mode)
Key Length 256 bits (32 bytes)
IV Length 96 bits (12 bytes)
Auth Tag 128 bits (16 bytes)
Why AES-256-GCM?
GCM mode provides both confidentiality and authenticity. It prevents tampering by including an authentication tag that verifies data integrity.
GCM mode is highly efficient and can be parallelized, making it fast even for large amounts of data.
AES-256-GCM is recommended by NIST and used by major security applications including 1Password and Bitwarden.
Native support in Web Crypto API without external dependencies.
Key Derivation
SecureVault uses PBKDF2 to derive cryptographic keys from the user’s master password.
PBKDF2 Configuration
/home/daytona/workspace/source/apps/secure/src/lib/crypto/client.ts
const PBKDF2_ITERATIONS = 600000 ; // OWASP 2023 recommendation
const KEY_LENGTH = 256 ; // bits
const HASH_ALGORITHM = 'SHA-256' ;
Dual Key Derivation
Two separate keys are derived for different purposes:
Generate Salts
Create separate salts for authentication and encryption: const encoder = new TextEncoder ();
// Authentication salt
const authSalt = await crypto . subtle . digest (
'SHA-256' ,
encoder . encode ( salt + ':auth' )
);
// Encryption salt
const encSalt = await crypto . subtle . digest (
'SHA-256' ,
encoder . encode ( salt + ':enc' )
);
Derive Authentication Key
Used for server authentication: const authKey = await crypto . subtle . deriveKey (
{
name: 'PBKDF2' ,
salt: new Uint8Array ( authSalt ),
iterations: PBKDF2_ITERATIONS ,
hash: 'SHA-256'
},
keyMaterial ,
{ name: 'HMAC' , hash: 'SHA-256' , length: KEY_LENGTH },
true ,
[ 'sign' ]
);
Derive Encryption Key
Used for vault encryption: const encryptionKey = await crypto . subtle . deriveKey (
{
name: 'PBKDF2' ,
salt: new Uint8Array ( encSalt ),
iterations: PBKDF2_ITERATIONS ,
hash: 'SHA-256'
},
keyMaterial ,
{ name: 'AES-GCM' , length: KEY_LENGTH },
true ,
[ 'encrypt' , 'decrypt' ]
);
Generate Auth Hash
Create hash to send to server: const authHashBuffer = await crypto . subtle . sign (
'HMAC' ,
authKey ,
saltBuffer
);
const authHash = arrayBufferToHex ( authHashBuffer );
Key Hierarchy
Encryption Process
Encrypting Data
Client Implementation
Encrypt Object
import { encrypt } from '@/lib/crypto/client' ;
export async function encrypt (
plaintext : string ,
key : CryptoKey
) : Promise < EncryptedData > {
const encoder = new TextEncoder ();
const data = encoder . encode ( plaintext );
// Generate random IV (12 bytes for GCM)
const iv = crypto . getRandomValues ( new Uint8Array ( 12 ));
// Encrypt with AES-256-GCM
const ciphertext = await crypto . subtle . encrypt (
{
name: 'AES-GCM' ,
iv
},
key ,
data
);
return {
ciphertext: arrayBufferToBase64 ( ciphertext ),
iv: arrayBufferToBase64 ( iv . buffer ),
version: 1
};
}
Decrypting Data
Decrypt Function
Decrypt Object
export async function decrypt (
encryptedData : EncryptedData ,
key : CryptoKey
) : Promise < string > {
const ciphertext = base64ToArrayBuffer ( encryptedData . ciphertext );
const iv = base64ToArrayBuffer ( encryptedData . iv );
// Decrypt with AES-256-GCM
const decrypted = await crypto . subtle . decrypt (
{
name: 'AES-GCM' ,
iv: new Uint8Array ( iv )
},
key ,
ciphertext
);
const decoder = new TextDecoder ();
return decoder . decode ( decrypted );
}
Server-Side Security
While the client performs encryption, the server adds additional security layers.
Auth Hash Verification
The server uses bcrypt to hash the authentication hash received from the client:
/home/daytona/workspace/source/apps/secure/src/lib/crypto/server.ts
import { hash , compare } from 'bcryptjs' ;
const SALT_ROUNDS = 12 ;
/**
* Hash the auth hash with bcrypt
* Adds server-side security layer
*/
export async function hashAuthHash ( authHash : string ) : Promise < string > {
return hash ( authHash , SALT_ROUNDS );
}
/**
* Verify client's auth hash
*/
export async function verifyAuthHash (
authHash : string ,
storedHash : string
) : Promise < boolean > {
return compare ( authHash , storedHash );
}
Double Hashing
Why hash the hash?
Even if the database is compromised, attackers cannot authenticate without the original master password. The bcrypt hash adds computational difficulty.
JWT Token Security
Generate Access Token
Generate Refresh Token
Verify Token
import { SignJWT } from 'jose' ;
const ACCESS_TOKEN_EXPIRY = '15m' ;
export async function generateAccessToken (
userId : string ,
email : string
) : Promise < string > {
return new SignJWT ({
sub: userId ,
email ,
type: 'access'
})
. setProtectedHeader ({ alg: 'HS256' })
. setIssuedAt ()
. setExpirationTime ( ACCESS_TOKEN_EXPIRY )
. sign ( getJwtSecretKey ());
}
Data Storage
What gets stored and where:
Client Storage (Browser)
sessionStorage
localStorage
Memory Only
Encryption Key (temporary)
Stored only while vault is unlocked
Cleared on browser close
Never persisted to disk
// Store encryption key in session
const keyString = await crypto . subtle . exportKey ( 'raw' , encryptionKey );
sessionStorage . setItem ( 'vaultKey' , arrayBufferToBase64 ( keyString ));
// Retrieve encryption key
const storedKey = sessionStorage . getItem ( 'vaultKey' );
const keyBuffer = base64ToArrayBuffer ( storedKey );
const encryptionKey = await crypto . subtle . importKey (
'raw' ,
keyBuffer ,
{ name: 'AES-GCM' , length: 256 },
true ,
[ 'encrypt' , 'decrypt' ]
);
User Preferences (non-sensitive)
Theme settings
View mode (grid/list)
UI preferences
localStorage . setItem ( 'theme' , 'dark' );
localStorage . setItem ( 'viewMode' , 'grid' );
Decrypted Passwords
Kept in React state
Never written to storage
Cleared on logout
const [ passwords , setPasswords ] = useState < DecryptedPasswordEntry []>([]);
Server Storage (MongoDB)
// What the server stores (from apps/secure/src/lib/db/models.ts)
const passwordSchema = new Schema ({
userId: { type: Schema . Types . ObjectId , required: true },
// Encrypted blob - server cannot decrypt
encryptedData: { type: String , required: true },
iv: { type: String , required: true },
// Metadata - not encrypted
categoryId: Schema . Types . ObjectId ,
tags: [ String ],
favorite: Boolean ,
// Security analysis
passwordStrength: { type: Number , min: 0 , max: 4 },
isCompromised: Boolean ,
isReused: Boolean ,
// Timestamps
createdAt: Date ,
updatedAt: Date ,
lastUsedAt: Date ,
passwordChangedAt: Date ,
deletedAt: Date ,
encryptionVersion: { type: Number , default: 1 }
});
Security Best Practices
All API inputs are validated using Zod schemas:
/home/daytona/workspace/source/apps/secure/src/lib/validations.ts
import { z } from 'zod' ;
export const createPasswordSchema = z . object ({
encryptedData: z . string (). min ( 1 , 'Encrypted data required' ),
iv: z . string (). min ( 1 , 'IV required' ),
metadata: z . object ({
categoryId: z . string (). optional (),
tags: z . array ( z . string (). max ( 50 )). max ( 20 ),
favorite: z . boolean (),
passwordStrength: z . union ([
z . literal ( 0 ),
z . literal ( 1 ),
z . literal ( 2 ),
z . literal ( 3 ),
z . literal ( 4 )
]),
isCompromised: z . boolean (),
isReused: z . boolean ()
})
});
HTTPS Only
All communication uses TLS 1.3:
const securityHeaders = {
'Strict-Transport-Security' : 'max-age=31536000; includeSubDomains; preload' ,
'Content-Security-Policy' : "default-src 'self';" ,
'X-Content-Type-Options' : 'nosniff' ,
'X-Frame-Options' : 'DENY' ,
'X-XSS-Protection' : '1; mode=block'
};
Session Management
Short-lived access tokens : 15 minutes
Longer refresh tokens : 7 days
Auto-lock on inactivity : Configurable (1-60 minutes)
Token rotation : New tokens on refresh
Threat Model
What SecureVault Protects Against
Server Breach Even if the server is compromised, vault data remains encrypted
Network Sniffing HTTPS encryption protects data in transit
Database Theft Stolen database contains only encrypted data
Insider Threats Zero-knowledge architecture prevents admin access to passwords
Limitations
Client-Side Attacks : SecureVault cannot protect against:
Compromised client device
Keyloggers or screen capture malware
Browser extensions with malicious code
XSS attacks (mitigated by CSP)
Security Auditing
Audit Logs
All security-relevant actions are logged:
type AuditAction =
| 'login'
| 'logout'
| 'login_failed'
| 'mfa_enabled'
| 'password_created'
| 'password_updated'
| 'password_deleted'
| 'password_viewed'
| 'export_requested' ;
interface AuditLog {
userId : string ;
action : AuditAction ;
ipAddress : string ;
userAgent : string ;
timestamp : Date ;
metadata : Record < string , unknown >;
}
Next Steps
Password Management Learn CRUD operations with encryption
Categories & Tags Organize encrypted passwords