import { describe, it, expect } from "vitest"; import { WORD_LIST } from "./recovery-word-list"; import { rootKeyToWords, wordsToRootKeyMaterial } from "./recovery-words"; /** * Recovery words are only a human-readable encoding of the root key's first 16 * bytes, so browser and Android MUST agree on the word list byte for byte. * They did not: Android shipped a list carrying a duplicate "abandon" at index * 1, which made it this list shifted by one from index 1 onward. The same * vault therefore printed different words on each platform, and a phrase * written down on the phone restored nothing here. * * The vector below is pinned identically in Android's * `CrossPlatformRecoveryWordsTest`. If either side changes its list, one of * these two tests fails instead of a user discovering it at restore time. */ describe("recovery words — cross-platform word list", () => { const VECTOR_ROOT_KEY = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; const VECTOR_WORDS = [ "abandon", "math", "mimic", "master", "filter", "design", "carbon", "crystal", "rookie", "group", "knife", "young", ]; function hexToBytes(hex: string): Uint8Array { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) { out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); } return out; } it("is the canonical 2048-word list with no duplicates", () => { expect(WORD_LIST.length).toBe(2048); expect(new Set(WORD_LIST).size).toBe(2048); expect(WORD_LIST[0]).toBe("abandon"); expect(WORD_LIST[1]).toBe("ability"); expect(WORD_LIST[2047]).toBe("zoo"); }); it("produces the shared vector Android pins", async () => { expect(await rootKeyToWords(hexToBytes(VECTOR_ROOT_KEY))).toEqual(VECTOR_WORDS); }); it("round-trips every reachable 11-bit index", () => { // The Android defect lived exactly here: index 1 encoded to "abandon" and // decoded back to index 0. for (let index = 0; index < 2048; index++) { expect(WORD_LIST.indexOf(WORD_LIST[index]!)).toBe(index); } }); it("round-trips random root keys without exception", async () => { for (let i = 0; i < 500; i++) { const key = crypto.getRandomValues(new Uint8Array(32)); const material = await wordsToRootKeyMaterial(await rootKeyToWords(key)); expect(material.slice(0, 16)).toEqual(key.slice(0, 16)); } }); });