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