UNPKG

@hpke/hybridkem-x-wing

Version:

A Hybrid Public Key Encryption (HPKE) module extension for X-Wing: general-purpose hybrid post-quantum KEM.

452 lines (451 loc) 17 kB
import { sha3_256, shake256 } from "@noble/hashes/sha3"; // @ts-ignore: Unreachable code error import { MlKem768 } from "mlkem"; import { base64UrlToBytes, concat, DecapError, DeriveKeyPairError, DeserializeError, EncapError, InvalidParamError, isCryptoKeyPair, KEM_USAGES, KemId, loadCrypto, NotSupportedError, SerializeError, XCryptoKey, } from "@hpke/common"; const ALG_NAME = "X-Wing"; import { HkdfSha256, X25519 } from "@hpke/dhkem-x25519"; // deno-fmt-ignore const X25519_BASE = new Uint8Array([ 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]); // XWingLabel = concat( // "\./", // "/^\", // ); // deno-fmt-ignore const XWING_LABEL = new Uint8Array([92, 46, 47, 47, 94, 92]); function combiner(ssM, ssX, ctX, pkX) { const ret = new Uint8Array(ssM.length + ssX.length + ctX.length + pkX.length + XWING_LABEL.length); ret.set(ssM, 0); ret.set(ssX, ssM.length); ret.set(ctX, ssM.length + ssX.length); ret.set(pkX, ssM.length + ssX.length + ctX.length); ret.set(XWING_LABEL, ssM.length + ssX.length + ctX.length + pkX.length); return sha3_256.create().update(ret).digest(); } /** * The Hybrid Post-Quantum KEM (X25519, Kyber768). * * This class is implemented using * {@link https://github.com/Argyle-Software/kyber | pqc-kyber }. * * The instance of this class can be specified to the * {@link https://jsr.io/@hpke/core/doc/~/CipherSuiteParams | CipherSuiteParams} as follows: * * @example Use with `@hpke/core`: * * ```ts * import { Aes128Gcm, CipherSuite, HkdfSha256 } from "@hpke/core"; * import { XWing } from "@hpke/hybridkem-x-wing"; * const suite = new CipherSuite({ * kem: new XWing(), * kdf: new HkdfSha256(), * aead: new Aes128Gcm(), * }); * ``` */ export class XWing { constructor() { Object.defineProperty(this, "id", { enumerable: true, configurable: true, writable: true, value: KemId.XWing }); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: ALG_NAME }); Object.defineProperty(this, "secretSize", { enumerable: true, configurable: true, writable: true, value: 32 }); Object.defineProperty(this, "encSize", { enumerable: true, configurable: true, writable: true, value: 1120 }); Object.defineProperty(this, "publicKeySize", { enumerable: true, configurable: true, writable: true, value: 1216 }); Object.defineProperty(this, "privateKeySize", { enumerable: true, configurable: true, writable: true, value: 32 }); Object.defineProperty(this, "auth", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "_m", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_x25519", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_api", { enumerable: true, configurable: true, writable: true, value: undefined }); this._m = new MlKem768(); this._x25519 = new X25519(new HkdfSha256()); } async serializePublicKey(key) { await this._setup(); try { return await this._serializePublicKey(key); } catch (e) { throw new SerializeError(e); } } async deserializePublicKey(key) { await this._setup(); try { return await this._deserializePublicKey(key); } catch (e) { throw new DeserializeError(e); } } async serializePrivateKey(key) { await this._setup(); try { return await this._serializePrivateKey(key); } catch (e) { throw new SerializeError(e); } } async deserializePrivateKey(key) { await this._setup(); try { return await this._deserializePrivateKey(key); } catch (e) { throw new DeserializeError(e); } } /** * Generates a new key pair. * * @returns {Promise<CryptoKeyPair>} A promise that resolves with a new key pair. */ async generateKeyPair() { await this._setup(); const sk = new Uint8Array(32); try { this._api.getRandomValues(sk); } catch (e) { throw new NotSupportedError(e); } try { const [_sk, pk] = await this._generateKeyPairDerand(sk); const dSk = await this.deserializePrivateKey(sk); const dPk = await this.deserializePublicKey(pk); return { privateKey: dSk, publicKey: dPk }; } catch (e) { throw new DeriveKeyPairError(e); } } /** * Generates a key pair from the secret key. * @param sk The secret key. * @returns {Promise<CryptoKeyPair>} A promise that resolves with a new key pair. * @throws {InvalidParamError} Thrown if the length of the secret key is not 32 bytes. * @throws {DeriveKeyPairError} Thrown if the key pair cannot be derived. */ async generateKeyPairDerand(sk) { if (sk.byteLength !== 32) { throw new InvalidParamError("Invalid length of sk"); } try { const [_sk, pk] = await this._generateKeyPairDerand(sk); const dSk = await this.deserializePrivateKey(sk); const dPk = await this.deserializePublicKey(pk); return { privateKey: dSk, publicKey: dPk }; } catch (e) { throw new DeriveKeyPairError(e); } } /** * Derives a key pair from the input keying material. * * @param {ArrayBuffer} ikm The input keying material. * @returns {Promise<CryptoKeyPair>} A promise that resolves with a new key pair. * @throws {DeriveKeyPairError} Thrown if the key pair cannot be derived. * @throws {InvalidParamError} Thrown if the length of the IKM is not 32 bytes. */ async deriveKeyPair(ikm) { await this._setup(); try { const sk = shake256.create({ dkLen: 32 }) .update(new Uint8Array(ikm)) .digest(); const [_sk, pk] = await this._generateKeyPairDerand(sk); const dSk = await this.deserializePrivateKey(sk); const dPk = await this.deserializePublicKey(pk); return { privateKey: dSk, publicKey: dPk }; } catch (e) { throw new DeriveKeyPairError(e); } } /** * Imports a key from the input. * @param format The format of the key. "raw" or "jwk" can be specified. * @param key The key to import. If the format is "raw", the key must be an ArrayBuffer. If the format is "jwk", the key must be a JsonWebKey. * @param isPublic A boolean indicating whether the key is public or not. The default is true. * @returns {Promise<CryptoKey>} A promise that resolves with the imported key. * @throws {DeserializeError} Thrown if the key cannot be imported. */ async importKey(format, key, isPublic = true) { await this._setup(); try { let ret; if (format === "jwk") { if (key instanceof ArrayBuffer || key instanceof Uint8Array) { throw new Error("Invalid jwk key format"); } ret = await this._importJWK(key, isPublic); } else { if (key instanceof ArrayBuffer) { ret = new Uint8Array(key); } else if (key instanceof Uint8Array) { ret = key; } else { throw new Error("Invalid key format"); } } if (isPublic && ret.byteLength !== this.publicKeySize) { throw new Error("Invalid length of the key"); } if (!isPublic && ret.byteLength !== this.privateKeySize) { throw new Error("Invalid length of the key"); } return new XCryptoKey(ALG_NAME, ret, isPublic ? "public" : "private", isPublic ? [] : KEM_USAGES); } catch (e) { throw new DeserializeError(e); } } /** * Encapsulates the shared secret and the `ct` (ciphertext) as `enc`. * @param params The parameters for encapsulation. * @returns {Promise<{ sharedSecret: ArrayBuffer; enc: ArrayBuffer }>} A promise that resolves with the `ss` (shared secret) as `sharedSecret` and the `ct` (ciphertext) as `enc`. * @throws {InvalidParamError} Thrown if the length of the `ekm` is not 64 bytes. * @throws {EncapError} Thrown if the shared secret cannot be encapsulated. */ async encap(params) { let ekm = undefined; if (params.ekm !== undefined && !isCryptoKeyPair(params.ekm)) { if (params.ekm.byteLength !== 64) { throw new InvalidParamError("ekm must be 64 bytes in length"); } ekm = params.ekm; } let ekM = undefined; let ekX; if (ekm !== undefined) { const ek = new Uint8Array(ekm); ekM = ek.subarray(0, 32); ekX = ek.subarray(32, 64); } else { ekX = new Uint8Array(32); try { this._api.getRandomValues(ekX); } catch (e) { throw new NotSupportedError(e); } } const pk = new Uint8Array(await this.serializePublicKey(params.recipientPublicKey)); if (pk.byteLength !== 1216) { throw new InvalidParamError("Invalid length of recipientPublicKey"); } await this._setup(); try { const pkM = pk.subarray(0, 1184); const pkX = pk.subarray(1184, 1216); const ctX = await this._x25519.derive(ekX, X25519_BASE); const ssX = await this._x25519.derive(ekX, pkX); const [ctM, ssM] = await this._m.encap(pkM, ekM); return { sharedSecret: combiner(ssM, ssX, ctX, pkX), enc: concat(ctM, ctX), }; } catch (e) { throw new EncapError(e); } } /** * Decapsulates the `ss` (shared secret) from the `enc` and the recipient's private key. * The `enc` is the same as the `ct` (ciphertext) resulting from `X-Wing::Encapsulate(), * which is executed under the `encap()`. * @param params The parameters for decapsulation. * @returns {Promise<ArrayBuffer>} A promise that resolves with the shared secret. * @throws {InvalidParamError} Thrown if the length of the `enc` is not 1120 bytes. * @throws {DecapError} Thrown if the shared secret cannot be decapsulated. */ async decap(params) { const rSk = isCryptoKeyPair(params.recipientKey) ? params.recipientKey.privateKey : params.recipientKey; if (params.enc.byteLength !== 1120) { throw new InvalidParamError("Invalid length of enc"); } const sk = new Uint8Array(await this.serializePrivateKey(rSk)); if (sk.byteLength !== 32) { throw new InvalidParamError("Invalid length of recipientKey"); } await this._setup(); try { const [skM, skX, _pkM, pkX] = await this._expandDecapsulationKey(sk); const ct = new Uint8Array(params.enc); const ctM = ct.subarray(0, 1088); const ctX = ct.subarray(1088); const ssM = await this._m.decap(ctM, skM); const ssX = await this._x25519.derive(skX, ctX); return combiner(ssM, ssX, ctX, pkX); } catch (e) { throw new DecapError(e); } } /** * Sets up the MlKemBase instance by loading the necessary crypto library. * If the crypto library is already loaded, this method does nothing. * @returns {Promise<void>} A promise that resolves when the setup is complete. */ async _setup() { if (this._api !== undefined) { return; } this._api = await loadCrypto(); } /** * Generates a key pair from the secret key. * @param sk The secret key. * @returns {Promise<[Uint8Array, Uint8Array]>} A promise that resolves with the key pair derived from the secret key. */ async _generateKeyPairDerand(sk) { const [_skM, _skX, pkM, pkX] = await this._expandDecapsulationKey(sk); return [sk, concat(pkM, pkX)]; } /** * Expands the decapsulation key. * @param sk The secret key. * @returns {Promise<[Uint8Array, Uint8Array, Uint8Array, Uint8Array]>} A promise that resolves with the keys derived by expanding the secret key. */ async _expandDecapsulationKey(sk) { const expanded = shake256.create({ dkLen: 96 }).update(sk).digest(); const [pkM, skM] = await this._m.deriveKeyPair(expanded.subarray(0, 64)); const skX = expanded.subarray(64, 96); const pkX = await this._x25519.derive(skX, X25519_BASE); return [skM, skX, pkM, pkX]; } _serializePublicKey(k) { return new Promise((resolve, reject) => { if (k.type !== "public") { reject(new Error("Not public key")); } if (k.algorithm.name !== this.name) { reject(new Error(`Invalid algorithm name: ${k.algorithm.name}`)); } if (k.key.byteLength !== this.publicKeySize) { reject(new Error(`Invalid key length: ${k.key.byteLength}`)); } resolve(k.key.buffer); }); } _deserializePublicKey(k) { return new Promise((resolve, reject) => { if (k.byteLength !== this.publicKeySize) { reject(new Error(`Invalid key length: ${k.byteLength}`)); } resolve(new XCryptoKey(this.name, new Uint8Array(k), "public")); }); } _serializePrivateKey(k) { return new Promise((resolve, reject) => { if (k.type !== "private") { reject(new Error("Not private key")); } if (k.algorithm.name !== this.name) { reject(new Error(`Invalid algorithm name: ${k.algorithm.name}`)); } if (k.key.byteLength !== this.privateKeySize) { reject(new Error(`Invalid key length: ${k.key.byteLength}`)); } resolve(k.key.buffer); }); } _deserializePrivateKey(k) { return new Promise((resolve, reject) => { if (k.byteLength !== this.privateKeySize) { reject(new Error(`Invalid key length: ${k.byteLength}`)); } resolve(new XCryptoKey(this.name, new Uint8Array(k), "private", ["deriveBits"])); }); } _importJWK(key, isPublic) { return new Promise((resolve, reject) => { if (typeof key.kty === "undefined" || key.kty !== "AKP") { reject(new Error(`Invalid kty: ${key.kty}`)); } if (typeof key.alg === "undefined" || key.alg !== ALG_NAME) { reject(new Error(`Invalid alg: ${key.alg}`)); } if (!isPublic) { if (typeof key.priv === "undefined") { reject(new Error("Invalid key: `priv` not found")); } if (typeof key.key_ops !== "undefined" && (key.key_ops.length !== 1 || key.key_ops[0] !== "deriveBits")) { reject(new Error("Invalid key: `key_ops` should be ['deriveBits']")); } resolve(base64UrlToBytes(key.priv)); } if (typeof key.priv !== "undefined") { reject(new Error("Invalid key: `priv` should not be set")); } if (typeof key.pub === "undefined") { reject(new Error("Invalid key: `pub` not found")); } if (typeof key.key_ops !== "undefined" && key.key_ops.length > 0) { reject(new Error("Invalid key: `key_ops` should not be set")); } resolve(base64UrlToBytes(key.pub)); }); } }