fad06fd
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<U12>;
type GcmTag = Array<u8, U16>;
fn cipher(key: &Key) -> Result<Aes256Gcm, &'static str> {
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<String, &'static str> {
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<String, SessionError> {
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);
}
}