mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
Implement security patches: CSP headers, console disabling, key rotation, clipboard security, network blocking, log cleanup, and PGP validation
This commit is contained in:
166
src/App.tsx
166
src/App.tsx
@@ -1,26 +1,11 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
QrCode,
|
||||
RefreshCw,
|
||||
CheckCircle2,
|
||||
Lock,
|
||||
AlertCircle,
|
||||
Camera,
|
||||
Dices,
|
||||
Mic,
|
||||
Unlock,
|
||||
EyeOff,
|
||||
FileKey,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { QrCode, RefreshCw, CheckCircle2, Lock, AlertCircle, Camera, Dices, Mic, Unlock, EyeOff, FileKey, Info } from 'lucide-react';
|
||||
import { PgpKeyInput } from './components/PgpKeyInput';
|
||||
import { useRef } from 'react';
|
||||
import { QrDisplay } from './components/QrDisplay';
|
||||
import QRScanner from './components/QRScanner';
|
||||
import { validateBip39Mnemonic } from './lib/bip39';
|
||||
import { buildPlaintext, encryptToSeed, decryptFromSeed, detectEncryptionMode } from './lib/seedpgp';
|
||||
import { buildPlaintext, encryptToSeed, decryptFromSeed, detectEncryptionMode, validatePGPKey } from './lib/seedpgp';
|
||||
import { encodeStandardSeedQR, encodeCompactSeedQREntropy } from './lib/seedqr';
|
||||
import * as openpgp from 'openpgp';
|
||||
import { SecurityWarnings } from './components/SecurityWarnings';
|
||||
import { getSessionKey, encryptJsonToBlob, destroySessionKey, EncryptedBlob } from './lib/sessionCrypto';
|
||||
import { EncryptionMode, EncryptionResult } from './lib/types'; // Import EncryptionMode and EncryptionResult
|
||||
@@ -34,7 +19,6 @@ import DiceEntropy from './components/DiceEntropy';
|
||||
import { InteractionEntropy } from './lib/interactionEntropy';
|
||||
|
||||
import AudioEntropy from './AudioEntropy';
|
||||
console.log("OpenPGP.js version:", openpgp.config.versionString);
|
||||
|
||||
interface StorageItem {
|
||||
key: string;
|
||||
@@ -241,7 +225,7 @@ function App() {
|
||||
};
|
||||
|
||||
|
||||
const copyToClipboard = async (text: string | Uint8Array) => {
|
||||
const copyToClipboard = async (text: string | Uint8Array, fieldName = 'Data') => {
|
||||
if (isReadOnly) {
|
||||
setError("Copy to clipboard is disabled in Read-only mode.");
|
||||
return;
|
||||
@@ -252,6 +236,36 @@ function App() {
|
||||
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");
|
||||
@@ -297,7 +311,7 @@ function App() {
|
||||
setRecipientFpr('');
|
||||
|
||||
try {
|
||||
const validation = validateBip39Mnemonic(mnemonic);
|
||||
const validation = await validateBip39Mnemonic(mnemonic);
|
||||
if (!validation.valid) {
|
||||
throw new Error(validation.error);
|
||||
}
|
||||
@@ -308,21 +322,21 @@ function App() {
|
||||
if (encryptionMode === 'seedqr') {
|
||||
if (seedQrFormat === 'standard') {
|
||||
const qrString = await encodeStandardSeedQR(mnemonic);
|
||||
console.log('📋 Standard SeedQR generated:', qrString.slice(0, 50) + '...');
|
||||
result = { framed: qrString };
|
||||
} else { // compact
|
||||
const qrEntropy = await encodeCompactSeedQREntropy(mnemonic);
|
||||
|
||||
console.log('🔐 Compact SeedQR generated:');
|
||||
console.log(' - Type:', qrEntropy instanceof Uint8Array ? 'Uint8Array' : typeof qrEntropy);
|
||||
console.log(' - Length:', qrEntropy.length);
|
||||
console.log(' - Hex:', Array.from(qrEntropy).map(b => b.toString(16).padStart(2, '0')).join(''));
|
||||
console.log(' - First 16 bytes:', Array.from(qrEntropy.slice(0, 16)));
|
||||
|
||||
result = { framed: qrEntropy }; // framed will hold the Uint8Array
|
||||
}
|
||||
} else {
|
||||
// Existing PGP and Krux encryption
|
||||
// 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,
|
||||
@@ -467,25 +481,88 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
(navigator.serviceWorker as any).register = async () => {
|
||||
throw new Error('Network blocked: Service Workers disabled');
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
// Block network
|
||||
console.log('🚫 Network BLOCKED - No external requests allowed');
|
||||
// Optional: Override fetch/XMLHttpRequest
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).__originalFetch = window.fetch;
|
||||
// Create a mock fetch function with proper type assertion
|
||||
const mockFetch = (async () => Promise.reject(new Error('Network blocked by user'))) as unknown as typeof window.fetch;
|
||||
window.fetch = mockFetch;
|
||||
}
|
||||
blockAllNetworks();
|
||||
} else {
|
||||
// Unblock network
|
||||
console.log('🌐 Network ACTIVE');
|
||||
if ((window as any).__originalFetch) {
|
||||
window.fetch = (window as any).__originalFetch;
|
||||
}
|
||||
unblockAllNetworks();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -526,7 +603,6 @@ function App() {
|
||||
|
||||
// Go to Create tab (fresh start)
|
||||
setActiveTab('create');
|
||||
console.log('✅ All data reset');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user