Skip to main content

rns_crypto/
ratchet.rs

1//! Destination ratchet history. No clock, filesystem, or global state is required.
2use crate::{x25519::X25519PrivateKey, Rng};
3use alloc::vec::Vec;
4use zeroize::Zeroizing;
5
6pub const DEFAULT_INTERVAL: u64 = 30 * 60;
7pub const DEFAULT_RETAINED: usize = 512;
8/// Allocation/decryption-work limit, including when importing untrusted storage.
9pub const MAX_RETAINED: usize = 4096;
10
11pub struct Decrypted {
12    pub plaintext: Vec<u8>,
13    pub ratchet_id: Option<[u8; 10]>,
14}
15
16pub fn ratchet_id(public: &[u8; 32]) -> [u8; 10] {
17    crate::sha256::sha256(public)[..10].try_into().unwrap()
18}
19
20/// Newest-first private key history. Clones and retired keys are zeroized on drop.
21#[derive(Clone)]
22pub struct RatchetRing {
23    keys: Zeroizing<Vec<[u8; 32]>>,
24    last_rotation: Option<u64>,
25}
26
27impl core::fmt::Debug for RatchetRing {
28    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
29        f.debug_struct("RatchetRing")
30            .field("retained", &self.keys.len())
31            .finish_non_exhaustive()
32    }
33}
34
35impl Default for RatchetRing {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl RatchetRing {
42    pub fn new() -> Self {
43        Self {
44            keys: Zeroizing::new(Vec::new()),
45            last_rotation: None,
46        }
47    }
48
49    /// Imports newest-first keys; the first fresh announce after reload rotates,
50    /// matching the reference (rotation time is not stored in its key file).
51    pub fn from_keys(keys: Vec<[u8; 32]>) -> Option<Self> {
52        let keys = Zeroizing::new(keys);
53        if keys.len() > MAX_RETAINED {
54            return None;
55        }
56        Some(Self {
57            keys,
58            last_rotation: None,
59        })
60    }
61
62    /// Exposes secrets only for explicit persistence implementations.
63    pub fn keys(&self) -> &[[u8; 32]] {
64        &self.keys
65    }
66
67    pub fn current_public(&self) -> Option<[u8; 32]> {
68        self.keys.first().map(|key| {
69            X25519PrivateKey::from_bytes(key)
70                .public_key()
71                .public_bytes()
72        })
73    }
74
75    pub fn retains(&self, public: &[u8; 32]) -> bool {
76        self.keys.iter().any(|key| {
77            X25519PrivateKey::from_bytes(key)
78                .public_key()
79                .public_bytes()
80                == *public
81        })
82    }
83
84    /// Build a candidate; callers must durably commit it before replacing self.
85    pub fn rotated(
86        &self,
87        now: u64,
88        interval: u64,
89        retained: usize,
90        rng: &mut dyn Rng,
91    ) -> Option<Self> {
92        if interval == 0 || !(1..=MAX_RETAINED).contains(&retained) {
93            return None;
94        }
95        if !self.keys.is_empty()
96            && self
97                .last_rotation
98                .is_some_and(|last| now <= last.saturating_add(interval))
99        {
100            return None;
101        }
102        let mut keys = Zeroizing::new(Vec::with_capacity((self.keys.len() + 1).min(retained)));
103        keys.push(X25519PrivateKey::generate(rng).private_bytes());
104        keys.extend_from_slice(&self.keys[..self.keys.len().min(retained - 1)]);
105        Some(Self {
106            keys,
107            last_rotation: Some(now),
108        })
109    }
110
111    pub fn prune(&mut self, retained: usize) {
112        use zeroize::Zeroize;
113        for key in self.keys.iter_mut().skip(retained) {
114            key.zeroize();
115        }
116        self.keys.truncate(retained);
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::{identity::Identity, FixedRng};
124
125    #[test]
126    fn pinned_python_ratchet_vectors() {
127        fn bytes(value: &serde_json::Value) -> Vec<u8> {
128            let text = value.as_str().unwrap();
129            (0..text.len())
130                .step_by(2)
131                .map(|i| u8::from_str_radix(&text[i..i + 2], 16).unwrap())
132                .collect()
133        }
134        let vectors: serde_json::Value = serde_json::from_str(include_str!(
135            "../../tests/fixtures/crypto/local_ratchet_1_5_6.json"
136        ))
137        .unwrap();
138        let identity =
139            Identity::from_private_key(&bytes(&vectors["identity_private"]).try_into().unwrap());
140        let ring = RatchetRing::from_keys(
141            vectors["keys_newest_first"]
142                .as_array()
143                .unwrap()
144                .iter()
145                .map(|v| bytes(v).try_into().unwrap())
146                .collect(),
147        )
148        .unwrap();
149        for case in vectors["cases"].as_array().unwrap() {
150            for enforce in [false, true] {
151                let result =
152                    identity.decrypt_with_ratchets(&bytes(&case["ciphertext"]), &ring, enforce);
153                let accept = case[if enforce {
154                    "enforced_accept"
155                } else {
156                    "fallback_accept"
157                }]
158                .as_bool()
159                .unwrap();
160                assert_eq!(result.is_ok(), accept, "{} enforce={enforce}", case["name"]);
161                if let Ok(result) = result {
162                    assert_eq!(result.plaintext, bytes(&case["plaintext"]));
163                    assert_eq!(
164                        result.ratchet_id,
165                        if case["ratchet_id"].is_null() {
166                            None
167                        } else {
168                            Some(bytes(&case["ratchet_id"]).try_into().unwrap())
169                        }
170                    );
171                }
172            }
173        }
174    }
175
176    #[test]
177    fn lifecycle_and_enforcement() {
178        let id = Identity::from_private_key(&[3; 64]);
179        let mut rng = FixedRng::new(&[7; 64]);
180        let first = RatchetRing::new().rotated(100, 10, 2, &mut rng).unwrap();
181        assert!(first.rotated(110, 10, 2, &mut rng).is_none());
182        assert!(first.rotated(90, 10, 2, &mut rng).is_none());
183        let ciphertext = id
184            .encrypt_with_ratchet(b"", first.current_public().as_ref(), &mut rng)
185            .unwrap();
186        let second = first
187            .rotated(111, 10, 2, &mut FixedRng::new(&[8; 64]))
188            .unwrap();
189        let result = id
190            .decrypt_with_ratchets(&ciphertext, &second, true)
191            .unwrap();
192        assert!(result.plaintext.is_empty());
193        assert_eq!(
194            result.ratchet_id,
195            Some(ratchet_id(&first.current_public().unwrap()))
196        );
197        let third = second
198            .rotated(122, 10, 2, &mut FixedRng::new(&[9; 64]))
199            .unwrap();
200        assert!(id.decrypt_with_ratchets(&ciphertext, &third, true).is_err());
201        let legacy = id.encrypt(b"legacy", &mut rng).unwrap();
202        assert!(id.decrypt_with_ratchets(&legacy, &third, true).is_err());
203        assert_eq!(
204            id.decrypt_with_ratchets(&legacy, &third, false)
205                .unwrap()
206                .plaintext,
207            b"legacy"
208        );
209        assert!(id
210            .decrypt_with_ratchets(&legacy, &RatchetRing::new(), true)
211            .is_err());
212        for size in 0..100 {
213            assert!(id
214                .decrypt_with_ratchets(&alloc::vec![0; size], &third, true)
215                .is_err());
216        }
217        assert!(Identity::from_private_key(&[4; 64])
218            .decrypt_with_ratchets(&ciphertext, &first, true)
219            .is_err());
220    }
221}