Skip to main content

relay_ciphers/
x25519_xchacha20poly1305.rs

1use chacha20poly1305::{
2    KeyInit as _, XChaCha20Poly1305, XNonce,
3    aead::{Aead as _, Payload as AeadPayload},
4};
5use relay_core::prelude::Payload;
6use serde::{Deserialize, Serialize};
7use serde_with::{base64::Base64, serde_as};
8use x25519_dalek::{PublicKey, StaticSecret};
9
10use relay_crypto::{
11    alg::{AlgorithmError, EncryptionAlgorithm, EncryptionContext},
12    hex,
13    hkdf::Hkdf,
14    kid::kid_from_key,
15    rand_core::{CryptoRng, RngCore},
16    record::{Key, PubRecord},
17    sha2::Sha256,
18};
19
20#[serde_as]
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct Meta {
23    #[serde_as(as = "Base64")]
24    pub sender_ephemeral: Vec<u8>,
25    #[serde_as(as = "Base64")]
26    pub nonce: Vec<u8>,
27}
28
29#[derive(Debug, Clone)]
30pub struct Context {
31    key: [u8; 32],
32    nonce: [u8; 24],
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Secrets {
37    #[serde(with = "hex::serde")]
38    pub secret: [u8; 32],
39}
40
41pub struct X25519XChaCha20Poly1305;
42
43impl EncryptionAlgorithm for X25519XChaCha20Poly1305 {
44    type Meta = Meta;
45    type Secrets = Secrets;
46    type Context = Context;
47
48    fn alg() -> &'static str {
49        "x25519-hkdf-sha256-xchacha20poly1305"
50    }
51
52    fn generate(
53        &self,
54        mut rng: impl RngCore + CryptoRng,
55    ) -> Result<(Key, Self::Secrets), AlgorithmError> {
56        let secret = StaticSecret::random_from_rng(&mut rng);
57        let public = PublicKey::from(&secret);
58        let kid = kid_from_key(public.as_bytes().to_vec());
59
60        Ok((
61            Key {
62                alg: Self::alg().to_string(),
63                kid,
64                data: public.as_bytes().to_vec(),
65            },
66            Secrets {
67                secret: secret.to_bytes(),
68            },
69        ))
70    }
71
72    fn encrypt_inner(
73        &self,
74        mut rng: impl RngCore + CryptoRng,
75        recipient: &PubRecord,
76        payload: &[u8],
77        aad: &[u8],
78    ) -> Result<(Self::Meta, Vec<u8>), AlgorithmError> {
79        let recipient_public_key: &[u8; 32] = recipient
80            .encryption
81            .data
82            .as_slice()
83            .try_into()
84            .map_err(|_| {
85                AlgorithmError::InvalidMeta("recipient public key must be 32 bytes".into())
86            })?;
87        let recipient = PublicKey::from(*recipient_public_key);
88
89        let ephemeral = x25519_dalek::EphemeralSecret::random_from_rng(&mut rng);
90        let ephemeral_pub = PublicKey::from(&ephemeral);
91
92        let shared = ephemeral.diffie_hellman(&recipient);
93
94        let hkdf = Hkdf::<Sha256>::new(None, shared.as_bytes());
95        let mut key = [0u8; 32];
96        hkdf.expand(&Self::info(), &mut key)
97            .map_err(|e| AlgorithmError::KeyDerivation(e.to_string()))?;
98
99        let aead = XChaCha20Poly1305::new(&key.into());
100
101        let mut nonce = [0u8; 24];
102        rng.fill_bytes(&mut nonce);
103
104        let ciphertext = aead
105            .encrypt(
106                XNonce::from_slice(&nonce),
107                AeadPayload { msg: payload, aad },
108            )
109            .map_err(|e| AlgorithmError::Encrypt(e.to_string()))?;
110
111        Ok((
112            Meta {
113                sender_ephemeral: ephemeral_pub.as_bytes().to_vec(),
114                nonce: nonce.to_vec(),
115            },
116            ciphertext,
117        ))
118    }
119
120    fn open_inner(
121        &self,
122        meta: &Self::Meta,
123        secrets: &Self::Secrets,
124    ) -> Result<Self::Context, AlgorithmError> {
125        let ephemeral: &[u8; 32] = meta
126            .sender_ephemeral
127            .as_slice()
128            .try_into()
129            .map_err(|_| AlgorithmError::Ephemeral)?;
130        let sender = PublicKey::from(*ephemeral);
131
132        let secret_key = StaticSecret::from(secrets.secret);
133        let shared = secret_key.diffie_hellman(&sender);
134
135        let hkdf = Hkdf::<Sha256>::new(None, shared.as_bytes());
136        let mut key = [0u8; 32];
137        hkdf.expand(&Self::info(), &mut key)
138            .map_err(|e| AlgorithmError::KeyDerivation(e.to_string()))?;
139
140        let nonce: [u8; 24] = meta
141            .nonce
142            .as_slice()
143            .try_into()
144            .map_err(|_| AlgorithmError::Nonce)?;
145
146        Ok(Context { key, nonce })
147    }
148}
149
150impl EncryptionContext for Context {
151    fn decrypt<T>(&self, payload: &Payload<T>, aad: &[u8]) -> Result<Vec<u8>, AlgorithmError> {
152        let aead = XChaCha20Poly1305::new(&self.key.into());
153
154        aead.decrypt(
155            XNonce::from_slice(&self.nonce),
156            AeadPayload {
157                msg: &payload.ciphertext,
158                aad,
159            },
160        )
161        .map_err(|e| AlgorithmError::Decrypt(e.to_string()))
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use relay_core::chrono::Utc;
169    use relay_crypto::rng::os_rng_hkdf;
170
171    #[test]
172    fn encrypt_decrypt() {
173        let mut rng = os_rng_hkdf(None, b"test").unwrap();
174        let (key, secrets) = X25519XChaCha20Poly1305
175            .generate(&mut rng)
176            .expect("Generation failed");
177        let record = PubRecord {
178            id: "recipient".to_string(),
179            created_at: Utc::now(),
180            expires_at: None,
181            encryption: key,
182            signing: Key {
183                alg: String::new(),
184                kid: String::new(),
185                data: vec![],
186            },
187        };
188
189        let message = b"Hello, world!";
190        let aad = b"Additional authenticated data";
191
192        let payload: Payload<()> = X25519XChaCha20Poly1305
193            .encrypt(rng, &record, message, aad)
194            .expect("Encryption failed");
195
196        let context = X25519XChaCha20Poly1305
197            .open(&payload.info, &serde_json::to_vec(&secrets).unwrap())
198            .unwrap();
199
200        let decrypted_message = context.decrypt(&payload, aad).unwrap();
201        assert_eq!(decrypted_message, message);
202    }
203}