auth-yes/ui/utils/bip39.ts
google-labs-jules[bot] f63d0c9afe feat: implement 2-of-3 SSS recovery matrix using Wasm/Rust
- Scaffolds a new Rust crate `wasm/sss_recovery` for constant-time Shamir's Secret Sharing over GF(256) with strict Wasm `zeroize`
- Implements purely typed BIP-39 fallback mapped via Deno WebCrypto in `ui/utils/bip39.ts`
- Migrates `server/recovery.ts` logic mapping Device/Voucher + Server shares with Valkey rate-limiting
- Applies mandatory in-memory JS zeroization on all reconstructed buffers

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-24 07:49:34 +00:00

72 lines
2.2 KiB
TypeScript

import { WORDLIST } from "./bip39_wordlist.ts";
export async function entropyToMnemonic(entropy: Uint8Array): Promise<string> {
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: string): Promise<Uint8Array> {
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;
}