From ae4c130fdecdc0f14d8cca5cb0265214c69e83e3 Mon Sep 17 00:00:00 2001 From: kccleoc Date: Fri, 20 Feb 2026 23:53:41 +0800 Subject: [PATCH] Create offline_recovery_playbook.md --- doc/offline_recovery_playbook.md | 658 +++++++++++++++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100644 doc/offline_recovery_playbook.md diff --git a/doc/offline_recovery_playbook.md b/doc/offline_recovery_playbook.md new file mode 100644 index 0000000..7feb164 --- /dev/null +++ b/doc/offline_recovery_playbook.md @@ -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. + +---