# Priority Security Patches for SeedPGP This document outlines the critical security patches needed for production deployment. ## PATCH 1: Add Content Security Policy (CSP) **File:** `index.html` Add this meta tag in the ``: ```html ``` **Why:** Prevents malicious extensions and XSS attacks from injecting code that steals seeds. --- ## PATCH 2: Encrypt All Seeds in State (Stop Using Plain Strings) **File:** `src/App.tsx` **Change From:** ```typescript const [mnemonic, setMnemonic] = useState(''); const [backupMessagePassword, setBackupMessagePassword] = useState(''); ``` **Change To:** ```typescript // Only store encrypted reference const [mnemonicEncrypted, setMnemonicEncrypted] = useState(null); const [passwordEncrypted, setPasswordEncrypted] = useState(null); // Use wrapper function to decrypt temporarily when needed async function withMnemonic( callback: (mnemonic: string) => Promise ): Promise { if (!mnemonicEncrypted) return null; const decrypted = await decryptBlobToJson<{ value: string }>(mnemonicEncrypted); try { return await callback(decrypted.value); } finally { // Zero attempt (won't fully work, but good practice) Object.assign(decrypted, { value: '\0'.repeat(decrypted.value.length) }); } } ``` **Why:** Sensitive data stored encrypted in React state, not as plaintext. Prevents memory dumps and malware from easily accessing seeds. --- ## PATCH 3: Implement BIP39 Checksum Validation **File:** `src/lib/bip39.ts` **Replace:** ```typescript export function validateBip39Mnemonic(words: string): { valid: boolean; error?: string } { // ... word count only return { valid: true }; } ``` **With:** ```typescript export async function validateBip39Mnemonic(words: string): Promise<{ valid: boolean; error?: string }> { const normalized = normalizeBip39Mnemonic(words); const arr = normalized.length ? normalized.split(" ") : []; const validCounts = new Set([12, 15, 18, 21, 24]); if (!validCounts.has(arr.length)) { return { valid: false, error: `Invalid word count: ${arr.length}. Must be 12, 15, 18, 21, or 24.`, }; } // āœ… NEW: Verify each word is in wordlist and checksum is valid try { // This will throw if mnemonic is invalid await mnemonicToEntropy(normalized); return { valid: true }; } catch (e) { return { valid: false, error: `Invalid BIP39 mnemonic: ${e instanceof Error ? e.message : 'Unknown error'}` }; } } ``` **Usage Update:** ```typescript // Update all validation calls to use await const validation = await validateBip39Mnemonic(mnemonic); if (!validation.valid) { setError(validation.error); return; } ``` **Why:** Prevents users from backing up invalid seeds or corrupted mnemonics. --- ## PATCH 4: Disable Console Output of Sensitive Data **File:** `src/main.tsx` **Add at top:** ```typescript // Disable all console output in production if (import.meta.env.PROD) { console.log = () => {}; console.error = () => {}; console.warn = () => {}; console.debug = () => {}; } ``` **File:** `src/lib/krux.ts` **Remove:** ```typescript console.log('šŸ” KEF Debug:', { label, iterations, version, length: kef.length, base43: kefBase43.slice(0, 50) }); console.error("Krux decryption internal error:", error); ``` **File:** `src/components/QrDisplay.tsx` **Remove:** ```typescript console.log('šŸŽØ QrDisplay generating QR for:', value); console.log(' - Type:', value instanceof Uint8Array ? 'Uint8Array' : typeof value); console.log(' - Length:', value.length); console.log(' - Hex:', Array.from(value).map(b => b.toString(16).padStart(2, '0')).join('')); ``` **Why:** Prevents seeds from being recoverable via browser history, crash dumps, or remote debugging. --- ## PATCH 5: Secure Clipboard Access **File:** `src/App.tsx` **Replace `copyToClipboard` function:** ```typescript const copyToClipboard = async (text: string | Uint8Array, fieldName = 'Data') => { if (isReadOnly) { setError("Copy to clipboard is disabled in Read-only mode."); return; } const textToCopy = typeof text === 'string' ? text : Array.from(text).map(b => b.toString(16).padStart(2, '0')).join(''); // Mark when copy started const copyStartTime = Date.now(); try { await navigator.clipboard.writeText(textToCopy); setCopied(true); // Add warning for sensitive data if (fieldName.toLowerCase().includes('mnemonic') || fieldName.toLowerCase().includes('seed')) { setClipboardEvents(prev => [ { timestamp: new Date(), field: fieldName, length: textToCopy.length, willClearIn: 10 }, ...prev.slice(0, 9) ]); // Auto-clear clipboard after 10 seconds setTimeout(async () => { try { // Write garbage to obscure previous content (best effort) const garbage = crypto.getRandomValues(new Uint8Array(textToCopy.length)) .reduce((s, b) => s + String.fromCharCode(b), ''); await navigator.clipboard.writeText(garbage); } catch {} }, 10000); // Show warning alert(`āš ļø ${fieldName} copied to clipboard!\n\nāœ… Will auto-clear in 10 seconds.\n\nšŸ”’ Recommend: Use QR codes instead for maximum security.`); } // Always clear the UI state after a moment window.setTimeout(() => setCopied(false), 1500); } catch (err) { // Fallback for browsers that don't support clipboard API const ta = document.createElement("textarea"); ta.value = textToCopy; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); setCopied(true); window.setTimeout(() => setCopied(false), 1500); } }; ``` **Update the textarea field:** ```typescript // When copying mnemonic: onClick={() => copyToClipboard(mnemonic, 'BIP39 Mnemonic (āš ļø Sensitive)')} ``` **Why:** Automatically clears clipboard content and warns users about clipboard exposure. --- ## PATCH 6: Comprehensive Network Blocking **File:** `src/App.tsx` **Replace `handleToggleNetwork` function:** ```typescript const handleToggleNetwork = () => { setIsNetworkBlocked(!isNetworkBlocked); const blockAllNetworks = () => { console.log('🚫 Network BLOCKED - All external requests disabled'); // Store originals (window as any).__originalFetch = window.fetch; (window as any).__originalXHR = window.XMLHttpRequest; (window as any).__originalWS = window.WebSocket; (window as any).__originalImage = window.Image; if (navigator.sendBeacon) { (window as any).__originalBeacon = navigator.sendBeacon; } // 1. Block fetch window.fetch = (async () => Promise.reject(new Error('Network blocked by user')) ) as any; // 2. Block XMLHttpRequest window.XMLHttpRequest = new Proxy(XMLHttpRequest, { construct() { throw new Error('Network blocked: XMLHttpRequest not allowed'); } }) as any; // 3. Block WebSocket window.WebSocket = new Proxy(WebSocket, { construct() { throw new Error('Network blocked: WebSocket not allowed'); } }) as any; // 4. Block BeaconAPI if (navigator.sendBeacon) { (navigator as any).sendBeacon = () => { console.error('Network blocked: sendBeacon not allowed'); return false; }; } // 5. Block Image src for external resources const OriginalImage = window.Image; window.Image = new Proxy(OriginalImage, { construct(target) { const img = Reflect.construct(target, []); const originalSrcSetter = Object.getOwnPropertyDescriptor( HTMLImageElement.prototype, 'src' )?.set; Object.defineProperty(img, 'src', { configurable: true, set(value) { if (value && !value.startsWith('data:') && !value.startsWith('blob:')) { throw new Error(`Network blocked: cannot load external image [${value}]`); } originalSrcSetter?.call(this, value); }, get: Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src')?.get }); return img; } }) as any; // 6. Block Service Workers if (navigator.serviceWorker) { (navigator.serviceWorker as any).register = async () => { throw new Error('Network blocked: Service Workers disabled'); }; } }; const unblockAllNetworks = () => { console.log('🌐 Network ACTIVE - All requests allowed'); // Restore everything if ((window as any).__originalFetch) window.fetch = (window as any).__originalFetch; if ((window as any).__originalXHR) window.XMLHttpRequest = (window as any).__originalXHR; if ((window as any).__originalWS) window.WebSocket = (window as any).__originalWS; if ((window as any).__originalImage) window.Image = (window as any).__originalImage; if ((window as any).__originalBeacon) navigator.sendBeacon = (window as any).__originalBeacon; }; if (!isNetworkBlocked) { blockAllNetworks(); } else { unblockAllNetworks(); } }; ``` **Why:** Comprehensively blocks all network APIs, not just fetch(), preventing seed exfiltration. --- ## PATCH 7: Validate PGP Keys **File:** `src/lib/seedpgp.ts` **Add new function:** ```typescript export async function validatePGPKey(armoredKey: string): Promise<{ valid: boolean; error?: string; fingerprint?: string; keySize?: number; expirationDate?: Date; }> { try { const key = await openpgp.readKey({ armoredKey }); // 1. Verify encryption capability try { await key.getEncryptionKey(); } catch { return { valid: false, error: "Key has no encryption subkey" }; } // 2. Check key expiration const expirationTime = await key.getExpirationTime(); if (expirationTime && expirationTime < new Date()) { return { valid: false, error: "Key has expired" }; } // 3. Check key strength (try to extract key size) let keySize = 0; try { const mainKey = key.primaryKey as any; if (mainKey.getBitSize) { keySize = mainKey.getBitSize(); } if (keySize > 0 && keySize < 2048) { return { valid: false, error: `Key too small (${keySize} bits). Minimum 2048.` }; } } catch (e) { // Unable to determine key size, but continue } // 4. Verify primary key can encrypt const result = await key.verifyPrimaryKey(); if (result.status !== 'valid') { return { valid: false, error: "Key has invalid signature" }; } return { valid: true, fingerprint: key.getFingerprint().toUpperCase(), keySize: keySize || undefined, expirationDate: expirationTime || undefined }; } catch (e) { return { valid: false, error: `Failed to parse key: ${e instanceof Error ? e.message : 'Unknown error'}` }; } } ``` **Use before encrypting:** ```typescript if (publicKeyInput) { const validation = await validatePGPKey(publicKeyInput); if (!validation.valid) { setError(validation.error); return; } // Show fingerprint to user for verification setRecipientFpr(validation.fingerprint || ''); } ``` **Why:** Ensures only valid, strong PGP keys are used for encryption. --- ## PATCH 8: Add Key Rotation **File:** `src/lib/sessionCrypto.ts` **Add rotation logic:** ```typescript let sessionKey: CryptoKey | null = null; let keyCreatedAt = 0; const KEY_ROTATION_INTERVAL = 5 * 60 * 1000; // 5 minutes const MAX_KEY_OPERATIONS = 1000; // Rotate after N operations let keyOperationCount = 0; export async function getSessionKey(): Promise { const now = Date.now(); const shouldRotate = !sessionKey || (now - keyCreatedAt) > KEY_ROTATION_INTERVAL || keyOperationCount > MAX_KEY_OPERATIONS; if (shouldRotate) { if (sessionKey) { // Log key rotation (no sensitive data) console.debug(`Rotating session key (age: ${now - keyCreatedAt}ms, ops: ${keyOperationCount})`); destroySessionKey(); } const key = await window.crypto.subtle.generateKey( { name: KEY_ALGORITHM, length: KEY_LENGTH, }, false, ['encrypt', 'decrypt'], ); sessionKey = key; keyCreatedAt = now; keyOperationCount = 0; } return sessionKey; } export async function encryptJsonToBlob(data: T): Promise { keyOperationCount++; // ... rest of function } // Auto-clear on visibility change document.addEventListener('visibilitychange', () => { if (document.hidden) { console.debug('Page hidden - clearing session key'); destroySessionKey(); } }); ``` **Why:** Limits the time and operations a single session key is used, reducing risk of key compromise. --- ## Deployment Checklist - [ ] Add CSP meta tag to index.html - [ ] Encrypt all sensitive strings in state (use EncryptedBlob) - [ ] Implement BIP39 checksum validation with await - [ ] Disable console.log/error/warn in production - [ ] Update copyToClipboard to auto-clear and warn - [ ] Implement comprehensive network blocking - [ ] Add PGP key validation - [ ] Add session key rotation - [ ] Run full test suite - [ ] Test in offline mode (Tails OS) - [ ] Test with hardware wallets (Krux, Coldcard) - [ ] Security review of all changes - [ ] Deploy to staging - [ ] Final audit - [ ] Deploy to production --- **Note:** These patches should be reviewed and tested thoroughly before production deployment. Consider having security auditor review changes before release.