import { useState, useEffect } from 'react'; import { QrCode, RefreshCw, CheckCircle2, Lock, AlertCircle, 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 } from './lib/seedpgp'; import * as openpgp from 'openpgp'; import { SecurityWarnings } from './components/SecurityWarnings'; import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto'; 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'; console.log("OpenPGP.js version:", openpgp.config.versionString); interface StorageItem { key: string; value: string; size: number; isSensitive: boolean; } interface ClipboardEvent { timestamp: Date; field: string; length: number; } function App() { const [activeTab, setActiveTab] = useState<'backup' | 'restore' | 'seedblender'>('backup'); 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 [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); // Krux integration state const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux'>('pgp'); const [kruxLabel, setKruxLabel] = useState('Seed Backup'); const [kruxIterations, setKruxIterations] = useState(200000); const [detectedMode, setDetectedMode] = useState<'pgp' | 'krux' | null>(null); 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'; if (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT') { // Try multiple ways to identify the field 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; } // Check for data-sensitive attribute const sensitiveAttr = target.getAttribute('data-sensitive') || target.closest('[data-sensitive]')?.getAttribute('data-sensitive'); if (sensitiveAttr) { field = sensitiveAttr; } // 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 mode if not already set if (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) => { if (isReadOnly) { setError("Copy to clipboard is disabled in Read-only mode."); return; } try { await navigator.clipboard.writeText(text); setCopied(true); window.setTimeout(() => setCopied(false), 1500); } catch { const ta = document.createElement("textarea"); ta.value = text; 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); } }; const handleBackup = async () => { setLoading(true); setError(''); setQrPayload(''); setRecipientFpr(''); try { const validation = validateBip39Mnemonic(mnemonic); if (!validation.valid) { throw new Error(validation.error); } const plaintext = buildPlaintext(mnemonic, hasBip39Passphrase); const result = await encryptToSeed({ plaintext, publicKeyArmored: publicKeyInput || undefined, messagePassword: backupMessagePassword || undefined, mode: encryptionMode, kruxLabel: encryptionMode === 'krux' ? kruxLabel : undefined, kruxIterations: encryptionMode === 'krux' ? kruxIterations : undefined, }); 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 } catch (e) { setError(e instanceof Error ? e.message : 'Encryption failed'); } finally { setLoading(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); setTimeout(() => { setDecryptedRestoredMnemonic(null); }, 10000); // Auto-clear after 10 seconds } catch (e) { setError(e instanceof Error ? e.message : 'Decryption failed'); } finally { setLoading(false); } }; const handleLockAndClear = () => { destroySessionKey(); setEncryptedMnemonicCache(null); setMnemonic(''); setBackupMessagePassword(''); setRestoreMessagePassword(''); setPublicKeyInput(''); setPrivateKeyInput(''); setPrivateKeyPassphrase(''); setQrPayload(''); setRecipientFpr(''); setRestoreInput(''); setDecryptedRestoredMnemonic(null); setError(''); setCopied(false); setShowQRScanner(false); }; const handleToggleLock = () => { if (!isReadOnly) { // About to lock - show confirmation setShowLockConfirm(true); } else { // Unlocking - no confirmation needed setIsReadOnly(false); } }; const confirmLock = () => { setIsReadOnly(true); setShowLockConfirm(false); }; return (
setShowSecurityModal(true)} localItems={localItems} sessionItems={sessionItems} onOpenStorageModal={() => setShowStorageModal(true)} events={clipboardEvents} onOpenClipboardModal={() => setShowClipboardModal(true)} activeTab={activeTab} setActiveTab={setActiveTab} encryptedMnemonicCache={encryptedMnemonicCache} handleLockAndClear={handleLockAndClear} appVersion={__APP_VERSION__} isLocked={isReadOnly} onToggleLock={handleToggleLock} />
{/* Error Display */} {error && (

Error

{error}

)} {/* Info Banner */} {recipientFpr && activeTab === 'backup' && (
Recipient Key: {recipientFpr}
)} {/* Main Content Grid */}
{activeTab === 'backup' ? ( <>