use crate::{CorteqError, Result, TenantContext};
use aes_gcm::{
aead::{Aead, KeyInit, OsRng},
Aes256Gcm, Nonce,
};
use rand::RngCore;
use serde::{Deserialize, Serialize};
const NONCE_SIZE: usize = 12;
#[derive(Debug, Clone)]
pub struct EncryptionService;
impl EncryptionService {
pub fn new() -> Self {
Self
}
pub fn encrypt(&self, plaintext: &[u8], tenant_ctx: &TenantContext) -> Result<EncryptedData> {
let key = derive_key(&tenant_ctx.encryption_key_id)?;
let cipher = Aes256Gcm::new(&key);
let mut nonce_bytes = [0u8; NONCE_SIZE];
OsRng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|e| CorteqError::EncryptionError(format!("Encryption failed: {e}")))?;
Ok(EncryptedData {
nonce: nonce_bytes.to_vec(),
ciphertext,
})
}
pub fn decrypt(
&self,
encrypted: &EncryptedData,
tenant_ctx: &TenantContext,
) -> Result<Vec<u8>> {
let key = derive_key(&tenant_ctx.encryption_key_id)?;
let cipher = Aes256Gcm::new(&key);
if encrypted.nonce.len() != NONCE_SIZE {
return Err(CorteqError::EncryptionError(format!(
"Invalid nonce size: expected {}, got {}",
NONCE_SIZE,
encrypted.nonce.len()
)));
}
let nonce = Nonce::from_slice(&encrypted.nonce);
let plaintext = cipher
.decrypt(nonce, encrypted.ciphertext.as_ref())
.map_err(|e| {
CorteqError::EncryptionError(format!(
"Decryption failed (wrong key or tampered data): {e}"
))
})?;
Ok(plaintext)
}
}
impl Default for EncryptionService {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EncryptedData {
nonce: Vec<u8>,
ciphertext: Vec<u8>,
}
impl EncryptedData {
pub fn nonce(&self) -> &[u8] {
&self.nonce
}
pub fn ciphertext(&self) -> &[u8] {
&self.ciphertext
}
#[doc(hidden)]
pub fn set_ciphertext(&mut self, ciphertext: Vec<u8>) {
self.ciphertext = ciphertext;
}
#[doc(hidden)]
pub fn set_nonce(&mut self, nonce: Vec<u8>) {
self.nonce = nonce;
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let nonce_len = self.nonce.len() as u32;
bytes.extend_from_slice(&nonce_len.to_le_bytes());
bytes.extend_from_slice(&self.nonce);
bytes.extend_from_slice(&self.ciphertext);
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() < 4 {
return Err(CorteqError::EncryptionError(
"Invalid encrypted data: too short".to_string(),
));
}
let nonce_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
if bytes.len() < 4 + nonce_len {
return Err(CorteqError::EncryptionError(format!(
"Invalid encrypted data: expected {} bytes, got {}",
4 + nonce_len,
bytes.len()
)));
}
let nonce = bytes[4..4 + nonce_len].to_vec();
let ciphertext = bytes[4 + nonce_len..].to_vec();
Ok(EncryptedData { nonce, ciphertext })
}
}
fn derive_key(encryption_key_id: &str) -> Result<aes_gcm::Key<Aes256Gcm>> {
use ring::digest;
let hash = digest::digest(&digest::SHA256, encryption_key_id.as_bytes());
let hash_bytes = hash.as_ref();
assert_eq!(hash_bytes.len(), 32, "SHA-256 should produce 32 bytes");
let mut key_bytes = [0u8; 32];
key_bytes.copy_from_slice(hash_bytes);
Ok(aes_gcm::Key::<Aes256Gcm>::from(key_bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_derivation_consistent() {
let key_id = "test-key-123";
let key1 = derive_key(key_id).expect("Key derivation 1 failed");
let key2 = derive_key(key_id).expect("Key derivation 2 failed");
assert_eq!(
key1.as_slice(),
key2.as_slice(),
"Same key ID should produce same derived key"
);
}
#[test]
fn test_key_derivation_different_inputs() {
let key1 = derive_key("key-1").expect("Key derivation 1 failed");
let key2 = derive_key("key-2").expect("Key derivation 2 failed");
assert_ne!(
key1.as_slice(),
key2.as_slice(),
"Different key IDs should produce different derived keys"
);
}
#[test]
fn test_encrypted_data_serialization() {
let data = EncryptedData {
nonce: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
ciphertext: vec![100, 101, 102, 103, 104],
};
let bytes = data.to_bytes();
let restored = EncryptedData::from_bytes(&bytes).expect("Deserialization failed");
assert_eq!(restored.nonce, data.nonce);
assert_eq!(restored.ciphertext, data.ciphertext);
}
}