use crate::cipher::{Aes256, Gcm};
use crate::rng::RngCore;
use alloc::vec::Vec;
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;
const MIN_PLAIN_LEN: usize = 2 + 48 + 8 + 1 + 1;
#[derive(Clone, Debug)]
pub(crate) struct Ticket12Plaintext {
pub(crate) cipher_suite: u16,
pub(crate) master_secret: [u8; 48],
pub(crate) creation_time: u64,
pub(crate) ems_used: bool,
pub(crate) alpn: Option<Vec<u8>>,
}
impl Ticket12Plaintext {
pub(crate) fn encode(&self) -> Vec<u8> {
let alpn = self.alpn.as_deref().unwrap_or(&[]);
let mut out = Vec::with_capacity(MIN_PLAIN_LEN + alpn.len());
out.extend_from_slice(&self.cipher_suite.to_be_bytes());
out.extend_from_slice(&self.master_secret);
out.extend_from_slice(&self.creation_time.to_be_bytes());
out.push(if self.ems_used { 1 } else { 0 });
out.push(alpn.len() as u8);
out.extend_from_slice(alpn);
out
}
pub(crate) fn decode(buf: &[u8]) -> Option<Self> {
if buf.len() < MIN_PLAIN_LEN {
return None;
}
let cipher_suite = u16::from_be_bytes([buf[0], buf[1]]);
let mut master_secret = [0u8; 48];
master_secret.copy_from_slice(&buf[2..50]);
let creation_time = u64::from_be_bytes([
buf[50], buf[51], buf[52], buf[53], buf[54], buf[55], buf[56], buf[57],
]);
let ems_used = match buf[58] {
0 => false,
1 => true,
_ => return None,
};
let alpn_len = buf[59] as usize;
if buf.len() != MIN_PLAIN_LEN + alpn_len {
return None;
}
let alpn = if alpn_len == 0 {
None
} else {
Some(buf[60..60 + alpn_len].to_vec())
};
Some(Ticket12Plaintext {
cipher_suite,
master_secret,
creation_time,
ems_used,
alpn,
})
}
}
pub(crate) fn seal_ticket<R: RngCore>(rng: &mut R, key: &[u8; 32], plain: &[u8]) -> Vec<u8> {
let mut nonce = [0u8; NONCE_LEN];
rng.fill_bytes(&mut nonce);
let gcm = Gcm::new(Aes256::new(key));
let mut buf = plain.to_vec();
let tag = gcm.encrypt(&nonce, &[], &mut buf);
let mut ticket = Vec::with_capacity(NONCE_LEN + buf.len() + TAG_LEN);
ticket.extend_from_slice(&nonce);
ticket.extend_from_slice(&buf);
ticket.extend_from_slice(&tag);
ticket
}
pub(crate) fn open_ticket(key: &[u8; 32], ticket: &[u8]) -> Option<Vec<u8>> {
if ticket.len() < NONCE_LEN + TAG_LEN {
return None;
}
let nonce: &[u8; NONCE_LEN] = ticket[..NONCE_LEN].try_into().ok()?;
let body = &ticket[NONCE_LEN..];
let (ct, tag_slice) = body.split_at(body.len() - TAG_LEN);
let tag: &[u8; TAG_LEN] = tag_slice.try_into().ok()?;
let mut buf = ct.to_vec();
let gcm = Gcm::new(Aes256::new(key));
gcm.decrypt(nonce, &[], &mut buf, tag).ok()?;
Some(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::Sha256;
use crate::rng::HmacDrbg;
#[test]
fn plaintext_roundtrip_no_alpn() {
let p = Ticket12Plaintext {
cipher_suite: 0xC02F,
master_secret: [0xa5; 48],
creation_time: 0x1122334455667788,
ems_used: true,
alpn: None,
};
let buf = p.encode();
let dec = Ticket12Plaintext::decode(&buf).unwrap();
assert_eq!(dec.cipher_suite, p.cipher_suite);
assert_eq!(dec.master_secret, p.master_secret);
assert_eq!(dec.creation_time, p.creation_time);
assert!(dec.ems_used);
assert!(dec.alpn.is_none());
}
#[test]
fn plaintext_roundtrip_with_alpn() {
let p = Ticket12Plaintext {
cipher_suite: 0xCCA9,
master_secret: [0x3c; 48],
creation_time: 1_700_000_000,
ems_used: false,
alpn: Some(b"h2".to_vec()),
};
let buf = p.encode();
let dec = Ticket12Plaintext::decode(&buf).unwrap();
assert_eq!(dec.cipher_suite, p.cipher_suite);
assert!(!dec.ems_used);
assert_eq!(dec.alpn.as_deref(), Some(b"h2".as_ref()));
}
#[test]
fn plaintext_rejects_truncated() {
assert!(Ticket12Plaintext::decode(&[]).is_none());
assert!(Ticket12Plaintext::decode(&[0u8; 58]).is_none());
}
#[test]
fn plaintext_rejects_bad_ems_flag() {
let p = Ticket12Plaintext {
cipher_suite: 0xC02F,
master_secret: [0x11; 48],
creation_time: 1,
ems_used: false,
alpn: None,
};
let mut buf = p.encode();
buf[58] = 2; assert!(Ticket12Plaintext::decode(&buf).is_none());
}
#[test]
fn seal_open_roundtrip() {
let mut rng = HmacDrbg::<Sha256>::new(b"ticket12", b"nonce", &[]);
let key = [0x42u8; 32];
let plain = b"the quick brown fox jumps over the lazy dog";
let ticket = seal_ticket(&mut rng, &key, plain);
assert!(ticket.len() > NONCE_LEN + TAG_LEN);
let recovered = open_ticket(&key, &ticket).unwrap();
assert_eq!(recovered, plain);
}
#[test]
fn open_ticket_rejects_tampering() {
let mut rng = HmacDrbg::<Sha256>::new(b"ticket12-tamper", b"nonce", &[]);
let key = [0x42u8; 32];
let plain = b"payload";
let mut ticket = seal_ticket(&mut rng, &key, plain);
let i = ticket.len() / 2;
ticket[i] ^= 1;
assert!(open_ticket(&key, &ticket).is_none());
}
#[test]
fn open_ticket_rejects_short() {
let key = [0u8; 32];
assert!(open_ticket(&key, &[]).is_none());
assert!(open_ticket(&key, &[0u8; 12]).is_none());
}
}