mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 01:47:52 +08:00
Compare commits
4 Commits
02f58f5ef0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42be142e11 | ||
|
|
87bf40f27b | ||
|
|
573cdce585 | ||
| ae4c130fde |
28
bun.lock
28
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=="],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SeedPGP Web - TailsOS Offline Build
|
||||
|
||||
Built: Thu 19 Feb 2026 22:31:58 HKT
|
||||
Built: Sat Feb 21 23:47:29 HKT 2026
|
||||
|
||||
Usage Instructions:
|
||||
1. Copy this entire folder to a USB drive
|
||||
@@ -17,7 +17,7 @@ Security Features:
|
||||
- Session-only crypto keys (destroyed on tab close)
|
||||
|
||||
SHA-256 Checksums:
|
||||
32621ec84de2d13307181ed49050b9ba89429f2c43e340585b9efc189e4c0376 ./assets/index-D4JSYqq2.css
|
||||
3c716a34a15cf1fb65f5b0e2af025ebc003c9e4e9efbf7c1b1b4c494466d0cbe ./assets/index-rrnn41w7.js
|
||||
5cbbcb8adc7acc3b78a3fd31c76d573302705ff5fd714d03f5a2602591197cb5 ./assets/secp256k1-Cao5Swmf.wasm
|
||||
aab3ea208db02b2cb40902850c203f23159f515288b26ca5a131e1188b4362af ./assets/index-DW74Yc8k.css
|
||||
c5d6ba57285386d3c4a4e082b831ca24e6e925d7e25a4c38533a10e06c37b238 ./assets/index-Bwz_2nW3.js
|
||||
c7cd63f8c0a39b0aca861668029aa569597e3b4f9bcd2e40aa274598522e0e8e ./index.html
|
||||
e233270f7e649c773433b6bf85f68012aa95ed6936aa40e5ec11ee8cb9bb164c ./index.html
|
||||
|
||||
1
dist-tails/assets/index-D4JSYqq2.css
Normal file
1
dist-tails/assets/index-D4JSYqq2.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,10 +8,12 @@
|
||||
<title>SeedPGP Web</title>
|
||||
|
||||
<!-- Baseline CSP for generic builds.
|
||||
TailsOS builds override this via Makefile (build-tails target). -->
|
||||
TailsOS builds override this via Makefile (build-tails target).
|
||||
Commented out for development to avoid CSP issues with WebAssembly.
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' blob: data:; font-src 'self'; object-src 'none'; media-src 'self' blob:; base-uri 'self'; form-action 'none';" data-env="tails">
|
||||
<script type="module" crossorigin src="./assets/index-Bwz_2nW3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DW74Yc8k.css">
|
||||
-->
|
||||
<script type="module" crossorigin src="./assets/index-rrnn41w7.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-D4JSYqq2.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
## SeedPGP Recovery Playbook - Offline Recovery Guide
|
||||
|
||||
**Generated:** Feb 3, 2026 | **SeedPGP v1.4.7** | **Frame Format:** `SEEDPGP1:0:CRC16:BASE45_PAYLOAD`
|
||||
|
||||
***
|
||||
|
||||
## 📋 Recovery Requirements
|
||||
|
||||
```
|
||||
✅ SEEDPGP1 QR code or printed text
|
||||
✅ PGP Private Key (.asc file) OR Message Password (if symmetric encryption used)
|
||||
✅ Offline computer with terminal access
|
||||
✅ gpg command line tool (GNU Privacy Guard)
|
||||
```
|
||||
|
||||
**⚠️ Important:** This playbook assumes you have the original encryption parameters:
|
||||
|
||||
- PGP private key (if PGP encryption was used)
|
||||
- Private key passphrase (if the key is encrypted)
|
||||
- Message password (if symmetric encryption was used)
|
||||
- BIP39 passphrase (if 25th word was used during backup)
|
||||
|
||||
***
|
||||
|
||||
## 🔓 Step 1: Understand Frame Format
|
||||
|
||||
**SeedPGP Frame Structure:**
|
||||
|
||||
```
|
||||
SEEDPGP1:0:CRC16:BASE45_PAYLOAD
|
||||
```
|
||||
|
||||
- **SEEDPGP1:** Protocol identifier
|
||||
- **0:** Frame version (single frame)
|
||||
- **CRC16:** 4-character hexadecimal CRC16-CCITT checksum
|
||||
- **BASE45_PAYLOAD:** Base45-encoded PGP binary data
|
||||
|
||||
**Example Frame:**
|
||||
|
||||
```
|
||||
SEEDPGP1:0:58B5:2KO K0S-U. M:E1T*A%50%886N2SDITXSQVE VV$BA7.FZ+I01N%ISK$KBGESBRNOHYIK%A8N1FUOE.Z1T:8JBHDNNBV2AVJRGC1-OY67AU777I07UB88TQN0B5033IJOGG7$2ID/QNIR.:UGUO/M0BH0O94468TXM 0RGSIYT FNSQGNJKDCHP3JV/V-77:%KVZG+6VA7P826W0N0TBI5AMSQX60A%2E$OMWF1TV/J0SJJ 0M-VF0TH60W4TL1/519HS7BO%OT-QGZ5.AS.18AWSGF9O5E%MCYLM4STPI5+.3A5K7ZULFQM.JO:J3/C.IOB1819L8*ME027S9DJ0+18WCVTC30928T72W5D4P0UHC4O11IPRQ I5T39RSI9BTVT6LK6A9PWUF7B2CBEI43M%TT47%I4KBT-0H44L.RP$U02F8-7A*LH2$G44Q.880WF0BJ5SB5OR*39W/N3T9 -DQ4C
|
||||
```
|
||||
|
||||
### Extract Base45 Payload
|
||||
|
||||
```bash
|
||||
# Extract everything after the 3rd colon
|
||||
FRAME="SEEDPGP1:0:58B5:2KO K0S-U. M:E1T*A%50%886N2SDITXSQVE VV$BA7.FZ+I01N%ISK$KBGESBRNOHYIK%A8N1FUOE.Z1T:8JBHDNNBV2AVJRGC1-OY67AU777I07UB88TQN0B5033IJOGG7$2ID/QNIR.:UGUO/M0BH0O94468TXM 0RGSIYT FNSQGNJKDCHP3JV/V-77:%KVZG+6VA7P826W0N0TBI5AMSQX60A%2E$OMWF1TV/J0SJJ 0M-VF0TH60W4TL1/519HS7BO%OT-QGZ5.AS.18AWSGF9O5E%MCYLM4STPI5+.3A5K7ZULFQM.JO:J3/C.IOB1819L8*ME027S9DJ0+18WCVTC30928T72W5D4P0UHC4O11IPRQ I5T39RSI9BTVT6LK6A9PWUF7B2CBEI43M%TT47%I4KBT-0H44L.RP$U02F8-7A*LH2$G44Q.880WF0BJ5SB5OR*39W/N3T9 -DQ4C"
|
||||
PAYLOAD=$(echo "$FRAME" | cut -d: -f4-)
|
||||
echo "$PAYLOAD" > payload.b45
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 🔓 Step 2: Decode Base45 → PGP Binary
|
||||
|
||||
**Option A: Using base45 CLI tool:**
|
||||
|
||||
```bash
|
||||
# Install base45 if needed
|
||||
npm install -g base45
|
||||
|
||||
# Decode the payload
|
||||
base45decode < payload.b45 > encrypted.pgp
|
||||
```
|
||||
|
||||
**Option B: Using CyberChef (offline browser tool):**
|
||||
|
||||
1. Download CyberChef HTML from <https://gchq.github.io/CyberChef/>
|
||||
2. Open it in an offline browser
|
||||
3. Input → Paste your Base45 payload
|
||||
4. Operation → `From Base45`
|
||||
5. Save output as `encrypted.pgp`
|
||||
|
||||
**Option C: Manual verification (check CRC):**
|
||||
|
||||
```bash
|
||||
# Verify CRC16 checksum matches
|
||||
# The CRC16-CCITT-FALSE checksum should match the value in the frame (58B5 in example)
|
||||
# If using the web app, this is automatically verified during decryption
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 🔓 Step 3: Decrypt PGP Binary
|
||||
|
||||
### Option A: PGP Private Key Decryption (PKESK)
|
||||
|
||||
If the backup was encrypted with a PGP public key:
|
||||
|
||||
```bash
|
||||
# Import your private key (if not already imported)
|
||||
gpg --import private-key.asc
|
||||
|
||||
# List keys to verify fingerprint
|
||||
gpg --list-secret-keys --keyid-format LONG
|
||||
|
||||
# Decrypt using your private key
|
||||
gpg --batch --yes --decrypt encrypted.pgp
|
||||
```
|
||||
|
||||
**Expected JSON Output:**
|
||||
|
||||
```json
|
||||
{"v":1,"t":"bip39","w":"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about","l":"en","pp":0}
|
||||
```
|
||||
|
||||
**If private key has a passphrase:**
|
||||
|
||||
```bash
|
||||
gpg --batch --yes --passphrase "YOUR-PGP-KEY-PASSPHRASE" --decrypt encrypted.pgp
|
||||
```
|
||||
|
||||
### Option B: Message Password Decryption (SKESK)
|
||||
|
||||
If the backup was encrypted with a symmetric password:
|
||||
|
||||
```bash
|
||||
gpg --batch --yes --passphrase "YOUR-MESSAGE-PASSWORD" --decrypt encrypted.pgp
|
||||
```
|
||||
|
||||
**Expected JSON Output:**
|
||||
|
||||
```json
|
||||
{"v":1,"t":"bip39","w":"your seed phrase words here","l":"en","pp":1}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 🔓 Step 4: Parse Decrypted Data
|
||||
|
||||
The decrypted output is a JSON object with the following structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"v": 1, // Version (always 1)
|
||||
"t": "bip39", // Type (always "bip39")
|
||||
"w": "word1 word2 ...", // BIP39 mnemonic words (lowercase, single spaces)
|
||||
"l": "en", // Language (always "en" for English)
|
||||
"pp": 0 // BIP39 passphrase flag: 0 = no passphrase, 1 = passphrase used
|
||||
}
|
||||
```
|
||||
|
||||
**Extract the mnemonic:**
|
||||
|
||||
```bash
|
||||
# After decryption, extract the 'w' field
|
||||
DECRYPTED='{"v":1,"t":"bip39","w":"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about","l":"en","pp":0}'
|
||||
MNEMONIC=$(echo "$DECRYPTED" | grep -o '"w":"[^"]*"' | cut -d'"' -f4)
|
||||
echo "Mnemonic: $MNEMONIC"
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 💰 Step 5: Wallet Recovery
|
||||
|
||||
### BIP39 Passphrase Status
|
||||
|
||||
Check the `pp` field in the decrypted JSON:
|
||||
|
||||
- `"pp": 0` → No BIP39 passphrase was used during backup
|
||||
- `"pp": 1` → **BIP39 passphrase was used** (25th word/extra passphrase)
|
||||
|
||||
### Recovery Instructions
|
||||
|
||||
**Without BIP39 Passphrase (`pp": 0`):**
|
||||
|
||||
```
|
||||
Seed Words: [extracted from 'w' field]
|
||||
BIP39 Passphrase: None required
|
||||
```
|
||||
|
||||
**With BIP39 Passphrase (`pp": 1`):**
|
||||
|
||||
```
|
||||
Seed Words: [extracted from 'w' field]
|
||||
BIP39 Passphrase: [Your original 25th word/extra passphrase]
|
||||
```
|
||||
|
||||
**Wallet Recovery Steps:**
|
||||
|
||||
1. **Hardware Wallets (Ledger/Trezor):**
|
||||
- Start recovery process
|
||||
- Enter 12/24 word mnemonic
|
||||
- **If `pp": 1`:** Enable passphrase option and enter your BIP39 passphrase
|
||||
|
||||
2. **Software Wallets (Electrum, MetaMask, etc.):**
|
||||
- Create/restore wallet
|
||||
- Enter mnemonic phrase
|
||||
- **If `pp": 1`:** Look for "Advanced options" or "Passphrase" field
|
||||
|
||||
3. **Bitcoin Core (using `hdseed`):**
|
||||
|
||||
```bash
|
||||
# Use the mnemonic with appropriate BIP39 passphrase
|
||||
# Consult your wallet's specific recovery documentation
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 🛠️ GPG Setup (One-time)
|
||||
|
||||
**Mac (Homebrew):**
|
||||
|
||||
```bash
|
||||
brew install gnupg
|
||||
```
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
|
||||
```bash
|
||||
sudo apt update && sudo apt install gnupg
|
||||
```
|
||||
|
||||
**Fedora/RHEL/CentOS:**
|
||||
|
||||
```bash
|
||||
sudo dnf install gnupg
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
|
||||
- Download Gpg4win from <https://www.gpg4win.org/>
|
||||
- Install and use Kleopatra or command-line gpg
|
||||
|
||||
**Verify installation:**
|
||||
|
||||
```bash
|
||||
gpg --version
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
| Error | Likely Cause | Solution |
|
||||
|-------|-------------|----------|
|
||||
| `gpg: decryption failed: No secret key` | Wrong PGP private key or key not imported | Import correct private key: `gpg --import private-key.asc` |
|
||||
| `gpg: BAD decrypt` | Wrong passphrase (key passphrase or message password) | Verify you're using the correct passphrase |
|
||||
| `base45decode: command not found` | base45 CLI tool not installed | Use CyberChef or install: `npm install -g base45` |
|
||||
| `gpg: no valid OpenPGP data found` | Invalid Base45 decoding or corrupted payload | Verify Base45 decoding step, check for scanning errors |
|
||||
| `gpg: CRC error` | Frame corrupted during scanning/printing | Rescan QR code or use backup copy |
|
||||
| `gpg: packet(3) too short` | Truncated PGP binary | Ensure complete frame was captured |
|
||||
| JSON parsing error after decryption | Output not valid JSON | Check if decryption succeeded, may need different passphrase |
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
1. **Wrong encryption method:** Trying PGP decryption when symmetric password was used, or vice versa
|
||||
2. **BIP39 passphrase mismatch:** Forgetting the 25th word used during backup
|
||||
3. **Frame format errors:** Missing `SEEDPGP1:` prefix or incorrect colon separation
|
||||
|
||||
***
|
||||
|
||||
## 📦 Recovery Checklist
|
||||
|
||||
```
|
||||
[ ] Airgapped computer prepared (offline, clean OS)
|
||||
[ ] GPG installed and verified
|
||||
[ ] Base45 decoder available (CLI tool or CyberChef)
|
||||
[ ] SEEDPGP1 frame extracted and verified
|
||||
[ ] Base45 payload decoded to PGP binary
|
||||
[ ] CRC16 checksum verified (optional but recommended)
|
||||
[ ] Correct decryption method identified (PGP key vs password)
|
||||
[ ] Private key imported (if PGP encryption)
|
||||
[ ] Decryption successful with valid JSON output
|
||||
[ ] Mnemonic extracted from 'w' field
|
||||
[ ] BIP39 passphrase status checked ('pp' field)
|
||||
[ ] Appropriate BIP39 passphrase ready (if 'pp': 1)
|
||||
[ ] Wallet recovery tool selected (hardware/software wallet)
|
||||
[ ] Test recovery on testnet/small amount first
|
||||
[ ] Browser/terminal history cleared after recovery
|
||||
[ ] Original backup securely stored or destroyed after successful recovery
|
||||
[ ] Funds moved to new addresses after recovery
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## ⚠️ Security Best Practices
|
||||
|
||||
**Critical Security Measures:**
|
||||
|
||||
1. **Always use airgapped computer** for recovery operations
|
||||
2. **Never type mnemonics or passwords on internet-connected devices**
|
||||
3. **Clear clipboard and terminal history** after recovery
|
||||
4. **Test with small amounts** before recovering significant funds
|
||||
5. **Move funds to new addresses** after successful recovery
|
||||
6. **Destroy recovery materials** or store them separately from private keys
|
||||
|
||||
**Storage Recommendations:**
|
||||
|
||||
- Print QR code on archival paper or metal
|
||||
- Store playbook separately from private keys/passphrases
|
||||
- Use multiple geographically distributed backups
|
||||
- Consider Shamir's Secret Sharing for critical components
|
||||
|
||||
***
|
||||
|
||||
## 🔄 Alternative Recovery Methods
|
||||
|
||||
**Using the SeedPGP Web App (Online):**
|
||||
|
||||
1. Open <https://seedpgp.com> (or local instance)
|
||||
2. Switch to "Restore" tab
|
||||
3. Scan QR code or paste SEEDPGP1 frame
|
||||
4. Provide private key or message password
|
||||
5. App handles Base45 decoding, CRC verification, and decryption automatically
|
||||
|
||||
**Using Custom Script (Advanced):**
|
||||
|
||||
```python
|
||||
# Example Python recovery script (conceptual)
|
||||
import base45
|
||||
import gnupg
|
||||
import json
|
||||
|
||||
frame = "SEEDPGP1:0:58B5:2KO K0S-U. M:..."
|
||||
parts = frame.split(":", 3)
|
||||
crc_expected = parts[2]
|
||||
b45_payload = parts[3]
|
||||
|
||||
# Decode Base45
|
||||
pgp_binary = base45.b45decode(b45_payload)
|
||||
|
||||
# Decrypt with GPG
|
||||
gpg = gnupg.GPG()
|
||||
decrypted = gpg.decrypt(pgp_binary, passphrase="your-passphrase")
|
||||
|
||||
# Parse JSON
|
||||
data = json.loads(str(decrypted))
|
||||
print(f"Mnemonic: {data['w']}")
|
||||
print(f"BIP39 Passphrase used: {'YES' if data['pp'] == 1 else 'NO'}")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 📝 Technical Details
|
||||
|
||||
**Encryption Algorithms:**
|
||||
|
||||
- **PGP Encryption:** AES-256 (OpenPGP standard)
|
||||
- **Symmetric Encryption:** AES-256 with random session key
|
||||
- **CRC Algorithm:** CRC16-CCITT-FALSE (polynomial 0x1021)
|
||||
- **Encoding:** Base45 (RFC 9285)
|
||||
|
||||
**JSON Schema:**
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["v", "t", "w", "l", "pp"],
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer",
|
||||
"const": 1,
|
||||
"description": "Protocol version"
|
||||
},
|
||||
"t": {
|
||||
"type": "string",
|
||||
"const": "bip39",
|
||||
"description": "Data type (BIP39 mnemonic)"
|
||||
},
|
||||
"w": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]+( [a-z]+){11,23}$",
|
||||
"description": "BIP39 mnemonic words (lowercase, space-separated)"
|
||||
},
|
||||
"l": {
|
||||
"type": "string",
|
||||
"const": "en",
|
||||
"description": "Language (English)"
|
||||
},
|
||||
"pp": {
|
||||
"type": "integer",
|
||||
"enum": [0, 1],
|
||||
"description": "BIP39 passphrase flag: 0 = none, 1 = used"
|
||||
},
|
||||
"fpr": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Optional: Recipient key fingerprints"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Frame Validation Rules:**
|
||||
|
||||
1. Must start with `SEEDPGP1:`
|
||||
2. Frame version must be `0` (single frame)
|
||||
3. CRC16 must be 4 hex characters `[0-9A-F]{4}`
|
||||
4. Base45 payload must use valid Base45 alphabet
|
||||
5. Decoded PGP binary must pass CRC16 verification
|
||||
|
||||
***
|
||||
|
||||
## 🆘 Emergency Contact & Support
|
||||
|
||||
**No Technical Support Available:**
|
||||
|
||||
- SeedPGP is a self-sovereign tool with no central authority
|
||||
- You are solely responsible for your recovery
|
||||
- Test backups regularly to ensure they work
|
||||
|
||||
**Community Resources:**
|
||||
|
||||
- GitHub Issues: <https://github.com/kccleoc/seedpgp-web/issues>
|
||||
- Bitcoin StackExchange: Use `seedpgp` tag
|
||||
- Local Bitcoin meetups for in-person help
|
||||
|
||||
**Remember:** The security of your funds depends on your ability to successfully execute this recovery process. Practice with test backups before relying on it for significant amounts.
|
||||
|
||||
***
|
||||
|
||||
**Print this playbook on archival paper or metal. Store separately from encrypted backups and private keys.** 🔒
|
||||
|
||||
**Last Updated:** February 3, 2026
|
||||
**SeedPGP Version:** 1.4.7
|
||||
**Frame Example CRC:** 58B5 ✓
|
||||
**Test Recovery:** [ ] Completed [ ] Not Tested
|
||||
|
||||
***
|
||||
658
doc/offline_recovery_playbook.md
Normal file
658
doc/offline_recovery_playbook.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# 🆘 SeedPGP Offline Recovery Playbook
|
||||
|
||||
**EMERGENCY SEED RECOVERY WITHOUT THE SEEDPGP WEB APP**
|
||||
|
||||
---
|
||||
|
||||
## 📋 What This Document Is For
|
||||
|
||||
You created an encrypted backup of your cryptocurrency seed phrase using SeedPGP. This document explains **how to decrypt that backup if the SeedPGP web app is no longer available** (website down, GitHub deleted, domain expired, etc.).
|
||||
|
||||
**Print this document and store it with your encrypted QR backup.**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Reference: What You Need
|
||||
|
||||
Depending on how you encrypted your backup, you need:
|
||||
|
||||
| Encryption Method | What You Need to Decrypt |
|
||||
|-------------------|--------------------------|
|
||||
| **Password-only** | Password + this playbook + any computer with GPG |
|
||||
| **PGP Public Key** | Private key + private key passphrase + this playbook + any computer with GPG |
|
||||
| **Krux KEF format** | Passphrase + Python 3 + this playbook |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Step 1: Identify Your Backup Format
|
||||
|
||||
Look at your encrypted backup. The format determines which recovery method to use:
|
||||
|
||||
### **Format A: SeedPGP Standard (PGP)**
|
||||
|
||||
Your QR code or text starts with:
|
||||
```
|
||||
SEEDPGP1:0:A1B2:CDEFG...
|
||||
```
|
||||
|
||||
**OR** your backup is a PGP armored message:
|
||||
```
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hQEMA...
|
||||
-----END PGP MESSAGE-----
|
||||
```
|
||||
|
||||
➜ **Use Method 1: GPG Command-Line Recovery** (see below)
|
||||
|
||||
---
|
||||
|
||||
### **Format B: Krux KEF**
|
||||
|
||||
Your QR code or text starts with:
|
||||
```
|
||||
KEF:1234+ABCD...
|
||||
```
|
||||
|
||||
**OR** it's a Base43-encoded string using only these characters:
|
||||
```
|
||||
0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$%*+-./:
|
||||
```
|
||||
|
||||
➜ **Use Method 2: Python Krux Decryption** (see below)
|
||||
|
||||
---
|
||||
|
||||
### **Format C: Plain SeedQR** (NOT ENCRYPTED)
|
||||
|
||||
Your QR is all digits (48 or 96 digits for 12/24 words):
|
||||
```
|
||||
0216007100420461...
|
||||
```
|
||||
|
||||
**OR** all hex (32 or 64 hex characters):
|
||||
```
|
||||
1a2b3c4d5e6f...
|
||||
```
|
||||
|
||||
➜ **Use Method 3: SeedQR Decoder** (see below)
|
||||
|
||||
---
|
||||
|
||||
## 📖 Method 1: GPG Command-Line Recovery (PGP Format)
|
||||
|
||||
### **What You Need:**
|
||||
- ✅ Your encrypted backup (QR scan result or PGP armored text)
|
||||
- ✅ Your password (if password-encrypted)
|
||||
- ✅ Your PGP private key + passphrase (if key-encrypted)
|
||||
- ✅ A computer with GPG installed (Linux/Mac: pre-installed, Windows: download Gpg4win)
|
||||
|
||||
---
|
||||
|
||||
### **Step 1A: Extract the PGP Message**
|
||||
|
||||
If your backup is a `SEEDPGP1:...` QR string, you need to decode it first:
|
||||
|
||||
```bash
|
||||
# Your QR scan result looks like:
|
||||
# SEEDPGP1:0:A1B2:BASE45_ENCODED_DATA_HERE
|
||||
|
||||
# Extract just the Base45 part (everything after the third colon)
|
||||
echo "SEEDPGP1:0:A1B2:BASE45DATA" | cut -d: -f4- > base45.txt
|
||||
|
||||
# Decode Base45 to binary PGP (requires Python script - see Appendix A)
|
||||
python3 decode_base45.py base45.txt > encrypted.pgp
|
||||
```
|
||||
|
||||
If your backup is already a PGP armored message (`-----BEGIN PGP MESSAGE-----`), save it to a file:
|
||||
|
||||
```bash
|
||||
cat > encrypted.asc << 'EOF'
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hQEMA...
|
||||
-----END PGP MESSAGE-----
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Step 1B: Decrypt with GPG**
|
||||
|
||||
**If encrypted with PASSWORD only:**
|
||||
|
||||
```bash
|
||||
# Decrypt using password
|
||||
gpg --decrypt encrypted.pgp
|
||||
|
||||
# OR if you have the armored version:
|
||||
gpg --decrypt encrypted.asc
|
||||
|
||||
# GPG will prompt: "Enter passphrase:"
|
||||
# Type your password exactly as you created it
|
||||
# Output will be JSON like: {"v":1,"t":"bip39","w":"word1 word2 word3...","l":"en","pp":0}
|
||||
```
|
||||
|
||||
**If encrypted with PGP PUBLIC KEY:**
|
||||
|
||||
```bash
|
||||
# First, import your private key (if not already in your GPG keyring)
|
||||
gpg --import my-private-key.asc
|
||||
|
||||
# Decrypt
|
||||
gpg --decrypt encrypted.pgp
|
||||
|
||||
# GPG will prompt for your PRIVATE KEY PASSPHRASE (not the backup password)
|
||||
# Output will be JSON like: {"v":1,"t":"bip39","w":"word1 word2 word3...","l":"en","pp":0}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Step 1C: Extract Your Seed Phrase**
|
||||
|
||||
The decrypted output is JSON format:
|
||||
```json
|
||||
{"v":1,"t":"bip39","w":"abandon ability able about above...","l":"en","pp":0}
|
||||
```
|
||||
|
||||
**Your seed phrase is the value of the `"w"` field.**
|
||||
|
||||
Extract it:
|
||||
```bash
|
||||
# If output is in a file:
|
||||
cat decrypted.json | grep -o '"w":"[^"]*"' | cut -d'"' -f4
|
||||
|
||||
# Or use Python:
|
||||
python3 -c 'import json; print(json.load(open("decrypted.json"))["w"])'
|
||||
```
|
||||
|
||||
**Write down your seed phrase immediately on paper.**
|
||||
|
||||
---
|
||||
|
||||
### **JSON Field Meanings:**
|
||||
|
||||
| Field | Meaning | Example Value |
|
||||
|-------|---------|---------------|
|
||||
| `v` | Format version | `1` |
|
||||
| `t` | Mnemonic type | `"bip39"` |
|
||||
| `w` | **Your seed phrase (words)** | `"abandon ability able..."` |
|
||||
| `l` | Language | `"en"` (English) |
|
||||
| `pp` | BIP39 passphrase used? | `0` (no) or `1` (yes) |
|
||||
| `fpr` | Recipient PGP fingerprints | `["ABC123..."]` (optional) |
|
||||
|
||||
**If `pp` is `1`:** You used a BIP39 passphrase in addition to your seed words. You need BOTH to restore your wallet.
|
||||
|
||||
---
|
||||
|
||||
## 🐍 Method 2: Python Krux Decryption (KEF Format)
|
||||
|
||||
### **What You Need:**
|
||||
- ✅ Your Krux KEF backup (QR scan result starting with `KEF:` or Base43 string)
|
||||
- ✅ Your passphrase
|
||||
- ✅ A computer with Python 3
|
||||
|
||||
---
|
||||
|
||||
### **Step 2A: Prepare the Decryption Script**
|
||||
|
||||
Save this Python script as `decrypt_krux.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Krux KEF (Krux Encryption Format) Offline Decryption Tool
|
||||
For emergency recovery when SeedPGP is unavailable
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from getpass import getpass
|
||||
|
||||
# Base43 alphabet (Krux standard)
|
||||
B43_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$%*+-./:"
|
||||
|
||||
def base43_decode(s):
|
||||
"""Decode Base43 string to bytes"""
|
||||
n = 0
|
||||
for c in s:
|
||||
n = n * 43 + B43_CHARS.index(c)
|
||||
|
||||
byte_len = (n.bit_length() + 7) // 8
|
||||
return n.to_bytes(byte_len, 'big')
|
||||
|
||||
def unwrap_kef(kef_bytes):
|
||||
"""Extract label, version, iterations, and payload from KEF envelope"""
|
||||
if len(kef_bytes) < 5:
|
||||
raise ValueError("Invalid KEF: too short")
|
||||
|
||||
label_len = kef_bytes[0]
|
||||
if label_len > 252 or len(kef_bytes) < 1 + label_len + 4:
|
||||
raise ValueError("Invalid KEF: malformed header")
|
||||
|
||||
label = kef_bytes[1:1+label_len].decode('utf-8')
|
||||
version = kef_bytes[1+label_len]
|
||||
|
||||
iter_bytes = kef_bytes[2+label_len:5+label_len]
|
||||
iterations = int.from_bytes(iter_bytes, 'big')
|
||||
if iterations <= 10000:
|
||||
iterations *= 10000
|
||||
|
||||
payload = kef_bytes[5+label_len:]
|
||||
return label, version, iterations, payload
|
||||
|
||||
def pbkdf2_hmac_sha256(password, salt, iterations, dklen=32):
|
||||
"""PBKDF2-HMAC-SHA256 key derivation"""
|
||||
return hashlib.pbkdf2_hmac('sha256', password.encode(), salt, iterations, dklen)
|
||||
|
||||
def aes_gcm_decrypt(key, iv, ciphertext, tag):
|
||||
"""AES-GCM decryption using cryptography library"""
|
||||
try:
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
aesgcm = AESGCM(key)
|
||||
return aesgcm.decrypt(iv, ciphertext + tag, None)
|
||||
except ImportError:
|
||||
print("ERROR: 'cryptography' library not found.")
|
||||
print("Install with: pip3 install cryptography")
|
||||
raise
|
||||
|
||||
def entropy_to_mnemonic(entropy_bytes):
|
||||
"""Convert entropy to BIP39 mnemonic (requires bip39 library)"""
|
||||
try:
|
||||
from mnemonic import Mnemonic
|
||||
mnemo = Mnemonic("english")
|
||||
return mnemo.to_mnemonic(entropy_bytes)
|
||||
except ImportError:
|
||||
print("ERROR: 'mnemonic' library not found.")
|
||||
print("Install with: pip3 install mnemonic")
|
||||
raise
|
||||
|
||||
def decrypt_krux(kef_data, passphrase):
|
||||
"""Main decryption function"""
|
||||
# Step 1: Decode from Base43 or hex
|
||||
if kef_data.startswith('KEF:'):
|
||||
kef_data = kef_data[4:].strip()
|
||||
|
||||
try:
|
||||
kef_bytes = base43_decode(kef_data)
|
||||
except:
|
||||
try:
|
||||
kef_bytes = bytes.fromhex(kef_data)
|
||||
except:
|
||||
raise ValueError("Invalid KEF format: not Base43 or hex")
|
||||
|
||||
# Step 2: Unwrap KEF envelope
|
||||
label, version, iterations, payload = unwrap_kef(kef_bytes)
|
||||
|
||||
if version not in [20, 21]:
|
||||
raise ValueError(f"Unsupported KEF version: {version}")
|
||||
|
||||
print(f"KEF Label: {label}")
|
||||
print(f"Version: {version} (AES-GCM{' +compress' if version == 21 else ''})")
|
||||
print(f"Iterations: {iterations}")
|
||||
|
||||
# Step 3: Derive key from passphrase
|
||||
salt = label.encode('utf-8')
|
||||
key = pbkdf2_hmac_sha256(passphrase, salt, iterations, 32)
|
||||
|
||||
# Step 4: Extract IV, ciphertext, and tag
|
||||
iv = payload[:12]
|
||||
ciphertext = payload[12:-4]
|
||||
tag = payload[-4:]
|
||||
|
||||
# Step 5: Decrypt
|
||||
decrypted = aes_gcm_decrypt(key, iv, ciphertext, tag)
|
||||
|
||||
# Step 6: Decompress if needed
|
||||
if version == 21:
|
||||
import zlib
|
||||
decrypted = zlib.decompress(decrypted)
|
||||
|
||||
# Step 7: Convert to mnemonic
|
||||
mnemonic = entropy_to_mnemonic(decrypted)
|
||||
return mnemonic
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("KRUX KEF EMERGENCY DECRYPTION TOOL")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
kef_input = input("Paste your KEF backup (Base43 or hex): ").strip()
|
||||
passphrase = getpass("Enter passphrase: ")
|
||||
|
||||
try:
|
||||
mnemonic = decrypt_krux(kef_input, passphrase)
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("SUCCESS! Your seed phrase:")
|
||||
print("=" * 60)
|
||||
print(mnemonic)
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("⚠️ Write this down on paper immediately!")
|
||||
print("⚠️ Never save to disk or take a screenshot!")
|
||||
|
||||
except Exception as e:
|
||||
print()
|
||||
print(f"ERROR: {e}")
|
||||
print()
|
||||
print("Common issues:")
|
||||
print("1. Wrong passphrase")
|
||||
print("2. Missing Python libraries (run: pip3 install cryptography mnemonic)")
|
||||
print("3. Corrupted KEF data")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Step 2B: Install Dependencies**
|
||||
|
||||
```bash
|
||||
# Install required Python libraries
|
||||
pip3 install cryptography mnemonic
|
||||
|
||||
# Or on Ubuntu/Debian:
|
||||
sudo apt install python3-pip
|
||||
pip3 install cryptography mnemonic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Step 2C: Run the Decryption**
|
||||
|
||||
```bash
|
||||
# Make script executable
|
||||
chmod +x decrypt_krux.py
|
||||
|
||||
# Run it
|
||||
python3 decrypt_krux.py
|
||||
|
||||
# It will prompt:
|
||||
# "Paste your KEF backup (Base43 or hex):"
|
||||
# → Paste your full QR scan result or KEF string
|
||||
|
||||
# "Enter passphrase:"
|
||||
# → Type your passphrase (won't show on screen)
|
||||
|
||||
# Output:
|
||||
# SUCCESS! Your seed phrase:
|
||||
# abandon ability able about above absent absorb...
|
||||
```
|
||||
|
||||
**Write down your seed phrase immediately.**
|
||||
|
||||
---
|
||||
|
||||
## 📱 Method 3: SeedQR Decoder (Unencrypted Format)
|
||||
|
||||
### **What You Need:**
|
||||
- ✅ Your SeedQR backup (all digits or hex)
|
||||
- ✅ BIP39 wordlist (see Appendix B)
|
||||
- ✅ Optional: Python script (see below)
|
||||
|
||||
---
|
||||
|
||||
### **Step 3A: Identify SeedQR Type**
|
||||
|
||||
**Standard SeedQR (all digits):**
|
||||
- 48 digits = 12-word seed
|
||||
- 96 digits = 24-word seed
|
||||
- Each 4 digits = one BIP39 word index (0000-2047)
|
||||
|
||||
**Compact SeedQR (hex):**
|
||||
- 32 hex chars = 12-word seed
|
||||
- 64 hex chars = 24-word seed
|
||||
- Raw entropy encoded as hexadecimal
|
||||
|
||||
---
|
||||
|
||||
### **Step 3B: Manual Decoding (Standard SeedQR)**
|
||||
|
||||
**Example:** `0216007100420461...` (48 digits for 12 words)
|
||||
|
||||
1. Split into 4-digit chunks: `0216`, `0071`, `0042`, `0461`, ...
|
||||
2. Each chunk is a word index (0-2047)
|
||||
3. Look up each index in the BIP39 wordlist (Appendix B)
|
||||
|
||||
```
|
||||
0216 → word #216 = "brick"
|
||||
0071 → word #71 = "appear"
|
||||
0042 → word #42 = "advise"
|
||||
0461 → word #461 = "dove"
|
||||
...
|
||||
```
|
||||
|
||||
**Your seed phrase is the words in order.**
|
||||
|
||||
---
|
||||
|
||||
### **Step 3C: Python Script (Standard SeedQR)**
|
||||
|
||||
Save as `decode_seedqr.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""SeedQR to BIP39 Mnemonic Decoder"""
|
||||
|
||||
# BIP39 English wordlist (2048 words)
|
||||
# Download from: https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt
|
||||
# Or see Appendix B of this document
|
||||
|
||||
def load_wordlist(filepath='bip39_wordlist.txt'):
|
||||
with open(filepath) as f:
|
||||
return [line.strip() for line in f]
|
||||
|
||||
def decode_standard_seedqr(qr_digits):
|
||||
"""Decode standard SeedQR (4-digit word indices)"""
|
||||
if len(qr_digits) not in [48, 96]:
|
||||
raise ValueError(f"Invalid length: {len(qr_digits)} (expected 48 or 96)")
|
||||
|
||||
wordlist = load_wordlist()
|
||||
mnemonic = []
|
||||
|
||||
for i in range(0, len(qr_digits), 4):
|
||||
index = int(qr_digits[i:i+4])
|
||||
if index >= 2048:
|
||||
raise ValueError(f"Invalid word index: {index} (max 2047)")
|
||||
mnemonic.append(wordlist[index])
|
||||
|
||||
return ' '.join(mnemonic)
|
||||
|
||||
def decode_compact_seedqr(qr_hex):
|
||||
"""Decode compact SeedQR (hex-encoded entropy)"""
|
||||
if len(qr_hex) not in [32, 64]:
|
||||
raise ValueError(f"Invalid hex length: {len(qr_hex)} (expected 32 or 64)")
|
||||
|
||||
try:
|
||||
from mnemonic import Mnemonic
|
||||
mnemo = Mnemonic("english")
|
||||
entropy = bytes.fromhex(qr_hex)
|
||||
return mnemo.to_mnemonic(entropy)
|
||||
except ImportError:
|
||||
print("ERROR: 'mnemonic' library required for compact SeedQR")
|
||||
print("Install: pip3 install mnemonic")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
qr_data = input("Paste your SeedQR data: ").strip()
|
||||
|
||||
if qr_data.isdigit():
|
||||
mnemonic = decode_standard_seedqr(qr_data)
|
||||
elif all(c in '0123456789abcdefABCDEF' for c in qr_data):
|
||||
mnemonic = decode_compact_seedqr(qr_data)
|
||||
else:
|
||||
print("ERROR: Not a valid SeedQR (must be all digits or all hex)")
|
||||
exit(1)
|
||||
|
||||
print()
|
||||
print("Your seed phrase:")
|
||||
print(mnemonic)
|
||||
print()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Appendix A: Base45 Decoder
|
||||
|
||||
If you need to decode SeedPGP's Base45 format manually, save this as `decode_base45.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Base45 Decoder for SeedPGP Recovery"""
|
||||
|
||||
B45_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
|
||||
|
||||
def base45_decode(s):
|
||||
"""Decode Base45 string to bytes"""
|
||||
result = []
|
||||
i = 0
|
||||
|
||||
while i < len(s):
|
||||
if i + 2 < len(s):
|
||||
# Process 3 characters → 2 bytes
|
||||
c = B45_CHARS.index(s[i])
|
||||
d = B45_CHARS.index(s[i+1])
|
||||
e = B45_CHARS.index(s[i+2])
|
||||
x = c + d * 45 + e * 45 * 45
|
||||
|
||||
result.append(x // 256)
|
||||
result.append(x % 256)
|
||||
i += 3
|
||||
else:
|
||||
# Process 2 characters → 1 byte
|
||||
c = B45_CHARS.index(s[i])
|
||||
d = B45_CHARS.index(s[i+1])
|
||||
x = c + d * 45
|
||||
|
||||
if x > 255:
|
||||
raise ValueError("Invalid Base45 encoding")
|
||||
result.append(x)
|
||||
i += 2
|
||||
|
||||
return bytes(result)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
# Read from file
|
||||
with open(sys.argv[1]) as f:
|
||||
b45_input = f.read().strip()
|
||||
else:
|
||||
# Read from stdin
|
||||
b45_input = input("Paste Base45 data: ").strip()
|
||||
|
||||
try:
|
||||
decoded = base45_decode(b45_input)
|
||||
sys.stdout.buffer.write(decoded)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}", file=sys.stderr)
|
||||
exit(1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Appendix B: BIP39 English Wordlist
|
||||
|
||||
**Download the official wordlist:**
|
||||
```bash
|
||||
wget https://raw.githubusercontent.com/bitcoin/bips/master/bip-0039/english.txt
|
||||
```
|
||||
|
||||
**Or manually:** The BIP39 English wordlist contains exactly 2048 words, indexed 0-2047:
|
||||
|
||||
```
|
||||
0000: abandon
|
||||
0001: ability
|
||||
0002: able
|
||||
0003: about
|
||||
...
|
||||
2045: zero
|
||||
2046: zone
|
||||
2047: zoo
|
||||
```
|
||||
|
||||
**Full wordlist:** https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Security Recommendations
|
||||
|
||||
### **When Recovering Your Seed:**
|
||||
|
||||
1. ✅ **Use an air-gapped computer** (TailsOS or Ubuntu Live USB)
|
||||
2. ✅ **Disconnect from internet** before decrypting
|
||||
3. ✅ **Write seed on paper immediately** after decryption
|
||||
4. ✅ **Never screenshot or save to disk**
|
||||
5. ✅ **Verify your seed** by importing to a test wallet (small amount first)
|
||||
6. ✅ **Destroy digital traces** after recovery (shutdown amnesic OS)
|
||||
|
||||
### **Storage Best Practices:**
|
||||
|
||||
- 📄 **Print this document** and store with your encrypted backup
|
||||
- 🔐 Store backup + recovery instructions in different locations
|
||||
- 💾 Keep a copy of Python scripts on offline USB
|
||||
- 📦 Include a copy of the BIP39 wordlist (offline reference)
|
||||
- 🗂️ Label everything clearly: "SEEDPGP BACKUP + RECOVERY GUIDE - DO NOT LOSE"
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Emergency Contact
|
||||
|
||||
**If you're stuck:**
|
||||
|
||||
1. Search GitHub for "seedpgp-web" (project may still exist)
|
||||
2. Check Internet Archive: https://web.archive.org/
|
||||
3. Ask in Bitcoin/crypto forums (describe format, don't share actual data!)
|
||||
4. Hire a professional cryptocurrency recovery service (last resort)
|
||||
|
||||
**Never share:**
|
||||
- ❌ Your encrypted backup data with strangers
|
||||
- ❌ Your password or passphrase
|
||||
- ❌ Your PGP private key
|
||||
- ❌ Your decrypted seed phrase
|
||||
|
||||
---
|
||||
|
||||
## ✅ Recovery Checklist
|
||||
|
||||
Before attempting recovery, verify you have:
|
||||
|
||||
- [ ] This printed playbook
|
||||
- [ ] Your encrypted backup (QR code or text file)
|
||||
- [ ] Your password/passphrase written down
|
||||
- [ ] Your PGP private key (if used) + passphrase
|
||||
- [ ] An air-gapped computer (TailsOS/Ubuntu Live recommended)
|
||||
- [ ] GPG installed (for PGP decryption)
|
||||
- [ ] Python 3 + libraries (for Krux/SeedQR decryption)
|
||||
- [ ] BIP39 wordlist (for manual SeedQR decoding)
|
||||
- [ ] Paper and pen (to write recovered seed)
|
||||
|
||||
**If missing any item above, DO NOT PROCEED. Secure it first.**
|
||||
|
||||
---
|
||||
|
||||
## 📅 Recommended Practice Schedule
|
||||
|
||||
**Every 6 months:**
|
||||
1. Test that you can still decrypt your backup
|
||||
2. Verify the recovery tools still work
|
||||
3. Update this playbook if formats change
|
||||
4. Check that your passwords/keys are still accessible
|
||||
|
||||
**Test with a dummy backup first!** Create a test seed, encrypt it, then practice recovery.
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** February 2026
|
||||
**Compatible with:** SeedPGP v1.4.7+
|
||||
|
||||
---
|
||||
|
||||
**🔒 KEEP THIS DOCUMENT SAFE AND ACCESSIBLE 🔒**
|
||||
|
||||
Your encrypted backup is worthless without the ability to decrypt it.
|
||||
Print this. Store it with your backup. Test it regularly.
|
||||
|
||||
---
|
||||
@@ -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",
|
||||
|
||||
85
src/App.tsx
85
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,17 @@ function App() {
|
||||
onSeedReceived={() => setSeedForBlender('')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={activeTab === 'test-recovery' ? 'block' : 'hidden'}>
|
||||
<TestRecovery
|
||||
encryptionMode={encryptionMode}
|
||||
backupMessagePassword={backupMessagePassword}
|
||||
restoreMessagePassword={restoreMessagePassword}
|
||||
publicKeyInput={publicKeyInput}
|
||||
privateKeyInput={privateKeyInput}
|
||||
privateKeyPassphrase={privateKeyPassphrase}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security Panel */}
|
||||
@@ -1215,8 +1272,28 @@ function App() {
|
||||
{qrPayload && activeTab === 'backup' && (
|
||||
<div className="pt-6 border-t border-[#00f0ff]/20 space-y-6 animate-in fade-in slide-in-from-bottom-4">
|
||||
<div className={isReadOnly ? 'blur-lg' : ''}>
|
||||
<QrDisplay value={qrPayload} />
|
||||
<QrDisplay value={qrPayload} encryptionMode={encryptionMode} fingerprint={recipientFpr} />
|
||||
</div>
|
||||
|
||||
{/* Download Recovery Kit Button */}
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
onClick={handleDownloadRecoveryKit}
|
||||
disabled={!qrPayload || loading || isReadOnly}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#00f0ff] to-[#00c8ff] text-[#0a0a0f] rounded-xl font-bold text-sm uppercase tracking-wider hover:shadow-[0_0_30px_rgba(0,240,255,0.5)] transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<Package size={20} />
|
||||
)}
|
||||
{loading ? 'Generating...' : '📦 Download Recovery Kit'}
|
||||
</button>
|
||||
<p className="text-xs text-[#6ef3f7] text-center">
|
||||
Contains encrypted backup, recovery scripts, instructions, and BIP39 wordlist
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
|
||||
|
||||
@@ -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<HeaderProps> = ({
|
||||
</div>
|
||||
|
||||
{/* ROW 3: Navigation Tabs */}
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
<button
|
||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'create'
|
||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
@@ -157,6 +157,17 @@ const Header: React.FC<HeaderProps> = ({
|
||||
>
|
||||
Blender
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'test-recovery'
|
||||
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
|
||||
: 'bg-[#0a0a0f] text-[#9d84b7] border-2 border-[#00f0ff30] hover:text-[#6ef3f7] hover:border-[#00f0ff50]'
|
||||
}`}
|
||||
style={activeTab === 'test-recovery' ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
|
||||
onClick={() => onRequestTabChange('test-recovery')}
|
||||
>
|
||||
🧪 Test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -4,9 +4,11 @@ import QRCode from 'qrcode';
|
||||
|
||||
interface QrDisplayProps {
|
||||
value: string | Uint8Array;
|
||||
encryptionMode?: 'pgp' | 'krux' | 'seedqr';
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
export const QrDisplay: React.FC<QrDisplayProps> = ({ value, encryptionMode = 'pgp', fingerprint }) => {
|
||||
const [dataUrl, setDataUrl] = useState('');
|
||||
const [debugInfo, setDebugInfo] = useState('');
|
||||
|
||||
@@ -94,8 +96,15 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ 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 (
|
||||
<div className="border-4 border-[#00f0ff] rounded-xl shadow-[0_0_40px_rgba(0,240,255,0.6)] p-4 bg-[#0a0a0f] space-y-4">
|
||||
<div className="border-4 border-[#00f0ff] rounded-xl shadow-[0_0_40px_rgba(0,240,255,0.6)] p-4 bg-[#0a0a0f] space-y-4 qr-container">
|
||||
<div className="bg-[#16213e] p-6 rounded-lg inline-block shadow-[0_0_20px_rgba(0,240,255,0.3)] border-2 border-[#00f0ff]/30">
|
||||
<img src={dataUrl} alt="QR Code" className="w-full h-auto" />
|
||||
</div>
|
||||
@@ -106,6 +115,27 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* NEW: Metadata below QR */}
|
||||
<div className="bg-[#0a0a0f] border border-[#00f0ff]/30 rounded-lg p-3 text-xs font-mono qr-metadata">
|
||||
<div className="grid grid-cols-2 gap-2 text-[#6ef3f7]">
|
||||
<div>Format:</div>
|
||||
<div className="text-[#00f0ff] font-bold">{metadata.format}</div>
|
||||
|
||||
<div>Created:</div>
|
||||
<div className="text-[#00f0ff]">{metadata.created}</div>
|
||||
|
||||
{fingerprint && (
|
||||
<>
|
||||
<div>PGP Key:</div>
|
||||
<div className="text-[#00f0ff] break-all">{metadata.fingerprint.slice(0, 16)}...</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="whitespace-nowrap">Recovery Guide:</div>
|
||||
<div className="text-[#00f0ff] break-all text-right min-w-0">{metadata.recovery_url}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#00f0ff] hover:bg-[#00f0ff]/80 text-[#0a0a0f] rounded-lg transition-all hover:shadow-[0_0_15px_rgba(0,240,255,0.5)]"
|
||||
@@ -114,6 +144,10 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
||||
Download QR Code
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-[#6ef3f7] text-center">
|
||||
💡 Screenshot this entire area (QR + metadata) for easy identification
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-[#6ef3f7]">
|
||||
Downloads as: SeedPGP_{new Date().toISOString().split('T')[0]}_HHMMSS.png
|
||||
</p>
|
||||
|
||||
748
src/components/TestRecovery.tsx
Normal file
748
src/components/TestRecovery.tsx
Normal file
@@ -0,0 +1,748 @@
|
||||
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' | 'path-select' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete';
|
||||
type PracticePath = 'pgp' | 'krux' | 'seedqr' | 'encrypt-seedqr';
|
||||
|
||||
interface TestRecoveryProps {
|
||||
encryptionMode?: EncryptionMode;
|
||||
backupMessagePassword?: string;
|
||||
restoreMessagePassword?: string;
|
||||
publicKeyInput?: string;
|
||||
privateKeyInput?: string;
|
||||
privateKeyPassphrase?: string;
|
||||
}
|
||||
|
||||
export const TestRecovery: React.FC<TestRecoveryProps> = ({
|
||||
encryptionMode: externalEncryptionMode = 'pgp',
|
||||
backupMessagePassword: externalBackupPassword = '',
|
||||
restoreMessagePassword: externalRestorePassword = '',
|
||||
publicKeyInput: externalPublicKey = '',
|
||||
privateKeyInput: externalPrivateKey = '',
|
||||
privateKeyPassphrase: externalPrivateKeyPassphrase = '',
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState<TestStep>('intro');
|
||||
const [selectedPath, setSelectedPath] = useState<PracticePath>('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<string>('');
|
||||
const [showRecoveryKitDetails, setShowRecoveryKitDetails] = useState(false);
|
||||
const [showRecoveryInstructions, setShowRecoveryInstructions] = useState(false);
|
||||
const [useExternalSettings, setUseExternalSettings] = useState(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('');
|
||||
|
||||
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);
|
||||
setCurrentStep('download');
|
||||
} catch (err: any) {
|
||||
setError(`Failed to encrypt dummy seed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadRecoveryKit = async () => {
|
||||
try {
|
||||
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: 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-${selectedPath}-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('');
|
||||
|
||||
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');
|
||||
} 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');
|
||||
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: <Key className="w-6 h-6" />
|
||||
};
|
||||
case 'krux':
|
||||
return {
|
||||
title: 'KRUX Path',
|
||||
description: 'Practice with Krux KEF format (passphrase-based encryption)',
|
||||
icon: <Shield className="w-6 h-6" />
|
||||
};
|
||||
case 'seedqr':
|
||||
return {
|
||||
title: 'SEED QR Path',
|
||||
description: 'Practice with unencrypted SeedQR format (QR code only)',
|
||||
icon: <QrCode className="w-6 h-6" />
|
||||
};
|
||||
case 'encrypt-seedqr':
|
||||
return {
|
||||
title: 'SEED QR (Encrypt) Path',
|
||||
description: 'Practice with encrypted SeedQR (encrypt then QR encode)',
|
||||
icon: <Lock className="w-6 h-6" />
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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 <paste_qr_data_here>
|
||||
\`\`\`
|
||||
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 "<scanned_data>" | gpg --decrypt
|
||||
\`\`\`
|
||||
4. **Enter your password** when prompted
|
||||
5. **Write down the recovered seed** on paper`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-0 py-6">
|
||||
<div className="bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 p-6">
|
||||
<h2 className="text-2xl font-bold text-[#00f0ff] mb-4">
|
||||
🧪 Test Your Recovery Ability
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-[#1a1a2e] border-2 border-[#ff006e] rounded-lg text-[#ff006e] text-sm shadow-[0_0_20px_rgba(255,0,110,0.3)] flex gap-3 items-start mb-4">
|
||||
<AlertCircle className="shrink-0 mt-0.5" size={20} />
|
||||
<div>
|
||||
<p className="font-bold mb-1">Error</p>
|
||||
<p className="whitespace-pre-wrap">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'intro' && (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<p className="text-[#6ef3f7]">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="bg-[#0a0a0f] border border-[#ff006e] rounded-lg p-4">
|
||||
<h3 className="text-[#ff006e] font-bold mb-2">What You'll Learn:</h3>
|
||||
<ul className="text-sm text-[#6ef3f7] space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[#39ff14] shrink-0 mt-0.5" />
|
||||
<span>How to operate the recovery kit files</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[#39ff14] shrink-0 mt-0.5" />
|
||||
<span>Different recovery methods for PGP, KRUX, and SEED QR</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[#39ff14] shrink-0 mt-0.5" />
|
||||
<span>How to decrypt backups without the SeedPGP website</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 className="w-4 h-4 text-[#39ff14] shrink-0 mt-0.5" />
|
||||
<span>Offline recovery procedures</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 p-4 bg-[#16213e] border border-[#00f0ff]/30 rounded-lg">
|
||||
<Info className="w-5 h-5 text-[#00f0ff] shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
<strong className="text-[#00f0ff]">Tip:</strong> You can use the security settings from the main app or use test defaults.
|
||||
Practice multiple paths to become proficient with all recovery methods.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold text-[#00f0ff]">Settings</h3>
|
||||
<button
|
||||
onClick={() => setUseExternalSettings(!useExternalSettings)}
|
||||
className={`px-4 py-2 rounded-lg flex items-center gap-2 ${useExternalSettings ? 'bg-[#00f0ff] text-[#0a0a0f]' : 'bg-[#16213e] border-2 border-[#00f0ff]/50 text-[#00f0ff]'}`}
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
{useExternalSettings ? 'Using App Settings' : 'Use Test Defaults'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{useExternalSettings && (
|
||||
<div className="p-4 bg-[#0a0a0f] rounded-lg border border-[#00f0ff]/30">
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
Using security settings from the main app: <strong className="text-[#00f0ff]">{encryptionMode}</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentStep('path-select')}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-xl font-bold uppercase flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<PlayCircle size={20} />
|
||||
)}
|
||||
{loading ? 'Loading...' : 'Start Test Recovery Drill'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'path-select' && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-xl font-bold text-[#00f0ff] mb-2">Choose Practice Path</h3>
|
||||
<p className="text-[#6ef3f7]">
|
||||
Select which encryption method you want to practice recovering from:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{(['pgp', 'krux', 'seedqr', 'encrypt-seedqr'] as PracticePath[]).map((path) => {
|
||||
const desc = getPathDescription(path);
|
||||
return (
|
||||
<button
|
||||
key={path}
|
||||
onClick={() => {
|
||||
setSelectedPath(path);
|
||||
setCurrentStep('generate');
|
||||
}}
|
||||
className={`p-4 rounded-xl border-2 text-left transition-all ${selectedPath === path
|
||||
? 'bg-[#1a1a2e] border-[#ff006e] shadow-[0_0_20px_rgba(255,0,110,0.5)]'
|
||||
: 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff] hover:bg-[#1a1a2e]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`p-2 rounded-lg ${selectedPath === path ? 'bg-[#ff006e] text-white' : 'bg-[#00f0ff]/20 text-[#00f0ff]'}`}>
|
||||
{desc.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-[#00f0ff]">{desc.title}</h4>
|
||||
<p className="text-xs text-[#6ef3f7] mt-1">{desc.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setCurrentStep('intro')}
|
||||
className="flex-1 py-3 bg-[#16213e] border-2 border-[#00f0ff]/50 text-[#00f0ff] rounded-xl font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'generate' && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-[#00f0ff]/20 rounded-lg">
|
||||
{getPathDescription(selectedPath).icon}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-[#00f0ff] font-bold">Practicing: {getPathDescription(selectedPath).title}</h3>
|
||||
<p className="text-xs text-[#6ef3f7]">{getPathDescription(selectedPath).description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 1: Dummy Seed Generated</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-xs text-[#6ef3f7] mb-2">Test Seed (DO NOT USE FOR REAL FUNDS):</p>
|
||||
<p className="font-mono text-sm text-[#00f0ff]">{dummySeed}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={encryptDummySeed}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-[#00f0ff] text-[#0a0a0f] rounded-xl font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<Lock size={20} />
|
||||
)}
|
||||
{loading ? 'Encrypting...' : 'Next: Encrypt This Seed'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'encrypt' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 2: Seed Encrypted</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-xs text-[#6ef3f7] mb-2">Encryption Details:</p>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<span className="text-xs text-[#6ef3f7]">Path: </span>
|
||||
<span className="text-sm text-[#00f0ff] font-bold">{getPathDescription(selectedPath).title}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-[#6ef3f7]">Method: </span>
|
||||
<span className="text-sm text-[#00f0ff]">
|
||||
{selectedPath === 'seedqr' ? 'Unencrypted QR' :
|
||||
selectedPath === 'encrypt-seedqr' ? 'Encrypted QR' :
|
||||
publicKeyInput ? 'PGP Public Key' : 'Password-based'}
|
||||
</span>
|
||||
</div>
|
||||
{backupMessagePassword && (
|
||||
<div>
|
||||
<span className="text-xs text-[#6ef3f7]">Password: </span>
|
||||
<span className="font-mono text-sm text-[#00f0ff]">{backupMessagePassword}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadRecoveryKit}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#00f0ff] to-[#00c8ff] text-[#0a0a0f] rounded-xl font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<Package size={20} />
|
||||
)}
|
||||
{loading ? 'Generating...' : 'Next: Download Recovery Kit'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'download' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 3: Recovery Kit Downloaded</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
The recovery kit ZIP file has been downloaded to your computer.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<button
|
||||
onClick={() => setShowRecoveryKitDetails(!showRecoveryKitDetails)}
|
||||
className="w-full py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 text-[#00f0ff] rounded-lg flex items-center justify-center gap-2"
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
{showRecoveryKitDetails ? 'Hide Kit Contents' : 'Show Kit Contents'}
|
||||
</button>
|
||||
|
||||
{showRecoveryKitDetails && (
|
||||
<div className="p-3 bg-[#0a0a0f] border border-[#00f0ff]/30 rounded-lg">
|
||||
<h4 className="text-xs font-bold text-[#00f0ff] mb-2">Recovery Kit Contents:</h4>
|
||||
<ul className="text-xs text-[#6ef3f7] space-y-1">
|
||||
{getRecoveryKitFiles().map((file, index) => (
|
||||
<li key={index} className="flex items-start gap-2">
|
||||
<ChevronRight className="w-3 h-3 text-[#00f0ff] shrink-0 mt-0.5" />
|
||||
<span>{file}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setShowRecoveryInstructions(!showRecoveryInstructions)}
|
||||
className="w-full py-2 bg-[#16213e] border-2 border-[#39ff14]/50 text-[#39ff14] rounded-lg flex items-center justify-center gap-2"
|
||||
>
|
||||
<BookOpen size={16} />
|
||||
{showRecoveryInstructions ? 'Hide Instructions' : 'Show Recovery Instructions'}
|
||||
</button>
|
||||
|
||||
{showRecoveryInstructions && (
|
||||
<div className="p-3 bg-[#0a0a0f] border border-[#39ff14]/30 rounded-lg">
|
||||
<h4 className="text-xs font-bold text-[#39ff14] mb-2">How to Use Recovery Kit:</h4>
|
||||
<pre className="text-xs text-[#6ef3f7] whitespace-pre-wrap font-mono">
|
||||
{getRecoveryInstructions()}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={clearDummySeed}
|
||||
className="w-full py-3 bg-[#ff006e] text-white rounded-xl font-bold"
|
||||
>
|
||||
Next: Clear Seed & Test Recovery
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'clear' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 4: Seed Cleared</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
The dummy seed has been cleared from browser memory. This simulates what would happen if:
|
||||
</p>
|
||||
<ul className="text-xs text-[#6ef3f7] list-disc list-inside mt-2 space-y-1">
|
||||
<li>The SeedPGP website goes down</li>
|
||||
<li>You lose access to this browser</li>
|
||||
<li>You need to recover from the backup alone</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={recoverFromBackup}
|
||||
disabled={loading}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white rounded-xl font-bold flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="animate-spin" size={20} />
|
||||
) : (
|
||||
<Unlock size={20} />
|
||||
)}
|
||||
{loading ? 'Decrypting...' : 'Next: Recover Seed from Backup'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'recover' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 5: Seed Recovered</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-xs text-[#6ef3f7] mb-2">Recovered Seed:</p>
|
||||
<p className="font-mono text-sm text-[#00f0ff]">{recoveredSeed}</p>
|
||||
<p className="text-xs text-[#6ef3f7] mt-2">
|
||||
The seed has been successfully decrypted from the backup using the {selectedPath === 'seedqr' ? 'QR decoding' : 'password'}.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={verifyRecovery}
|
||||
className="w-full py-3 bg-[#39ff14] text-[#0a0a0f] rounded-xl font-bold"
|
||||
>
|
||||
Next: Verify Recovery
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'verify' && (
|
||||
<div className="space-y-4">
|
||||
<CheckCircle2 className="text-[#39ff14]" size={32} />
|
||||
<h3 className="text-[#39ff14] font-bold">Step 6: Verification</h3>
|
||||
<div className="bg-[#0a0a0f] p-4 rounded-lg">
|
||||
<p className="text-sm text-[#6ef3f7]">
|
||||
Comparing original seed with recovered seed...
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4 mt-3">
|
||||
<div>
|
||||
<p className="text-xs text-[#6ef3f7] mb-1">Original:</p>
|
||||
<p className="font-mono text-xs text-[#00f0ff] truncate">{dummySeed}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-[#6ef3f7] mb-1">Recovered:</p>
|
||||
<p className="font-mono text-xs text-[#00f0ff] truncate">{recoveredSeed}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={verifyRecovery}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#39ff14] to-[#00ff88] text-[#0a0a0f] rounded-xl font-bold"
|
||||
>
|
||||
Verify Match
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'complete' && (
|
||||
<div className="space-y-4 text-center">
|
||||
<CheckCircle2 className="text-[#39ff14] mx-auto" size={64} />
|
||||
<h3 className="text-2xl font-bold text-[#39ff14]">🎉 Test Passed!</h3>
|
||||
<p className="text-[#6ef3f7]">
|
||||
You've successfully proven you can recover a seed phrase from an encrypted backup.
|
||||
You're ready to trust this system with real funds.
|
||||
</p>
|
||||
<div className="bg-[#0a0a0f] border border-[#39ff14] rounded-lg p-4 mt-4">
|
||||
<h4 className="text-[#39ff14] font-bold mb-2">Key Takeaways:</h4>
|
||||
<ul className="text-sm text-[#6ef3f7] space-y-1 text-left">
|
||||
<li>✅ You can decrypt backups without the SeedPGP website</li>
|
||||
<li>✅ The recovery kit contains everything needed</li>
|
||||
<li>✅ You understand the recovery process for {getPathDescription(selectedPath).title}</li>
|
||||
<li>✅ Your real backups are recoverable</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={resetTest}
|
||||
className="py-3 px-6 bg-[#16213e] border-2 border-[#00f0ff] text-[#00f0ff] rounded-xl font-bold flex items-center justify-center gap-2 mx-auto"
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
Run Test Again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="mt-6 pt-4 border-t border-[#00f0ff]/20">
|
||||
<div className="flex justify-between text-xs text-[#6ef3f7] mb-2">
|
||||
<span>Progress:</span>
|
||||
<span>
|
||||
{currentStep === 'intro' && '0/7'}
|
||||
{currentStep === 'path-select' && '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'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[#0a0a0f] rounded-full h-2">
|
||||
<div
|
||||
className="bg-gradient-to-r from-[#00f0ff] to-[#00c8ff] h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: currentStep === 'intro' || currentStep === 'path-select' ? '0%' :
|
||||
currentStep === 'generate' ? '14%' :
|
||||
currentStep === 'encrypt' ? '28%' :
|
||||
currentStep === 'download' ? '42%' :
|
||||
currentStep === 'clear' ? '57%' :
|
||||
currentStep === 'recover' ? '71%' :
|
||||
currentStep === 'verify' ? '85%' :
|
||||
'100%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
326
src/lib/recoveryKit.ts
Normal file
326
src/lib/recoveryKit.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import JSZip from 'jszip';
|
||||
import { EncryptionMode } from './types';
|
||||
|
||||
// Top-level imports (Vite bundles them as strings)
|
||||
import bip39WordlistRaw from '../bip39_wordlist.txt?raw';
|
||||
import offlinePlaybookRaw from '../../doc/offline_recovery_playbook.md?raw';
|
||||
|
||||
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<Blob> {
|
||||
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 offline recovery playbook
|
||||
zip.file('OFFLINE_RECOVERY_PLAYBOOK.md', offlinePlaybookRaw);
|
||||
|
||||
// 6. 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',
|
||||
offline_playbook_included: true,
|
||||
};
|
||||
zip.file('recovery_info.json', JSON.stringify(metadata, null, 2));
|
||||
|
||||
// Generate ZIP blob
|
||||
return await zip.generateAsync({ type: 'blob' });
|
||||
}
|
||||
|
||||
function getRecoveryScripts(mode: EncryptionMode): Record<string, string> {
|
||||
const scripts: Record<string, string> = {};
|
||||
|
||||
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 (included in this ZIP) for complete offline recovery instructions.
|
||||
|
||||
## 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<string> {
|
||||
try {
|
||||
// Try network fetch first (offline recovery users can use local)
|
||||
const response = await fetch('/src/bip39_wordlist.txt');
|
||||
if (!response.ok) throw new Error('Network fetch failed');
|
||||
return await response.text();
|
||||
} catch {
|
||||
// Fallback: bundled file (works offline/air-gapped)
|
||||
return bip39WordlistRaw;
|
||||
}
|
||||
}
|
||||
|
||||
// Embedded recovery scripts
|
||||
const DECRYPT_PGP_SCRIPT = `#!/bin/bash
|
||||
# Decrypt PGP-encrypted SeedPGP backup
|
||||
# Usage: ./decrypt_pgp.sh <encrypted_file.txt>
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <encrypted_file.txt>"
|
||||
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 <base45_string>
|
||||
|
||||
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 <base45_string>")
|
||||
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 <seedqr_data>
|
||||
|
||||
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 <seedqr_data>")
|
||||
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}")
|
||||
`;
|
||||
|
||||
@@ -62,5 +62,6 @@ export default defineConfig({
|
||||
'__BUILD_HASH__': JSON.stringify(gitHash),
|
||||
'__BUILD_TIMESTAMP__': JSON.stringify(new Date().toISOString()),
|
||||
'global': 'globalThis',
|
||||
}
|
||||
})
|
||||
},
|
||||
assetsInclude: ['**/*.md', '**/*.txt'] // Enables raw imports for .txt files
|
||||
})
|
||||
Reference in New Issue
Block a user