1use 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;
8pub 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#[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 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 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 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}