| use crate::rand; |
| use crate::server::ProducesTickets; |
| use crate::Error; |
| |
| use ring::aead; |
| use std::mem; |
| use std::sync::{Arc, Mutex, MutexGuard}; |
| use std::time; |
| |
| /// The timebase for expiring and rolling tickets and ticketing |
| /// keys. This is UNIX wall time in seconds. |
| /// |
| /// This is guaranteed to be on or after the UNIX epoch. |
| #[derive(Clone, Copy, Debug)] |
| pub struct TimeBase(time::Duration); |
| |
| impl TimeBase { |
| #[inline] |
| pub fn now() -> Result<Self, time::SystemTimeError> { |
| Ok(Self( |
| time::SystemTime::now().duration_since(time::UNIX_EPOCH)?, |
| )) |
| } |
| |
| #[inline] |
| pub fn as_secs(&self) -> u64 { |
| self.0.as_secs() |
| } |
| } |
| |
| /// This is a `ProducesTickets` implementation which uses |
| /// any *ring* `aead::Algorithm` to encrypt and authentication |
| /// the ticket payload. It does not enforce any lifetime |
| /// constraint. |
| struct AeadTicketer { |
| alg: &'static aead::Algorithm, |
| key: aead::LessSafeKey, |
| lifetime: u32, |
| } |
| |
| impl AeadTicketer { |
| /// Make a ticketer with recommended configuration and a random key. |
| fn new() -> Result<Self, rand::GetRandomFailed> { |
| let mut key = [0u8; 32]; |
| rand::fill_random(&mut key)?; |
| |
| let alg = &aead::CHACHA20_POLY1305; |
| let key = aead::UnboundKey::new(alg, &key).unwrap(); |
| |
| Ok(Self { |
| alg, |
| key: aead::LessSafeKey::new(key), |
| lifetime: 60 * 60 * 12, |
| }) |
| } |
| } |
| |
| impl ProducesTickets for AeadTicketer { |
| fn enabled(&self) -> bool { |
| true |
| } |
| fn lifetime(&self) -> u32 { |
| self.lifetime |
| } |
| |
| /// Encrypt `message` and return the ciphertext. |
| fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> { |
| // Random nonce, because a counter is a privacy leak. |
| let mut nonce_buf = [0u8; 12]; |
| rand::fill_random(&mut nonce_buf).ok()?; |
| let nonce = ring::aead::Nonce::assume_unique_for_key(nonce_buf); |
| let aad = ring::aead::Aad::empty(); |
| |
| let mut ciphertext = |
| Vec::with_capacity(nonce_buf.len() + message.len() + self.key.algorithm().tag_len()); |
| ciphertext.extend(nonce_buf); |
| ciphertext.extend(message); |
| self.key |
| .seal_in_place_separate_tag(nonce, aad, &mut ciphertext[nonce_buf.len()..]) |
| .map(|tag| { |
| ciphertext.extend(tag.as_ref()); |
| ciphertext |
| }) |
| .ok() |
| } |
| |
| /// Decrypt `ciphertext` and recover the original message. |
| fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> { |
| // Non-panicking `let (nonce, ciphertext) = ciphertext.split_at(...)`. |
| let nonce = ciphertext.get(..self.alg.nonce_len())?; |
| let ciphertext = ciphertext.get(nonce.len()..)?; |
| |
| // This won't fail since `nonce` has the required length. |
| let nonce = ring::aead::Nonce::try_assume_unique_for_key(nonce).ok()?; |
| |
| let mut out = Vec::from(ciphertext); |
| |
| let plain_len = self |
| .key |
| .open_in_place(nonce, aead::Aad::empty(), &mut out) |
| .ok()? |
| .len(); |
| out.truncate(plain_len); |
| |
| Some(out) |
| } |
| } |
| |
| struct TicketSwitcherState { |
| next: Option<Box<dyn ProducesTickets>>, |
| current: Box<dyn ProducesTickets>, |
| previous: Option<Box<dyn ProducesTickets>>, |
| next_switch_time: u64, |
| } |
| |
| /// A ticketer that has a 'current' sub-ticketer and a single |
| /// 'previous' ticketer. It creates a new ticketer every so |
| /// often, demoting the current ticketer. |
| struct TicketSwitcher { |
| generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>, |
| lifetime: u32, |
| state: Mutex<TicketSwitcherState>, |
| } |
| |
| impl TicketSwitcher { |
| /// `lifetime` is in seconds, and is how long the current ticketer |
| /// is used to generate new tickets. Tickets are accepted for no |
| /// longer than twice this duration. `generator` produces a new |
| /// `ProducesTickets` implementation. |
| fn new( |
| lifetime: u32, |
| generator: fn() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed>, |
| ) -> Result<Self, Error> { |
| let now = TimeBase::now()?; |
| Ok(Self { |
| generator, |
| lifetime, |
| state: Mutex::new(TicketSwitcherState { |
| next: Some(generator()?), |
| current: generator()?, |
| previous: None, |
| next_switch_time: now |
| .as_secs() |
| .saturating_add(u64::from(lifetime)), |
| }), |
| }) |
| } |
| |
| /// If it's time, demote the `current` ticketer to `previous` (so it |
| /// does no new encryptions but can do decryption) and use next for a |
| /// new `current` ticketer. |
| /// |
| /// Calling this regularly will ensure timely key erasure. Otherwise, |
| /// key erasure will be delayed until the next encrypt/decrypt call. |
| /// |
| /// For efficiency, this is also responsible for locking the state mutex |
| /// and returning the mutexguard. |
| fn maybe_roll(&self, now: TimeBase) -> Option<MutexGuard<TicketSwitcherState>> { |
| // The code below aims to make switching as efficient as possible |
| // in the common case that the generator never fails. To achieve this |
| // we run the following steps: |
| // 1. If no switch is necessary, just return the mutexguard |
| // 2. Shift over all of the ticketers (so current becomes previous, |
| // and next becomes current). After this, other threads can |
| // start using the new current ticketer. |
| // 3. unlock mutex and generate new ticketer. |
| // 4. Place new ticketer in next and return current |
| // |
| // There are a few things to note here. First, we don't check whether |
| // a new switch might be needed in step 4, even though, due to locking |
| // and entropy collection, significant amounts of time may have passed. |
| // This is to guarantee that the thread doing the switch will eventually |
| // make progress. |
| // |
| // Second, because next may be None, step 2 can fail. In that case |
| // we enter a recovery mode where we generate 2 new ticketers, one for |
| // next and one for the current ticketer. We then take the mutex a |
| // second time and redo the time check to see if a switch is still |
| // necessary. |
| // |
| // This somewhat convoluted approach ensures good availability of the |
| // mutex, by ensuring that the state is usable and the mutex not held |
| // during generation. It also ensures that, so long as the inner |
| // ticketer never generates panics during encryption/decryption, |
| // we are guaranteed to never panic when holding the mutex. |
| |
| let now = now.as_secs(); |
| let mut are_recovering = false; // Are we recovering from previous failure? |
| { |
| // Scope the mutex so we only take it for as long as needed |
| let mut state = self.state.lock().ok()?; |
| |
| // Fast path in case we do not need to switch to the next ticketer yet |
| if now <= state.next_switch_time { |
| return Some(state); |
| } |
| |
| // Make the switch, or mark for recovery if not possible |
| if let Some(next) = state.next.take() { |
| state.previous = Some(mem::replace(&mut state.current, next)); |
| state.next_switch_time = now.saturating_add(u64::from(self.lifetime)); |
| } else { |
| are_recovering = true; |
| } |
| } |
| |
| // We always need a next, so generate it now |
| let next = (self.generator)().ok()?; |
| if !are_recovering { |
| // Normal path, generate new next and place it in the state |
| let mut state = self.state.lock().ok()?; |
| state.next = Some(next); |
| Some(state) |
| } else { |
| // Recovering, generate also a new current ticketer, and modify state |
| // as needed. (we need to redo the time check, otherwise this might |
| // result in very rapid switching of ticketers) |
| let new_current = (self.generator)().ok()?; |
| let mut state = self.state.lock().ok()?; |
| state.next = Some(next); |
| if now > state.next_switch_time { |
| state.previous = Some(mem::replace(&mut state.current, new_current)); |
| state.next_switch_time = now.saturating_add(u64::from(self.lifetime)); |
| } |
| Some(state) |
| } |
| } |
| } |
| |
| impl ProducesTickets for TicketSwitcher { |
| fn lifetime(&self) -> u32 { |
| self.lifetime * 2 |
| } |
| |
| fn enabled(&self) -> bool { |
| true |
| } |
| |
| fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> { |
| let state = self.maybe_roll(TimeBase::now().ok()?)?; |
| |
| state.current.encrypt(message) |
| } |
| |
| fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> { |
| let state = self.maybe_roll(TimeBase::now().ok()?)?; |
| |
| // Decrypt with the current key; if that fails, try with the previous. |
| state |
| .current |
| .decrypt(ciphertext) |
| .or_else(|| { |
| state |
| .previous |
| .as_ref() |
| .and_then(|previous| previous.decrypt(ciphertext)) |
| }) |
| } |
| } |
| |
| /// A concrete, safe ticket creation mechanism. |
| pub struct Ticketer {} |
| |
| fn generate_inner() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed> { |
| Ok(Box::new(AeadTicketer::new()?)) |
| } |
| |
| impl Ticketer { |
| /// Make the recommended Ticketer. This produces tickets |
| /// with a 12 hour life and randomly generated keys. |
| /// |
| /// The encryption mechanism used in Chacha20Poly1305. |
| pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> { |
| Ok(Arc::new(TicketSwitcher::new(6 * 60 * 60, generate_inner)?)) |
| } |
| } |
| |
| #[test] |
| fn basic_pairwise_test() { |
| let t = Ticketer::new().unwrap(); |
| assert!(t.enabled()); |
| let cipher = t.encrypt(b"hello world").unwrap(); |
| let plain = t.decrypt(&cipher).unwrap(); |
| assert_eq!(plain, b"hello world"); |
| } |
| |
| #[test] |
| fn ticketswitcher_switching_test() { |
| let t = Arc::new(TicketSwitcher::new(1, generate_inner).unwrap()); |
| let now = TimeBase::now().unwrap(); |
| let cipher1 = t.encrypt(b"ticket 1").unwrap(); |
| assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1"); |
| { |
| // Trigger new ticketer |
| t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(10))); |
| } |
| let cipher2 = t.encrypt(b"ticket 2").unwrap(); |
| assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1"); |
| assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2"); |
| { |
| // Trigger new ticketer |
| t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(20))); |
| } |
| let cipher3 = t.encrypt(b"ticket 3").unwrap(); |
| assert!(t.decrypt(&cipher1).is_none()); |
| assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2"); |
| assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3"); |
| } |
| |
| #[cfg(test)] |
| fn fail_generator() -> Result<Box<dyn ProducesTickets>, rand::GetRandomFailed> { |
| Err(rand::GetRandomFailed) |
| } |
| |
| #[test] |
| fn ticketswitcher_recover_test() { |
| let mut t = TicketSwitcher::new(1, generate_inner).unwrap(); |
| let now = TimeBase::now().unwrap(); |
| let cipher1 = t.encrypt(b"ticket 1").unwrap(); |
| assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1"); |
| t.generator = fail_generator; |
| { |
| // Failed new ticketer |
| t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(10))); |
| } |
| t.generator = generate_inner; |
| let cipher2 = t.encrypt(b"ticket 2").unwrap(); |
| assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1"); |
| assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2"); |
| { |
| // recover |
| t.maybe_roll(TimeBase(now.0 + std::time::Duration::from_secs(20))); |
| } |
| let cipher3 = t.encrypt(b"ticket 3").unwrap(); |
| assert!(t.decrypt(&cipher1).is_none()); |
| assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2"); |
| assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3"); |
| } |