use crate::SessionError; use aes_gcm::aead::array::typenum::consts::{U12, U16}; use aes_gcm::aead::array::Array; use aes_gcm::aead::inout::InOutBuf; use aes_gcm::aead::{AeadInOut, KeyInit}; use aes_gcm::{Aes256Gcm, Nonce}; use base64::{engine::general_purpose, Engine as _}; use cookie::Key; use rand::TryRng; pub(crate) const NONCE_LEN: usize = 12; pub(crate) const TAG_LEN: usize = 16; pub(crate) const KEY_LEN: usize = 32; type GcmNonce = Nonce; type GcmTag = Array; fn cipher(key: &Key) -> Result { Aes256Gcm::new_from_slice(key.encryption()).map_err(|_| "encryption failure!") } fn bad_seal(error: impl std::fmt::Display) -> SessionError { SessionError::GenericNotSupportedError(format!("invalid key/nonce/value: {error}")) } ///Used to encrypt the database Values pub(crate) fn encrypt(name: &str, value: &str, key: &Key) -> Result { let val = value.as_bytes(); let mut data = vec![0; NONCE_LEN + val.len() + TAG_LEN]; let (nonce, in_out) = data.split_at_mut(NONCE_LEN); let (in_out, tag) = in_out.split_at_mut(val.len()); in_out.copy_from_slice(val); let mut rng = rand::rng(); rng.try_fill_bytes(nonce) .map_err(|_| "couldn't random fill nonce")?; let nonce = GcmNonce::try_from(&*nonce).map_err(|_| "invalid nonce length")?; // Use the UUID to preform actual cookie Sealing. let aad = name.as_bytes(); let tag_out = cipher(key)? .encrypt_inout_detached(&nonce, aad, InOutBuf::from(in_out)) .map_err(|_| "encryption failure!")?; tag.copy_from_slice(tag_out.as_slice()); Ok(general_purpose::STANDARD.encode(&data)) } ///Used to decrypt the database Values. pub(crate) fn decrypt(name: &str, value: &str, key: &Key) -> Result { let mut data = general_purpose::STANDARD.decode(value)?; if data.len() <= NONCE_LEN + TAG_LEN { return Err(SessionError::GenericNotSupportedError( "length of decoded data is <= NONCE_LEN + TAG_LEN".to_owned(), )); } let (nonce, ciphertext) = data.split_at_mut(NONCE_LEN); let nonce = GcmNonce::try_from(&*nonce).map_err(bad_seal)?; let (msg, tag) = ciphertext.split_at_mut(ciphertext.len() - TAG_LEN); let tag = GcmTag::try_from(&*tag).map_err(bad_seal)?; cipher(key) .map_err(|error| SessionError::GenericNotSupportedError(error.to_string()))? // `InOutBuf` borrows the buffer mutably for the call only; the plaintext // is readable again afterwards because the borrow ends here. .decrypt_inout_detached(&nonce, name.as_bytes(), InOutBuf::from(&mut *msg), &tag) .map_err(bad_seal)?; Ok(String::from_utf8(msg.to_vec())?) } #[cfg(test)] mod tests { use super::*; fn key() -> Key { Key::from(&[7u8; 64]) } #[test] fn round_trips_and_binds_the_name_as_aad() { let sealed = encrypt("cookie:a", "hello world", &key()).expect("encrypt"); assert_eq!( decrypt("cookie:a", &sealed, &key()).expect("decrypt"), "hello world" ); // The associated data is authenticated, so a different name cannot decrypt. assert!(decrypt("cookie:b", &sealed, &key()).is_err()); } #[test] fn rejects_truncated_or_tampered_payloads() { assert!(decrypt("n", "AAAA", &key()).is_err()); let sealed = encrypt("n", "value", &key()).expect("encrypt"); let mut raw = general_purpose::STANDARD.decode(&sealed).expect("base64"); let last = raw.len() - 1; raw[last] ^= 0xff; let tampered = general_purpose::STANDARD.encode(&raw); assert!(decrypt("n", &tampered, &key()).is_err()); } #[test] fn ciphertext_layout_is_nonce_msg_tag() { let sealed = encrypt("n", "abcd", &key()).expect("encrypt"); let raw = general_purpose::STANDARD.decode(&sealed).expect("base64"); assert_eq!(raw.len(), NONCE_LEN + 4 + TAG_LEN); } }