@hpke/core
Version:
A Hybrid Public Key Encryption (HPKE) core module for various JavaScript runtimes
346 lines (345 loc) • 15 kB
JavaScript
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "@hpke/common", "./exporterContext.js", "./recipientContext.js", "./senderContext.js"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CipherSuiteNative = void 0;
const common_1 = require("@hpke/common");
const exporterContext_js_1 = require("./exporterContext.js");
const recipientContext_js_1 = require("./recipientContext.js");
const senderContext_js_1 = require("./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(),
* });
* ```
*/
class CipherSuiteNative extends common_1.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 common_1.InvalidParamError("KemId cannot be used");
}
this._kem = params.kem;
// KDF
if (typeof params.kdf === "number") {
throw new common_1.InvalidParamError("KdfId cannot be used");
}
this._kdf = params.kdf;
// AEAD
if (typeof params.aead === "number") {
throw new common_1.InvalidParamError("AeadId cannot be used");
}
this._aead = params.aead;
this._suiteId = new Uint8Array(SUITE_ID_HEADER_HPKE);
this._suiteId.set((0, common_1.i2Osp)(this._kem.id, 2), 4);
this._suiteId.set((0, common_1.i2Osp)(this._kdf.id, 2), 6);
this._suiteId.set((0, common_1.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 ? common_1.Mode.AuthPsk : common_1.Mode.Psk;
}
else {
mode = params.senderKey !== undefined ? common_1.Mode.Auth : common_1.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 ? common_1.Mode.AuthPsk : common_1.Mode.Psk;
}
else {
mode = params.senderPublicKey !== undefined ? common_1.Mode.Auth : common_1.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 = common_1.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 = common_1.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
? common_1.EMPTY
: new Uint8Array(params.psk.id);
const pskIdHash = await this._kdf.labeledExtract(common_1.EMPTY.buffer, LABEL_PSK_ID_HASH, pskId);
const info = params.info === undefined
? common_1.EMPTY
: new Uint8Array(params.info);
const infoHash = await this._kdf.labeledExtract(common_1.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
? common_1.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 === common_1.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 exporterContext_js_1.SenderExporterContextImpl(this._api, this._kdf, res.exporterSecret, enc);
}
return new senderContext_js_1.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 exporterContext_js_1.RecipientExporterContextImpl(this._api, this._kdf, res.exporterSecret);
}
return new recipientContext_js_1.RecipientContextImpl(this._api, this._kdf, res);
}
_validateInputLength(params) {
if (params.info !== undefined &&
params.info.byteLength > common_1.INFO_LENGTH_LIMIT) {
throw new common_1.InvalidParamError("Too long info");
}
if (params.psk !== undefined) {
if (params.psk.key.byteLength < common_1.MINIMUM_PSK_LENGTH) {
throw new common_1.InvalidParamError(`PSK must have at least ${common_1.MINIMUM_PSK_LENGTH} bytes`);
}
if (params.psk.key.byteLength > common_1.INPUT_LENGTH_LIMIT) {
throw new common_1.InvalidParamError("Too long psk.key");
}
if (params.psk.id.byteLength > common_1.INPUT_LENGTH_LIMIT) {
throw new common_1.InvalidParamError("Too long psk.id");
}
}
return;
}
}
exports.CipherSuiteNative = CipherSuiteNative;
});