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 |
|
---|---|---|---|---|---|
DHKEM (P-256, HKDF-SHA256) | ✅ | ✅ v16.x- |
✅ v1.23.x- |
||
DHKEM (P-384, HKDF-SHA384) | ✅ | ✅ v16.x- |
|||
DHKEM (P-521, HKDF-SHA512) | ✅ | ✅ v16.x- |
|||
DHKEM (X25519, HKDF-SHA256) | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | @stablelib/x25519 is used until Secure Curves is implemented. |
DHKEM (X448, HKDF-SHA512) | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | x448-js is used until Secure Curves is implemented. |
DHKEM (secp256k1, HKDF-SHA256) | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | NOT STANDARDIZED EXPERIMENTAL IMPLEMENTATION using elliptic. |
Key Derivation Functions (KDFs)
KDFs | Browser | Node.js | Deno | Cloudflare Workers |
|
---|---|---|---|---|---|
HKDF-SHA256 | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | |
HKDF-SHA384 | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | |
HKDF-SHA512 | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ |
Authenticated Encryption with Associated Data (AEAD) Functions
AEADs | Browser | Node.js | Deno | Cloudflare Workers |
|
---|---|---|---|---|---|
AES-128-GCM | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | |
AES-256-GCM | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | |
ChaCha20Poly1305 | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ | @stablelib/chacha20poly1305 is used. |
Export Only | ✅ | ✅ v16.x- |
✅ v1.15.x- |
✅ |
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
- Deno: 1.x (1.15-)
- Cloudflare Workers
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.17.1";
// ...
</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.17.1/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@v0.17.1/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.17.1/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.17.1/es2022/hpke.min.js
Emits a single js file by using deno bundle
:
deno bundle https://deno.land/x/hpke@v0.17.1/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.17.1";
import { Kem, Kdf, Aead, CipherSuite } from "https://esm.sh/hpke-js@0.17.1";
globalThis.doHpke = async () => {
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.Aes128Gcm
});
const rkp = await suite.generateKeyPair();
const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey
});
const recipient = await suite.createRecipientContext({
recipientKey: rkp.privateKey, // rkp (CryptoKeyPair) is also acceptable.
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 { Kem, Kdf, Aead, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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 { Kem, Kdf, Aead, CipherSuite } from "https://deno.land/x/hpke@v0.17.1/mod.ts";
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemX25519HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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 { Kem, Kdf, Aead, CipherSuite } = require('hpke-js');
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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 { Kem, Kdf, Aead, CipherSuite } = require("hpke-js");
const te = new TextEncoder();
const td = new TextDecoder();
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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 { Kem, Kdf, Aead, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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 { Kem, Kdf, Aead, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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 { Kem, Kdf, Aead, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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 { Kem, Kdf, Aead, CipherSuite } = require("hpke-js");
async function doHpke() {
// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.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.