Skip to main content

rskit_encryption/
chacha20.rs

1//! ChaCha20-Poly1305 encryption implementation.
2
3use 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
14/// ChaCha20-Poly1305 encryptor.
15///
16/// Uses PBKDF2-SHA256 for key derivation with a random 16-byte salt per encryption. Output format:
17/// `base64(version[1] || algorithm[1] || salt[16] || nonce[12] || ciphertext)`.
18pub struct ChaCha20Encryptor {
19    passphrase: Vec<u8>,
20}
21
22impl ChaCha20Encryptor {
23    /// Creates a new ChaCha20-Poly1305 encryptor.
24    ///
25    /// The passphrase is stored and used with PBKDF2-SHA256 to derive keys per operation.
26    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}