#![doc = include_str!("../README.md")]
#![cfg_attr(not(feature="std"), no_std)]
pub type SigningKey = [u8; 32];
pub type Nonce = [u8; 12];
const NONCE_LENGTH: usize = core::mem::size_of::<Nonce>();
const SIGNATURE_LENGTH: usize = 16;
#[cfg(feature="rand")]
pub fn generate_signing_key() -> SigningKey {
let mut data = [0; 32];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut data);
data
}
#[cfg(feature="rand")]
pub fn generate_nonce() -> Nonce {
let mut data = [0; 12];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut data);
data
}
pub fn parse_cookie_header_value(header: &[u8]) -> impl Iterator<Item = (&str, &[u8])> {
header
.split(|c| *c == b';')
.map(|x| x.trim_ascii())
.filter_map(|x| {
let mut key_value_iterator = x.split(|c| *c == b'=').into_iter();
let key: &[u8] = key_value_iterator.next()?;
let key: &[u8] = key.trim_ascii();
let key: &str = core::str::from_utf8(key).ok()?;
let value: &[u8] = key_value_iterator.next()?.trim_ascii();
let value: &[u8] = value.strip_prefix(&[b'"']).unwrap_or(value);
let value: &[u8] = value.strip_suffix(&[b'"']).unwrap_or(value);
Some((key, value))
})
}
#[cfg(all(feature="std", feature="rand"))]
pub fn encode_cookie(key: SigningKey, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> String {
let nonce = generate_nonce();
let mut output = vec![0; encoded_buffer_size(value.as_ref().len()).expect("unreachable, len comes from a slice and no slice can be large enough to make this operation overflow")];
encode_cookie_advanced(key, nonce, name, value, &mut output).expect("unreachable, the buffer should always be correctly sized");
String::from_utf8(output).expect("unreachable, encode_cookie_advanced should always produce ascii data")
}
pub fn encode_cookie_advanced<'a>(
key: SigningKey,
nonce: Nonce,
name: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
output: &'a mut [u8],
) -> Result<(), OutputBufferWrongSize> {
let value: &[u8] = value.as_ref();
let expected_size =
match encoded_buffer_size(value.len()) {
None => return Err(OutputBufferWrongSize { expected_size: None, was: value.len(), }),
Some(x) if output.len() < x => return Err(OutputBufferWrongSize { expected_size: Some(x), was: value.len(), }),
Some(x) if x < output.len() => return Err(OutputBufferWrongSize { expected_size: Some(x), was: value.len(), }),
Some(x) => x,
};
let (nonce_slot, rest_of_output) = output.split_at_mut(NONCE_LENGTH);
let (encrypted_slot, rest_of_output) = rest_of_output.split_at_mut(value.len());
let (signature_slot, _rest_of_output) = rest_of_output.split_at_mut(SIGNATURE_LENGTH);
nonce_slot.copy_from_slice(&nonce);
encrypted_slot.copy_from_slice(value);
use aes_gcm::{AeadInPlace, KeyInit};
let key_array = aes_gcm::aead::generic_array::GenericArray::from_slice(&key);
let nonce_array = aes_gcm::aead::generic_array::GenericArray::from_slice(nonce_slot);
let encryptor = aes_gcm::Aes256Gcm::new(key_array);
let signature = encryptor
.encrypt_in_place_detached(&nonce_array, name.as_ref(), encrypted_slot)
.expect("failed to encrypt");
signature_slot.copy_from_slice(&signature);
let total_length = NONCE_LENGTH + value.len() + SIGNATURE_LENGTH;
encode_bytes_as_ascii(&mut output[..expected_size], total_length).unwrap();
Ok(())
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct OutputBufferWrongSize {
pub expected_size: Option<usize>,
pub was: usize,
}
pub const fn encoded_buffer_size(value_length: usize) -> Option<usize> {
if (usize::MAX / 2) - NONCE_LENGTH - SIGNATURE_LENGTH < value_length {
None
} else {
Some((NONCE_LENGTH + value_length + SIGNATURE_LENGTH) * 2)
}
}
pub const fn decode_buffer_size(value_length: usize) -> Option<usize> {
if value_length < (NONCE_LENGTH + SIGNATURE_LENGTH) * 2 {
None
} else {
Some((value_length / 2) - NONCE_LENGTH - SIGNATURE_LENGTH)
}
}
#[cfg(feature="std")]
pub fn decode_cookie(key: SigningKey, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
let Some(output_buffer_length) = decode_buffer_size(value.as_ref().len()) else {
return Err(DecodeError);
};
let mut output = vec![0; output_buffer_length];
match decode_cookie_advanced(key, name, value, &mut output) {
Ok(_) => Ok(output),
Err(reason) => Err(reason),
}
}
pub fn decode_cookie_advanced(key: SigningKey, name: impl AsRef<[u8]>, value: impl AsRef<[u8]>, output: &mut [u8]) -> Result<(), DecodeError> {
let value = value.as_ref();
if value.len() == 0 { return Err(DecodeError); }
if output.len() != decode_buffer_size(value.len()).ok_or(DecodeError)? {
return Err(DecodeError);
}
let merged_values_length = value.len() / 2;
let mut nonce = [0u8; NONCE_LENGTH];
decode_ascii_as_bytes(value, &mut nonce, 0, NONCE_LENGTH);
let mut signature = [0u8; SIGNATURE_LENGTH];
decode_ascii_as_bytes(value, &mut signature, merged_values_length - SIGNATURE_LENGTH, merged_values_length);
decode_ascii_as_bytes(value, output, NONCE_LENGTH, merged_values_length - SIGNATURE_LENGTH);
let key_array = aes_gcm::aead::generic_array::GenericArray::from_slice(&key);
let nonce_array = aes_gcm::aead::generic_array::GenericArray::from_slice(&nonce);
let signature = aes_gcm::aead::generic_array::GenericArray::from_slice(&signature);
use aes_gcm::KeyInit;
use aes_gcm::AeadInPlace;
aes_gcm::Aes256Gcm::new(key_array)
.decrypt_in_place_detached(
nonce_array,
name.as_ref(),
output,
signature,
)
.or(Err(DecodeError))
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct DecodeError;
fn encode_bytes_as_ascii<'a>(input: &'a mut [u8], length: usize) -> Option<&'a mut str> {
if input.len() < length * 2 {
return None;
}
let mut read_index = length;
let mut write_index = length * 2;
while 0 < read_index {
read_index -= 1;
write_index -= 2;
let byte = input[read_index];
let high = byte >> 4;
let low = byte & 0b1111;
input[write_index + 0] = high + b'a';
input[write_index + 1] = low + b'a';
}
let string =
core::str::from_utf8_mut(&mut input[..length * 2])
.expect("unreachable: code can only generate valid ascii");
Some(string)
}
fn decode_ascii_as_bytes<'a>(input: &[u8], output: &'a mut [u8], from: usize, to: usize) -> &'a mut [u8] {
if to < from {
return &mut output[..0];
}
let length = (to - from).min(output.len());
for (index, chunk) in input.chunks_exact(2).skip(from).take(length).enumerate() {
let [high, low] = chunk else { unreachable!() };
output[index] =
((high.saturating_sub(b'a')) & 0b1111) << 4
| ((low.saturating_sub(b'a')) & 0b1111);
}
&mut output[..length]
}
#[cfg(test)]
mod test {
use super::*;
pub fn init_random() -> oorandom::Rand64 {
let seed = rand::Rng::gen_range(&mut rand::thread_rng(), 100_000_000..999_999_999);
println!("Seed: {}", seed);
oorandom::Rand64::new(seed)
}
pub fn random_bytes(random: &mut oorandom::Rand64) -> Vec<u8> {
let length = random.rand_range(0..50);
let mut data = vec![0; length as usize];
for entry in data.iter_mut() {
*entry = random.rand_u64() as u8;
}
data
}
pub const fn const_unwrap(input: Option<usize>) -> usize {
match input {
None => panic!("Tried to unwrap a None value"),
Some(t) => t,
}
}
#[test]
fn test_ascii_encode() {
let mut random = test::init_random();
for _ in 0..100 {
let raw_data = test::random_bytes(&mut random);
if raw_data.len() == 0 { continue; }
let mut encoded_buffer = vec![0u8; raw_data.len() * 2];
encoded_buffer[..raw_data.len()].copy_from_slice(&raw_data);
encode_bytes_as_ascii(&mut encoded_buffer, raw_data.len()).unwrap();
for _ in 0..10 {
let from = random.rand_range(0..raw_data.len() as u64) as usize;
let to = random.rand_range(from as u64..raw_data.len() as u64) as usize;
let mut decoded_buffer = vec![0u8; to - from];
decode_ascii_as_bytes(&encoded_buffer, &mut decoded_buffer, from, to);
assert_eq!(&raw_data[from..to], &decoded_buffer);
}
}
}
#[test]
fn encode_decode_succeeds() {
let key = generate_signing_key();
let nonce = generate_nonce();
let name = "session";
let data = r#"{"id":5}"#;
let mut encoded = [0u8; const_unwrap(encoded_buffer_size(8))];
encode_cookie_advanced(key, nonce, name, data, &mut encoded).unwrap();
let mut decoded = [0u8; 8];
decode_cookie_advanced(key, name, encoded, &mut decoded).unwrap();
assert_eq!(decoded, data.as_bytes());
}
#[test]
fn returns_error_for_invalid_buffer_lengths() {
let key = generate_signing_key();
assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "", &mut []));
assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "a", &mut []));
assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "asdklfjaskdf", &mut []));
assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "asdklfjaskdf", &mut [0u8]));
assert_eq!(Err(DecodeError), decode_cookie_advanced(key, "", "asdklfjaskdf", &mut [0u8; 5]));
}
#[test]
fn different_keys_fails() {
let key_a = generate_signing_key();
let nonce = generate_nonce();
let name = "session";
let data = r#"{"id":5}"#;
let mut encoded = [0u8; const_unwrap(encoded_buffer_size(8))];
encode_cookie_advanced(key_a, nonce, name, data, &mut encoded).unwrap();
let key_b = generate_signing_key();
let mut decoded = [0u8; 8];
let decode_result = decode_cookie_advanced(key_b, name, encoded, &mut decoded);
assert_eq!(decode_result, Err(DecodeError));
}
#[test]
fn different_names_fails() {
let key = generate_signing_key();
let nonce = generate_nonce();
let name_a = "session";
let data = r#"{"id":5}"#;
let mut encoded = [0u8; const_unwrap(encoded_buffer_size(8))];
encode_cookie_advanced(key, nonce, name_a, data, &mut encoded).unwrap();
let name_b = "laskdjf";
let mut decoded = [0u8; 8];
let decode_result = decode_cookie_advanced(key, name_b, encoded, &mut decoded);
assert_eq!(decode_result, Err(DecodeError));
}
#[test]
fn identical_values_have_different_ciphers() {
let key = generate_signing_key();
let name = "session";
let data = "which wolf do you feed?";
let mut encoded_1 = [0u8; const_unwrap(encoded_buffer_size(23))];
encode_cookie_advanced(key, generate_nonce(), name, data, &mut encoded_1).unwrap();
let mut encoded_2 = [0u8; const_unwrap(encoded_buffer_size(23))];
encode_cookie_advanced(key, generate_nonce(), name, data, &mut encoded_2).unwrap();
assert_ne!(encoded_1, encoded_2);
}
#[test]
fn parses_spaceless_header() {
let header = b"session=213lkj1;another=3829";
let mut iterator = parse_cookie_header_value(header);
let (name, value) = iterator.next().unwrap();
assert_eq!(name, "session");
assert_eq!(value, b"213lkj1");
let (name, value) = iterator.next().unwrap();
assert_eq!(name, "another");
assert_eq!(value, b"3829");
}
#[test]
fn parses_spaced_header() {
let header = b"session = 123kj; sakjdf = klsjdf23";
let mut iterator = parse_cookie_header_value(header);
let (name, value) = iterator.next().unwrap();
assert_eq!(name, "session");
assert_eq!(value, b"123kj");
let (name, value) = iterator.next().unwrap();
assert_eq!(name, "sakjdf");
assert_eq!(value, b"klsjdf23");
}
#[test]
fn strips_value_quotes() {
let header = b"session=\"alkjs\"";
let mut iterator = parse_cookie_header_value(header);
let (name, value) = iterator.next().unwrap();
assert_eq!(name, "session");
assert_eq!(value, b"alkjs");
}
#[test]
fn includes_name_quotes() {
let header = b"\"session\"=asdf";
let mut iterator = parse_cookie_header_value(header);
let (name, value) = iterator.next().unwrap();
assert_eq!(name, "\"session\"");
assert_eq!(value, b"asdf");
}
}