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,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 };
}