- 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>
72 lines
2.2 KiB
TypeScript
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;
|
|
}
|