Skip to content

Define an easy API#100

Open
rozbb wants to merge 2 commits intomainfrom
easy-mode
Open

Define an easy API#100
rozbb wants to merge 2 commits intomainfrom
easy-mode

Conversation

@rozbb
Copy link
Owner

@rozbb rozbb commented Feb 26, 2026

The existing interface is filled with code noise. Here is the current example in the crate root:

use hpke::{
    aead::ChaCha20Poly1305,
    kdf::HkdfSha384,
    kem::X25519HkdfSha256,
    Kem as KemTrait, OpModeR, OpModeS, setup_receiver, setup_sender,
};

type Kem = X25519HkdfSha256;
type Aead = ChaCha20Poly1305;
type Kdf = HkdfSha384;

// Bob generates keypair
let (bob_sk, bob_pk) = Kem::gen_keypair();

let info_str = b"Alice and Bob's weekly chat";

// Alice encrypts to Bob
let (encapsulated_key, mut encryption_context) =
    hpke::setup_sender::<Aead, Kdf, Kem>(&OpModeS::Base, &bob_pk, info_str)
        .expect("invalid server pubkey!");
let msg = b"fronthand or backhand?";
let aad = b"a gentleman's game";
let ciphertext = encryption_context.seal(msg, aad).expect("encryption failed!");

// Bob decrypts
let mut decryption_context =
    hpke::setup_receiver::<Aead, Kdf, Kem>(
        &OpModeR::Base,
        &bob_sk,
        &encapsulated_key,
        info_str,
    ).expect("failed to set up receiver!");
let plaintext = decryption_context.open(&ciphertext, aad).expect("invalid ciphertext!");

assert_eq!(&plaintext, b"fronthand or backhand?");

This PR defines a new module called easy that lets the common case usage (ie the example above) be less noisy. The new example code looks like this

// We will be using the "easy" interface for the ciphersuite
// (DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20Poly1305)
use hpke::easy::x25519_chacha::*;

// Bob generates keypair
let (bob_sk, bob_pk) = gen_keypair();

let info_str = b"Alice and Bob's weekly chat";

// Alice encrypts to Bob
let (encapsulated_key, mut encryption_context) =
    setup_sender(&bob_pk, info_str).expect("invalid server pubkey!");
let msg = b"fronthand or backhand?";
let aad = b"a gentleman's game";
let ciphertext = encryption_context.seal(msg, aad).expect("encryption failed!");

// Bob decrypts
let mut decryption_context =
    setup_receiver(&bob_sk, &encapsulated_key, info_str).expect("failed to set up receiver!");
let plaintext = decryption_context.open(&ciphertext, aad).expect("invalid ciphertext!");

assert_eq!(&plaintext, b"fronthand or backhand?");

I think this looks a lot better, and answers some complaints I've gotten over the years (not to mention explicitly here in #79). Any opinions?

One interesting point is: I can now just say there is "easy mode" (use this module), and "hard mode" (don't use this module). If that's the distinction, I think I'd be fine reverting the recent change that split every deterministic function into X and X_with_rng. Lower complexity to maintain.

cc @tarcieri @josh-brown-anchor @tgeoghegan @elichai

@tgeoghegan
Copy link
Contributor

I haven't reviewed the implementation or entire API surface, just commenting on the API illustrated in the snippet in the description.

I think defaulting to base mode is a good idea. I also think not making the user spell out the generic parameters for the crypto components is nice and makes the usage less noisy. But if we're talking about making the API easier to use:

What's difficult is that I have to know statically which KEM, AEAD and KDF I want to use. In my application (https://github.com/divviup/janus) I discover which components to use dynamically at runtime. Some clients want boring NIST curves and AES but others want cool kid curves and ChaCha. I have no way of knowing their preference at compile time so I can't just configure the appropriate specialization of things like setup_sender.

This might be exacerbated by setup_sender and setup_receiver being functions instead of structs. If instead they were some object of a type generic over KEM, AEAD and KDF, then at least we could define a trait HpkeDoer, implement that on the HPKE doer object, and then I could work with Box<dyn HpkeDoer> and it'd be a little easier. I'm not sure if that's a good idea.

Anyway, we wound up writing hpke_dispatch to make this less painful. Mind you, I'm not arguing against the changes you have right here. Just trying to share where we've had friction before.

@rozbb
Copy link
Owner Author

rozbb commented Feb 26, 2026

@tgeoghegan Ahh I forgot that was the pain point. hpke_dispatch looks great!

There’s a chance it makes sense to upstream this, but my main concern is complexity. I think having agile-HPKE as a separate crate makes a lot of sense actually. I wonder if you agree?

I am happy to devote time to maintaining hpke_dispatch. I’ll be linking it in the top-level docs, and I’ll also send an upgrade PR once v0.14 drops.

@tgeoghegan
Copy link
Contributor

I think having agile-HPKE as a separate crate makes a lot of sense actually. I wonder if you agree?

I can see merits either way but I'm comfortable continuing with the status quo. I think we'd be happy to collaborate on the next major version of hpke-dispatch. Let's talk about that over here to keep this issue focused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants