docs: enhance documentation with threat model, limitations, air-gapped guidance

- Update version to v1.4.4
- Add explicit threat model documentation
- Document known limitations prominently
- Include air-gapped usage recommendations
- Polish all documentation for clarity and examples
- Update README, DEVELOPMENT.md, GEMINI.md, RECOVERY_PLAYBOOK.md
This commit is contained in:
LC mac
2026-02-03 02:24:59 +08:00
parent a7ab757669
commit 4353ec0cc2
10 changed files with 1208 additions and 333 deletions

View File

@@ -14,7 +14,7 @@ import { PgpKeyInput } from './components/PgpKeyInput';
import { QrDisplay } from './components/QrDisplay';
import QRScanner from './components/QRScanner';
import { validateBip39Mnemonic } from './lib/bip39';
import { buildPlaintext, encryptToSeedPgp, decryptSeedPgp } from './lib/seedpgp';
import { buildPlaintext, encryptToSeed, decryptFromSeed, detectEncryptionMode } from './lib/seedpgp';
import * as openpgp from 'openpgp';
import { SecurityWarnings } from './components/SecurityWarnings';
import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto';
@@ -66,6 +66,12 @@ function App() {
const [clipboardEvents, setClipboardEvents] = useState<ClipboardEvent[]>([]);
const [showLockConfirm, setShowLockConfirm] = useState(false);
// Krux integration state
const [encryptionMode, setEncryptionMode] = useState<'pgp' | 'krux'>('pgp');
const [kruxLabel, setKruxLabel] = useState('Seed Backup');
const [kruxIterations, setKruxIterations] = useState(200000);
const [detectedMode, setDetectedMode] = useState<'pgp' | 'krux' | null>(null);
const SENSITIVE_PATTERNS = ['key', 'mnemonic', 'seed', 'private', 'secret', 'pgp', 'password'];
const isSensitiveKey = (key: string): boolean => {
@@ -170,6 +176,20 @@ function App() {
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 mode if not already set
if (detected !== encryptionMode) {
setEncryptionMode(detected);
}
} else {
setDetectedMode(null);
}
}, [restoreInput, activeTab, encryptionMode]);
const clearClipboard = async () => {
try {
// Actually clear the system clipboard
@@ -233,10 +253,13 @@ function App() {
const plaintext = buildPlaintext(mnemonic, hasBip39Passphrase);
const result = await encryptToSeedPgp({
const result = await encryptToSeed({
plaintext,
publicKeyArmored: publicKeyInput || undefined,
messagePassword: backupMessagePassword || undefined,
mode: encryptionMode,
kruxLabel: encryptionMode === 'krux' ? kruxLabel : undefined,
kruxIterations: encryptionMode === 'krux' ? kruxIterations : undefined,
});
setQrPayload(result.framed);
@@ -263,11 +286,15 @@ function App() {
setDecryptedRestoredMnemonic(null);
try {
const result = await decryptSeedPgp({
// 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
@@ -459,6 +486,66 @@ function App() {
<div className="p-5 bg-gradient-to-br from-slate-50 to-slate-100 rounded-2xl border-2 border-slate-200 shadow-inner space-y-4">
{/* Removed h3 */}
{/* Encryption Mode Toggle */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Encryption Mode</label>
<select
value={encryptionMode}
onChange={(e) => setEncryptionMode(e.target.value as 'pgp' | 'krux')}
disabled={isReadOnly}
className="w-full px-3 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all"
>
<option value="pgp">PGP (Asymmetric)</option>
<option value="krux">Krux KEF (Passphrase)</option>
</select>
<p className="text-[10px] text-slate-500 mt-1">
{encryptionMode === 'pgp'
? 'Uses PGP keys or password'
: 'Uses passphrase only (Krux compatible)'}
</p>
</div>
{/* Krux-specific fields */}
{encryptionMode === 'krux' && activeTab === 'backup' && (
<>
<div className="space-y-2 pt-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Krux Label</label>
<div className="relative">
<input
type="text"
className={`w-full pl-3 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="e.g., My Seed 2026"
value={kruxLabel}
onChange={(e) => setKruxLabel(e.target.value)}
readOnly={isReadOnly}
/>
</div>
<p className="text-[10px] text-slate-500 mt-1">Label for identification (max 252 bytes)</p>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">PBKDF2 Iterations</label>
<div className="relative">
<input
type="number"
className={`w-full pl-3 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="e.g., 200000"
value={kruxIterations}
onChange={(e) => setKruxIterations(Number(e.target.value))}
min={10000}
step={10000}
readOnly={isReadOnly}
/>
</div>
<p className="text-[10px] text-slate-500 mt-1">Higher = more secure but slower (default: 200,000)</p>
</div>
</>
)}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase tracking-wider">Message Password</label>
@@ -469,13 +556,17 @@ function App() {
className={`w-full pl-10 pr-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-teal-500 transition-all ${
isReadOnly ? 'blur-sm select-none' : ''
}`}
placeholder="Optional password..."
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-slate-500 mt-1">Symmetric encryption password (SKESK)</p>
<p className="text-[10px] text-slate-500 mt-1">
{encryptionMode === 'krux'
? 'Required passphrase for Krux encryption'
: 'Symmetric encryption password (SKESK)'}
</p>
</div>