add recovery kit

This commit is contained in:
LC mac
2026-02-21 01:20:38 +08:00
parent ae4c130fde
commit 573cdce585
7 changed files with 2925 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
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 { QrDisplay } from './components/QrDisplay';
import QRScanner from './components/QRScanner';
@@ -18,6 +18,8 @@ import CameraEntropy from './components/CameraEntropy';
import DiceEntropy from './components/DiceEntropy';
import RandomOrgEntropy from './components/RandomOrgEntropy';
import { InteractionEntropy } from './lib/interactionEntropy';
import { TestRecovery } from './components/TestRecovery';
import { generateRecoveryKit } from './lib/recoveryKit';
import AudioEntropy from './AudioEntropy';
@@ -35,7 +37,7 @@ interface ClipboardEvent {
}
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 [backupMessagePassword, setBackupMessagePassword] = 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
// User can manually reset Seed Blender with "Reset All" button
setActiveTab(newTab);
@@ -646,6 +648,50 @@ function App() {
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('')}
/>
</div>
<div className={activeTab === 'test-recovery' ? 'block' : 'hidden'}>
<TestRecovery />
</div>
</div>
{/* Security Panel */}
@@ -1215,8 +1265,28 @@ function App() {
{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={isReadOnly ? 'blur-lg' : ''}>
<QrDisplay value={qrPayload} />
<QrDisplay value={qrPayload} encryptionMode={encryptionMode} fingerprint={recipientFpr} />
</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="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)' }}>