diff --git a/hpke_spec_test.py b/hpke_spec_test.py index d004fdb..033e435 100644 --- a/hpke_spec_test.py +++ b/hpke_spec_test.py @@ -21,3 +21,10 @@ def test_wrong_pk_size(self): raise AssertionError( "hpke_seal failed to raise Exception on malformed public key" ) + + def test_hpke_roundtrip(self): + skR, pkR = hpke_spec.generate_hpke_keypair() + ptxt = b"my name is Vincent Law" + ctxt = hpke_spec.hpke_seal(pkR, ptxt) + ptxt_roundtrip = hpke_spec.hpke_open(skR, ctxt) + assert ptxt == ptxt_roundtrip diff --git a/src/lib.rs b/src/lib.rs index b6be455..0dd1efe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,12 @@ -use hacspec_lib::{Seq, U8}; -use hpke::{AdditionalData, HPKECiphertext, HPKEConfig, HpkePublicKey, HpkeSeal, Mode}; +use hacspec_lib::{ByteSeq, Seq, U8}; +use hpke::{ + AdditionalData, Ciphertext, HPKECiphertext, HPKEConfig, HpkeOpen, HpkePrivateKey, + HpkePublicKey, HpkeSeal, KemOutput, Mode, +}; use hpke_aead::AEAD; use hpke_errors::HpkeError; -use hpke_kdf::{Info, KDF}; -use hpke_kem::{Randomness, KEM}; +use hpke_kdf::{Info, InputKeyMaterial, KDF}; +use hpke_kem::{DeriveKeyPair, Nsk, Randomness, KEM}; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use pyo3::types::PyBytes; @@ -17,6 +20,28 @@ fn get_default_hpke_config() -> HPKEConfig { HPKEConfig(mode, kem, kdf, aead) } +fn hpke_open_bytes(ctxtb: &[u8], skb: &[u8]) -> Result, HpkeError> { + let hpke_config = get_default_hpke_config(); + let kem_output = KemOutput::from_public_slice(&ctxtb[..32]); + let ctxt = Ciphertext::from_public_slice(&ctxtb[32..]); + let ciphertext = HPKECiphertext(kem_output, ctxt); + let sk_r = HpkePrivateKey::from_public_slice(skb); + let info = Info::new(0); + let aad = AdditionalData::new(0); + let result: ByteSeq = HpkeOpen( + hpke_config, + &ciphertext, + &sk_r, + &info, + &aad, + None, + None, + None, + )?; + let plaintext = result.into_native(); + Ok(plaintext) +} + fn hpke_seal_bytes(pkb: &[u8], ptxtb: &[u8]) -> Result, HpkeError> { let hpke_config = get_default_hpke_config(); let pk = HpkePublicKey::from_public_slice(pkb); @@ -43,6 +68,26 @@ fn hpke_seal_bytes(pkb: &[u8], ptxtb: &[u8]) -> Result, HpkeError> { Ok(encapsulated) } +fn generate_keypair() -> Result<(Vec, Vec), HpkeError> { + let hpke_config = get_default_hpke_config(); + let kem = hpke_config.1; + let nbytes = Nsk(kem); + let mut rand_bytes = vec![0u8; nbytes]; + OsRng.fill_bytes(&mut rand_bytes); + let randomness = InputKeyMaterial::from_public_slice(&rand_bytes); + let keypair = DeriveKeyPair(kem, &randomness)?; + Ok((keypair.0.into_native(), keypair.1.into_native())) +} + +#[pyfunction] +fn generate_hpke_keypair(py: Python) -> PyResult<(&PyBytes, &PyBytes)> { + let keypair_bytes = generate_keypair() + .map_err(|hpke_error| PyRuntimeError::new_err(format!("{hpke_error:?}")))?; + let sk_py = PyBytes::new(py, &keypair_bytes.0); + let pk_py = PyBytes::new(py, &keypair_bytes.1); + Ok((sk_py, pk_py)) +} + /// Python binding to hpke-spec's Single-Shot API function hpke::HpkeSeal #[pyfunction] fn hpke_seal<'p>(py: Python<'p>, pk_py: &PyBytes, ptxt_py: &PyBytes) -> PyResult<&'p PyBytes> { @@ -53,9 +98,21 @@ fn hpke_seal<'p>(py: Python<'p>, pk_py: &PyBytes, ptxt_py: &PyBytes) -> PyResult Ok(PyBytes::new(py, &ciphertext_bytes)) } +/// Python binding to hpke-spec's Single-Shot API function hpke::HpkeOpen +#[pyfunction] +fn hpke_open<'p>(py: Python<'p>, sk_py: &PyBytes, ctxt_py: &PyBytes) -> PyResult<&'p PyBytes> { + let skb = sk_py.as_bytes(); + let ctxtb = ctxt_py.as_bytes(); + let plaintext_bytes = hpke_open_bytes(ctxtb, skb) + .map_err(|hpke_error| PyRuntimeError::new_err(format!("{hpke_error:?}")))?; + Ok(PyBytes::new(py, &plaintext_bytes)) +} + /// A Python module implemented in Rust. #[pymodule] fn hpke_spec(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(generate_hpke_keypair, m)?)?; + m.add_function(wrap_pyfunction!(hpke_open, m)?)?; m.add_function(wrap_pyfunction!(hpke_seal, m)?)?; Ok(()) }