security improvement and bugs fixing; modify makefile

This commit is contained in:
LC mac
2026-02-18 03:24:05 +08:00
parent 127b479f4f
commit 4da39b7b89
21 changed files with 52111 additions and 930 deletions

View File

@@ -1,10 +1,10 @@
# SeedPGP Security Patches - Implementation Summary
## Overview
## Overview (February 17, 2026)
All critical security patches from the forensic security audit have been successfully implemented into the SeedPGP web application. The application is now protected against seed theft, malware injection, memory exposure, and cryptographic attacks.
## Implementation Status: ✅ COMPLETE
## Implementation Status: ✅ COMPLETE (v1.4.7)
### Patch 1: Content Security Policy (CSP) Headers ✅ COMPLETE
@@ -14,16 +14,7 @@ All critical security patches from the forensic security audit have been success
**Implementation:**
```html
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
script-src 'self' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'none';
frame-ancestors 'none';
base-uri 'self';
form-action 'none';
"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'none'; font-src 'self'; object-src 'none'; media-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'none';">
```
**Additional Headers:**

View File

@@ -1,13 +1,13 @@
# SeedPGP Web Application - Comprehensive Forensic Security Audit Report
**Audit Date:** February 12, 2026
**Audit Date:** February 12, 2026 (Patched February 17, 2026)
**Application:** seedpgp-web v1.4.7
**Scope:** Full encryption, key management, and seed handling application
**Severity Levels:** CRITICAL | HIGH | MEDIUM | LOW
---
## Executive Summary
## Executive Summary & Remediation Status
This forensic audit identified **19 actively exploitable security vulnerabilities** across the SeedPGP web application that could result in:
@@ -1802,13 +1802,13 @@ export function diceToBytes(diceRolls: string): Uint8Array {
### Immediate Critical Fixes (Do First)
| Issue | Fix | Effort | Impact |
|-------|-----|--------|--------|
| Add CSP Header | Implement strict CSP in index.html | 30 min | CRITICAL |
| Remove Plaintext Mnemonic State | Encrypt all seeds in state | 4 hours | CRITICAL |
| Add BIP39 Validation | Implement checksum verification | 1 hour | CRITICAL |
| Disable Console Logs | Remove all crypto output from console | 30 min | CRITICAL |
| Restrict Clipboard Access | Add warnings and auto-clear | 1 hour | CRITICAL |
| Issue | Status |
|-------|--------|
| Add CSP Header | **Fixed** |
| Remove Plaintext Mnemonic State | **Fixed** |
| Add BIP39 Validation | **Fixed** |
| Disable Console Logs | **Fixed** |
| Restrict Clipboard Access | **Fixed** |
### High Priority (Next Sprint)
@@ -1929,6 +1929,6 @@ For production use with large sums, recommend: **Krux Device** or **Trezor** har
---
**Report Compiled:** February 12, 2026
**Report Compiled:** February 12, 2026 (Updated: February 17, 2026)
**Audit Conducted By:** Security Forensics Analysis System
**Severity Rating:** CRITICAL - 19 Issues Identified
**Remediation Status:** COMPLETE

View File

@@ -1,499 +0,0 @@
# Priority Security Patches for SeedPGP
This document outlines the critical security patches needed for production deployment.
## PATCH 1: Add Content Security Policy (CSP)
**File:** `index.html`
Add this meta tag in the `<head>`:
```html
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
script-src 'self' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'none';
form-action 'none';
frame-ancestors 'none';
base-uri 'self';
upgrade-insecure-requests;
block-all-mixed-content;
report-uri https://security.example.com/csp-report
" />
```
**Why:** Prevents malicious extensions and XSS attacks from injecting code that steals seeds.
---
## PATCH 2: Encrypt All Seeds in State (Stop Using Plain Strings)
**File:** `src/App.tsx`
**Change From:**
```typescript
const [mnemonic, setMnemonic] = useState('');
const [backupMessagePassword, setBackupMessagePassword] = useState('');
```
**Change To:**
```typescript
// Only store encrypted reference
const [mnemonicEncrypted, setMnemonicEncrypted] = useState<EncryptedBlob | null>(null);
const [passwordEncrypted, setPasswordEncrypted] = useState<EncryptedBlob | null>(null);
// Use wrapper function to decrypt temporarily when needed
async function withMnemonic<T>(
callback: (mnemonic: string) => Promise<T>
): Promise<T | null> {
if (!mnemonicEncrypted) return null;
const decrypted = await decryptBlobToJson<{ value: string }>(mnemonicEncrypted);
try {
return await callback(decrypted.value);
} finally {
// Zero attempt (won't fully work, but good practice)
Object.assign(decrypted, { value: '\0'.repeat(decrypted.value.length) });
}
}
```
**Why:** Sensitive data stored encrypted in React state, not as plaintext. Prevents memory dumps and malware from easily accessing seeds.
---
## PATCH 3: Implement BIP39 Checksum Validation
**File:** `src/lib/bip39.ts`
**Replace:**
```typescript
export function validateBip39Mnemonic(words: string): { valid: boolean; error?: string } {
// ... word count only
return { valid: true };
}
```
**With:**
```typescript
export async function validateBip39Mnemonic(words: string): Promise<{ valid: boolean; error?: string }> {
const normalized = normalizeBip39Mnemonic(words);
const arr = normalized.length ? normalized.split(" ") : [];
const validCounts = new Set([12, 15, 18, 21, 24]);
if (!validCounts.has(arr.length)) {
return {
valid: false,
error: `Invalid word count: ${arr.length}. Must be 12, 15, 18, 21, or 24.`,
};
}
// ✅ NEW: Verify each word is in wordlist and checksum is valid
try {
// This will throw if mnemonic is invalid
await mnemonicToEntropy(normalized);
return { valid: true };
} catch (e) {
return {
valid: false,
error: `Invalid BIP39 mnemonic: ${e instanceof Error ? e.message : 'Unknown error'}`
};
}
}
```
**Usage Update:**
```typescript
// Update all validation calls to use await
const validation = await validateBip39Mnemonic(mnemonic);
if (!validation.valid) {
setError(validation.error);
return;
}
```
**Why:** Prevents users from backing up invalid seeds or corrupted mnemonics.
---
## PATCH 4: Disable Console Output of Sensitive Data
**File:** `src/main.tsx`
**Add at top:**
```typescript
// Disable all console output in production
if (import.meta.env.PROD) {
console.log = () => {};
console.error = () => {};
console.warn = () => {};
console.debug = () => {};
}
```
**File:** `src/lib/krux.ts`
**Remove:**
```typescript
console.log('🔐 KEF Debug:', { label, iterations, version, length: kef.length, base43: kefBase43.slice(0, 50) });
console.error("Krux decryption internal error:", error);
```
**File:** `src/components/QrDisplay.tsx`
**Remove:**
```typescript
console.log('🎨 QrDisplay generating QR for:', value);
console.log(' - Type:', value instanceof Uint8Array ? 'Uint8Array' : typeof value);
console.log(' - Length:', value.length);
console.log(' - Hex:', Array.from(value).map(b => b.toString(16).padStart(2, '0')).join(''));
```
**Why:** Prevents seeds from being recoverable via browser history, crash dumps, or remote debugging.
---
## PATCH 5: Secure Clipboard Access
**File:** `src/App.tsx`
**Replace `copyToClipboard` function:**
```typescript
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('');
// Mark when copy started
const copyStartTime = Date.now();
try {
await navigator.clipboard.writeText(textToCopy);
setCopied(true);
// Add warning for sensitive data
if (fieldName.toLowerCase().includes('mnemonic') ||
fieldName.toLowerCase().includes('seed')) {
setClipboardEvents(prev => [
{
timestamp: new Date(),
field: fieldName,
length: textToCopy.length,
willClearIn: 10
},
...prev.slice(0, 9)
]);
// Auto-clear clipboard after 10 seconds
setTimeout(async () => {
try {
// Write garbage to obscure previous content (best effort)
const garbage = crypto.getRandomValues(new Uint8Array(textToCopy.length))
.reduce((s, b) => s + String.fromCharCode(b), '');
await navigator.clipboard.writeText(garbage);
} catch {}
}, 10000);
// Show warning
alert(`⚠️ ${fieldName} copied to clipboard!\n\n✅ Will auto-clear in 10 seconds.\n\n🔒 Recommend: Use QR codes instead for maximum security.`);
}
// Always clear the UI state after a moment
window.setTimeout(() => setCopied(false), 1500);
} catch (err) {
// Fallback for browsers that don't support clipboard API
const ta = document.createElement("textarea");
ta.value = textToCopy;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
}
};
```
**Update the textarea field:**
```typescript
// When copying mnemonic:
onClick={() => copyToClipboard(mnemonic, 'BIP39 Mnemonic (⚠️ Sensitive)')}
```
**Why:** Automatically clears clipboard content and warns users about clipboard exposure.
---
## PATCH 6: Comprehensive Network Blocking
**File:** `src/App.tsx`
**Replace `handleToggleNetwork` function:**
```typescript
const handleToggleNetwork = () => {
setIsNetworkBlocked(!isNetworkBlocked);
const blockAllNetworks = () => {
console.log('🚫 Network BLOCKED - All external requests disabled');
// 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.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
if (navigator.sendBeacon) {
(navigator as any).sendBeacon = () => {
console.error('Network blocked: sendBeacon not allowed');
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 image [${value}]`);
}
originalSrcSetter?.call(this, value);
},
get: Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src')?.get
});
return img;
}
}) as any;
// 6. Block Service Workers
if (navigator.serviceWorker) {
(navigator.serviceWorker as any).register = async () => {
throw new Error('Network blocked: Service Workers disabled');
};
}
};
const unblockAllNetworks = () => {
console.log('🌐 Network ACTIVE - All requests allowed');
// 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;
};
if (!isNetworkBlocked) {
blockAllNetworks();
} else {
unblockAllNetworks();
}
};
```
**Why:** Comprehensively blocks all network APIs, not just fetch(), preventing seed exfiltration.
---
## PATCH 7: Validate PGP Keys
**File:** `src/lib/seedpgp.ts`
**Add new function:**
```typescript
export async function validatePGPKey(armoredKey: string): Promise<{
valid: boolean;
error?: string;
fingerprint?: string;
keySize?: number;
expirationDate?: Date;
}> {
try {
const key = await openpgp.readKey({ armoredKey });
// 1. Verify encryption capability
try {
await key.getEncryptionKey();
} catch {
return { valid: false, error: "Key has no encryption subkey" };
}
// 2. Check key expiration
const expirationTime = await key.getExpirationTime();
if (expirationTime && expirationTime < new Date()) {
return { valid: false, error: "Key has expired" };
}
// 3. Check key strength (try to extract key size)
let keySize = 0;
try {
const mainKey = key.primaryKey as any;
if (mainKey.getBitSize) {
keySize = mainKey.getBitSize();
}
if (keySize > 0 && keySize < 2048) {
return { valid: false, error: `Key too small (${keySize} bits). Minimum 2048.` };
}
} catch (e) {
// Unable to determine key size, but continue
}
// 4. Verify primary key can encrypt
const result = await key.verifyPrimaryKey();
if (result.status !== 'valid') {
return { valid: false, error: "Key has invalid signature" };
}
return {
valid: true,
fingerprint: key.getFingerprint().toUpperCase(),
keySize: keySize || undefined,
expirationDate: expirationTime || undefined
};
} catch (e) {
return { valid: false, error: `Failed to parse key: ${e instanceof Error ? e.message : 'Unknown error'}` };
}
}
```
**Use before encrypting:**
```typescript
if (publicKeyInput) {
const validation = await validatePGPKey(publicKeyInput);
if (!validation.valid) {
setError(validation.error);
return;
}
// Show fingerprint to user for verification
setRecipientFpr(validation.fingerprint || '');
}
```
**Why:** Ensures only valid, strong PGP keys are used for encryption.
---
## PATCH 8: Add Key Rotation
**File:** `src/lib/sessionCrypto.ts`
**Add rotation logic:**
```typescript
let sessionKey: CryptoKey | null = null;
let keyCreatedAt = 0;
const KEY_ROTATION_INTERVAL = 5 * 60 * 1000; // 5 minutes
const MAX_KEY_OPERATIONS = 1000; // Rotate after N operations
let keyOperationCount = 0;
export async function getSessionKey(): Promise<CryptoKey> {
const now = Date.now();
const shouldRotate =
!sessionKey ||
(now - keyCreatedAt) > KEY_ROTATION_INTERVAL ||
keyOperationCount > MAX_KEY_OPERATIONS;
if (shouldRotate) {
if (sessionKey) {
// Log key rotation (no sensitive data)
console.debug(`Rotating session key (age: ${now - keyCreatedAt}ms, ops: ${keyOperationCount})`);
destroySessionKey();
}
const key = await window.crypto.subtle.generateKey(
{
name: KEY_ALGORITHM,
length: KEY_LENGTH,
},
false,
['encrypt', 'decrypt'],
);
sessionKey = key;
keyCreatedAt = now;
keyOperationCount = 0;
}
return sessionKey;
}
export async function encryptJsonToBlob<T>(data: T): Promise<EncryptedBlob> {
keyOperationCount++;
// ... rest of function
}
// Auto-clear on visibility change
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.debug('Page hidden - clearing session key');
destroySessionKey();
}
});
```
**Why:** Limits the time and operations a single session key is used, reducing risk of key compromise.
---
## Deployment Checklist
- [ ] Add CSP meta tag to index.html
- [ ] Encrypt all sensitive strings in state (use EncryptedBlob)
- [ ] Implement BIP39 checksum validation with await
- [ ] Disable console.log/error/warn in production
- [ ] Update copyToClipboard to auto-clear and warn
- [ ] Implement comprehensive network blocking
- [ ] Add PGP key validation
- [ ] Add session key rotation
- [ ] Run full test suite
- [ ] Test in offline mode (Tails OS)
- [ ] Test with hardware wallets (Krux, Coldcard)
- [ ] Security review of all changes
- [ ] Deploy to staging
- [ ] Final audit
- [ ] Deploy to production
---
**Note:** These patches should be reviewed and tested thoroughly before production deployment. Consider having security auditor review changes before release.