rskit_encryption/
chacha20.rs1use chacha20poly1305::{
4 ChaCha20Poly1305, Nonce,
5 aead::{Aead, KeyInit, Payload},
6};
7use rskit_errors::{AppError, AppResult, ErrorCode};
8use sha2::Sha256;
9use zeroize::Zeroize;
10
11use crate::envelope::{Envelope, KEY_LEN, NONCE_SIZE, PBKDF2_ITERATIONS, SALT_SIZE};
12use crate::traits::{Algorithm, Encryptor};
13
14pub struct ChaCha20Encryptor {
19 passphrase: Vec<u8>,
20}
21
22impl ChaCha20Encryptor {
23 pub fn new(key: &[u8]) -> Self {
27 Self {
28 passphrase: key.to_vec(),
29 }
30 }
31
32 fn derive_key(&self, salt: &[u8]) -> [u8; KEY_LEN] {
33 let mut key = [0u8; KEY_LEN];
34 pbkdf2::pbkdf2_hmac::<Sha256>(&self.passphrase, salt, PBKDF2_ITERATIONS, &mut key);
35 key
36 }
37}
38
39impl Drop for ChaCha20Encryptor {
40 fn drop(&mut self) {
41 self.passphrase.zeroize();
42 }
43}
44
45impl Encryptor for ChaCha20Encryptor {
46 fn encrypt(&self, plaintext: &[u8]) -> AppResult<String> {
47 let salt: [u8; SALT_SIZE] = rand::random();
48
49 let mut key_bytes = self.derive_key(&salt);
50 let cipher = ChaCha20Poly1305::new((&key_bytes[..]).into());
51 key_bytes.zeroize();
52
53 let nonce_bytes: [u8; NONCE_SIZE] = rand::random();
54 let nonce = Nonce::from_slice(&nonce_bytes);
55 let aad =
56 crate::envelope::associated_data(Algorithm::ChaCha20Poly1305, &salt, &nonce_bytes);
57
58 let ciphertext = cipher
59 .encrypt(
60 nonce,
61 Payload {
62 msg: plaintext,
63 aad: &aad,
64 },
65 )
66 .map_err(|_| AppError::new(ErrorCode::Internal, "encryption failed"))?;
67
68 Ok(Envelope::encode(
69 Algorithm::ChaCha20Poly1305,
70 &salt,
71 &nonce_bytes,
72 &ciphertext,
73 ))
74 }
75
76 fn decrypt(&self, ciphertext: &str) -> AppResult<Vec<u8>> {
77 let envelope = Envelope::decode(ciphertext)?;
78 if envelope.algorithm()? != Algorithm::ChaCha20Poly1305 {
79 return Err(AppError::new(
80 ErrorCode::InvalidFormat,
81 "ciphertext algorithm does not match encryptor",
82 ));
83 }
84
85 let mut key_bytes = self.derive_key(envelope.salt());
86 let cipher = ChaCha20Poly1305::new((&key_bytes[..]).into());
87 key_bytes.zeroize();
88
89 let nonce = Nonce::from_slice(envelope.nonce());
90
91 cipher
92 .decrypt(
93 nonce,
94 Payload {
95 msg: envelope.ciphertext(),
96 aad: envelope.associated_data(),
97 },
98 )
99 .map_err(|_| {
100 AppError::new(ErrorCode::InvalidFormat, "ciphertext authentication failed")
101 })
102 }
103
104 fn algorithm(&self) -> Algorithm {
105 Algorithm::ChaCha20Poly1305
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn test_encrypt_decrypt_roundtrip() {
115 let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
116 let plaintext = b"Hello, World!";
117
118 let ciphertext = encryptor.encrypt(plaintext).unwrap();
119 let decrypted = encryptor.decrypt(&ciphertext).unwrap();
120
121 assert_eq!(decrypted, plaintext);
122 }
123
124 #[test]
125 fn test_encrypt_produces_different_ciphertext() {
126 let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
127 let plaintext = b"Same plaintext";
128
129 let ct1 = encryptor.encrypt(plaintext).unwrap();
130 let ct2 = encryptor.encrypt(plaintext).unwrap();
131
132 assert_ne!(ct1, ct2);
133 }
134
135 #[test]
136 fn test_decrypt_with_wrong_key_fails() {
137 let encryptor1 = ChaCha20Encryptor::new(b"key-1");
138 let encryptor2 = ChaCha20Encryptor::new(b"key-2");
139
140 let plaintext = b"Secret data";
141 let ciphertext = encryptor1.encrypt(plaintext).unwrap();
142
143 let result = encryptor2.decrypt(&ciphertext);
144 assert!(result.is_err());
145 }
146
147 #[test]
148 fn test_encrypt_empty_plaintext() {
149 let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
150 let plaintext = b"";
151
152 let ciphertext = encryptor.encrypt(plaintext).unwrap();
153 let decrypted = encryptor.decrypt(&ciphertext).unwrap();
154
155 assert_eq!(decrypted, plaintext);
156 }
157
158 #[test]
159 fn test_algorithm() {
160 let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
161 assert_eq!(encryptor.algorithm(), Algorithm::ChaCha20Poly1305);
162 }
163
164 #[test]
165 fn test_invalid_ciphertext() {
166 let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
167 let result = encryptor.decrypt("invalid-base64!@#$");
168 assert!(result.is_err());
169 }
170
171 #[test]
172 fn test_rejects_ciphertext_for_other_algorithm() {
173 let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
174 let other = crate::AesGcmEncryptor::new(b"my-secret-key");
175 let ciphertext = other.encrypt(b"secret").unwrap();
176
177 let err = encryptor.decrypt(&ciphertext).unwrap_err();
178 assert_eq!(err.code(), ErrorCode::InvalidFormat);
179 }
180
181 #[test]
182 fn test_corrupted_ciphertext() {
183 let encryptor = ChaCha20Encryptor::new(b"my-secret-key");
184 let plaintext = b"Original";
185 let ciphertext = encryptor.encrypt(plaintext).unwrap();
186
187 let mut corrupted = ciphertext.clone();
188 let chars: Vec<char> = corrupted.chars().collect();
189 if chars.len() > 20 {
190 let mut new_chars = chars.clone();
191 new_chars[20] = if chars[20] == 'A' { 'B' } else { 'A' };
192 corrupted = new_chars.into_iter().collect();
193 }
194
195 let result = encryptor.decrypt(&corrupted);
196 assert!(result.is_err());
197 }
198}