recovery-words.cross-platform.test.ts

2.4 KB · raw

1import { describe, it, expect } from "vitest";23import { WORD_LIST } from "./recovery-word-list";4import { rootKeyToWords, wordsToRootKeyMaterial } from "./recovery-words";56/**7 * Recovery words are only a human-readable encoding of the root key's first 168 * bytes, so browser and Android MUST agree on the word list byte for byte.9 * They did not: Android shipped a list carrying a duplicate "abandon" at index10 * 1, which made it this list shifted by one from index 1 onward. The same11 * vault therefore printed different words on each platform, and a phrase12 * written down on the phone restored nothing here.13 *14 * The vector below is pinned identically in Android's15 * `CrossPlatformRecoveryWordsTest`. If either side changes its list, one of16 * these two tests fails instead of a user discovering it at restore time.17 */18describe("recovery words — cross-platform word list", () => {19  const VECTOR_ROOT_KEY =20    "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff";2122  const VECTOR_WORDS = [23    "abandon", "math", "mimic", "master", "filter", "design",24    "carbon", "crystal", "rookie", "group", "knife", "young",25  ];2627  function hexToBytes(hex: string): Uint8Array {28    const out = new Uint8Array(hex.length / 2);29    for (let i = 0; i < out.length; i++) {30      out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);31    }32    return out;33  }3435  it("is the canonical 2048-word list with no duplicates", () => {36    expect(WORD_LIST.length).toBe(2048);37    expect(new Set(WORD_LIST).size).toBe(2048);38    expect(WORD_LIST[0]).toBe("abandon");39    expect(WORD_LIST[1]).toBe("ability");40    expect(WORD_LIST[2047]).toBe("zoo");41  });4243  it("produces the shared vector Android pins", async () => {44    expect(await rootKeyToWords(hexToBytes(VECTOR_ROOT_KEY))).toEqual(VECTOR_WORDS);45  });4647  it("round-trips every reachable 11-bit index", () => {48    // The Android defect lived exactly here: index 1 encoded to "abandon" and49    // decoded back to index 0.50    for (let index = 0; index < 2048; index++) {51      expect(WORD_LIST.indexOf(WORD_LIST[index]!)).toBe(index);52    }53  });5455  it("round-trips random root keys without exception", async () => {56    for (let i = 0; i < 500; i++) {57      const key = crypto.getRandomValues(new Uint8Array(32));58      const material = await wordsToRootKeyMaterial(await rootKeyToWords(key));59      expect(material.slice(0, 16)).toEqual(key.slice(0, 16));60    }61  });62});