Skip to main content

async_snmp/v3/crypto/
rustcrypto.rs

1use super::{CryptoError, CryptoProvider, CryptoResult};
2use crate::v3::{AuthProtocol, PrivProtocol};
3
4/// Default crypto provider backed by the `RustCrypto` crate ecosystem.
5///
6/// This is a stateless unit struct that dispatches to the appropriate
7/// `RustCrypto` implementations based on the protocol enum values.
8pub struct RustCryptoProvider;
9
10// --- Dispatch macro for auth protocol -> concrete hash type ---
11
12macro_rules! dispatch_auth {
13    ($protocol:expr, $fn:ident, $($arg:expr),*) => {
14        match $protocol {
15            AuthProtocol::Md5 => $fn::<md5::Md5>($($arg),*),
16            AuthProtocol::Sha1 => $fn::<sha1::Sha1>($($arg),*),
17            AuthProtocol::Sha224 => $fn::<sha2::Sha224>($($arg),*),
18            AuthProtocol::Sha256 => $fn::<sha2::Sha256>($($arg),*),
19            AuthProtocol::Sha384 => $fn::<sha2::Sha384>($($arg),*),
20            AuthProtocol::Sha512 => $fn::<sha2::Sha512>($($arg),*),
21        }
22    };
23}
24
25impl CryptoProvider for RustCryptoProvider {
26    fn hash(&self, protocol: AuthProtocol, data: &[u8]) -> CryptoResult<Vec<u8>> {
27        Ok(dispatch_auth!(protocol, hash_impl, data))
28    }
29
30    fn password_to_key(&self, protocol: AuthProtocol, password: &[u8]) -> CryptoResult<Vec<u8>> {
31        const EXPANSION_SIZE: usize = 1_048_576; // 1MB
32        if password.len() < crate::v3::auth::MIN_PASSWORD_LENGTH {
33            return Err(CryptoError::PasswordTooShort);
34        }
35        Ok(dispatch_auth!(
36            protocol,
37            password_to_key_impl,
38            password,
39            EXPANSION_SIZE
40        ))
41    }
42
43    fn localize_key(
44        &self,
45        protocol: AuthProtocol,
46        master_key: &[u8],
47        engine_id: &[u8],
48    ) -> CryptoResult<Vec<u8>> {
49        Ok(dispatch_auth!(
50            protocol,
51            localize_key_impl,
52            master_key,
53            engine_id
54        ))
55    }
56
57    fn compute_hmac(
58        &self,
59        protocol: AuthProtocol,
60        key: &[u8],
61        slices: &[&[u8]],
62        truncate_len: usize,
63    ) -> CryptoResult<Vec<u8>> {
64        Ok(dispatch_auth!(
65            protocol,
66            compute_hmac_impl,
67            key,
68            slices,
69            truncate_len
70        ))
71    }
72
73    fn encrypt(
74        &self,
75        protocol: PrivProtocol,
76        key: &[u8],
77        iv: &[u8],
78        data: &mut Vec<u8>,
79    ) -> CryptoResult<()> {
80        match protocol {
81            PrivProtocol::Des | PrivProtocol::Des3 => {
82                // RFC 3414 §8.1.1.2: pad only up to the next 8-byte block boundary;
83                // the pad value is irrelevant and no full block is added when already
84                // aligned (i.e. not PKCS7).
85                let block = 8;
86                let padded_len = data.len().next_multiple_of(block);
87                let pad_byte = (padded_len - data.len()) as u8;
88                data.resize(padded_len, pad_byte);
89                match protocol {
90                    PrivProtocol::Des => encrypt_des_cbc(key, iv, data),
91                    _ => encrypt_des3_cbc(key, iv, data),
92                }
93            }
94            PrivProtocol::Aes128 | PrivProtocol::Aes192 | PrivProtocol::Aes256 => {
95                encrypt_aes_cfb(key, iv, data)
96            }
97        }
98    }
99
100    fn decrypt(
101        &self,
102        protocol: PrivProtocol,
103        key: &[u8],
104        iv: &[u8],
105        data: &mut [u8],
106    ) -> CryptoResult<()> {
107        match protocol {
108            PrivProtocol::Des => decrypt_des_cbc(key, iv, data),
109            PrivProtocol::Des3 => decrypt_des3_cbc(key, iv, data),
110            PrivProtocol::Aes128 | PrivProtocol::Aes192 | PrivProtocol::Aes256 => {
111                decrypt_aes_cfb(key, iv, data)
112            }
113        }
114    }
115}
116
117// --- Auth primitive implementations ---
118
119use digest::block_api::BlockSizeUser;
120use digest::{Digest, KeyInit, Mac, OutputSizeUser};
121
122fn hash_impl<D>(data: &[u8]) -> Vec<u8>
123where
124    D: Digest + Default,
125{
126    let mut hasher = D::new();
127    hasher.update(data);
128    hasher.finalize().to_vec()
129}
130
131fn password_to_key_impl<D>(password: &[u8], expansion_size: usize) -> Vec<u8>
132where
133    D: Digest + Default,
134{
135    if password.is_empty() {
136        return vec![0u8; <D as OutputSizeUser>::output_size()];
137    }
138
139    let mut hasher = D::new();
140
141    let mut buf = [0u8; 64];
142    let password_len = password.len();
143    let mut password_index = 0;
144    let mut count = 0;
145
146    while count < expansion_size {
147        for byte in &mut buf {
148            *byte = password[password_index];
149            password_index = (password_index + 1) % password_len;
150        }
151        hasher.update(buf);
152        count += 64;
153    }
154
155    hasher.finalize().to_vec()
156}
157
158fn localize_key_impl<D>(master_key: &[u8], engine_id: &[u8]) -> Vec<u8>
159where
160    D: Digest + Default,
161{
162    let mut hasher = D::new();
163    hasher.update(master_key);
164    hasher.update(engine_id);
165    hasher.update(master_key);
166    hasher.finalize().to_vec()
167}
168
169fn compute_hmac_impl<D>(key: &[u8], slices: &[&[u8]], truncate_len: usize) -> Vec<u8>
170where
171    D: Digest + BlockSizeUser + Clone,
172{
173    use hmac::SimpleHmac;
174
175    let mut mac =
176        <SimpleHmac<D> as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
177    for slice in slices {
178        Mac::update(&mut mac, slice);
179    }
180    let result = mac.finalize().into_bytes();
181    result[..truncate_len].to_vec()
182}
183
184// --- Privacy primitive implementations ---
185
186fn encrypt_des_cbc(key: &[u8], iv: &[u8], data: &mut [u8]) -> CryptoResult<()> {
187    use cbc::cipher::{BlockModeEncrypt, KeyIvInit};
188    type DesCbc = cbc::Encryptor<des::Des>;
189
190    let cipher = DesCbc::new_from_slices(key, iv).map_err(|_| {
191        tracing::debug!(target: "async_snmp::crypto", "DES encryption failed: invalid key length");
192        CryptoError::InvalidKeyLength
193    })?;
194    let len = data.len();
195    cipher
196        .encrypt_padded::<cbc::cipher::block_padding::NoPadding>(data, len)
197        .map_err(|_| {
198            tracing::debug!(target: "async_snmp::crypto", "DES encryption failed: cipher error");
199            CryptoError::CipherError
200        })?;
201    Ok(())
202}
203
204fn decrypt_des_cbc(key: &[u8], iv: &[u8], data: &mut [u8]) -> CryptoResult<()> {
205    use cbc::cipher::{BlockModeDecrypt, KeyIvInit};
206    type DesCbc = cbc::Decryptor<des::Des>;
207
208    let cipher = DesCbc::new_from_slices(key, iv).map_err(|_| {
209        tracing::debug!(target: "async_snmp::crypto", "DES decryption failed: invalid key length");
210        CryptoError::InvalidKeyLength
211    })?;
212    cipher
213        .decrypt_padded::<cbc::cipher::block_padding::NoPadding>(data)
214        .map_err(|_| {
215            tracing::debug!(target: "async_snmp::crypto", "DES decryption failed: cipher error");
216            CryptoError::CipherError
217        })?;
218    Ok(())
219}
220
221fn encrypt_des3_cbc(key: &[u8], iv: &[u8], data: &mut [u8]) -> CryptoResult<()> {
222    use cbc::cipher::{BlockModeEncrypt, KeyIvInit};
223    type Des3Cbc = cbc::Encryptor<des::TdesEde3>;
224
225    let cipher = Des3Cbc::new_from_slices(key, iv).map_err(|_| {
226        tracing::debug!(target: "async_snmp::crypto", "3DES encryption failed: invalid key length");
227        CryptoError::InvalidKeyLength
228    })?;
229    let len = data.len();
230    cipher
231        .encrypt_padded::<cbc::cipher::block_padding::NoPadding>(data, len)
232        .map_err(|_| {
233            tracing::debug!(target: "async_snmp::crypto", "3DES encryption failed: cipher error");
234            CryptoError::CipherError
235        })?;
236    Ok(())
237}
238
239fn decrypt_des3_cbc(key: &[u8], iv: &[u8], data: &mut [u8]) -> CryptoResult<()> {
240    use cbc::cipher::{BlockModeDecrypt, KeyIvInit};
241    type Des3Cbc = cbc::Decryptor<des::TdesEde3>;
242
243    let cipher = Des3Cbc::new_from_slices(key, iv).map_err(|_| {
244        tracing::debug!(target: "async_snmp::crypto", "3DES decryption failed: invalid key length");
245        CryptoError::InvalidKeyLength
246    })?;
247    cipher
248        .decrypt_padded::<cbc::cipher::block_padding::NoPadding>(data)
249        .map_err(|_| {
250            tracing::debug!(target: "async_snmp::crypto", "3DES decryption failed: cipher error");
251            CryptoError::CipherError
252        })?;
253    Ok(())
254}
255
256fn encrypt_aes_cfb(key: &[u8], iv: &[u8], data: &mut [u8]) -> CryptoResult<()> {
257    use aes::{Aes128, Aes192, Aes256};
258    use cfb_mode::cipher::KeyIvInit;
259
260    match key.len() {
261        16 => {
262            type Aes128Cfb = cfb_mode::Encryptor<Aes128>;
263            let cipher = Aes128Cfb::new_from_slices(key, iv).map_err(|_| {
264                tracing::debug!(target: "async_snmp::crypto", "AES-128 encryption failed: invalid key length");
265                CryptoError::InvalidKeyLength
266            })?;
267            cipher.encrypt(data);
268        }
269        24 => {
270            type Aes192Cfb = cfb_mode::Encryptor<Aes192>;
271            let cipher = Aes192Cfb::new_from_slices(key, iv).map_err(|_| {
272                tracing::debug!(target: "async_snmp::crypto", "AES-192 encryption failed: invalid key length");
273                CryptoError::InvalidKeyLength
274            })?;
275            cipher.encrypt(data);
276        }
277        32 => {
278            type Aes256Cfb = cfb_mode::Encryptor<Aes256>;
279            let cipher = Aes256Cfb::new_from_slices(key, iv).map_err(|_| {
280                tracing::debug!(target: "async_snmp::crypto", "AES-256 encryption failed: invalid key length");
281                CryptoError::InvalidKeyLength
282            })?;
283            cipher.encrypt(data);
284        }
285        key_len => {
286            tracing::debug!(target: "async_snmp::crypto", { key_len }, "AES encryption failed: unsupported key length");
287            return Err(CryptoError::InvalidKeyLength);
288        }
289    }
290    Ok(())
291}
292
293fn decrypt_aes_cfb(key: &[u8], iv: &[u8], data: &mut [u8]) -> CryptoResult<()> {
294    use aes::{Aes128, Aes192, Aes256};
295    use cfb_mode::cipher::KeyIvInit;
296
297    match key.len() {
298        16 => {
299            type Aes128Cfb = cfb_mode::Decryptor<Aes128>;
300            let cipher = Aes128Cfb::new_from_slices(key, iv).map_err(|_| {
301                tracing::debug!(target: "async_snmp::crypto", "AES-128 decryption failed: invalid key length");
302                CryptoError::InvalidKeyLength
303            })?;
304            cipher.decrypt(data);
305        }
306        24 => {
307            type Aes192Cfb = cfb_mode::Decryptor<Aes192>;
308            let cipher = Aes192Cfb::new_from_slices(key, iv).map_err(|_| {
309                tracing::debug!(target: "async_snmp::crypto", "AES-192 decryption failed: invalid key length");
310                CryptoError::InvalidKeyLength
311            })?;
312            cipher.decrypt(data);
313        }
314        32 => {
315            type Aes256Cfb = cfb_mode::Decryptor<Aes256>;
316            let cipher = Aes256Cfb::new_from_slices(key, iv).map_err(|_| {
317                tracing::debug!(target: "async_snmp::crypto", "AES-256 decryption failed: invalid key length");
318                CryptoError::InvalidKeyLength
319            })?;
320            cipher.decrypt(data);
321        }
322        key_len => {
323            tracing::debug!(target: "async_snmp::crypto", { key_len }, "AES decryption failed: unsupported key length");
324            return Err(CryptoError::InvalidKeyLength);
325        }
326    }
327    Ok(())
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    /// RFC 3414 §8.1.1.2: "if [the length] is not [a multiple of 8], the data
335    /// is padded at the end as necessary." The encrypt operation must handle
336    /// unaligned plaintext by padding to the next block boundary.
337    #[test]
338    fn des_encrypt_pads_unaligned_plaintext() {
339        let provider = RustCryptoProvider;
340        let key = b"\x00\x11\x22\x33\x44\x55\x66\x77";
341        let iv = [0u8; 8];
342        let mut data = b"Hello".to_vec(); // 5 bytes, not a multiple of 8
343
344        let result = provider.encrypt(PrivProtocol::Des, key, &iv, &mut data);
345        assert!(
346            result.is_ok(),
347            "DES encrypt must pad unaligned plaintext, got: {result:?}"
348        );
349        assert_eq!(data.len(), 8, "output must be padded to 8-byte boundary");
350    }
351
352    /// Same as DES: 3DES-CBC must pad unaligned plaintext.
353    #[test]
354    fn des3_encrypt_pads_unaligned_plaintext() {
355        let provider = RustCryptoProvider;
356        let key = [0x01u8; 24];
357        let iv = [0u8; 8];
358        let mut data = b"Hello".to_vec(); // 5 bytes
359
360        let result = provider.encrypt(PrivProtocol::Des3, &key, &iv, &mut data);
361        assert!(
362            result.is_ok(),
363            "3DES encrypt must pad unaligned plaintext, got: {result:?}"
364        );
365        assert_eq!(data.len(), 8, "output must be padded to 8-byte boundary");
366    }
367
368    /// DES roundtrip: unaligned plaintext should encrypt and decrypt correctly.
369    #[test]
370    fn des_roundtrip_unaligned() {
371        let provider = RustCryptoProvider;
372        let key = b"\x00\x11\x22\x33\x44\x55\x66\x77";
373        let iv = [0u8; 8];
374        let plaintext = b"Hello";
375        let mut data = plaintext.to_vec();
376
377        provider
378            .encrypt(PrivProtocol::Des, key, &iv, &mut data)
379            .unwrap();
380        assert_eq!(data.len(), 8);
381
382        provider
383            .decrypt(PrivProtocol::Des, key, &iv, &mut data)
384            .unwrap();
385        assert_eq!(&data[..plaintext.len()], plaintext);
386    }
387
388    /// Already-aligned DES plaintext should still work (no regression).
389    #[test]
390    fn des_encrypt_aligned_unchanged() {
391        let provider = RustCryptoProvider;
392        let key = b"\x00\x11\x22\x33\x44\x55\x66\x77";
393        let iv = [0u8; 8];
394        let mut data = vec![0x41u8; 8]; // already 8 bytes
395
396        let result = provider.encrypt(PrivProtocol::Des, key, &iv, &mut data);
397        assert!(result.is_ok());
398        assert_eq!(data.len(), 8);
399    }
400}