1use crate::rand;
2use crate::server::ProducesTickets;
3use crate::Error;
45use ring::aead;
6use std::mem;
7use std::sync::{Arc, Mutex, MutexGuard};
8use std::time;
910/// The timebase for expiring and rolling tickets and ticketing
11/// keys. This is UNIX wall time in seconds.
12///
13/// This is guaranteed to be on or after the UNIX epoch.
14#[derive(Clone, Copy, Debug)]
15pub struct TimeBase(time::Duration);
1617impl TimeBase {
18#[inline]
19pub fn now() -> Result<Self, time::SystemTimeError> {
20Ok(Self(
21 time::SystemTime::now().duration_since(time::UNIX_EPOCH)?,
22 ))
23 }
2425#[inline]
26pub fn as_secs(&self) -> u64 {
27self.0.as_secs()
28 }
29}
3031/// This is a `ProducesTickets` implementation which uses
32/// any *ring* `aead::Algorithm` to encrypt and authentication
33/// the ticket payload. It does not enforce any lifetime
34/// constraint.
35struct AeadTicketer {
36 alg: &'static aead::Algorithm,
37 key: aead::LessSafeKey,
38 lifetime: u32,
39}
4041impl AeadTicketer {
42/// Make a ticketer with recommended configuration and a random key.
43fn new() -> Result<Self, rand::GetRandomFailed> {
44let mut key = [0u8; 32];
45 rand::fill_random(&mut key)?;
4647let alg = &aead::CHACHA20_POLY1305;
48let key = aead::UnboundKey::new(alg, &key).unwrap();
4950Ok(Self {
51 alg,
52 key: aead::LessSafeKey::new(key),
53 lifetime: 60 * 60 * 12,
54 })
55 }
56}
5758impl ProducesTickets for AeadTicketer {
59fn enabled(&self) -> bool {
60true
61}
62fn lifetime(&self) -> u32 {
63self.lifetime
64 }
6566/// Encrypt `message` and return the ciphertext.
67fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
68// Random nonce, because a counter is a privacy leak.
69let mut nonce_buf = [0u8; 12];
70 rand::fill_random(&mut nonce_buf).ok()?;
71let nonce = aead::Nonce::assume_unique_for_key(nonce_buf);
72let aad = aead::Aad::empty();
7374let mut ciphertext =
75 Vec::with_capacity(nonce_buf.len() + message.len() + self.key.algorithm().tag_len());
76 ciphertext.extend(nonce_buf);
77 ciphertext.extend(message);
78self.key
79 .seal_in_place_separate_tag(nonce, aad, &mut ciphertext[nonce_buf.len()..])
80 .map(|tag| {
81 ciphertext.extend(tag.as_ref());
82 ciphertext
83 })
84 .ok()
85 }
8687/// Decrypt `ciphertext` and recover the original message.
88fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
89// Non-panicking `let (nonce, ciphertext) = ciphertext.split_at(...)`.
90let nonce = ciphertext.get(..self.alg.nonce_len())?;
91let ciphertext = ciphertext.get(nonce.len()..)?;
9293// This won't fail since `nonce` has the required length.
94let nonce = aead::Nonce::try_assume_unique_for_key(nonce).ok()?;
9596let mut out = Vec::from(ciphertext);
9798let plain_len = self
99.key
100 .open_in_place(nonce, aead::Aad::empty(), &mut out)
101 .ok()?
102.len();
103 out.truncate(plain_len);
104105Some(out)
106 }
107}
108109struct TicketSwitcherState {
110 next: Option<Box<dyn ProducesTickets>>,
111 current: Box<dyn ProducesTickets>,
112 previous: Option<Box<dyn ProducesTickets>>,
113 next_switch_time: u64,
114}
115116/// A ticketer that has a 'current' sub-ticketer and a single
117/// 'previous' ticketer. It creates a new ticketer every so
118/// often, demoting the current ticketer.
119struct TicketSwitcher {
120 generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
121 lifetime: u32,
122 state: Mutex<TicketSwitcherState>,
123}
124125impl TicketSwitcher {
126/// `lifetime` is in seconds, and is how long the current ticketer
127 /// is used to generate new tickets. Tickets are accepted for no
128 /// longer than twice this duration. `generator` produces a new
129 /// `ProducesTickets` implementation.
130fn new(
131 lifetime: u32,
132 generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>,
133 ) -> Result<Self, Error> {
134let now = TimeBase::now()?;
135Ok(Self {
136 generator,
137 lifetime,
138 state: Mutex::new(TicketSwitcherState {
139 next: Some(generator()?),
140 current: generator()?,
141 previous: None,
142 next_switch_time: now
143 .as_secs()
144 .saturating_add(u64::from(lifetime)),
145 }),
146 })
147 }
148149/// If it's time, demote the `current` ticketer to `previous` (so it
150 /// does no new encryptions but can do decryption) and use next for a
151 /// new `current` ticketer.
152 ///
153 /// Calling this regularly will ensure timely key erasure. Otherwise,
154 /// key erasure will be delayed until the next encrypt/decrypt call.
155 ///
156 /// For efficiency, this is also responsible for locking the state mutex
157 /// and returning the mutexguard.
158fn maybe_roll(&self, now: TimeBase) -> Option<MutexGuard<TicketSwitcherState>> {
159// The code below aims to make switching as efficient as possible
160 // in the common case that the generator never fails. To achieve this
161 // we run the following steps:
162 // 1. If no switch is necessary, just return the mutexguard
163 // 2. Shift over all of the ticketers (so current becomes previous,
164 // and next becomes current). After this, other threads can
165 // start using the new current ticketer.
166 // 3. unlock mutex and generate new ticketer.
167 // 4. Place new ticketer in next and return current
168 //
169 // There are a few things to note here. First, we don't check whether
170 // a new switch might be needed in step 4, even though, due to locking
171 // and entropy collection, significant amounts of time may have passed.
172 // This is to guarantee that the thread doing the switch will eventually
173 // make progress.
174 //
175 // Second, because next may be None, step 2 can fail. In that case
176 // we enter a recovery mode where we generate 2 new ticketers, one for
177 // next and one for the current ticketer. We then take the mutex a
178 // second time and redo the time check to see if a switch is still
179 // necessary.
180 //
181 // This somewhat convoluted approach ensures good availability of the
182 // mutex, by ensuring that the state is usable and the mutex not held
183 // during generation. It also ensures that, so long as the inner
184 // ticketer never generates panics during encryption/decryption,
185 // we are guaranteed to never panic when holding the mutex.
186187let now = now.as_secs();
188let mut are_recovering = false; // Are we recovering from previous failure?
189{
190// Scope the mutex so we only take it for as long as needed
191let mut state = self.state.lock().ok()?;
192193// Fast path in case we do not need to switch to the next ticketer yet
194if now <= state.next_switch_time {
195return Some(state);
196 }
197198// Make the switch, or mark for recovery if not possible
199if let Some(next) = state.next.take() {
200 state.previous = Some(mem::replace(&mut state.current, next));
201 state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
202 } else {
203 are_recovering = true;
204 }
205 }
206207// We always need a next, so generate it now
208let next = (self.generator)().ok()?;
209if !are_recovering {
210// Normal path, generate new next and place it in the state
211let mut state = self.state.lock().ok()?;
212 state.next = Some(next);
213Some(state)
214 } else {
215// Recovering, generate also a new current ticketer, and modify state
216 // as needed. (we need to redo the time check, otherwise this might
217 // result in very rapid switching of ticketers)
218let new_current = (self.generator)().ok()?;
219let mut state = self.state.lock().ok()?;
220 state.next = Some(next);
221if now > state.next_switch_time {
222 state.previous = Some(mem::replace(&mut state.current, new_current));
223 state.next_switch_time = now.saturating_add(u64::from(self.lifetime));
224 }
225Some(state)
226 }
227 }
228}
229230impl ProducesTickets for TicketSwitcher {
231fn lifetime(&self) -> u32 {
232self.lifetime * 2
233}
234235fn enabled(&self) -> bool {
236true
237}
238239fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
240let state = self.maybe_roll(TimeBase::now().ok()?)?;
241242 state.current.encrypt(message)
243 }
244245fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
246let state = self.maybe_roll(TimeBase::now().ok()?)?;
247248// Decrypt with the current key; if that fails, try with the previous.
249state
250 .current
251 .decrypt(ciphertext)
252 .or_else(|| {
253 state
254 .previous
255 .as_ref()
256 .and_then(|previous| previous.decrypt(ciphertext))
257 })
258 }
259}
260261/// A concrete, safe ticket creation mechanism.
262pub struct Ticketer {}
263264fn generate_inner() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed> {
265Ok(Box::new(AeadTicketer::new()?))
266}
267268impl Ticketer {
269/// Make the recommended Ticketer. This produces tickets
270 /// with a 12 hour life and randomly generated keys.
271 ///
272 /// The encryption mechanism used in Chacha20Poly1305.
273pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> {
274Ok(Arc::new(TicketSwitcher::new(6 * 60 * 60, generate_inner)?))
275 }
276}
277278#[test]
279fn basic_pairwise_test() {
280let t = Ticketer::new().unwrap();
281assert!(t.enabled());
282let cipher = t.encrypt(b"hello world").unwrap();
283let plain = t.decrypt(&cipher).unwrap();
284assert_eq!(plain, b"hello world");
285}
286287#[test]
288fn ticketswitcher_switching_test() {
289let t = Arc::new(TicketSwitcher::new(1, generate_inner).unwrap());
290let now = TimeBase::now().unwrap();
291let cipher1 = t.encrypt(b"ticket 1").unwrap();
292assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
293 {
294// Trigger new ticketer
295t.maybe_roll(TimeBase(now.0 + time::Duration::from_secs(10)));
296 }
297let cipher2 = t.encrypt(b"ticket 2").unwrap();
298assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
299assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
300 {
301// Trigger new ticketer
302t.maybe_roll(TimeBase(now.0 + time::Duration::from_secs(20)));
303 }
304let cipher3 = t.encrypt(b"ticket 3").unwrap();
305assert!(t.decrypt(&cipher1).is_none());
306assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
307assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
308}
309310#[cfg(test)]
311fn fail_generator() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed> {
312Err(rand::GetRandomFailed)
313}
314315#[test]
316fn ticketswitcher_recover_test() {
317let mut t = TicketSwitcher::new(1, generate_inner).unwrap();
318let now = TimeBase::now().unwrap();
319let cipher1 = t.encrypt(b"ticket 1").unwrap();
320assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
321 t.generator = fail_generator;
322 {
323// Failed new ticketer
324t.maybe_roll(TimeBase(now.0 + time::Duration::from_secs(10)));
325 }
326 t.generator = generate_inner;
327let cipher2 = t.encrypt(b"ticket 2").unwrap();
328assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
329assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
330 {
331// recover
332t.maybe_roll(TimeBase(now.0 + time::Duration::from_secs(20)));
333 }
334let cipher3 = t.encrypt(b"ticket 3").unwrap();
335assert!(t.decrypt(&cipher1).is_none());
336assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
337assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
338}