Tyler Gillispie 8b30b17dbe
feat(arch): implement Phase 2 base vertical slices and shared UI (#57)
- Scaffold shared UI fragments and styles in src/shared/ui/
- Implement full Auth vertical slice in src/features/auth/ with WebAuthn ceremonies and recovery endpoints
- Implement full Admin vertical slice in src/features/admin/ with responsive tables and mobile decks
- Add public client-side JS utilities and pure JS BIP-39 module
- Mount routes in src/main.ts and keep legacy server/ and ui/ quarantined
- Add pure JSX and API tests for Auth and Admin slices
2026-08-27 19:29:08 -07:00

72 lines
2.1 KiB
JavaScript

import { WORDLIST } from "./bip39_wordlist.js";
export async function entropyToMnemonic(entropy) {
if (entropy.length < 16 || entropy.length > 32 || entropy.length % 4 !== 0) {
throw new Error("Invalid entropy length");
}
const entropyBits = Array.from(entropy)
.map((b) => b.toString(2).padStart(8, "0"))
.join("");
const entropyBuffer = new Uint8Array(entropy.length);
entropyBuffer.set(entropy);
const hashBuffer = await crypto.subtle.digest("SHA-256", entropyBuffer);
const hashBits = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(2).padStart(8, "0"))
.join("");
const checksumLength = entropy.length / 4;
const checksum = hashBits.slice(0, checksumLength);
const bits = entropyBits + checksum;
const chunks = bits.match(/(.{1,11})/g) || [];
const mnemonic = chunks.map((binaryStr) => {
const index = parseInt(binaryStr, 2);
return WORDLIST[index];
});
return mnemonic.join(" ");
}
export async function mnemonicToEntropy(mnemonic) {
const words = mnemonic.normalize("NFKD").trim().split(/\s+/);
if (words.length % 3 !== 0) {
throw new Error("Invalid mnemonic length");
}
const bits = words
.map((word) => {
const index = WORDLIST.indexOf(word);
if (index === -1) {
throw new Error(`Invalid word in mnemonic: ${word}`);
}
return index.toString(2).padStart(11, "0");
})
.join("");
const dividerIndex = Math.floor(bits.length / 33) * 32;
const entropyBits = bits.slice(0, dividerIndex);
const checksumBits = bits.slice(dividerIndex);
const entropy = new Uint8Array(entropyBits.length / 8);
for (let i = 0; i < entropy.length; i++) {
entropy[i] = parseInt(entropyBits.slice(i * 8, (i + 1) * 8), 2);
}
const hashBuffer = await crypto.subtle.digest("SHA-256", entropy);
const hashBits = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(2).padStart(8, "0"))
.join("");
const expectedChecksum = hashBits.slice(0, checksumBits.length);
if (expectedChecksum !== checksumBits) {
// Explicitly zeroize on failure
entropy.fill(0);
throw new Error("Invalid mnemonic checksum");
}
return entropy;
}