import type { CryptoProvider } from '@shade/core'; import { ValidationError } from '@shade/core'; import { STREAM_ID_BYTES, STREAM_SECRET_BYTES } from './ids.js'; const TEXT = new TextEncoder(); const STREAM_KEY_INFO = TEXT.encode('shade-stream/v1\0master'); const LANE_KEY_INFO_PREFIX = TEXT.encode('shade-stream/v1\0lane\0'); const STREAM_KEY_BYTES = 32; export const LANE_KEY_BYTES = 32; /** * Derive the master streamKey from a streamSecret + streamId. * * streamKey = HKDF(ikm=streamSecret, salt=streamId, * info="shade-stream/v1\0master", length=32) * * The streamKey is NEVER used to encrypt chunks directly — it is a root for * per-lane key derivation (see `deriveLaneKey`). */ export async function deriveStreamKey( crypto: CryptoProvider, streamSecret: Uint8Array, streamId: Uint8Array, ): Promise { if (streamSecret.length !== STREAM_SECRET_BYTES) { throw new ValidationError( `streamSecret must be ${STREAM_SECRET_BYTES} bytes`, 'streamSecret', ); } if (streamId.length !== STREAM_ID_BYTES) { throw new ValidationError(`streamId must be ${STREAM_ID_BYTES} bytes`, 'streamId'); } return crypto.hkdf(streamSecret, streamId, STREAM_KEY_INFO, STREAM_KEY_BYTES); } /** * Derive a lane-specific AEAD key. * * laneKey[i] = HKDF(ikm=streamKey, salt=streamId, * info="shade-stream/v1\0lane\0" || u32_be(laneId), length=32) * * Distinct laneIds produce independent keys; receiver derives the same key * given the same (streamSecret, streamId, laneId). */ export async function deriveLaneKey( crypto: CryptoProvider, streamKey: Uint8Array, streamId: Uint8Array, laneId: number, ): Promise { if (streamKey.length !== STREAM_KEY_BYTES) { throw new ValidationError(`streamKey must be ${STREAM_KEY_BYTES} bytes`, 'streamKey'); } if (streamId.length !== STREAM_ID_BYTES) { throw new ValidationError(`streamId must be ${STREAM_ID_BYTES} bytes`, 'streamId'); } if (!Number.isInteger(laneId) || laneId < 0 || laneId > 0xffff_ffff) { throw new ValidationError(`laneId must fit in u32: ${laneId}`, 'laneId'); } const info = new Uint8Array(LANE_KEY_INFO_PREFIX.length + 4); info.set(LANE_KEY_INFO_PREFIX, 0); new DataView(info.buffer).setUint32(LANE_KEY_INFO_PREFIX.length, laneId, false); return crypto.hkdf(streamKey, streamId, info, LANE_KEY_BYTES); }