diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2a6fd70 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,53 @@ +# SeedPGP Agent Brief (read first) + +## What this repo is + +SeedPGP: a client-side BIP39 mnemonic encryption web app. +Goal: add features without changing security assumptions or breaking GH Pages deploy. + +## Non-negotiables + +- Small diffs only: one feature slice per PR (1-5 files if possible). +- No big code dumps; propose plan first, then implement. +- Never persist secrets (mnemonic, passphrases, private keys) to localStorage/sessionStorage. +- Prefer “explain what you found in the repo” over guessing. + +## How to run + +- Install deps: `bun install` +- Dev: `bun run dev` +- Build: `bun run build` +- Tests/lint (if present): `bun run test`, `bun run lint`, `bun run typecheck` + +## Repo map (confirm/update) + +- UI entry: `src/main.tsx` +- Components: `src/components/` +- Core logic/types: `src/lib/` + +## Deploy + +There is a deploy script (see `scripts/deploy.sh`) and a separate public repo for built output. + +## Required workflow for every task + +1) Repo study: identify entry points + relevant modules, list files to touch. +2) Plan: smallest vertical slice, with acceptance criteria. +3) Implement: code + minimal tests or manual verification steps. +4) Evidence: paste command output (build/test) and note any tradeoffs. + +## Security Architecture (v1.3.0+) + +- **Session-key encryption**: Ephemeral AES-GCM-256 key (non-exportable) encrypts sensitive state +- **Auto-clear**: Plaintext mnemonic cleared from UI immediately after QR generation +- **Encrypted cache**: Only ciphertext stored in React state; key lives in memory only +- **Lock/Clear**: Manual cleanup destroys session key + clears all state +- **Lifecycle**: Session key auto-destroyed on page close/refresh + +## Module: src/lib/sessionCrypto.ts + +- `getSessionKey()` - Generates/returns non-exportable AES-GCM key (idempotent) +- `encryptJsonToBlob(obj)` - Encrypts to {v, alg, iv_b64, ct_b64} +- `decryptBlobToJson(blob)` - Decrypts back to original object +- `destroySessionKey()` - Drops key reference for GC +- Test: `await window.runSessionCryptoTest()` (DEV only) diff --git a/src/App.tsx b/src/App.tsx index 936e172..5e3e1b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,16 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Shield, QrCode, RefreshCw, - CheckCircle2, + CheckCircle2, Lock, AlertCircle, - Lock, Unlock, Eye, EyeOff, FileKey, - Info + Info, + WifiOff } from 'lucide-react'; import { PgpKeyInput } from './components/PgpKeyInput'; import { QrDisplay } from './components/QrDisplay'; @@ -22,431 +22,523 @@ import * as openpgp from 'openpgp'; import { StorageIndicator } from './components/StorageIndicator'; import { SecurityWarnings } from './components/SecurityWarnings'; import { ClipboardTracker } from './components/ClipboardTracker'; - -console.log("OpenPGP.js version:", openpgp.config.versionString); - -function App() { - const [activeTab, setActiveTab] = useState<'backup' | 'restore'>('backup'); - 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(''); - const [recipientFpr, setRecipientFpr] = useState(''); - const [restoreInput, setRestoreInput] = useState(''); - const [restoredData, setRestoredData] = useState(null); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - const [showMnemonic, setShowMnemonic] = useState(false); - const [copied, setCopied] = useState(false); - const [showQRScanner, setShowQRScanner] = useState(false); - - const copyToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - setCopied(true); - window.setTimeout(() => setCopied(false), 1500); - } catch { - const ta = document.createElement("textarea"); - ta.value = text; - 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); - } - }; - - const handleBackup = async () => { - setLoading(true); - setError(''); - setQrPayload(''); - setRecipientFpr(''); - - try { - const validation = validateBip39Mnemonic(mnemonic); - if (!validation.valid) { - throw new Error(validation.error); - } - - const plaintext = buildPlaintext(mnemonic, hasBip39Passphrase); - - const result = await encryptToSeedPgp({ - plaintext, - publicKeyArmored: publicKeyInput || undefined, - messagePassword: backupMessagePassword || undefined, // Changed - }); - - setQrPayload(result.framed); - if (result.recipientFingerprint) { - setRecipientFpr(result.recipientFingerprint); - } - } catch (e) { - setError(e instanceof Error ? e.message : 'Encryption failed'); - } finally { - setLoading(false); - } - }; - - const handleRestore = async () => { - setLoading(true); - setError(''); - setRestoredData(null); - - try { - const result = await decryptSeedPgp({ - frameText: restoreInput, - privateKeyArmored: privateKeyInput || undefined, - privateKeyPassphrase: privateKeyPassphrase || undefined, - messagePassword: restoreMessagePassword || undefined, // Changed - }); - - - setRestoredData(result); - } catch (e) { - setError(e instanceof Error ? e.message : 'Decryption failed'); - } finally { - setLoading(false); - } - }; - - - return ( - <> -
-
- - {/* Header */} -
-
-
- -
-
-

- SeedPGP v1.2 -

-

OpenPGP-secured BIP39 backup

-
-
-
- - -
-
- -
- {/* Error Display */} - {error && ( -
- -
-

Error

-

{error}

-
-
- )} - - {/* Info Banner */} - {recipientFpr && activeTab === 'backup' && ( -
- -
- Recipient Key: {recipientFpr} -
-
- )} - - {/* Main Content Grid */} -
-
- {activeTab === 'backup' ? ( - <> -
- -