use hpke::{
Deserializable, Kem as KemTrait, OpModeR, OpModeS, Serializable, aead::ChaCha20Poly1305,
kdf::HkdfSha256, kem::X25519HkdfSha256, single_shot_open, single_shot_seal,
};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use super::error::SealedTransferError;
const HPKE_INFO: &[u8] = b"vta-sealed-transfer/v1";
type Aead = ChaCha20Poly1305;
type Kdf = HkdfSha256;
type Kem = X25519HkdfSha256;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HpkeSealed {
pub kem_encap: [u8; 32],
pub aead_ciphertext: Vec<u8>,
}
pub fn seal(
recipient_pubkey: &[u8; 32],
plaintext: &[u8],
aad: &[u8],
) -> Result<HpkeSealed, SealedTransferError> {
let pk = <Kem as KemTrait>::PublicKey::from_bytes(recipient_pubkey)
.map_err(SealedTransferError::hpke)?;
let (encap, ciphertext) =
single_shot_seal::<Aead, Kdf, Kem>(&OpModeS::Base, &pk, HPKE_INFO, plaintext, aad)
.map_err(SealedTransferError::hpke)?;
let encap_bytes = encap.to_bytes();
let kem_encap: [u8; 32] = encap_bytes
.as_slice()
.try_into()
.map_err(|_| SealedTransferError::Hpke("KEM encap not 32 bytes".into()))?;
Ok(HpkeSealed {
kem_encap,
aead_ciphertext: ciphertext,
})
}
pub fn open(
recipient_secret: &[u8; 32],
sealed: &HpkeSealed,
aad: &[u8],
) -> Result<Vec<u8>, SealedTransferError> {
let sk = <Kem as KemTrait>::PrivateKey::from_bytes(recipient_secret)
.map_err(SealedTransferError::hpke)?;
let encap = <Kem as KemTrait>::EncappedKey::from_bytes(&sealed.kem_encap)
.map_err(SealedTransferError::hpke)?;
single_shot_open::<Aead, Kdf, Kem>(
&OpModeR::Base,
&sk,
&encap,
HPKE_INFO,
&sealed.aead_ciphertext,
aad,
)
.map_err(SealedTransferError::hpke)
}
pub fn generate_keypair() -> (Zeroizing<[u8; 32]>, [u8; 32]) {
let (sk, pk) = <Kem as KemTrait>::gen_keypair();
let sk_bytes: [u8; 32] = sk
.to_bytes()
.as_slice()
.try_into()
.expect("X25519 secret is 32 bytes");
let pk_bytes: [u8; 32] = pk
.to_bytes()
.as_slice()
.try_into()
.expect("X25519 public is 32 bytes");
(Zeroizing::new(sk_bytes), pk_bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seal_open_round_trip() {
let (sk, pk) = generate_keypair();
let plaintext = b"the quick brown fox";
let aad = b"binding context";
let sealed = seal(&pk, plaintext, aad).expect("seal");
let opened = open(&sk, &sealed, aad).expect("open");
assert_eq!(opened, plaintext);
}
#[test]
fn open_with_wrong_recipient_secret_rejected() {
let (_sk_intended, pk_intended) = generate_keypair();
let (sk_attacker, _pk_attacker) = generate_keypair();
let sealed = seal(&pk_intended, b"secret content", b"aad").expect("seal");
let err = open(&sk_attacker, &sealed, b"aad")
.expect_err("opening with the wrong X25519 secret must fail");
assert!(
matches!(err, SealedTransferError::Hpke(_)),
"expected Hpke variant, got {err:?}"
);
}
#[test]
fn open_with_wrong_aad_rejected() {
let (sk, pk) = generate_keypair();
let sealed = seal(&pk, b"payload", b"correct aad").expect("seal");
let err = open(&sk, &sealed, b"different aad")
.expect_err("AAD mismatch must fail authentication");
assert!(matches!(err, SealedTransferError::Hpke(_)), "got {err:?}");
}
#[test]
fn open_with_tampered_kem_encap_rejected() {
let (sk, pk) = generate_keypair();
let mut sealed = seal(&pk, b"payload", b"aad").expect("seal");
sealed.kem_encap[0] ^= 0x01;
let err = open(&sk, &sealed, b"aad").expect_err("KEM-encap tampering must fail to open");
assert!(matches!(err, SealedTransferError::Hpke(_)), "got {err:?}");
}
}