root-derivation-gate.test.ts

2.5 KB · raw

1// Campaign task 2.4 — no shipping source may derive device key material from the2// vault root.3//4// Root→device derivation survives only in the test-only owners5// (src/shared/legacy-identity-derivation.test-support.ts and Android6// app/src/test/.../LegacyIdentityDerivation.kt). Device identities are random7// per installation, so a new production reference is a regression rather than a8// style choice — and after the campaign dropped vault-root rotation, this gate9// is a load-bearing compensating control (see Amendment A-1, §15.3).1011import { existsSync, readdirSync, readFileSync } from "node:fs";12import { dirname, extname, resolve } from "node:path";13import { fileURLToPath } from "node:url";14import { describe, expect, it } from "vitest";1516const browserRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");17const androidRoot = resolve(browserRoot, "../peach-android");1819const LEGACY_DERIVATION =20  /\b(?:deriveSyncIdentityKeypair|deriveSyncSigningKeypair|deriveLocalSyncKeypair)\b/;2122const TEST_SUFFIXES = [".test.ts", ".test.tsx", ".test-support.ts"];2324function sourceFiles(root: string, extensions: string[]): string[] {25  const found: string[] = [];26  for (const entry of readdirSync(root, { withFileTypes: true })) {27    const path = resolve(root, entry.name);28    if (entry.isDirectory()) found.push(...sourceFiles(path, extensions));29    else if (extensions.includes(extname(entry.name))) found.push(path);30  }31  return found;32}3334function offenders(root: string, extensions: string[], isTest: (path: string) => boolean): string[] {35  return sourceFiles(root, extensions)36    .filter((path) => !isTest(path))37    .filter((path) => LEGACY_DERIVATION.test(readFileSync(path, "utf8")))38    .map((path) => path.replace(`${browserRoot}/`, "").replace(`${androidRoot}/`, ""));39}4041describe("root→device key derivation stays out of shipping source", () => {42  it("browser production sources are clean", () => {43    const found = offenders(44      resolve(browserRoot, "src"),45      [".ts", ".tsx"],46      (path) => TEST_SUFFIXES.some((suffix) => path.endsWith(suffix)),47    );48    expect(found).toEqual([]);49  });5051  it("android main sources are clean", () => {52    // The sibling worktree is required by the cross-client gate too; missing it53    // must fail loudly rather than silently skip a security check.54    expect(existsSync(resolve(androidRoot, "app/src/main"))).toBe(true);55    const found = offenders(resolve(androidRoot, "app/src/main"), [".kt"], () => false);56    expect(found).toEqual([]);57  });58});