/** * T6 — HKDF session-key derivation with DIRECTION-SPLIT subkeys. * * CRITICAL (finding #1): one shared AEAD key across both directions + a deterministic seq-nonce * ⇒ client-seq=N and host-seq=N reuse an identical (key, nonce) ⇒ GCM catastrophe, and lets a * relay reflect a c2h frame back as h2c. Fix: derive the §4.4 master, then HKDF-Expand it into two * disjoint subkeys under the FROZEN labels so client-write/host-read share one key and * host-write/client-read share the OTHER, never the same. */ import { hkdf } from '@noble/hashes/hkdf' import { sha256 } from '@noble/hashes/sha2' import type { AeadAlg, DirectionalKeys } from 'relay-contracts' import { HKDF_INFO, HKDF_INFO_C2H, HKDF_INFO_H2C, AEAD_KEY_BYTES } from 'relay-contracts' import { concatBytes } from 'relay-contracts' import { wrapAeadKey } from './aead-key.js' const encoder = new TextEncoder() const EMPTY = new Uint8Array(0) /** §4.4 master secret: HKDF-SHA256(ikm=sharedSecret, salt=clientNonce‖hostNonce, info=HKDF_INFO). */ export function deriveMaster( sharedSecret: Uint8Array, clientNonce: Uint8Array, hostNonce: Uint8Array, ): Uint8Array { const salt = concatBytes([clientNonce, hostNonce]) return hkdf(sha256, sharedSecret, salt, encoder.encode(HKDF_INFO), AEAD_KEY_BYTES) } /** Expand the master into a single direction subkey under the given frozen label. */ function expandSubkey(master: Uint8Array, label: string): Uint8Array { return hkdf(sha256, master, EMPTY, encoder.encode(label), AEAD_KEY_BYTES) } /** * Derive the direction-split `DirectionalKeys` (c2h / h2c) for a session. `salt` is BOTH nonces in * fixed client‖host order, so swapping the nonce roles yields different keys; the two subkeys are * disjoint by construction (distinct frozen labels). */ export async function deriveSessionKeys( sharedSecret: Uint8Array, clientNonce: Uint8Array, hostNonce: Uint8Array, alg: AeadAlg, ): Promise { const master = deriveMaster(sharedSecret, clientNonce, hostNonce) const c2h = expandSubkey(master, HKDF_INFO_C2H) const h2c = expandSubkey(master, HKDF_INFO_H2C) return { c2h: wrapAeadKey(c2h, alg, 'c2h'), h2c: wrapAeadKey(h2c, alg, 'h2c'), } }