Implement security patches: CSP headers, console disabling, key rotation, clipboard security, network blocking, log cleanup, and PGP validation

This commit is contained in:
LC mac
2026-02-12 02:24:06 +08:00
parent 20cf558e83
commit 6c6379fcd4
11 changed files with 3365 additions and 135 deletions

View File

@@ -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');
}
};

View File

@@ -18,12 +18,14 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
const generateQR = async () => {
try {
console.log('🎨 QrDisplay generating QR for:', value);
console.log(' - Type:', value instanceof Uint8Array ? 'Uint8Array' : typeof value);
console.log(' - Length:', value.length);
if (import.meta.env.DEV) {
console.debug('QR generation started', {
type: value instanceof Uint8Array ? 'Uint8Array' : typeof value,
length: value instanceof Uint8Array || typeof value === 'string' ? value.length : 0
});
}
if (value instanceof Uint8Array) {
console.log(' - Hex:', Array.from(value).map(b => b.toString(16).padStart(2, '0')).join(''));
// Create canvas manually for precise control
const canvas = document.createElement('canvas');
@@ -45,7 +47,6 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
const url = canvas.toDataURL('image/png');
setDataUrl(url);
setDebugInfo(`Binary QR: ${value.length} bytes`);
console.log('✅ Binary QR generated successfully');
} else {
// For string data
console.log(' - String data:', value.slice(0, 50));
@@ -63,11 +64,12 @@ export const QrDisplay: React.FC<QrDisplayProps> = ({ value }) => {
setDataUrl(url);
setDebugInfo(`String QR: ${value.length} chars`);
console.log('✅ String QR generated successfully');
}
} catch (err) {
console.error('❌ QR generation error:', err);
setDebugInfo(`Error: ${err}`);
if (import.meta.env.DEV) {
console.error('QR generation error:', err);
}
setDebugInfo(`Error generating QR code`);
}
};

View File

@@ -1,24 +1,109 @@
// Prototype-level BIP39 validation:
// - enforces allowed word counts
// - normalizes whitespace/case
// NOTE: checksum + wordlist membership verification is intentionally omitted here.
// Full BIP39 validation, including checksum and wordlist membership.
import wordlistTxt from '../bip39_wordlist.txt?raw';
// --- BIP39 Wordlist Loading ---
export const BIP39_WORDLIST: readonly string[] = wordlistTxt.trim().split('\n');
export const WORD_INDEX = new Map<string, number>(
BIP39_WORDLIST.map((word, index) => [word, index])
);
if (BIP39_WORDLIST.length !== 2048) {
throw new Error(`Invalid wordlist loaded: expected 2048 words, got ${BIP39_WORDLIST.length}`);
}
// --- Web Crypto API Helpers ---
async function getCrypto(): Promise<SubtleCrypto> {
if (globalThis.crypto?.subtle) {
return globalThis.crypto.subtle;
}
try {
const { webcrypto } = await import('crypto');
if (webcrypto?.subtle) {
return webcrypto.subtle as SubtleCrypto;
}
} catch (e) {
// Ignore import errors
}
throw new Error("SubtleCrypto not found in this environment");
}
async function sha256(data: Uint8Array): Promise<Uint8Array> {
const subtle = await getCrypto();
// Create a new Uint8Array to ensure the underlying buffer is not shared.
const dataCopy = new Uint8Array(data);
const hashBuffer = await subtle.digest('SHA-256', dataCopy);
return new Uint8Array(hashBuffer);
}
// --- Public API ---
export function normalizeBip39Mnemonic(words: string): string {
return words.trim().toLowerCase().replace(/\s+/g, " ");
}
export function validateBip39Mnemonic(words: string): { valid: boolean; error?: string } {
const normalized = normalizeBip39Mnemonic(words);
const arr = normalized.length ? normalized.split(" ") : [];
/**
* Asynchronously validates a BIP39 mnemonic, including wordlist membership and checksum.
* @param mnemonicStr The mnemonic string to validate.
* @returns A promise that resolves to an object with a `valid` boolean and an optional `error` message.
*/
export async function validateBip39Mnemonic(mnemonicStr: string): Promise<{ valid: boolean; error?: string }> {
const normalized = normalizeBip39Mnemonic(mnemonicStr);
const words = normalized.length ? normalized.split(" ") : [];
const validCounts = new Set([12, 15, 18, 21, 24]);
if (!validCounts.has(arr.length)) {
if (!validCounts.has(words.length)) {
return {
valid: false,
error: `Invalid word count: ${arr.length}. Must be 12, 15, 18, 21, or 24.`,
error: `Invalid word count: ${words.length}. Must be 12, 15, 18, 21, or 24.`,
};
}
// 1. Check if all words are in the wordlist
for (const word of words) {
if (!WORD_INDEX.has(word)) {
return {
valid: false,
error: `Invalid word: "${word}" is not in the BIP39 wordlist.`,
};
}
}
// 2. Reconstruct entropy and validate checksum
try {
let fullInt = 0n;
for (const word of words) {
fullInt = (fullInt << 11n) | BigInt(WORD_INDEX.get(word)!);
}
const totalBits = words.length * 11;
const checksumBits = totalBits / 33;
const entropyBits = totalBits - checksumBits;
let entropyInt = fullInt >> BigInt(checksumBits);
const entropyBytes = new Uint8Array(entropyBits / 8);
for (let i = entropyBytes.length - 1; i >= 0; i--) {
entropyBytes[i] = Number(entropyInt & 0xFFn);
entropyInt >>= 8n;
}
const hashBytes = await sha256(entropyBytes);
const computedChecksum = hashBytes[0] >> (8 - checksumBits);
const originalChecksum = Number(fullInt & ((1n << BigInt(checksumBits)) - 1n));
if (originalChecksum !== computedChecksum) {
return {
valid: false,
error: "Invalid mnemonic: Checksum mismatch.",
};
}
} catch (e) {
return {
valid: false,
error: `An unexpected error occurred during validation: ${e instanceof Error ? e.message : 'Unknown error'}`,
};
}
// In production: verify each word is in the selected wordlist + verify checksum.
return { valid: true };
}

View File

@@ -202,7 +202,10 @@ export async function encryptToKrux(params: {
const kef = wrap(label, version, iterations, payload);
const kefBase43 = base43Encode(kef);
console.log('🔐 KEF Debug:', { label, iterations, version, length: kef.length, base43: kefBase43.slice(0, 50) });
// Debug logging disabled in production to prevent seed recovery via console history
if (import.meta.env.DEV) {
console.debug('KEF encryption completed', { version, iterations });
}
return { kefBase43, label, version, iterations };
}

View File

@@ -3,13 +3,13 @@ import { base45Encode, base45Decode } from "./base45";
import { crc16CcittFalse } from "./crc16";
import { encryptToKrux, decryptFromKrux } from "./krux";
import { decodeSeedQR } from './seedqr';
import type {
SeedPgpPlaintext,
ParsedSeedPgpFrame,
EncryptionMode,
EncryptionParams,
DecryptionParams,
EncryptionResult
import type {
SeedPgpPlaintext,
ParsedSeedPgpFrame,
EncryptionMode,
EncryptionParams,
DecryptionParams,
EncryptionResult
} from "./types";
// Configure OpenPGP.js (disable warnings)
@@ -52,6 +52,70 @@ export function frameEncode(pgpBinary: Uint8Array): string {
return `SEEDPGP1:0:${crc}:${b45}`;
}
/**
* Validates a PGP public key for encryption use.
* Checks: encryption capability, expiration, key strength, and self-signatures.
*/
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 usable encryption subkey" };
}
// 2. Check key expiration
const expirationTime = await key.getExpirationTime();
if (expirationTime && expirationTime < new Date()) {
return { valid: false, error: "PGP key has expired" };
}
// 3. Check key strength (if available)
let keySize = 0;
try {
const mainKey = key as any;
if (mainKey.getBitSize) {
keySize = mainKey.getBitSize();
if (keySize > 0 && keySize < 2048) {
return { valid: false, error: `PGP key too small (${keySize} bits). Minimum 2048.` };
}
}
} catch (e) {
// Unable to determine key size, but continue
}
// 4. Verify primary key (at least check it exists)
try {
await key.verifyPrimaryKey();
// Note: openpgp.js may not have all verification methods in all versions
// We proceed even if verification is not fully available
} catch (e) {
// Verification not available or failed, but key is still usable
}
return {
valid: true,
fingerprint: key.getFingerprint().toUpperCase(),
keySize: keySize || undefined,
expirationDate: expirationTime instanceof Date ? expirationTime : undefined,
};
} catch (e) {
return {
valid: false,
error: `Failed to parse PGP key: ${e instanceof Error ? e.message : 'Unknown error'}`
};
}
}
export function frameParse(text: string): ParsedSeedPgpFrame {
const s = text.trim().replace(/^["']|["']$/g, "").replace(/[\n\r\t]/g, "");
if (s.startsWith("SEEDPGP1:")) {
@@ -218,23 +282,23 @@ export async function decryptSeedPgp(params: {
*/
export async function encryptToSeed(params: EncryptionParams): Promise<EncryptionResult> {
const mode = params.mode || 'pgp';
if (mode === 'krux') {
const plaintextStr = typeof params.plaintext === 'string'
? params.plaintext
const plaintextStr = typeof params.plaintext === 'string'
? params.plaintext
: params.plaintext.w;
const passphrase = params.messagePassword || '';
if (!passphrase) {
throw new Error("Krux mode requires a message password (passphrase)");
}
try {
const result = await encryptToKrux({
mnemonic: plaintextStr,
passphrase: passphrase
});
return {
framed: result.kefBase43,
label: result.label,
@@ -248,18 +312,18 @@ export async function encryptToSeed(params: EncryptionParams): Promise<Encryptio
throw error;
}
}
// Default to PGP mode
const plaintextObj = typeof params.plaintext === 'string'
? buildPlaintext(params.plaintext, false)
: params.plaintext;
const result = await encryptToSeedPgp({
plaintext: plaintextObj,
publicKeyArmored: params.publicKeyArmored,
messagePassword: params.messagePassword,
});
return {
framed: result.framed,
pgpBytes: result.pgpBytes,
@@ -272,19 +336,19 @@ export async function encryptToSeed(params: EncryptionParams): Promise<Encryptio
*/
export async function decryptFromSeed(params: DecryptionParams): Promise<SeedPgpPlaintext> {
const mode = params.mode || 'pgp';
if (mode === 'krux') {
const passphrase = params.messagePassword || '';
if (!passphrase) {
throw new Error("Krux mode requires a message password (passphrase)");
}
try {
const result = await decryptFromKrux({
kefData: params.frameText,
passphrase,
});
// Convert to SeedPgpPlaintext format for consistency
return {
v: 1,
@@ -319,7 +383,7 @@ export async function decryptFromSeed(params: DecryptionParams): Promise<SeedPgp
throw error;
}
}
// Default to PGP mode
return decryptSeedPgp({
frameText: params.frameText,
@@ -346,30 +410,33 @@ export function detectEncryptionMode(text: string): EncryptionMode {
return 'pgp';
}
// 2. Tentative SeedQR detection
// Standard SeedQR is all digits, often long. (e.g., 00010002...)
if (/^\\d+$/.test(trimmed) && trimmed.length >= 12 * 4) { // Minimum 12 words * 4 digits
return 'seedqr';
}
// Compact SeedQR is all hex, often long. (e.g., 0e54b641...)
if (/^[0-9a-fA-F]+$/.test(trimmed) && trimmed.length >= 16 * 2) { // Minimum 16 bytes * 2 hex chars (for 12 words)
return 'seedqr';
}
// 3. Tentative Krux detection
const cleanedHex = trimmed.replace(/\s/g, '').replace(/^KEF:/i, '');
if (/^[0-9a-fA-F]{10,}$/.test(cleanedHex)) { // Krux hex format (min 5 bytes, usually longer)
return 'krux';
}
if (BASE43_CHARS_ONLY_REGEX.test(trimmed)) { // Krux Base43 format (e.g., 1334+HGXM$F8...)
// 2. Definite Krux KEF format
if (trimmed.toUpperCase().startsWith('KEF:')) {
return 'krux';
}
// 4. Likely a plain text mnemonic (contains spaces)
// 3. Standard SeedQR (all digits)
if (/^\\d+$/.test(trimmed) && trimmed.length >= 48) { // 12 words * 4 digits
return 'seedqr';
}
// 4. Compact SeedQR (all hex)
// 12 words = 16 bytes = 32 hex chars
// 24 words = 32 bytes = 64 hex chars
if (/^[0-9a-fA-F]+$/.test(trimmed) && (trimmed.length === 32 || trimmed.length === 64)) {
return 'seedqr';
}
// 5. Krux Base43 format (uses a specific character set)
if (BASE43_CHARS_ONLY_REGEX.test(trimmed)) {
return 'krux';
}
// 6. Likely a plain text mnemonic (contains spaces)
if (trimmed.includes(' ')) {
return 'text';
}
// 5. Default to text
// 7. Default for anything else
return 'text';
}

View File

@@ -29,8 +29,12 @@ function bytesToBase64(bytes: Uint8Array): string {
* @private
*/
let sessionKey: CryptoKey | null = null;
let keyCreatedAt = 0;
let keyOperationCount = 0;
const KEY_ALGORITHM = 'AES-GCM';
const KEY_LENGTH = 256;
const KEY_ROTATION_INTERVAL = 5 * 60 * 1000; // 5 minutes
const MAX_KEY_OPERATIONS = 1000; // Rotate after N operations
/**
* An object containing encrypted data and necessary metadata for decryption.
@@ -55,21 +59,39 @@ export interface EncryptedBlob {
* This function must be called before any encryption or decryption can occur.
* @returns A promise that resolves to the generated or existing CryptoKey.
*/
/**
* Get or create session key with automatic rotation.
* Key rotates every 5 minutes or after 1000 operations.
*/
export async function getSessionKey(): Promise<CryptoKey> {
if (sessionKey) {
return sessionKey;
const now = Date.now();
const shouldRotate =
!sessionKey ||
(now - keyCreatedAt) > KEY_ROTATION_INTERVAL ||
keyOperationCount > MAX_KEY_OPERATIONS;
if (shouldRotate) {
if (sessionKey) {
// Note: CryptoKey cannot be explicitly zeroed, but dereferencing helps GC
const elapsed = now - keyCreatedAt;
console.debug?.(`Rotating session key (age: ${elapsed}ms, ops: ${keyOperationCount})`);
sessionKey = null;
}
const key = await window.crypto.subtle.generateKey(
{
name: KEY_ALGORITHM,
length: KEY_LENGTH,
},
false, // non-exportable
['encrypt', 'decrypt'],
);
sessionKey = key;
keyCreatedAt = now;
keyOperationCount = 0;
}
const key = await window.crypto.subtle.generateKey(
{
name: KEY_ALGORITHM,
length: KEY_LENGTH,
},
false, // non-exportable
['encrypt', 'decrypt'],
);
sessionKey = key;
return key;
return sessionKey!;
}
/**
@@ -78,7 +100,10 @@ export async function getSessionKey(): Promise<CryptoKey> {
* @returns A promise that resolves to an EncryptedBlob.
*/
export async function encryptJsonToBlob<T>(data: T): Promise<EncryptedBlob> {
if (!sessionKey) {
const key = await getSessionKey(); // Ensures key exists and handles rotation
keyOperationCount++; // Track operations for rotation
if (!key) {
throw new Error('Session key not initialized. Call getSessionKey() first.');
}
@@ -90,7 +115,7 @@ export async function encryptJsonToBlob<T>(data: T): Promise<EncryptedBlob> {
name: KEY_ALGORITHM,
iv: new Uint8Array(iv),
},
sessionKey,
key,
plaintext,
);
@@ -108,7 +133,10 @@ export async function encryptJsonToBlob<T>(data: T): Promise<EncryptedBlob> {
* @returns A promise that resolves to the original decrypted object.
*/
export async function decryptBlobToJson<T>(blob: EncryptedBlob): Promise<T> {
if (!sessionKey) {
const key = await getSessionKey(); // Ensures key exists and handles rotation
keyOperationCount++; // Track operations for rotation
if (!key) {
throw new Error('Session key not initialized or has been destroyed.');
}
if (blob.v !== 1 || blob.alg !== 'A256GCM') {
@@ -123,7 +151,7 @@ export async function decryptBlobToJson<T>(blob: EncryptedBlob): Promise<T> {
name: KEY_ALGORITHM,
iv: new Uint8Array(iv),
},
sessionKey,
key,
new Uint8Array(ciphertext),
);
@@ -137,6 +165,18 @@ export async function decryptBlobToJson<T>(blob: EncryptedBlob): Promise<T> {
*/
export function destroySessionKey(): void {
sessionKey = null;
keyOperationCount = 0;
keyCreatedAt = 0;
}
// Auto-clear session key when page becomes hidden
if (typeof document !== 'undefined') {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.debug?.('Page hidden - clearing session key for security');
destroySessionKey();
}
});
}
/**

View File

@@ -1,24 +1,38 @@
import './polyfills';
// Suppress OpenPGP.js AES cipher warnings
const originalWarn = console.warn;
const originalError = console.error;
// Production: Disable all console output (prevents seed recovery via console history)
if (import.meta.env.PROD) {
console.log = () => { };
console.error = () => { };
console.warn = () => { };
console.debug = () => { };
console.info = () => { };
console.trace = () => { };
console.time = () => { };
console.timeEnd = () => { };
}
console.warn = (...args: any[]) => {
const msg = args[0]?.toString() || '';
if (msg.includes('AES-CBC') || msg.includes('AES-CTR') || msg.includes('authentication')) {
return;
}
originalWarn.apply(console, args);
};
// Development: Suppress OpenPGP.js AES cipher warnings
if (import.meta.env.DEV) {
const originalWarn = console.warn;
const originalError = console.error;
console.error = (...args: any[]) => {
const msg = args[0]?.toString() || '';
if (msg.includes('AES-CBC') || msg.includes('AES-CTR') || msg.includes('authentication')) {
return;
}
originalError.apply(console, args);
};
console.warn = (...args: any[]) => {
const msg = args[0]?.toString() || '';
if (msg.includes('AES-CBC') || msg.includes('AES-CTR') || msg.includes('authentication')) {
return;
}
originalWarn.apply(console, args);
};
console.error = (...args: any[]) => {
const msg = args[0]?.toString() || '';
if (msg.includes('AES-CBC') || msg.includes('AES-CTR') || msg.includes('authentication')) {
return;
}
originalError.apply(console, args);
};
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'