17 KiB
🆘 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:
# 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:
cat > encrypted.asc << 'EOF'
-----BEGIN PGP MESSAGE-----
hQEMA...
-----END PGP MESSAGE-----
EOF
Step 1B: Decrypt with GPG
If encrypted with PASSWORD only:
# 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:
# 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:
{"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:
# 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:
#!/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
# 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
# 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)
- Split into 4-digit chunks:
0216,0071,0042,0461, ... - Each chunk is a word index (0-2047)
- 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:
#!/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:
#!/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:
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:
- ✅ Use an air-gapped computer (TailsOS or Ubuntu Live USB)
- ✅ Disconnect from internet before decrypting
- ✅ Write seed on paper immediately after decryption
- ✅ Never screenshot or save to disk
- ✅ Verify your seed by importing to a test wallet (small amount first)
- ✅ 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:
- Search GitHub for "seedpgp-web" (project may still exist)
- Check Internet Archive: https://web.archive.org/
- Ask in Bitcoin/crypto forums (describe format, don't share actual data!)
- 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:
- Test that you can still decrypt your backup
- Verify the recovery tools still work
- Update this playbook if formats change
- 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.