hpke-js
A TypeScript Hybrid Public Key Encryption (HPKE)
implementation build on top of Web Cryptography API.
This module works on web browsers, Node.js, Deno and various other JavaScript runtimes.
Documentation: deno.land | pages (only for the latest ver.)
For Node.js, you can install hpke-js
via npm/yarn:
npm install @hpke/core
# if necessary...
npm install @hpke/dhkem-x25519
npm install @hpke/dhkem-x448
npm install @hpke/chacha20poly1305
# ...or you can use the v0.x-compatible all-in-one package below.
npm install hpke-js
Then, you can use it as follows:
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";
// const { AeadId, CipherSuite, KdfId, KemId } = require("hpke-js");
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core";
async function doHpke() {
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
// When using "@hpke/core", specify the cryptographic algorithm instances
// as follows, instead of identities above.
// const suite = new CipherSuite({
// kem: new DhkemP256HkdfSha256(),
// kdf: new HkdfSha256(),
// aead: new Aes128Gcm(),
// });
// A recipient generates a key pair.
const rkp = await suite.kem.generateKeyPair();
// A sender encrypts a message with the recipient public key.
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});
const ct = await sender.seal(new TextEncoder().encode("Hello world!"));
// The recipient decrypts it.
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
});
const pt = await recipient.open(ct);
// Hello world!
console.log("new TextDecoder().decode(pt));
}
try {
doHpke();
} catch (e) {
console.log("failed:", e.message);
}
Index
- Packages
- Supported Features
- Supported Environments
- Warnings and Restrictions
- Installation
- Usage
- Base mode - for web browsers, Node.js and Deno.
- Base mode with Single-Shot APIs
- Base mode with export-only AEAD
- PSK mode
- Auth mode
- AuthPSK mode
- Contributing
- References
Packages
The hpke-js includes the following packages.
name | since | description |
---|---|---|
hpke-js | v0.1.0- | The HPKE module supporting all of the ciphersuites defined in RFC9180, which consists of the following @hpke/{core, dhkem-x25519, dhkem-x448, chacha20poly1305} internally. |
@hpke/core | v1.0.0- | The HPKE core module implemented using only Web Cryptography API. It does not support the X25519/X448-based KEMs and the ChaCha20/Poly1305 AEAD, but it has no external module dependencies. It's small in size and tree-shaking friendly. See /core/README. |
@hpke/chacha20poly1305 | v1.0.0- | The HPKE module extension for ChaCha20Poly1305 AEAD. See /x/chacha20poly1305/README. |
@hpke/dhkem-x25519 | v1.0.0- | The HPKE module extension for DHKEM(X25519, HKDF-SHA256). See /x/dhkem-x25519/README. |
@hpke/dhkem-x448 | v1.0.0- | The HPKE module extension for DHKEM(X448, HKDF-SHA512). See /x/dhkem-x448/README. |
@hpke/hybridkem-x25519-kyber768 | v1.2.1- | EXPERIMENTAL AND NOT STANDARDIZED The HPKE module extension for the hybrid post-quantum KEM currently named X25519Kyber768Draft00. See /x/hybridkem-x25519-kyber768/README. |
@hpke/dhkem-secp256k1 | v1.0.0- | EXPERIMENTAL AND NOT STANDARDIZED The HPKE module extension for DHKEM(secp256k1, HKDF-SHA256). See /x/dhkem-secp256k1/README. |
Supported Features
HPKE Modes
Base | PSK | Auth | AuthPSK |
---|---|---|---|
✅ | ✅ | ✅ | ✅ |
Key Encapsulation Machanisms (KEMs)
KEMs | Browser | Node.js | Deno | Cloudflare Workers |
bun |
---|---|---|---|---|---|
DHKEM (P-256, HKDF-SHA256) | ✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
DHKEM (P-384, HKDF-SHA384) | ✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
DHKEM (P-521, HKDF-SHA512) | ✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
|
DHKEM (X25519, HKDF-SHA256) | ✅ hpke-js @hpke/dhkem-x25519 |
✅ hpke-js @hpke/dhkem-x25519 |
✅ hpke-js @hpke/dhkem-x25519 |
✅ hpke-js @hpke/dhkem-x25519 |
✅ hpke-js @hpke/dhkem-x25519 |
DHKEM (X448, HKDF-SHA512) | ✅ hpke-js @hpke/dhkem-x448 |
✅ hpke-js @hpke/dhkem-x448 |
✅ hpke-js @hpke/dhkem-x448 |
✅ hpke-js @hpke/dhkem-x448 |
✅ hpke-js @hpke/dhkem-x448 |
Hybrid KEM (X25519, Kyber768) | ✅ @hpke/hybridkem-x25519-kyber768 |
✅ @hpke/hybridkem-x25519-kyber768 |
✅ @hpke/hybridkem-x25519-kyber768 |
✅ @hpke/hybridkem-x25519-kyber768 |
✅ @hpke/hybridkem-x25519-kyber768 |
DHKEM (secp256k1, HKDF-SHA256) | ✅ @hpke/dhkem-secp256k1 |
✅ @hpke/dhkem-secp256k1 |
✅ @hpke/dhkem-secp256k1 |
✅ @hpke/dhkem-secp256k1 |
✅ @hpke/dhkem-secp256k1 |
Key Derivation Functions (KDFs)
KDFs | Browser | Node.js | Deno | Cloudflare Workers |
bun |
---|---|---|---|---|---|
HKDF-SHA256 | ✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
HKDF-SHA384 | ✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
HKDF-SHA512 | ✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
✅ hpke-js @hpke/core(*1) |
- (*1) The HKDF functions built in
@hpke/core
can derive keys of the same length as the hash size. If you want to derive keys longer than the hash size, usehpke-js
.
Authenticated Encryption with Associated Data (AEAD) Functions
AEADs | Browser | Node.js | Deno | Cloudflare Workers |
bun |
---|---|---|---|---|---|
AES-128-GCM | ✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
AES-256-GCM | ✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
ChaCha20 Poly1305 |
✅ hpke-js @hpke/chacha 20poly1305 |
✅ hpke-js @hpke/chacha 20poly1305 |
✅ hpke-js @hpke/chacha 20poly1305 |
✅ hpke-js @hpke/chacha 20poly1305 |
✅ hpke-js @hpke/chacha 20poly1305 |
Export Only | ✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
✅ hpke-js @hpke/core |
Supported Environments
- Web Browser: Web Cryptography API
supported browsers
- Confirmed: Chrome, Firefox, Edge, Safari, Opera, Vivaldi, Brave
- Node.js: 16.x, 17.x, 18.x, 19.x, 20.x
- Deno: 1.x (1.25-)
- Cloudflare Workers
- bun: 0.x (0.4.0-)
Warnings and Restrictions
- Although this library has been passed the following test vectors, it has not been formally audited.
- The upper limit of the AEAD sequence number is further rounded to JavaScript's
MAX_SAFE_INTEGER (
2^53-1
).
Installation
Node.js
Using npm:
npm install @hpke/core
# if necessary...
npm install @hpke/dhkem-x25519
npm install @hpke/dhkem-x448
npm install @hpke/chacha20poly1305
# ...or you can use the v0.x-compatible all-in-one package below.
npm install hpke-js
Using yarn:
yarn add @hpke/core
# if necessary...
yarn add @hpke/dhkem-x25519
yarn add @hpke/dhkem-x448
yarn add @hpke/chacha20poly1305
# ...or you can use the v0.x-compatible all-in-one package below.
yarn add hpke-js
Deno
Using deno.land:
// use a specific version
import * as hpke from "https://deno.land/x/hpke@1.2.1/mod.ts";
// import * as hpke from "https://deno.land/x/hpke@1.2.1/core/mod.ts";
// import * as hpke from "https://deno.land/x/hpke@1.2.1/x/dhkem-x25519/mod.ts";
// use the latest stable version
import * as hpke from "https://deno.land/x/hpke/mod.ts";
// import * as hpke from "https://deno.land/x/hpke/core/mod.ts";
// import * as hpke from "https://deno.land/x/hpke/x/dhkem-x25519/mod.ts";
Web Browsers
Followings are how to use the module with typical CDNs. Other CDNs can be used as well.
Using esm.sh:
<!-- use a specific version -->
<script type="module">
import * as hpke from "https://esm.sh/hpke-js@1.2.1";
// import * as hpke from "https://esm.sh/@hpke/core@1.2.1";
// ...
</script>
<!-- use the latest stable version -->
<script type="module">
import * as hpke from "https://esm.sh/hpke-js";
// import * as hpke from "https://esm.sh/@hpke/core";
// ...
</script>
Using unpkg:
<!-- use a specific version -->
<script type="module">
import * as hpke from "https://unpkg.com/hpke-js@1.2.1/esm/mod.js";
// import * as hpke from "https://unpkg.com/@hpke/core@1.2.1/esm/mod.js";
// ...
</script>
Cloudflare Workers
git clone git@github.com:dajiaji/hpke-js.git
cd hpke-js
# for hpke-js
npm install -g esbuild
deno task dnt
deno task minify > $YOUR_SRC_PATH/hpke.js
# for @hpke/core
cd hpke-js/core
npm install -g esbuild
deno task dnt
deno task minify > $YOUR_SRC_PATH/hpke-core.js
# for @hpke/dhkem-x25519
cd hpke-js/x/dhkem-x25519
npm install -g esbuild
deno task dnt
deno task minify > $YOUR_SRC_PATH/hpke-dhkem-x25519.js
Usage
This section shows some typical usage examples.
Base mode
Node.js:
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";
// const { AeadId, CipherSuite, KdfId, KemId } = require("hpke-js");
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core";
async function doHpke() {
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
// A recipient generates a key pair.
const rkp = await suite.kem.generateKeyPair();
// A sender encrypts a message with the recipient public key.
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});
const ct = await sender.seal(new TextEncoder().encode("Hello world!"));
// The recipient decrypts it.
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
});
const pt = await recipient.open(ct);
// Hello world!
console.log("decrypted: ", new TextDecoder().decode(pt));
}
try {
doHpke();
} catch (e) {
console.log("failed:", e.message);
}
Deno:
import { AeadId, CipherSuite, KdfId, KemId } from "https://deno.land/x/hpke@1.2.1/mod.ts";
// import {
// Aes128Gcm, CipherSuite, HkdfSha256,
// } from "https://deno.land/x/hpke@1.2.1/core/mod.ts";
// import { DhkemX25519HkdfSha256 } from "https://deno.land/x/hpke@1.2.1/x/dhkem-x25519/mod.ts";
async function doHpke() {
// setup
const suite = new CipherSuite({
// When using "https://deno.land/x/hpke/mod.ts" (not "/x/hpke/core/mod.ts"),
// you can specify the identifier as follows:
kem: KemId.DhkemX25519HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
// When using hpke/core/mod.ts, specify the instances as follows:
const suite = new CipherSuite({
kem: new DhkemX25519HkdfSha256(),
kdf: new HkdfSha256(),
aead: new Aes128Gcm(),
});
const rkp = await suite.kem.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});
// A JWK-formatted recipient public key can also be used.
// const jwkPkR = {
// kty: "EC",
// crv: "P-256",
// kid: "P-256-01",
// x: "-eZXC6nV-xgthy8zZMCN8pcYSeE2XfWWqckA2fsxHPc",
// y: "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI",
// key_ops: [],
// };
// const pkR = await suite.kem.importKey("jwk", jwkPkR, true);
// const sender = await suite.createSenderContext({
// recipientPublicKey: pkR,
// });
// encrypt
const ct = await sender.seal(new TextEncoder().encode("Hello world!"));
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
});
// A JWK-formatted recipient private key can also be used.
// const jwkSkR = {
// kty: "EC",
// crv: "P-256",
// kid: "P-256-01",
// x: "-eZXC6nV-xgthy8zZMCN8pcYSeE2XfWWqckA2fsxHPc",
// y: "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI",
// d: "kwibx3gas6Kz1V2fyQHKSnr-ybflddSjN0eOnbmLmyo",
// key_ops: ["deriveBits"],
// };
// const skR = await suite.kem.importKey("jwk", jwkSkR, false);
// const recipient = await suite.createRecipientContext({
// recipientKey: skR,
// enc: sender.enc,
// });
// decrypt
const pt = await recipient.open(ct);
// Hello world!
console.log(new TextDecoder().decode(pt));
}
try {
doHpke();
} catch (_err: unknown) {
console.log("failed.");
}
Browsers:
<html>
<head></head>
<body>
<script type="module">
import { AeadId, CipherSuite, KdfId, KemId } from "https://esm.sh/hpke-js@1.2.1";
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core@1.2.1";
globalThis.doHpke = async () => {
try {
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm
});
const rkp = await suite.kem.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("Hello world!"));
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey, // rkp (CryptoKeyPair) is also acceptable.
enc: sender.enc,
});
// decrypt
const pt = await recipient.open(ct);
// Hello world!
alert(new TextDecoder().decode(pt));
} catch (err) {
alert("failed:", err.message);
}
}
</script>
<button type="button" onclick="doHpke()">do HPKE</button>
</body>
</html>
Base mode with Single-Shot APIs
Node.js:
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";
// const { AeadId, CipherSuite, KdfId, KemId } = require("hpke-js");
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core";
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm
});
const rkp = await suite.kem.generateKeyPair();
const pt = new TextEncoder().encode('Hello world!'),
// encrypt
const { ct, enc } = await suite.seal({ recipientPublicKey: rkp.publicKey }, pt);
// decrypt
const pt = await suite.open({ recipientKey: rkp.privateKey, enc: enc }, ct);
// Hello world!
console.log(new TextDecoder().decode(pt));
}
try {
doHpke();
} catch (err) {
console.log("failed:", err.message);
}
Base mode with export-only AEAD
Node.js:
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";
// const { AeadId, CipherSuite, KdfId, KemId } = require("hpke-js");
// import {
// CipherSuite, DhkemP256HkdfSha256, ExportOnly, HkdfSha256,
// } from "@hpke/core";
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.ExportOnly,
});
// When using "@hpke/core",
const suite = new CipherSuite({
kem: new DhkemP256HkdfSha256(),
kdf: new HkdfSha256(),
aead: new ExportOnly(),
});
const rkp = await suite.kem.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
});
const te = new TextEncoder();
// export
const pskS = sender.export(te.encode("jugemujugemu"), 32);
const pskR = recipient.export(te.encode("jugemujugemu"), 32);
// pskR === pskS
}
try {
doHpke();
} catch (err) {
console.log("failed:", err.message);
}
PSK mode
Node.js:
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";
// const { AeadId, CipherSuite, KdfId, KemId } = require("hpke-js");
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core";
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.kem.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
psk: {
id: new TextEncoder().encode("our-pre-shared-key-id"),
// a PSK MUST have at least 32 bytes.
key: new TextEncoder().encode("jugemujugemugokounosurikirekaija"),
},
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("Hello world!"));
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
psk: {
id: new TextEncoder().encode("our-pre-shared-key-id"),
// a PSK MUST have at least 32 bytes.
key: new TextEncoder().encode("jugemujugemugokounosurikirekaija"),
},
});
// decrypt
const pt = await recipient.open(ct);
// Hello world!
console.log(new TextDecoder().decode(pt));
}
try {
doHpke();
} catch (err) {
console.log("failed:", err.message);
}
Auth mode
Node.js:
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";
// const { AeadId, CipherSuite, KdfId, KemId } = require("hpke-js");
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core";
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.kem.generateKeyPair();
const skp = await suite.kem.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
senderKey: skp,
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("Hello world!"));
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
senderPublicKey: skp.publicKey,
});
// decrypt
const pt = await recipient.open(ct);
// Hello world!
console.log(new TextDecoder().decode(pt));
}
try {
doHpke();
} catch (err) {
console.log("failed:", err.message);
}
AuthPSK mode
Node.js:
import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";
// const { AeadId, CipherSuite, KdfId, KemId } = require("hpke-js");
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core";
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.kem.generateKeyPair();
const skp = await suite.kem.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
senderKey: skp,
psk: {
id: new TextEncoder().encode("our-pre-shared-key-id"),
// a PSK MUST have at least 32 bytes.
key: new TextEncoder().encode("jugemujugemugokounosurikirekaija"),
},
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("Hello world!"));
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
senderPublicKey: skp.publicKey,
psk: {
id: new TextEncoder().encode("our-pre-shared-key-id"),
// a PSK MUST have at least 32 bytes.
key: new TextEncoder().encode("jugemujugemugokounosurikirekaija"),
},
});
// decrypt
const pt = await recipient.open(ct);
// Hello world!
console.log(new TextDecoder().decode(pt));
}
try {
doHpke();
} catch (err) {
console.log("failed:", err.message);
}
Contributing
We welcome all kind of contributions, filing issues, suggesting new features or sending PRs.