Files
seedpgp-web/src/App.tsx
2026-03-02 00:36:46 +08:00

1483 lines
71 KiB
TypeScript

import { useState, useEffect, useCallback, useRef } from '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';
import { validateBip39Mnemonic } from './lib/bip39';
import { buildPlaintext, encryptToSeed, decryptFromSeed, detectEncryptionMode, validatePGPKey } from './lib/seedpgp';
import { encodeStandardSeedQR, encodeCompactSeedQREntropy } from './lib/seedqr';
import { SecurityWarnings } from './components/SecurityWarnings';
import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto';
import { EncryptionMode, EncryptionResult } from './lib/types'; // Import EncryptionMode and EncryptionResult
import Header from './components/Header';
import { StorageDetails } from './components/StorageDetails';
import { ClipboardDetails } from './components/ClipboardDetails';
import Footer from './components/Footer';
import { SeedBlender } from './components/SeedBlender';
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';
interface StorageItem {
key: string;
value: string;
size: number;
isSensitive: boolean;
}
interface ClipboardEvent {
timestamp: Date;
field: string;
length: number;
}
function App() {
const [activeTab, setActiveTab] = useState<'create' | 'backup' | 'restore' | 'seedblender' | 'test-recovery'>('create');
const [mnemonic, setMnemonic] = useState('');
const [backupMessagePassword, setBackupMessagePassword] = useState('');
const [restoreMessagePassword, setRestoreMessagePassword] = useState('');
const [publicKeyInput, setPublicKeyInput] = useState('');
const [privateKeyInput, setPrivateKeyInput] = useState('');
const [privateKeyPassphrase, setPrivateKeyPassphrase] = useState('');
const [hasBip39Passphrase, setHasBip39Passphrase] = useState(false);
const [qrPayload, setQrPayload] = useState<string | Uint8Array>('');
const [recipientFpr, setRecipientFpr] = useState('');
const [restoreInput, setRestoreInput] = useState('');
const [decryptedRestoredMnemonic, setDecryptedRestoredMnemonic] = useState<string | null>(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false);
const [showQRScanner, setShowQRScanner] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isReadOnly, setIsReadOnly] = useState(false);
const [_encryptedMnemonicCache, _setEncryptedMnemonicCache] = useState<EncryptedBlob | null>(null);
const [showSecurityModal, setShowSecurityModal] = useState(false);
const [showStorageModal, setShowStorageModal] = useState(false);
const [showClipboardModal, setShowClipboardModal] = useState(false);
const [localItems, setLocalItems] = useState<StorageItem[]>([]);
const [sessionItems, setSessionItems] = useState<StorageItem[]>([]);
const [clipboardEvents, setClipboardEvents] = useState<ClipboardEvent[]>([]);
const [showLockConfirm, setShowLockConfirm] = useState(false);
const [resetCounter, setResetCounter] = useState(0);
// Krux integration state
const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux' | 'seedqr'>('pgp');
const [seedQrFormat, setSeedQrFormat] = useState<'standard' | 'compact'>('standard');
const [generatedSeed, setGeneratedSeed] = useState<string>('');
const [seedWordCount, setSeedWordCount] = useState<12 | 24>(24);
const [seedDestination, setSeedDestination] = useState<'backup' | 'seedblender'>('backup');
const [detectedMode, setDetectedMode] = useState<EncryptionMode | null>(null);
const [seedForBlender, setSeedForBlender] = useState<string>('');
const [blenderResetKey, setBlenderResetKey] = useState(0);
// Network blocking state
const [isNetworkBlocked, setIsNetworkBlocked] = useState(true);
// Entropy generation states
const [entropySource, setEntropySource] = useState<'camera' | 'dice' | 'audio' | 'randomorg' | null>(null);
const [entropyStats, setEntropyStats] = useState<any>(null);
const interactionEntropyRef = useRef(new InteractionEntropy());
const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password'];
const isSensitiveKey = (key: string): boolean => {
const lowerKey = key.toLowerCase();
return SENSITIVE_PATTERNS.some(pattern => lowerKey.includes(pattern));
};
const getStorageItems = (storage: Storage): StorageItem[] => {
const items: StorageItem[] = [];
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key) {
const value = storage.getItem(key) || '';
items.push({
key,
value: value.substring(0, 50) + (value.length > 50 ? '...' : ''),
size: new Blob([value]).size,
isSensitive: isSensitiveKey(key)
});
}
}
return items.sort((a, b) => (b.isSensitive ? 1 : 0) - (a.isSensitive ? 1 : 0));
};
const refreshStorage = () => {
setLocalItems(getStorageItems(localStorage));
setSessionItems(getStorageItems(sessionStorage));
};
useEffect(() => {
refreshStorage();
const interval = setInterval(refreshStorage, 2000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
blockAllNetworks();
// setIsNetworkBlocked(true); // already set by default state
}, []);
useEffect(() => {
blockAllNetworks();
// setIsNetworkBlocked(true); // already set by default state
}, []);
// Cleanup session key on component unmount
useEffect(() => {
return () => {
destroySessionKey();
};
}, []);
useEffect(() => {
const handleCopy = (e: ClipboardEvent & Event) => {
const target = e.target as HTMLElement;
// Get selection to measure length
const selection = window.getSelection()?.toString() || '';
const length = selection.length;
if (length === 0) return; // Nothing copied
// Detect field name
let field = 'Unknown field';
// Check for data-sensitive attribute on any element first
const sensitiveAttr = target.getAttribute('data-sensitive') ||
target.closest('[data-sensitive]')?.getAttribute('data-sensitive');
if (sensitiveAttr) {
field = sensitiveAttr;
} else if (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT') {
// Try multiple ways to identify the field for legacy inputs
field =
target.getAttribute('aria-label') ||
target.getAttribute('name') ||
target.getAttribute('id') ||
(target as HTMLInputElement).type ||
target.tagName.toLowerCase();
// Check parent labels
const label = target.closest('label') ||
document.querySelector(`label[for="${target.id}"]`);
if (label) {
field = label.textContent?.trim() || field;
}
// Detect if it looks like sensitive data
const isSensitive = /mnemonic|seed|key|private|password|secret/i.test(
target.className + ' ' + field + ' ' + (target.getAttribute('placeholder') || '')
);
if (isSensitive && field === target.tagName.toLowerCase()) {
// Try to guess from placeholder
const placeholder = target.getAttribute('placeholder');
if (placeholder) {
field = placeholder.substring(0, 40) + '...';
}
}
}
setClipboardEvents(prev => [
{ timestamp: new Date(), field, length },
...prev.slice(0, 9) // Keep last 10 events
]);
};
document.addEventListener('copy', handleCopy as EventListener);
return () => document.removeEventListener('copy', handleCopy as EventListener);
}, []);
// Detect encryption mode from restore input
useEffect(() => {
if (activeTab === 'restore' && restoreInput.trim()) {
const detected = detectEncryptionMode(restoreInput);
setDetectedMode(detected);
// Auto-switch encryption mode if not already set AND it's an encrypted type
if ((detected === 'pgp' || detected === 'krux') && detected !== encryptionMode) {
setEncryptionMode(detected);
}
} else {
setDetectedMode(null);
}
}, [restoreInput, activeTab, encryptionMode]);
const clearClipboard = async () => {
try {
// Actually clear the system clipboard
await navigator.clipboard.writeText('');
// Clear history
setClipboardEvents([]);
// Show success briefly
alert('✅ Clipboard cleared and history wiped');
} catch (err) {
// Fallback for browsers that don't support clipboard API
const dummy = document.createElement('textarea');
dummy.value = '';
document.body.appendChild(dummy);
dummy.select();
document.execCommand('copy');
document.body.removeChild(dummy);
setClipboardEvents([]);
alert('✅ History cleared (clipboard may require manual clearing)');
}
};
const copyToClipboard = async (text: string | Uint8Array, fieldName = 'Data') => {
if (isReadOnly) {
setError("Copy to clipboard is disabled in Read-only mode.");
return;
}
const textToCopy = typeof text === 'string' ? text : Array.from(text).map(b => b.toString(16).padStart(2, '0')).join('');
try {
await navigator.clipboard.writeText(textToCopy);
setCopied(true);
// Add warning for sensitive data
const isSensitive = fieldName.toLowerCase().includes('mnemonic') ||
fieldName.toLowerCase().includes('seed') ||
fieldName.toLowerCase().includes('password') ||
fieldName.toLowerCase().includes('key');
if (isSensitive) {
setClipboardEvents(prev => [
{
timestamp: new Date(),
field: `${fieldName} (will clear in 10s)`,
length: textToCopy.length
},
...prev.slice(0, 9)
]);
// Auto-clear clipboard after 10 seconds by writing random data
setTimeout(async () => {
try {
const garbage = crypto.getRandomValues(new Uint8Array(Math.max(textToCopy.length, 64)))
.reduce((s, b) => s + String.fromCharCode(32 + (b % 95)), '');
await navigator.clipboard.writeText(garbage);
} catch { }
}, 10000);
// Show warning
alert(`⚠️ ${fieldName} copied to clipboard!\n\n✅ Will auto-clear in 10 seconds.\n\n🔒 Warning: Clipboard is accessible to other apps and browser extensions.`);
}
window.setTimeout(() => setCopied(false), 1500);
} catch {
const ta = document.createElement("textarea");
ta.value = textToCopy;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
}
};
// Handler for entropy generation
const handleEntropyGenerated = (mnemonic: string, stats: any) => {
setGeneratedSeed(mnemonic);
setEntropyStats(stats);
};
// Handler for sending to destination
const handleSendToDestination = () => {
if (seedDestination === 'backup') {
setMnemonic(generatedSeed);
setActiveTab('backup');
} else if (seedDestination === 'seedblender') {
setSeedForBlender(generatedSeed);
setActiveTab('seedblender');
}
// Reset Create tab
setGeneratedSeed('');
setEntropySource(null);
setEntropyStats(null);
};
const handleBackup = async () => {
setLoading(true);
setError('');
setQrPayload('');
setRecipientFpr('');
try {
const validation = await validateBip39Mnemonic(mnemonic);
if (!validation.valid) {
throw new Error(validation.error);
}
const plaintext = buildPlaintext(mnemonic, hasBip39Passphrase);
let result: EncryptionResult;
if (encryptionMode === 'seedqr') {
if (seedQrFormat === 'standard') {
const qrString = await encodeStandardSeedQR(mnemonic);
result = { framed: qrString };
} else { // compact
const qrEntropy = await encodeCompactSeedQREntropy(mnemonic);
result = { framed: qrEntropy }; // framed will hold the Uint8Array
}
} else {
// Validate PGP public key before encryption
if (publicKeyInput) {
const validation = await validatePGPKey(publicKeyInput);
if (!validation.valid) {
throw new Error(`PGP Key Validation Failed: ${validation.error}`);
}
}
// Encrypt with PGP or Krux
result = await encryptToSeed({
plaintext,
publicKeyArmored: publicKeyInput || undefined,
messagePassword: backupMessagePassword || undefined,
mode: encryptionMode,
});
}
setQrPayload(result.framed);
if (result.recipientFingerprint) {
setRecipientFpr(result.recipientFingerprint);
}
// Initialize session key before encrypting
await getSessionKey();
// Encrypt mnemonic with session key and clear plaintext state
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
_setEncryptedMnemonicCache(blob);
setMnemonic(''); // Clear plaintext mnemonic
// Clear password after successful encryption (security best practice)
setBackupMessagePassword('');
setPrivateKeyPassphrase(''); // Also clear PGP passphrase if used
} catch (e) {
setError(e instanceof Error ? e.message : 'Encryption failed');
} finally {
setLoading(false);
}
};
const handleFileUpload = async (file: File) => {
setLoading(true);
setError('');
try {
// Handle image files (QR codes)
if (file.type.startsWith('image/')) {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
canvas.width = img.width;
canvas.height = img.height;
ctx?.drawImage(img, 0, 0);
const imageData = ctx?.getImageData(0, 0, canvas.width, canvas.height);
if (!imageData) throw new Error('Could not read image');
const jsqr = (await import('jsqr')).default;
const code = jsqr(imageData.data, imageData.width, imageData.height);
if (!code) throw new Error('No QR code found in image');
// Handle binary or text QR data
const isBinary = (code.binaryData.length === 16 || code.binaryData.length === 32);
if (isBinary) {
const hex = Array.from(code.binaryData).map(b => b.toString(16).padStart(2, '0')).join('');
setRestoreInput(hex);
} else {
setRestoreInput(code.data);
}
URL.revokeObjectURL(img.src);
}
// Handle text files (PGP armor, hex, numeric SeedQR)
else if (file.type === 'text/plain' || file.name.endsWith('.txt') || file.name.endsWith('.asc')) {
const text = await file.text();
setRestoreInput(text.trim());
}
else {
throw new Error('Unsupported file type. Use PNG/JPG (QR) or TXT/ASC (armor)');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'File upload failed');
} finally {
setLoading(false);
}
};
const handleDrop = async (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0]; // Access the first file
if (file) await handleFileUpload(file);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleRestore = async () => {
setLoading(true);
setError('');
setDecryptedRestoredMnemonic(null);
try {
// Auto-detect mode if not manually set
const modeToUse = detectedMode || encryptionMode;
const result = await decryptFromSeed({
frameText: restoreInput,
privateKeyArmored: privateKeyInput || undefined,
privateKeyPassphrase: privateKeyPassphrase || undefined,
messagePassword: restoreMessagePassword || undefined,
mode: modeToUse,
});
// 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);
// Clear passwords after successful decryption (security best practice)
setRestoreMessagePassword('');
setPrivateKeyPassphrase('');
// Also clear restore input after successful decrypt
setRestoreInput('');
setTimeout(() => {
setDecryptedRestoredMnemonic(null);
}, 10000); // Auto-clear after 10 seconds
} catch (e) {
setError(e instanceof Error ? e.message : 'Decryption failed');
} finally {
setLoading(false);
}
};
const blockAllNetworks = () => {
// Store originals
(window as any).__originalFetch = window.fetch;
(window as any).__originalXHR = window.XMLHttpRequest;
(window as any).__originalWS = window.WebSocket;
(window as any).__originalImage = window.Image;
if ((navigator as any).sendBeacon) {
(window as any).__originalBeacon = navigator.sendBeacon;
}
// 1. Block fetch
window.fetch = (async () =>
Promise.reject(new Error('Network blocked by user'))
) as any;
// 2. Block XMLHttpRequest
window.XMLHttpRequest = new Proxy(XMLHttpRequest, {
construct() {
throw new Error('Network blocked: XMLHttpRequest not allowed');
}
}) as any;
// 3. Block WebSocket
window.WebSocket = new Proxy(WebSocket, {
construct() {
throw new Error('Network blocked: WebSocket not allowed');
}
}) as any;
// 4. Block BeaconAPI
(navigator as any).sendBeacon = () => {
return false;
};
// 5. Block Image src for external resources
const OriginalImage = window.Image;
window.Image = new Proxy(OriginalImage, {
construct(target) {
const img = Reflect.construct(target, []);
const originalSrcSetter = Object.getOwnPropertyDescriptor(
HTMLImageElement.prototype, 'src'
)?.set;
Object.defineProperty(img, 'src', {
configurable: true,
set(value) {
if (value && !value.startsWith('data:') && !value.startsWith('blob:')) {
throw new Error(`Network blocked: cannot load external resource`);
}
originalSrcSetter?.call(this, value);
},
get: Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src')?.get
});
return img;
}
}) as any;
// 6. Block Service Workers
if (navigator.serviceWorker) {
// Block new registrations
(navigator.serviceWorker as any).register = async () => {
throw new Error('Network blocked: Service Workers disabled');
};
// Unregister any existing service workers (defense-in-depth)
navigator.serviceWorker
.getRegistrations()
.then(regs => regs.forEach(reg => reg.unregister()))
.catch(() => {
// Ignore errors; SWs are defense-in-depth only.
});
}
};
const unblockAllNetworks = () => {
// Restore everything
if ((window as any).__originalFetch) window.fetch = (window as any).__originalFetch;
if ((window as any).__originalXHR) window.XMLHttpRequest = (window as any).__originalXHR;
if ((window as any).__originalWS) window.WebSocket = (window as any).__originalWS;
if ((window as any).__originalImage) window.Image = (window as any).__originalImage;
if ((window as any).__originalBeacon) navigator.sendBeacon = (window as any).__originalBeacon;
};
const handleToggleNetwork = () => {
setIsNetworkBlocked(!isNetworkBlocked);
if (!isNetworkBlocked) {
blockAllNetworks();
} else {
unblockAllNetworks();
}
};
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);
};
const handleResetAll = () => {
if (window.confirm('⚠️ Reset ALL data? This will clear everything including any displayed entropy analysis.')) {
// Clear component state
setMnemonic('');
setGeneratedSeed('');
setBackupMessagePassword('');
setRestoreMessagePassword('');
setPublicKeyInput('');
setPrivateKeyInput('');
setPrivateKeyPassphrase('');
setQrPayload('');
setRecipientFpr('');
setRestoreInput('');
setDecryptedRestoredMnemonic(null);
setError('');
setEntropySource(null);
setEntropyStats(null);
setSeedForBlender('');
// Clear storage and session
localStorage.clear();
sessionStorage.clear();
destroySessionKey();
_setEncryptedMnemonicCache(null);
// Force remount of key-driven components
setResetCounter(prev => prev + 1);
setBlenderResetKey(prev => prev + 1);
// Go to Create tab (fresh start)
setActiveTab('create');
}
};
const handleRestoreScanSuccess = useCallback((scannedData: string | Uint8Array) => {
if (typeof scannedData === 'string') {
setRestoreInput(scannedData);
} else {
const hex = Array.from(scannedData).map(b => b.toString(16).padStart(2, '0')).join('');
setRestoreInput(hex);
}
setShowQRScanner(false);
setError('');
}, []); // Empty dependency array means this function is stable
const handleRestoreClose = useCallback(() => {
setShowQRScanner(false);
}, []);
const handleRestoreError = useCallback((err: string) => {
setError(err);
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);
}
};
return (
<div className="min-h-screen bg-black">
<div className="max-w-md mx-auto min-h-screen bg-[#0a0a0f] bg-[radial-gradient(ellipse_at_top,#1a1a2e_0%,#0a0a0f_100%)] relative">
{/* Cyberpunk grid overlay */}
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,240,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(0,240,255,0.03)_1px,transparent_1px)] bg-[size:50px_50px] pointer-events-none" />
{/* Content wrapper */}
<div className="relative z-10">
<Header
onOpenSecurityModal={() => setShowSecurityModal(true)}
localItems={localItems}
sessionItems={sessionItems}
onOpenStorageModal={() => setShowStorageModal(true)}
events={clipboardEvents}
onOpenClipboardModal={() => setShowClipboardModal(true)}
activeTab={activeTab}
onRequestTabChange={handleRequestTabChange}
appVersion={__APP_VERSION__}
isNetworkBlocked={isNetworkBlocked}
onToggleNetwork={handleToggleNetwork}
onResetAll={handleResetAll}
/>
<main className="w-full px-4 py-3">
<div className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 shadow-[0_0_30px_rgba(0,240,255,0.3)] p-0">
<div className="p-6 md:p-8 space-y-6">
{/* Error Display */}
{error && (
<div
className="p-4 bg-[#1a1a2e] border-2 border-[#ff006e] rounded-lg text-[#ff006e] text-sm shadow-[0_0_20px_rgba(255,0,110,0.3)] flex gap-3 items-start animate-in slide-in-from-top-2"
style={{ textShadow: '0 0 5px rgba(255,0,110,0.5)' }}
>
<AlertCircle className="shrink-0 mt-0.5" size={20} />
<div>
<p className="font-bold mb-1">Error</p>
<p className="whitespace-pre-wrap">{error}</p>
</div>
</div>
)}
{/* Info Banner */}
{recipientFpr && activeTab === 'backup' && (
<div className="p-3 bg-[#16213e] border border-[#9d84b7] rounded-lg text-[#6ef3f7] text-xs shadow-[0_0_10px_rgba(157,132,183,0.3)] flex items-start gap-3 animate-in fade-in">
<Info size={16} className="shrink-0 mt-0.5" />
<div>
<strong>Recipient Key:</strong> <code className="bg-[#16213e] border border-[#00f0ff]/50 px-1.5 py-0.5 rounded font-mono text-[#00f0ff]">{recipientFpr}</code>
</div>
</div>
)}
{/* Main Content Grid */}
<div className="space-y-6">
<div className="space-y-6">
{activeTab === 'create' && (
<div className="space-y-6">
{/* Seed Length Selector */}
<div className="space-y-3">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest block text-center" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Seed Length
</label>
<div className="grid grid-cols-2 gap-3 max-w-sm mx-auto">
<button
onClick={() => setSeedWordCount(12)}
className={`py-2.5 text-sm rounded-lg font-medium transition-all ${seedWordCount === 12
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
}`}
style={seedWordCount === 12 ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
>
12 Words
</button>
<button
onClick={() => setSeedWordCount(24)}
className={`py-2.5 text-sm rounded-lg font-medium transition-all ${seedWordCount === 24
? 'bg-[#1a1a2e] text-[#00f0ff] border-2 border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]'
: 'bg-[#16213e] text-[#9d84b7] border-2 border-[#00f0ff]/30 hover:text-[#6ef3f7] hover:border-[#00f0ff]/50'
}`}
style={seedWordCount === 24 ? { textShadow: '0 0 10px rgba(0,240,255,0.8)' } : undefined}
>
24 Words
</button>
</div>
</div>
{/* Entropy Source Selection */}
{!entropySource && !generatedSeed && (
<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-xs font-bold text-[#00f0ff] uppercase tracking-widest text-center" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Choose Entropy Source
</h3>
<p className="text-[10px] text-[#6ef3f7] text-center">
All methods enhanced with mouse/keyboard timing + browser crypto
</p>
</div>
{entropyStats && (
<div className="text-xs text-center text-[#6ef3f7]">
Last entropy generated: {entropyStats.totalBits} bits
</div>
)}
<div className="space-y-3">
<button onClick={() => setEntropySource('camera')} className="w-full p-4 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg hover:bg-[#1a1a2e] hover:border-[#00f0ff] hover:shadow-[0_0_20px_rgba(0,240,255,0.3)] transition-all text-left">
<div className="flex items-center gap-3">
<Camera size={24} className="text-[#00f0ff]" />
<div>
<p className="text-sm font-bold text-[#00f0ff]">📷 Camera Entropy</p>
<p className="text-[10px] text-[#6ef3f7]">Point at bright, textured surface</p>
</div>
</div>
</button>
<button onClick={() => setEntropySource('dice')} className="w-full p-4 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg hover:bg-[#1a1a2e] hover:border-[#00f0ff] hover:shadow-[0_0_20px_rgba(0,240,255,0.3)] transition-all text-left">
<div className="flex items-center gap-3">
<Dices size={24} className="text-[#00f0ff]" />
<div>
<p className="text-sm font-bold text-[#00f0ff]">🎲 Dice Rolls</p>
<p className="text-[10px] text-[#6ef3f7]">Roll physical dice 99+ times</p>
</div>
</div>
</button>
<button onClick={() => setEntropySource('audio')} className="w-full p-4 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg hover:bg-[#1a1a2e] hover:border-[#00f0ff] hover:shadow-[0_0_20px_rgba(0,240,255,0.3)] transition-all text-left">
<div className="flex items-center gap-3">
<Mic size={24} className="text-[#00f0ff]" />
<div className="flex-1">
<div className="flex items-center gap-2">
<p className="text-sm font-bold text-[#00f0ff]">🎤 Audio Noise</p>
<span className="px-2 py-0.5 bg-[#ff006e] text-white text-[9px] rounded font-bold">BETA</span>
</div>
<p className="text-[10px] text-[#6ef3f7]">Capture ambient sound entropy</p>
</div>
</div>
</button>
<button onClick={() => setEntropySource('randomorg')} className="w-full p-4 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg hover:bg-[#1a1a2e] hover:border-[#00f0ff] hover:shadow-[0_0_20px_rgba(0,240,255,0.3)] transition-all text-left">
<div className="flex items-center gap-3">
<Dices size={24} className="text-[#00f0ff]" />
<div>
<p className="text-sm font-bold text-[#00f0ff]">🌐 Random.org D6</p>
<p className="text-[10px] text-[#6ef3f7]">Manual entropy via random.org</p>
</div>
</div>
</button>
</div>
<div className="flex items-start gap-2 p-3 bg-[#16213e] border border-[#00f0ff]/30 rounded-lg">
<Info size={14} className="text-[#00f0ff] shrink-0 mt-0.5" />
<p className="text-[10px] text-[#6ef3f7]">
<strong className="text-[#00f0ff]">Privacy:</strong> All processing happens locally in your browser. Images/audio never stored or transmitted. This app is 100% stateless.
</p>
</div>
</div>
)}
{/* Camera Entropy Component */}
{entropySource === 'camera' && !generatedSeed && (
<CameraEntropy
key={`camera-${resetCounter}`} // Force remount on reset
wordCount={seedWordCount}
onEntropyGenerated={handleEntropyGenerated}
onCancel={() => setEntropySource(null)}
interactionEntropy={interactionEntropyRef.current}
/>
)}
{/* Dice Entropy Component */}
{entropySource === 'dice' && !generatedSeed && (
<DiceEntropy
key={`dice-${resetCounter}`} // Force remount on reset
wordCount={seedWordCount}
onEntropyGenerated={handleEntropyGenerated}
onCancel={() => setEntropySource(null)}
interactionEntropy={interactionEntropyRef.current}
/>
)}
{/* Audio Entropy Component - TODO */}
{entropySource === 'audio' && !generatedSeed && (
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#ff006e] text-center">
<p className="text-sm text-[#ff006e]">Audio entropy coming soon...</p>
<button onClick={() => setEntropySource(null)} className="mt-4 px-4 py-2 bg-[#16213e] border-2 border-[#ff006e] text-[#ff006e] rounded-lg text-sm hover:bg-[#ff006e]/20 transition-all">
Back
</button>
</div>
)}
{/* Audio Entropy Component */}
{entropySource === 'audio' && !generatedSeed && (
<AudioEntropy
key={`audio-${resetCounter}`}
wordCount={seedWordCount}
onEntropyGenerated={handleEntropyGenerated}
onCancel={() => setEntropySource(null)}
interactionEntropy={interactionEntropyRef.current}
/>
)}
{/* Random.org Entropy Component */}
{entropySource === 'randomorg' && !generatedSeed && (
<RandomOrgEntropy
key={`randomorg-${resetCounter}`}
wordCount={seedWordCount}
onEntropyGenerated={handleEntropyGenerated}
onCancel={() => setEntropySource(null)}
interactionEntropy={interactionEntropyRef.current}
/>
)}
{/* Generated Seed Display + Destination Selector */}
{generatedSeed && (
<div className="space-y-4">
<div className="p-6 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-lg shadow-[0_0_30px_rgba(57,255,20,0.4)] space-y-4 animate-in zoom-in-95">
<div className="flex items-center justify-between">
<span className="font-bold text-sm text-[#39ff14] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}>
<CheckCircle2 size={20} /> Generated Successfully
</span>
</div>
<div className="relative">
<p
className="font-mono text-xs text-[#39ff14] break-words leading-relaxed blur-sensitive"
title="Hover to reveal seed"
style={{ textShadow: '0 0 5px rgba(57,255,20,0.5)' }}
>
{generatedSeed}
</p>
<p className="text-[9px] text-[#6ef3f7] mt-2 text-center">
👆 Hover to reveal - Write down securely
</p>
</div>
</div>
{/* Destination Selector */}
<div className="space-y-3">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest block text-center" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
Send Generated Seed To
</label>
<div className="grid grid-cols-2 gap-3 max-w-sm mx-auto">
<label className={`p-3 md:p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'backup' ? 'bg-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]' : 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'}`}>
<input type="radio" name="destination" value="backup" checked={seedDestination === 'backup'} onChange={(e) => setSeedDestination(e.target.value as 'backup' | 'seedblender')} className="hidden" />
<div className="flex flex-col items-center justify-center gap-1 md:gap-2">
<Lock className={`w-5 h-5 md:w-6 md:h-6 transition-colors ${seedDestination === 'backup' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`} />
<div className={`text-xs md:text-sm font-bold ${seedDestination === 'backup' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}>Backup</div>
<p className="text-[10px] text-[#6ef3f7]">Encrypt immediately</p>
</div>
</label>
<label className={`p-3 md:p-4 rounded-lg border-2 cursor-pointer transition-all ${seedDestination === 'seedblender' ? 'bg-[#1a1a2e] border-[#ff006e] shadow-[0_0_15px_rgba(255,0,110,0.5)]' : 'bg-[#16213e] border-[#00f0ff]/30 hover:border-[#00f0ff]/50'}`}>
<input type="radio" name="destination" value="seedblender" checked={seedDestination === 'seedblender'} onChange={(e) => setSeedDestination(e.target.value as 'backup' | 'seedblender')} className="hidden" />
<div className="flex flex-col items-center justify-center gap-1 md:gap-2">
<Dices className={`w-5 h-5 md:w-6 md:h-6 transition-colors ${seedDestination === 'seedblender' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`} />
<div className={`text-xs md:text-sm font-bold ${seedDestination === 'seedblender' ? 'text-[#00f0ff]' : 'text-[#9d84b7]'}`}>Seed Blender</div>
<p className="text-[10px] text-[#6ef3f7]">Use for XOR blending</p>
</div>
</label>
</div>
</div>
{/* Send Button */}
<button
onClick={handleSendToDestination}
className="w-full py-2.5 bg-[#1a1a2e] border-2 border-[#ff006e] text-[#00f0ff] text-xs md:text-sm rounded-lg font-bold uppercase tracking-wide hover:shadow-[0_0_25px_rgba(255,0,110,0.7)] active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-[0_0_15px_rgba(255,0,110,0.5)]"
style={{ textShadow: '0 0 8px rgba(0,240,255,0.7)' }}
>
Continue to {seedDestination === 'backup' ? 'Backup' : 'Seed Blender'}
</button>
<button onClick={() => { setGeneratedSeed(''); setEntropySource(null); setEntropyStats(null); }} className="w-full py-2 bg-[#16213e] border-2 border-[#00f0ff] text-[#00f0ff] rounded-lg text-sm font-bold hover:bg-[#00f0ff]/20 transition-all">
Generate Another Seed
</button>
</div>
)}
</div>
)}
<div className={activeTab === 'backup' ? 'block' : 'hidden'}>
<div className="space-y-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>BIP39 Mnemonic</label>
<div className="relative">
<textarea
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
onFocus={(e) => e.target.classList.remove('blur-sensitive')}
onBlur={(e) => mnemonic && e.target.classList.add('blur-sensitive')}
placeholder="Enter your 12 or 24 word seed phrase..."
className={`w-full h-32 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden ${mnemonic ? 'blur-sensitive' : ''
}`}
style={{
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
textShadow: '0 0 5px rgba(0,240,255,0.5)'
}}
data-sensitive="BIP39 Mnemonic"
readOnly={isReadOnly}
/>
{mnemonic && (
<p className="text-[9px] text-[#6ef3f7] mt-1">
👆 Hover or click to reveal
</p>
)}
</div>
</div>
<PgpKeyInput
label="PGP Public Key (Optional)"
icon={FileKey}
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----&#10;&#10;Paste or drag & drop your public key..."
value={publicKeyInput}
onChange={setPublicKeyInput}
readOnly={isReadOnly}
/>
</div>
<div className={activeTab === 'restore' ? 'block' : 'hidden'}>
<div className="space-y-4">
{/* File Upload Zone */}
<div
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${isDragging
? 'border-[#ff006e] bg-[#1a1a2e] shadow-[0_0_30px_rgba(255,0,110,0.5)]'
: 'border-[#00f0ff50] bg-[#16213e] hover:border-[#00f0ff] hover:bg-[#1a1a2e]'
}`}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
>
<div className="space-y-2">
<div className="text-4xl">📎</div>
<p className="text-xs text-[#00f0ff] font-medium" style={{ textShadow: '0 0 10px rgba(0,240,255,0.5)' }}>
Drag & drop file or click Browse
</p>
<p className="text-[10px] text-[#6ef3f7]">QR image, PGP armor, hex, numeric</p>
<div className="flex gap-2 justify-center pt-1">
<label className="px-3 py-1.5 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-xs font-medium text-[#00f0ff] hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] cursor-pointer transition-all flex items-center gap-2">
📁 Browse
<input
type="file"
accept="image/*,.txt,.asc"
className="hidden"
onChange={(e) => e.target.files?.length && handleFileUpload(e.target.files[0])}
/>
</label>
<button
onClick={() => setShowQRScanner(true)}
className="px-3 py-1.5 bg-transparent text-[#00f0ff] rounded-lg font-medium border-2 border-[#00f0ff] hover:bg-[#00f0ff]/10 hover:shadow-[0_0_20px_rgba(0,240,255,0.5)] transition-all flex items-center gap-2 text-xs"
>
📷 Scan QR
</button> </div>
</div>
</div>
{/* Existing restore input textarea stays here */}
<textarea
className={`w-full p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-sm text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden`}
rows={6}
placeholder="Or paste encrypted data here..."
value={restoreInput}
onChange={(e) => setRestoreInput(e.target.value)}
style={{
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
textShadow: '0 0 5px rgba(0,240,255,0.5)'
}}
/>
{/* Auto-detection hint */}
{detectedMode && (
<p className="text-xs text-[#00f0ff] flex items-center gap-1">
<Info size={14} />
Detected: {detectedMode.toUpperCase()} format
</p>
)}
</div>
<PgpKeyInput
label="PGP Private Key (Optional)"
icon={FileKey}
data-sensitive="PGP Private Key"
placeholder="-----BEGIN PGP PRIVATE KEY BLOCK-----&#10;&#10;Paste or drag & drop your private key..."
value={privateKeyInput}
onChange={setPrivateKeyInput}
readOnly={isReadOnly}
/>
{privateKeyInput && (
<div className="space-y-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Private Key Passphrase</label>
<div className="relative">
<Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} />
<input
type="password"
data-sensitive="Message Password"
className={`w-full pl-10 pr-4 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : ''
}`}
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
placeholder="Unlock private key..."
value={privateKeyPassphrase}
onChange={(e) => setPrivateKeyPassphrase(e.target.value)}
readOnly={isReadOnly}
/>
</div>
</div>
)}
</div>
<div className={activeTab === 'seedblender' ? 'block' : 'hidden'}>
<SeedBlender
key={blenderResetKey}
onDirtyStateChange={() => { }}
setMnemonicForBackup={setMnemonic}
requestTabChange={handleRequestTabChange}
incomingSeed={seedForBlender}
onSeedReceived={() => setSeedForBlender('')}
/>
</div>
<div className={activeTab === 'test-recovery' ? 'block' : 'hidden'}>
<TestRecovery
encryptionMode={encryptionMode}
backupMessagePassword={backupMessagePassword}
restoreMessagePassword={restoreMessagePassword}
publicKeyInput={publicKeyInput}
privateKeyInput={privateKeyInput}
privateKeyPassphrase={privateKeyPassphrase}
/>
</div>
</div>
{/* Security Panel */}
{activeTab !== 'seedblender' && activeTab !== 'create' && (<div className="space-y-2"> {/* Added space-y-2 wrapper */}
<label className="text-xs font-semibold text-[#00f0ff] flex items-center gap-2" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>
<Lock size={14} /> SECURITY OPTIONS
</label>
<div className="p-5 bg-[#16213e] rounded-2xl border-2 border-[#00f0ff]/30 shadow-[0_0_20px_rgba(0,240,255,0.2)] space-y-4">
{/* Removed h3 */}
{/* Encryption Mode Toggle */}
<div className="space-y-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>Encryption Mode</label>
<select
value={encryptionMode}
onChange={(e) => setEncryptionMode(e.target.value as 'pgp' | 'krux' | 'seedqr')}
disabled={isReadOnly}
className="w-full px-3 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgPHBhdGggZD0iTTEgMUw2IDZMMTEgMSIgc3Ryb2tlPSIjMDBmMGZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:12px] bg-[position:right_12px_center] bg-no-repeat pr-10"
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
>
<option value="pgp">PGP (Asymmetric)</option>
<option value="krux">Krux KEF (Passphrase)</option>
<option value="seedqr">SeedQR (Unencrypted)</option>
</select>
<p className="text-[10px] text-[#6ef3f7] mt-1">
{encryptionMode === 'pgp'
? 'Uses PGP keys or password'
: 'Uses passphrase only (Krux compatible)'}
</p>
</div>
{/* SeedQR Format Toggle */}
{encryptionMode === 'seedqr' && (
<div className="space-y-2 pt-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>SeedQR Format</label>
<select
value={seedQrFormat}
onChange={(e) => setSeedQrFormat(e.target.value as 'standard' | 'compact')}
disabled={isReadOnly}
className="w-full px-3 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all appearance-none bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iOCIgPHBhdGggZD0iTTEgMUw2IDZMMTEgMSIgc3Ryb2tlPSIjMDBmMGZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPjwvc3ZnPg==')] bg-[length:12px] bg-[position:right_12px_center] bg-no-repeat pr-10"
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
>
<option value="standard">Standard (Numeric)</option>
<option value="compact">Compact (Binary)</option>
</select>
<p className="text-[10px] text-[#6ef3f7] mt-1">
{seedQrFormat === 'standard'
? 'Numeric format, human-readable.'
: 'Compact binary format, smaller QR code.'}
</p>
</div>
)}
{/* Krux-specific fields */}
{encryptionMode === 'krux' && activeTab === 'backup' && (
<>
</>
)}
<div className="space-y-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest" style={{ textShadow: '0 0 10px rgba(0,240,255,0.7)' }}>MESSAGE PASSWORD</label>
<div className="relative">
<Lock className="absolute left-3 top-3 text-[#6ef3f7]" size={16} />
<input
type="password"
className={`w-full pl-10 pr-4 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all ${isReadOnly ? 'blur-sm select-none' : ''
}`}
style={{ textShadow: '0 0 5px rgba(0,240,255,0.5)' }}
placeholder={encryptionMode === 'krux' ? "Required for Krux encryption" : "Optional password..."}
value={activeTab === 'backup' ? backupMessagePassword : restoreMessagePassword}
onChange={(e) => activeTab === 'backup' ? setBackupMessagePassword(e.target.value) : setRestoreMessagePassword(e.target.value)}
readOnly={isReadOnly}
/>
</div>
<p className="text-[10px] text-[#6ef3f7] mt-1">
{encryptionMode === 'krux'
? 'Required passphrase for Krux encryption'
: 'Symmetric encryption password (SKESK)'}
</p>
</div>
<div className="pt-3 border-t border-[#00f0ff]/30">
<div className="flex items-start gap-2 text-xs text-[#6ef3f7]">
<Info size={14} className="shrink-0 mt-0.5" />
<p>
<strong>Krux Compatible Mode:</strong><br />
Uses wallet fingerprint as salt and 100,000 iterations (Krux defaults).
</p>
</div>
</div>
{activeTab === 'backup' && (
<div className="pt-3 border-t border-[#00f0ff]/30">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={hasBip39Passphrase}
onChange={(e) => setHasBip39Passphrase(e.target.checked)}
disabled={isReadOnly}
className="rounded text-[#00f0ff] focus:ring-2 focus:ring-[#00f0ff] transition-all"
/>
<span className="text-xs font-medium text-[#6ef3f7] group-hover:text-[#00f0ff] transition-colors">
BIP39 25th word active
</span>
</label>
</div>
)}
</div>
{/* Action Button */}
{activeTab === 'backup' ? (
<button
onClick={handleBackup}
disabled={!mnemonic || loading || isReadOnly}
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm rounded-xl font-bold uppercase tracking-wider flex items-center justify-center gap-2 hover:shadow-[0_0_30px_rgba(255,0,110,0.8)] active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-[#ff006e]"
style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
>
{loading ? (
<RefreshCw className="animate-spin" size={20} />
) : (
<QrCode size={20} />
)}
{loading ? 'Generating...' : 'Generate QR Backup'}
</button>
) : (
<button
onClick={handleRestore}
disabled={!restoreInput || loading || isReadOnly}
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm rounded-xl font-bold uppercase tracking-wider flex items-center justify-center gap-2 hover:shadow-[0_0_30px_rgba(255,0,110,0.8)] active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed border border-[#ff006e]"
style={{ textShadow: '0 0 10px rgba(255,255,255,0.8)' }}
>
{loading ? (
<RefreshCw className="animate-spin" size={20} />
) : (
<Unlock size={20} />
)}
{loading ? 'Decrypting...' : 'Decrypt & Restore'}
</button>
)}
</div>
)} </div>
{/* QR Output */}
{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} 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)' }}>
Raw payload (copy for backup)
</label>
<button
type="button"
onClick={() => copyToClipboard(qrPayload)}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#16213e] border-2 border-[#00f0ff]/50 text-[#00f0ff] text-xs font-medium hover:bg-[#1a1a2e] hover:shadow-[0_0_15px_rgba(0,240,255,0.3)] transition-all"
>
{copied ? <CheckCircle2 size={14} /> : <QrCode size={14} />}
{copied ? "Copied" : "Copy"}
</button>
</div>
<textarea
readOnly
value={typeof qrPayload === 'string' ? qrPayload : Array.from(qrPayload).map(b => b.toString(16).padStart(2, '0')).join('')}
onFocus={(e) => e.currentTarget.select()}
className="w-full h-28 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-[10px] text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] focus:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all relative overflow-hidden"
style={{
backgroundImage: 'repeating-linear-gradient(0deg, rgba(0,240,255,0.03) 0px, transparent 1px, transparent 2px, rgba(0,240,255,0.03) 3px)',
textShadow: '0 0 5px rgba(0,240,255,0.5)'
}}
/>
<p className="text-[11px] text-[#6ef3f7]">
Tip: click the box to select all, or use Copy.
</p>
</div>
</div>
)}
{/* Restored Mnemonic */}
{decryptedRestoredMnemonic && activeTab === 'restore' && (
<div className="pt-6 border-t border-[#00f0ff]/20 animate-in zoom-in-95">
<div className="p-6 bg-[#0a0a0f] border-2 border-[#39ff14] rounded-lg shadow-[0_0_30px_rgba(57,255,20,0.4)] relative overflow-hidden">
<div className="flex items-center justify-between mb-3">
<span className="font-bold text-[#39ff14] flex items-center gap-2 text-lg" style={{ textShadow: '0 0 10px rgba(57,255,20,0.8)' }}>
<CheckCircle2 size={22} /> Mnemonic Recovered
</span>
<button
onClick={() => setDecryptedRestoredMnemonic(null)}
className="p-2.5 hover:bg-[#16213e] rounded-xl transition-all text-[#39ff14] hover:shadow-[0_0_15px_rgba(57,255,20,0.5)] flex items-center gap-2"
>
<EyeOff size={22} /> Hide
</button>
</div>
<div className="p-4 bg-[#0a0a0f] rounded-xl border-2 border-[#39ff14] shadow-[0_0_20px_rgba(57,255,20,0.3)]">
<div className="relative">
<p
className="font-mono text-center text-base break-words text-[#39ff14] blur-sensitive"
title="Hover to reveal"
style={{ textShadow: '0 0 8px rgba(57,255,20,0.8)' }}
>
{decryptedRestoredMnemonic}
</p>
<p className="text-[9px] text-[#6ef3f7] mt-2 text-center">
👆 Hover to reveal decrypted seed
</p>
</div>
</div>
</div>
</div>
)}
</div>
</div> {/* Close new cyberpunk main container */}
</main >
<Footer
appVersion={__APP_VERSION__}
buildHash={__BUILD_HASH__}
buildTimestamp={__BUILD_TIMESTAMP__}
/>
{/* QR Scanner Modal */}
{showQRScanner && (
<QRScanner
onScanSuccess={handleRestoreScanSuccess}
onClose={handleRestoreClose}
onError={handleRestoreError}
/>
)}
{/* Security Modal */}
{showSecurityModal && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
onClick={() => setShowSecurityModal(false)}
>
<div
className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 p-6 max-w-md w-full mx-4 shadow-[0_0_30px_rgba(0,240,255,0.3)]"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold text-[#00f0ff] mb-4">Security Limitations</h3>
<div className="text-sm text-[#6ef3f7] space-y-2">
<SecurityWarnings />
</div>
</div>
</div>
)}
{/* Storage Modal */}
{showStorageModal && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
onClick={() => setShowStorageModal(false)}
>
<div
className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 p-6 max-w-md w-full mx-4 shadow-[0_0_30px_rgba(0,240,255,0.3)]"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold text-[#00f0ff] mb-4">Storage Details</h3>
<div className="text-sm text-[#6ef3f7] space-y-2">
<StorageDetails localItems={localItems} sessionItems={sessionItems} />
</div>
</div>
</div>
)}
{/* Clipboard Modal */}
{showClipboardModal && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
onClick={() => setShowClipboardModal(false)}
>
<div
className="bg-[#1a1a2e] rounded-xl border-2 border-[#00f0ff]/30 p-6 max-w-md w-full mx-4 shadow-[0_0_30px_rgba(0,240,255,0.3)]"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold text-[#00f0ff] mb-4">Clipboard Activity</h3>
<div className="text-sm text-[#6ef3f7] space-y-2">
<ClipboardDetails events={clipboardEvents} onClear={clearClipboard} />
</div>
</div>
</div>
)}
{/* Lock Confirmation Modal */}
{showLockConfirm && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50"
onClick={() => setShowLockConfirm(false)}
>
<div
className="bg-[#1a1a2e] rounded-xl border-2 border-[#ff006e] p-6 max-w-md w-full mx-4 shadow-[0_0_30px_rgba(255,0,110,0.3)]"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold text-[#ff006e] mb-4 flex items-center gap-2">
<Lock className="w-5 h-5 text-[#ff006e]" />
Lock Sensitive Data?
</h3>
<div className="text-sm text-[#6ef3f7] space-y-3 mb-6">
<p>This will:</p>
<ul className="list-disc list-inside space-y-1 text-[#9d84b7]">
<li>Blur all sensitive data (mnemonics, keys, passwords)</li>
<li>Disable all inputs</li>
<li>Prevent clipboard operations</li>
</ul>
<p className="text-xs text-[#6ef3f7] mt-2">
Use this when showing the app to others or stepping away from your device.
</p>
</div>
<div className="flex gap-3">
<button
className="flex-1 py-2 bg-[#16213e] hover:bg-[#1a1a2e] text-[#6ef3f7] rounded-lg transition-all border-2 border-[#00f0ff]/30 hover:border-[#00f0ff]/50"
onClick={() => setShowLockConfirm(false)}
>
Cancel
</button>
<button
className="flex-1 py-2 bg-[#ff006e] hover:bg-[#ff4d8f] text-white font-semibold rounded-lg transition-all hover:shadow-[0_0_15px_rgba(255,0,110,0.5)]"
onClick={() => setIsReadOnly(true)}
>
Lock Data
</button>
</div>
</div>
</div>
)}
</div> {/* Close relative z-10 content wrapper */}
</div>
</div> /* Close root min-h-screen */
);
}
export default App;