Skip to main content

p2panda_encryption/crypto/
hpke.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Hybrid Public Key Encryption (HPKE) with DHKEM-X25519, HKDF SHA256 and ChaCha20Poly1305 AEAD
4//! parameters.
5//!
6//! <https://www.rfc-editor.org/rfc/rfc9180>
7use hpke_rs::{Hpke, HpkePrivateKey, HpkePublicKey, Mode};
8use hpke_rs_crypto::types::{AeadAlgorithm, KdfAlgorithm, KemAlgorithm};
9use hpke_rs_rust_crypto::HpkeRustCrypto;
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13use crate::crypto::x25519::{PublicKey, SecretKey};
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16pub struct HpkeCiphertext {
17    /// Encrypted, shared secret generated for this transaction.
18    #[serde(with = "serde_bytes")]
19    pub kem_output: Vec<u8>,
20
21    /// Encrypted payload.
22    #[serde(with = "serde_bytes")]
23    pub ciphertext: Vec<u8>,
24}
25
26/// Encrypt a secret payload to a public key using HPKE.
27///
28/// The sender in HPKE uses a KEM to generate the shared secret as well as the encapsulation. The
29/// shared secret is then used in an AEAD (after running it through a key schedule) in order to
30/// encrypt a payload.
31///
32/// In order to encrypt a payload to a public key the sender needs to provide the receiver’s public
33/// key, some information `info` and additional data `aad` to bind the encryption to a certain
34/// context, as well as the payload `plaintext`.
35pub fn hpke_seal(
36    verifying_key: &PublicKey,
37    info: Option<&[u8]>,
38    aad: Option<&[u8]>,
39    plaintext: &[u8],
40) -> Result<HpkeCiphertext, HpkeError> {
41    // Unfortunately `hpke-rs` doesn't allow us to pass in our own rng without writing a lot of
42    // boilerplate, so we hope to replace it with a different API or solution sometime.
43    let mut hpke = Hpke::<HpkeRustCrypto>::new(
44        Mode::Base,
45        KemAlgorithm::DhKem25519,
46        KdfAlgorithm::HkdfSha256,
47        AeadAlgorithm::ChaCha20Poly1305,
48    );
49    let pk_r = HpkePublicKey::new(verifying_key.as_bytes().to_vec());
50    let (kem_output, ciphertext) = hpke
51        .seal(
52            &pk_r,
53            info.unwrap_or_default(),
54            aad.unwrap_or_default(),
55            plaintext,
56            None,
57            None,
58            None,
59        )
60        .map_err(HpkeError::Encryption)?;
61    Ok(HpkeCiphertext {
62        kem_output,
63        ciphertext,
64    })
65}
66
67/// Decrypt a secret payload for a receiver holding the secret key using HPKE.
68///
69/// When decrypting the receiver uses the secret key to retrieve the shared secret and decrypt the
70/// ciphertext. The `info` and `aad` (additional data) are the same as entered on the sender’s
71/// side.
72pub fn hpke_open(
73    input: &HpkeCiphertext,
74    secret_key: &SecretKey,
75    info: Option<&[u8]>,
76    aad: Option<&[u8]>,
77) -> Result<Vec<u8>, HpkeError> {
78    let hpke = Hpke::<HpkeRustCrypto>::new(
79        Mode::Base,
80        KemAlgorithm::DhKem25519,
81        KdfAlgorithm::HkdfSha256,
82        AeadAlgorithm::ChaCha20Poly1305,
83    );
84    let sk_r = HpkePrivateKey::new(secret_key.as_bytes().to_vec());
85    let plaintext = hpke
86        .open(
87            &input.kem_output,
88            &sk_r,
89            info.unwrap_or_default(),
90            aad.unwrap_or_default(),
91            &input.ciphertext,
92            None,
93            None,
94            None,
95        )
96        .map_err(HpkeError::Decryption)?;
97    Ok(plaintext)
98}
99
100#[derive(Debug, Error)]
101pub enum HpkeError {
102    #[error("could not encrypt with hpke: {0:?}")]
103    Encryption(hpke_rs::HpkeError),
104
105    #[error("could not decrypt with hpke: {0:?}")]
106    Decryption(hpke_rs::HpkeError),
107}
108
109#[cfg(test)]
110mod tests {
111    use crate::crypto::Rng;
112    use crate::crypto::x25519::SecretKey;
113
114    use super::{HpkeError, hpke_open, hpke_seal};
115
116    #[test]
117    fn seal_and_open() {
118        let rng = Rng::from_seed([1; 32]);
119
120        let secret_key = SecretKey::from_bytes(rng.random_array().unwrap());
121        let verifying_key = secret_key.verifying_key().unwrap();
122
123        let info = b"some info";
124        let aad = b"some aad";
125        let ciphertext =
126            hpke_seal(&verifying_key, Some(info), Some(aad), b"Hello, Panda!").unwrap();
127        let plaintext = hpke_open(&ciphertext, &secret_key, Some(info), Some(aad)).unwrap();
128
129        assert_eq!(plaintext, b"Hello, Panda!");
130    }
131
132    #[test]
133    fn decryption_failed() {
134        let rng = Rng::from_seed([1; 32]);
135
136        let valid_secret_key = SecretKey::from_bytes(rng.random_array().unwrap());
137        let verifying_key = valid_secret_key.verifying_key().unwrap();
138
139        let info = b"some info";
140        let aad = b"some aad";
141        let ciphertext =
142            hpke_seal(&verifying_key, Some(info), Some(aad), b"Hello, Panda!").unwrap();
143
144        // Invalid secret key.
145        let invalid_secret_key = SecretKey::from_bytes(rng.random_array().unwrap());
146        let result = hpke_open(&ciphertext, &invalid_secret_key, Some(info), Some(aad));
147        std::assert_matches!(result, Err(HpkeError::Decryption(_)));
148
149        // Invalid info tag.
150        let result = hpke_open(&ciphertext, &valid_secret_key, None, Some(aad));
151        std::assert_matches!(result, Err(HpkeError::Decryption(_)));
152
153        // Invalid aad.
154        let result = hpke_open(&ciphertext, &valid_secret_key, Some(info), None);
155        std::assert_matches!(result, Err(HpkeError::Decryption(_)));
156    }
157}