use alloc::boxed::Box;
use alloc::vec::Vec;
use core::fmt;
use core::fmt::{Debug, Formatter};
use core::sync::atomic::{AtomicUsize, Ordering};
use aws_lc_rs::cipher::{
DecryptionContext, PaddedBlockDecryptingKey, PaddedBlockEncryptingKey, UnboundCipherKey,
AES_256, AES_256_KEY_LEN, AES_CBC_IV_LEN,
};
use aws_lc_rs::{hmac, iv};
use super::ring_like::rand::{SecureRandom, SystemRandom};
use super::unspecified_err;
use crate::error::Error;
#[cfg(debug_assertions)]
use crate::log::debug;
use crate::polyfill::try_split_at;
use crate::rand::GetRandomFailed;
use crate::server::ProducesTickets;
use crate::sync::Arc;
pub struct Ticketer {}
impl Ticketer {
#[cfg(feature = "std")]
pub fn new() -> Result<Arc<dyn ProducesTickets>, Error> {
Ok(Arc::new(crate::ticketer::TicketRotator::new(
6 * 60 * 60,
make_ticket_generator,
)?))
}
#[cfg(not(feature = "std"))]
pub fn new<M: crate::lock::MakeMutex>(
time_provider: &'static dyn TimeProvider,
) -> Result<Arc<dyn ProducesTickets>, Error> {
Ok(Arc::new(crate::ticketer::TicketSwitcher::new::<M>(
6 * 60 * 60,
make_ticket_generator,
time_provider,
)?))
}
}
fn make_ticket_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
Ok(Box::new(
Rfc5077Ticketer::new().map_err(|_| GetRandomFailed)?,
))
}
struct Rfc5077Ticketer {
aes_encrypt_key: PaddedBlockEncryptingKey,
aes_decrypt_key: PaddedBlockDecryptingKey,
hmac_key: hmac::Key,
key_name: [u8; 16],
lifetime: u32,
maximum_ciphertext_len: AtomicUsize,
}
impl Rfc5077Ticketer {
fn new() -> Result<Self, Error> {
let rand = SystemRandom::new();
let mut aes_key = [0u8; AES_256_KEY_LEN];
rand.fill(&mut aes_key)
.map_err(|_| GetRandomFailed)?;
let aes_encrypt_key =
UnboundCipherKey::new(&AES_256, &aes_key[..]).map_err(unspecified_err)?;
let aes_encrypt_key =
PaddedBlockEncryptingKey::cbc_pkcs7(aes_encrypt_key).map_err(unspecified_err)?;
let aes_decrypt_key =
UnboundCipherKey::new(&AES_256, &aes_key[..]).map_err(unspecified_err)?;
let aes_decrypt_key =
PaddedBlockDecryptingKey::cbc_pkcs7(aes_decrypt_key).map_err(unspecified_err)?;
let hmac_key = hmac::Key::generate(hmac::HMAC_SHA256, &rand).map_err(unspecified_err)?;
let mut key_name = [0u8; 16];
rand.fill(&mut key_name)
.map_err(|_| GetRandomFailed)?;
Ok(Self {
aes_encrypt_key,
aes_decrypt_key,
hmac_key,
key_name,
lifetime: 60 * 60 * 12,
maximum_ciphertext_len: AtomicUsize::new(0),
})
}
}
impl ProducesTickets for Rfc5077Ticketer {
fn enabled(&self) -> bool {
true
}
fn lifetime(&self) -> u32 {
self.lifetime
}
fn encrypt(&self, message: &[u8]) -> Option<Vec<u8>> {
let mut encrypted_state = Vec::from(message);
let dec_ctx = self
.aes_encrypt_key
.encrypt(&mut encrypted_state)
.ok()?;
let iv: &[u8] = (&dec_ctx).try_into().ok()?;
let mut hmac_data =
Vec::with_capacity(self.key_name.len() + iv.len() + 2 + encrypted_state.len());
hmac_data.extend(&self.key_name);
hmac_data.extend(iv);
hmac_data.extend(
u16::try_from(encrypted_state.len())
.ok()?
.to_be_bytes(),
);
hmac_data.extend(&encrypted_state);
let tag = hmac::sign(&self.hmac_key, &hmac_data);
let tag = tag.as_ref();
let mut ciphertext =
Vec::with_capacity(self.key_name.len() + iv.len() + encrypted_state.len() + tag.len());
ciphertext.extend(self.key_name);
ciphertext.extend(iv);
ciphertext.extend(encrypted_state);
ciphertext.extend(tag);
self.maximum_ciphertext_len
.fetch_max(ciphertext.len(), Ordering::SeqCst);
Some(ciphertext)
}
fn decrypt(&self, ciphertext: &[u8]) -> Option<Vec<u8>> {
if ciphertext.len()
> self
.maximum_ciphertext_len
.load(Ordering::SeqCst)
{
#[cfg(debug_assertions)]
debug!("rejected over-length ticket");
return None;
}
let (alleged_key_name, ciphertext) = try_split_at(ciphertext, self.key_name.len())?;
let (iv, ciphertext) = try_split_at(ciphertext, AES_CBC_IV_LEN)?;
let tag_len = self
.hmac_key
.algorithm()
.digest_algorithm()
.output_len();
let (enc_state, mac) = try_split_at(ciphertext, ciphertext.len() - tag_len)?;
let mut hmac_data =
Vec::with_capacity(alleged_key_name.len() + iv.len() + 2 + enc_state.len());
hmac_data.extend(alleged_key_name);
hmac_data.extend(iv);
hmac_data.extend(
u16::try_from(enc_state.len())
.ok()?
.to_be_bytes(),
);
hmac_data.extend(enc_state);
hmac::verify(&self.hmac_key, &hmac_data, mac).ok()?;
let iv = iv::FixedLength::try_from(iv).ok()?;
let dec_context = DecryptionContext::Iv128(iv);
let mut out = Vec::from(enc_state);
let plaintext = self
.aes_decrypt_key
.decrypt(&mut out, dec_context)
.ok()?;
Some(plaintext.into())
}
}
impl Debug for Rfc5077Ticketer {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Rfc5077Ticketer")
.field("lifetime", &self.lifetime)
.finish()
}
}
#[cfg(test)]
mod tests {
use core::time::Duration;
use pki_types::UnixTime;
use super::*;
#[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 refuses_decrypt_before_encrypt() {
let t = Ticketer::new().unwrap();
assert_eq!(t.decrypt(b"hello"), None);
}
#[test]
fn refuses_decrypt_larger_than_largest_encryption() {
let t = Ticketer::new().unwrap();
let mut cipher = t.encrypt(b"hello world").unwrap();
assert_eq!(t.decrypt(&cipher), Some(b"hello world".to_vec()));
cipher.push(0);
assert_eq!(t.decrypt(&cipher), None);
}
#[test]
fn ticketrotator_switching_test() {
let t = Arc::new(crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap());
let now = UnixTime::now();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_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");
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_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");
}
#[test]
fn ticketrotator_remains_usable_over_temporary_ticketer_creation_failure() {
let mut t = crate::ticketer::TicketRotator::new(1, make_ticket_generator).unwrap();
let now = UnixTime::now();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
t.generator = fail_generator;
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_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");
t.generator = make_ticket_generator;
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_secs() + 20,
)));
}
let cipher3 = t.encrypt(b"ticket 3").unwrap();
assert!(t.decrypt(&cipher1).is_some());
assert_eq!(t.decrypt(&cipher2).unwrap(), b"ticket 2");
assert_eq!(t.decrypt(&cipher3).unwrap(), b"ticket 3");
}
#[test]
fn ticketswitcher_switching_test() {
#[expect(deprecated)]
let t = Arc::new(crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap());
let now = UnixTime::now();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_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");
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_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");
}
#[test]
fn ticketswitcher_recover_test() {
#[expect(deprecated)]
let mut t = crate::ticketer::TicketSwitcher::new(1, make_ticket_generator).unwrap();
let now = UnixTime::now();
let cipher1 = t.encrypt(b"ticket 1").unwrap();
assert_eq!(t.decrypt(&cipher1).unwrap(), b"ticket 1");
t.generator = fail_generator;
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_secs() + 10,
)));
}
t.generator = make_ticket_generator;
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");
{
t.maybe_roll(UnixTime::since_unix_epoch(Duration::from_secs(
now.as_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");
}
#[test]
fn rfc5077ticketer_is_debug_and_producestickets() {
use alloc::format;
use super::*;
let t = make_ticket_generator().unwrap();
assert_eq!(format!("{:?}", t), "Rfc5077Ticketer { lifetime: 43200 }");
assert!(t.enabled());
assert_eq!(t.lifetime(), 43200);
}
fn fail_generator() -> Result<Box<dyn ProducesTickets>, GetRandomFailed> {
Err(GetRandomFailed)
}
}