use ::hpke::{
aead::ChaCha20Poly1305 as AeadImpl, kdf::HkdfSha256 as KdfImpl, kem::X25519HkdfSha256 as KemImpl, Deserializable,
Kem as KemTrait, OpModeR, OpModeS, Serializable,
};
use rand_core::OsRng;
use zeroize::Zeroizing;
use super::error::CryptoError;
type Kem = KemImpl;
type Kdf = KdfImpl;
type Aead = AeadImpl;
#[derive(Debug, Clone)]
pub struct HpkePublicKey(Vec<u8>);
impl HpkePublicKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
<Kem as KemTrait>::PublicKey::from_bytes(bytes).map_err(|e| CryptoError::InvalidPublicKey {
reason: format!("failed to deserialize X25519 public key: {e}"),
})?;
Ok(Self(bytes.to_vec()))
}
pub fn to_bytes(&self) -> &[u8] {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct HpkePrivateKey(Zeroizing<Vec<u8>>);
impl HpkePrivateKey {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, CryptoError> {
<Kem as KemTrait>::PrivateKey::from_bytes(bytes).map_err(|e| CryptoError::InvalidPrivateKey {
reason: format!("failed to deserialize X25519 private key: {e}"),
})?;
Ok(Self(Zeroizing::new(bytes.to_vec())))
}
pub fn to_bytes(&self) -> &[u8] {
&self.0
}
pub fn to_public_key(&self) -> HpkePublicKey {
let sk_bytes: [u8; 32] = self.0[..].try_into().expect("X25519 key is always 32 bytes");
let sk = x25519_dalek::StaticSecret::from(sk_bytes);
let pk = x25519_dalek::PublicKey::from(&sk);
HpkePublicKey(pk.as_bytes().to_vec())
}
}
pub fn generate_keypair() -> (HpkePrivateKey, HpkePublicKey) {
let mut rng = OsRng;
let (sk, pk) = Kem::gen_keypair(&mut rng);
let sk_bytes = Zeroizing::new(sk.to_bytes().to_vec());
let pk_bytes = pk.to_bytes().to_vec();
(HpkePrivateKey(sk_bytes), HpkePublicKey(pk_bytes))
}
pub fn encrypt(recipient_pk: &HpkePublicKey, plaintext: &[u8], aad: &[u8]) -> Result<(Vec<u8>, Vec<u8>), CryptoError> {
let pk = <Kem as KemTrait>::PublicKey::from_bytes(recipient_pk.to_bytes())
.map_err(|e| CryptoError::InvalidPublicKey { reason: format!("{e}") })?;
let mut rng = OsRng;
let (encapped_key, ciphertext) =
::hpke::single_shot_seal::<Aead, Kdf, Kem, _>(&OpModeS::Base, &pk, &[], plaintext, aad, &mut rng)
.map_err(|e| CryptoError::HpkeEncrypt(format!("{e}")))?;
Ok((encapped_key.to_bytes().to_vec(), ciphertext))
}
pub fn decrypt(
recipient_sk: &HpkePrivateKey,
encapped_key: &[u8],
ciphertext: &[u8],
aad: &[u8],
) -> Result<Zeroizing<Vec<u8>>, CryptoError> {
let sk = <Kem as KemTrait>::PrivateKey::from_bytes(recipient_sk.to_bytes())
.map_err(|e| CryptoError::InvalidPrivateKey { reason: format!("{e}") })?;
let enc = <Kem as KemTrait>::EncappedKey::from_bytes(encapped_key)
.map_err(|e| CryptoError::HpkeDecrypt(format!("invalid encapped key: {e}")))?;
let plaintext = ::hpke::single_shot_open::<Aead, Kdf, Kem>(&OpModeR::Base, &sk, &enc, &[], ciphertext, aad)
.map_err(|e| CryptoError::HpkeDecrypt(format!("{e}")))?;
Ok(Zeroizing::new(plaintext))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encrypt_decrypt_roundtrip() {
let (sk, pk) = generate_keypair();
let plaintext = b"hello, newton privacy layer";
let aad = b"task-context";
let (enc, ct) = encrypt(&pk, plaintext, aad).expect("encrypt failed");
let recovered = decrypt(&sk, &enc, &ct, aad).expect("decrypt failed");
assert_eq!(&*recovered, plaintext);
}
#[test]
fn decrypt_with_wrong_key_fails() {
let (_sk, pk) = generate_keypair();
let (wrong_sk, _wrong_pk) = generate_keypair();
let plaintext = b"secret data";
let aad = b"ctx";
let (enc, ct) = encrypt(&pk, plaintext, aad).expect("encrypt failed");
let result = decrypt(&wrong_sk, &enc, &ct, aad);
assert!(result.is_err(), "decryption with wrong key should fail");
}
#[test]
fn decrypt_with_wrong_aad_fails() {
let (sk, pk) = generate_keypair();
let plaintext = b"secret data";
let aad = b"correct-aad";
let (enc, ct) = encrypt(&pk, plaintext, aad).expect("encrypt failed");
let result = decrypt(&sk, &enc, &ct, b"wrong-aad");
assert!(result.is_err(), "decryption with wrong AAD should fail");
}
#[test]
fn encrypt_decrypt_empty_plaintext() {
let (sk, pk) = generate_keypair();
let plaintext = b"";
let aad = b"empty-test";
let (enc, ct) = encrypt(&pk, plaintext, aad).expect("encrypt failed");
let recovered = decrypt(&sk, &enc, &ct, aad).expect("decrypt failed");
assert_eq!(&*recovered, plaintext);
}
#[test]
fn key_from_bytes_roundtrip() {
let (sk, pk) = generate_keypair();
let sk2 = HpkePrivateKey::from_bytes(sk.to_bytes()).expect("sk from_bytes failed");
let pk2 = HpkePublicKey::from_bytes(pk.to_bytes()).expect("pk from_bytes failed");
assert_eq!(sk.to_bytes(), sk2.to_bytes());
assert_eq!(pk.to_bytes(), pk2.to_bytes());
}
#[test]
fn invalid_key_bytes_rejected() {
let result = HpkePublicKey::from_bytes(&[0u8; 5]);
assert!(result.is_err(), "short bytes should be rejected for public key");
let result = HpkePrivateKey::from_bytes(&[0u8; 5]);
assert!(result.is_err(), "short bytes should be rejected for private key");
}
}