diff --git a/package.json b/package.json index 64d275f..ab9de6e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "seedpgp-web", "private": true, - "version": "1.4.4", + "version": "1.4.5", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.tsx b/src/App.tsx index df4ad5a..fa1057b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,7 +42,7 @@ interface ClipboardEvent { } function App() { - const [activeTab, setActiveTab] = useState<'backup' | 'restore' | 'seedblender'>('backup'); + const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender'>('create'); const [mnemonic, setMnemonic] = useState(''); const [backupMessagePassword, setBackupMessagePassword] = useState(''); const [restoreMessagePassword, setRestoreMessagePassword] = useState(''); @@ -76,7 +76,12 @@ function App() { 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); const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password']; @@ -246,6 +251,45 @@ function App() { } }; + const generateNewSeed = async () => { + try { + setLoading(true); + setError(''); + + // Generate random entropy + const entropyLength = seedWordCount === 12 ? 16 : 32; // 128 bits for 12 words, 256 for 24 + const entropy = new Uint8Array(entropyLength); + crypto.getRandomValues(entropy); + + // Convert to mnemonic using your existing lib + const { entropyToMnemonic } = await import('./lib/seedblend'); + const newMnemonic = await entropyToMnemonic(entropy); + + setGeneratedSeed(newMnemonic); + + // Set mnemonic for backup if that's the destination + if (seedDestination === 'backup') { + setMnemonic(newMnemonic); + } else if (seedDestination === 'seedblender') { + setSeedForBlender(newMnemonic); + } + + // Auto-switch to chosen destination after generation + setTimeout(() => { + setActiveTab(seedDestination); + // Reset Create tab state after switching + setTimeout(() => { + setGeneratedSeed(''); + }, 300); + }, 1500); + + } catch (e) { + setError(e instanceof Error ? e.message : 'Seed generation failed'); + } finally { + setLoading(false); + } + }; + const handleBackup = async () => { setLoading(true); setError(''); @@ -298,6 +342,10 @@ function App() { 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 { @@ -402,6 +450,12 @@ function App() { // 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 @@ -446,15 +500,37 @@ function App() { setShowLockConfirm(false); }; - const handleRequestTabChange = (newTab: 'backup' | 'restore' | 'seedblender') => { - if (activeTab === 'seedblender' && isBlenderDirty) { - if (window.confirm("You have unsaved data in the Seed Blender. Are you sure you want to leave? All progress will be lost.")) { - setActiveTab(newTab); - setIsBlenderDirty(false); // Reset dirty state on leaving - } - // else: user cancelled, do nothing - } else { - setActiveTab(newTab); + 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 entire app? This will clear all seeds, passwords, and generated data.")) { + // Clear all state + setMnemonic(''); + setGeneratedSeed(''); + setBackupMessagePassword(''); + setRestoreMessagePassword(''); + setPublicKeyInput(''); + setPrivateKeyInput(''); + setPrivateKeyPassphrase(''); + setQrPayload(''); + setRecipientFpr(''); + setRestoreInput(''); + setDecryptedRestoredMnemonic(null); + setError(''); + setSeedForBlender(''); + setIsBlenderDirty(false); + // Clear session + destroySessionKey(); + setEncryptedMnemonicCache(null); + // Go to Create tab (fresh start) + // Force SeedBlender to remount (resets its internal state) + setBlenderResetKey(prev => prev + 1); + + setActiveTab('create'); } }; @@ -509,6 +585,7 @@ function App() { appVersion={__APP_VERSION__} isLocked={isReadOnly} onToggleLock={handleToggleLock} + onResetAll={handleResetAll} />
@@ -540,136 +617,274 @@ function App() { {/* Main Content Grid */}
- {activeTab === 'backup' ? ( - <> -
- -