1use alloc::boxed::Box;
2use alloc::vec::Vec;
3use core::mem;
4use core::time::Duration;
5use std::sync::{RwLock, RwLockReadGuard};
6use std::time::Instant;
7
8use crate::crypto::TicketProducer;
9use crate::error::Error;
10
11pub struct TicketRotator {
15 pub(crate) generator: fn() -> Result<Box<dyn TicketProducer>, Error>,
16 lifetime: Duration,
17 state: RwLock<TicketRotatorState>,
18}
19
20impl TicketRotator {
21 pub fn new(
32 lifetime: Duration,
33 generator: fn() -> Result<Box<dyn TicketProducer>, Error>,
34 ) -> Result<Self, Error> {
35 Ok(Self {
36 generator,
37 lifetime,
38 state: RwLock::new(TicketRotatorState {
39 current: Some(Generation {
40 producer: generator()?,
41 expires_at: Instant::now() + lifetime,
42 }),
43 previous: None,
44 }),
45 })
46 }
47
48 fn encrypt_at(&self, message: &[u8], now: Instant) -> Option<Vec<u8>> {
49 let state = self.maybe_roll(now)?;
50
51 if let Some(current) = &state.current {
54 return current.producer.encrypt(message);
55 }
56
57 let Some(prev) = &state.previous else {
59 return None;
60 };
61
62 if !prev.in_grace_period(now, self.lifetime) {
64 return None;
65 }
66
67 prev.producer.encrypt(message)
68 }
69
70 fn decrypt_at(&self, ciphertext: &[u8], now: Instant) -> Option<Vec<u8>> {
71 let state = self.maybe_roll(now)?;
72
73 if let Some(current) = &state.current {
76 if let Some(plain) = current.producer.decrypt(ciphertext) {
78 return Some(plain);
79 }
80 }
81
82 let Some(prev) = &state.previous else {
84 return None;
85 };
86
87 if !prev.in_grace_period(now, self.lifetime) {
89 return None;
90 }
91
92 prev.producer.decrypt(ciphertext)
93 }
94
95 pub(crate) fn maybe_roll(
105 &self,
106 now: Instant,
107 ) -> Option<RwLockReadGuard<'_, TicketRotatorState>> {
108 {
111 let read = self.state.read().ok()?;
112 match &read.current {
113 Some(current) if now <= current.expires_at => return Some(read),
114 _ => {}
115 }
116 }
117
118 let mut write = self.state.write().ok()?;
119 if let Some(current) = &write.current {
120 if now <= current.expires_at {
121 drop(write);
123 return self.state.read().ok();
124 }
125 }
126
127 let next = (self.generator)()
130 .ok()
131 .map(|producer| Generation {
132 producer,
133 expires_at: now + self.lifetime,
134 });
135
136 let prev = mem::replace(&mut write.current, next);
141 if prev.is_some() {
142 write.previous = prev;
143 }
144 drop(write);
145
146 self.state.read().ok()
147 }
148}
149
150impl TicketProducer for TicketRotator {
151 fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
152 self.encrypt_at(message, Instant::now())
153 }
154
155 fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
156 self.decrypt_at(ciphertext, Instant::now())
157 }
158
159 fn lifetime(&self) -> Duration {
160 self.lifetime
161 }
162}
163
164impl core::fmt::Debug for TicketRotator {
165 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
166 f.debug_struct("TicketRotator")
167 .finish_non_exhaustive()
168 }
169}
170
171pub(crate) struct TicketRotatorState {
172 current: Option<Generation>,
173 previous: Option<Generation>,
174}
175
176struct Generation {
177 producer: Box<dyn TicketProducer>,
178 expires_at: Instant,
179}
180
181impl Generation {
182 fn in_grace_period(&self, now: Instant, lifetime: Duration) -> bool {
183 now <= self.expires_at + lifetime
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use core::sync::atomic::{AtomicU8, Ordering};
190 use core::time::Duration;
191
192 use super::*;
193
194 #[test]
195 fn ticketrotator_switching_test() {
196 let t = TicketRotator::new(Duration::from_secs(1), FakeTicketer::new).unwrap();
197 let now = Instant::now();
198 let cipher1 = t.encrypt(b"ticket 1").unwrap();
199 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
200 {
201 t.maybe_roll(now + Duration::from_secs(10));
203 }
204 let cipher2 = t.encrypt(b"ticket 2").unwrap();
205 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
206 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
207 {
208 t.maybe_roll(now + Duration::from_secs(20));
210 }
211 let cipher3 = t.encrypt(b"ticket 3").unwrap();
212 assert!(t.decrypt(&cipher1).is_none());
213 assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
214 assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
215 }
216
217 #[test]
218 fn ticketrotator_remains_usable_over_temporary_ticketer_creation_failure() {
219 let mut t = TicketRotator::new(Duration::from_secs(1), FakeTicketer::new).unwrap();
220 let expiry = t
221 .state
222 .read()
223 .unwrap()
224 .current
225 .as_ref()
226 .unwrap()
227 .expires_at;
228 let cipher1 = t.encrypt(b"ticket 1").unwrap();
229 assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
230 t.generator = fail_generator;
231
232 let t1 = expiry;
234 drop(t.maybe_roll(t1));
235 assert!(t.encrypt_at(b"ticket 2", t1).is_some());
236
237 let t2 = expiry + Duration::from_secs(1);
239 let cipher3 = t.encrypt_at(b"ticket 3", t2).unwrap();
240 assert_eq!(t.decrypt_at(&cipher1, t2).unwrap(), b"ticket 1");
241 assert_eq!(t.decrypt_at(&cipher3, t2).unwrap(), b"ticket 3");
242
243 let t3 = expiry + Duration::from_secs(2);
244 assert_eq!(t.encrypt_at(b"ticket 4", t3), None);
245 assert_eq!(t.decrypt_at(&cipher3, t3), None);
246
247 t.generator = FakeTicketer::new;
249 let t4 = expiry + Duration::from_secs(3);
250 drop(t.maybe_roll(t4));
251
252 let t5 = expiry + Duration::from_secs(4);
253 let cipher5 = t.encrypt_at(b"ticket 5", t5).unwrap();
254 assert!(t.decrypt_at(&cipher1, t5).is_none());
255 assert!(t.decrypt_at(&cipher3, t5).is_none());
256 assert_eq!(t.decrypt_at(&cipher5, t5).unwrap(), b"ticket 5");
257
258 t.generator = fail_generator;
260 let mut write = t.state.write().unwrap();
261 write.current = None;
262 write.previous = None;
263 drop(write);
264 assert!(t.encrypt(b"ticket 6").is_none());
265 }
266
267 #[derive(Debug)]
268 struct FakeTicketer {
269 generation: u8,
270 }
271
272 impl FakeTicketer {
273 #[expect(clippy::new_ret_no_self)]
274 fn new() -> Result<Box<dyn TicketProducer>, Error> {
275 Ok(Box::new(Self {
276 generation: std::dbg!(FAKE_GEN.fetch_add(1, Ordering::SeqCst)),
277 }))
278 }
279 }
280
281 impl TicketProducer for FakeTicketer {
282 fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
283 let mut v = Vec::with_capacity(1 + message.len());
284 v.push(self.generation);
285 v.extend(
286 message
287 .iter()
288 .copied()
289 .map(|b| b ^ self.generation),
290 );
291 Some(v)
292 }
293
294 fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
295 if ciphertext.first()? != &self.generation {
296 return None;
297 }
298
299 Some(
300 ciphertext[1..]
301 .iter()
302 .copied()
303 .map(|b| b ^ self.generation)
304 .collect(),
305 )
306 }
307
308 fn lifetime(&self) -> Duration {
309 Duration::ZERO }
311 }
312
313 static FAKE_GEN: AtomicU8 = AtomicU8::new(0);
314
315 fn fail_generator() -> Result<Box<dyn TicketProducer>, Error> {
316 Err(Error::FailedToGetRandomBytes)
317 }
318}