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:
105
src/lib/bip39.ts
105
src/lib/bip39.ts
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user