import { useState, useEffect, useCallback, useRef } from 'react'; import { QrCode, RefreshCw, CheckCircle2, Lock, AlertCircle, Camera, Dices, Mic, Unlock, EyeOff, FileKey, Info } from 'lucide-react'; import { PgpKeyInput } from './components/PgpKeyInput'; import { QrDisplay } from './components/QrDisplay'; import QRScanner from './components/QRScanner'; import { validateBip39Mnemonic } from './lib/bip39'; import { buildPlaintext, encryptToSeed, decryptFromSeed, detectEncryptionMode, validatePGPKey } from './lib/seedpgp'; import { encodeStandardSeedQR, encodeCompactSeedQREntropy } from './lib/seedqr'; import { SecurityWarnings } from './components/SecurityWarnings'; import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto'; import { EncryptionMode, EncryptionResult } from './lib/types'; // Import EncryptionMode and EncryptionResult import Header from './components/Header'; import { StorageDetails } from './components/StorageDetails'; import { ClipboardDetails } from './components/ClipboardDetails'; import Footer from './components/Footer'; import { SeedBlender } from './components/SeedBlender'; import CameraEntropy from './components/CameraEntropy'; import DiceEntropy from './components/DiceEntropy'; import RandomOrgEntropy from './components/RandomOrgEntropy'; import { InteractionEntropy } from './lib/interactionEntropy'; import AudioEntropy from './AudioEntropy'; interface StorageItem { key: string; value: string; size: number; isSensitive: boolean; } interface ClipboardEvent { timestamp: Date; field: string; length: number; } function App() { const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender'>('create'); const [mnemonic, setMnemonic] = useState(''); const [backupMessagePassword, setBackupMessagePassword] = useState(''); const [restoreMessagePassword, setRestoreMessagePassword] = useState(''); const [publicKeyInput, setPublicKeyInput] = useState(''); const [privateKeyInput, setPrivateKeyInput] = useState(''); const [privateKeyPassphrase, setPrivateKeyPassphrase] = useState(''); const [hasBip39Passphrase, setHasBip39Passphrase] = useState(false); const [qrPayload, setQrPayload] = useState(''); const [recipientFpr, setRecipientFpr] = useState(''); const [restoreInput, setRestoreInput] = useState(''); const [decryptedRestoredMnemonic, setDecryptedRestoredMnemonic] = useState(null); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [copied, setCopied] = useState(false); const [showQRScanner, setShowQRScanner] = useState(false); const [isDragging, setIsDragging] = useState(false); const [isReadOnly, setIsReadOnly] = useState(false); const [_encryptedMnemonicCache, _setEncryptedMnemonicCache] = useState(null); const [showSecurityModal, setShowSecurityModal] = useState(false); const [showStorageModal, setShowStorageModal] = useState(false); const [showClipboardModal, setShowClipboardModal] = useState(false); const [localItems, setLocalItems] = useState([]); const [sessionItems, setSessionItems] = useState([]); const [clipboardEvents, setClipboardEvents] = useState([]); const [showLockConfirm, setShowLockConfirm] = useState(false); const [resetCounter, setResetCounter] = useState(0); // Krux integration state const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp'); const [seedQrFormat, setSeedQrFormat] = useState<'standard' | 'compact'>('standard'); const [generatedSeed, setGeneratedSeed] = useState(''); const [seedWordCount, setSeedWordCount] = useState<12 | 24>(24); const [seedDestination, setSeedDestination] = useState<'backup' | 'seedblender'>('backup'); const [detectedMode, setDetectedMode] = useState(null); const [seedForBlender, setSeedForBlender] = useState(''); const [blenderResetKey, setBlenderResetKey] = useState(0); // Network blocking state const [isNetworkBlocked, setIsNetworkBlocked] = useState(false); // Entropy generation states const [entropySource, setEntropySource] = useState<'camera' | 'dice' | 'audio' | 'randomorg' | null>(null); const [entropyStats, setEntropyStats] = useState(null); const interactionEntropyRef = useRef(new InteractionEntropy()); const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password']; const isSensitiveKey = (key: string): boolean => { const lowerKey = key.toLowerCase(); return SENSITIVE_PATTERNS.some(pattern => lowerKey.includes(pattern)); }; const getStorageItems = (storage: Storage): StorageItem[] => { const items: StorageItem[] = []; for (let i = 0; i < storage.length; i++) { const key = storage.key(i); if (key) { const value = storage.getItem(key) || ''; items.push({ key, value: value.substring(0, 50) + (value.length > 50 ? '...' : ''), size: new Blob([value]).size, isSensitive: isSensitiveKey(key) }); } } return items.sort((a, b) => (b.isSensitive ? 1 : 0) - (a.isSensitive ? 1 : 0)); }; const refreshStorage = () => { setLocalItems(getStorageItems(localStorage)); setSessionItems(getStorageItems(sessionStorage)); }; useEffect(() => { refreshStorage(); const interval = setInterval(refreshStorage, 2000); return () => clearInterval(interval); }, []); // Cleanup session key on component unmount useEffect(() => { return () => { destroySessionKey(); }; }, []); useEffect(() => { const handleCopy = (e: ClipboardEvent & Event) => { const target = e.target as HTMLElement; // Get selection to measure length const selection = window.getSelection()?.toString() || ''; const length = selection.length; if (length === 0) return; // Nothing copied // Detect field name let field = 'Unknown field'; // Check for data-sensitive attribute on any element first const sensitiveAttr = target.getAttribute('data-sensitive') || target.closest('[data-sensitive]')?.getAttribute('data-sensitive'); if (sensitiveAttr) { field = sensitiveAttr; } else if (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT') { // Try multiple ways to identify the field for legacy inputs field = target.getAttribute('aria-label') || target.getAttribute('name') || target.getAttribute('id') || (target as HTMLInputElement).type || target.tagName.toLowerCase(); // Check parent labels const label = target.closest('label') || document.querySelector(`label[for="${target.id}"]`); if (label) { field = label.textContent?.trim() || field; } // Detect if it looks like sensitive data const isSensitive = /mnemonic|seed|key|private|password|secret/i.test( target.className + ' ' + field + ' ' + (target.getAttribute('placeholder') || '') ); if (isSensitive && field === target.tagName.toLowerCase()) { // Try to guess from placeholder const placeholder = target.getAttribute('placeholder'); if (placeholder) { field = placeholder.substring(0, 40) + '...'; } } } setClipboardEvents(prev => [ { timestamp: new Date(), field, length }, ...prev.slice(0, 9) // Keep last 10 events ]); }; document.addEventListener('copy', handleCopy as EventListener); return () => document.removeEventListener('copy', handleCopy as EventListener); }, []); // Detect encryption mode from restore input useEffect(() => { if (activeTab === 'restore' && restoreInput.trim()) { const detected = detectEncryptionMode(restoreInput); setDetectedMode(detected); // Auto-switch encryption mode if not already set AND it's an encrypted type if ((detected === 'pgp' || detected === 'krux') && detected !== encryptionMode) { setEncryptionMode(detected); } } else { setDetectedMode(null); } }, [restoreInput, activeTab, encryptionMode]); const clearClipboard = async () => { try { // Actually clear the system clipboard await navigator.clipboard.writeText(''); // Clear history setClipboardEvents([]); // Show success briefly alert('✅ Clipboard cleared and history wiped'); } catch (err) { // Fallback for browsers that don't support clipboard API const dummy = document.createElement('textarea'); dummy.value = ''; document.body.appendChild(dummy); dummy.select(); document.execCommand('copy'); document.body.removeChild(dummy); setClipboardEvents([]); alert('✅ History cleared (clipboard may require manual clearing)'); } }; 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(''); try { await navigator.clipboard.writeText(textToCopy); setCopied(true); // Add warning for sensitive data const isSensitive = fieldName.toLowerCase().includes('mnemonic') || fieldName.toLowerCase().includes('seed') || fieldName.toLowerCase().includes('password') || fieldName.toLowerCase().includes('key'); if (isSensitive) { setClipboardEvents(prev => [ { timestamp: new Date(), field: `${fieldName} (will clear in 10s)`, length: textToCopy.length }, ...prev.slice(0, 9) ]); // Auto-clear clipboard after 10 seconds by writing random data setTimeout(async () => { try { const garbage = crypto.getRandomValues(new Uint8Array(Math.max(textToCopy.length, 64))) .reduce((s, b) => s + String.fromCharCode(32 + (b % 95)), ''); await navigator.clipboard.writeText(garbage); } catch { } }, 10000); // Show warning alert(`⚠️ ${fieldName} copied to clipboard!\n\n✅ Will auto-clear in 10 seconds.\n\n🔒 Warning: Clipboard is accessible to other apps and browser extensions.`); } window.setTimeout(() => setCopied(false), 1500); } catch { const ta = document.createElement("textarea"); ta.value = textToCopy; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.focus(); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); setCopied(true); window.setTimeout(() => setCopied(false), 1500); } }; // Handler for entropy generation const handleEntropyGenerated = (mnemonic: string, stats: any) => { setGeneratedSeed(mnemonic); setEntropyStats(stats); }; // Handler for sending to destination const handleSendToDestination = () => { if (seedDestination === 'backup') { setMnemonic(generatedSeed); setActiveTab('backup'); } else if (seedDestination === 'seedblender') { setSeedForBlender(generatedSeed); setActiveTab('seedblender'); } // Reset Create tab setGeneratedSeed(''); setEntropySource(null); setEntropyStats(null); }; const handleBackup = async () => { setLoading(true); setError(''); setQrPayload(''); setRecipientFpr(''); try { const validation = await validateBip39Mnemonic(mnemonic); if (!validation.valid) { throw new Error(validation.error); } const plaintext = buildPlaintext(mnemonic, hasBip39Passphrase); let result: EncryptionResult; if (encryptionMode === 'seedqr') { if (seedQrFormat === 'standard') { const qrString = await encodeStandardSeedQR(mnemonic); result = { framed: qrString }; } else { // compact const qrEntropy = await encodeCompactSeedQREntropy(mnemonic); result = { framed: qrEntropy }; // framed will hold the Uint8Array } } else { // Validate PGP public key before encryption if (publicKeyInput) { const validation = await validatePGPKey(publicKeyInput); if (!validation.valid) { throw new Error(`PGP Key Validation Failed: ${validation.error}`); } } // Encrypt with PGP or Krux result = await encryptToSeed({ plaintext, publicKeyArmored: publicKeyInput || undefined, messagePassword: backupMessagePassword || undefined, mode: encryptionMode, }); } setQrPayload(result.framed); if (result.recipientFingerprint) { setRecipientFpr(result.recipientFingerprint); } // Initialize session key before encrypting await getSessionKey(); // Encrypt mnemonic with session key and clear plaintext state const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() }); _setEncryptedMnemonicCache(blob); setMnemonic(''); // Clear plaintext mnemonic // Clear password after successful encryption (security best practice) setBackupMessagePassword(''); setPrivateKeyPassphrase(''); // Also clear PGP passphrase if used } catch (e) { setError(e instanceof Error ? e.message : 'Encryption failed'); } finally { setLoading(false); } }; const handleFileUpload = async (file: File) => { setLoading(true); setError(''); try { // Handle image files (QR codes) if (file.type.startsWith('image/')) { const img = new Image(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = URL.createObjectURL(file); }); canvas.width = img.width; canvas.height = img.height; ctx?.drawImage(img, 0, 0); const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height); if (!imageData) throw new Error('Could not read image'); const jsqr = (await import('jsqr')).default; const code = jsqr(imageData.data, imageData.width, imageData.height); if (!code) throw new Error('No QR code found in image'); // Handle binary or text QR data const isBinary = (code.binaryData.length === 16 || code.binaryData.length === 32); if (isBinary) { const hex = Array.from(code.binaryData).map(b => b.toString(16).padStart(2, '0')).join(''); setRestoreInput(hex); } else { setRestoreInput(code.data); } URL.revokeObjectURL(img.src); } // Handle text files (PGP armor, hex, numeric SeedQR) else if (file.type === 'text/plain' || file.name.endsWith('.txt') || file.name.endsWith('.asc')) { const text = await file.text(); setRestoreInput(text.trim()); } else { throw new Error('Unsupported file type. Use PNG/JPG (QR) or TXT/ASC (armor)'); } } catch (err) { setError(err instanceof Error ? err.message : 'File upload failed'); } finally { setLoading(false); } }; const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; // Access the first file if (file) await handleFileUpload(file); }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }; const handleDragLeave = () => { setIsDragging(false); }; const handleRestore = async () => { setLoading(true); setError(''); setDecryptedRestoredMnemonic(null); try { // Auto-detect mode if not manually set const modeToUse = detectedMode || encryptionMode; const result = await decryptFromSeed({ frameText: restoreInput, privateKeyArmored: privateKeyInput || undefined, privateKeyPassphrase: privateKeyPassphrase || undefined, messagePassword: restoreMessagePassword || undefined, mode: modeToUse, }); // Encrypt the restored mnemonic with the session key await getSessionKey(); const blob = await encryptJsonToBlob({ mnemonic: result.w, timestamp: Date.now() }); _setEncryptedMnemonicCache(blob); // Temporarily display the mnemonic and then clear it setDecryptedRestoredMnemonic(result.w); // Clear passwords after successful decryption (security best practice) setRestoreMessagePassword(''); setPrivateKeyPassphrase(''); // Also clear restore input after successful decrypt setRestoreInput(''); setTimeout(() => { setDecryptedRestoredMnemonic(null); }, 10000); // Auto-clear after 10 seconds } catch (e) { setError(e instanceof Error ? e.message : 'Decryption failed'); } finally { setLoading(false); } }; const blockAllNetworks = () => { // 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 as any).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 (navigator as any).sendBeacon = () => { 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 resource`); } 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 = () => { // 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; }; const handleToggleNetwork = () => { setIsNetworkBlocked(!isNetworkBlocked); if (!isNetworkBlocked) { blockAllNetworks(); } else { unblockAllNetworks(); } }; const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender') => { // Allow free navigation - no warnings // User can manually reset Seed Blender with "Reset All" button setActiveTab(newTab); }; const handleResetAll = () => { if (window.confirm('⚠️ Reset ALL data? This will clear everything including any displayed entropy analysis.')) { // Clear component state setMnemonic(''); setGeneratedSeed(''); setBackupMessagePassword(''); setRestoreMessagePassword(''); setPublicKeyInput(''); setPrivateKeyInput(''); setPrivateKeyPassphrase(''); setQrPayload(''); setRecipientFpr(''); setRestoreInput(''); setDecryptedRestoredMnemonic(null); setError(''); setEntropySource(null); setEntropyStats(null); setSeedForBlender(''); // Clear storage and session localStorage.clear(); sessionStorage.clear(); destroySessionKey(); _setEncryptedMnemonicCache(null); // Force remount of key-driven components setResetCounter(prev => prev + 1); setBlenderResetKey(prev => prev + 1); // Go to Create tab (fresh start) setActiveTab('create'); } }; const handleRestoreScanSuccess = useCallback((scannedData: string | Uint8Array) => { if (typeof scannedData === 'string') { setRestoreInput(scannedData); } else { const hex = Array.from(scannedData).map(b => b.toString(16).padStart(2, '0')).join(''); setRestoreInput(hex); } setShowQRScanner(false); setError(''); }, []); // Empty dependency array means this function is stable const handleRestoreClose = useCallback(() => { setShowQRScanner(false); }, []); const handleRestoreError = useCallback((err: string) => { setError(err); setShowQRScanner(false); }, []); return (
{/* Cyberpunk grid overlay */}
{/* Content wrapper */}
setShowSecurityModal(true)} localItems={localItems} sessionItems={sessionItems} onOpenStorageModal={() => setShowStorageModal(true)} events={clipboardEvents} onOpenClipboardModal={() => setShowClipboardModal(true)} activeTab={activeTab} onRequestTabChange={handleRequestTabChange} appVersion={__APP_VERSION__} isNetworkBlocked={isNetworkBlocked} onToggleNetwork={handleToggleNetwork} onResetAll={handleResetAll} />
{/* Error Display */} {error && (

Error

{error}

)} {/* Info Banner */} {recipientFpr && activeTab === 'backup' && (
Recipient Key: {recipientFpr}
)} {/* Main Content Grid */}
{activeTab === 'create' && (
{/* Seed Length Selector */}
{/* Entropy Source Selection */} {!entropySource && !generatedSeed && (

Choose Entropy Source

All methods enhanced with mouse/keyboard timing + browser crypto

{entropyStats && (
Last entropy generated: {entropyStats.totalBits} bits
)}

Privacy: All processing happens locally in your browser. Images/audio never stored or transmitted. This app is 100% stateless.

)} {/* Camera Entropy Component */} {entropySource === 'camera' && !generatedSeed && ( setEntropySource(null)} interactionEntropy={interactionEntropyRef.current} /> )} {/* Dice Entropy Component */} {entropySource === 'dice' && !generatedSeed && ( setEntropySource(null)} interactionEntropy={interactionEntropyRef.current} /> )} {/* Audio Entropy Component - TODO */} {entropySource === 'audio' && !generatedSeed && (

Audio entropy coming soon...

)} {/* Audio Entropy Component */} {entropySource === 'audio' && !generatedSeed && ( setEntropySource(null)} interactionEntropy={interactionEntropyRef.current} /> )} {/* Random.org Entropy Component */} {entropySource === 'randomorg' && !generatedSeed && ( setEntropySource(null)} interactionEntropy={interactionEntropyRef.current} /> )} {/* Generated Seed Display + Destination Selector */} {generatedSeed && (
Generated Successfully

{generatedSeed}

👆 Hover to reveal - Write down securely

{/* Destination Selector */}
{/* Send Button */}
)}
)}