@hpke/core
Version:
A Hybrid Public Key Encryption (HPKE) core module for various JavaScript runtimes
332 lines (331 loc) • 12.8 kB
JavaScript
import { AeadId, EMPTY, i2Osp, INFO_LENGTH_LIMIT, INPUT_LENGTH_LIMIT, InvalidParamError, MINIMUM_PSK_LENGTH, Mode, NativeAlgorithm, } from "@hpke/common";
import { RecipientExporterContextImpl, SenderExporterContextImpl, } from "./exporterContext.js";
import { RecipientContextImpl } from "./recipientContext.js";
import { SenderContextImpl } from "./senderContext.js";
// b"base_nonce"
// deno-fmt-ignore
const LABEL_BASE_NONCE = new Uint8Array([
98, 97, 115, 101, 95, 110, 111, 110, 99, 101,
]);
// b"exp"
const LABEL_EXP = new Uint8Array([101, 120, 112]);
// b"info_hash"
// deno-fmt-ignore
const LABEL_INFO_HASH = new Uint8Array([
105, 110, 102, 111, 95, 104, 97, 115, 104,
]);
// b"key"
const LABEL_KEY = new Uint8Array([107, 101, 121]);
// b"psk_id_hash"
// deno-fmt-ignore
const LABEL_PSK_ID_HASH = new Uint8Array([
112, 115, 107, 95, 105, 100, 95, 104, 97, 115, 104,
]);
// b"secret"
const LABEL_SECRET = new Uint8Array([115, 101, 99, 114, 101, 116]);
// b"HPKE"
// deno-fmt-ignore
const SUITE_ID_HEADER_HPKE = new Uint8Array([
72, 80, 75, 69, 0, 0, 0, 0, 0, 0,
]);
/**
* The Hybrid Public Key Encryption (HPKE) ciphersuite,
* which is implemented using only
* {@link https://www.w3.org/TR/WebCryptoAPI/ | Web Cryptography API}.
*
* This is the super class of {@link CipherSuite} and the same as
* {@link https://jsr.io/@hpke/core/doc/~/CipherSuite | @hpke/core#CipherSuite} as follows:
* which supports only the ciphersuites that can be implemented on the native
* {@link https://www.w3.org/TR/WebCryptoAPI/ | Web Cryptography API}.
* Therefore, the following cryptographic algorithms are not supported for now:
* - DHKEM(X25519, HKDF-SHA256)
* - DHKEM(X448, HKDF-SHA512)
* - ChaCha20Poly1305
*
* In addtion, the HKDF functions contained in this class can only derive
* keys of the same length as the `hashSize`.
*
* If you want to use the unsupported cryptographic algorithms
* above or derive keys longer than the `hashSize`,
* please use {@link CipherSuite}.
*
* This class provides following functions:
*
* - Creates encryption contexts both for senders and recipients.
* - {@link createSenderContext}
* - {@link createRecipientContext}
* - Provides single-shot encryption API.
* - {@link seal}
* - {@link open}
*
* The calling of the constructor of this class is the starting
* point for HPKE operations for both senders and recipients.
*
* @example Use only ciphersuites supported by Web Cryptography API.
*
* ```ts
* import {
* Aes128Gcm,
* DhkemP256HkdfSha256,
* HkdfSha256,
* CipherSuite,
* } from "@hpke/core";
*
* const suite = new CipherSuite({
* kem: new DhkemP256HkdfSha256(),
* kdf: new HkdfSha256(),
* aead: new Aes128Gcm(),
* });
* ```
*
* @example Use a ciphersuite which is currently not supported by Web Cryptography API.
*
* ```ts
* import { Aes128Gcm, HkdfSha256, CipherSuite } from "@hpke/core";
* // Use an extension module.
* import { DhkemX25519HkdfSha256 } from "@hpke/dhkem-x25519";
*
* const suite = new CipherSuite({
* kem: new DhkemX25519HkdfSha256(),
* kdf: new HkdfSha256(),
* aead: new Aes128Gcm(),
* });
* ```
*/
export class CipherSuiteNative extends NativeAlgorithm {
/**
* @param params A set of parameters for building a cipher suite.
*
* If the error occurred, throws {@link InvalidParamError}.
*
* @throws {@link InvalidParamError}
*/
constructor(params) {
super();
Object.defineProperty(this, "_kem", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_kdf", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_aead", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "_suiteId", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
// KEM
if (typeof params.kem === "number") {
throw new InvalidParamError("KemId cannot be used");
}
this._kem = params.kem;
// KDF
if (typeof params.kdf === "number") {
throw new InvalidParamError("KdfId cannot be used");
}
this._kdf = params.kdf;
// AEAD
if (typeof params.aead === "number") {
throw new InvalidParamError("AeadId cannot be used");
}
this._aead = params.aead;
this._suiteId = new Uint8Array(SUITE_ID_HEADER_HPKE);
this._suiteId.set(i2Osp(this._kem.id, 2), 4);
this._suiteId.set(i2Osp(this._kdf.id, 2), 6);
this._suiteId.set(i2Osp(this._aead.id, 2), 8);
this._kdf.init(this._suiteId);
}
/**
* Gets the KEM context of the ciphersuite.
*/
get kem() {
return this._kem;
}
/**
* Gets the KDF context of the ciphersuite.
*/
get kdf() {
return this._kdf;
}
/**
* Gets the AEAD context of the ciphersuite.
*/
get aead() {
return this._aead;
}
/**
* Creates an encryption context for a sender.
*
* If the error occurred, throws {@link DecapError} | {@link ValidationError}.
*
* @param params A set of parameters for the sender encryption context.
* @returns A sender encryption context.
* @throws {@link EncapError}, {@link ValidationError}
*/
async createSenderContext(params) {
this._validateInputLength(params);
await this._setup();
const dh = await this._kem.encap(params);
let mode;
if (params.psk !== undefined) {
mode = params.senderKey !== undefined ? Mode.AuthPsk : Mode.Psk;
}
else {
mode = params.senderKey !== undefined ? Mode.Auth : Mode.Base;
}
return await this._keyScheduleS(mode, dh.sharedSecret, dh.enc, params);
}
/**
* Creates an encryption context for a recipient.
*
* If the error occurred, throws {@link DecapError}
* | {@link DeserializeError} | {@link ValidationError}.
*
* @param params A set of parameters for the recipient encryption context.
* @returns A recipient encryption context.
* @throws {@link DecapError}, {@link DeserializeError}, {@link ValidationError}
*/
async createRecipientContext(params) {
this._validateInputLength(params);
await this._setup();
const sharedSecret = await this._kem.decap(params);
let mode;
if (params.psk !== undefined) {
mode = params.senderPublicKey !== undefined ? Mode.AuthPsk : Mode.Psk;
}
else {
mode = params.senderPublicKey !== undefined ? Mode.Auth : Mode.Base;
}
return await this._keyScheduleR(mode, sharedSecret, params);
}
/**
* Encrypts a message to a recipient.
*
* If the error occurred, throws `EncapError` | `MessageLimitReachedError` | `SealError` | `ValidationError`.
*
* @param params A set of parameters for building a sender encryption context.
* @param pt A plain text as bytes to be encrypted.
* @param aad Additional authenticated data as bytes fed by an application.
* @returns A cipher text and an encapsulated key as bytes.
* @throws {@link EncapError}, {@link MessageLimitReachedError}, {@link SealError}, {@link ValidationError}
*/
async seal(params, pt, aad = EMPTY.buffer) {
const ctx = await this.createSenderContext(params);
return {
ct: await ctx.seal(pt, aad),
enc: ctx.enc,
};
}
/**
* Decrypts a message from a sender.
*
* If the error occurred, throws `DecapError` | `DeserializeError` | `OpenError` | `ValidationError`.
*
* @param params A set of parameters for building a recipient encryption context.
* @param ct An encrypted text as bytes to be decrypted.
* @param aad Additional authenticated data as bytes fed by an application.
* @returns A decrypted plain text as bytes.
* @throws {@link DecapError}, {@link DeserializeError}, {@link OpenError}, {@link ValidationError}
*/
async open(params, ct, aad = EMPTY.buffer) {
const ctx = await this.createRecipientContext(params);
return await ctx.open(ct, aad);
}
// private verifyPskInputs(mode: Mode, params: KeyScheduleParams) {
// const gotPsk = (params.psk !== undefined);
// const gotPskId = (params.psk !== undefined && params.psk.id.byteLength > 0);
// if (gotPsk !== gotPskId) {
// throw new Error('Inconsistent PSK inputs');
// }
// if (gotPsk && (mode === Mode.Base || mode === Mode.Auth)) {
// throw new Error('PSK input provided when not needed');
// }
// if (!gotPsk && (mode === Mode.Psk || mode === Mode.AuthPsk)) {
// throw new Error('Missing required PSK input');
// }
// return;
// }
async _keySchedule(mode, sharedSecret, params) {
// Currently, there is no point in executing this function
// because this hpke library does not allow users to explicitly specify the mode.
//
// this.verifyPskInputs(mode, params);
const pskId = params.psk === undefined
? EMPTY
: new Uint8Array(params.psk.id);
const pskIdHash = await this._kdf.labeledExtract(EMPTY.buffer, LABEL_PSK_ID_HASH, pskId);
const info = params.info === undefined
? EMPTY
: new Uint8Array(params.info);
const infoHash = await this._kdf.labeledExtract(EMPTY.buffer, LABEL_INFO_HASH, info);
const keyScheduleContext = new Uint8Array(1 + pskIdHash.byteLength + infoHash.byteLength);
keyScheduleContext.set(new Uint8Array([mode]), 0);
keyScheduleContext.set(new Uint8Array(pskIdHash), 1);
keyScheduleContext.set(new Uint8Array(infoHash), 1 + pskIdHash.byteLength);
const psk = params.psk === undefined
? EMPTY
: new Uint8Array(params.psk.key);
const ikm = this._kdf.buildLabeledIkm(LABEL_SECRET, psk)
.buffer;
const exporterSecretInfo = this._kdf.buildLabeledInfo(LABEL_EXP, keyScheduleContext, this._kdf.hashSize).buffer;
const exporterSecret = await this._kdf.extractAndExpand(sharedSecret, ikm, exporterSecretInfo, this._kdf.hashSize);
if (this._aead.id === AeadId.ExportOnly) {
return { aead: this._aead, exporterSecret: exporterSecret };
}
const keyInfo = this._kdf.buildLabeledInfo(LABEL_KEY, keyScheduleContext, this._aead.keySize).buffer;
const key = await this._kdf.extractAndExpand(sharedSecret, ikm, keyInfo, this._aead.keySize);
const baseNonceInfo = this._kdf.buildLabeledInfo(LABEL_BASE_NONCE, keyScheduleContext, this._aead.nonceSize).buffer;
const baseNonce = await this._kdf.extractAndExpand(sharedSecret, ikm, baseNonceInfo, this._aead.nonceSize);
return {
aead: this._aead,
exporterSecret: exporterSecret,
key: key,
baseNonce: new Uint8Array(baseNonce),
seq: 0,
};
}
async _keyScheduleS(mode, sharedSecret, enc, params) {
const res = await this._keySchedule(mode, sharedSecret, params);
if (res.key === undefined) {
return new SenderExporterContextImpl(this._api, this._kdf, res.exporterSecret, enc);
}
return new SenderContextImpl(this._api, this._kdf, res, enc);
}
async _keyScheduleR(mode, sharedSecret, params) {
const res = await this._keySchedule(mode, sharedSecret, params);
if (res.key === undefined) {
return new RecipientExporterContextImpl(this._api, this._kdf, res.exporterSecret);
}
return new RecipientContextImpl(this._api, this._kdf, res);
}
_validateInputLength(params) {
if (params.info !== undefined &&
params.info.byteLength > INFO_LENGTH_LIMIT) {
throw new InvalidParamError("Too long info");
}
if (params.psk !== undefined) {
if (params.psk.key.byteLength < MINIMUM_PSK_LENGTH) {
throw new InvalidParamError(`PSK must have at least ${MINIMUM_PSK_LENGTH} bytes`);
}
if (params.psk.key.byteLength > INPUT_LENGTH_LIMIT) {
throw new InvalidParamError("Too long psk.key");
}
if (params.psk.id.byteLength > INPUT_LENGTH_LIMIT) {
throw new InvalidParamError("Too long psk.id");
}
}
return;
}
}