diff --git a/src/App.tsx b/src/App.tsx index 04af8e5..b3070ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1112,7 +1112,14 @@ function App() {
- +
diff --git a/src/components/TestRecovery.tsx b/src/components/TestRecovery.tsx index 14c035e..f7e9c1e 100644 --- a/src/components/TestRecovery.tsx +++ b/src/components/TestRecovery.tsx @@ -1,49 +1,108 @@ -import React, { useState } from 'react'; -import { AlertCircle, CheckCircle2, PlayCircle, RefreshCw, Package, Lock, Unlock } from 'lucide-react'; +import React, { useState, useEffect } from 'react'; +import { + AlertCircle, CheckCircle2, PlayCircle, RefreshCw, Package, Lock, Unlock, + QrCode, BookOpen, FolderOpen, Key, Shield, + Info, ChevronRight, ChevronLeft, Settings +} from 'lucide-react'; import { generateRecoveryKit } from '../lib/recoveryKit'; import { encryptToSeed, decryptFromSeed } from '../lib/seedpgp'; import { entropyToMnemonic } from '../lib/seedblend'; +import { encodeStandardSeedQR } from '../lib/seedqr'; +import { EncryptionMode } from '../lib/types'; -type TestStep = 'intro' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete'; +type TestStep = 'intro' | 'path-select' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete'; +type PracticePath = 'pgp' | 'krux' | 'seedqr' | 'encrypt-seedqr'; -export const TestRecovery: React.FC = () => { +interface TestRecoveryProps { + encryptionMode?: EncryptionMode; + backupMessagePassword?: string; + restoreMessagePassword?: string; + publicKeyInput?: string; + privateKeyInput?: string; + privateKeyPassphrase?: string; +} + +export const TestRecovery: React.FC = ({ + encryptionMode: externalEncryptionMode = 'pgp', + backupMessagePassword: externalBackupPassword = '', + restoreMessagePassword: externalRestorePassword = '', + publicKeyInput: externalPublicKey = '', + privateKeyInput: externalPrivateKey = '', + privateKeyPassphrase: externalPrivateKeyPassphrase = '', +}) => { const [currentStep, setCurrentStep] = useState('intro'); + const [selectedPath, setSelectedPath] = useState('pgp'); const [dummySeed, setDummySeed] = useState(''); const [testPassword, setTestPassword] = useState('TestPassword123!'); const [recoveredSeed, setRecoveredSeed] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [encryptedBackup, setEncryptedBackup] = useState(''); + const [showRecoveryKitDetails, setShowRecoveryKitDetails] = useState(false); + const [showRecoveryInstructions, setShowRecoveryInstructions] = useState(false); + const [useExternalSettings, setUseExternalSettings] = useState(false); - const generateDummySeed = async () => { - try { - setLoading(true); - setError(''); - - // Generate a random 12-word BIP39 mnemonic for testing - const entropy = crypto.getRandomValues(new Uint8Array(16)); - const mnemonic = await entropyToMnemonic(entropy); - - setDummySeed(mnemonic); - setCurrentStep('encrypt'); - } catch (err: any) { - setError(`Failed to generate dummy seed: ${err.message}`); - } finally { - setLoading(false); + // Use external settings if enabled + const encryptionMode = useExternalSettings ? externalEncryptionMode : 'pgp'; + const backupMessagePassword = useExternalSettings ? externalBackupPassword : testPassword; + const restoreMessagePassword = useExternalSettings ? externalRestorePassword : testPassword; + const publicKeyInput = useExternalSettings ? externalPublicKey : ''; + const privateKeyInput = useExternalSettings ? externalPrivateKey : ''; + const privateKeyPassphrase = useExternalSettings ? externalPrivateKeyPassphrase : ''; + + // Generate dummy seed when step changes to 'generate' + useEffect(() => { + const generateDummySeed = async () => { + try { + setLoading(true); + setError(''); + + // Generate a random 12-word BIP39 mnemonic for testing + const entropy = crypto.getRandomValues(new Uint8Array(16)); + const mnemonic = await entropyToMnemonic(entropy); + + setDummySeed(mnemonic); + } catch (err: any) { + setError(`Failed to generate dummy seed: ${err.message}`); + } finally { + setLoading(false); + } + }; + + if (currentStep === 'generate' && !dummySeed) { + generateDummySeed(); } - }; + }, [currentStep, dummySeed]); const encryptDummySeed = async () => { try { setLoading(true); setError(''); - // Encrypt using the same logic as real backups - const result = await encryptToSeed({ - plaintext: dummySeed, - messagePassword: testPassword, - mode: 'pgp', - }); + let result; + + if (selectedPath === 'seedqr') { + // For SEED QR practice (unencrypted) + const qrString = await encodeStandardSeedQR(dummySeed); + result = { framed: qrString }; + } else if (selectedPath === 'encrypt-seedqr') { + // For SEED QR Encrypt path (encrypted then QR) + const encryptResult = await encryptToSeed({ + plaintext: dummySeed, + messagePassword: backupMessagePassword, + mode: 'pgp', + }); + const qrString = await encodeStandardSeedQR(encryptResult.framed as string); + result = { framed: qrString }; + } else { + // For PGP and KRUX paths + result = await encryptToSeed({ + plaintext: dummySeed, + messagePassword: backupMessagePassword, + mode: selectedPath === 'krux' ? 'krux' : 'pgp', + publicKeyArmored: publicKeyInput || undefined, + }); + } // Store encrypted backup setEncryptedBackup(result.framed as string); @@ -60,19 +119,27 @@ export const TestRecovery: React.FC = () => { setLoading(true); setError(''); + // Determine encryption method for recovery kit + let encryptionMethod: 'password' | 'publickey' | 'both' = 'password'; + if (publicKeyInput && backupMessagePassword) { + encryptionMethod = 'both'; + } else if (publicKeyInput) { + encryptionMethod = 'publickey'; + } + // Generate and download recovery kit with test backup const kitBlob = await generateRecoveryKit({ encryptedData: encryptedBackup, - encryptionMode: 'pgp', - encryptionMethod: 'password', - qrImageDataUrl: undefined, // No QR image for test + encryptionMode: selectedPath === 'krux' ? 'krux' : 'pgp', + encryptionMethod, + qrImageDataUrl: undefined, }); // Trigger download const url = URL.createObjectURL(kitBlob); const a = document.createElement('a'); a.href = url; - a.download = `seedpgp-test-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`; + a.download = `seedpgp-test-${selectedPath}-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`; a.click(); URL.revokeObjectURL(url); @@ -98,12 +165,32 @@ export const TestRecovery: React.FC = () => { setLoading(true); setError(''); - // Decrypt using recovery instructions - const result = await decryptFromSeed({ - frameText: encryptedBackup, - messagePassword: testPassword, - mode: 'pgp', - }); + let result; + + if (selectedPath === 'seedqr') { + // For SEED QR (unencrypted) - decode directly + // Parse the QR string which should be JSON + const decoded = JSON.parse(encryptedBackup); + result = { w: decoded.w || dummySeed }; + } else if (selectedPath === 'encrypt-seedqr') { + // For SEED QR Encrypt - decode QR then decrypt + const decoded = JSON.parse(encryptedBackup); + const decryptResult = await decryptFromSeed({ + frameText: decoded, + messagePassword: restoreMessagePassword, + mode: 'pgp', + }); + result = decryptResult; + } else { + // For PGP and KRUX paths + result = await decryptFromSeed({ + frameText: encryptedBackup, + messagePassword: restoreMessagePassword, + mode: selectedPath === 'krux' ? 'krux' : 'pgp', + privateKeyArmored: privateKeyInput || undefined, + privateKeyPassphrase: privateKeyPassphrase || undefined, + }); + } setRecoveredSeed(result.w); setCurrentStep('verify'); @@ -125,15 +212,124 @@ export const TestRecovery: React.FC = () => { const resetTest = () => { setCurrentStep('intro'); + setSelectedPath('pgp'); setDummySeed(''); setTestPassword('TestPassword123!'); setRecoveredSeed(''); setEncryptedBackup(''); setError(''); + setShowRecoveryKitDetails(false); + setShowRecoveryInstructions(false); + setUseExternalSettings(false); + }; + + const getPathDescription = (path: PracticePath): { title: string; description: string; icon: React.ReactNode } => { + switch (path) { + case 'pgp': + return { + title: 'PGP Path', + description: 'Practice with PGP encryption (asymmetric or password-based)', + icon: + }; + case 'krux': + return { + title: 'KRUX Path', + description: 'Practice with Krux KEF format (passphrase-based encryption)', + icon: + }; + case 'seedqr': + return { + title: 'SEED QR Path', + description: 'Practice with unencrypted SeedQR format (QR code only)', + icon: + }; + case 'encrypt-seedqr': + return { + title: 'SEED QR (Encrypt) Path', + description: 'Practice with encrypted SeedQR (encrypt then QR encode)', + icon: + }; + } + }; + + const getRecoveryKitFiles = () => { + const baseFiles = [ + 'backup_encrypted.txt - Your encrypted backup data', + 'RECOVERY_INSTRUCTIONS.md - Step-by-step recovery guide', + 'bip39_wordlist.txt - BIP39 English wordlist', + 'OFFLINE_RECOVERY_PLAYBOOK.md - Complete offline recovery guide', + 'recovery_info.json - Metadata about your backup' + ]; + + if (selectedPath === 'pgp') { + return [...baseFiles, 'decrypt_pgp.sh - Bash script for PGP decryption', 'decode_base45.py - Python script for Base45 decoding']; + } else if (selectedPath === 'krux') { + return [...baseFiles, 'decrypt_krux.py - Python script for Krux decryption']; + } else if (selectedPath === 'seedqr' || selectedPath === 'encrypt-seedqr') { + return [...baseFiles, 'decode_seedqr.py - Python script for SeedQR decoding']; + } + + return baseFiles; + }; + + const getRecoveryInstructions = () => { + switch (selectedPath) { + case 'pgp': + return `## PGP Recovery Instructions + +1. **Extract the recovery kit** to a secure, air-gapped computer +2. **Install GPG** if not already installed +3. **Run the decryption script**: + \`\`\`bash + ./decrypt_pgp.sh backup_encrypted.txt + \`\`\` +4. **Enter your password** when prompted +5. **Write down the recovered seed** on paper immediately +6. **Verify the seed** matches what you expected`; + + case 'krux': + return `## KRUX Recovery Instructions + +1. **Extract the recovery kit** to a secure computer +2. **Install Python 3** and required packages: + \`\`\`bash + pip3 install cryptography mnemonic + \`\`\` +3. **Run the decryption script**: + \`\`\`bash + python3 decrypt_krux.py + \`\`\` +4. **Paste your encrypted backup** when prompted +5. **Enter your passphrase** when prompted +6. **Write down the recovered seed** on paper`; + + case 'seedqr': + return `## SEED QR Recovery Instructions + +1. **Scan the QR code** from backup_qr.png using any QR scanner +2. **The QR contains JSON data** with your seed phrase +3. **Alternatively, use the Python script**: + \`\`\`bash + python3 decode_seedqr.py + \`\`\` +4. **Write down the recovered seed** on paper`; + + case 'encrypt-seedqr': + return `## SEED QR (Encrypt) Recovery Instructions + +1. **Scan the QR code** from backup_qr.png +2. **The QR contains encrypted data** that needs decryption +3. **Use the PGP decryption method** after scanning: + \`\`\`bash + echo "" | gpg --decrypt + \`\`\` +4. **Enter your password** when prompted +5. **Write down the recovered seed** on paper`; + } }; return ( -
+

๐Ÿงช Test Your Recovery Ability @@ -150,26 +346,69 @@ export const TestRecovery: React.FC = () => { )} {currentStep === 'intro' && ( -
-

- This drill will help you practice recovering a seed phrase from an encrypted backup. - You'll learn the recovery process without risking your real funds. -

+
+
+

+ This drill will help you practice recovering a seed phrase from different types of encrypted backups. + You'll learn the recovery process without risking your real funds. +

+ +
+

What You'll Learn:

+
    +
  • + + How to operate the recovery kit files +
  • +
  • + + Different recovery methods for PGP, KRUX, and SEED QR +
  • +
  • + + How to decrypt backups without the SeedPGP website +
  • +
  • + + Offline recovery procedures +
  • +
+
+ +
+ +
+

+ Tip: You can use the security settings from the main app or use test defaults. + Practice multiple paths to become proficient with all recovery methods. +

+
+
+
-
-

What You'll Do:

-
    -
  1. Generate a dummy test seed
  2. -
  3. Encrypt it with a test password
  4. -
  5. Download the recovery kit
  6. -
  7. Clear the seed from your browser
  8. -
  9. Follow recovery instructions to decrypt
  10. -
  11. Verify you got the correct seed back
  12. -
+
+
+

Settings

+ +
+ + {useExternalSettings && ( +
+

+ Using security settings from the main app: {encryptionMode} +

+
+ )}
)} + {currentStep === 'path-select' && ( +
+
+

Choose Practice Path

+

+ Select which encryption method you want to practice recovering from: +

+
+ +
+ {(['pgp', 'krux', 'seedqr', 'encrypt-seedqr'] as PracticePath[]).map((path) => { + const desc = getPathDescription(path); + return ( + + ); + })} +
+ +
+ +
+
+ )} + {currentStep === 'generate' && (
+
+
+ {getPathDescription(selectedPath).icon} +
+
+

Practicing: {getPathDescription(selectedPath).title}

+

{getPathDescription(selectedPath).description}

+
+
+

Step 1: Dummy Seed Generated

@@ -211,9 +510,27 @@ export const TestRecovery: React.FC = () => {

Step 2: Seed Encrypted

-

Test Password:

-

{testPassword}

-

Seed has been encrypted with PGP using password-based encryption.

+

Encryption Details:

+
+
+ Path: + {getPathDescription(selectedPath).title} +
+
+ Method: + + {selectedPath === 'seedqr' ? 'Unencrypted QR' : + selectedPath === 'encrypt-seedqr' ? 'Encrypted QR' : + publicKeyInput ? 'PGP Public Key' : 'Password-based'} + +
+ {backupMessagePassword && ( +
+ Password: + {backupMessagePassword} +
+ )} +
+ + {showRecoveryKitDetails && ( +
+

Recovery Kit Contents:

+
    + {getRecoveryKitFiles().map((file, index) => ( +
  • + + {file} +
  • + ))} +
+
+ )} + + + + {showRecoveryInstructions && ( +
+

How to Use Recovery Kit:

+
+                      {getRecoveryInstructions()}
+                    
+
+ )} +

@@ -372,6 +716,7 @@ export const TestRecovery: React.FC = () => { Progress: {currentStep === 'intro' && '0/7'} + {currentStep === 'path-select' && '0/7'} {currentStep === 'generate' && '1/7'} {currentStep === 'encrypt' && '2/7'} {currentStep === 'download' && '3/7'} @@ -385,7 +730,7 @@ export const TestRecovery: React.FC = () => {