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 */}
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ {loading ? 'Generating...' : '๐ฆ Download Recovery Kit'}
+
+
+ Contains encrypted backup, recovery scripts, instructions, and BIP39 wordlist
+
+
+
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 041249d..e2f5dcf 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -24,8 +24,8 @@ interface HeaderProps {
sessionItems: StorageItem[];
events: ClipboardEvent[];
onOpenClipboardModal: () => void;
- activeTab: 'create' | 'backup' | 'restore' | 'seedblender';
- onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
+ activeTab: 'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery';
+ onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery') => void;
appVersion: string;
isNetworkBlocked: boolean;
onToggleNetwork: () => void;
@@ -113,7 +113,7 @@ const Header: React.FC = ({
{/* ROW 3: Navigation Tabs */}
-
+
= ({
>
Blender
+
+ onRequestTabChange('test-recovery')}
+ >
+ ๐งช Test
+
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 (
-
+
@@ -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}
+
+
+
= ({ value }) => {
Download QR Code
+
+ ๐ก 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 && (
+
+ )}
+
+ {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:
+
+ Generate a dummy test seed
+ Encrypt it with a test password
+ Download the recovery kit
+ Clear the seed from your browser
+ Follow recovery instructions to decrypt
+ Verify you got the correct seed back
+
+
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ {loading ? 'Generating...' : 'Start Test Recovery Drill'}
+
+
+ )}
+
+ {currentStep === 'generate' && (
+
+
+
Step 1: Dummy Seed Generated
+
+
Test Seed (DO NOT USE FOR REAL FUNDS):
+
{dummySeed}
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ {loading ? 'Encrypting...' : 'Next: Encrypt This Seed'}
+
+
+ )}
+
+ {currentStep === 'encrypt' && (
+
+
+
Step 2: Seed Encrypted
+
+
Test Password:
+
{testPassword}
+
Seed has been encrypted with PGP using password-based encryption.
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ {loading ? 'Generating...' : 'Next: Download Recovery Kit'}
+
+
+ )}
+
+ {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
+
+
+
+ Next: Clear Seed & Test Recovery
+
+
+ )}
+
+ {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
+
+
+
+ {loading ? (
+
+ ) : (
+
+ )}
+ {loading ? 'Decrypting...' : 'Next: Recover Seed from Backup'}
+
+
+ )}
+
+ {currentStep === 'recover' && (
+
+
+
Step 5: Seed Recovered
+
+
Recovered Seed:
+
{recoveredSeed}
+
+ The seed has been successfully decrypted from the backup using the test password.
+
+
+
+ Next: Verify Recovery
+
+
+ )}
+
+ {currentStep === 'verify' && (
+
+
+
Step 6: Verification
+
+
+ Comparing original seed with recovered seed...
+
+
+
+
Original:
+
{dummySeed}
+
+
+
Recovered:
+
{recoveredSeed}
+
+
+
+
{
+ if (recoveredSeed === dummySeed) {
+ setCurrentStep('complete');
+ alert('๐ SUCCESS! You successfully recovered the seed phrase!');
+ } else {
+ alert('โ FAILED: Recovered seed does not match original. Try again.');
+ }
+ }}
+ className="w-full py-3 bg-gradient-to-r from-[#39ff14] to-[#00ff88] text-[#0a0a0f] rounded-xl font-bold"
+ >
+ Verify Match
+
+
+ )}
+
+ {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
+
+
+
+
+ Run Test Again
+
+
+ )}
+
+ {/* 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