hpke-js

hpke-js

deno doc Browser CI Node.js CI Deno CI Cloudflare Workers CI bun CI codecov

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

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, use hpke-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

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.7/mod.ts";
// import * as hpke from "https://deno.land/x/hpke@1.2.7/core/mod.ts";
// import * as hpke from "https://deno.land/x/hpke@1.2.7/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.7";
// import * as hpke from "https://esm.sh/@hpke/core@1.2.7";
// ...
</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.7/esm/mod.js";
// import * as hpke from "https://unpkg.com/@hpke/core@1.2.7/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.7/mod.ts";
// import {
// Aes128Gcm, CipherSuite, HkdfSha256,
// } from "https://deno.land/x/hpke@1.2.7/core/mod.ts";
// import { DhkemX25519HkdfSha256 } from "https://deno.land/x/hpke@1.2.7/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.7";
// import {
// Aes128Gcm, CipherSuite, DhkemP256HkdfSha256, HkdfSha256,
// } from "@hpke/core@1.2.7";

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.

References

Generated using TypeDoc