Skip to main content

Built and signed on GitHub Actions

A Hybrid Public Key Encryption (HPKE) module for various JavaScript runtimes.

This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
This package works with Cloudflare Workers
This package works with Node.js
This package works with Deno
This package works with Bun
This package works with Browsers
JSR Score
100%
Published
2 days ago (1.5.0)

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.

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

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

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

Built and signed on
GitHub Actions
View transparency log

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";