Implementing HPKE in JavaScript: From Prototype to 1.0 #20
panva
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
If you've ever needed to encrypt data for a recipient using only their public key, you've probably encountered the awkward dance of asymmetric cryptography: RSA key sizes, padding schemes, hybrid encryption bolted together from separate primitives. Hybrid Public Key Encryption (HPKE), standardized in RFC 9180, addresses this by combining key encapsulation with authenticated encryption into a single, well-designed scheme. The specification is also being re-published in the IETF Standards Track and extended with post-quantum (PQ) and hybrid post-quantum/traditional (PQ/T) KEM constructions, thus future-proofing it against quantum computing threats.
This is the story of building a JavaScript HPKE implementation. Three weeks of burning the midnight oil to find the right abstractions, followed by two and a half weeks of public development from npm placeholder to stable v1.0.0.
HPKE in Brief
HPKE answers a deceptively simple question: How do I encrypt a message for someone when I only have their public key?
The scheme combines three cryptographic building blocks:
the recipient recover it
The sender encapsulates a shared secret to the recipient's public key, derives a symmetric encryption key, and encrypts the payload. The recipient decapsulates using their private key, derives the same symmetric key and decrypts. Clean, composable, and secure.
HPKE is already being adopted in TLS Encrypted Client Hello (ECH), the Messaging Layer Security (MLS) protocol, and OHTTP (Oblivious HTTP). Both JOSE (JWE) and COSE are also getting HPKE algorithm identifiers, which will bring post-quantum key exchange to the widely-deployed JWT and COSE ecosystems. If you're building anything involving end-to-end encryption, HPKE should be on your radar.
Three Weeks of Private Iteration
Before any code went public, there were three weeks of experimentation. The challenge wasn't implementing HPKE's cryptographic operations (RFC 9180 specifies those clearly). The challenge was designing an API that was:
Multiple approaches were prototyped and discarded. Class hierarchies that became unwieldy. Configuration objects that were too verbose. What worked was the factory pattern: each algorithm as a function that returns an implementation object. This enabled tree-shaking while keeping the
CipherSuiteconstructor clean and composable.Browser Reality Checks
Web Cryptography implementations vary across browsers. Firefox and Safari handle EC PKCS#8 key formats differently than Chrome and Node.js. Unfortunately, fixing this incompatibility required implementing elliptic curve scalar multiplication. That's not something I expected to have to do for a library that aims to rely solely on platform primitives. Firefox is already working to fix their implementation, and I'd be much happier once Safari follows suit.
"Works in Chrome" is not the same as "works in browsers."
The Buffer.slice Trap
In standard JavaScript,
Uint8Array.prototype.slice()creates a copy. But Node.js'sBuffer.prototype.slice()returns a view into the same memory. This legacy deprecated behavior can cause unexpected mutations. The fix was explicit: always useUint8Array.prototype.slice.call()to ensure copy semantics.Node.js's
Bufferhas surprising behaviors. When writing cross-runtime code, be explicit.Test Vectors Help a Ton
One thing that made this implementation possible on such a tight timeline: comprehensive test vectors. Both the main HPKE specification and the post-quantum draft include extensive test vectors covering a wide range of algorithm combinations.
When you're implementing cryptography, "it encrypts and decrypts" isn't enough. You need to verify that your implementation produces exactly the same intermediate values and outputs as other implementations.
Running against these vectors caught subtle bugs that would never have surfaced in basic round-trip testing. If you're implementing any IETF specification, seek out the test vectors first.
Design Decisions That Shaped the Library
Zero Dependencies
The library has no runtime dependencies. Every built-in algorithm uses Web Cryptography (
crypto.subtle), which is available in Node.js, Deno, Bun, browsers, Cloudflare Workers, and other Web-interoperable runtimes.This isn't just about keeping
node_modulesslim. It's about trust. Cryptographic code with fewer dependencies has a smaller attack surface. I'm not a cryptographer, and I don't aspire to be one. Relying exclusively on platform-provided primitives shifts the responsibility for correctness to the runtime implementers who are under a lot more scrutiny to provide cryptographically safe implementations.Factory Pattern for Tree-Shaking
Every algorithm is a factory function:
Only the algorithms you import get bundled. If you only need P-256 with AES-GCM, you don't pay for X25519 or ChaCha20-Poly1305.
What the Library Offers Today
After a few intermediate releases, v1.0.0 finally shipped with:
The API offers both single-shot functions (encrypt one message) and context-based APIs (encrypt multiple messages efficiently with the same keys).
The
hpkepackage is available on npmjs.com, jsdelivr.com, and github.com. If you're building anything that needs public key encryption in JavaScript, give it a spin.Acknowledgements
Thanks to Orie Steele for test driving the implementation during the private iteration phase and providing valuable feedback. Thanks to the folks at Divvi Up for freeing up the
hpkepackage name, and to Deb Cooley and Mike Jones for aiding the introductions that led to the npm package name transition.The library is MIT licensed and open source at github.com/panva/hpke. Contributions and feedback welcome.
Beta Was this translation helpful? Give feedback.
All reactions