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 Cloudflare Workers.
Index
- 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 bidirectional encryption
- Base mode with export-only AEAD
- PSK mode
- Auth mode
- AuthPSK mode
- Contributing
- References
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) | ✅ |
✅ v16.x- |
✅ v1.23.x- |
✅ |
✅ 0.3.0- |
DHKEM (P-384, HKDF-SHA384) | ✅ |
✅ v16.x- |
✅ v1.25.x- |
✅ |
✅ 0.3.0- |
DHKEM (P-521, HKDF-SHA512) | ✅ |
✅ v16.x- |
✅ |
✅ 0.3.0- |
|
DHKEM (X25519, HKDF-SHA256) | ✅*1 |
✅*1 |
✅*1 |
✅*1 |
✅*1 |
DHKEM (X448, HKDF-SHA512) | ✅*2 |
✅*2 |
✅*2 |
✅*2 |
✅*2 |
DHKEM (secp256k1, HKDF-SHA256) | ✅*3 |
✅*3 |
✅*3 |
✅*3 |
✅*3 |
- *1: @noble/curves/ed25519 is used until Secure Curves is implemented.
- *2: @noble/curves/ed448 is used until Secure Curves is implemented.
- *3: NOT STANDARDIZED EXPERIMENTAL IMPLEMENTATION using @noble/curves/secp256k1.
Key Derivation Functions (KDFs)
KDFs | Browser | Node.js | Deno | Cloudflare Workers |
bun |
---|---|---|---|---|---|
HKDF-SHA256 | ✅ |
✅ v16.x- |
✅ v1.15.x- |
✅ |
✅ 0.3.0- |
HKDF-SHA384 | ✅ |
✅ v16.x- |
✅ v1.15.x- |
✅ |
✅ 0.3.0- |
HKDF-SHA512 | ✅ |
✅ v16.x- |
✅ v1.15.x- |
✅ |
✅ 0.3.0- |
Authenticated Encryption with Associated Data (AEAD) Functions
AEADs | Browser | Node.js | Deno | Cloudflare Workers |
bun |
---|---|---|---|---|---|
AES-128-GCM | ✅ |
✅ v16.x- |
✅ v1.15.x- |
✅ |
✅ 0.3.0- |
AES-256-GCM | ✅ |
✅ v16.x- |
✅ v1.15.x- |
✅ |
✅ 0.3.0- |
ChaCha20Poly1305 | ✅*4 |
✅*4 |
✅*4 |
✅*4 |
✅*4 |
Export Only | ✅ |
✅ |
✅ |
✅ |
✅ |
- *4: @noble/ciphers/chacha is used.
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.15-)
- Cloudflare Workers
- bun: 0.x (0.3.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
Web Browser
Followings are how to use 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@0.21.0";
// ...
</script>
<!-- use the latest stable version -->
<script type="module">
import * as hpke from "https://esm.sh/hpke-js";
// ...
</script>
Using unpkg:
<!-- use a specific version -->
<script type="module">
import * as hpke from "https://unpkg.com/hpke-js@0.21.0/esm/mod.js";
// ...
</script>
Node.js
Using npm:
npm install hpke-js
Using yarn:
yarn add hpke-js
Deno
Using deno.land:
// use a specific version
import * as hpke from "https://deno.land/x/hpke@0.21.0/mod.ts";
// use the latest stable version
import * as hpke from "https://deno.land/x/hpke/mod.ts";
Cloudflare Workers
Downloads a single js file from esm.sh:
curl -sS -o $YOUR_SRC_PATH/hpke.js https://esm.sh/v86/hpke-js@0.21.0/es2022/hpke-js.js
# if you want to use a minified version:
curl -sS -o $YOUR_SRC_PATH/hpke.min.js https://esm.sh/v86/hpke-js@0.21.0/es2022/hpke.min.js
Emits a single js file by using deno bundle
:
deno bundle https://deno.land/x/hpke@0.21.0/mod.ts > $YOUR_SRC_PATH/hpke.js
Usage
This section shows some typical usage examples.
Base mode
Browsers:
<html>
<head></head>
<body>
<script type="module">
// import * as hpke from "https://esm.sh/hpke-js@0.21.0";
import { KemId, KdfId, AeadId, CipherSuite } from "https://esm.sh/hpke-js@0.21.0";
globalThis.doHpke = async () => {
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm
});
const rkp = await suite.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.importKey("jwk", jwkPkR, true);
// const sender = await suite.createSenderContext({
// recipientPublicKey: pkR,
// });
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey, // rkp (CryptoKeyPair) is also acceptable.
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.importKey("jwk", jwkSkR, false);
// const recipient = await suite.createRecipientContext({
// recipientKey: skR,
// enc: sender.enc,
// });
// encrypt
const ct = await sender.seal(new TextEncoder().encode("hello world!"));
// decrypt
try {
const pt = await recipient.open(ct);
// hello world!
alert(new TextDecoder().decode(pt));
} catch (err) {
alert("failed to decrypt.");
}
}
</script>
<button type="button" onclick="doHpke()">do HPKE</button>
</body>
</html>
Node.js:
const { KemId, KdfId, AeadId, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("my-secret-message"));
// decrypt
try {
const pt = await recipient.open(ct);
console.log("decrypted: ", new TextDecoder().decode(pt));
// decrypted: my-secret-message
} catch (err) {
console.log("failed to decrypt.");
}
}
doHpke();
Deno:
import { KemId, KdfId, AeadId, CipherSuite } from "https://deno.land/x/hpke@0.21.0/mod.ts";
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemX25519HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("my-secret-message"));
try {
// decrypt
const pt = await recipient.open(ct);
console.log("decrypted: ", new TextDecoder().decode(pt));
// decrypted: my-secret-message
} catch (_err: unknown) {
console.log("failed to decrypt.");
}
}
doHpke();
Base mode with Single-Shot APIs
Node.js:
const { KemId, KdfId, AeadId, CipherSuite } = require('hpke-js');
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm
});
const rkp = await suite.generateKeyPair();
const pt = new TextEncoder().encode('my-secret-message'),
// encrypt
const { ct, enc } = await suite.seal({ recipientPublicKey: rkp.publicKey }, pt);
// decrypt
try {
const pt = await suite.open({ recipientKey: rkp.privateKey, enc: enc }, ct);
console.log('decrypted: ', new TextDecoder().decode(pt));
// decrypted: my-secret-message
} catch (err) {
console.log("failed to decrypt.");
}
}
doHpke();
Base mode with bidirectional encryption
Node.js:
const { KemId, KdfId, AeadId, CipherSuite } = require("hpke-js");
const te = new TextEncoder();
const td = new TextDecoder();
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
});
// setup bidirectional encryption
await sender.setupBidirectional(
te.encode("seed-for-key"),
te.encode("seed-for-nonce"),
);
await recipient.setupBidirectional(
te.encode("seed-for-key"),
te.encode("seed-for-nonce"),
);
// encrypt
const ct = await sender.seal(te.encode("my-secret-message-s"));
// decrypt
try {
const pt = await recipient.open(ct);
console.log("recipient decrypted: ", td.decode(pt));
// decrypted: my-secret-message-s
} catch (err) {
console.log("failed to decrypt.");
}
// encrypt reversely
const rct = await recipient.seal(te.encode("my-secret-message-r"));
// decrypt reversely
try {
const rpt = await sender.open(rct);
console.log("sender decrypted: ", td.decode(rpt));
// decrypted: my-secret-message-r
} catch (err) {
console.log("failed to decrypt.");
}
}
doHpke();
Base mode with export-only AEAD
Node.js:
const { KemId, KdfId, AeadId, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.ExportOnly,
});
const rkp = await suite.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
}
doHpke();
PSK mode
Node.js:
const { KemId, KdfId, AeadId, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.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"),
},
});
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"),
},
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("my-secret-message"));
// decrypt
try {
const pt = await recipient.open(ct);
console.log("decrypted: ", new TextDecoder().decode(pt));
// decrypted: my-secret-message
} catch (err) {
console.log("failed to decrypt:", err.message);
}
}
doHpke();
Auth mode
Node.js:
const { KemId, KdfId, AeadId, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.generateKeyPair();
const skp = await suite.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
senderKey: skp,
});
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey,
enc: sender.enc,
senderPublicKey: skp.publicKey,
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("my-secret-message"));
try {
// decrypt
const pt = await recipient.open(ct);
console.log("decrypted: ", new TextDecoder().decode(pt));
// decrypted: my-secret-message
} catch (err) {
console.log("failed to decrypt:", err.message);
}
}
doHpke();
AuthPSK mode
Node.js:
const { KemId, KdfId, AeadId, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
});
const rkp = await suite.generateKeyPair();
const skp = await suite.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"),
},
});
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"),
},
});
// encrypt
const ct = await sender.seal(new TextEncoder().encode("my-secret-message"));
// decrypt
try {
const pt = await recipient.open(ct);
console.log("decrypted: ", new TextDecoder().decode(pt));
// decrypted: my-secret-message
} catch (err) {
console.log("failed to decrypt:", err.message);
}
}
doHpke();
Contributing
We welcome all kind of contributions, filing issues, suggesting new features or sending PRs.