mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
add recovery kit
This commit is contained in:
78
src/App.tsx
78
src/App.tsx
@@ -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)' }}>
|
||||
|
||||
Reference in New Issue
Block a user