use chacha20poly1305::{
ChaCha20Poly1305, Nonce,
aead::{Aead, KeyInit, Payload},
};
use rskit_errors::{AppError, AppResult, ErrorCode};
use sha2::Sha256;
use zeroize::Zeroize;
use crate::envelope::{Envelope, KEY_LEN, NONCE_SIZE, PBKDF2_ITERATIONS, SALT_SIZE};
use crate::traits::{Algorithm, Encryptor};
pub struct ChaCha20Encryptor {
passphrase: Vec<u8>,
}
impl ChaCha20Encryptor {
pub fn new(key: &[u8]) -> Self {
Self {
passphrase: key.to_vec(),
}
}
fn derive_key(&self, salt: &[u8]) -> [u8; KEY_LEN] {
let mut key = [0u8; KEY_LEN];
pbkdf2::pbkdf2_hmac::<Sha256>(&self.passphrase, salt, PBKDF2_ITERATIONS, &mut key);
key
}
}
impl Drop for ChaCha20Encryptor {
fn drop(&mut self) {
self.passphrase.zeroize();
}
}
impl Encryptor for ChaCha20Encryptor {
fn encrypt(&self, plaintext: &[u8]) -> AppResult<String> {
let salt: [u8; SALT_SIZE] = rand::random();
let mut key_bytes = self.derive_key(&salt);
let cipher = ChaCha20Poly1305::new((&key_bytes[..]).into());
key_bytes.zeroize();
let nonce_bytes: [u8; NONCE_SIZE] = rand::random();
let nonce = Nonce::from_slice(&nonce_bytes);
let aad =
crate::envelope::associated_data(Algorithm::ChaCha20Poly1305, &salt, &nonce_bytes);
let ciphertext = cipher
.encrypt(
nonce,
Payload {
msg: plaintext,
aad: &aad,
},
)
.map_err(|_| AppError::new(ErrorCode::Internal, "encryption failed"))?;
Ok(Envelope::encode(
Algorithm::ChaCha20Poly1305,
&salt,
&nonce_bytes,
&ciphertext,
))
}
fn decrypt(&self, ciphertext: &str) -> AppResult<Vec<u8>> {
let envelope = Envelope::decode(ciphertext)?;
if envelope.algorithm()? != Algorithm::ChaCha20Poly1305 {
return Err(AppError::new(
ErrorCode::InvalidFormat,
"ciphertext algorithm does not match encryptor",
));
}
let mut key_bytes = self.derive_key(envelope.salt());
let cipher = ChaCha20Poly1305::new((&key_bytes[..]).into());
key_bytes.zeroize();
let nonce = Nonce::from_slice(envelope.nonce());
cipher
.decrypt(
nonce,
Payload {
msg: envelope.ciphertext(),
aad: envelope.associated_data(),
},
)
.map_err(|_| {
AppError::new(ErrorCode::InvalidFormat, "ciphertext authentication failed")
})
}
fn algorithm(&self) -> Algorithm {
Algorithm::ChaCha20Poly1305
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encrypt_decrypt_roundtrip() {
let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
let plaintext = b"Hello, World!";
let ciphertext = encryptor.encrypt(plaintext).unwrap();
let decrypted = encryptor.decrypt(&ciphertext).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypt_produces_different_ciphertext() {
let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
let plaintext = b"Same plaintext";
let ct1 = encryptor.encrypt(plaintext).unwrap();
let ct2 = encryptor.encrypt(plaintext).unwrap();
assert_ne!(ct1, ct2);
}
#[test]
fn test_decrypt_with_wrong_key_fails() {
let encryptor1 = ChaCha20Encryptor::new(b"key-1");
let encryptor2 = ChaCha20Encryptor::new(b"key-2");
let plaintext = b"Secret data";
let ciphertext = encryptor1.encrypt(plaintext).unwrap();
let result = encryptor2.decrypt(&ciphertext);
assert!(result.is_err());
}
#[test]
fn test_encrypt_empty_plaintext() {
let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
let plaintext = b"";
let ciphertext = encryptor.encrypt(plaintext).unwrap();
let decrypted = encryptor.decrypt(&ciphertext).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_algorithm() {
let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
assert_eq!(encryptor.algorithm(), Algorithm::ChaCha20Poly1305);
}
#[test]
fn test_invalid_ciphertext() {
let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
let result = encryptor.decrypt("invalid-base64!@#$");
assert!(result.is_err());
}
#[test]
fn test_rejects_ciphertext_for_other_algorithm() {
let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
let other = crate::AesGcmEncryptor::new(b"my-secret-key");
let ciphertext = other.encrypt(b"secret").unwrap();
let err = encryptor.decrypt(&ciphertext).unwrap_err();
assert_eq!(err.code(), ErrorCode::InvalidFormat);
}
#[test]
fn test_corrupted_ciphertext() {
let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
let plaintext = b"Original";
let ciphertext = encryptor.encrypt(plaintext).unwrap();
let mut corrupted = ciphertext.clone();
let chars: Vec<char> = corrupted.chars().collect();
if chars.len() > 20 {
let mut new_chars = chars.clone();
new_chars[20] = if chars[20] == 'A' { 'B' } else { 'A' };
corrupted = new_chars.into_iter().collect();
}
let result = encryptor.decrypt(&corrupted);
assert!(result.is_err());
}
}