p2panda_encryption/message_scheme/
ratchet.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Encryption and decryption ratches with lost or out-of-order messages.
4use std::collections::VecDeque;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9use crate::crypto::Secret;
10use crate::crypto::aead::AeadNonce;
11use crate::crypto::hkdf::{HkdfError, hkdf};
12
13/// 256-bit secret message key.
14pub const MESSAGE_KEY_SIZE: usize = 32;
15
16/// Secret message key.
17pub type RatchetKey = Secret<MESSAGE_KEY_SIZE>;
18
19/// AEAD nonce to encrypt message.
20pub type RatchetNonce = AeadNonce;
21
22/// AEAD parameters to encrypt message.
23pub type RatchetKeyMaterial = (RatchetKey, RatchetNonce);
24
25/// Key generation of message ratchet.
26pub type Generation = u32;
27
28/// Message ratchet that can output key material either for encryption or decryption.
29#[derive(Debug)]
30pub struct RatchetSecret;
31
32#[derive(Debug, Serialize, Deserialize)]
33#[cfg_attr(any(test, feature = "test_utils"), derive(Clone))]
34pub struct RatchetSecretState {
35    secret: Secret<MESSAGE_KEY_SIZE>,
36    generation: Generation,
37}
38
39impl RatchetSecret {
40    pub fn init(secret: Secret<MESSAGE_KEY_SIZE>) -> RatchetSecretState {
41        RatchetSecretState {
42            secret,
43            generation: 0,
44        }
45    }
46
47    /// Consume the current ratchet secret to derive key material and establish a new secret for
48    /// the next generation.
49    pub fn ratchet_forward(
50        mut y: RatchetSecretState,
51    ) -> Result<(RatchetSecretState, Generation, RatchetKeyMaterial), RatchetError> {
52        let generation = y.generation;
53
54        // Derive key material from current secret.
55        let nonce: AeadNonce = hkdf(b"nonce", y.secret.as_bytes(), None)?;
56        let key: [u8; MESSAGE_KEY_SIZE] = hkdf(b"key", y.secret.as_bytes(), None)?;
57
58        // Ratchet forward.
59        y.generation += 1;
60        y.secret = Secret::from_bytes(hkdf(b"chain", y.secret.as_bytes(), None)?);
61
62        Ok((y, generation, (Secret::from_bytes(key), nonce)))
63    }
64}
65
66/// Message ratchet for decryption with support for handling lost or out-of-order messages.
67///
68/// ## Out-of-order handling
69///
70/// Out-of-order messages cause the ratchet to "jump" ahead and keep "unused" keys persisted until
71/// they're used eventually.
72///
73/// In this example our chain has a length of 2 at the moment a message for generation 4 arrives
74/// out of order (we've expected generation 2). Now we pre-generate the keys for the "jumped"
75/// messages (generation 2 and 3) and keep them persisted for later. We decrypt the new message for
76/// generation 4 with the regular, now "latest", chain state.
77///
78/// ```text
79/// 0
80/// 1 <- Current chain "head"
81/// 2
82/// 3
83/// 4 <- New chain "head" after receiving message @ generation 4
84/// ```
85///
86/// ## Tolerance limits
87///
88/// Developers can and should set bounds to how much a decryption ratchet can tolerate messages
89/// arriving out of order, that is, into the "future" and into the "past". Setting these "window"
90/// limits has implications for the forward secrecy of an application as unused keys stay around
91/// for a while. A setting should be picked wisely based on the network's reliability to deliver
92/// and order messages and security requirements.
93#[derive(Debug)]
94pub struct DecryptionRatchet;
95
96#[derive(Debug, Serialize, Deserialize)]
97#[cfg_attr(any(test, feature = "test_utils"), derive(Clone))]
98pub struct DecryptionRatchetState {
99    past_secrets: VecDeque<Option<RatchetKeyMaterial>>,
100    ratchet_head: RatchetSecretState,
101}
102
103impl DecryptionRatchet {
104    pub fn init(secret: Secret<MESSAGE_KEY_SIZE>) -> DecryptionRatchetState {
105        DecryptionRatchetState {
106            past_secrets: VecDeque::new(),
107            ratchet_head: RatchetSecret::init(secret),
108        }
109    }
110
111    /// Returns a secret from the ratchet for decryption. Throws an error if requested secret is
112    /// out of bounds.
113    ///
114    /// ## Limits Configuration
115    ///
116    /// - Out-of-order (ooo) tolerance:
117    ///   This parameter defines a window for which decryption secrets are kept. This is useful in
118    ///   case the ratchet cannot guarantee that all application messages have total order within
119    ///   an epoch. Use this carefully, since keeping decryption secrets affects forward secrecy
120    ///   within an epoch.
121    /// - Maximum forward distance:
122    ///   This parameter defines how many incoming messages can be skipped. This is useful if the
123    ///   application drops messages.
124    pub fn secret_for_decryption(
125        mut y: DecryptionRatchetState,
126        generation: Generation,
127        maximum_forward_distance: u32,
128        ooo_tolerance: u32,
129    ) -> Result<(DecryptionRatchetState, RatchetKeyMaterial), RatchetError> {
130        let generation_head = y.ratchet_head.generation;
131
132        // If generation is too distant in the future.
133        if generation_head < u32::MAX - maximum_forward_distance
134            && generation > generation_head + maximum_forward_distance
135        {
136            return Err(RatchetError::TooDistantInTheFuture);
137        }
138
139        // If generation is too distant in the past.
140        if generation < generation_head && (generation_head - generation) > ooo_tolerance {
141            return Err(RatchetError::TooDistantInThePast);
142        }
143
144        // If generation is the one the ratchet is currently at (regular case) or in the future.
145        if generation >= generation_head {
146            // Ratchet the chain forward as far as necessary.
147            for _ in 0..(generation - generation_head) {
148                // Derive the key material.
149                let (y_ratchet_head_i, _, ratchet_secrets) =
150                    RatchetSecret::ratchet_forward(y.ratchet_head)?;
151                y.ratchet_head = y_ratchet_head_i;
152                // Add it to the front of the queue.
153                y.past_secrets.push_front(Some(ratchet_secrets));
154            }
155            let (y_ratchet_head_i, _, ratchet_secrets) =
156                RatchetSecret::ratchet_forward(y.ratchet_head)?;
157            y.ratchet_head = y_ratchet_head_i;
158            // Add an entry to the past secrets queue to keep indexing consistent.
159            y.past_secrets.push_front(None);
160            // Remove persisted keys until it is within the bounds determined by the config.
161            y.past_secrets.truncate(ooo_tolerance as usize);
162            Ok((y, ratchet_secrets))
163        } else {
164            // If the requested generation is within the window of past secrets, we should get a
165            // positive index.
166            let window_index = ((generation_head - generation) as i32) - 1;
167            // We might not have the key material (e.g. we might have discarded it when generating
168            // an encryption secret).
169            let index = if window_index >= 0 {
170                window_index as usize
171            } else {
172                return Err(RatchetError::TooDistantInThePast);
173            };
174            // Get the relevant secrets from the past secrets queue.
175            let ratchet_secrets = y
176                .past_secrets
177                .get_mut(index)
178                .ok_or(RatchetError::IndexOutOfBounds)?
179                // We use take here to replace the entry in the `past_secrets` with `None`, thus
180                // achieving FS for that secret as soon as the caller of this function drops it.
181                .take()
182                // If the requested generation was used to decrypt a message earlier, throw an
183                // error.
184                .ok_or(RatchetError::SecretReuse)?;
185            Ok((y, ratchet_secrets))
186        }
187    }
188}
189
190#[derive(Debug, Error)]
191pub enum RatchetError {
192    #[error(transparent)]
193    Hkdf(#[from] HkdfError),
194
195    #[error("generation for message ratchet is too far into the future")]
196    TooDistantInTheFuture,
197
198    #[error("generation for message ratchet is too far into the past")]
199    TooDistantInThePast,
200
201    #[error("unknown message secret")]
202    IndexOutOfBounds,
203
204    #[error("tried to re-use secret for same generation")]
205    SecretReuse,
206}
207
208#[cfg(test)]
209mod tests {
210    use crate::Rng;
211    use crate::crypto::Secret;
212
213    use super::{DecryptionRatchet, MESSAGE_KEY_SIZE, RatchetError, RatchetSecret};
214
215    #[test]
216    fn ratchet_forward() {
217        let rng = Rng::from_seed([1; 32]);
218
219        let update_secret = Secret::from_bytes(rng.random_array::<MESSAGE_KEY_SIZE>().unwrap());
220
221        let ratchet = RatchetSecret::init(update_secret);
222        let (ratchet, generation, secret_0) = RatchetSecret::ratchet_forward(ratchet).unwrap();
223        assert_eq!(generation, 0);
224        assert_eq!(ratchet.generation, 1);
225
226        let (ratchet, generation, secret_1) = RatchetSecret::ratchet_forward(ratchet).unwrap();
227        assert_eq!(generation, 1);
228        assert_eq!(ratchet.generation, 2);
229
230        // Ratchet secrets do not match across generations.
231        assert_ne!(secret_0, secret_1);
232    }
233
234    #[test]
235    fn forward_secrecy() {
236        let rng = Rng::from_seed([1; 32]);
237
238        let update_secret = Secret::from_bytes(rng.random_array::<MESSAGE_KEY_SIZE>().unwrap());
239
240        let ooo_tolerance = 4;
241        let max_forward = 100;
242
243        let ratchet = DecryptionRatchet::init(update_secret);
244
245        let (ratchet, secret) =
246            DecryptionRatchet::secret_for_decryption(ratchet, 0, max_forward, ooo_tolerance)
247                .unwrap();
248
249        // Generation should have increased.
250        assert_eq!(ratchet.ratchet_head.generation, 1);
251
252        // No secrets have been kept.
253        assert_ne!(ratchet.ratchet_head.secret, secret.0);
254        assert!(!ratchet.past_secrets.iter().any(|secret| secret.is_some()));
255
256        // Re-trying to retreive the secret for the same generation should fail.
257        assert!(matches!(
258            DecryptionRatchet::secret_for_decryption(
259                ratchet.clone(),
260                0,
261                max_forward,
262                ooo_tolerance
263            ),
264            Err(RatchetError::SecretReuse),
265        ));
266
267        // Move the ratchet forwards a few generations.
268        let jump = 10;
269        let (mut ratchet, _) =
270            DecryptionRatchet::secret_for_decryption(ratchet, jump, max_forward, ooo_tolerance)
271                .unwrap();
272
273        // Now let's get a few keys. The first time we're trying to get the key of a given
274        // generation, it should work. The second time, we should get an error.
275        for generation in jump - ooo_tolerance + 1..jump {
276            let (ratchet_i, _) = DecryptionRatchet::secret_for_decryption(
277                ratchet,
278                generation,
279                max_forward,
280                ooo_tolerance,
281            )
282            .unwrap();
283
284            assert!(matches!(
285                DecryptionRatchet::secret_for_decryption(
286                    ratchet_i.clone(),
287                    generation,
288                    max_forward,
289                    ooo_tolerance
290                ),
291                Err(RatchetError::SecretReuse),
292            ));
293
294            ratchet = ratchet_i;
295        }
296
297        // No secrets have been kept.
298        assert!(!ratchet.past_secrets.iter().any(|secret| secret.is_some()));
299    }
300
301    #[test]
302    fn out_of_order() {
303        let rng = Rng::from_seed([1; 32]);
304
305        let update_secret = Secret::from_bytes(rng.random_array::<MESSAGE_KEY_SIZE>().unwrap());
306
307        let max_forward = 3;
308        let ooo_tolerance = 3;
309
310        let alice = RatchetSecret::init(update_secret.clone());
311        let bob = DecryptionRatchet::init(update_secret);
312
313        let (alice, _, alice_secret_0) = RatchetSecret::ratchet_forward(alice).unwrap();
314        let (alice, _, _alice_secret_1) = RatchetSecret::ratchet_forward(alice).unwrap();
315        let (alice, _, alice_secret_2) = RatchetSecret::ratchet_forward(alice).unwrap();
316        let (alice, _, alice_secret_3) = RatchetSecret::ratchet_forward(alice).unwrap();
317        let (alice, _, alice_secret_4) = RatchetSecret::ratchet_forward(alice).unwrap();
318        assert_eq!(alice.generation, 5);
319
320        // Bob derives the first secret for Alice's first message.
321        let (bob, bob_secret_0) =
322            DecryptionRatchet::secret_for_decryption(bob, 0, max_forward, ooo_tolerance).unwrap();
323        assert_eq!(alice_secret_0, bob_secret_0);
324
325        // Alice's messages arrive out-of-order, Bob derives them still.
326        let (bob, bob_secret_4) =
327            DecryptionRatchet::secret_for_decryption(bob, 4, max_forward, ooo_tolerance).unwrap();
328        assert_eq!(alice_secret_4, bob_secret_4);
329        assert_eq!(bob.ratchet_head.generation, 5);
330        let (bob, bob_secret_3) =
331            DecryptionRatchet::secret_for_decryption(bob, 3, max_forward, ooo_tolerance).unwrap();
332        assert_eq!(alice_secret_3, bob_secret_3);
333        let (bob, bob_secret_2) =
334            DecryptionRatchet::secret_for_decryption(bob, 2, max_forward, ooo_tolerance).unwrap();
335        assert_eq!(alice_secret_2, bob_secret_2);
336
337        // Alice's message from generation 1 arrives, but it's already outside of the tolerance
338        // window, we expect an error here.
339        assert!(matches!(
340            DecryptionRatchet::secret_for_decryption(bob.clone(), 1, max_forward, ooo_tolerance),
341            Err(RatchetError::TooDistantInThePast)
342        ));
343
344        // Bob receives a message very far into the future from Alice, but this is also outside the
345        // tolerated window, we expect an error here.
346        assert!(matches!(
347            DecryptionRatchet::secret_for_decryption(
348                bob.clone(),
349                bob.ratchet_head.generation + max_forward + 1,
350                max_forward,
351                ooo_tolerance
352            ),
353            Err(RatchetError::TooDistantInTheFuture)
354        ));
355    }
356}