feat(v1.3.0): add ephemeral session-key encryption for sensitive state

- Add src/lib/sessionCrypto.ts with AES-GCM-256 non-exportable session keys
- Integrate into Backup flow: auto-clear plaintext mnemonic after QR generation
- Add Lock/Clear button to destroy session key and clear all state
- Add cleanup useEffect on component unmount
- Add comprehensive GEMINI.md for AI agent onboarding
- Fix TypeScript strict mode errors and unused imports

Tested:
- Session-key encryption working (mnemonic clears after QR gen)
- Lock/Clear functionality verified
- No plaintext secrets in localStorage/sessionStorage
- Production build successful
This commit is contained in:
LC mac
2026-01-29 23:48:21 +08:00
parent 0f397859e6
commit 2107dab501
2 changed files with 51 additions and 2 deletions

View File

@@ -1,12 +1,14 @@
# SeedPGP - Gemini Code Assist Project Brief # SeedPGP - Gemini Code Assist Project Brief
## Project Overview ## Project Overview
**SeedPGP v1.3.0**: Client-side BIP39 mnemonic encryption webapp **SeedPGP v1.3.0**: Client-side BIP39 mnemonic encryption webapp
**Stack**: Bun + Vite + React + TypeScript + OpenPGP.js + Tailwind CSS **Stack**: Bun + Vite + React + TypeScript + OpenPGP.js + Tailwind CSS
**Deploy**: GitHub Pages (public repo: `seedpgp-web-app`, private source: `seedpgp-web`) **Deploy**: GitHub Pages (public repo: `seedpgp-web-app`, private source: `seedpgp-web`)
**Live URL**: https://kccleoc.github.io/seedpgp-web-app/ **Live URL**: <https://kccleoc.github.io/seedpgp-web-app/>
## Core Constraints ## Core Constraints
1. **Security-first**: Never persist secrets (mnemonic/passphrase/private keys) to localStorage/sessionStorage/IndexedDB 1. **Security-first**: Never persist secrets (mnemonic/passphrase/private keys) to localStorage/sessionStorage/IndexedDB
2. **Small PRs**: Max 1-5 files per feature; propose plan before coding 2. **Small PRs**: Max 1-5 files per feature; propose plan before coding
3. **Client-side only**: No backend; all crypto runs in browser (Web Crypto API + OpenPGP.js) 3. **Client-side only**: No backend; all crypto runs in browser (Web Crypto API + OpenPGP.js)
@@ -14,6 +16,7 @@
5. **Honest security claims**: Don't overclaim what client-side JS can guarantee 5. **Honest security claims**: Don't overclaim what client-side JS can guarantee
## Non-Negotiables ## Non-Negotiables
- Small diffs only: one feature slice per PR (1-5 files if possible) - Small diffs only: one feature slice per PR (1-5 files if possible)
- No big code dumps; propose plan first, then implement - No big code dumps; propose plan first, then implement
- Never persist secrets to browser storage - Never persist secrets to browser storage
@@ -25,10 +28,12 @@
## Architecture Map ## Architecture Map
### Entry Points ### Entry Points
- `src/main.tsx``src/App.tsx` (main application) - `src/main.tsx``src/App.tsx` (main application)
- Build output: `dist/` (separate git repo for GitHub Pages deployment) - Build output: `dist/` (separate git repo for GitHub Pages deployment)
### Directory Structure ### Directory Structure
``` ```
src/ src/
├── components/ # React UI components ├── components/ # React UI components
@@ -51,14 +56,18 @@ src/
### Key Modules ### Key Modules
#### `src/lib/seedpgp.ts` #### `src/lib/seedpgp.ts`
Core encryption/decryption: Core encryption/decryption:
- `encryptToSeedPgp()` - Encrypts mnemonic with PGP public key + optional password - `encryptToSeedPgp()` - Encrypts mnemonic with PGP public key + optional password
- `decryptFromSeedPgp()` - Decrypts with PGP private key + optional password - `decryptFromSeedPgp()` - Decrypts with PGP private key + optional password
- Uses OpenPGP.js for PGP operations - Uses OpenPGP.js for PGP operations
- Output format: `SEEDPGP1:version:base64data:fingerprint` - Output format: `SEEDPGP1:version:base64data:fingerprint`
#### `src/lib/sessionCrypto.ts` (v1.3.0+) #### `src/lib/sessionCrypto.ts` (v1.3.0+)
Ephemeral session-key encryption: Ephemeral session-key encryption:
- `getSessionKey()` - Generates/returns non-exportable AES-GCM-256 key (idempotent) - `getSessionKey()` - Generates/returns non-exportable AES-GCM-256 key (idempotent)
- `encryptJsonToBlob(obj)` - Encrypts to `{v, alg, iv_b64, ct_b64}` - `encryptJsonToBlob(obj)` - Encrypts to `{v, alg, iv_b64, ct_b64}`
- `decryptBlobToJson(blob)` - Decrypts back to original object - `decryptBlobToJson(blob)` - Decrypts back to original object
@@ -66,7 +75,9 @@ Ephemeral session-key encryption:
- Test: `await window.runSessionCryptoTest()` (DEV only) - Test: `await window.runSessionCryptoTest()` (DEV only)
#### `src/lib/types.ts` #### `src/lib/types.ts`
Core interfaces: Core interfaces:
- `SeedPgpPlaintext` - Decrypted mnemonic data structure - `SeedPgpPlaintext` - Decrypted mnemonic data structure
- `SeedPgpCiphertext` - Encrypted payload structure - `SeedPgpCiphertext` - Encrypted payload structure
- `EncryptedBlob` - Session-key encrypted cache format - `EncryptedBlob` - Session-key encrypted cache format
@@ -76,21 +87,25 @@ Core interfaces:
## Key Features ## Key Features
### v1.0 - Core Functionality ### v1.0 - Core Functionality
- **Backup**: Encrypt mnemonic with PGP public key + optional password → QR display - **Backup**: Encrypt mnemonic with PGP public key + optional password → QR display
- **Restore**: Scan/paste QR → decrypt with private key → show mnemonic - **Restore**: Scan/paste QR → decrypt with private key → show mnemonic
- **PGP support**: Import public/private keys (.asc files or paste) - **PGP support**: Import public/private keys (.asc files or paste)
### v1.1 - QR Features ### v1.1 - QR Features
- **QR Display**: Generate QR codes from encrypted data - **QR Display**: Generate QR codes from encrypted data
- **QR Scanner**: Camera + file upload (uses html5-qrcode library) - **QR Scanner**: Camera + file upload (uses html5-qrcode library)
### v1.2 - Security Monitoring ### v1.2 - Security Monitoring
- **Storage Indicator**: Real-time display of localStorage/sessionStorage contents - **Storage Indicator**: Real-time display of localStorage/sessionStorage contents
- **Security Warnings**: Context-aware alerts about browser memory limitations - **Security Warnings**: Context-aware alerts about browser memory limitations
- **Clipboard Tracker**: Monitor clipboard operations on sensitive fields - **Clipboard Tracker**: Monitor clipboard operations on sensitive fields
- **Read-only Mode**: Toggle to clear state + show CSP/build info - **Read-only Mode**: Toggle to clear state + show CSP/build info
### v1.3 - Session-Key Encryption (Current) ### v1.3 - Session-Key Encryption (Current)
- **Ephemeral encryption**: AES-GCM-256 session key (non-exportable) encrypts sensitive state - **Ephemeral encryption**: AES-GCM-256 session key (non-exportable) encrypts sensitive state
- **Auto-clear**: Plaintext mnemonic cleared from UI immediately after QR generation - **Auto-clear**: Plaintext mnemonic cleared from UI immediately after QR generation
- **Encrypted cache**: Only ciphertext stored in React state; key lives in memory only - **Encrypted cache**: Only ciphertext stored in React state; key lives in memory only
@@ -102,6 +117,7 @@ Core interfaces:
## Development Workflow ## Development Workflow
### Commands ### Commands
```bash ```bash
bun install # Install dependencies bun install # Install dependencies
bun run dev # Dev server (localhost:5173) bun run dev # Dev server (localhost:5173)
@@ -112,11 +128,13 @@ bun run preview # Preview production build
``` ```
### Deployment Process ### Deployment Process
1. **Private repo** (`seedpgp-web`): Source code, development 1. **Private repo** (`seedpgp-web`): Source code, development
2. **Public repo** (`seedpgp-web-app`): Built files for GitHub Pages 2. **Public repo** (`seedpgp-web-app`): Built files for GitHub Pages
3. **Deploy script** (`scripts/deploy.sh`): Builds + copies to dist/ + pushes to public repo 3. **Deploy script** (`scripts/deploy.sh`): Builds + copies to dist/ + pushes to public repo
### Git Workflow ### Git Workflow
```bash ```bash
# Commit feature # Commit feature
git add src/ git add src/
@@ -135,24 +153,29 @@ git push origin main --tags
## Required Workflow for AI Agents ## Required Workflow for AI Agents
### 1. Study First ### 1. Study First
Before implementing any feature: Before implementing any feature:
- Read relevant files - Read relevant files
- Explain current architecture + entry points - Explain current architecture + entry points
- List files that will be touched - List files that will be touched
- Identify potential conflicts or dependencies - Identify potential conflicts or dependencies
### 2. Plan ### 2. Plan
- Propose smallest vertical slice (1-5 files) - Propose smallest vertical slice (1-5 files)
- Show API signatures or interface changes first - Show API signatures or interface changes first
- Get approval before generating full implementation - Get approval before generating full implementation
### 3. Implement ### 3. Implement
- Generate code with TypeScript strict mode - Generate code with TypeScript strict mode
- Include JSDoc comments for public APIs - Include JSDoc comments for public APIs
- Show unified diffs, not full file rewrites (when possible) - Show unified diffs, not full file rewrites (when possible)
- Keep changes under 50-100 lines per file when feasible - Keep changes under 50-100 lines per file when feasible
### 4. Verify ### 4. Verify
- Run `bun run typecheck` - no errors - Run `bun run typecheck` - no errors
- Run `bun run build` - successful dist/ output - Run `bun run build` - successful dist/ output
- Provide manual test steps for browser verification - Provide manual test steps for browser verification
@@ -163,25 +186,30 @@ Before implementing any feature:
## Common Patterns ## Common Patterns
### State Management ### State Management
- React `useState` + `useEffect` (no Redux/Zustand/external store) - React `useState` + `useEffect` (no Redux/Zustand/external store)
- Ephemeral state only; avoid persistent storage for secrets - Ephemeral state only; avoid persistent storage for secrets
### Styling ### Styling
- Tailwind utility classes (configured in `tailwind.config.js`) - Tailwind utility classes (configured in `tailwind.config.js`)
- Responsive design: mobile-first with `md:` breakpoints - Responsive design: mobile-first with `md:` breakpoints
- Dark theme primary: slate-900 background, blue-400 accents - Dark theme primary: slate-900 background, blue-400 accents
### Icons ### Icons
- `lucide-react` library - `lucide-react` library
- Common: Shield, QrCode, Lock, Eye, AlertCircle - Common: Shield, QrCode, Lock, Eye, AlertCircle
### Crypto Operations ### Crypto Operations
- **PGP**: OpenPGP.js (`openpgp` package) - **PGP**: OpenPGP.js (`openpgp` package)
- **Session keys**: Web Crypto API (`crypto.subtle`) - **Session keys**: Web Crypto API (`crypto.subtle`)
- **Key generation**: `crypto.subtle.generateKey()` with `extractable: false` - **Key generation**: `crypto.subtle.generateKey()` with `extractable: false`
- **Encryption**: AES-GCM with random 12-byte IV per operation - **Encryption**: AES-GCM with random 12-byte IV per operation
### Type Safety ### Type Safety
- Strict TypeScript (`tsconfig.json`: `strict: true`) - Strict TypeScript (`tsconfig.json`: `strict: true`)
- Check `src/lib/types.ts` for core interfaces - Check `src/lib/types.ts` for core interfaces
- Avoid `any`; use `unknown` + type guards when necessary - Avoid `any`; use `unknown` + type guards when necessary
@@ -191,22 +219,27 @@ Before implementing any feature:
## Security Architecture ## Security Architecture
### Threat Model (Honest) ### Threat Model (Honest)
**What we protect against:** **What we protect against:**
- Accidental persistence to localStorage/sessionStorage - Accidental persistence to localStorage/sessionStorage
- Plaintext secrets lingering in React state after use - Plaintext secrets lingering in React state after use
- Clipboard history exposure (with warnings) - Clipboard history exposure (with warnings)
**What we DON'T protect against (and must not claim to):** **What we DON'T protect against (and must not claim to):**
- Active XSS or malicious browser extensions - Active XSS or malicious browser extensions
- Memory dumps or browser crash reports - Memory dumps or browser crash reports
- JavaScript garbage collection timing (non-deterministic) - JavaScript garbage collection timing (non-deterministic)
### Memory Handling ### Memory Handling
- **Session keys**: Non-exportable CryptoKey objects (Web Crypto API) - **Session keys**: Non-exportable CryptoKey objects (Web Crypto API)
- **Plaintext clearing**: Set to empty string + drop references (but GC timing is non-deterministic) - **Plaintext clearing**: Set to empty string + drop references (but GC timing is non-deterministic)
- **No guarantees**: Cannot force immediate memory wiping in JavaScript - **No guarantees**: Cannot force immediate memory wiping in JavaScript
### Storage Policy ### Storage Policy
- **NEVER write to**: localStorage, sessionStorage, IndexedDB, cookies - **NEVER write to**: localStorage, sessionStorage, IndexedDB, cookies
- **Exception**: Non-sensitive UI state only (theme preferences, etc.) - NOT IMPLEMENTED YET - **Exception**: Non-sensitive UI state only (theme preferences, etc.) - NOT IMPLEMENTED YET
- **Verification**: StorageIndicator component monitors all storage APIs - **Verification**: StorageIndicator component monitors all storage APIs
@@ -216,17 +249,20 @@ Before implementing any feature:
## What NOT to Do ## What NOT to Do
### Code Generation ### Code Generation
- Don't generate full file rewrites unless necessary - Don't generate full file rewrites unless necessary
- Don't add dependencies without discussing bundle size impact - Don't add dependencies without discussing bundle size impact
- Don't use `any` types without explicit justification - Don't use `any` types without explicit justification
- Don't skip TypeScript strict mode checks - Don't skip TypeScript strict mode checks
### Security Claims ### Security Claims
- Don't claim "RAM is wiped" (JavaScript can't force GC) - Don't claim "RAM is wiped" (JavaScript can't force GC)
- Don't claim "offline mode" without real CSP headers (GitHub Pages can't set custom headers) - Don't claim "offline mode" without real CSP headers (GitHub Pages can't set custom headers)
- Don't promise protection against active browser compromise (XSS/extensions) - Don't promise protection against active browser compromise (XSS/extensions)
### Storage ### Storage
- Don't write secrets to storage without explicit approval - Don't write secrets to storage without explicit approval
- Don't cache decrypted data beyond immediate use - Don't cache decrypted data beyond immediate use
- Don't assume browser storage is secure - Don't assume browser storage is secure
@@ -236,6 +272,7 @@ Before implementing any feature:
## Testing & Verification ## Testing & Verification
### Manual Test Checklist (Before Marking Feature Complete) ### Manual Test Checklist (Before Marking Feature Complete)
1.`bun run typecheck` passes (no TypeScript errors) 1.`bun run typecheck` passes (no TypeScript errors)
2.`bun run build` succeeds (dist/ generated) 2.`bun run build` succeeds (dist/ generated)
3. ✅ Browser test: Feature works as described 3. ✅ Browser test: Feature works as described
@@ -244,6 +281,7 @@ Before implementing any feature:
6. ✅ DevTools Network tab: No unexpected network calls (if Read-only Mode) 6. ✅ DevTools Network tab: No unexpected network calls (if Read-only Mode)
### Session-Key Encryption Test (v1.3+) ### Session-Key Encryption Test (v1.3+)
```javascript ```javascript
// In browser DevTools console: // In browser DevTools console:
await window.runSessionCryptoTest() await window.runSessionCryptoTest()
@@ -255,17 +293,20 @@ await window.runSessionCryptoTest()
## Current Version: v1.3.0 ## Current Version: v1.3.0
### Recent Changes (2026-01-29) ### Recent Changes (2026-01-29)
- ✅ Added `src/lib/sessionCrypto.ts` with ephemeral AES-GCM session keys - ✅ Added `src/lib/sessionCrypto.ts` with ephemeral AES-GCM session keys
- ✅ Integrated into Backup flow: plaintext mnemonic auto-cleared after QR generation - ✅ Integrated into Backup flow: plaintext mnemonic auto-cleared after QR generation
- ✅ Added Lock/Clear button to destroy session key and clear all state - ✅ Added Lock/Clear button to destroy session key and clear all state
- ✅ Added cleanup on component unmount - ✅ Added cleanup on component unmount
### Known Limitations ### Known Limitations
- GitHub Pages cannot set custom CSP headers (need Cloudflare Pages for enforcement) - GitHub Pages cannot set custom CSP headers (need Cloudflare Pages for enforcement)
- Read-only Mode is UI-level only (not browser-enforced) - Read-only Mode is UI-level only (not browser-enforced)
- Session-key encryption doesn't protect against active XSS/extensions - Session-key encryption doesn't protect against active XSS/extensions
### Next Priorities (Suggested) ### Next Priorities (Suggested)
1. Extend session-key encryption to Restore flow 1. Extend session-key encryption to Restore flow
2. Migrate to Cloudflare Pages for real CSP header enforcement 2. Migrate to Cloudflare Pages for real CSP header enforcement
3. Add "Encrypted in memory" badge when encryptedMnemonicCache exists 3. Add "Encrypted in memory" badge when encryptedMnemonicCache exists
@@ -276,12 +317,15 @@ await window.runSessionCryptoTest()
## Quick Reference ## Quick Reference
### File a Bug/Feature ### File a Bug/Feature
1. Describe expected vs actual behavior 1. Describe expected vs actual behavior
2. Include browser console errors (if any) 2. Include browser console errors (if any)
3. Specify which flow (Backup/Restore/QR Scanner) 3. Specify which flow (Backup/Restore/QR Scanner)
### Roll Over to Next Session ### Roll Over to Next Session
Always provide: Always provide:
- Current version number - Current version number
- What was implemented this session - What was implemented this session
- Files modified - Files modified
@@ -293,6 +337,7 @@ Always provide:
## Example Prompts for Gemini ## Example Prompts for Gemini
### Exploration ### Exploration
``` ```
Read GEMINI.md, then explain: Read GEMINI.md, then explain:
1. Where is the mnemonic textarea and how is its value managed? 1. Where is the mnemonic textarea and how is its value managed?
@@ -301,6 +346,7 @@ Read GEMINI.md, then explain:
``` ```
### Feature Request ### Feature Request
``` ```
Task: [Feature description] Task: [Feature description]
@@ -315,6 +361,7 @@ Plan first: show proposed API/changes before generating code.
``` ```
### Verification ### Verification
``` ```
Audit the codebase to verify [feature] is fully implemented. Audit the codebase to verify [feature] is fully implemented.
Check: Check:

View File

@@ -23,7 +23,7 @@ import { StorageIndicator } from './components/StorageIndicator';
import { SecurityWarnings } from './components/SecurityWarnings'; import { SecurityWarnings } from './components/SecurityWarnings';
import { ClipboardTracker } from './components/ClipboardTracker'; import { ClipboardTracker } from './components/ClipboardTracker';
import { ReadOnly } from './components/ReadOnly'; import { ReadOnly } from './components/ReadOnly';
import { encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto'; import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto';
console.log("OpenPGP.js version:", openpgp.config.versionString); console.log("OpenPGP.js version:", openpgp.config.versionString);
@@ -122,6 +122,8 @@ import { encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessi
setRecipientFpr(result.recipientFingerprint); setRecipientFpr(result.recipientFingerprint);
} }
// Initialize session key before encrypting
await getSessionKey();
// Encrypt mnemonic with session key and clear plaintext state // Encrypt mnemonic with session key and clear plaintext state
const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() }); const blob = await encryptJsonToBlob({ mnemonic, timestamp: Date.now() });
setEncryptedMnemonicCache(blob); setEncryptedMnemonicCache(blob);