SeedPGP v1.4.3
Secure BIP39 mnemonic backup using PGP encryption and QR codes
A client-side web app for encrypting cryptocurrency seed phrases with OpenPGP and encoding them as QR-friendly Base45 frames with CRC16 integrity checking.
Live App: https://seedpgp-web.pages.dev
Features
- 🔐 PGP Encryption: Uses cv25519 (Curve25519) for modern elliptic curve cryptography
- 📱 QR Code Ready: Base45 encoding optimized for QR code generation
- ✅ Integrity Checking: CRC16-CCITT-FALSE checksums prevent corruption
- 🔑 BIP39 Support: Full support for 12/18/24-word mnemonics with passphrase indicator
- 🧪 Battle-Tested: Validated against official Trezor BIP39 test vectors
- ⚡ Fast: Built with Bun runtime and Vite for optimal performance
- 🔒 Session-Key Encryption: Ephemeral AES-GCM-256 encryption for in-memory protection
- 🛡️ CSP Enforcement: Real Content Security Policy headers block all network requests
- 📸 QR Scanner: Camera and file upload support for scanning encrypted QR codes
- 👁️ Security Monitoring: Real-time storage monitoring and clipboard tracking
Installation
# Clone repository
git clone https://github.com/kccleoc/seedpgp-web.git
cd seedpgp-web
# Install dependencies
bun install
# Run tests
bun test
# Start development server
bun run dev
Usage
Web Interface
Visit https://seedpgp-web.pages.dev or run locally:
bun run dev
# Open http://localhost:5173
Backup Flow:
- Enter your BIP39 mnemonic (12/18/24 words)
- Import PGP public key or set encryption password
- Click "Backup" to encrypt and generate QR code
- Save/print QR code for offline storage
Restore Flow:
- Scan QR code or paste encrypted text
- Import PGP private key or enter password
- Click "Restore" to decrypt mnemonic
- Mnemonic auto-clears after 10 seconds
API Usage
import { encryptToSeedPgp, buildPlaintext } from "./lib/seedpgp";
const mnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow";
const plaintext = buildPlaintext(mnemonic, false); // false = no BIP39 passphrase used
const result = await encryptToSeedPgp({
plaintext,
publicKeyArmored: yourPgpPublicKey,
});
console.log(result.framed); // SEEDPGP1:0:ABCD:BASE45DATA...
console.log(result.recipientFingerprint); // Key fingerprint for verification
Decrypt a SeedPGP Frame
import { decryptSeedPgp } from "./lib/seedpgp";
const decrypted = await decryptSeedPgp({
frameText: "SEEDPGP1:0:ABCD:BASE45DATA...",
privateKeyArmored: yourPrivateKey,
privateKeyPassphrase: "your-key-password",
});
console.log(decrypted.w); // Recovered mnemonic
console.log(decrypted.pp); // BIP39 passphrase indicator (0 or 1)
Deployment
Production: Cloudflare Pages (auto-deploys from main branch)
Live URL: https://seedpgp-web.pages.dev
Cloudflare Pages Setup
This project is deployed on Cloudflare Pages for enhanced security features:
- Repository:
seedpgp-web(private repo) - Build command:
bun run build - Output directory:
dist/ - Security headers: Automatically enforced via
public/_headers
Benefits Over GitHub Pages
- ✅ Real CSP header enforcement (blocks network requests at browser level)
- ✅ Custom security headers (X-Frame-Options, X-Content-Type-Options)
- ✅ Auto-deploy on push to main
- ✅ Build preview for PRs
- ✅ Better performance (global CDN)
- ✅ Cost: $0/month
Deployment Workflow
# Commit feature
git add src/
git commit -m "feat(v1.x): description"
# Tag version (triggers auto-deploy to Cloudflare)
git tag v1.x.x
git push origin main --tags
No manual deployment needed! Cloudflare Pages auto-deploys when you push to main.
Frame Format
SEEDPGP1:FRAME:CRC16:BASE45DATA
SEEDPGP1 - Protocol identifier and version
0 - Frame number (0 = single frame)
ABCD - 4-digit hex CRC16-CCITT-FALSE checksum
BASE45 - Base45-encoded PGP message
API Reference
buildPlaintext(mnemonic, bip39PassphraseUsed, recipientFingerprints?)
Creates a SeedPGP plaintext object.
Parameters:
mnemonic(string): BIP39 mnemonic phrase (12/18/24 words)bip39PassphraseUsed(boolean): Whether a BIP39 passphrase was usedrecipientFingerprints(string[]): Optional array of recipient key fingerprints
Returns: SeedPgpPlaintext object
encryptToSeedPgp(params)
Encrypts a plaintext object to SeedPGP format.
Parameters:
{
plaintext: SeedPgpPlaintext;
publicKeyArmored?: string; // PGP public key (PKESK)
messagePassword?: string; // Symmetric password (SKESK)
}
Returns:
{
framed: string; // SEEDPGP1 frame
pgpBytes: Uint8Array; // Raw PGP message
recipientFingerprint?: string; // Key fingerprint
}
decryptSeedPgp(params)
Decrypts a SeedPGP frame.
Parameters:
{
frameText: string; // SEEDPGP1 frame
privateKeyArmored?: string; // PGP private key
privateKeyPassphrase?: string; // Key unlock password
messagePassword?: string; // SKESK password
}
Returns: SeedPgpPlaintext object
Testing
# Run all tests
bun test
# Run with verbose output
bun test --verbose
# Watch mode (auto-rerun on changes)
bun test --watch
Test Coverage
- ✅ 15 comprehensive tests
- ✅ 8 official Trezor BIP39 test vectors
- ✅ Edge cases (wrong key, wrong passphrase)
- ✅ Frame format validation
- ✅ CRC16 integrity checking
Security Considerations
✅ Best Practices
- Uses AES-256 for symmetric encryption
- cv25519 provides ~128-bit security level
- CRC16 detects QR scan errors (not cryptographic)
- Key fingerprint validation prevents wrong-key usage
- Session-key encryption: Ephemeral AES-GCM-256 for in-memory protection
- CSP headers: Browser-enforced network blocking via Cloudflare Pages
⚠️ Important Notes
- Never share your private key or encrypted QR codes publicly
- Store backup QR codes in secure physical locations (safe, safety deposit box)
- Use a strong PGP key passphrase (20+ characters)
- Test decryption immediately after generating backups
- Consider password-only (SKESK) encryption as additional fallback
🔒 Production Deployment Warning
The Cloudflare Pages deployment at https://seedpgp-web.pages.dev is for:
- ✅ Personal use with enhanced security
- ✅ CSP enforcement blocks all network requests
- ✅ Convenient access from any device
- ⚠️ Always verify the URL before use
For maximum security with real funds:
- Run locally:
bun run dev - Or self-host on your own domain with HTTPS
- Use an airgapped device for critical operations
Threat Model (Honest)
What we protect against:
- Accidental persistence to localStorage/sessionStorage
- Plaintext secrets lingering in React state after use
- Clipboard history exposure (with warnings)
What we DON'T protect against:
- Active XSS or malicious browser extensions
- Memory dumps or browser crash reports
- JavaScript garbage collection timing (non-deterministic)
Project Structure
seedpgp-web/
├── src/
│ ├── components/
│ │ ├── PgpKeyInput.tsx # PGP key import UI
│ │ ├── QrDisplay.tsx # QR code generation
│ │ ├── QrScanner.tsx # Camera + file scanner
│ │ ├── ReadOnly.tsx # Read-only mode toggle
│ │ ├── StorageIndicator.tsx # Storage monitoring
│ │ ├── SecurityWarnings.tsx # Context alerts
│ │ └── ClipboardTracker.tsx # Clipboard monitoring
│ ├── lib/
│ │ ├── seedpgp.ts # Core encryption/decryption
│ │ ├── seedpgp.test.ts # Test vectors
│ │ ├── sessionCrypto.ts # Ephemeral session keys
│ │ ├── base45.ts # Base45 codec
│ │ ├── crc16.ts # CRC16-CCITT-FALSE
│ │ ├── qr.ts # QR utilities
│ │ └── types.ts # TypeScript definitions
│ ├── App.tsx # Main application
│ └── main.tsx # React entry point
├── public/
│ └── _headers # Cloudflare CSP headers
├── package.json
├── vite.config.ts # Vite configuration
├── GEMINI.md # AI agent project brief
└── README.md # This file
Tech Stack
- Runtime: Bun v1.3.6+
- Language: TypeScript (strict mode)
- Crypto: OpenPGP.js v6.3.0
- Framework: React + Vite
- UI: Tailwind CSS
- Icons: lucide-react
- QR: html5-qrcode, qrcode
- Testing: Bun test runner
- Deployment: Cloudflare Pages
Version History
v1.4.3 (2026-01-30)
- ✅ Fixed textarea contrast for readability
- ✅ Fixed overlapping floating boxes
- ✅ Polished UI with modern crypto wallet design
- ✅ Updated background color to be lighter
v1.4.2 (2026-01-30)
- ✅ Migrated to Cloudflare Pages for real CSP enforcement
- ✅ Added "Encrypted in memory" badge when mnemonic locked
- ✅ Improved security header configuration
- ✅ Updated deployment documentation
v1.4.0 (2026-01-29)
- ✅ Extended session-key encryption to Restore flow
- ✅ Added 10-second auto-clear timer for restored mnemonic
- ✅ Added manual Hide button for immediate clearing
- ✅ Removed debug console logs from production
v1.3.0 (2026-01-28)
- ✅ Implemented ephemeral session-key encryption (AES-GCM-256)
- ✅ Auto-clear mnemonic after QR generation (Backup flow)
- ✅ Encrypted cache for sensitive state
- ✅ Manual Lock/Clear functionality
v1.2.0 (2026-01-27)
- ✅ Added storage monitoring (StorageIndicator)
- ✅ Added security warnings (context-aware)
- ✅ Added clipboard tracking
- ✅ Implemented read-only mode
v1.1.0 (2026-01-26)
- ✅ Initial public release
- ✅ QR code generation and scanning
- ✅ Full BIP39 mnemonic support
- ✅ Trezor test vector validation
- ✅ Production-ready implementation
Roadmap
- UI polish (modern crypto wallet design)
- Multi-frame support for larger payloads
- Hardware wallet integration
- Mobile scanning app
- Shamir Secret Sharing support
- Reproducible builds with git hash verification
License
MIT License - see LICENSE file for details
Author
kccleoc - GitHub
⚠️ Disclaimer: This software is provided as-is. Always test thoroughly before trusting with real funds. The author is not responsible for lost funds due to software bugs or user error.