Files
rsauth2-proxy/src/crypto.rs
T

108 lines
3.3 KiB
Rust
Raw Normal View History

2026-05-05 13:10:16 +01:00
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Nonce};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use rand::RngCore;
#[derive(Clone)]
pub struct CookieCrypto {
cipher: Aes256Gcm,
}
impl CookieCrypto {
pub fn new(key_bytes: &[u8; 32]) -> Self {
Self {
cipher: Aes256Gcm::new_from_slice(key_bytes).expect("valid 32-byte key"),
}
}
pub fn encrypt(&self, plaintext: &[u8]) -> anyhow::Result<String> {
let mut nonce_bytes = [0u8; 12];
rand::thread_rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = self
.cipher
.encrypt(nonce, plaintext)
.map_err(|e| anyhow::anyhow!("encryption failed: {}", e))?;
let mut result = Vec::with_capacity(12 + ciphertext.len());
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&ciphertext);
Ok(URL_SAFE_NO_PAD.encode(&result))
}
pub fn decrypt(&self, encoded: &str) -> anyhow::Result<Vec<u8>> {
let data = URL_SAFE_NO_PAD.decode(encoded)?;
if data.len() < 13 {
anyhow::bail!("ciphertext too short");
}
let (nonce_bytes, ciphertext) = data.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
self.cipher
.decrypt(nonce, ciphertext)
.map_err(|e| anyhow::anyhow!("decryption failed: {}", e))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encrypt_decrypt_roundtrip() {
let crypto = CookieCrypto::new(&[0x42; 32]);
let plaintext = b"hello world";
let encrypted = crypto.encrypt(plaintext).unwrap();
let decrypted = crypto.decrypt(&encrypted).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn encrypt_produces_different_ciphertext_each_time() {
let crypto = CookieCrypto::new(&[0x42; 32]);
let a = crypto.encrypt(b"same").unwrap();
let b = crypto.encrypt(b"same").unwrap();
assert_ne!(a, b); // different nonces
}
#[test]
fn wrong_key_fails() {
let crypto1 = CookieCrypto::new(&[0x42; 32]);
let crypto2 = CookieCrypto::new(&[0x43; 32]);
let encrypted = crypto1.encrypt(b"hello").unwrap();
assert!(crypto2.decrypt(&encrypted).is_err());
}
#[test]
fn tampered_ciphertext_fails() {
let crypto = CookieCrypto::new(&[0x42; 32]);
let encrypted = crypto.encrypt(b"hello").unwrap();
let mut data = URL_SAFE_NO_PAD.decode(&encrypted).unwrap();
*data.last_mut().unwrap() ^= 0xFF;
let tampered = URL_SAFE_NO_PAD.encode(&data);
assert!(crypto.decrypt(&tampered).is_err());
}
#[test]
fn empty_plaintext_roundtrip() {
let crypto = CookieCrypto::new(&[0x42; 32]);
let encrypted = crypto.encrypt(b"").unwrap();
let decrypted = crypto.decrypt(&encrypted).unwrap();
assert!(decrypted.is_empty());
}
#[test]
fn short_ciphertext_rejected() {
let crypto = CookieCrypto::new(&[0x42; 32]);
assert!(crypto.decrypt("dG9vc2hvcnQ").is_err()); // "tooshort" base64
}
#[test]
fn invalid_base64_rejected() {
let crypto = CookieCrypto::new(&[0x42; 32]);
assert!(crypto.decrypt("not valid base64!!!").is_err());
}
}