legacy-identity-derivation.test-support.ts

3.1 KB · raw

1// Root→device key derivation, retained for **tests only** — campaign task 2.4.2//3// Production no longer derives device key material from the vault root: device4// identities are random per installation (src/background/device-identity.ts)5// and route capabilities come from the DH mesh derivation6// (deriveMeshRendezvousSecret). No shipping module under src/ may import this7// file; fixtures use it purely to build deterministic keypairs.8//9// The implementations are byte-identical to the ones removed from10// src/shared/sync-identity.ts so pinned manifest vectors do not drift.1112import { HKDF } from "@stablelib/hkdf";13import { SHA256 } from "@stablelib/sha256";14import { generateKeyPairFromSeed } from "@stablelib/x25519";15import type { SyncIdentityKeypair } from "./sync-identity";1617const LEGACY_SYNC_IDENTITY_SALT = new TextEncoder().encode("peach-sync-identity-v1");18const LEGACY_SYNC_SIGNING_SALT = new TextEncoder().encode("peach-sync-signing-v1");1920// PKCS#8 prefix for a raw 32-byte Ed25519 seed (RFC 8410 §7).21//   SEQUENCE(46) { INTEGER 0, AlgorithmIdentifier(id-Ed25519),22//                  OCTET STRING(34) { OCTET STRING(32) <seed> } }23const ED25519_PKCS8_PREFIX = Uint8Array.of(24  0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,25  0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,26);2728export function deriveSyncIdentityKeypair(29  masterKey: Uint8Array,30  deviceId: string,31): SyncIdentityKeypair {32  if (masterKey.length === 0) throw new Error("masterKey must not be empty");33  if (!deviceId) throw new Error("deviceId must not be empty");34  const info = new TextEncoder().encode(deviceId);35  const seed = new HKDF(SHA256, masterKey, LEGACY_SYNC_IDENTITY_SALT, info).expand(32);36  const kp = generateKeyPairFromSeed(seed);37  return { publicKey: kp.publicKey, secretKey: kp.secretKey };38}3940export async function deriveSyncSigningKeypair(41  masterKey: Uint8Array,42  deviceId: string,43): Promise<{ privateKey: CryptoKey; publicKeyRaw: Uint8Array }> {44  if (masterKey.length === 0) throw new Error("masterKey must not be empty");45  if (!deviceId) throw new Error("deviceId must not be empty");46  const info = new TextEncoder().encode(deviceId);47  const seed = new HKDF(SHA256, masterKey, LEGACY_SYNC_SIGNING_SALT, info).expand(32);4849  const pkcs8 = new Uint8Array(ED25519_PKCS8_PREFIX.length + 32);50  pkcs8.set(ED25519_PKCS8_PREFIX, 0);51  pkcs8.set(seed, ED25519_PKCS8_PREFIX.length);5253  const privateKey = await crypto.subtle.importKey(54    "pkcs8",55    pkcs8 as Uint8Array<ArrayBuffer>,56    { name: "Ed25519" },57    true,58    ["sign"],59  );60  const jwk = (await crypto.subtle.exportKey("jwk", privateKey)) as JsonWebKey & { x?: string };61  if (!jwk.x) throw new Error("Ed25519 JWK missing 'x' component");62  return { privateKey, publicKeyRaw: base64UrlToBytes(jwk.x) };63}6465function base64UrlToBytes(b64url: string): Uint8Array {66  const pad = (4 - (b64url.length % 4)) % 4;67  const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(pad);68  const bin = atob(b64);69  const out = new Uint8Array(bin.length);70  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);71  return out;72}