@hpke/ohttp
Version:
Oblivious HTTP (RFC 9458) client and server using HPKE
137 lines (136 loc) • 5.03 kB
JavaScript
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);
}