Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
A Hybrid Public Key Encryption (HPKE) module for various JavaScript runtimes.
hpke-js
Documentation: jsr.io | 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 { Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256, } from "@hpke/core"; async function doHpke() { const suite = new CipherSuite({ kem: new DhkemP256HkdfSha256(), kdf: new HkdfSha256(), aead: new Aes128Gcm(), }); 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); // The sender encrypts a message. const sender = await suite.createSenderContext({ recipientPublicKey: pkR, }); const ct = await sender.seal(new TextEncoder().encode("Hello world!")); 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); // The recipient decrypts it. const recipient = await suite.createRecipientContext({ recipientKey: skR, 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.6.0-), 1.x
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
Starting from version 1.3.0, hpke-js packages are available from the JSR registry. From this version onwards, please use JSR import instead of HTTPS import in Deno.
JSR imoprt (recommended on >=1.3.0
):
Add hpke-js packages using the commands below:
deno add @hpke/hpke-js
Then, you can use the module from code like this:
import { CipherSuite, DhkemP256HkdfSha256, HkdfSha256 } from "@hpke/core"; import { Chacha20Poly1305 } from "@hpke/chacha20poly1305";
HTTPS imoprt (deprecated):
import { CipherSuite, DhkemP256HkdfSha256, HkdfSha256, } from "https://deno.land/x/hpke/core/mod.ts"; import { Chacha20Poly1305 } from "https://deno.land/x/hpke/x/chacha20poly1305/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@<SEMVER>"; // import * as hpke from "https://esm.sh/@hpke/core@<SEMVER>"; // ... </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@<SEMVER>/esm/mod.js"; // import * as hpke from "https://unpkg.com/@hpke/core@<SEMVER>/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() { // When using "hpke-js": // const suite = new CipherSuite({ // kem: KemId.DhkemP256HkdfSha256, // kdf: KdfId.HkdfSha256, // aead: AeadId.Aes128Gcm, // }); // When using "@hpke/core": 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("decrypted: ", new TextDecoder().decode(pt)); } try { doHpke(); } catch (e) { console.log("failed:", e.message); }
Deno:
import { AeadId, CipherSuite, KdfId, KemId } from "@hpke/hpke-js"; async function doHpke() { // When using "@hpke/hpke-js", you can specify the identifier as follows: const suite = new CipherSuite({ kem: KemId.DhkemX25519HkdfSha256, kdf: KdfId.HkdfSha256, aead: AeadId.Aes128Gcm, }); // When using "@hpke/core" and @hpke/dhkem-x25519, 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@<SEMVER>"; // import { // Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256, // } from "https://esm.sh/@hpke/core@<SEMVER>"; 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
Deno:
import { AeadId, CipherSuite, KdfId, KemId } from "@hpke/hpke-js"; async function doHpke() { const suite = new CipherSuite({ kem: KemId.DhkemP256HkdfSha256, kdf: KdfId.HkdfSha256, aead: AeadId.Aes128Gcm, }); const rkp = await suite.kem.generateKeyPair(); const pt: ArrayBuffer = new TextEncoder().encode("Hello world!"); // encrypt const { ct, enc } = await suite.seal( { recipientPublicKey: rkp.publicKey }, pt, ); // decrypt const dt = await suite.open({ recipientKey: rkp.privateKey, enc: enc }, ct); // Hello world! console.log(new TextDecoder().decode(dt)); } try { doHpke(); } catch (err: unknown) { console.log("failed:", (err as Error).message); }
Base mode with export-only AEAD
Deno:
import { AeadId, CipherSuite, KdfId, KemId } from "@hpke/hpke-js"; async function doHpke() { // When using "hpke-js": const suite = new CipherSuite({ kem: KemId.DhkemP256HkdfSha256, kdf: KdfId.HkdfSha256, aead: AeadId.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: unknown) { console.log("failed:", (err as Error).message); }
PSK mode
Deno:
import { AeadId, CipherSuite, KdfId, KemId } from "@hpke/hpke-js"; async function doHpke() { 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: unknown) { console.log("failed:", (err as Error).message); }
Auth mode
Deno:
import { AeadId, CipherSuite, KdfId, KemId } from "@hpke/hpke-js"; async function doHpke() { 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: unknown) { console.log("failed:", (err as Error).message); }
AuthPSK mode
Deno:
import { AeadId, CipherSuite, KdfId, KemId } from "@hpke/hpke-js"; async function doHpke() { 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: unknown) { console.log("failed:", (err as Error).message); }
Contributing
We welcome all kind of contributions, filing issues, suggesting new features or sending PRs.
References
Add Package
deno add jsr:@hpke/hpke-js
Import symbol
import * as mod from "@hpke/hpke-js";
---- OR ----
Import directly with a jsr specifier
import * as mod from "jsr:@hpke/hpke-js";
Add Package
npx jsr add @hpke/hpke-js
Import symbol
import * as mod from "@hpke/hpke-js";
Add Package
yarn dlx jsr add @hpke/hpke-js
Import symbol
import * as mod from "@hpke/hpke-js";
Add Package
pnpm dlx jsr add @hpke/hpke-js
Import symbol
import * as mod from "@hpke/hpke-js";
Add Package
bunx jsr add @hpke/hpke-js
Import symbol
import * as mod from "@hpke/hpke-js";