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