use aes_gcm::{
Aes256Gcm,
aead::{Aead, AeadCore, KeyInit},
};
use base64::Engine;
use crate::error::ServiceError;
const NONCE_LEN: usize = 12;
fn cipher(key: &[u8]) -> Result<Aes256Gcm, ServiceError> {
if key.len() != 32 {
return Err(ServiceError::Configuration(
"encryption key must be exactly 32 bytes".into(),
));
}
Aes256Gcm::new_from_slice(key).map_err(|e| {
ServiceError::Configuration(format!("invalid encryption key: {e}"))
})
}
pub fn encrypt(plaintext: &str, key: &[u8]) -> Result<String, ServiceError> {
let cipher = cipher(key)?;
let nonce = Aes256Gcm::generate_nonce(&mut rand_core::OsRng);
let mut buffer = nonce.to_vec();
let ciphertext = cipher.encrypt(&nonce, plaintext.as_bytes()).map_err(|e| {
ServiceError::Configuration(format!("encrypt: {e}"))
})?;
buffer.extend_from_slice(&ciphertext);
Ok(base64::engine::general_purpose::STANDARD.encode(&buffer))
}
pub fn decrypt(ciphertext_b64: &str, key: &[u8]) -> Result<String, ServiceError> {
let cipher = cipher(key)?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(ciphertext_b64)
.map_err(|e| {
ServiceError::Configuration(format!("base64: {e}"))
})?;
if bytes.len() < NONCE_LEN {
return Err(ServiceError::Configuration(
"ciphertext too short".into(),
));
}
let (nonce, ciphertext) = bytes.split_at(NONCE_LEN);
#[allow(deprecated)]
let nonce = aes_gcm::Nonce::from_slice(nonce);
let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|e| {
ServiceError::Configuration(format!("decrypt: {e}"))
})?;
String::from_utf8(plaintext).map_err(|e| {
ServiceError::Configuration(format!("decrypt utf8: {e}"))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_encryption() {
let key = [0u8; 32];
let encrypted = encrypt("super secret", &key).unwrap();
assert_ne!(encrypted, "super secret");
let decrypted = decrypt(&encrypted, &key).unwrap();
assert_eq!(decrypted, "super secret");
}
#[test]
fn wrong_key_fails_decryption() {
let key = [0u8; 32];
let encrypted = encrypt("super secret", &key).unwrap();
let wrong_key = [1u8; 32];
assert!(decrypt(&encrypted, &wrong_key).is_err());
}
#[test]
fn short_key_rejected() {
assert!(encrypt("x", &[0u8; 16]).is_err());
}
}