use crate::error::{Error, Result};
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
use sha1::{Digest, Sha1};
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
fn custom_base64_encode(data: &[u8]) -> String {
use base64::{engine::general_purpose::STANDARD, Engine as _};
let standard = STANDARD.encode(data);
standard
.replace('+', "-")
.replace('/', ".")
.replace('=', "_")
}
fn derive_encryption_key(password: &str) -> [u8; 16] {
let mut hasher = Sha1::new();
hasher.update(password.as_bytes());
let hash = hasher.finalize();
let hex_string = format!("{:x}", hash);
let key_hex = &hex_string[..32];
let mut key = [0u8; 16];
for (i, chunk) in key_hex.as_bytes().chunks(2).enumerate() {
let hex_byte = std::str::from_utf8(chunk).unwrap();
key[i] = u8::from_str_radix(hex_byte, 16).unwrap();
}
key
}
pub fn encrypt_text(plaintext: &str, password: &str, iv: &[u8; 16]) -> Result<String> {
let key = derive_encryption_key(password);
let plaintext_bytes = plaintext.as_bytes();
let cipher = Aes128CbcEnc::new(&key.into(), iv.into());
let mut buffer = vec![0u8; plaintext_bytes.len() + 16];
buffer[..plaintext_bytes.len()].copy_from_slice(plaintext_bytes);
let ciphertext = cipher
.encrypt_padded_mut::<Pkcs7>(&mut buffer, plaintext_bytes.len())
.map_err(|_| Error::InvalidConfig("Encryption failed".to_string()))?;
Ok(custom_base64_encode(ciphertext))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_custom_base64_encode() {
let input = b"Hello, World!";
let encoded = custom_base64_encode(input);
assert!(!encoded.contains('+'));
assert!(!encoded.contains('/'));
assert!(!encoded.contains('='));
}
#[test]
fn test_derive_encryption_key() {
let password = "test_password";
let key = derive_encryption_key(password);
assert_eq!(key.len(), 16);
let key2 = derive_encryption_key(password);
assert_eq!(key, key2);
let key3 = derive_encryption_key("different_password");
assert_ne!(key, key3);
}
#[test]
fn test_encrypt_text() {
let plaintext = "Hello, World!";
let password = "test_password";
let iv = [0u8; 16];
let result = encrypt_text(plaintext, password, &iv);
assert!(result.is_ok());
let encrypted = result.unwrap();
assert!(!encrypted.is_empty());
assert_ne!(encrypted, plaintext);
assert!(!encrypted.contains('+'));
assert!(!encrypted.contains('/'));
assert!(!encrypted.contains('='));
}
#[test]
fn test_encrypt_with_random_iv() {
use rand::Rng;
let plaintext = "Secret message";
let password = "secure_password";
let mut iv = [0u8; 16];
rand::thread_rng().fill(&mut iv);
let result = encrypt_text(plaintext, password, &iv);
assert!(result.is_ok());
let mut iv2 = [0u8; 16];
rand::thread_rng().fill(&mut iv2);
let result2 = encrypt_text(plaintext, password, &iv2);
assert!(result2.is_ok());
assert_ne!(result.unwrap(), result2.unwrap());
}
#[test]
fn test_encrypt_empty_string() {
let plaintext = "";
let password = "test_password";
let iv = [0u8; 16];
let result = encrypt_text(plaintext, password, &iv);
assert!(result.is_ok());
let encrypted = result.unwrap();
assert!(!encrypted.is_empty());
}
#[test]
fn test_encrypt_long_message() {
let plaintext = "This is a much longer message that spans multiple AES blocks and should be properly encrypted with PKCS7 padding.";
let password = "test_password";
let iv = [0u8; 16];
let result = encrypt_text(plaintext, password, &iv);
assert!(result.is_ok());
let encrypted = result.unwrap();
assert!(!encrypted.is_empty());
}
}