Skip to main content

async_snmp/v3/
privacy.rs

1//! Privacy (encryption) protocols for SNMPv3 (RFC 3414, RFC 3826).
2//!
3//! This module implements:
4//! - DES-CBC privacy (RFC 3414 Section 8)
5//! - AES-128-CFB privacy (RFC 3826)
6//! - AES-192-CFB privacy (RFC 3826)
7//! - AES-256-CFB privacy (RFC 3826)
8//!
9//! # Salt/IV Construction
10//!
11//! ## DES-CBC
12//! - Salt (privParameters): engineBoots (4 bytes) || counter (4 bytes) = 8 bytes
13//! - IV: pre-IV XOR salt (pre-IV is last 8 bytes of 16-byte privKey)
14//!
15//! ## AES-CFB-128
16//! - Salt (privParameters): 64-bit counter = 8 bytes
17//! - IV: engineBoots (4 bytes) || engineTime (4 bytes) || salt (8 bytes) = 16 bytes
18//!   (concatenation, NOT XOR)
19
20use std::sync::atomic::{AtomicU64, Ordering};
21
22use bytes::Bytes;
23use zeroize::{Zeroize, ZeroizeOnDrop};
24
25use super::crypto::{CryptoError, CryptoProvider};
26use super::{AuthProtocol, PrivProtocol};
27
28/// Error type for privacy (encryption/decryption) operations.
29///
30/// These errors indicate cryptographic failures. Callers should convert
31/// these to `Error::Auth` with appropriate target context.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum PrivacyError {
34    /// Invalid privParameters length (expected 8 bytes).
35    InvalidPrivParamsLength { expected: usize, actual: usize },
36    /// Ciphertext length not a multiple of block size.
37    InvalidCiphertextLength { length: usize, block_size: usize },
38    /// Cryptographic provider error (unsupported algorithm, invalid key, cipher failure).
39    Crypto(CryptoError),
40}
41
42impl From<CryptoError> for PrivacyError {
43    fn from(e: CryptoError) -> Self {
44        Self::Crypto(e)
45    }
46}
47
48impl std::fmt::Display for PrivacyError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::InvalidPrivParamsLength { expected, actual } => {
52                write!(
53                    f,
54                    "invalid privParameters length: expected {}, got {}",
55                    expected, actual
56                )
57            }
58            Self::InvalidCiphertextLength { length, block_size } => {
59                write!(
60                    f,
61                    "ciphertext length {} not multiple of block size {}",
62                    length, block_size
63                )
64            }
65            Self::Crypto(e) => write!(f, "{}", e),
66        }
67    }
68}
69
70impl std::error::Error for PrivacyError {
71    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
72        match self {
73            Self::Crypto(e) => Some(e),
74            _ => None,
75        }
76    }
77}
78
79/// Result type for privacy operations.
80pub type PrivacyResult<T> = std::result::Result<T, PrivacyError>;
81
82/// Generate a random non-zero u64 for salt initialization.
83///
84/// Uses the OS cryptographic random source via `getrandom`.
85fn random_nonzero_u64() -> super::crypto::CryptoResult<u64> {
86    let mut buf = [0u8; 8];
87    loop {
88        getrandom::fill(&mut buf).map_err(|_| super::crypto::CryptoError::RandomSource)?;
89        let val = u64::from_ne_bytes(buf);
90        if val != 0 {
91            return Ok(val);
92        }
93        // Extremely unlikely (1 in 2^64), but loop if we got zero
94    }
95}
96
97/// Privacy key for encryption/decryption operations.
98///
99/// Derives encryption keys from a password and engine ID using the same
100/// process as authentication keys, then uses the appropriate portion
101/// based on the privacy protocol.
102///
103/// # Security
104///
105/// Key material is automatically zeroed from memory when the key is dropped,
106/// using the `zeroize` crate. This provides defense-in-depth against memory
107/// scraping attacks.
108#[derive(Zeroize, ZeroizeOnDrop)]
109pub struct PrivKey {
110    /// The localized key bytes
111    key: Vec<u8>,
112    /// Privacy protocol
113    #[zeroize(skip)]
114    protocol: PrivProtocol,
115    /// Salt counter for generating unique IVs.
116    /// Uses interior mutability so encrypt() can take &self.
117    #[zeroize(skip)]
118    salt_counter: AtomicU64,
119}
120
121/// Thread-safe salt counter for shared use across multiple encryptions.
122pub struct SaltCounter(AtomicU64);
123
124impl SaltCounter {
125    /// Create a new salt counter initialized from cryptographic randomness.
126    ///
127    /// Panics if the OS random source is unavailable.
128    pub fn new() -> Self {
129        Self(AtomicU64::new(
130            random_nonzero_u64().expect("OS random source unavailable"),
131        ))
132    }
133
134    /// Create a salt counter initialized to a specific value.
135    ///
136    /// This is primarily for testing purposes.
137    pub fn from_value(value: u64) -> Self {
138        Self(AtomicU64::new(value))
139    }
140
141    /// Get the next salt value and increment the counter.
142    ///
143    /// This method never returns zero. Per net-snmp behavior, zero is skipped
144    /// on wraparound to avoid potential IV reuse issues.
145    ///
146    /// Uses the post-increment value as the salt and a single compare_exchange
147    /// to skip zero atomically, avoiding the two-fetch_add race that could
148    /// cause IV reuse under concurrent access.
149    pub fn next(&self) -> u64 {
150        let old = self.0.fetch_add(1, Ordering::SeqCst);
151        let val = old.wrapping_add(1);
152        if val != 0 {
153            return val;
154        }
155        // Counter wrapped to zero. Only one thread reaches here (only one fetch_add
156        // returns u64::MAX). Atomically advance from 0 to 1, then return 1.
157        let _ = self
158            .0
159            .compare_exchange(0, 1, Ordering::SeqCst, Ordering::SeqCst);
160        1
161    }
162}
163
164impl Default for SaltCounter {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170impl PrivKey {
171    /// Derive a privacy key from a password and engine ID.
172    ///
173    /// The key derivation uses the same algorithm as authentication keys
174    /// (RFC 3414 A.2), but the resulting key is used differently:
175    /// - DES: first 8 bytes = key, last 8 bytes = pre-IV
176    /// - 3DES: first 24 bytes = key, last 8 bytes = pre-IV
177    /// - AES: first 16/24/32 bytes = key (depending on AES variant)
178    ///
179    /// Key extension is automatically applied when needed based on the auth/priv
180    /// protocol combination:
181    ///
182    /// - AES-192/256 with SHA-1 or MD5: Blumenthal extension (draft-blumenthal-aes-usm-04)
183    /// - 3DES with SHA-1 or MD5: Reeder extension (draft-reeder-snmpv3-usm-3desede-00)
184    ///
185    /// # Performance Note
186    ///
187    /// This method performs the full key derivation (~850μs for SHA-256). When
188    /// polling many engines with shared credentials, use [`MasterKey`](super::MasterKey)
189    /// and call [`PrivKey::from_master_key`] for each engine.
190    ///
191    /// # Example
192    ///
193    /// ```rust
194    /// use async_snmp::{AuthProtocol, PrivProtocol, v3::PrivKey};
195    ///
196    /// let engine_id = [0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04];
197    ///
198    /// // SHA-1 only produces 20 bytes, but AES-256 needs 32.
199    /// // Blumenthal extension is automatically applied.
200    /// let priv_key = PrivKey::from_password(
201    ///     AuthProtocol::Sha1,
202    ///     PrivProtocol::Aes256,
203    ///     b"password",
204    ///     &engine_id,
205    /// ).unwrap();
206    /// ```
207    pub fn from_password(
208        auth_protocol: AuthProtocol,
209        priv_protocol: PrivProtocol,
210        password: &[u8],
211        engine_id: &[u8],
212    ) -> super::crypto::CryptoResult<Self> {
213        use super::MasterKey;
214
215        let master = MasterKey::from_password(auth_protocol, password)?;
216        Self::from_master_key(&master, priv_protocol, engine_id)
217    }
218
219    /// Derive a privacy key from a master key and engine ID.
220    ///
221    /// This is the efficient path when you have a cached [`MasterKey`](super::MasterKey).
222    /// Key extension is automatically applied when needed based on the auth/priv
223    /// protocol combination:
224    ///
225    /// - AES-192/256 with SHA-1 or MD5: Blumenthal extension (draft-blumenthal-aes-usm-04)
226    /// - 3DES with SHA-1 or MD5: Reeder extension (draft-reeder-snmpv3-usm-3desede-00)
227    ///
228    /// # Example
229    ///
230    /// ```rust
231    /// use async_snmp::{AuthProtocol, MasterKey, PrivProtocol, v3::PrivKey};
232    ///
233    /// let master = MasterKey::from_password(AuthProtocol::Sha1, b"password").unwrap();
234    /// let engine_id = [0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04];
235    ///
236    /// // SHA-1 only produces 20 bytes, but AES-256 needs 32.
237    /// // Blumenthal extension is automatically applied.
238    /// let priv_key = PrivKey::from_master_key(&master, PrivProtocol::Aes256, &engine_id).unwrap();
239    /// ```
240    pub fn from_master_key(
241        master: &super::MasterKey,
242        priv_protocol: PrivProtocol,
243        engine_id: &[u8],
244    ) -> super::crypto::CryptoResult<Self> {
245        use super::{
246            KeyExtension,
247            auth::{extend_key, extend_key_reeder},
248        };
249
250        let auth_protocol = master.protocol();
251        let key_extension = priv_protocol.key_extension_for(auth_protocol);
252
253        // Localize the master key (per RFC 3826 Section 1.2)
254        let localized = master.localize(engine_id)?;
255        let key_bytes = localized.as_bytes();
256
257        let key = match key_extension {
258            KeyExtension::None => key_bytes.to_vec(),
259            KeyExtension::Blumenthal => {
260                extend_key(auth_protocol, key_bytes, priv_protocol.key_len())?
261            }
262            KeyExtension::Reeder => {
263                extend_key_reeder(auth_protocol, key_bytes, engine_id, priv_protocol.key_len())?
264            }
265        };
266
267        Ok(Self {
268            key,
269            protocol: priv_protocol,
270            salt_counter: Self::init_salt()?,
271        })
272    }
273
274    /// Create a privacy key from raw localized key bytes.
275    pub fn from_bytes(
276        protocol: PrivProtocol,
277        key: impl Into<Vec<u8>>,
278    ) -> super::crypto::CryptoResult<Self> {
279        Ok(Self {
280            key: key.into(),
281            protocol,
282            salt_counter: Self::init_salt()?,
283        })
284    }
285
286    /// Initialize salt counter from cryptographic randomness.
287    ///
288    /// Never returns zero to avoid IV reuse issues on wraparound.
289    fn init_salt() -> super::crypto::CryptoResult<AtomicU64> {
290        Ok(AtomicU64::new(random_nonzero_u64()?))
291    }
292
293    /// Get the privacy protocol.
294    pub fn protocol(&self) -> PrivProtocol {
295        self.protocol
296    }
297
298    /// Get the encryption key portion.
299    pub fn encryption_key(&self) -> &[u8] {
300        match self.protocol {
301            PrivProtocol::Des => &self.key[..8],
302            PrivProtocol::Des3 => &self.key[..24],
303            PrivProtocol::Aes128 => &self.key[..16],
304            PrivProtocol::Aes192 => &self.key[..24],
305            PrivProtocol::Aes256 => &self.key[..32],
306        }
307    }
308
309    /// Encrypt data and return (ciphertext, privParameters).
310    ///
311    /// # Arguments
312    /// * `plaintext` - The data to encrypt (typically the serialized ScopedPDU)
313    /// * `engine_boots` - The authoritative engine's boot count
314    /// * `engine_time` - The authoritative engine's time
315    /// * `salt_counter` - Optional shared salt counter; if None, uses internal counter
316    ///
317    /// # Returns
318    /// * `Ok((ciphertext, priv_params))` on success
319    /// * `Err` on encryption failure
320    pub fn encrypt(
321        &self,
322        plaintext: &[u8],
323        engine_boots: u32,
324        engine_time: u32,
325        salt_counter: Option<&SaltCounter>,
326    ) -> PrivacyResult<(Bytes, Bytes)> {
327        let salt = salt_counter.map(|c| c.next()).unwrap_or_else(|| {
328            // Fetch the current value, then increment. Skip zero.
329            let val = self.salt_counter.fetch_add(1, Ordering::Relaxed);
330            if val != 0 {
331                return val;
332            }
333            // Counter was zero (initial or wrapped). Fetch the next value.
334            self.salt_counter.fetch_add(1, Ordering::Relaxed)
335        });
336
337        match self.protocol {
338            PrivProtocol::Des => self.encrypt_des(plaintext, engine_boots, salt),
339            PrivProtocol::Des3 => self.encrypt_des3(plaintext, engine_boots, salt),
340            PrivProtocol::Aes128 => {
341                self.encrypt_aes(plaintext, engine_boots, engine_time, salt, 16)
342            }
343            PrivProtocol::Aes192 => {
344                self.encrypt_aes(plaintext, engine_boots, engine_time, salt, 24)
345            }
346            PrivProtocol::Aes256 => {
347                self.encrypt_aes(plaintext, engine_boots, engine_time, salt, 32)
348            }
349        }
350    }
351
352    /// Decrypt data using the privParameters from the message.
353    ///
354    /// # Arguments
355    /// * `ciphertext` - The encrypted data
356    /// * `engine_boots` - The authoritative engine's boot count (from message)
357    /// * `engine_time` - The authoritative engine's time (from message)
358    /// * `priv_params` - The privParameters field from the message
359    ///
360    /// # Returns
361    /// * `Ok(plaintext)` on success
362    /// * `Err` on decryption failure
363    pub fn decrypt(
364        &self,
365        ciphertext: &[u8],
366        engine_boots: u32,
367        engine_time: u32,
368        priv_params: &[u8],
369    ) -> PrivacyResult<Bytes> {
370        if priv_params.len() != 8 {
371            tracing::debug!(target: "async_snmp::crypto", { expected = 8, actual = priv_params.len() }, "invalid privParameters length");
372            return Err(PrivacyError::InvalidPrivParamsLength {
373                expected: 8,
374                actual: priv_params.len(),
375            });
376        }
377
378        match self.protocol {
379            PrivProtocol::Des => self.decrypt_des(ciphertext, priv_params),
380            PrivProtocol::Des3 => self.decrypt_des3(ciphertext, priv_params),
381            PrivProtocol::Aes128 | PrivProtocol::Aes192 | PrivProtocol::Aes256 => {
382                self.decrypt_aes(ciphertext, engine_boots, engine_time, priv_params)
383            }
384        }
385    }
386
387    /// DES-CBC encryption (RFC 3414 Section 8.1.1).
388    fn encrypt_des(
389        &self,
390        plaintext: &[u8],
391        engine_boots: u32,
392        salt_int: u64,
393    ) -> PrivacyResult<(Bytes, Bytes)> {
394        // DES key is first 8 bytes
395        let key = &self.key[..8];
396        // Pre-IV is last 8 bytes of 16-byte privKey
397        let pre_iv = &self.key[8..16];
398
399        // Salt = engineBoots (4 bytes MSB) || counter (4 bytes MSB)
400        // We use the lower 32 bits of salt_int as the counter
401        let mut salt = [0u8; 8];
402        salt[..4].copy_from_slice(&engine_boots.to_be_bytes());
403        salt[4..].copy_from_slice(&(salt_int as u32).to_be_bytes());
404
405        // IV = pre-IV XOR salt
406        let mut iv = [0u8; 8];
407        for i in 0..8 {
408            iv[i] = pre_iv[i] ^ salt[i];
409        }
410
411        // Pad plaintext to multiple of 8 bytes
412        let padded_len = plaintext.len().next_multiple_of(8);
413        let mut buffer = vec![0u8; padded_len];
414        buffer[..plaintext.len()].copy_from_slice(plaintext);
415
416        super::crypto::provider().encrypt(PrivProtocol::Des, key, &iv, &mut buffer)?;
417
418        Ok((Bytes::from(buffer), Bytes::copy_from_slice(&salt)))
419    }
420
421    /// DES-CBC decryption (RFC 3414 Section 8.1.1).
422    fn decrypt_des(&self, ciphertext: &[u8], priv_params: &[u8]) -> PrivacyResult<Bytes> {
423        if !ciphertext.len().is_multiple_of(8) {
424            tracing::debug!(target: "async_snmp::crypto", { length = ciphertext.len(), block_size = 8 }, "DES decryption failed: invalid ciphertext length");
425            return Err(PrivacyError::InvalidCiphertextLength {
426                length: ciphertext.len(),
427                block_size: 8,
428            });
429        }
430
431        // DES key is first 8 bytes
432        let key = &self.key[..8];
433        // Pre-IV is last 8 bytes of 16-byte privKey
434        let pre_iv = &self.key[8..16];
435
436        // Salt is the privParameters
437        let salt = priv_params;
438
439        // IV = pre-IV XOR salt
440        let mut iv = [0u8; 8];
441        for i in 0..8 {
442            iv[i] = pre_iv[i] ^ salt[i];
443        }
444
445        let mut buffer = ciphertext.to_vec();
446        super::crypto::provider().decrypt(PrivProtocol::Des, key, &iv, &mut buffer)?;
447
448        Ok(Bytes::from(buffer))
449    }
450
451    /// 3DES-EDE CBC encryption (draft-reeder-snmpv3-usm-3desede-00 Section 5.1.1.2).
452    fn encrypt_des3(
453        &self,
454        plaintext: &[u8],
455        engine_boots: u32,
456        salt_int: u64,
457    ) -> PrivacyResult<(Bytes, Bytes)> {
458        // 3DES key is first 24 bytes (K1, K2, K3)
459        let key = &self.key[..24];
460        // Pre-IV is bytes 24-31 of the 32-byte privKey
461        let pre_iv = &self.key[24..32];
462
463        // Salt = engineBoots (4 bytes MSB) || counter (4 bytes MSB)
464        let mut salt = [0u8; 8];
465        salt[..4].copy_from_slice(&engine_boots.to_be_bytes());
466        salt[4..].copy_from_slice(&(salt_int as u32).to_be_bytes());
467
468        // IV = pre-IV XOR salt
469        let mut iv = [0u8; 8];
470        for i in 0..8 {
471            iv[i] = pre_iv[i] ^ salt[i];
472        }
473
474        // Pad plaintext to multiple of 8 bytes
475        let padded_len = plaintext.len().next_multiple_of(8);
476        let mut buffer = vec![0u8; padded_len];
477        buffer[..plaintext.len()].copy_from_slice(plaintext);
478
479        super::crypto::provider().encrypt(PrivProtocol::Des3, key, &iv, &mut buffer)?;
480
481        Ok((Bytes::from(buffer), Bytes::copy_from_slice(&salt)))
482    }
483
484    /// 3DES-EDE CBC decryption (draft-reeder-snmpv3-usm-3desede-00 Section 5.1.1.3).
485    fn decrypt_des3(&self, ciphertext: &[u8], priv_params: &[u8]) -> PrivacyResult<Bytes> {
486        if !ciphertext.len().is_multiple_of(8) {
487            tracing::debug!(target: "async_snmp::crypto", { length = ciphertext.len(), block_size = 8 }, "3DES decryption failed: invalid ciphertext length");
488            return Err(PrivacyError::InvalidCiphertextLength {
489                length: ciphertext.len(),
490                block_size: 8,
491            });
492        }
493
494        // 3DES key is first 24 bytes (K1, K2, K3)
495        let key = &self.key[..24];
496        // Pre-IV is bytes 24-31 of the 32-byte privKey
497        let pre_iv = &self.key[24..32];
498
499        // Salt is the privParameters
500        let salt = priv_params;
501
502        // IV = pre-IV XOR salt
503        let mut iv = [0u8; 8];
504        for i in 0..8 {
505            iv[i] = pre_iv[i] ^ salt[i];
506        }
507
508        let mut buffer = ciphertext.to_vec();
509        super::crypto::provider().decrypt(PrivProtocol::Des3, key, &iv, &mut buffer)?;
510
511        Ok(Bytes::from(buffer))
512    }
513
514    /// AES-CFB encryption (RFC 3826 Section 3.1).
515    fn encrypt_aes(
516        &self,
517        plaintext: &[u8],
518        engine_boots: u32,
519        engine_time: u32,
520        salt: u64,
521        key_len: usize,
522    ) -> PrivacyResult<(Bytes, Bytes)> {
523        // AES key is first key_len bytes
524        let key = &self.key[..key_len];
525
526        // Salt as 8 bytes (big-endian)
527        let salt_bytes = salt.to_be_bytes();
528
529        // IV = engineBoots (4) || engineTime (4) || salt (8) = 16 bytes
530        // This is CONCATENATION, not XOR (unlike DES)
531        let mut iv = [0u8; 16];
532        iv[..4].copy_from_slice(&engine_boots.to_be_bytes());
533        iv[4..8].copy_from_slice(&engine_time.to_be_bytes());
534        iv[8..].copy_from_slice(&salt_bytes);
535
536        let mut buffer = plaintext.to_vec();
537        super::crypto::provider().encrypt(self.protocol, key, &iv, &mut buffer)?;
538
539        Ok((Bytes::from(buffer), Bytes::copy_from_slice(&salt_bytes)))
540    }
541
542    /// AES-CFB decryption (RFC 3826 Section 3.1.4).
543    fn decrypt_aes(
544        &self,
545        ciphertext: &[u8],
546        engine_boots: u32,
547        engine_time: u32,
548        priv_params: &[u8],
549    ) -> PrivacyResult<Bytes> {
550        let key_len = match self.protocol {
551            PrivProtocol::Aes128 => 16,
552            PrivProtocol::Aes192 => 24,
553            PrivProtocol::Aes256 => 32,
554            _ => unreachable!(),
555        };
556
557        // AES key is first key_len bytes
558        let key = &self.key[..key_len];
559
560        // IV = engineBoots (4) || engineTime (4) || salt (8) = 16 bytes
561        let mut iv = [0u8; 16];
562        iv[..4].copy_from_slice(&engine_boots.to_be_bytes());
563        iv[4..8].copy_from_slice(&engine_time.to_be_bytes());
564        iv[8..].copy_from_slice(priv_params);
565
566        let mut buffer = ciphertext.to_vec();
567        super::crypto::provider().decrypt(self.protocol, key, &iv, &mut buffer)?;
568
569        Ok(Bytes::from(buffer))
570    }
571}
572
573impl std::fmt::Debug for PrivKey {
574    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
575        f.debug_struct("PrivKey")
576            .field("protocol", &self.protocol)
577            .field("key", &"[REDACTED]")
578            .finish()
579    }
580}
581
582impl Clone for PrivKey {
583    fn clone(&self) -> Self {
584        Self {
585            key: self.key.clone(),
586            protocol: self.protocol,
587            // Fresh counter so the clone does not replay the same salt sequence.
588            salt_counter: Self::init_salt().expect("OS random source unavailable"),
589        }
590    }
591}
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596    use crate::format::hex::decode as decode_hex;
597
598    #[cfg(feature = "crypto-rustcrypto")]
599    #[test]
600    fn test_des_encrypt_decrypt_roundtrip() {
601        // Create a 16-byte key (8 for DES, 8 for pre-IV)
602        let key = vec![
603            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // DES key
604            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // pre-IV
605        ];
606        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, key).unwrap();
607
608        let plaintext = b"Hello, SNMPv3 World!";
609        let engine_boots = 100u32;
610        let engine_time = 12345u32;
611
612        let (ciphertext, priv_params) = priv_key
613            .encrypt(plaintext, engine_boots, engine_time, None)
614            .expect("encryption failed");
615
616        // Verify ciphertext is different from plaintext
617        assert_ne!(ciphertext.as_ref(), plaintext);
618        // Verify priv_params is 8 bytes
619        assert_eq!(priv_params.len(), 8);
620
621        // Decrypt
622        let decrypted = priv_key
623            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
624            .expect("decryption failed");
625
626        // DES pads to 8-byte boundary, so decrypted may be longer
627        assert!(decrypted.len() >= plaintext.len());
628        assert_eq!(&decrypted[..plaintext.len()], plaintext);
629    }
630
631    #[cfg(feature = "crypto-rustcrypto")]
632    #[test]
633    fn test_des3_encrypt_decrypt_roundtrip() {
634        // Create a 32-byte key (24 for 3DES, 8 for pre-IV)
635        let key = vec![
636            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // K1
637            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // K2
638            0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, // K3
639            0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, // pre-IV
640        ];
641        let priv_key = PrivKey::from_bytes(PrivProtocol::Des3, key).unwrap();
642
643        let plaintext = b"Hello, SNMPv3 World with 3DES!";
644        let engine_boots = 100u32;
645        let engine_time = 12345u32;
646
647        let (ciphertext, priv_params) = priv_key
648            .encrypt(plaintext, engine_boots, engine_time, None)
649            .expect("encryption failed");
650
651        // Verify ciphertext is different from plaintext
652        assert_ne!(ciphertext.as_ref(), plaintext);
653        // Verify priv_params is 8 bytes
654        assert_eq!(priv_params.len(), 8);
655
656        // Decrypt
657        let decrypted = priv_key
658            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
659            .expect("decryption failed");
660
661        // 3DES pads to 8-byte boundary, so decrypted may be longer
662        assert!(decrypted.len() >= plaintext.len());
663        assert_eq!(&decrypted[..plaintext.len()], plaintext);
664    }
665
666    #[test]
667    fn test_aes128_encrypt_decrypt_roundtrip() {
668        // Create a 16-byte key for AES-128
669        let key = vec![
670            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
671            0x0f, 0x10,
672        ];
673        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
674
675        let plaintext = b"Hello, SNMPv3 AES World!";
676        let engine_boots = 200u32;
677        let engine_time = 54321u32;
678
679        let (ciphertext, priv_params) = priv_key
680            .encrypt(plaintext, engine_boots, engine_time, None)
681            .expect("encryption failed");
682
683        // Verify ciphertext is different from plaintext
684        assert_ne!(ciphertext.as_ref(), plaintext);
685        // Verify priv_params is 8 bytes (salt)
686        assert_eq!(priv_params.len(), 8);
687
688        // Decrypt
689        let decrypted = priv_key
690            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
691            .expect("decryption failed");
692
693        // AES-CFB doesn't require padding, so lengths should match
694        assert_eq!(decrypted.len(), plaintext.len());
695        assert_eq!(decrypted.as_ref(), plaintext);
696    }
697
698    #[cfg(feature = "crypto-rustcrypto")]
699    #[test]
700    fn test_des_invalid_ciphertext_length() {
701        let key = vec![0u8; 16];
702        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, key).unwrap();
703
704        // Ciphertext not multiple of 8
705        let ciphertext = [0u8; 13];
706        let priv_params = [0u8; 8];
707
708        let result = priv_key.decrypt(&ciphertext, 0, 0, &priv_params);
709        assert!(result.is_err());
710    }
711
712    #[test]
713    fn test_invalid_priv_params_length() {
714        let key = vec![0u8; 16];
715        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
716
717        // priv_params should be 8 bytes
718        let ciphertext = [0u8; 16];
719        let priv_params = [0u8; 4]; // Wrong length
720
721        let result = priv_key.decrypt(&ciphertext, 0, 0, &priv_params);
722        assert!(result.is_err());
723    }
724
725    #[test]
726    fn test_salt_counter() {
727        let counter = SaltCounter::new();
728        let s1 = counter.next();
729        let s2 = counter.next();
730        let s3 = counter.next();
731
732        // Each call should increment
733        assert_eq!(s2, s1.wrapping_add(1));
734        assert_eq!(s3, s2.wrapping_add(1));
735    }
736
737    /// Test that SaltCounter never returns zero.
738    ///
739    /// Per net-snmp behavior (snmpusm.c:1319-1320), zero salt values should be
740    /// skipped to avoid potential IV reuse issues on wraparound.
741    #[test]
742    fn test_salt_counter_skips_zero() {
743        // Create a counter initialized to u64::MAX - 1 so the next call wraps through MAX.
744        // next() returns post-increment, so:
745        //   call 1: old=MAX-1, val=MAX, returns MAX
746        //   call 2: old=MAX,   val=0 (wrapped), skips 0, returns 1
747        //   call 3: old=1,     val=2, returns 2
748        let counter = SaltCounter::from_value(u64::MAX - 1);
749
750        let s1 = counter.next();
751        assert_eq!(s1, u64::MAX);
752
753        // This call wraps to zero; should skip and return 1
754        let s2 = counter.next();
755        assert_ne!(s2, 0, "SaltCounter should never return zero");
756        assert_eq!(s2, 1, "SaltCounter should skip 0 and return 1");
757
758        // Subsequent calls should continue normally
759        let s3 = counter.next();
760        assert_eq!(s3, 2);
761    }
762
763    /// Test that PrivKey's internal salt counter never produces zero.
764    ///
765    /// When using the internal counter (not a shared SaltCounter), the salt
766    /// should also skip zero on wraparound.
767    #[test]
768    fn test_priv_key_internal_salt_skips_zero() {
769        let key = vec![0u8; 16];
770        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
771
772        // Set the internal counter to u64::MAX
773        priv_key.salt_counter.store(u64::MAX, Ordering::Relaxed);
774
775        let plaintext = b"test";
776
777        // First encryption uses u64::MAX
778        let (_, salt1) = priv_key.encrypt(plaintext, 0, 0, None).unwrap();
779        assert_eq!(
780            u64::from_be_bytes(salt1.as_ref().try_into().unwrap()),
781            u64::MAX
782        );
783
784        // Second encryption should skip 0 and use 1
785        let (_, salt2) = priv_key.encrypt(plaintext, 0, 0, None).unwrap();
786        let salt2_value = u64::from_be_bytes(salt2.as_ref().try_into().unwrap());
787        assert_ne!(salt2_value, 0, "Salt should never be zero");
788        assert_eq!(salt2_value, 1, "Salt should skip 0 and be 1");
789
790        // Third encryption should use 2
791        let (_, salt3) = priv_key.encrypt(plaintext, 0, 0, None).unwrap();
792        let salt3_value = u64::from_be_bytes(salt3.as_ref().try_into().unwrap());
793        assert_eq!(salt3_value, 2);
794    }
795
796    #[test]
797    fn test_multiple_encryptions_different_salt() {
798        let key = vec![0u8; 16];
799        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
800
801        let plaintext = b"test data";
802
803        let (_, salt1) = priv_key.encrypt(plaintext, 0, 0, None).unwrap();
804        let (_, salt2) = priv_key.encrypt(plaintext, 0, 0, None).unwrap();
805
806        // Salts should be different for each encryption
807        assert_ne!(salt1, salt2);
808    }
809
810    #[test]
811    fn test_from_password() {
812        // Test that we can derive a privacy key from a password
813        let password = b"maplesyrup";
814        let engine_id = decode_hex("000000000000000000000002").unwrap();
815
816        let priv_key = PrivKey::from_password(
817            AuthProtocol::Sha1,
818            PrivProtocol::Aes128,
819            password,
820            &engine_id,
821        )
822        .unwrap();
823
824        // Just verify we can encrypt/decrypt with the derived key
825        let plaintext = b"test message";
826        let (ciphertext, priv_params) = priv_key.encrypt(plaintext, 100, 200, None).unwrap();
827        let decrypted = priv_key
828            .decrypt(&ciphertext, 100, 200, &priv_params)
829            .unwrap();
830
831        assert_eq!(decrypted.as_ref(), plaintext);
832    }
833
834    #[test]
835    fn test_aes192_encrypt_decrypt_roundtrip() {
836        // Create a 24-byte key for AES-192
837        let key = vec![
838            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
839            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
840        ];
841        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes192, key).unwrap();
842
843        let plaintext = b"Hello, SNMPv3 AES-192 World!";
844        let engine_boots = 300u32;
845        let engine_time = 67890u32;
846
847        let (ciphertext, priv_params) = priv_key
848            .encrypt(plaintext, engine_boots, engine_time, None)
849            .expect("AES-192 encryption failed");
850
851        // Verify ciphertext is different from plaintext
852        assert_ne!(ciphertext.as_ref(), plaintext);
853        // Verify priv_params is 8 bytes (salt)
854        assert_eq!(priv_params.len(), 8);
855
856        // Decrypt
857        let decrypted = priv_key
858            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
859            .expect("AES-192 decryption failed");
860
861        // AES-CFB doesn't require padding, so lengths should match
862        assert_eq!(decrypted.len(), plaintext.len());
863        assert_eq!(decrypted.as_ref(), plaintext);
864    }
865
866    #[test]
867    fn test_aes256_encrypt_decrypt_roundtrip() {
868        // Create a 32-byte key for AES-256
869        let key = vec![
870            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
871            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
872            0x1d, 0x1e, 0x1f, 0x20,
873        ];
874        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes256, key).unwrap();
875
876        let plaintext = b"Hello, SNMPv3 AES-256 World!";
877        let engine_boots = 400u32;
878        let engine_time = 11111u32;
879
880        let (ciphertext, priv_params) = priv_key
881            .encrypt(plaintext, engine_boots, engine_time, None)
882            .expect("AES-256 encryption failed");
883
884        // Verify ciphertext is different from plaintext
885        assert_ne!(ciphertext.as_ref(), plaintext);
886        // Verify priv_params is 8 bytes (salt)
887        assert_eq!(priv_params.len(), 8);
888
889        // Decrypt
890        let decrypted = priv_key
891            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
892            .expect("AES-256 decryption failed");
893
894        // AES-CFB doesn't require padding, so lengths should match
895        assert_eq!(decrypted.len(), plaintext.len());
896        assert_eq!(decrypted.as_ref(), plaintext);
897    }
898
899    #[test]
900    fn test_aes192_from_password() {
901        // For AES-192 (24-byte key), we need SHA-224 or higher auth protocol
902        let password = b"longpassword123";
903        let engine_id = decode_hex("80001f8880e9b104617361000000").unwrap();
904
905        let priv_key = PrivKey::from_password(
906            AuthProtocol::Sha256, // SHA-256 produces 32 bytes, enough for AES-192
907            PrivProtocol::Aes192,
908            password,
909            &engine_id,
910        )
911        .unwrap();
912
913        let plaintext = b"test message for AES-192";
914        let (ciphertext, priv_params) = priv_key.encrypt(plaintext, 100, 200, None).unwrap();
915        let decrypted = priv_key
916            .decrypt(&ciphertext, 100, 200, &priv_params)
917            .unwrap();
918
919        assert_eq!(decrypted.as_ref(), plaintext);
920    }
921
922    #[test]
923    fn test_aes256_from_password() {
924        // For AES-256 (32-byte key), we need SHA-256 or higher auth protocol
925        let password = b"anotherlongpassword456";
926        let engine_id = decode_hex("80001f8880e9b104617361000000").unwrap();
927
928        let priv_key = PrivKey::from_password(
929            AuthProtocol::Sha256, // SHA-256 produces 32 bytes, exactly enough for AES-256
930            PrivProtocol::Aes256,
931            password,
932            &engine_id,
933        )
934        .unwrap();
935
936        let plaintext = b"test message for AES-256";
937        let (ciphertext, priv_params) = priv_key.encrypt(plaintext, 100, 200, None).unwrap();
938        let decrypted = priv_key
939            .decrypt(&ciphertext, 100, 200, &priv_params)
940            .unwrap();
941
942        assert_eq!(decrypted.as_ref(), plaintext);
943    }
944
945    // ========================================================================
946    // Wrong Key Decryption Tests
947    //
948    // These tests verify that decryption with the wrong key produces garbage,
949    // not the original plaintext. Note: Stream ciphers like AES-CFB don't return
950    // errors on wrong-key decryption - they produce garbage. The authentication
951    // layer (HMAC) is what detects tampering/wrong keys in practice (RFC 3414).
952    // ========================================================================
953
954    #[cfg(feature = "crypto-rustcrypto")]
955    #[test]
956    fn test_des_wrong_key_produces_garbage() {
957        // Correct 16-byte key
958        let correct_key = vec![
959            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
960            0x17, 0x18,
961        ];
962        // Wrong key (different from correct key)
963        let wrong_key = vec![
964            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xE7, 0xE6, 0xE5, 0xE4, 0xE3, 0xE2,
965            0xE1, 0xE0,
966        ];
967
968        let correct_priv_key = PrivKey::from_bytes(PrivProtocol::Des, correct_key).unwrap();
969        let wrong_priv_key = PrivKey::from_bytes(PrivProtocol::Des, wrong_key).unwrap();
970
971        let plaintext = b"Secret SNMPv3 message data!";
972        let engine_boots = 100u32;
973        let engine_time = 12345u32;
974
975        // Encrypt with correct key
976        let (ciphertext, priv_params) = correct_priv_key
977            .encrypt(plaintext, engine_boots, engine_time, None)
978            .expect("encryption failed");
979
980        // Decrypt with wrong key - this will "succeed" but produce garbage
981        let wrong_decrypted = wrong_priv_key
982            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
983            .expect("decryption should succeed cryptographically");
984
985        // Verify wrong key produces different output (not the original plaintext)
986        assert_ne!(
987            &wrong_decrypted[..plaintext.len()],
988            plaintext,
989            "wrong key should NOT produce the original plaintext"
990        );
991
992        // Verify correct key still works
993        let correct_decrypted = correct_priv_key
994            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
995            .expect("correct key decryption failed");
996        assert_eq!(
997            &correct_decrypted[..plaintext.len()],
998            plaintext,
999            "correct key should produce the original plaintext"
1000        );
1001    }
1002
1003    #[test]
1004    fn test_aes128_wrong_key_produces_garbage() {
1005        let correct_key = vec![
1006            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1007            0x0f, 0x10,
1008        ];
1009        let wrong_key = vec![
1010            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2,
1011            0xF1, 0xF0,
1012        ];
1013
1014        let correct_priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, correct_key).unwrap();
1015        let wrong_priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, wrong_key).unwrap();
1016
1017        let plaintext = b"Secret AES-128 message data!";
1018        let engine_boots = 200u32;
1019        let engine_time = 54321u32;
1020
1021        // Encrypt with correct key
1022        let (ciphertext, priv_params) = correct_priv_key
1023            .encrypt(plaintext, engine_boots, engine_time, None)
1024            .expect("encryption failed");
1025
1026        // Decrypt with wrong key
1027        let wrong_decrypted = wrong_priv_key
1028            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1029            .expect("decryption should succeed cryptographically");
1030
1031        // Wrong key should produce garbage (not the original plaintext)
1032        assert_ne!(
1033            wrong_decrypted.as_ref(),
1034            plaintext,
1035            "wrong key should NOT produce the original plaintext"
1036        );
1037
1038        // Correct key should work
1039        let correct_decrypted = correct_priv_key
1040            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1041            .expect("correct key decryption failed");
1042        assert_eq!(correct_decrypted.as_ref(), plaintext);
1043    }
1044
1045    #[test]
1046    fn test_aes192_wrong_key_produces_garbage() {
1047        let correct_key = vec![
1048            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1049            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
1050        ];
1051        let wrong_key = vec![
1052            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2,
1053            0xF1, 0xF0, 0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8,
1054        ];
1055
1056        let correct_priv_key = PrivKey::from_bytes(PrivProtocol::Aes192, correct_key).unwrap();
1057        let wrong_priv_key = PrivKey::from_bytes(PrivProtocol::Aes192, wrong_key).unwrap();
1058
1059        let plaintext = b"Secret AES-192 message data!";
1060        let engine_boots = 300u32;
1061        let engine_time = 67890u32;
1062
1063        let (ciphertext, priv_params) = correct_priv_key
1064            .encrypt(plaintext, engine_boots, engine_time, None)
1065            .expect("encryption failed");
1066
1067        let wrong_decrypted = wrong_priv_key
1068            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1069            .expect("decryption should succeed cryptographically");
1070
1071        assert_ne!(
1072            wrong_decrypted.as_ref(),
1073            plaintext,
1074            "wrong key should NOT produce the original plaintext"
1075        );
1076    }
1077
1078    #[test]
1079    fn test_aes256_wrong_key_produces_garbage() {
1080        let correct_key = vec![
1081            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1082            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
1083            0x1d, 0x1e, 0x1f, 0x20,
1084        ];
1085        let wrong_key = vec![
1086            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2,
1087            0xF1, 0xF0, 0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8, 0xE7, 0xE6, 0xE5, 0xE4,
1088            0xE3, 0xE2, 0xE1, 0xE0,
1089        ];
1090
1091        let correct_priv_key = PrivKey::from_bytes(PrivProtocol::Aes256, correct_key).unwrap();
1092        let wrong_priv_key = PrivKey::from_bytes(PrivProtocol::Aes256, wrong_key).unwrap();
1093
1094        let plaintext = b"Secret AES-256 message data!";
1095        let engine_boots = 400u32;
1096        let engine_time = 11111u32;
1097
1098        let (ciphertext, priv_params) = correct_priv_key
1099            .encrypt(plaintext, engine_boots, engine_time, None)
1100            .expect("encryption failed");
1101
1102        let wrong_decrypted = wrong_priv_key
1103            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1104            .expect("decryption should succeed cryptographically");
1105
1106        assert_ne!(
1107            wrong_decrypted.as_ref(),
1108            plaintext,
1109            "wrong key should NOT produce the original plaintext"
1110        );
1111    }
1112
1113    #[cfg(feature = "crypto-rustcrypto")]
1114    #[test]
1115    fn test_des_wrong_priv_params_produces_garbage() {
1116        // Verify that even with the correct key, wrong priv_params (salt/IV)
1117        // produces garbage. This tests the IV derivation logic.
1118        let key = vec![
1119            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
1120            0x17, 0x18,
1121        ];
1122
1123        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, key).unwrap();
1124
1125        let plaintext = b"DES test message";
1126        let engine_boots = 100u32;
1127        let engine_time = 12345u32;
1128
1129        let (ciphertext, correct_priv_params) = priv_key
1130            .encrypt(plaintext, engine_boots, engine_time, None)
1131            .expect("encryption failed");
1132
1133        // Use wrong priv_params (different salt)
1134        let wrong_priv_params = [0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88];
1135
1136        let wrong_decrypted = priv_key
1137            .decrypt(&ciphertext, engine_boots, engine_time, &wrong_priv_params)
1138            .expect("decryption should succeed cryptographically");
1139
1140        // Wrong IV should produce garbage
1141        assert_ne!(
1142            &wrong_decrypted[..plaintext.len()],
1143            plaintext,
1144            "wrong priv_params should NOT produce the original plaintext"
1145        );
1146
1147        // Correct priv_params should work
1148        let correct_decrypted = priv_key
1149            .decrypt(&ciphertext, engine_boots, engine_time, &correct_priv_params)
1150            .expect("correct decryption failed");
1151        assert_eq!(&correct_decrypted[..plaintext.len()], plaintext);
1152    }
1153
1154    /// Test that SaltCounter never emits duplicate salts under concurrent access.
1155    ///
1156    /// This is a regression test for the two-fetch_add race where two threads
1157    /// could both return 1 after a wraparound left the counter at 0.
1158    #[test]
1159    fn test_salt_counter_no_duplicates_concurrent() {
1160        use std::collections::HashSet;
1161        use std::sync::{Arc, Mutex};
1162        use std::thread;
1163
1164        let counter = Arc::new(SaltCounter::new());
1165        let results = Arc::new(Mutex::new(HashSet::new()));
1166        let iterations = 10_000usize;
1167        let threads = 8usize;
1168
1169        let handles: Vec<_> = (0..threads)
1170            .map(|_| {
1171                let counter = Arc::clone(&counter);
1172                let results = Arc::clone(&results);
1173                thread::spawn(move || {
1174                    for _ in 0..iterations {
1175                        let salt = counter.next();
1176                        assert_ne!(salt, 0, "SaltCounter must never return zero");
1177                        let mut set = results.lock().unwrap();
1178                        assert!(set.insert(salt), "SaltCounter emitted duplicate: {salt}");
1179                    }
1180                })
1181            })
1182            .collect();
1183
1184        for h in handles {
1185            h.join().expect("thread panicked");
1186        }
1187    }
1188
1189    /// Test that a cloned PrivKey starts with an independent salt counter.
1190    ///
1191    /// This is a regression test for derive(Clone) copying the salt_counter
1192    /// field, which caused clones to emit identical salts for their first encryptions.
1193    #[test]
1194    fn test_priv_key_clone_independent_salts() {
1195        let key = vec![0u8; 16];
1196        let original = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
1197        let cloned = original.clone();
1198
1199        let plaintext = b"test";
1200
1201        // Encrypt once with each key; the priv_params (salt) must differ.
1202        let (_, salt_orig) = original.encrypt(plaintext, 0, 0, None).unwrap();
1203        let (_, salt_clone) = cloned.encrypt(plaintext, 0, 0, None).unwrap();
1204
1205        assert_ne!(
1206            salt_orig, salt_clone,
1207            "cloned PrivKey must start with an independent salt counter"
1208        );
1209    }
1210
1211    #[test]
1212    fn test_aes_wrong_engine_time_produces_garbage() {
1213        // For AES, the IV includes engine_boots and engine_time.
1214        // Wrong values should produce garbage.
1215        let key = vec![
1216            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1217            0x0f, 0x10,
1218        ];
1219
1220        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
1221
1222        let plaintext = b"AES test message";
1223        let engine_boots = 200u32;
1224        let engine_time = 54321u32;
1225
1226        let (ciphertext, priv_params) = priv_key
1227            .encrypt(plaintext, engine_boots, engine_time, None)
1228            .expect("encryption failed");
1229
1230        // Decrypt with wrong engine_time (IV mismatch)
1231        let wrong_decrypted = priv_key
1232            .decrypt(&ciphertext, engine_boots, engine_time + 1, &priv_params)
1233            .expect("decryption should succeed cryptographically");
1234
1235        assert_ne!(
1236            wrong_decrypted.as_ref(),
1237            plaintext,
1238            "wrong engine_time should NOT produce the original plaintext"
1239        );
1240
1241        // Decrypt with wrong engine_boots (IV mismatch)
1242        let wrong_decrypted2 = priv_key
1243            .decrypt(&ciphertext, engine_boots + 1, engine_time, &priv_params)
1244            .expect("decryption should succeed cryptographically");
1245
1246        assert_ne!(
1247            wrong_decrypted2.as_ref(),
1248            plaintext,
1249            "wrong engine_boots should NOT produce the original plaintext"
1250        );
1251    }
1252}