mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 01:47:52 +08:00
Compare commits
2 Commits
02f58f5ef0
...
573cdce585
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
573cdce585 | ||
| ae4c130fde |
28
bun.lock
28
bun.lock
@@ -7,11 +7,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/bip32": "^2.0.4",
|
"@types/bip32": "^2.0.4",
|
||||||
"@types/bip39": "^3.0.4",
|
"@types/bip39": "^3.0.4",
|
||||||
|
"@types/jszip": "^3.4.1",
|
||||||
"@types/pako": "^2.0.4",
|
"@types/pako": "^2.0.4",
|
||||||
"bip32": "^5.0.0",
|
"bip32": "^5.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"openpgp": "^6.3.0",
|
"openpgp": "^6.3.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
@@ -250,6 +252,8 @@
|
|||||||
|
|
||||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
@@ -394,6 +406,10 @@
|
|||||||
|
|
||||||
"jsqr": ["jsqr@1.4.0", "", {}, "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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": ["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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|||||||
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": {
|
"dependencies": {
|
||||||
"@types/bip32": "^2.0.4",
|
"@types/bip32": "^2.0.4",
|
||||||
"@types/bip39": "^3.0.4",
|
"@types/bip39": "^3.0.4",
|
||||||
|
"@types/jszip": "^3.4.1",
|
||||||
"@types/pako": "^2.0.4",
|
"@types/pako": "^2.0.4",
|
||||||
"bip32": "^5.0.0",
|
"bip32": "^5.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"openpgp": "^6.3.0",
|
"openpgp": "^6.3.0",
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
|
|||||||
78
src/App.tsx
78
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
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 { PgpKeyInput } from './components/PgpKeyInput';
|
||||||
import { QrDisplay } from './components/QrDisplay';
|
import { QrDisplay } from './components/QrDisplay';
|
||||||
import QRScanner from './components/QRScanner';
|
import QRScanner from './components/QRScanner';
|
||||||
@@ -18,6 +18,8 @@ import CameraEntropy from './components/CameraEntropy';
|
|||||||
import DiceEntropy from './components/DiceEntropy';
|
import DiceEntropy from './components/DiceEntropy';
|
||||||
import RandomOrgEntropy from './components/RandomOrgEntropy';
|
import RandomOrgEntropy from './components/RandomOrgEntropy';
|
||||||
import { InteractionEntropy } from './lib/interactionEntropy';
|
import { InteractionEntropy } from './lib/interactionEntropy';
|
||||||
|
import { TestRecovery } from './components/TestRecovery';
|
||||||
|
import { generateRecoveryKit } from './lib/recoveryKit';
|
||||||
|
|
||||||
import AudioEntropy from './AudioEntropy';
|
import AudioEntropy from './AudioEntropy';
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ interface ClipboardEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
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 [mnemonic, setMnemonic] = useState('');
|
||||||
const [backupMessagePassword, setBackupMessagePassword] = useState('');
|
const [backupMessagePassword, setBackupMessagePassword] = useState('');
|
||||||
const [restoreMessagePassword, setRestoreMessagePassword] = 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
|
// Allow free navigation - no warnings
|
||||||
// User can manually reset Seed Blender with "Reset All" button
|
// User can manually reset Seed Blender with "Reset All" button
|
||||||
setActiveTab(newTab);
|
setActiveTab(newTab);
|
||||||
@@ -646,6 +648,50 @@ function App() {
|
|||||||
setShowQRScanner(false);
|
setShowQRScanner(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle download recovery kit
|
||||||
|
const handleDownloadRecoveryKit = async () => {
|
||||||
|
if (!qrPayload) {
|
||||||
|
setError('No backup available to export');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Get QR image as data URL from canvas
|
||||||
|
const qrCanvas = document.querySelector('canvas');
|
||||||
|
const qrImageDataUrl = qrCanvas?.toDataURL('image/png');
|
||||||
|
|
||||||
|
// Determine encryption method
|
||||||
|
const encryptionMethod = publicKeyInput && backupMessagePassword ? 'both'
|
||||||
|
: publicKeyInput ? 'publickey'
|
||||||
|
: 'password';
|
||||||
|
|
||||||
|
const kitBlob = await generateRecoveryKit({
|
||||||
|
encryptedData: qrPayload,
|
||||||
|
encryptionMode: encryptionMode,
|
||||||
|
encryptionMethod: encryptionMethod,
|
||||||
|
fingerprint: recipientFpr,
|
||||||
|
qrImageDataUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
const url = URL.createObjectURL(kitBlob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `seedpgp-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
alert('✅ Recovery kit downloaded! Store this ZIP file safely.');
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(`Failed to generate recovery kit: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1064,6 +1110,10 @@ function App() {
|
|||||||
onSeedReceived={() => setSeedForBlender('')}
|
onSeedReceived={() => setSeedForBlender('')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={activeTab === 'test-recovery' ? 'block' : 'hidden'}>
|
||||||
|
<TestRecovery />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Security Panel */}
|
{/* Security Panel */}
|
||||||
@@ -1215,8 +1265,28 @@ function App() {
|
|||||||
{qrPayload && activeTab === 'backup' && (
|
{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="pt-6 border-t border-[#00f0ff]/20 space-y-6 animate-in fade-in slide-in-from-bottom-4">
|
||||||
<div className={isReadOnly ? 'blur-lg' : ''}>
|
<div className={isReadOnly ? 'blur-lg' : ''}>
|
||||||
<QrDisplay value={qrPayload} />
|
<QrDisplay value={qrPayload} encryptionMode={encryptionMode} fingerprint={recipientFpr} />
|
||||||
</div>
|
</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="space-y-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<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)' }}>
|
<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[];
|
sessionItems: StorageItem[];
|
||||||
events: ClipboardEvent[];
|
events: ClipboardEvent[];
|
||||||
onOpenClipboardModal: () => void;
|
onOpenClipboardModal: () => void;
|
||||||
activeTab: 'create' | 'backup' | 'restore' | 'seedblender';
|
activeTab: 'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery';
|
||||||
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender') => void;
|
onRequestTabChange: (tab: 'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery') => void;
|
||||||
appVersion: string;
|
appVersion: string;
|
||||||
isNetworkBlocked: boolean;
|
isNetworkBlocked: boolean;
|
||||||
onToggleNetwork: () => void;
|
onToggleNetwork: () => void;
|
||||||
@@ -113,7 +113,7 @@ const Header: React.FC<HeaderProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ROW 3: Navigation Tabs */}
|
{/* ROW 3: Navigation Tabs */}
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-5 gap-2">
|
||||||
<button
|
<button
|
||||||
className={`py-2 rounded-lg font-medium text-xs whitespace-nowrap transition-all ${activeTab === 'create'
|
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)]'
|
? '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
|
Blender
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import QRCode from 'qrcode';
|
|||||||
|
|
||||||
interface QrDisplayProps {
|
interface QrDisplayProps {
|
||||||
value: string | Uint8Array;
|
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 [dataUrl, setDataUrl] = useState('');
|
||||||
const [debugInfo, setDebugInfo] = useState('');
|
const [debugInfo, setDebugInfo] = useState('');
|
||||||
|
|
||||||
@@ -94,8 +96,15 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
|||||||
|
|
||||||
if (!dataUrl) return null;
|
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 (
|
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">
|
<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" />
|
<img src={dataUrl} alt="QR Code" className="w-full h-auto" />
|
||||||
</div>
|
</div>
|
||||||
@@ -106,6 +115,27 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
|
|||||||
</div>
|
</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>Recovery Guide:</div>
|
||||||
|
<div className="text-[#00f0ff]">{metadata.recovery_url}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleDownload}
|
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)]"
|
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
|
Download QR Code
|
||||||
</button>
|
</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]">
|
<p className="text-xs text-[#6ef3f7]">
|
||||||
Downloads as: SeedPGP_{new Date().toISOString().split('T')[0]}_HHMMSS.png
|
Downloads as: SeedPGP_{new Date().toISOString().split('T')[0]}_HHMMSS.png
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
403
src/components/TestRecovery.tsx
Normal file
403
src/components/TestRecovery.tsx
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { AlertCircle, CheckCircle2, PlayCircle, RefreshCw, Package, Lock, Unlock } from 'lucide-react';
|
||||||
|
import { generateRecoveryKit } from '../lib/recoveryKit';
|
||||||
|
import { encryptToSeed, decryptFromSeed } from '../lib/seedpgp';
|
||||||
|
import { entropyToMnemonic } from '../lib/seedblend';
|
||||||
|
|
||||||
|
type TestStep = 'intro' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete';
|
||||||
|
|
||||||
|
export const TestRecovery: React.FC = () => {
|
||||||
|
const [currentStep, setCurrentStep] = useState<TestStep>('intro');
|
||||||
|
const [dummySeed, setDummySeed] = useState('');
|
||||||
|
const [testPassword, setTestPassword] = useState('TestPassword123!');
|
||||||
|
const [recoveredSeed, setRecoveredSeed] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [encryptedBackup, setEncryptedBackup] = useState<string>('');
|
||||||
|
|
||||||
|
const generateDummySeed = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Generate a random 12-word BIP39 mnemonic for testing
|
||||||
|
const entropy = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const mnemonic = await entropyToMnemonic(entropy);
|
||||||
|
|
||||||
|
setDummySeed(mnemonic);
|
||||||
|
setCurrentStep('encrypt');
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(`Failed to generate dummy seed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptDummySeed = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Encrypt using the same logic as real backups
|
||||||
|
const result = await encryptToSeed({
|
||||||
|
plaintext: dummySeed,
|
||||||
|
messagePassword: testPassword,
|
||||||
|
mode: 'pgp',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store encrypted backup
|
||||||
|
setEncryptedBackup(result.framed as string);
|
||||||
|
setCurrentStep('download');
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(`Failed to encrypt dummy seed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadRecoveryKit = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Generate and download recovery kit with test backup
|
||||||
|
const kitBlob = await generateRecoveryKit({
|
||||||
|
encryptedData: encryptedBackup,
|
||||||
|
encryptionMode: 'pgp',
|
||||||
|
encryptionMethod: 'password',
|
||||||
|
qrImageDataUrl: undefined, // No QR image for test
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger download
|
||||||
|
const url = URL.createObjectURL(kitBlob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `seedpgp-test-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
alert('✅ Recovery kit downloaded! Now let\'s test if you can recover the seed.');
|
||||||
|
setCurrentStep('clear');
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(`Failed to generate recovery kit: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearDummySeed = () => {
|
||||||
|
// Clear the dummy seed from state (simulating app unavailability)
|
||||||
|
setDummySeed('');
|
||||||
|
setRecoveredSeed('');
|
||||||
|
alert('✅ Dummy seed cleared. Now follow the recovery instructions to get it back!');
|
||||||
|
setCurrentStep('recover');
|
||||||
|
};
|
||||||
|
|
||||||
|
const recoverFromBackup = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
// Decrypt using recovery instructions
|
||||||
|
const result = await decryptFromSeed({
|
||||||
|
frameText: encryptedBackup,
|
||||||
|
messagePassword: testPassword,
|
||||||
|
mode: 'pgp',
|
||||||
|
});
|
||||||
|
|
||||||
|
setRecoveredSeed(result.w);
|
||||||
|
setCurrentStep('verify');
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(`❌ Recovery failed: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyRecovery = () => {
|
||||||
|
if (recoveredSeed === dummySeed) {
|
||||||
|
setCurrentStep('complete');
|
||||||
|
alert('🎉 SUCCESS! You successfully recovered the seed phrase!');
|
||||||
|
} else {
|
||||||
|
alert('❌ FAILED: Recovered seed does not match original. Try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetTest = () => {
|
||||||
|
setCurrentStep('intro');
|
||||||
|
setDummySeed('');
|
||||||
|
setTestPassword('TestPassword123!');
|
||||||
|
setRecoveredSeed('');
|
||||||
|
setEncryptedBackup('');
|
||||||
|
setError('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-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-4">
|
||||||
|
<p className="text-[#6ef3f7]">
|
||||||
|
This drill will help you practice recovering a seed phrase from an encrypted backup.
|
||||||
|
You'll learn the recovery process without risking your real funds.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-[#0a0a0f] border border-[#ff006e] rounded-lg p-4">
|
||||||
|
<h3 className="text-[#ff006e] font-bold mb-2">What You'll Do:</h3>
|
||||||
|
<ol className="text-sm text-[#6ef3f7] space-y-1 list-decimal list-inside">
|
||||||
|
<li>Generate a dummy test seed</li>
|
||||||
|
<li>Encrypt it with a test password</li>
|
||||||
|
<li>Download the recovery kit</li>
|
||||||
|
<li>Clear the seed from your browser</li>
|
||||||
|
<li>Follow recovery instructions to decrypt</li>
|
||||||
|
<li>Verify you got the correct seed back</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={generateDummySeed}
|
||||||
|
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 ? 'Generating...' : 'Start Test Recovery Drill'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 'generate' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<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">Test Password:</p>
|
||||||
|
<p className="font-mono text-sm text-[#00f0ff]">{testPassword}</p>
|
||||||
|
<p className="text-xs text-[#6ef3f7] mt-2">Seed has been encrypted with PGP using password-based encryption.</p>
|
||||||
|
</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. It contains:
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-[#6ef3f7] list-disc list-inside mt-2 space-y-1">
|
||||||
|
<li>Encrypted backup file</li>
|
||||||
|
<li>Recovery scripts (Python/Bash)</li>
|
||||||
|
<li>Personalized instructions</li>
|
||||||
|
<li>BIP39 wordlist</li>
|
||||||
|
<li>Metadata file</li>
|
||||||
|
</ul>
|
||||||
|
</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 test 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={() => {
|
||||||
|
if (recoveredSeed === dummySeed) {
|
||||||
|
setCurrentStep('complete');
|
||||||
|
alert('🎉 SUCCESS! You successfully recovered the seed phrase!');
|
||||||
|
} else {
|
||||||
|
alert('❌ FAILED: Recovered seed does not match original. Try again.');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full py-3 bg-gradient-to-r from-[#39ff14] to-[#00ff88] text-[#0a0a0f] rounded-xl font-bold"
|
||||||
|
>
|
||||||
|
Verify Match
|
||||||
|
</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</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 === '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' ? '0%' :
|
||||||
|
currentStep === 'generate' ? '14%' :
|
||||||
|
currentStep === 'encrypt' ? '28%' :
|
||||||
|
currentStep === 'download' ? '42%' :
|
||||||
|
currentStep === 'clear' ? '57%' :
|
||||||
|
currentStep === 'recover' ? '71%' :
|
||||||
|
currentStep === 'verify' ? '85%' :
|
||||||
|
'100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
2368
src/lib/recoveryKit.ts
Normal file
2368
src/lib/recoveryKit.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user