UNPKG

@hpke/ohttp

Version:

Oblivious HTTP (RFC 9458) client and server using HPKE

137 lines (136 loc) • 5.03 kB
import { CipherSuite } from "@hpke/core"; import { decodeResponse } from "./bhttp/decoder.js"; import { encodeRequest } from "./bhttp/encoder.js"; import { decapsulateResponse, encapsulateRequest } from "./encapsulation.js"; import { OhttpError } from "./errors.js"; import { deserializeKeyConfig } from "./keyConfig.js"; import { bindKdf } from "./suite.js"; class OhttpClientImpl { constructor(suite, keyId, publicKey, relayUrl) { Object.defineProperty(this, "_suite", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_keyId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_publicKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_relayUrl", { enumerable: true, configurable: true, writable: true, value: void 0 }); this._suite = suite; this._keyId = keyId; this._publicKey = publicKey; this._relayUrl = relayUrl; } async fetch(input, init) { if (this._relayUrl === undefined) { throw new OhttpError("relayUrl is required to use fetch()"); } const request = input instanceof Request ? (init ? new Request(input, init) : input) : new Request(input, init); const ctx = await this.encapsulateRequest(request); const relayResponse = await fetch(this._relayUrl, { method: "POST", headers: { "Content-Type": "message/ohttp-req" }, body: ctx.encapsulatedRequest.slice().buffer, }); if (!relayResponse.ok) { throw new OhttpError(`Relay returned HTTP ${relayResponse.status}`); } return ctx.decapsulateResponse(new Uint8Array(await relayResponse.arrayBuffer())); } async encapsulateRequest(request) { const result = await encapsulateRequest(this._suite, this._keyId, this._suite.kem.id, this._suite.kdf.id, this._suite.aead.id, this._publicKey, await encodeRequest(request)); const reqCtx = result.context; return { encapsulatedRequest: result.encRequest, async decapsulateResponse(encapsulatedResponse) { return decodeResponse(await decapsulateResponse(reqCtx, encapsulatedResponse)); }, }; } } /** * Selects the key configuration and KDF/AEAD pair to use. Key * configurations and their cipher suites are examined in the gateway's * order of preference (RFC 9458 Section 3.1). */ function selectKeyConfig(params) { const configs = params.keyConfig instanceof Uint8Array ? deserializeKeyConfig(params.keyConfig) : [params.keyConfig]; if (configs.length === 0) { throw new OhttpError("Empty key configuration"); } const kemId = params.kem.id; let kemMatched = false; for (const config of configs) { if (config.kem !== kemId) { continue; } kemMatched = true; for (const cs of config.cipherSuites) { const suite = params.suites.find((s) => s.kdf.id === cs.kdf && s.aead.id === cs.aead); if (suite) { return { config, suite }; } } } if (!kemMatched) { throw new OhttpError(`KEM 0x${kemId.toString(16)} not found in key configuration`); } throw new OhttpError("No mutually supported KDF/AEAD pair in key configuration"); } /** * Creates an OHTTP client (RFC 9458). * * The gateway's key configuration is validated against the given * primitives and its public key is imported once, up front. * * @throws {OhttpError} if the key configuration is invalid or not supported * by the given primitives. */ export async function createOhttpClient(params) { if (params.suites.length === 0) { throw new OhttpError("At least one KDF/AEAD pair is required"); } const { config, suite: pair } = selectKeyConfig(params); const suite = new CipherSuite({ kem: params.kem, // Keep the KDF state of this suite independent of the given instance. kdf: bindKdf(pair.kdf), aead: pair.aead, }); let publicKey; try { publicKey = await suite.kem.importKey("raw", config.publicKey.slice().buffer, true); } catch (e) { throw new OhttpError(`Invalid public key in key configuration: ${e instanceof Error ? e.message : String(e)}`); } let relayUrl; if (params.relayUrl !== undefined) { try { relayUrl = new URL(params.relayUrl).toString(); } catch { throw new OhttpError(`Invalid relayUrl: ${params.relayUrl}`); } } return new OhttpClientImpl(suite, config.keyId, publicKey, relayUrl); }