UNPKG

@hpke/ohttp

Version:

Oblivious HTTP (RFC 9458) client and server using HPKE

126 lines (125 loc) • 4.16 kB
import { OhttpError } from "./errors.js"; /** * Deserialize an `application/ohttp-keys` payload into key configurations. * * The wire format is a sequence of length-prefixed KeyConfig entries: * * KeyConfig { * Key Identifier (8), * KEM ID (16), * Public Key (Npk bytes, determined by KEM), * Cipher Suites Length (16), * Cipher Suites (variable) { * KDF ID (16), * AEAD ID (16), * } ... * } * * Each KeyConfig is prefixed with its 2-byte length. */ export function deserializeKeyConfig(data) { const configs = []; const view = new DataView(data.buffer, data.byteOffset, data.byteLength); let offset = 0; while (offset < data.byteLength) { if (offset + 2 > data.byteLength) { throw new OhttpError("Truncated key configuration length prefix"); } const configLen = view.getUint16(offset); offset += 2; if (offset + configLen > data.byteLength) { throw new OhttpError("Truncated key configuration entry"); } const configEnd = offset + configLen; const config = parseSingleKeyConfig(data, view, offset, configEnd); configs.push(config); offset = configEnd; } return configs; } function parseSingleKeyConfig(data, view, start, end) { let offset = start; if (offset + 1 > end) throw new OhttpError("Truncated key config: keyId"); const keyId = data[offset]; offset += 1; if (offset + 2 > end) throw new OhttpError("Truncated key config: kem"); const kem = view.getUint16(offset); offset += 2; const npk = kemPublicKeyLength(kem); if (offset + npk > end) { throw new OhttpError("Truncated key config: publicKey"); } const publicKey = data.slice(offset, offset + npk); offset += npk; if (offset + 2 > end) { throw new OhttpError("Truncated key config: cipherSuites length"); } const csLen = view.getUint16(offset); offset += 2; if (csLen % 4 !== 0) { throw new OhttpError("Invalid cipher suites length (must be multiple of 4)"); } if (offset + csLen > end) { throw new OhttpError("Truncated key config: cipherSuites"); } const cipherSuites = []; const csEnd = offset + csLen; while (offset < csEnd) { const kdf = view.getUint16(offset); offset += 2; const aead = view.getUint16(offset); offset += 2; cipherSuites.push({ kdf, aead }); } return { keyId, kem, publicKey, cipherSuites }; } /** * Serialize a single key configuration to `application/ohttp-keys` format. * The result is a length-prefixed KeyConfig entry. */ export function serializeKeyConfig(config) { const npk = config.publicKey.byteLength; const csLen = config.cipherSuites.length * 4; // keyId(1) + kem(2) + publicKey(npk) + csLen(2) + cipherSuites(csLen) const configLen = 1 + 2 + npk + 2 + csLen; // 2-byte length prefix + config body const buf = new Uint8Array(2 + configLen); const view = new DataView(buf.buffer); let offset = 0; view.setUint16(offset, configLen); offset += 2; buf[offset] = config.keyId; offset += 1; view.setUint16(offset, config.kem); offset += 2; buf.set(config.publicKey, offset); offset += npk; view.setUint16(offset, csLen); offset += 2; for (const cs of config.cipherSuites) { view.setUint16(offset, cs.kdf); offset += 2; view.setUint16(offset, cs.aead); offset += 2; } return buf; } /** Return the public key length in bytes for a given KEM ID. */ function kemPublicKeyLength(kemId) { switch (kemId) { case 0x0010: // DHKEM(P-256, HKDF-SHA256) return 65; case 0x0011: // DHKEM(P-384, HKDF-SHA384) return 97; case 0x0012: // DHKEM(P-521, HKDF-SHA512) return 133; case 0x0020: // DHKEM(X25519, HKDF-SHA256) return 32; case 0x0021: // DHKEM(X448, HKDF-SHA512) return 56; default: throw new OhttpError(`Unsupported KEM ID: 0x${kemId.toString(16)}`); } }