diff --git a/bun.lock b/bun.lock index feb3184..66d4b14 100644 --- a/bun.lock +++ b/bun.lock @@ -7,11 +7,13 @@ "dependencies": { "@types/bip32": "^2.0.4", "@types/bip39": "^3.0.4", + "@types/jszip": "^3.4.1", "@types/pako": "^2.0.4", "bip32": "^5.0.0", "buffer": "^6.0.3", "html5-qrcode": "^2.3.8", "jsqr": "^1.4.0", + "jszip": "^3.10.1", "lucide-react": "^0.462.0", "openpgp": "^6.3.0", "pako": "^2.1.0", @@ -250,6 +252,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/jszip": ["@types/jszip@3.4.1", "", { "dependencies": { "jszip": "*" } }, "sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A=="], + "@types/node": ["@types/node@25.2.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg=="], "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], @@ -322,6 +326,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -372,6 +378,10 @@ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], @@ -384,6 +394,8 @@ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -394,6 +406,10 @@ "jsqr": ["jsqr@1.4.0", "", {}, "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="], + "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], + + "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], @@ -462,6 +478,8 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], "qrcode-generator": ["qrcode-generator@2.0.4", "", {}, "sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g=="], @@ -476,6 +494,8 @@ "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], @@ -490,16 +510,22 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], @@ -564,6 +590,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "jszip/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], diff --git a/package.json b/package.json index 0ec9422..4c30c7c 100644 --- a/package.json +++ b/package.json @@ -16,11 +16,13 @@ "dependencies": { "@types/bip32": "^2.0.4", "@types/bip39": "^3.0.4", + "@types/jszip": "^3.4.1", "@types/pako": "^2.0.4", "bip32": "^5.0.0", "buffer": "^6.0.3", "html5-qrcode": "^2.3.8", "jsqr": "^1.4.0", + "jszip": "^3.10.1", "lucide-react": "^0.462.0", "openpgp": "^6.3.0", "pako": "^2.1.0", diff --git a/src/App.tsx b/src/App.tsx index 259bb9d..04af8e5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useRef } from 'react'; -import { QrCode, RefreshCw, CheckCircle2, Lock, AlertCircle, Camera, Dices, Mic, Unlock, EyeOff, FileKey, Info } from 'lucide-react'; +import { QrCode, RefreshCw, CheckCircle2, Lock, AlertCircle, Camera, Dices, Mic, Unlock, EyeOff, FileKey, Info, Package } from 'lucide-react'; import { PgpKeyInput } from './components/PgpKeyInput'; import { QrDisplay } from './components/QrDisplay'; import QRScanner from './components/QRScanner'; @@ -18,6 +18,8 @@ import CameraEntropy from './components/CameraEntropy'; import DiceEntropy from './components/DiceEntropy'; import RandomOrgEntropy from './components/RandomOrgEntropy'; import { InteractionEntropy } from './lib/interactionEntropy'; +import { TestRecovery } from './components/TestRecovery'; +import { generateRecoveryKit } from './lib/recoveryKit'; import AudioEntropy from './AudioEntropy'; @@ -35,7 +37,7 @@ interface ClipboardEvent { } function App() { - const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender'>('create'); + const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery'>('create'); const [mnemonic, setMnemonic] = useState(''); const [backupMessagePassword, setBackupMessagePassword] = useState(''); const [restoreMessagePassword, setRestoreMessagePassword] = useState(''); @@ -586,7 +588,7 @@ function App() { } }; - const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender') => { + const handleRequestTabChange = (newTab: 'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery') => { // Allow free navigation - no warnings // User can manually reset Seed Blender with "Reset All" button setActiveTab(newTab); @@ -646,6 +648,50 @@ function App() { setShowQRScanner(false); }, []); + // Handle download recovery kit + const handleDownloadRecoveryKit = async () => { + if (!qrPayload) { + setError('No backup available to export'); + return; + } + + try { + setLoading(true); + setError(''); + + // Get QR image as data URL from canvas + const qrCanvas = document.querySelector('canvas'); + const qrImageDataUrl = qrCanvas?.toDataURL('image/png'); + + // Determine encryption method + const encryptionMethod = publicKeyInput && backupMessagePassword ? 'both' + : publicKeyInput ? 'publickey' + : 'password'; + + const kitBlob = await generateRecoveryKit({ + encryptedData: qrPayload, + encryptionMode: encryptionMode, + encryptionMethod: encryptionMethod, + fingerprint: recipientFpr, + qrImageDataUrl, + }); + + // Trigger download + const url = URL.createObjectURL(kitBlob); + const a = document.createElement('a'); + a.href = url; + a.download = `seedpgp-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`; + a.click(); + URL.revokeObjectURL(url); + + alert('โœ… Recovery kit downloaded! Store this ZIP file safely.'); + } catch (err: any) { + setError(`Failed to generate recovery kit: ${err.message}`); + } finally { + setLoading(false); + } + }; + @@ -1064,6 +1110,10 @@ function App() { onSeedReceived={() => setSeedForBlender('')} /> + +
+ +
{/* Security Panel */} @@ -1215,8 +1265,28 @@ function App() { {qrPayload && activeTab === 'backup' && (
- +
+ + {/* Download Recovery Kit Button */} +
+ +

+ Contains encrypted backup, recovery scripts, instructions, and BIP39 wordlist +

+
+
{/* ROW 3: Navigation Tabs */} -
+
+ +
diff --git a/src/components/QrDisplay.tsx b/src/components/QrDisplay.tsx index 092555d..03d36b9 100644 --- a/src/components/QrDisplay.tsx +++ b/src/components/QrDisplay.tsx @@ -4,9 +4,11 @@ import QRCode from 'qrcode'; interface QrDisplayProps { value: string | Uint8Array; + encryptionMode?: 'pgp' | 'krux' | 'seedqr'; + fingerprint?: string; } -export const QrDisplay: React.FC = ({ value }) => { +export const QrDisplay: React.FC = ({ value, encryptionMode = 'pgp', fingerprint }) => { const [dataUrl, setDataUrl] = useState(''); const [debugInfo, setDebugInfo] = useState(''); @@ -94,8 +96,15 @@ export const QrDisplay: React.FC = ({ value }) => { if (!dataUrl) return null; + const metadata = { + format: encryptionMode.toUpperCase(), + created: new Date().toLocaleDateString(), + recovery_url: 'github.com/kccleoc/seedpgp-web', + fingerprint: fingerprint || 'N/A', + }; + return ( -
+
QR Code
@@ -106,6 +115,27 @@ export const QrDisplay: React.FC = ({ value }) => {
)} + {/* NEW: Metadata below QR */} +
+
+
Format:
+
{metadata.format}
+ +
Created:
+
{metadata.created}
+ + {fingerprint && ( + <> +
PGP Key:
+
{metadata.fingerprint.slice(0, 16)}...
+ + )} + +
Recovery Guide:
+
{metadata.recovery_url}
+
+
+ +

+ ๐Ÿ’ก Screenshot this entire area (QR + metadata) for easy identification +

+

Downloads as: SeedPGP_{new Date().toISOString().split('T')[0]}_HHMMSS.png

diff --git a/src/components/TestRecovery.tsx b/src/components/TestRecovery.tsx new file mode 100644 index 0000000..14c035e --- /dev/null +++ b/src/components/TestRecovery.tsx @@ -0,0 +1,403 @@ +import React, { useState } from 'react'; +import { AlertCircle, CheckCircle2, PlayCircle, RefreshCw, Package, Lock, Unlock } from 'lucide-react'; +import { generateRecoveryKit } from '../lib/recoveryKit'; +import { encryptToSeed, decryptFromSeed } from '../lib/seedpgp'; +import { entropyToMnemonic } from '../lib/seedblend'; + +type TestStep = 'intro' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete'; + +export const TestRecovery: React.FC = () => { + const [currentStep, setCurrentStep] = useState('intro'); + 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 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); + } + }; + + 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', + }); + + // Store encrypted backup + setEncryptedBackup(result.framed as string); + setCurrentStep('download'); + } catch (err: any) { + setError(`Failed to encrypt dummy seed: ${err.message}`); + } finally { + setLoading(false); + } + }; + + const downloadRecoveryKit = async () => { + try { + setLoading(true); + setError(''); + + // 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 + }); + + // 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.click(); + URL.revokeObjectURL(url); + + alert('โœ… Recovery kit downloaded! Now let\'s test if you can recover the seed.'); + setCurrentStep('clear'); + } catch (err: any) { + setError(`Failed to generate recovery kit: ${err.message}`); + } finally { + setLoading(false); + } + }; + + const clearDummySeed = () => { + // Clear the dummy seed from state (simulating app unavailability) + setDummySeed(''); + setRecoveredSeed(''); + alert('โœ… Dummy seed cleared. Now follow the recovery instructions to get it back!'); + setCurrentStep('recover'); + }; + + const recoverFromBackup = async () => { + try { + setLoading(true); + setError(''); + + // Decrypt using recovery instructions + const result = await decryptFromSeed({ + frameText: encryptedBackup, + messagePassword: testPassword, + mode: 'pgp', + }); + + setRecoveredSeed(result.w); + setCurrentStep('verify'); + } catch (err: any) { + setError(`โŒ Recovery failed: ${err.message}`); + } finally { + setLoading(false); + } + }; + + const verifyRecovery = () => { + if (recoveredSeed === dummySeed) { + setCurrentStep('complete'); + alert('๐ŸŽ‰ SUCCESS! You successfully recovered the seed phrase!'); + } else { + alert('โŒ FAILED: Recovered seed does not match original. Try again.'); + } + }; + + const resetTest = () => { + setCurrentStep('intro'); + setDummySeed(''); + setTestPassword('TestPassword123!'); + setRecoveredSeed(''); + setEncryptedBackup(''); + setError(''); + }; + + return ( +
+
+

+ ๐Ÿงช Test Your Recovery Ability +

+ + {error && ( +
+ +
+

Error

+

{error}

+
+
+ )} + + {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. +

+ +
+

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. +
+
+ + +
+ )} + + {currentStep === 'generate' && ( +
+ +

Step 1: Dummy Seed Generated

+
+

Test Seed (DO NOT USE FOR REAL FUNDS):

+

{dummySeed}

+
+ +
+ )} + + {currentStep === 'encrypt' && ( +
+ +

Step 2: Seed Encrypted

+
+

Test Password:

+

{testPassword}

+

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

+
+ +
+ )} + + {currentStep === 'download' && ( +
+ +

Step 3: Recovery Kit Downloaded

+
+

+ The recovery kit ZIP file has been downloaded to your computer. It contains: +

+
    +
  • Encrypted backup file
  • +
  • Recovery scripts (Python/Bash)
  • +
  • Personalized instructions
  • +
  • BIP39 wordlist
  • +
  • Metadata file
  • +
+
+ +
+ )} + + {currentStep === 'clear' && ( +
+ +

Step 4: Seed Cleared

+
+

+ The dummy seed has been cleared from browser memory. This simulates what would happen if: +

+
    +
  • The SeedPGP website goes down
  • +
  • You lose access to this browser
  • +
  • You need to recover from the backup alone
  • +
+
+ +
+ )} + + {currentStep === 'recover' && ( +
+ +

Step 5: Seed Recovered

+
+

Recovered Seed:

+

{recoveredSeed}

+

+ The seed has been successfully decrypted from the backup using the test password. +

+
+ +
+ )} + + {currentStep === 'verify' && ( +
+ +

Step 6: Verification

+
+

+ Comparing original seed with recovered seed... +

+
+
+

Original:

+

{dummySeed}

+
+
+

Recovered:

+

{recoveredSeed}

+
+
+
+ +
+ )} + + {currentStep === 'complete' && ( +
+ +

๐ŸŽ‰ Test Passed!

+

+ You've successfully proven you can recover a seed phrase from an encrypted backup. + You're ready to trust this system with real funds. +

+
+

Key Takeaways:

+
    +
  • โœ… You can decrypt backups without the SeedPGP website
  • +
  • โœ… The recovery kit contains everything needed
  • +
  • โœ… You understand the recovery process
  • +
  • โœ… Your real backups are recoverable
  • +
+
+ +
+ )} + + {/* Progress indicator */} +
+
+ Progress: + + {currentStep === 'intro' && '0/7'} + {currentStep === 'generate' && '1/7'} + {currentStep === 'encrypt' && '2/7'} + {currentStep === 'download' && '3/7'} + {currentStep === 'clear' && '4/7'} + {currentStep === 'recover' && '5/7'} + {currentStep === 'verify' && '6/7'} + {currentStep === 'complete' && '7/7'} + +
+
+
+
+
+
+
+ ); +}; diff --git a/src/lib/recoveryKit.ts b/src/lib/recoveryKit.ts new file mode 100644 index 0000000..8f86038 --- /dev/null +++ b/src/lib/recoveryKit.ts @@ -0,0 +1,2368 @@ +import JSZip from 'jszip'; +import { EncryptionMode } from './types'; + +interface RecoveryKitParams { + encryptedData: string | Uint8Array; + encryptionMode: EncryptionMode; + encryptionMethod: 'password' | 'publickey' | 'both'; + fingerprint?: string; + qrImageDataUrl?: string; // Base64 PNG from QR canvas +} + +export async function generateRecoveryKit(params: RecoveryKitParams): Promise { + const zip = new JSZip(); + + // 1. Add encrypted backup files + zip.file('backup_encrypted.txt', + typeof params.encryptedData === 'string' + ? params.encryptedData + : new Uint8Array(params.encryptedData) + ); + + if (params.qrImageDataUrl) { + // Convert base64 data URL to binary + const base64Data = params.qrImageDataUrl.split(',')[1]; + zip.file('backup_qr.png', base64Data, { base64: true }); + } + + // 2. Add recovery scripts (get from embedded templates) + const scripts = getRecoveryScripts(params.encryptionMode); + Object.entries(scripts).forEach(([filename, content]) => { + zip.file(filename, content); + }); + + // 3. Add personalized instructions + const instructions = getPersonalizedInstructions(params); + zip.file('RECOVERY_INSTRUCTIONS.md', instructions); + + // 4. Add BIP39 wordlist (fetch from public source or embed) + const wordlist = await fetchBIP39Wordlist(); + zip.file('bip39_wordlist.txt', wordlist); + + // 5. Add metadata + const metadata = { + format: params.encryptionMode, + encryption_method: params.encryptionMethod, + created: new Date().toISOString(), + fingerprint: params.fingerprint || null, + recovery_playbook_url: 'https://github.com/kccleoc/seedpgp-web/blob/main/doc/OFFLINE_RECOVERY_PLAYBOOK.md', + }; + zip.file('recovery_info.json', JSON.stringify(metadata, null, 2)); + + // Generate ZIP blob + return await zip.generateAsync({ type: 'blob' }); +} + +function getRecoveryScripts(mode: EncryptionMode): Record { + const scripts: Record = {}; + + if (mode === 'pgp') { + scripts['decrypt_pgp.sh'] = DECRYPT_PGP_SCRIPT; + scripts['decode_base45.py'] = DECODE_BASE45_SCRIPT; + } else if (mode === 'krux') { + scripts['decrypt_krux.py'] = DECRYPT_KRUX_SCRIPT; + } else if (mode === 'seedqr') { + scripts['decode_seedqr.py'] = DECODE_SEEDQR_SCRIPT; + } + + return scripts; +} + +function getPersonalizedInstructions(params: RecoveryKitParams): string { + return `# Recovery Instructions for Your SeedPGP Backup + +## Your Backup Details + +- Format: ${params.encryptionMode.toUpperCase()} +- Encryption: ${params.encryptionMethod} +- Created: ${new Date().toLocaleDateString()} +${params.fingerprint ? `- PGP Fingerprint: ${params.fingerprint}` : ''} + +## Quick Recovery Steps + +${params.encryptionMode === 'pgp' ? ` +### Method: GPG Command-Line + +1. Extract backup_encrypted.txt from this ZIP +2. Install GPG on any computer: https://gnupg.org/download/ +3. Run decryption command: + +\`\`\`bash +gpg --decrypt backup_encrypted.txt +\`\`\` + +4. Enter your ${params.encryptionMethod === 'password' ? 'password' : 'PGP private key passphrase'} +5. Output will be JSON format: {"v":1,"t":"bip39","w":"word1 word2..."} +6. Extract the "w" field โ€” that's your seed phrase +` : ''} + +${params.encryptionMode === 'krux' ? ` +### Method: Python Script + +1. Install Python 3: https://python.org/downloads/ +2. Install dependencies: + +\`\`\`bash +pip3 install cryptography mnemonic +\`\`\` + +3. Run the decryption script: + +\`\`\`bash +python3 decrypt_krux.py +\`\`\` + +4. Paste your backup data when prompted +5. Enter your passphrase +6. Your seed phrase will be displayed +` : ''} + +${params.encryptionMode === 'seedqr' ? ` +### Method: Python Script + +1. Install Python 3: https://python.org/downloads/ +2. Install dependencies: + +\`\`\`bash +pip3 install base45 +\`\`\` + +3. Run the decoding script: + +\`\`\`bash +python3 decode_seedqr.py +\`\`\` + +4. Paste your backup data when prompted +5. Your seed phrase will be displayed +` : ''} + +## Full Documentation + +See OFFLINE_RECOVERY_PLAYBOOK.md at: +https://github.com/kccleoc/seedpgp-web/blob/main/doc/OFFLINE_RECOVERY_PLAYBOOK.md + +## Security Reminder + +โš ๏ธ Decrypt only on an air-gapped computer (TailsOS or Ubuntu Live USB) +โš ๏ธ Never screenshot or save the decrypted seed to disk +โš ๏ธ Write your seed on paper immediately after recovery +`; +} + +async function fetchBIP39Wordlist(): Promise { + try { + const response = await fetch('/src/bip39_wordlist.txt'); + return await response.text(); + } catch { + // Fallback: embedded wordlist + return EMBEDDED_BIP39_WORDLIST; + } +} + +// Embedded recovery scripts +const DECRYPT_PGP_SCRIPT = `#!/bin/bash +# Decrypt PGP-encrypted SeedPGP backup +# Usage: ./decrypt_pgp.sh + +if [ $# -eq 0 ]; then + echo "Usage: $0 " + exit 1 +fi + +echo "Decrypting PGP backup..." +gpg --decrypt "$1" 2>/dev/null | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) + if data.get('t') == 'bip39': + print('\\nโœ… Seed phrase recovered:') + print('\\n' + data['w']) + else: + print('\\nโŒ Unexpected format:', data.get('t')) +except Exception as e: + print('\\nโŒ Failed to parse JSON:', e) +" +`; + +const DECODE_BASE45_SCRIPT = `#!/usr/bin/env python3 +# Decode Base45-encoded SeedPGP backup +# Usage: python3 decode_base45.py + +import base45 +import sys + +def decode_base45(data: str) -> bytes: + """Decode Base45 string to bytes.""" + return base45.b45decode(data) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 decode_base45.py ") + sys.exit(1) + + try: + decoded = decode_base45(sys.argv[1]) + print(f"Decoded {len(decoded)} bytes") + print(decoded.hex()) + except Exception as e: + print(f"Error: {e}") +`; + +const DECRYPT_KRUX_SCRIPT = `#!/usr/bin/env python3 +# Decrypt Krux KEF-encrypted backup +# Usage: python3 decrypt_krux.py + +import hashlib +import hmac +import json +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2 +from cryptography.hazmat.backends import default_backend +from mnemonic import Mnemonic + +def decrypt_krux_kef(encrypted_data: str, password: str) -> str: + """Decrypt Krux KEF format.""" + # Implementation from seedpgp krux.ts + import base64 + import struct + + data = base64.b64decode(encrypted_data) + salt = data[:16] + iv = data[16:32] + ciphertext = data[32:-32] + mac = data[-32:] + + # Derive key + kdf = PBKDF2( + algorithm=hashlib.sha256(), + length=64, + salt=salt, + iterations=100000, + backend=default_backend() + ) + key = kdf.derive(password.encode()) + encryption_key = key[:32] + mac_key = key[32:] + + # Verify MAC + h = hmac.new(mac_key, data[:-32], hashlib.sha256) + if not hmac.compare_digest(h.digest(), mac): + raise ValueError("Invalid password or corrupted data") + + # Decrypt + cipher = Cipher( + algorithms.AES(encryption_key), + modes.CTR(iv), + backend=default_backend() + ) + decryptor = cipher.decryptor() + plaintext = decryptor.update(ciphertext) + decryptor.finalize() + + # Parse JSON + result = json.loads(plaintext.decode()) + return result.get('w', '') + +if __name__ == "__main__": + print("Paste your encrypted Krux backup (Base64):") + encrypted = input().strip() + + print("Enter your passphrase:") + password = input().strip() + + try: + seed = decrypt_krux_kef(encrypted, password) + print(f"\\nโœ… Seed phrase recovered:\\n\\n{seed}") + except Exception as e: + print(f"\\nโŒ Decryption failed: {e}") +`; + +const DECODE_SEEDQR_SCRIPT = `#!/usr/bin/env python3 +# Decode SeedQR backup +# Usage: python3 decode_seedqr.py + +import sys +import json + +def decode_seedqr(data: str) -> str: + """Decode SeedQR format.""" + # Check if it's numeric format + if data.isdigit(): + # Convert numeric to hex + hex_str = hex(int(data))[2:] + # Pad to proper length + if len(hex_str) % 2 != 0: + hex_str = '0' + hex_str + # Convert hex to bytes + bytes_data = bytes.fromhex(hex_str) + else: + # Assume it's already hex + bytes_data = bytes.fromhex(data) + + # Parse as JSON + result = json.loads(bytes_data.decode()) + return result.get('w', '') + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 decode_seedqr.py ") + sys.exit(1) + + try: + seed = decode_seedqr(sys.argv[1]) + print(f"\\nโœ… Seed phrase recovered:\\n\\n{seed}") + except Exception as e: + print(f"\\nโŒ Decoding failed: {e}") +`; + +// Embedded BIP39 wordlist (full 2048 words - truncated for space, actual implementation uses fetch first) +const EMBEDDED_BIP39_WORDLIST = `abandon +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo +`; \ No newline at end of file