@lib-q/hpke
v0.0.6
Published
Post-quantum HPKE (RFC 9180) for Node.js
Maintainers
Readme
lib-q-hpke
RFC 9180–aligned Hybrid Public Key Encryption using post-quantum-only primitives (ML-KEM family for the HPKE KEM role in the default provider path; no classical KEM or classical signatures in that path).
Overview
lib-q-hpke implements HPKE for the lib-q stack: protocol logic and types in this crate, with KEM/AEAD/hash work delegated through lib-q-kem, lib-q-aead, and lib-q-hash. The high-level HpkeContext holds a lib_q_core::CryptoProvider for KemContext and an Arc<dyn HpkeCryptoProvider + Send + Sync> (default PostQuantumProvider) for encapsulation, KDF, AEAD, and exporter operations. Use HpkeContext::with_hpke_crypto to swap the HPKE backend; with_provider only replaces the inner KemContext crypto. Today the default provider wires ML-KEM only for HPKE KEM (other PQ KEMs may exist in the workspace but are not in the HpkeKem catalog yet).
Interoperability
Profiles, a mode×suite matrix, and fixture provenance are documented under the workspace interoperability.md and hpke-architecture.md. For code, see lib_q_hpke::interop. Run the integrator-oriented example (requires std):
cargo run -p lib-q-hpke --example hpke_interop_negotiation --features std
Supported Algorithms
Key Encapsulation Mechanisms (KEM)
- ML-KEM-512 (Level 1 security)
- ML-KEM-768 (Level 3 security)
- ML-KEM-1024 (Level 5 security)
Key Derivation Functions (KDF)
- HKDF-SHAKE128
- HKDF-SHAKE256
- HKDF-SHA3-256
- HKDF-SHA3-512
Authenticated Encryption (AEAD)
- Saturnin-256 (32-byte key, 16-byte nonce, 32-byte tag)
- SHAKE256-based AEAD (16-byte tag)
- Keccak duplex-sponge AEAD via
lib-q-aead— enable Cargo featureduplex-sponge-aeadon this crate (when using the umbrellalib-qcrate, enablehpke-duplex-aead) - Export-only mode (
HpkeAead::Export) for exporter-secret usage without message encryption
Quick Start
use lib_q_core::{Algorithm, KemContext, KemPublicKey, KemSecretKey};
use lib_q_hpke::HpkeContext;
use libq::LibQCryptoProvider;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create HPKE context (umbrella `lib-q` crate, Rust name `libq`)
let provider = Box::new(LibQCryptoProvider::new()?);
let mut hpke_ctx = HpkeContext::with_provider(provider);
// Generate key pair
let mut kem_ctx = KemContext::with_provider(Box::new(LibQCryptoProvider::new()?));
let keypair = kem_ctx.generate_keypair(Algorithm::MlKem512)?;
let recipient_pk = KemPublicKey::new(keypair.public_key().as_bytes().to_vec());
let recipient_sk = KemSecretKey::new(keypair.secret_key().as_bytes().to_vec());
// Encrypt message
let message = b"Hello, HPKE!";
let (encapsulated_key, ciphertext) = hpke_ctx.seal(
&recipient_pk,
b"application-info",
b"additional-data",
message,
)?;
// Decrypt message
let decrypted = hpke_ctx.open(
&encapsulated_key,
&recipient_sk,
b"application-info",
b"additional-data",
&ciphertext,
)?;
assert_eq!(decrypted, message);
Ok(())
}HPKE Modes
Base Mode
Standard HPKE without additional authentication.
let mut sender_ctx = hpke_ctx.setup_sender(&recipient_pk, b"session-info")?;
let ciphertext = sender_ctx.seal(b"aad", message)?;PSK Mode
Pre-shared key authentication.
let psk = b"shared-secret-key";
let psk_id = b"psk-identifier";
let mut sender_ctx = hpke_ctx.setup_sender_psk(
&recipient_pk,
b"session-info",
psk,
psk_id,
)?;Auth Mode
Sender authentication using asymmetric keys.
let mut sender_ctx = hpke_ctx.setup_sender_auth(
&recipient_pk,
b"session-info",
&sender_sk,
&sender_pk,
)?;AuthPSK Mode
Combined PSK and sender authentication.
let mut sender_ctx = hpke_ctx.setup_sender_auth_psk(
&recipient_pk,
b"session-info",
psk,
psk_id,
&sender_sk,
&sender_pk,
)?;PSK / AuthPSK wire format
For PSK and AuthPSK modes, the default is HpkePskWireFormat::Rfc9180 (RFC 9180 on-the-wire layout). Both peers may set HpkePskWireFormat::LibQCommitmentSuffix with HpkeContext::set_psk_wire_format to reject wrong (psk, psk_id) or a mismatched primary KEM ciphertext before decapsulation; that suffix is not interoperable with strict third-party RFC 9180 implementations.
Cargo features (summary)
| Feature | Purpose |
|---------|---------|
| std | Standard library support |
| alloc | Required for normal operation (enforced by the crate) |
| ml-kem | ML-KEM through lib-q-kem (default) |
| hash | HKDF hash backends via lib-q-hash (default) |
| saturnin / shake256 | AEAD algorithms (defaults) |
| duplex-sponge-aead | Duplex-sponge AEAD in HpkeAead::DuplexSpongeAead |
| wasm | wasm32 bindings and helpers |
| secure-rng | OS-backed RNG where applicable (default) |
See Cargo.toml for the full feature matrix and optional dev dependencies.
Documentation
- Architecture overview — workspace-level HPKE design (PQ-only path, PSK wire format, interoperability)
- API Reference — public API
- Security Considerations — security analysis and best practices
- Architecture details — module layout and provider wiring in this crate
Testing
The implementation includes comprehensive test coverage:
# Run all tests
cargo test
# Run specific test suites
cargo test --test psk_mode_comprehensive_tests
cargo test --test authpsk_mode_comprehensive_tests
cargo test --test security_validation_comprehensive_testsWire sizes (KEM)
Public key and encapsulated ciphertext lengths match HpkeKem (public_key_len / enc_len):
| KEM | NIST category (approx.) | Public key | Encapsulated ciphertext | |-----|-------------------------|------------|-------------------------| | ML-KEM-512 | 1 | 800 B | 768 B | | ML-KEM-768 | 3 | 1184 B | 1088 B | | ML-KEM-1024 | 5 | 1568 B | 1568 B |
AEAD ciphertext expansion is plaintext length plus the AEAD tag (e.g. 32 bytes for Saturnin-256); see HpkeAead::tag_len in src/types.rs.
Dependencies
lib-q-core- Core cryptographic types and interfaceslib-q-kem- Key encapsulation mechanism implementationslib-q-hash- Hash function implementationslib-q-aead- Authenticated encryption implementationslib-q(Rust importlibq) —LibQCryptoProviderfor demos; production integrations may use narrower providers
License
This project is licensed under the same terms as the lib-q project.
Subresource integrity (SHA-384)
Paths in integrity-manifest.json are relative to the package root (including web/ and nodejs/ when both ship).
