1 Commits

Author SHA1 Message Date
LC mac
e4516f3d19 chore: bump version to 1.4.0 2026-01-30 00:35:00 +08:00

View File

@@ -6,7 +6,6 @@ import {
CheckCircle2, Lock, CheckCircle2, Lock,
AlertCircle, AlertCircle,
Unlock, Unlock,
Eye,
EyeOff, EyeOff,
FileKey, FileKey,
Info, Info,
@@ -17,7 +16,6 @@ import { QrDisplay } from './components/QrDisplay';
import QRScanner from './components/QRScanner'; import QRScanner from './components/QRScanner';
import { validateBip39Mnemonic } from './lib/bip39'; import { validateBip39Mnemonic } from './lib/bip39';
import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp'; import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp';
import type { SeedPgpPlaintext } from './lib/types';
import * as openpgp from 'openpgp'; import * as openpgp from 'openpgp';
import { StorageIndicator } from './components/StorageIndicator'; import { StorageIndicator } from './components/StorageIndicator';
import { SecurityWarnings } from './components/SecurityWarnings'; import { SecurityWarnings } from './components/SecurityWarnings';
@@ -40,10 +38,9 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
const [qrPayload, setQrPayload] = useState(''); const [qrPayload, setQrPayload] = useState('');
const [recipientFpr, setRecipientFpr] = useState(''); const [recipientFpr, setRecipientFpr] = useState('');
const [restoreInput, setRestoreInput] = useState(''); const [restoreInput, setRestoreInput] = useState('');
const [restoredData, setRestoredData] = useState<SeedPgpPlaintext | null>(null); const [decryptedRestoredMnemonic, setDecryptedRestoredMnemonic] = useState<string | null>(null);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [showMnemonic, setShowMnemonic] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [showQRScanner, setShowQRScanner] = useState(false); const [showQRScanner, setShowQRScanner] = useState(false);
const [isReadOnly, setIsReadOnly] = useState(false); const [isReadOnly, setIsReadOnly] = useState(false);
@@ -60,7 +57,7 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
setPrivateKeyPassphrase(''); setPrivateKeyPassphrase('');
setQrPayload(''); setQrPayload('');
setRestoreInput(''); setRestoreInput('');
setRestoredData(null); setDecryptedRestoredMnemonic(null);
setError(''); setError('');
} }
}, [isReadOnly]); }, [isReadOnly]);
@@ -138,7 +135,7 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
const handleRestore = async () => { const handleRestore = async () => {
setLoading(true); setLoading(true);
setError(''); setError('');
setRestoredData(null); setDecryptedRestoredMnemonic(null);
try { try {
const result = await decryptSeedPgp({ const result = await decryptSeedPgp({
@@ -148,8 +145,17 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
messagePassword: restoreMessagePassword || undefined, messagePassword: restoreMessagePassword || undefined,
}); });
// Encrypt the restored mnemonic with the session key
await getSessionKey();
const blob = await encryptJsonToBlob({ mnemonic: result.w, timestamp: Date.now() });
setEncryptedMnemonicCache(blob);
// Temporarily display the mnemonic and then clear it
setDecryptedRestoredMnemonic(result.w);
setTimeout(() => {
setDecryptedRestoredMnemonic(null);
}, 10000); // Auto-clear after 10 seconds
setRestoredData(result);
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : 'Decryption failed'); setError(e instanceof Error ? e.message : 'Decryption failed');
} finally { } finally {
@@ -169,9 +175,8 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
setQrPayload(''); setQrPayload('');
setRecipientFpr(''); setRecipientFpr('');
setRestoreInput(''); setRestoreInput('');
setRestoredData(null); setDecryptedRestoredMnemonic(null);
setError(''); setError('');
setShowMnemonic(false);
setCopied(false); setCopied(false);
setShowQRScanner(false); setShowQRScanner(false);
}; };
@@ -217,7 +222,7 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
setActiveTab('backup'); setActiveTab('backup');
setError(''); setError('');
setQrPayload(''); setQrPayload('');
setRestoredData(null); setDecryptedRestoredMnemonic(null);
}} }}
className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'backup' className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'backup'
? 'bg-white text-slate-900 shadow-lg' ? 'bg-white text-slate-900 shadow-lg'
@@ -231,7 +236,7 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
setActiveTab('restore'); setActiveTab('restore');
setError(''); setError('');
setQrPayload(''); setQrPayload('');
setRestoredData(null); setDecryptedRestoredMnemonic(null);
}} }}
className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'restore' className={`px-5 py-2 rounded-md text-sm font-semibold transition-all ${activeTab === 'restore'
? 'bg-white text-slate-900 shadow-lg' ? 'bg-white text-slate-900 shadow-lg'
@@ -463,7 +468,7 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
)} )}
{/* Restored Mnemonic */} {/* Restored Mnemonic */}
{restoredData && activeTab === 'restore' && ( {decryptedRestoredMnemonic && activeTab === 'restore' && (
<div className="pt-6 border-t border-slate-200 animate-in zoom-in-95"> <div className="pt-6 border-t border-slate-200 animate-in zoom-in-95">
<div className="p-6 bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-300 rounded-2xl shadow-lg"> <div className="p-6 bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-300 rounded-2xl shadow-lg">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@@ -471,35 +476,18 @@ import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } fr
<CheckCircle2 size={22} /> Mnemonic Recovered <CheckCircle2 size={22} /> Mnemonic Recovered
</span> </span>
<button <button
onClick={() => setShowMnemonic(!showMnemonic)} onClick={() => setDecryptedRestoredMnemonic(null)}
className="p-2.5 hover:bg-green-100 rounded-xl transition-all text-green-700 hover:shadow" className="p-2.5 hover:bg-green-100 rounded-xl transition-all text-green-700 hover:shadow"
> >
{showMnemonic ? <EyeOff size={22} /> : <Eye size={22} />} <EyeOff size={22} /> Hide
</button> </button>
</div> </div>
<div className={`p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm transition-all duration-300 ${showMnemonic ? 'blur-0' : 'blur-lg select-none' <div className="p-6 bg-white rounded-xl border-2 border-green-200 shadow-sm">
}`}>
<p className="font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words"> <p className="font-mono text-center text-lg text-slate-800 tracking-wide leading-relaxed break-words">
{restoredData.w} {decryptedRestoredMnemonic}
</p> </p>
</div> </div>
{restoredData.pp === 1 && (
<div className="mt-4 p-3 bg-orange-100 border border-orange-300 rounded-lg">
<p className="text-xs text-center text-orange-800 font-bold uppercase tracking-widest flex items-center justify-center gap-2">
<AlertCircle size={14} /> BIP39 Passphrase Required (25th Word)
</p>
</div>
)}
{restoredData.fpr && restoredData.fpr.length > 0 && (
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-xs text-blue-800">
<strong>Encrypted for keys:</strong> {restoredData.fpr.join(', ')}
</p>
</div>
)}
</div> </div>
</div> </div>
)} )}