diff --git a/TODO.md b/TODO.md index 6c47bd86..b622a4e9 100644 --- a/TODO.md +++ b/TODO.md @@ -127,7 +127,22 @@ everything still open, in the order it should be tackled. ### Blockers before real secrets -- [ ] **Independent security review of the TypeScript branch.** No one but the +- [ ] **Independent security review of the TypeScript branch.** Review + strategy (decided 2026-08-18): two AI families plus vectors, because + they fail differently. (1) `/code-review ultra` — **blocked**: PR #989 + spans main→port including beta's 150 commits (6795 files/720k lines), + over ultra's size limit; fallback options are a review-only PR + containing just the `js/` tree against main, or a narrower local-diff + target. (2) A GPT-family pass (Codex) over the four ranked targets in + the PR body, briefed to REFUTE the security claims in file comments, + not summarize them. (3) Third-party ground truth where possible: the + official BIP-85 spec vectors now run directly against the TS core + (test/bip85SpecVectors.test.ts; Python has had them all along), which + closes the both-implementations-agree-and-are-both-wrong hole for the + root derivation. Triage rule for findings: flagged by both families = + almost certainly real; flagged by one = verify against code, arbiter + is a reproduction test or the cross-impl suite, never the author's + opinion alone. No one but the authoring agent has read this code. Two earlier review rounds on this project each found criticals that self-review missed, so a green suite and an author's sign-off are not evidence of much. Scope it to diff --git a/js/packages/core/src/derive/bip85.ts b/js/packages/core/src/derive/bip85.ts index d7e09643..a9a99cc9 100644 --- a/js/packages/core/src/derive/bip85.ts +++ b/js/packages/core/src/derive/bip85.ts @@ -102,14 +102,25 @@ export interface DeriveEntropyOptions { export class Bip85 { private readonly root: HDKey; - constructor(seed: Uint8Array) { - this.root = HDKey.fromMasterSeed(seed); + constructor(seed: Uint8Array | HDKey) { + this.root = seed instanceof HDKey ? seed : HDKey.fromMasterSeed(seed); } static fromMnemonic(mnemonic: string, passphrase = ""): Bip85 { return new Bip85(mnemonicToSeedSync(canonicalizeMnemonic(mnemonic), passphrase)); } + /** + * From a BIP-32 extended private key. Exists so the official BIP-85 spec + * test vectors — which start from an xprv, not a mnemonic — can be run + * directly against this implementation (third-party ground truth, immune + * to the failure mode where both of our implementations agree with each + * other and are both wrong). + */ + static fromExtendedKey(xprv: string): Bip85 { + return new Bip85(HDKey.fromExtendedKey(xprv)); + } + deriveEntropy(opts: DeriveEntropyOptions): Uint8Array { const appNo = opts.appNo ?? 39; let path: string; diff --git a/js/packages/core/test/bip85SpecVectors.test.ts b/js/packages/core/test/bip85SpecVectors.test.ts new file mode 100644 index 00000000..ffa3c840 --- /dev/null +++ b/js/packages/core/test/bip85SpecVectors.test.ts @@ -0,0 +1,50 @@ +/** + * BIP-85 official spec test vectors, run directly against this implementation. + * + * Every other derivation check in this repo compares us against the Python + * implementation — which is cross-verification, not ground truth. These + * vectors come from the BIP-85 specification itself (also pinned in Python's + * test_bip85_vectors.py), so a shared misconception between our two + * implementations cannot pass here. + */ + +import { describe, expect, it } from "vitest"; +import { Bip85, deriveTotpSecret, bytesToHex } from "@seedpass/core"; + +// The spec's master key (BIP-85 test vectors section). +const MASTER_XPRV = + "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"; + +// Official BIP-39 application vectors (12 and 24 English words, index 0). +const SPEC_12 = + "girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose"; +const SPEC_24 = + "puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano"; + +// SeedPass-specific derivations from the same master, pinned identically in +// Python's suite (not spec vectors, but a cross-family anchor for app 32 and +// the TOTP path). +const PY_SSH_ENTROPY = + "52405cd0dd21c5be78314a7c1a3c65ffd8d896536cc7dee3157db5824f0c92e2"; +const PY_TOTP_SECRET = "VQYTWDNEWYBY2G3LOGGCEKR4LZ3LNEYY"; + +describe("BIP-85 spec vectors", () => { + const bip85 = Bip85.fromExtendedKey(MASTER_XPRV); + + it("derives the spec's 12-word BIP-39 vector", () => { + expect(bip85.deriveMnemonic(0, 12)).toBe(SPEC_12); + }); + + it("derives the spec's 24-word BIP-39 vector", () => { + expect(bip85.deriveMnemonic(0, 24)).toBe(SPEC_24); + }); + + it("matches Python's pinned app-32 entropy from the spec master", () => { + const entropy = bip85.deriveEntropy({ index: 0, entropyBytes: 32, appNo: 32 }); + expect(bytesToHex(entropy)).toBe(PY_SSH_ENTROPY); + }); + + it("matches Python's pinned TOTP secret derived from the spec's 24-word child", () => { + expect(deriveTotpSecret(SPEC_24, 0)).toBe(PY_TOTP_SECRET); + }); +});