// Client-side PGP utilities with encrypted storage const DB_NAME = 'pgp_storage'; const DB_VERSION = 1; const STORE_NAME = 'encrypted_keys'; // Rate limiting for unlock attempts let unlockAttempts = 0; let lockoutUntil = null; const MAX_ATTEMPTS = 5; const LOCKOUT_DURATION = 30 * 60 * 1000; // 30 minutes // Initialize IndexedDB async function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id' }); } }; }); } // Derive key from passphrase using PBKDF2 async function deriveKey(passphrase, salt) { const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', enc.encode(passphrase), 'PBKDF2', false, ['deriveBits', 'deriveKey'] ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); } // Validate passphrase strength export function validatePassphrase(passphrase) { if (!passphrase || passphrase.length < 16) { return 'Passphrase must be at least 16 characters'; } // Check for complexity const hasUpper = /[A-Z]/.test(passphrase); const hasLower = /[a-z]/.test(passphrase); const hasNumber = /[0-9]/.test(passphrase); const hasSpecial = /[!@#$%^&*(),.?":{}|<>\-_=+\[\]\\;'`~]/.test(passphrase); const complexity = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length; // Require all 4 types, OR 20+ characters with 3 types if (complexity < 4 && !(passphrase.length >= 20 && complexity >= 3)) { return 'Passphrase must contain uppercase, lowercase, numbers, AND special characters (or be 20+ chars with 3 types)'; } return null; // Valid } export async function generateKeyPair(username, passphrase) { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } // Validate passphrase const passphraseError = validatePassphrase(passphrase); if (passphraseError) { throw new Error(passphraseError); } const { generateKey, readKey } = await import('openpgp'); const { privateKey, publicKey } = await generateKey({ type: 'rsa', rsaBits: 2048, userIDs: [{ name: username }], passphrase // Always encrypt with passphrase }); const key = await readKey({ armoredKey: publicKey }); const fingerprint = key.getFingerprint(); return { privateKey, publicKey, fingerprint }; } export async function getFingerprint(publicKey) { if (typeof window === 'undefined') return null; try { const { readKey } = await import('openpgp'); const key = await readKey({ armoredKey: publicKey }); return key.getFingerprint(); } catch (error) { console.error('Error getting fingerprint:', error); return null; } } // Save encrypted private key to IndexedDB export async function saveEncryptedPrivateKey(passphrase, armoredKey) { if (typeof window === 'undefined') { throw new Error('Storage operations can only be performed in the browser'); } // Validate passphrase const passphraseError = validatePassphrase(passphrase); if (passphraseError) { throw new Error(passphraseError); } try { // Generate random salt and IV const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); // Derive encryption key const key = await deriveKey(passphrase, salt); // Encrypt the private key const enc = new TextEncoder(); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, key, enc.encode(armoredKey) ); // Store in IndexedDB const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); await store.put({ id: 'primary_key', salt: Array.from(salt), iv: Array.from(iv), encrypted: Array.from(new Uint8Array(encrypted)), timestamp: Date.now() }); return true; } catch (error) { console.error('Failed to save encrypted key:', error); throw new Error('Failed to save encrypted key'); } } // Unlock and retrieve private key from IndexedDB export async function unlockPrivateKey(passphrase) { if (typeof window === 'undefined') { throw new Error('Storage operations can only be performed in the browser'); } // Check lockout if (lockoutUntil && Date.now() < lockoutUntil) { const remaining = Math.ceil((lockoutUntil - Date.now()) / 60000); throw new Error(`Too many failed attempts. Try again in ${remaining} minute${remaining !== 1 ? 's' : ''}.`); } if (!passphrase) { throw new Error('Passphrase required'); } try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const data = await new Promise((resolve, reject) => { const request = store.get('primary_key'); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); if (!data) { throw new Error('No encrypted key found'); } // Reconstruct typed arrays const salt = new Uint8Array(data.salt); const iv = new Uint8Array(data.iv); const encrypted = new Uint8Array(data.encrypted); // Derive decryption key const key = await deriveKey(passphrase, salt); // Decrypt const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, key, encrypted ); // Success - reset attempts unlockAttempts = 0; lockoutUntil = null; const dec = new TextDecoder(); return dec.decode(decrypted); } catch (error) { // Track failed attempts unlockAttempts++; if (unlockAttempts >= MAX_ATTEMPTS) { lockoutUntil = Date.now() + LOCKOUT_DURATION; unlockAttempts = 0; console.error('Too many failed unlock attempts. Locked out for 30 minutes.'); throw new Error('Too many failed attempts. Locked out for 30 minutes.'); } console.error('Failed to unlock private key:', error); throw new Error(`Failed to unlock private key. Check your passphrase. (${MAX_ATTEMPTS - unlockAttempts} attempts remaining)`); } } // Check if an encrypted key exists export async function hasEncryptedKey() { if (typeof window === 'undefined') return false; try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const data = await new Promise((resolve) => { const request = store.get('primary_key'); request.onsuccess = () => resolve(request.result); request.onerror = () => resolve(null); }); return !!data; } catch { return false; } } // Remove encrypted key from IndexedDB export async function removeEncryptedPrivateKey() { if (typeof window === 'undefined') return; try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); await store.delete('primary_key'); } catch (error) { console.error('Failed to remove encrypted key:', error); } } export async function signMessage(message, privateKeyArmored, passphrase) { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } if (!passphrase) { throw new Error('Passphrase required to unlock private key'); } const { decryptKey, readPrivateKey, createMessage, sign } = await import('openpgp'); const privateKey = await decryptKey({ privateKey: await readPrivateKey({ armoredKey: privateKeyArmored }), passphrase }); const unsignedMessage = await createMessage({ text: message }); const signature = await sign({ message: unsignedMessage, signingKeys: privateKey, detached: true }); return signature; } export async function verifySignature(message, signature, publicKeyArmored) { if (typeof window === 'undefined') return false; try { const { readKey, readSignature, createMessage, verify } = await import('openpgp'); const publicKey = await readKey({ armoredKey: publicKeyArmored }); const signatureObj = await readSignature({ armoredSignature: signature }); const messageObj = await createMessage({ text: message }); const verificationResult = await verify({ message: messageObj, signature: signatureObj, verificationKeys: publicKey }); const { verified } = verificationResult.signatures[0]; return await verified; } catch (error) { console.error('Signature verification error:', error); return false; } } // DEPRECATED - DO NOT USE export function storePrivateKey() { throw new Error('Plaintext key storage is disabled for security. Use saveEncryptedPrivateKey() instead.'); } export function getStoredPrivateKey() { throw new Error('Plaintext key storage is disabled for security. Use unlockPrivateKey() instead.'); } export function removeStoredPrivateKey() { throw new Error('Plaintext key storage is disabled for security. Use removeEncryptedPrivateKey() instead.'); }