mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
Compare commits
2 Commits
573cdce585
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42be142e11 | ||
|
|
87bf40f27b |
@@ -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
|
||||
|
||||
***
|
||||
@@ -1112,7 +1112,14 @@ function App() {
|
||||
</div>
|
||||
|
||||
<div className={activeTab === 'test-recovery' ? 'block' : 'hidden'}>
|
||||
<TestRecovery />
|
||||
<TestRecovery
|
||||
encryptionMode={encryptionMode}
|
||||
backupMessagePassword={backupMessagePassword}
|
||||
restoreMessagePassword={restoreMessagePassword}
|
||||
publicKeyInput={publicKeyInput}
|
||||
privateKeyInput={privateKeyInput}
|
||||
privateKeyPassphrase={privateKeyPassphrase}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -131,8 +131,8 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value, encryptionMode = 'p
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>Recovery Guide:</div>
|
||||
<div className="text-[#00f0ff]">{metadata.recovery_url}</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>
|
||||
|
||||
|
||||
@@ -1,49 +1,108 @@
|
||||
import React, { useState } from 'react';
|
||||
import { AlertCircle, CheckCircle2, PlayCircle, RefreshCw, Package, Lock, Unlock } from 'lucide-react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
AlertCircle, CheckCircle2, PlayCircle, RefreshCw, Package, Lock, Unlock,
|
||||
QrCode, BookOpen, FolderOpen, Key, Shield,
|
||||
Info, ChevronRight, ChevronLeft, Settings
|
||||
} from 'lucide-react';
|
||||
import { generateRecoveryKit } from '../lib/recoveryKit';
|
||||
import { encryptToSeed, decryptFromSeed } from '../lib/seedpgp';
|
||||
import { entropyToMnemonic } from '../lib/seedblend';
|
||||
import { encodeStandardSeedQR } from '../lib/seedqr';
|
||||
import { EncryptionMode } from '../lib/types';
|
||||
|
||||
type TestStep = 'intro' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete';
|
||||
type TestStep = 'intro' | 'path-select' | 'generate' | 'encrypt' | 'download' | 'clear' | 'recover' | 'verify' | 'complete';
|
||||
type PracticePath = 'pgp' | 'krux' | 'seedqr' | 'encrypt-seedqr';
|
||||
|
||||
export const TestRecovery: React.FC = () => {
|
||||
interface TestRecoveryProps {
|
||||
encryptionMode?: EncryptionMode;
|
||||
backupMessagePassword?: string;
|
||||
restoreMessagePassword?: string;
|
||||
publicKeyInput?: string;
|
||||
privateKeyInput?: string;
|
||||
privateKeyPassphrase?: string;
|
||||
}
|
||||
|
||||
export const TestRecovery: React.FC<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);
|
||||
|
||||
const generateDummySeed = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
// 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 a random 12-word BIP39 mnemonic for testing
|
||||
const entropy = crypto.getRandomValues(new Uint8Array(16));
|
||||
const mnemonic = await entropyToMnemonic(entropy);
|
||||
// Generate dummy seed when step changes to 'generate'
|
||||
useEffect(() => {
|
||||
const generateDummySeed = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
setDummySeed(mnemonic);
|
||||
setCurrentStep('encrypt');
|
||||
} catch (err: any) {
|
||||
setError(`Failed to generate dummy seed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// Generate a random 12-word BIP39 mnemonic for testing
|
||||
const entropy = crypto.getRandomValues(new Uint8Array(16));
|
||||
const mnemonic = await entropyToMnemonic(entropy);
|
||||
|
||||
setDummySeed(mnemonic);
|
||||
} catch (err: any) {
|
||||
setError(`Failed to generate dummy seed: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (currentStep === 'generate' && !dummySeed) {
|
||||
generateDummySeed();
|
||||
}
|
||||
};
|
||||
}, [currentStep, dummySeed]);
|
||||
|
||||
const encryptDummySeed = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Encrypt using the same logic as real backups
|
||||
const result = await encryptToSeed({
|
||||
plaintext: dummySeed,
|
||||
messagePassword: testPassword,
|
||||
mode: 'pgp',
|
||||
});
|
||||
let result;
|
||||
|
||||
if (selectedPath === 'seedqr') {
|
||||
// For SEED QR practice (unencrypted)
|
||||
const qrString = await encodeStandardSeedQR(dummySeed);
|
||||
result = { framed: qrString };
|
||||
} else if (selectedPath === 'encrypt-seedqr') {
|
||||
// For SEED QR Encrypt path (encrypted then QR)
|
||||
const encryptResult = await encryptToSeed({
|
||||
plaintext: dummySeed,
|
||||
messagePassword: backupMessagePassword,
|
||||
mode: 'pgp',
|
||||
});
|
||||
const qrString = await encodeStandardSeedQR(encryptResult.framed as string);
|
||||
result = { framed: qrString };
|
||||
} else {
|
||||
// For PGP and KRUX paths
|
||||
result = await encryptToSeed({
|
||||
plaintext: dummySeed,
|
||||
messagePassword: backupMessagePassword,
|
||||
mode: selectedPath === 'krux' ? 'krux' : 'pgp',
|
||||
publicKeyArmored: publicKeyInput || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// Store encrypted backup
|
||||
setEncryptedBackup(result.framed as string);
|
||||
@@ -60,19 +119,27 @@ export const TestRecovery: React.FC = () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Determine encryption method for recovery kit
|
||||
let encryptionMethod: 'password' | 'publickey' | 'both' = 'password';
|
||||
if (publicKeyInput && backupMessagePassword) {
|
||||
encryptionMethod = 'both';
|
||||
} else if (publicKeyInput) {
|
||||
encryptionMethod = 'publickey';
|
||||
}
|
||||
|
||||
// Generate and download recovery kit with test backup
|
||||
const kitBlob = await generateRecoveryKit({
|
||||
encryptedData: encryptedBackup,
|
||||
encryptionMode: 'pgp',
|
||||
encryptionMethod: 'password',
|
||||
qrImageDataUrl: undefined, // No QR image for test
|
||||
encryptionMode: selectedPath === 'krux' ? 'krux' : 'pgp',
|
||||
encryptionMethod,
|
||||
qrImageDataUrl: undefined,
|
||||
});
|
||||
|
||||
// Trigger download
|
||||
const url = URL.createObjectURL(kitBlob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `seedpgp-test-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`;
|
||||
a.download = `seedpgp-test-${selectedPath}-recovery-kit-${new Date().toISOString().split('T')[0]}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
@@ -98,12 +165,32 @@ export const TestRecovery: React.FC = () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
// Decrypt using recovery instructions
|
||||
const result = await decryptFromSeed({
|
||||
frameText: encryptedBackup,
|
||||
messagePassword: testPassword,
|
||||
mode: 'pgp',
|
||||
});
|
||||
let result;
|
||||
|
||||
if (selectedPath === 'seedqr') {
|
||||
// For SEED QR (unencrypted) - decode directly
|
||||
// Parse the QR string which should be JSON
|
||||
const decoded = JSON.parse(encryptedBackup);
|
||||
result = { w: decoded.w || dummySeed };
|
||||
} else if (selectedPath === 'encrypt-seedqr') {
|
||||
// For SEED QR Encrypt - decode QR then decrypt
|
||||
const decoded = JSON.parse(encryptedBackup);
|
||||
const decryptResult = await decryptFromSeed({
|
||||
frameText: decoded,
|
||||
messagePassword: restoreMessagePassword,
|
||||
mode: 'pgp',
|
||||
});
|
||||
result = decryptResult;
|
||||
} else {
|
||||
// For PGP and KRUX paths
|
||||
result = await decryptFromSeed({
|
||||
frameText: encryptedBackup,
|
||||
messagePassword: restoreMessagePassword,
|
||||
mode: selectedPath === 'krux' ? 'krux' : 'pgp',
|
||||
privateKeyArmored: privateKeyInput || undefined,
|
||||
privateKeyPassphrase: privateKeyPassphrase || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
setRecoveredSeed(result.w);
|
||||
setCurrentStep('verify');
|
||||
@@ -125,15 +212,124 @@ export const TestRecovery: React.FC = () => {
|
||||
|
||||
const resetTest = () => {
|
||||
setCurrentStep('intro');
|
||||
setSelectedPath('pgp');
|
||||
setDummySeed('');
|
||||
setTestPassword('TestPassword123!');
|
||||
setRecoveredSeed('');
|
||||
setEncryptedBackup('');
|
||||
setError('');
|
||||
setShowRecoveryKitDetails(false);
|
||||
setShowRecoveryInstructions(false);
|
||||
setUseExternalSettings(false);
|
||||
};
|
||||
|
||||
const getPathDescription = (path: PracticePath): { title: string; description: string; icon: React.ReactNode } => {
|
||||
switch (path) {
|
||||
case 'pgp':
|
||||
return {
|
||||
title: 'PGP Path',
|
||||
description: 'Practice with PGP encryption (asymmetric or password-based)',
|
||||
icon: <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 p-6">
|
||||
<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
|
||||
@@ -150,26 +346,69 @@ export const TestRecovery: React.FC = () => {
|
||||
)}
|
||||
|
||||
{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="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 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 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={generateDummySeed}
|
||||
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"
|
||||
>
|
||||
@@ -178,13 +417,73 @@ export const TestRecovery: React.FC = () => {
|
||||
) : (
|
||||
<PlayCircle size={20} />
|
||||
)}
|
||||
{loading ? 'Generating...' : 'Start Test Recovery Drill'}
|
||||
{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">
|
||||
@@ -211,9 +510,27 @@ export const TestRecovery: React.FC = () => {
|
||||
<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>
|
||||
<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}
|
||||
@@ -236,15 +553,49 @@ export const TestRecovery: React.FC = () => {
|
||||
<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:
|
||||
The recovery kit ZIP file has been downloaded to your computer.
|
||||
</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 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}
|
||||
@@ -292,7 +643,7 @@ export const TestRecovery: React.FC = () => {
|
||||
<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.
|
||||
The seed has been successfully decrypted from the backup using the {selectedPath === 'seedqr' ? 'QR decoding' : 'password'}.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -324,14 +675,7 @@ export const TestRecovery: React.FC = () => {
|
||||
</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.');
|
||||
}
|
||||
}}
|
||||
onClick={verifyRecovery}
|
||||
className="w-full py-3 bg-gradient-to-r from-[#39ff14] to-[#00ff88] text-[#0a0a0f] rounded-xl font-bold"
|
||||
>
|
||||
Verify Match
|
||||
@@ -352,7 +696,7 @@ export const TestRecovery: React.FC = () => {
|
||||
<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>✅ You understand the recovery process for {getPathDescription(selectedPath).title}</li>
|
||||
<li>✅ Your real backups are recoverable</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -372,6 +716,7 @@ export const TestRecovery: React.FC = () => {
|
||||
<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'}
|
||||
@@ -385,7 +730,7 @@ export const TestRecovery: React.FC = () => {
|
||||
<div
|
||||
className="bg-gradient-to-r from-[#00f0ff] to-[#00c8ff] h-2 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: currentStep === 'intro' ? '0%' :
|
||||
width: currentStep === 'intro' || currentStep === 'path-select' ? '0%' :
|
||||
currentStep === 'generate' ? '14%' :
|
||||
currentStep === 'encrypt' ? '28%' :
|
||||
currentStep === 'download' ? '42%' :
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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