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 with explicit Blumenthal or Reeder key extension
7//!   (draft/vendor extension, not RFC 3826)
8//! - AES-256-CFB privacy with explicit Blumenthal or Reeder key extension
9//!   (draft/vendor extension, not RFC 3826)
10//!
11//! # Salt and IV construction
12//!
13//! ## DES-CBC
14//! - Salt (privParameters): engineBoots (4 bytes) || counter (4 bytes) = 8 bytes
15//! - IV: pre-IV XOR salt (pre-IV is last 8 bytes of 16-byte privKey)
16//!
17//! ## AES-CFB-128
18//! - Salt (privParameters): 64-bit counter = 8 bytes
19//! - IV: engineBoots (4 bytes) || engineTime (4 bytes) || salt (8 bytes) = 16 bytes
20//!   (concatenation, NOT XOR)
21
22use std::fmt::{Debug, Formatter};
23use std::sync::Arc;
24use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
25
26use bytes::Bytes;
27use zeroize::{Zeroize, ZeroizeOnDrop};
28
29use super::crypto::{CryptoBackend, CryptoError};
30use super::{AuthProtocol, PrivProtocol};
31
32/// Error type for privacy (encryption/decryption) operations.
33///
34/// These errors indicate privacy-state or cryptographic failures.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum PrivacyError {
37    /// Invalid privParameters length (expected 8 bytes).
38    InvalidPrivParamsLength { expected: usize, actual: usize },
39    /// Ciphertext length not a multiple of block size.
40    InvalidCiphertextLength { length: usize, block_size: usize },
41    /// Cryptographic provider error (unsupported algorithm, invalid key, cipher failure).
42    Crypto(CryptoError),
43    /// The supplied sender state does not match the key's privacy protocol.
44    SenderStateMismatch,
45    /// Every 32-bit salt in this DES generating-engine epoch has been used.
46    DesSaltExhausted { engine_boots: u32 },
47    /// Durable DES state does not match the current local generating engine.
48    DesEngineBootsMismatch {
49        state_engine_boots: u32,
50        generating_engine_boots: u32,
51    },
52}
53
54impl From<CryptoError> for PrivacyError {
55    fn from(e: CryptoError) -> Self {
56        Self::Crypto(e)
57    }
58}
59
60impl std::fmt::Display for PrivacyError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::InvalidPrivParamsLength { expected, actual } => {
64                write!(
65                    f,
66                    "invalid privParameters length: expected {expected}, got {actual}"
67                )
68            }
69            Self::InvalidCiphertextLength { length, block_size } => {
70                write!(
71                    f,
72                    "ciphertext length {length} not multiple of block size {block_size}"
73                )
74            }
75            Self::Crypto(e) => write!(f, "{e}"),
76            Self::SenderStateMismatch => {
77                f.write_str("privacy sender state does not match protocol")
78            }
79            Self::DesSaltExhausted { engine_boots } => write!(
80                f,
81                "DES privacy salt exhausted for generating-engine boots {engine_boots}"
82            ),
83            Self::DesEngineBootsMismatch {
84                state_engine_boots,
85                generating_engine_boots,
86            } => write!(
87                f,
88                "DES sender state boots {state_engine_boots} do not match generating-engine boots {generating_engine_boots}"
89            ),
90        }
91    }
92}
93
94type DesPersistenceSource = Box<dyn std::error::Error + Send + Sync + 'static>;
95
96/// The durable DES generating-engine transition being attempted.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98#[non_exhaustive]
99pub enum DesSaltPersistenceOperation {
100    /// Establishing a new key domain at boots epoch 1.
101    Install,
102    /// Atomically advancing a previously persisted boots epoch.
103    Restart,
104}
105
106/// Durable DES sender state loaded by an application at startup.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct PersistedDesSaltState {
109    engine_boots: u32,
110}
111
112impl PersistedDesSaltState {
113    /// Validate a persisted local generating-engine boots epoch.
114    pub fn new(engine_boots: u32) -> std::result::Result<Self, DesSaltStateError> {
115        if !(1..=super::MAX_ENGINE_TIME).contains(&engine_boots) {
116            return Err(DesSaltStateError::InvalidEpoch { engine_boots });
117        }
118        Ok(Self { engine_boots })
119    }
120
121    /// Return the persisted generating-engine boots epoch.
122    #[must_use]
123    pub fn engine_boots(self) -> u32 {
124        self.engine_boots
125    }
126}
127
128/// A failed durable DES generating-engine transition.
129#[derive(Debug)]
130pub struct DesSaltPersistenceError {
131    operation: DesSaltPersistenceOperation,
132    previous_engine_boots: Option<u32>,
133    attempted_engine_boots: u32,
134    source: DesPersistenceSource,
135}
136
137impl DesSaltPersistenceError {
138    /// Return the failed transition.
139    #[must_use]
140    pub fn operation(&self) -> DesSaltPersistenceOperation {
141        self.operation
142    }
143
144    /// Return the last durable epoch, if this was a restart.
145    #[must_use]
146    pub fn previous_engine_boots(&self) -> Option<u32> {
147        self.previous_engine_boots
148    }
149
150    /// Return the epoch that the callback was asked to persist.
151    #[must_use]
152    pub fn attempted_engine_boots(&self) -> u32 {
153        self.attempted_engine_boots
154    }
155
156    /// Return the concrete persistence callback error.
157    #[must_use]
158    pub fn persistence_source(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
159        self.source.as_ref()
160    }
161
162    /// Downcast the callback error to its concrete type.
163    #[must_use]
164    pub fn downcast_source_ref<E: std::error::Error + 'static>(&self) -> Option<&E> {
165        self.source.downcast_ref()
166    }
167}
168
169impl std::fmt::Display for DesSaltPersistenceError {
170    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
171        write!(
172            f,
173            "DES sender-state persistence failed during {:?} at boots {}: {}",
174            self.operation, self.attempted_engine_boots, self.source
175        )
176    }
177}
178
179impl std::error::Error for DesSaltPersistenceError {
180    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
181        Some(self.source.as_ref())
182    }
183}
184
185/// Error creating or advancing durable DES sender state.
186#[derive(Debug)]
187#[non_exhaustive]
188pub enum DesSaltStateError {
189    /// The persisted epoch is outside the SNMP engine-boots domain.
190    InvalidEpoch { engine_boots: u32 },
191    /// The boots epoch cannot be advanced without reuse.
192    EpochSaturated { engine_boots: u32 },
193    /// The durable compare-and-set/install operation failed.
194    Persistence(DesSaltPersistenceError),
195}
196
197impl std::fmt::Display for DesSaltStateError {
198    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
199        match self {
200            Self::InvalidEpoch { engine_boots } => {
201                write!(
202                    f,
203                    "invalid DES generating-engine boots epoch {engine_boots}"
204                )
205            }
206            Self::EpochSaturated { engine_boots } => {
207                write!(
208                    f,
209                    "DES generating-engine boots epoch {engine_boots} is saturated"
210                )
211            }
212            Self::Persistence(error) => std::fmt::Display::fmt(error, f),
213        }
214    }
215}
216
217impl std::error::Error for DesSaltStateError {
218    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
219        match self {
220            Self::Persistence(error) => Some(error),
221            _ => None,
222        }
223    }
224}
225
226/// Caller-owned durable sender state for DES and 3DES privacy.
227///
228/// One value (or its clones) must be shared by every live sender using the
229/// same effective localized encryption-key/pre-IV domain. `install` requires
230/// an atomic create-if-absent lease for a new domain, while `restart` requires
231/// an atomic compare-and-set from the supplied previous epoch to the attempted
232/// epoch. Those contracts reject a second live process starting from the same
233/// durable state.
234#[derive(Clone)]
235pub struct DesSaltState {
236    inner: Arc<DesSaltStateInner>,
237}
238
239/// One irrevocably allocated DES/3DES privacy salt.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct DesSaltReservation {
242    engine_boots: u32,
243    salt: u32,
244}
245
246impl DesSaltReservation {
247    /// Return the local generating-engine boots encoded in this reservation.
248    #[must_use]
249    pub fn engine_boots(self) -> u32 {
250        self.engine_boots
251    }
252
253    /// Return the non-repeating low 32-bit salt value.
254    #[must_use]
255    pub fn salt(self) -> u32 {
256        self.salt
257    }
258}
259
260struct DesSaltStateInner {
261    engine_boots: u32,
262    last_salt: AtomicU32,
263}
264
265impl Debug for DesSaltState {
266    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
267        f.debug_struct("DesSaltState")
268            .field("engine_boots", &self.engine_boots())
269            .finish_non_exhaustive()
270    }
271}
272
273impl DesSaltState {
274    /// Install a new DES key domain, persisting boots epoch 1 before use.
275    ///
276    /// The callback must atomically create the durable record only if it does
277    /// not already exist. A competing installer for the same effective key
278    /// domain must return an error rather than lease epoch 1 twice.
279    pub fn install<E, F>(mut persist: F) -> std::result::Result<Self, DesSaltStateError>
280    where
281        E: Into<DesPersistenceSource>,
282        F: FnMut(&PersistedDesSaltState) -> std::result::Result<(), E>,
283    {
284        Self::start(None, 1, DesSaltPersistenceOperation::Install, &mut persist)
285    }
286
287    /// Atomically advance persisted state and create a fresh boots epoch.
288    ///
289    /// The callback must compare-and-set durable state from `previous` to the
290    /// supplied attempted state; a stale second live owner must return an error.
291    pub fn restart<E, F>(
292        previous: PersistedDesSaltState,
293        mut persist: F,
294    ) -> std::result::Result<Self, DesSaltStateError>
295    where
296        E: Into<DesPersistenceSource>,
297        F: FnMut(&PersistedDesSaltState) -> std::result::Result<(), E>,
298    {
299        let next = previous
300            .engine_boots
301            .checked_add(1)
302            .filter(|boots| *boots <= super::MAX_ENGINE_TIME)
303            .ok_or(DesSaltStateError::EpochSaturated {
304                engine_boots: previous.engine_boots,
305            })?;
306        Self::start(
307            Some(previous.engine_boots),
308            next,
309            DesSaltPersistenceOperation::Restart,
310            &mut persist,
311        )
312    }
313
314    fn start<E, F>(
315        previous_engine_boots: Option<u32>,
316        engine_boots: u32,
317        operation: DesSaltPersistenceOperation,
318        persist: &mut F,
319    ) -> std::result::Result<Self, DesSaltStateError>
320    where
321        E: Into<DesPersistenceSource>,
322        F: FnMut(&PersistedDesSaltState) -> std::result::Result<(), E>,
323    {
324        let persisted = PersistedDesSaltState::new(engine_boots)?;
325        persist(&persisted).map_err(|source| {
326            DesSaltStateError::Persistence(DesSaltPersistenceError {
327                operation,
328                previous_engine_boots,
329                attempted_engine_boots: engine_boots,
330                source: source.into(),
331            })
332        })?;
333        Ok(Self {
334            inner: Arc::new(DesSaltStateInner {
335                engine_boots,
336                last_salt: AtomicU32::new(0),
337            }),
338        })
339    }
340
341    /// Return the local generating-engine boots epoch used in DES salts.
342    #[must_use]
343    pub fn engine_boots(&self) -> u32 {
344        self.inner.engine_boots
345    }
346
347    /// Return the durable record needed by a later process restart.
348    #[must_use]
349    pub fn persisted_state(&self) -> PersistedDesSaltState {
350        PersistedDesSaltState {
351            engine_boots: self.inner.engine_boots,
352        }
353    }
354
355    /// Irrevocably allocate the next salt in this boots epoch.
356    ///
357    /// The reservation is burned even if later message encoding or encryption
358    /// fails. It cannot be constructed or reused with a different epoch.
359    pub fn reserve(&self) -> PrivacyResult<DesSaltReservation> {
360        let salt = self
361            .inner
362            .last_salt
363            .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |current| {
364                current.checked_add(1)
365            })
366            .map(|previous| previous + 1)
367            .map_err(|_| PrivacyError::DesSaltExhausted {
368                engine_boots: self.inner.engine_boots,
369            })?;
370        Ok(DesSaltReservation {
371            engine_boots: self.inner.engine_boots,
372            salt,
373        })
374    }
375
376    pub(crate) fn validate_generating_engine_boots(
377        &self,
378        generating_engine_boots: u32,
379    ) -> PrivacyResult<()> {
380        if self.engine_boots() != generating_engine_boots {
381            return Err(PrivacyError::DesEngineBootsMismatch {
382                state_engine_boots: self.engine_boots(),
383                generating_engine_boots,
384            });
385        }
386        Ok(())
387    }
388
389    #[cfg(test)]
390    fn with_last_salt_for_test(engine_boots: u32, last_salt: u32) -> Self {
391        Self {
392            inner: Arc::new(DesSaltStateInner {
393                engine_boots,
394                last_salt: AtomicU32::new(last_salt),
395            }),
396        }
397    }
398}
399
400/// Protocol-specific sender inputs for one privacy encryption.
401pub(crate) enum PrivacyEncryptContext<'a> {
402    /// Local generating-engine state for DES or 3DES.
403    Des(DesSaltReservation),
404    /// Remote/local authoritative tuple and random salt allocator for AES.
405    Aes {
406        engine_boots: u32,
407        engine_time: u32,
408        salt_counter: &'a SaltCounter,
409    },
410}
411
412impl std::error::Error for PrivacyError {
413    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
414        match self {
415            Self::Crypto(e) => Some(e),
416            _ => None,
417        }
418    }
419}
420
421/// Result type for privacy operations.
422pub type PrivacyResult<T> = std::result::Result<T, PrivacyError>;
423
424/// Generate a random non-zero u64 for salt initialization.
425///
426/// Uses the OS cryptographic random source via `getrandom`.
427fn random_nonzero_u64() -> crate::error::Result<u64> {
428    random_nonzero_u64_with(getrandom::fill)
429}
430
431fn random_nonzero_u64_with(
432    mut fill: impl FnMut(&mut [u8]) -> std::result::Result<(), getrandom::Error>,
433) -> crate::error::Result<u64> {
434    let mut buf = [0u8; 8];
435    loop {
436        fill(&mut buf).map_err(|source| crate::Error::RandomSource { source }.boxed())?;
437        let val = u64::from_ne_bytes(buf);
438        if val != 0 {
439            return Ok(val);
440        }
441        // Extremely unlikely (1 in 2^64), but loop if we got zero
442    }
443}
444
445/// Privacy key for encryption/decryption operations.
446///
447/// Derives encryption keys from a password and engine ID using the same
448/// process as authentication keys, then uses the appropriate portion
449/// based on the privacy protocol.
450///
451/// # Security
452///
453/// The specifically owned key buffer is zeroized when this value is dropped.
454/// This does not promise erasure of caller inputs, live clones, allocator or
455/// provider internals, encoded message buffers, or kernel copies.
456#[derive(Clone, Zeroize, ZeroizeOnDrop)]
457pub struct PrivKey {
458    /// The localized key bytes
459    key: Vec<u8>,
460    /// Privacy protocol
461    #[zeroize(skip)]
462    protocol: PrivProtocol,
463    #[zeroize(skip)]
464    backend: CryptoBackend,
465}
466
467/// Thread-safe salt counter for shared use across privacy encryptions.
468///
469/// The owner of an authoritative engine/key domain must create one counter and
470/// pass that same counter to every encryption in the domain, including
471/// encryptions performed with cloned or re-derived [`PrivKey`] values. This
472/// keeps IV allocation independent of key object lifetime and cloning.
473pub struct SaltCounter(AtomicU64);
474
475impl SaltCounter {
476    /// Create a salt counter initialized from cryptographic randomness.
477    ///
478    /// # Errors
479    ///
480    /// Returns [`crate::Error::RandomSource`] if the operating system cannot
481    /// provide random bytes.
482    pub fn new() -> crate::error::Result<Self> {
483        Ok(Self(AtomicU64::new(random_nonzero_u64()?)))
484    }
485
486    /// Create a salt counter initialized to a specific value for internal tests.
487    #[cfg(test)]
488    #[must_use]
489    pub(crate) fn from_value(value: u64) -> Self {
490        Self(AtomicU64::new(value))
491    }
492
493    /// Returns the next salt value and increments the counter.
494    ///
495    /// This method never returns a value whose low 32 bits are zero. DES and
496    /// 3DES place only those low 32 bits in `privParameters`, so skipping that
497    /// value gives their counter portion the same non-zero wrap behavior as the
498    /// full 64-bit AES salt. A wrapping caller consumes another atomic value
499    /// rather than returning a fixed constant, preserving concurrency safety.
500    pub fn next(&self) -> u64 {
501        loop {
502            let old = self.0.fetch_add(1, Ordering::SeqCst);
503            let val = old.wrapping_add(1);
504            if val as u32 != 0 {
505                return val;
506            }
507        }
508    }
509}
510
511impl PrivKey {
512    /// Derive a privacy key from a password and engine ID.
513    ///
514    /// The key derivation uses the same algorithm as authentication keys
515    /// (RFC 3414 A.2), but the resulting key is used differently:
516    /// - DES: first 8 bytes = key, last 8 bytes = pre-IV
517    /// - 3DES: first 24 bytes = key, last 8 bytes = pre-IV
518    /// - AES: first 16/24/32 bytes = key (depending on AES variant)
519    ///
520    /// Key extension is applied when needed according to the selected privacy
521    /// protocol variant:
522    ///
523    /// - AES-192/256 Blumenthal variants: draft-blumenthal-aes-usm-04 extension
524    /// - AES-192/256 Reeder variants: Cisco/Reeder extension
525    /// - 3DES with SHA-1 or MD5: Reeder extension (draft-reeder-snmpv3-usm-3desede-00)
526    ///
527    /// The password length and both backend capabilities are validated before
528    /// password expansion or key localization begins.
529    ///
530    /// This method performs password expansion and localization. When multiple
531    /// engines share credentials, retain a [`MasterKey`](super::MasterKey) and
532    /// call [`PrivKey::from_master_key`] for each engine.
533    ///
534    /// # Example
535    ///
536    /// ```rust
537    /// # #[cfg(any(feature = "crypto-rustcrypto", feature = "crypto-fips"))]
538    /// # {
539    /// use async_snmp::{AuthProtocol, PrivProtocol, v3::PrivKey};
540    ///
541    /// let engine_id = [0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04];
542    ///
543    /// // SHA-1 only produces 20 bytes, but AES-256 needs 32.
544    /// // The configured Blumenthal extension is applied.
545    /// let priv_key = PrivKey::from_password(
546    ///     AuthProtocol::Sha1,
547    ///     PrivProtocol::Aes256Blumenthal,
548    ///     b"password",
549    ///     &engine_id,
550    /// ).unwrap();
551    /// # }
552    /// ```
553    pub fn from_password(
554        auth_protocol: AuthProtocol,
555        priv_protocol: PrivProtocol,
556        password: &[u8],
557        engine_id: &[u8],
558    ) -> super::crypto::CryptoResult<Self> {
559        if password.len() < super::auth::MIN_PASSWORD_LENGTH {
560            return Err(CryptoError::PasswordTooShort);
561        }
562        Self::from_password_with_backend(
563            auth_protocol,
564            priv_protocol,
565            password,
566            engine_id,
567            CryptoBackend::require_default()?,
568        )
569    }
570
571    /// Derive a privacy key using an explicitly selected backend.
572    pub fn from_password_with_backend(
573        auth_protocol: AuthProtocol,
574        priv_protocol: PrivProtocol,
575        password: &[u8],
576        engine_id: &[u8],
577        backend: CryptoBackend,
578    ) -> super::crypto::CryptoResult<Self> {
579        use super::MasterKey;
580
581        if password.len() < super::auth::MIN_PASSWORD_LENGTH {
582            return Err(CryptoError::PasswordTooShort);
583        }
584        backend.validate_auth_protocol(auth_protocol)?;
585        backend.validate_priv_protocol(priv_protocol)?;
586        let master = MasterKey::from_password_with_backend(auth_protocol, password, backend)?;
587        Self::from_master_key(&master, priv_protocol, engine_id)
588    }
589
590    /// Derive a privacy key from a master key and engine ID.
591    ///
592    /// This avoids repeating password expansion when a cached
593    /// [`MasterKey`](super::MasterKey) is available.
594    /// Key extension is applied when needed according to the selected privacy
595    /// protocol variant:
596    ///
597    /// - AES-192/256 Blumenthal variants: draft-blumenthal-aes-usm-04 extension
598    /// - AES-192/256 Reeder variants: Cisco/Reeder extension
599    /// - 3DES with SHA-1 or MD5: Reeder extension (draft-reeder-snmpv3-usm-3desede-00)
600    ///
601    /// Both the master key's authentication protocol and `priv_protocol` must
602    /// be supported by its selected backend.
603    ///
604    /// # Example
605    ///
606    /// ```rust
607    /// # #[cfg(any(feature = "crypto-rustcrypto", feature = "crypto-fips"))]
608    /// # {
609    /// use async_snmp::{AuthProtocol, MasterKey, PrivProtocol, v3::PrivKey};
610    ///
611    /// let master = MasterKey::from_password(AuthProtocol::Sha1, b"password").unwrap();
612    /// let engine_id = [0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04];
613    ///
614    /// // SHA-1 only produces 20 bytes, but AES-256 needs 32.
615    /// // The configured Blumenthal extension is applied.
616    /// let priv_key = PrivKey::from_master_key(&master, PrivProtocol::Aes256Blumenthal, &engine_id).unwrap();
617    /// # }
618    /// ```
619    pub fn from_master_key(
620        master: &super::MasterKey,
621        priv_protocol: PrivProtocol,
622        engine_id: &[u8],
623    ) -> super::crypto::CryptoResult<Self> {
624        use super::{
625            KeyExtension,
626            auth::{extend_key_reeder_with_backend, extend_key_with_backend},
627        };
628
629        let auth_protocol = master.protocol();
630        master
631            .crypto_backend()
632            .validate_auth_protocol(auth_protocol)?;
633        master
634            .crypto_backend()
635            .validate_priv_protocol(priv_protocol)?;
636        let key_extension = priv_protocol.key_extension_for(auth_protocol);
637
638        // Localize the master key (per RFC 3826 Section 1.2)
639        let localized = master.localize(engine_id)?;
640        let key_bytes = localized.as_bytes();
641
642        let key = match key_extension {
643            KeyExtension::None => key_bytes.to_vec(),
644            KeyExtension::Blumenthal => extend_key_with_backend(
645                master.crypto_backend(),
646                auth_protocol,
647                key_bytes,
648                priv_protocol.key_len(),
649            )?,
650            KeyExtension::Reeder => extend_key_reeder_with_backend(
651                master.crypto_backend(),
652                auth_protocol,
653                key_bytes,
654                engine_id,
655                priv_protocol.key_len(),
656            )?,
657        };
658
659        Ok(Self {
660            key,
661            protocol: priv_protocol,
662            backend: master.crypto_backend(),
663        })
664    }
665
666    /// Create a privacy key from raw localized key bytes.
667    ///
668    /// `key` must be at least `protocol.key_len()` octets; shorter keys would
669    /// later panic when the encryption/decryption routines slice into them, so
670    /// this returns `Err(CryptoError::InvalidKeyLength)` instead. A key longer
671    /// than `protocol.key_len()` is accepted (per RFC 3826 Section 3.1.2, the
672    /// localized key length is "at least" the required size); the extra
673    /// trailing bytes remain unused. Length validation precedes the
674    /// selected backend's privacy-protocol capability check.
675    ///
676    /// This constructor treats `key` as finalized and never extends it. The
677    /// AES dialect remains part of the protocol identity, but both dialects
678    /// produce identical encryption for the same raw key bytes.
679    pub fn from_bytes(
680        protocol: PrivProtocol,
681        key: impl Into<Vec<u8>>,
682    ) -> super::crypto::CryptoResult<Self> {
683        let key = key.into();
684        if key.len() < protocol.key_len() {
685            return Err(CryptoError::InvalidKeyLength);
686        }
687        Self::from_bytes_with_backend(protocol, key, CryptoBackend::require_default()?)
688    }
689
690    /// Create a privacy key for an explicitly selected backend.
691    pub fn from_bytes_with_backend(
692        protocol: PrivProtocol,
693        key: impl Into<Vec<u8>>,
694        backend: CryptoBackend,
695    ) -> super::crypto::CryptoResult<Self> {
696        let key = key.into();
697        if key.len() < protocol.key_len() {
698            return Err(CryptoError::InvalidKeyLength);
699        }
700        backend.validate_priv_protocol(protocol)?;
701        Ok(Self {
702            key,
703            protocol,
704            backend,
705        })
706    }
707
708    /// Returns the privacy protocol.
709    pub fn protocol(&self) -> PrivProtocol {
710        self.protocol
711    }
712
713    /// Return the backend used by this key.
714    pub fn crypto_backend(&self) -> CryptoBackend {
715        self.backend
716    }
717
718    /// Returns the encryption key portion.
719    pub fn encryption_key(&self) -> &[u8] {
720        match self.protocol {
721            PrivProtocol::Des => &self.key[..8],
722            PrivProtocol::Des3 => &self.key[..24],
723            PrivProtocol::Aes128 => &self.key[..16],
724            PrivProtocol::Aes192Blumenthal | PrivProtocol::Aes192Reeder => &self.key[..24],
725            PrivProtocol::Aes256Blumenthal | PrivProtocol::Aes256Reeder => &self.key[..32],
726        }
727    }
728
729    /// Encrypt with already selected protocol-specific sender inputs.
730    pub(crate) fn encrypt_with_context(
731        &self,
732        plaintext: &[u8],
733        context: PrivacyEncryptContext<'_>,
734    ) -> PrivacyResult<(Bytes, Bytes)> {
735        match (self.protocol, context) {
736            (PrivProtocol::Des, PrivacyEncryptContext::Des(reservation)) => self.encrypt_des_cbc(
737                plaintext,
738                reservation.engine_boots,
739                u64::from(reservation.salt),
740            ),
741            (PrivProtocol::Des3, PrivacyEncryptContext::Des(reservation)) => self.encrypt_des3_cbc(
742                plaintext,
743                reservation.engine_boots,
744                u64::from(reservation.salt),
745            ),
746            (
747                PrivProtocol::Aes128,
748                PrivacyEncryptContext::Aes {
749                    engine_boots,
750                    engine_time,
751                    salt_counter,
752                },
753            ) => self.encrypt_aes_cfb(
754                plaintext,
755                engine_boots,
756                engine_time,
757                salt_counter.next(),
758                16,
759            ),
760            (
761                PrivProtocol::Aes192Blumenthal | PrivProtocol::Aes192Reeder,
762                PrivacyEncryptContext::Aes {
763                    engine_boots,
764                    engine_time,
765                    salt_counter,
766                },
767            ) => self.encrypt_aes_cfb(
768                plaintext,
769                engine_boots,
770                engine_time,
771                salt_counter.next(),
772                24,
773            ),
774            (
775                PrivProtocol::Aes256Blumenthal | PrivProtocol::Aes256Reeder,
776                PrivacyEncryptContext::Aes {
777                    engine_boots,
778                    engine_time,
779                    salt_counter,
780                },
781            ) => self.encrypt_aes_cfb(
782                plaintext,
783                engine_boots,
784                engine_time,
785                salt_counter.next(),
786                32,
787            ),
788            _ => Err(PrivacyError::SenderStateMismatch),
789        }
790    }
791
792    /// Encrypt with DES or 3DES using caller-owned durable generating state.
793    ///
794    /// The local boots epoch and nonwrapping counter come only from `state`;
795    /// remote authoritative boots/time are deliberately not accepted. Pass
796    /// clones of one state to every sender using the same effective localized
797    /// key/pre-IV domain. A salt is burned before encryption is attempted.
798    ///
799    /// # Errors
800    ///
801    /// Returns [`PrivacyError::DesSaltExhausted`] at counter exhaustion,
802    /// [`PrivacyError::SenderStateMismatch`] for an AES key, or a crypto error.
803    pub fn encrypt_des_family(
804        &self,
805        plaintext: &[u8],
806        state: &DesSaltState,
807    ) -> PrivacyResult<(Bytes, Bytes)> {
808        let reservation = state.reserve()?;
809        self.encrypt_with_context(plaintext, PrivacyEncryptContext::Des(reservation))
810    }
811
812    /// Encrypt with an AES privacy variant using authoritative boots/time.
813    ///
814    /// # Errors
815    ///
816    /// Returns [`PrivacyError::SenderStateMismatch`] for a DES-family key or a
817    /// provider error when encryption fails.
818    pub fn encrypt_aes(
819        &self,
820        plaintext: &[u8],
821        engine_boots: u32,
822        engine_time: u32,
823        salt_counter: &SaltCounter,
824    ) -> PrivacyResult<(Bytes, Bytes)> {
825        self.encrypt_with_context(
826            plaintext,
827            PrivacyEncryptContext::Aes {
828                engine_boots,
829                engine_time,
830                salt_counter,
831            },
832        )
833    }
834
835    /// Decrypt data using the privParameters from the message.
836    ///
837    /// # Arguments
838    /// * `ciphertext` - The encrypted data
839    /// * `engine_boots` - The authoritative engine's boot count (from message)
840    /// * `engine_time` - The authoritative engine's time (from message)
841    /// * `priv_params` - The privParameters field from the message
842    ///
843    /// # Returns
844    /// * `Ok(plaintext)` on success
845    /// * `Err` on decryption failure
846    pub fn decrypt(
847        &self,
848        ciphertext: &[u8],
849        engine_boots: u32,
850        engine_time: u32,
851        priv_params: &[u8],
852    ) -> PrivacyResult<Bytes> {
853        if priv_params.len() != 8 {
854            tracing::debug!(target: "async_snmp::crypto", { expected = 8, actual = priv_params.len() }, "invalid privParameters length");
855            return Err(PrivacyError::InvalidPrivParamsLength {
856                expected: 8,
857                actual: priv_params.len(),
858            });
859        }
860
861        match self.protocol {
862            PrivProtocol::Des => self.decrypt_des(ciphertext, priv_params),
863            PrivProtocol::Des3 => self.decrypt_des3(ciphertext, priv_params),
864            PrivProtocol::Aes128
865            | PrivProtocol::Aes192Blumenthal
866            | PrivProtocol::Aes192Reeder
867            | PrivProtocol::Aes256Blumenthal
868            | PrivProtocol::Aes256Reeder => {
869                self.decrypt_aes(ciphertext, engine_boots, engine_time, priv_params)
870            }
871        }
872    }
873
874    /// DES-CBC encryption (RFC 3414 Section 8.1.1).
875    fn encrypt_des_cbc(
876        &self,
877        plaintext: &[u8],
878        engine_boots: u32,
879        salt_int: u64,
880    ) -> PrivacyResult<(Bytes, Bytes)> {
881        // DES key is first 8 bytes
882        let key = &self.key[..8];
883        // Pre-IV is last 8 bytes of 16-byte privKey
884        let pre_iv = &self.key[8..16];
885
886        // Salt = engineBoots (4 bytes MSB) || counter (4 bytes MSB)
887        // We use the lower 32 bits of salt_int as the counter
888        let mut salt = [0u8; 8];
889        salt[..4].copy_from_slice(&engine_boots.to_be_bytes());
890        salt[4..].copy_from_slice(&(salt_int as u32).to_be_bytes());
891
892        // IV = pre-IV XOR salt
893        let mut iv = [0u8; 8];
894        for i in 0..8 {
895            iv[i] = pre_iv[i] ^ salt[i];
896        }
897
898        let mut buffer = plaintext.to_vec();
899        self.backend
900            .encrypt(PrivProtocol::Des, key, &iv, &mut buffer)?;
901
902        Ok((Bytes::from(buffer), Bytes::copy_from_slice(&salt)))
903    }
904
905    /// DES-CBC decryption (RFC 3414 Section 8.1.1).
906    fn decrypt_des(&self, ciphertext: &[u8], priv_params: &[u8]) -> PrivacyResult<Bytes> {
907        if !ciphertext.len().is_multiple_of(8) {
908            tracing::debug!(target: "async_snmp::crypto", { length = ciphertext.len(), block_size = 8 }, "DES decryption failed: invalid ciphertext length");
909            return Err(PrivacyError::InvalidCiphertextLength {
910                length: ciphertext.len(),
911                block_size: 8,
912            });
913        }
914
915        // DES key is first 8 bytes
916        let key = &self.key[..8];
917        // Pre-IV is last 8 bytes of 16-byte privKey
918        let pre_iv = &self.key[8..16];
919
920        // Salt is the privParameters
921        let salt = priv_params;
922
923        // IV = pre-IV XOR salt
924        let mut iv = [0u8; 8];
925        for i in 0..8 {
926            iv[i] = pre_iv[i] ^ salt[i];
927        }
928
929        let mut buffer = ciphertext.to_vec();
930        self.backend
931            .decrypt(PrivProtocol::Des, key, &iv, &mut buffer)?;
932
933        Ok(Bytes::from(buffer))
934    }
935
936    /// 3DES-EDE CBC encryption (draft-reeder-snmpv3-usm-3desede-00 Section 5.1.1.2).
937    fn encrypt_des3_cbc(
938        &self,
939        plaintext: &[u8],
940        engine_boots: u32,
941        salt_int: u64,
942    ) -> PrivacyResult<(Bytes, Bytes)> {
943        // 3DES key is first 24 bytes (K1, K2, K3)
944        let key = &self.key[..24];
945        // Pre-IV is bytes 24-31 of the 32-byte privKey
946        let pre_iv = &self.key[24..32];
947
948        // Salt = engineBoots (4 bytes MSB) || counter (4 bytes MSB)
949        let mut salt = [0u8; 8];
950        salt[..4].copy_from_slice(&engine_boots.to_be_bytes());
951        salt[4..].copy_from_slice(&(salt_int as u32).to_be_bytes());
952
953        // IV = pre-IV XOR salt
954        let mut iv = [0u8; 8];
955        for i in 0..8 {
956            iv[i] = pre_iv[i] ^ salt[i];
957        }
958
959        let mut buffer = plaintext.to_vec();
960        self.backend
961            .encrypt(PrivProtocol::Des3, key, &iv, &mut buffer)?;
962
963        Ok((Bytes::from(buffer), Bytes::copy_from_slice(&salt)))
964    }
965
966    /// 3DES-EDE CBC decryption (draft-reeder-snmpv3-usm-3desede-00 Section 5.1.1.3).
967    fn decrypt_des3(&self, ciphertext: &[u8], priv_params: &[u8]) -> PrivacyResult<Bytes> {
968        if !ciphertext.len().is_multiple_of(8) {
969            tracing::debug!(target: "async_snmp::crypto", { length = ciphertext.len(), block_size = 8 }, "3DES decryption failed: invalid ciphertext length");
970            return Err(PrivacyError::InvalidCiphertextLength {
971                length: ciphertext.len(),
972                block_size: 8,
973            });
974        }
975
976        // 3DES key is first 24 bytes (K1, K2, K3)
977        let key = &self.key[..24];
978        // Pre-IV is bytes 24-31 of the 32-byte privKey
979        let pre_iv = &self.key[24..32];
980
981        // Salt is the privParameters
982        let salt = priv_params;
983
984        // IV = pre-IV XOR salt
985        let mut iv = [0u8; 8];
986        for i in 0..8 {
987            iv[i] = pre_iv[i] ^ salt[i];
988        }
989
990        let mut buffer = ciphertext.to_vec();
991        self.backend
992            .decrypt(PrivProtocol::Des3, key, &iv, &mut buffer)?;
993
994        Ok(Bytes::from(buffer))
995    }
996
997    /// AES-CFB encryption (RFC 3826 Section 3.1).
998    fn encrypt_aes_cfb(
999        &self,
1000        plaintext: &[u8],
1001        engine_boots: u32,
1002        engine_time: u32,
1003        salt: u64,
1004        key_len: usize,
1005    ) -> PrivacyResult<(Bytes, Bytes)> {
1006        // AES key is first key_len bytes
1007        let key = &self.key[..key_len];
1008
1009        // Salt as 8 bytes (big-endian)
1010        let salt_bytes = salt.to_be_bytes();
1011
1012        // IV = engineBoots (4) || engineTime (4) || salt (8) = 16 bytes
1013        // This is CONCATENATION, not XOR (unlike DES)
1014        let mut iv = [0u8; 16];
1015        iv[..4].copy_from_slice(&engine_boots.to_be_bytes());
1016        iv[4..8].copy_from_slice(&engine_time.to_be_bytes());
1017        iv[8..].copy_from_slice(&salt_bytes);
1018
1019        let mut buffer = plaintext.to_vec();
1020        self.backend.encrypt(self.protocol, key, &iv, &mut buffer)?;
1021
1022        Ok((Bytes::from(buffer), Bytes::copy_from_slice(&salt_bytes)))
1023    }
1024
1025    /// AES-CFB decryption (RFC 3826 Section 3.1.4).
1026    fn decrypt_aes(
1027        &self,
1028        ciphertext: &[u8],
1029        engine_boots: u32,
1030        engine_time: u32,
1031        priv_params: &[u8],
1032    ) -> PrivacyResult<Bytes> {
1033        let key_len = match self.protocol {
1034            PrivProtocol::Aes128 => 16,
1035            PrivProtocol::Aes192Blumenthal | PrivProtocol::Aes192Reeder => 24,
1036            PrivProtocol::Aes256Blumenthal | PrivProtocol::Aes256Reeder => 32,
1037            _ => unreachable!(),
1038        };
1039
1040        // AES key is first key_len bytes
1041        let key = &self.key[..key_len];
1042
1043        // IV = engineBoots (4) || engineTime (4) || salt (8) = 16 bytes
1044        let mut iv = [0u8; 16];
1045        iv[..4].copy_from_slice(&engine_boots.to_be_bytes());
1046        iv[4..8].copy_from_slice(&engine_time.to_be_bytes());
1047        iv[8..].copy_from_slice(priv_params);
1048
1049        let mut buffer = ciphertext.to_vec();
1050        self.backend.decrypt(self.protocol, key, &iv, &mut buffer)?;
1051
1052        Ok(Bytes::from(buffer))
1053    }
1054}
1055
1056impl std::fmt::Debug for PrivKey {
1057    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1058        f.debug_struct("PrivKey")
1059            .field("protocol", &self.protocol)
1060            .field("key", &"[REDACTED]")
1061            .finish()
1062    }
1063}
1064
1065#[cfg(all(test, not(any(feature = "crypto-rustcrypto", feature = "crypto-fips"))))]
1066mod no_backend_key_tests {
1067    use super::*;
1068
1069    #[test]
1070    fn active_privacy_key_constructors_reject_unavailable_backend() {
1071        for protocol in [
1072            PrivProtocol::Des,
1073            PrivProtocol::Des3,
1074            PrivProtocol::Aes128,
1075            PrivProtocol::Aes192Blumenthal,
1076            PrivProtocol::Aes192Reeder,
1077            PrivProtocol::Aes256Blumenthal,
1078            PrivProtocol::Aes256Reeder,
1079        ] {
1080            let len = protocol.key_len();
1081            assert_eq!(
1082                PrivKey::from_bytes(protocol, vec![0_u8; len - 1]).unwrap_err(),
1083                CryptoError::InvalidKeyLength
1084            );
1085            assert_eq!(
1086                PrivKey::from_bytes(protocol, vec![0_u8; len]).unwrap_err(),
1087                CryptoError::BackendUnavailable
1088            );
1089        }
1090        assert_eq!(
1091            PrivKey::from_password(
1092                AuthProtocol::Sha256,
1093                PrivProtocol::Aes128,
1094                b"short",
1095                b"engine-id",
1096            )
1097            .unwrap_err(),
1098            CryptoError::PasswordTooShort
1099        );
1100        assert_eq!(
1101            PrivKey::from_password(
1102                AuthProtocol::Sha256,
1103                PrivProtocol::Aes128,
1104                b"long-enough",
1105                b"engine-id",
1106            )
1107            .unwrap_err(),
1108            CryptoError::BackendUnavailable
1109        );
1110    }
1111}
1112
1113#[cfg(test)]
1114mod entropy_tests {
1115    use super::*;
1116
1117    #[test]
1118    fn salt_counter_propagates_random_source_failure() {
1119        let error = random_nonzero_u64_with(|_| Err(getrandom::Error::UNEXPECTED)).unwrap_err();
1120        assert_eq!(error.kind(), crate::ErrorKind::RandomSource);
1121    }
1122
1123    #[test]
1124    fn salt_counter_retries_a_zero_seed() {
1125        let mut attempts = 0;
1126        let value = random_nonzero_u64_with(|bytes| {
1127            attempts += 1;
1128            if attempts == 2 {
1129                bytes.copy_from_slice(&1_u64.to_ne_bytes());
1130            }
1131            Ok(())
1132        })
1133        .unwrap();
1134        assert_eq!(value, 1);
1135        assert_eq!(attempts, 2);
1136    }
1137}
1138
1139#[cfg(test)]
1140mod des_state_tests {
1141    use super::*;
1142    use std::sync::{Arc, Mutex};
1143
1144    #[derive(Debug)]
1145    struct PersistFailure(&'static str);
1146
1147    impl std::fmt::Display for PersistFailure {
1148        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1149            f.write_str(self.0)
1150        }
1151    }
1152
1153    impl std::error::Error for PersistFailure {}
1154
1155    #[test]
1156    fn install_and_restart_persist_before_use() {
1157        let writes = Arc::new(Mutex::new(Vec::new()));
1158        let install_writes = Arc::clone(&writes);
1159        let installed = DesSaltState::install(move |state| {
1160            install_writes.lock().unwrap().push(state.engine_boots());
1161            Ok::<(), std::convert::Infallible>(())
1162        })
1163        .unwrap();
1164        assert_eq!(installed.engine_boots(), 1);
1165
1166        let restart_writes = Arc::clone(&writes);
1167        let restarted = DesSaltState::restart(installed.persisted_state(), move |state| {
1168            restart_writes.lock().unwrap().push(state.engine_boots());
1169            Ok::<(), std::convert::Infallible>(())
1170        })
1171        .unwrap();
1172        assert_eq!(restarted.engine_boots(), 2);
1173        assert_eq!(*writes.lock().unwrap(), [1, 2]);
1174    }
1175
1176    #[test]
1177    fn persistence_failure_and_epoch_saturation_are_typed() {
1178        let error = DesSaltState::install(|_| Err(PersistFailure("disk unavailable"))).unwrap_err();
1179        let DesSaltStateError::Persistence(error) = error else {
1180            panic!("expected persistence error");
1181        };
1182        assert_eq!(error.operation(), DesSaltPersistenceOperation::Install);
1183        assert_eq!(error.attempted_engine_boots(), 1);
1184        assert_eq!(
1185            error.downcast_source_ref::<PersistFailure>().unwrap().0,
1186            "disk unavailable"
1187        );
1188
1189        assert!(matches!(
1190            DesSaltState::restart(
1191                PersistedDesSaltState::new(super::super::MAX_ENGINE_TIME).unwrap(),
1192                |_| Ok::<(), std::convert::Infallible>(())
1193            ),
1194            Err(DesSaltStateError::EpochSaturated { .. })
1195        ));
1196    }
1197
1198    #[test]
1199    fn atomic_restart_contract_rejects_a_second_live_owner() {
1200        let durable = Arc::new(Mutex::new(1_u32));
1201        let previous = PersistedDesSaltState::new(1).unwrap();
1202        let lease = |durable: Arc<Mutex<u32>>| {
1203            move |attempted: &PersistedDesSaltState| {
1204                let mut current = durable.lock().unwrap();
1205                if *current != 1 {
1206                    return Err(PersistFailure("stale compare-and-set"));
1207                }
1208                *current = attempted.engine_boots();
1209                Ok(())
1210            }
1211        };
1212
1213        assert!(DesSaltState::restart(previous, lease(Arc::clone(&durable))).is_ok());
1214        let error = DesSaltState::restart(previous, lease(durable)).unwrap_err();
1215        assert!(matches!(error, DesSaltStateError::Persistence(_)));
1216    }
1217
1218    #[test]
1219    fn atomic_install_contract_rejects_a_second_live_owner() {
1220        let durable = Arc::new(Mutex::new(None::<u32>));
1221        let lease = |durable: Arc<Mutex<Option<u32>>>| {
1222            move |attempted: &PersistedDesSaltState| {
1223                let mut current = durable.lock().unwrap();
1224                if current.is_some() {
1225                    return Err(PersistFailure("domain already installed"));
1226                }
1227                *current = Some(attempted.engine_boots());
1228                Ok(())
1229            }
1230        };
1231
1232        assert!(DesSaltState::install(lease(Arc::clone(&durable))).is_ok());
1233        let error = DesSaltState::install(lease(durable)).unwrap_err();
1234        assert!(matches!(error, DesSaltStateError::Persistence(_)));
1235    }
1236
1237    #[test]
1238    fn clones_share_one_nonwrapping_allocator() {
1239        let state = DesSaltState::install(|_| Ok::<(), std::convert::Infallible>(())).unwrap();
1240        let clone = state.clone();
1241        assert_eq!(state.reserve().unwrap().salt(), 1);
1242        assert_eq!(clone.reserve().unwrap().salt(), 2);
1243
1244        let exhausted = DesSaltState::with_last_salt_for_test(9, u32::MAX);
1245        assert!(matches!(
1246            exhausted.reserve(),
1247            Err(PrivacyError::DesSaltExhausted { engine_boots: 9 })
1248        ));
1249    }
1250}
1251
1252#[cfg(all(test, any(feature = "crypto-rustcrypto", feature = "crypto-fips")))]
1253mod tests {
1254    use super::*;
1255    use crate::format::hex::decode as decode_hex;
1256
1257    #[cfg(feature = "crypto-rustcrypto")]
1258    fn des_state(engine_boots: u32) -> DesSaltState {
1259        if engine_boots == 1 {
1260            DesSaltState::install(|_| Ok::<(), std::convert::Infallible>(())).unwrap()
1261        } else {
1262            DesSaltState::restart(
1263                PersistedDesSaltState::new(engine_boots - 1).unwrap(),
1264                |_| Ok::<(), std::convert::Infallible>(()),
1265            )
1266            .unwrap()
1267        }
1268    }
1269
1270    #[cfg(feature = "crypto-rustcrypto")]
1271    #[test]
1272    fn test_des_encrypt_decrypt_roundtrip() {
1273        // Create a 16-byte key (8 for DES, 8 for pre-IV)
1274        let key = vec![
1275            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // DES key
1276            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // pre-IV
1277        ];
1278        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, key).unwrap();
1279
1280        let plaintext = b"Hello, SNMPv3 World!";
1281        let engine_boots = 100u32;
1282        let engine_time = 12345u32;
1283
1284        let (ciphertext, priv_params) = priv_key
1285            .encrypt_des_family(plaintext, &des_state(engine_boots))
1286            .expect("encryption failed");
1287
1288        // Verify ciphertext is different from plaintext
1289        assert_ne!(ciphertext.as_ref(), plaintext);
1290        // Verify priv_params is 8 bytes
1291        assert_eq!(priv_params.len(), 8);
1292
1293        // Decrypt
1294        let decrypted = priv_key
1295            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1296            .expect("decryption failed");
1297
1298        // DES pads to 8-byte boundary, so decrypted may be longer
1299        assert!(decrypted.len() >= plaintext.len());
1300        assert_eq!(&decrypted[..plaintext.len()], plaintext);
1301    }
1302
1303    #[cfg(feature = "crypto-rustcrypto")]
1304    #[test]
1305    fn test_des3_encrypt_decrypt_roundtrip() {
1306        // Create a 32-byte key (24 for 3DES, 8 for pre-IV)
1307        let key = vec![
1308            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // K1
1309            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // K2
1310            0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, // K3
1311            0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, // pre-IV
1312        ];
1313        let priv_key = PrivKey::from_bytes(PrivProtocol::Des3, key).unwrap();
1314
1315        let plaintext = b"Hello, SNMPv3 World with 3DES!";
1316        let engine_boots = 100u32;
1317        let engine_time = 12345u32;
1318
1319        let (ciphertext, priv_params) = priv_key
1320            .encrypt_des_family(plaintext, &des_state(engine_boots))
1321            .expect("encryption failed");
1322
1323        // Verify ciphertext is different from plaintext
1324        assert_ne!(ciphertext.as_ref(), plaintext);
1325        // Verify priv_params is 8 bytes
1326        assert_eq!(priv_params.len(), 8);
1327
1328        // Decrypt
1329        let decrypted = priv_key
1330            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1331            .expect("decryption failed");
1332
1333        // 3DES pads to 8-byte boundary, so decrypted may be longer
1334        assert!(decrypted.len() >= plaintext.len());
1335        assert_eq!(&decrypted[..plaintext.len()], plaintext);
1336    }
1337
1338    #[test]
1339    fn test_aes128_encrypt_decrypt_roundtrip() {
1340        // Create a 16-byte key for AES-128
1341        let key = vec![
1342            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1343            0x0f, 0x10,
1344        ];
1345        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
1346
1347        let plaintext = b"Hello, SNMPv3 AES World!";
1348        let engine_boots = 200u32;
1349        let engine_time = 54321u32;
1350
1351        let (ciphertext, priv_params) = priv_key
1352            .encrypt_aes(
1353                plaintext,
1354                engine_boots,
1355                engine_time,
1356                &SaltCounter::new().unwrap(),
1357            )
1358            .expect("encryption failed");
1359
1360        // Verify ciphertext is different from plaintext
1361        assert_ne!(ciphertext.as_ref(), plaintext);
1362        // Verify priv_params is 8 bytes (salt)
1363        assert_eq!(priv_params.len(), 8);
1364
1365        // Decrypt
1366        let decrypted = priv_key
1367            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1368            .expect("decryption failed");
1369
1370        // AES-CFB doesn't require padding, so lengths should match
1371        assert_eq!(decrypted.len(), plaintext.len());
1372        assert_eq!(decrypted.as_ref(), plaintext);
1373    }
1374
1375    #[cfg(feature = "crypto-rustcrypto")]
1376    #[test]
1377    fn test_des_invalid_ciphertext_length() {
1378        let key = vec![0u8; 16];
1379        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, key).unwrap();
1380
1381        // Ciphertext not multiple of 8
1382        let ciphertext = [0u8; 13];
1383        let priv_params = [0u8; 8];
1384
1385        let result = priv_key.decrypt(&ciphertext, 0, 0, &priv_params);
1386        assert!(result.is_err());
1387    }
1388
1389    #[test]
1390    fn test_invalid_priv_params_length() {
1391        let key = vec![0u8; 16];
1392        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
1393
1394        // priv_params should be 8 bytes
1395        let ciphertext = [0u8; 16];
1396        let priv_params = [0u8; 4]; // Wrong length
1397
1398        let result = priv_key.decrypt(&ciphertext, 0, 0, &priv_params);
1399        assert!(result.is_err());
1400    }
1401
1402    #[test]
1403    fn test_from_bytes_rejects_undersized_key() {
1404        // Des requires 16 octets (8 key + 8 pre-IV); 4 is far too short.
1405        // Previously this succeeded and a later encrypt() would panic on the slice.
1406        let result = PrivKey::from_bytes(PrivProtocol::Des, vec![0u8; 4]);
1407        assert!(matches!(result, Err(CryptoError::InvalidKeyLength)));
1408    }
1409
1410    #[test]
1411    fn test_from_bytes_accepts_exact_length_key() {
1412        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, vec![0u8; 16]);
1413        if CryptoBackend::default_backend()
1414            .unwrap()
1415            .validate_priv_protocol(PrivProtocol::Des)
1416            .is_ok()
1417        {
1418            assert!(priv_key.is_ok());
1419        } else {
1420            assert_eq!(
1421                priv_key.unwrap_err(),
1422                CryptoError::UnsupportedAlgorithm("DES")
1423            );
1424        }
1425
1426        assert!(PrivKey::from_bytes(PrivProtocol::Aes128, vec![0u8; 16]).is_ok());
1427    }
1428
1429    #[test]
1430    fn test_from_bytes_accepts_oversized_key() {
1431        // RFC 3826 Section 3.1.2 specifies the localized key is ">= " the required
1432        // length; downstream slices only ever take a `[..N]` prefix, so extra
1433        // trailing bytes are unused but harmless.
1434        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, vec![0u8; 20]).unwrap();
1435        // Encrypting should not panic now that the key is validated as long enough.
1436        let _ = priv_key.encrypt_aes(b"data", 0, 0, &SaltCounter::new().unwrap());
1437    }
1438
1439    #[test]
1440    fn test_from_bytes_key_len_boundary() {
1441        let aes128_len = PrivProtocol::Aes128.key_len();
1442        assert!(PrivKey::from_bytes(PrivProtocol::Aes128, vec![0u8; aes128_len - 1]).is_err());
1443        assert!(PrivKey::from_bytes(PrivProtocol::Aes128, vec![0u8; aes128_len]).is_ok());
1444
1445        let aes256_len = PrivProtocol::Aes256Blumenthal.key_len();
1446        assert!(
1447            PrivKey::from_bytes(PrivProtocol::Aes256Blumenthal, vec![0u8; aes256_len - 1]).is_err()
1448        );
1449        assert!(PrivKey::from_bytes(PrivProtocol::Aes256Blumenthal, vec![0u8; aes256_len]).is_ok());
1450    }
1451
1452    #[test]
1453    fn raw_privacy_key_capabilities_for_each_protocol_and_backend() {
1454        let protocols = [
1455            PrivProtocol::Des,
1456            PrivProtocol::Des3,
1457            PrivProtocol::Aes128,
1458            PrivProtocol::Aes192Blumenthal,
1459            PrivProtocol::Aes192Reeder,
1460            PrivProtocol::Aes256Blumenthal,
1461            PrivProtocol::Aes256Reeder,
1462        ];
1463        let backends = [
1464            #[cfg(feature = "crypto-rustcrypto")]
1465            CryptoBackend::RustCrypto,
1466            #[cfg(feature = "crypto-fips")]
1467            CryptoBackend::AwsLcFips,
1468        ];
1469
1470        for backend in backends {
1471            for protocol in protocols {
1472                let len = protocol.key_len();
1473                assert_eq!(
1474                    PrivKey::from_bytes_with_backend(protocol, vec![0_u8; len - 1], backend)
1475                        .unwrap_err(),
1476                    CryptoError::InvalidKeyLength
1477                );
1478
1479                let result = PrivKey::from_bytes_with_backend(protocol, vec![0_u8; len], backend);
1480                let is_supported = match backend {
1481                    CryptoBackend::RustCrypto => true,
1482                    CryptoBackend::AwsLcFips => {
1483                        !matches!(protocol, PrivProtocol::Des | PrivProtocol::Des3)
1484                    }
1485                };
1486                if is_supported {
1487                    assert_eq!(result.unwrap().key.len(), protocol.key_len());
1488                } else {
1489                    let algorithm = match protocol {
1490                        PrivProtocol::Des => "DES",
1491                        PrivProtocol::Des3 => "3DES",
1492                        PrivProtocol::Aes128
1493                        | PrivProtocol::Aes192Blumenthal
1494                        | PrivProtocol::Aes192Reeder
1495                        | PrivProtocol::Aes256Blumenthal
1496                        | PrivProtocol::Aes256Reeder => {
1497                            unreachable!()
1498                        }
1499                    };
1500                    assert_eq!(
1501                        result.unwrap_err(),
1502                        CryptoError::UnsupportedAlgorithm(algorithm)
1503                    );
1504                }
1505            }
1506        }
1507    }
1508
1509    #[test]
1510    fn test_salt_counter() {
1511        let counter = SaltCounter::from_value(100);
1512        let s1 = counter.next();
1513        let s2 = counter.next();
1514        let s3 = counter.next();
1515
1516        // Each call should increment
1517        assert_eq!(s2, s1.wrapping_add(1));
1518        assert_eq!(s3, s2.wrapping_add(1));
1519    }
1520
1521    /// Test that `SaltCounter` never returns zero.
1522    ///
1523    /// Per net-snmp behavior (snmpusm.c:1319-1320), zero salt values should be
1524    /// skipped to avoid potential IV reuse issues on wraparound.
1525    #[test]
1526    fn test_salt_counter_skips_zero() {
1527        // Create a counter initialized to u64::MAX - 1 so the next call wraps through MAX.
1528        // next() returns post-increment, so:
1529        //   call 1: old=MAX-1, val=MAX, returns MAX
1530        //   call 2: old=MAX,   val=0 (wrapped), skips 0, returns 1
1531        //   call 3: old=1,     val=2, returns 2
1532        let counter = SaltCounter::from_value(u64::MAX - 1);
1533
1534        let s1 = counter.next();
1535        assert_eq!(s1, u64::MAX);
1536
1537        // This call wraps to zero; should skip and return 1
1538        let s2 = counter.next();
1539        assert_ne!(s2, 0, "SaltCounter should never return zero");
1540        assert_eq!(s2, 1, "SaltCounter should skip 0 and return 1");
1541
1542        // Subsequent calls should continue normally
1543        let s3 = counter.next();
1544        assert_eq!(s3, 2);
1545    }
1546
1547    /// Test that the wraparound path yields distinct, nonzero values.
1548    ///
1549    /// Regression: the wrap path previously returned a fixed `1`, which could
1550    /// duplicate the value produced by whichever thread read the counter as `0`.
1551    /// Driving several calls across the wrap boundary must produce no duplicate
1552    /// and no zero.
1553    #[test]
1554    fn test_salt_counter_wrap_no_duplicate() {
1555        use std::collections::HashSet;
1556
1557        let counter = SaltCounter::from_value(u64::MAX - 2);
1558        let mut seen = HashSet::new();
1559        // Sequence crosses u64::MAX and the wrap-to-zero boundary.
1560        for _ in 0..6 {
1561            let v = counter.next();
1562            assert_ne!(v, 0, "SaltCounter must never return zero");
1563            assert!(seen.insert(v), "SaltCounter emitted duplicate: {v}");
1564        }
1565    }
1566
1567    #[test]
1568    fn test_multiple_encryptions_use_shared_counter() {
1569        let key = vec![0u8; 16];
1570        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
1571        let counter = SaltCounter::from_value(100);
1572
1573        let (_, salt1) = priv_key.encrypt_aes(b"test data", 0, 0, &counter).unwrap();
1574        let (_, salt2) = priv_key.encrypt_aes(b"test data", 0, 0, &counter).unwrap();
1575
1576        assert_eq!(u64::from_be_bytes(salt1[..].try_into().unwrap()), 101);
1577        assert_eq!(u64::from_be_bytes(salt2[..].try_into().unwrap()), 102);
1578    }
1579
1580    #[test]
1581    fn test_from_password() {
1582        // Test that we can derive a privacy key from a password
1583        let password = b"maplesyrup";
1584        let engine_id = decode_hex("000000000000000000000002").unwrap();
1585
1586        let priv_key = PrivKey::from_password(
1587            AuthProtocol::Sha1,
1588            PrivProtocol::Aes128,
1589            password,
1590            &engine_id,
1591        )
1592        .unwrap();
1593
1594        // Just verify we can encrypt/decrypt with the derived key
1595        let plaintext = b"test message";
1596        let (ciphertext, priv_params) = priv_key
1597            .encrypt_aes(plaintext, 100, 200, &SaltCounter::new().unwrap())
1598            .unwrap();
1599        let decrypted = priv_key
1600            .decrypt(&ciphertext, 100, 200, &priv_params)
1601            .unwrap();
1602
1603        assert_eq!(decrypted.as_ref(), plaintext);
1604    }
1605
1606    #[test]
1607    fn test_aes192_encrypt_decrypt_roundtrip() {
1608        // Create a 24-byte key for AES-192
1609        let key = vec![
1610            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1611            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
1612        ];
1613        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes192Blumenthal, key).unwrap();
1614
1615        let plaintext = b"Hello, SNMPv3 AES-192 World!";
1616        let engine_boots = 300u32;
1617        let engine_time = 67890u32;
1618
1619        let (ciphertext, priv_params) = priv_key
1620            .encrypt_aes(
1621                plaintext,
1622                engine_boots,
1623                engine_time,
1624                &SaltCounter::new().unwrap(),
1625            )
1626            .expect("AES-192 encryption failed");
1627
1628        // Verify ciphertext is different from plaintext
1629        assert_ne!(ciphertext.as_ref(), plaintext);
1630        // Verify priv_params is 8 bytes (salt)
1631        assert_eq!(priv_params.len(), 8);
1632
1633        // Decrypt
1634        let decrypted = priv_key
1635            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1636            .expect("AES-192 decryption failed");
1637
1638        // AES-CFB doesn't require padding, so lengths should match
1639        assert_eq!(decrypted.len(), plaintext.len());
1640        assert_eq!(decrypted.as_ref(), plaintext);
1641    }
1642
1643    #[test]
1644    fn test_aes256_encrypt_decrypt_roundtrip() {
1645        // Create a 32-byte key for AES-256
1646        let key = vec![
1647            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1648            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
1649            0x1d, 0x1e, 0x1f, 0x20,
1650        ];
1651        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes256Blumenthal, key).unwrap();
1652
1653        let plaintext = b"Hello, SNMPv3 AES-256 World!";
1654        let engine_boots = 400u32;
1655        let engine_time = 11111u32;
1656
1657        let (ciphertext, priv_params) = priv_key
1658            .encrypt_aes(
1659                plaintext,
1660                engine_boots,
1661                engine_time,
1662                &SaltCounter::new().unwrap(),
1663            )
1664            .expect("AES-256 encryption failed");
1665
1666        // Verify ciphertext is different from plaintext
1667        assert_ne!(ciphertext.as_ref(), plaintext);
1668        // Verify priv_params is 8 bytes (salt)
1669        assert_eq!(priv_params.len(), 8);
1670
1671        // Decrypt
1672        let decrypted = priv_key
1673            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1674            .expect("AES-256 decryption failed");
1675
1676        // AES-CFB doesn't require padding, so lengths should match
1677        assert_eq!(decrypted.len(), plaintext.len());
1678        assert_eq!(decrypted.as_ref(), plaintext);
1679    }
1680
1681    #[test]
1682    fn test_aes192_from_password() {
1683        // For AES-192 (24-byte key), we need SHA-224 or higher auth protocol
1684        let password = b"longpassword123";
1685        let engine_id = decode_hex("80001f8880e9b104617361000000").unwrap();
1686
1687        let priv_key = PrivKey::from_password(
1688            AuthProtocol::Sha256, // SHA-256 produces 32 bytes, enough for AES-192
1689            PrivProtocol::Aes192Blumenthal,
1690            password,
1691            &engine_id,
1692        )
1693        .unwrap();
1694
1695        let plaintext = b"test message for AES-192";
1696        let (ciphertext, priv_params) = priv_key
1697            .encrypt_aes(plaintext, 100, 200, &SaltCounter::new().unwrap())
1698            .unwrap();
1699        let decrypted = priv_key
1700            .decrypt(&ciphertext, 100, 200, &priv_params)
1701            .unwrap();
1702
1703        assert_eq!(decrypted.as_ref(), plaintext);
1704    }
1705
1706    #[test]
1707    fn test_aes256_from_password() {
1708        // For AES-256 (32-byte key), we need SHA-256 or higher auth protocol
1709        let password = b"anotherlongpassword456";
1710        let engine_id = decode_hex("80001f8880e9b104617361000000").unwrap();
1711
1712        let priv_key = PrivKey::from_password(
1713            AuthProtocol::Sha256, // SHA-256 produces 32 bytes, exactly enough for AES-256
1714            PrivProtocol::Aes256Blumenthal,
1715            password,
1716            &engine_id,
1717        )
1718        .unwrap();
1719
1720        let plaintext = b"test message for AES-256";
1721        let (ciphertext, priv_params) = priv_key
1722            .encrypt_aes(plaintext, 100, 200, &SaltCounter::new().unwrap())
1723            .unwrap();
1724        let decrypted = priv_key
1725            .decrypt(&ciphertext, 100, 200, &priv_params)
1726            .unwrap();
1727
1728        assert_eq!(decrypted.as_ref(), plaintext);
1729    }
1730
1731    // ========================================================================
1732    // Wrong Key Decryption Tests
1733    //
1734    // These tests verify that decryption with the wrong key produces garbage,
1735    // not the original plaintext. Note: Stream ciphers like AES-CFB don't return
1736    // errors on wrong-key decryption - they produce garbage. The authentication
1737    // layer (HMAC) is what detects tampering/wrong keys in practice (RFC 3414).
1738    // ========================================================================
1739
1740    #[cfg(feature = "crypto-rustcrypto")]
1741    #[test]
1742    fn test_des_wrong_key_produces_garbage() {
1743        // Correct 16-byte key
1744        let correct_key = vec![
1745            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
1746            0x17, 0x18,
1747        ];
1748        // Wrong key (different from correct key)
1749        let wrong_key = vec![
1750            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xE7, 0xE6, 0xE5, 0xE4, 0xE3, 0xE2,
1751            0xE1, 0xE0,
1752        ];
1753
1754        let correct_priv_key = PrivKey::from_bytes(PrivProtocol::Des, correct_key).unwrap();
1755        let wrong_priv_key = PrivKey::from_bytes(PrivProtocol::Des, wrong_key).unwrap();
1756
1757        let plaintext = b"Secret SNMPv3 message data!";
1758        let engine_boots = 100u32;
1759        let engine_time = 12345u32;
1760
1761        // Encrypt with correct key
1762        let (ciphertext, priv_params) = correct_priv_key
1763            .encrypt_des_family(plaintext, &des_state(engine_boots))
1764            .expect("encryption failed");
1765
1766        // Decrypt with wrong key - this will "succeed" but produce garbage
1767        let wrong_decrypted = wrong_priv_key
1768            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1769            .expect("decryption should succeed cryptographically");
1770
1771        // Verify wrong key produces different output (not the original plaintext)
1772        assert_ne!(
1773            &wrong_decrypted[..plaintext.len()],
1774            plaintext,
1775            "wrong key should NOT produce the original plaintext"
1776        );
1777
1778        // Verify correct key still works
1779        let correct_decrypted = correct_priv_key
1780            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1781            .expect("correct key decryption failed");
1782        assert_eq!(
1783            &correct_decrypted[..plaintext.len()],
1784            plaintext,
1785            "correct key should produce the original plaintext"
1786        );
1787    }
1788
1789    #[test]
1790    fn test_aes128_wrong_key_produces_garbage() {
1791        let correct_key = vec![
1792            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1793            0x0f, 0x10,
1794        ];
1795        let wrong_key = vec![
1796            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2,
1797            0xF1, 0xF0,
1798        ];
1799
1800        let correct_priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, correct_key).unwrap();
1801        let wrong_priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, wrong_key).unwrap();
1802
1803        let plaintext = b"Secret AES-128 message data!";
1804        let engine_boots = 200u32;
1805        let engine_time = 54321u32;
1806
1807        // Encrypt with correct key
1808        let (ciphertext, priv_params) = correct_priv_key
1809            .encrypt_aes(
1810                plaintext,
1811                engine_boots,
1812                engine_time,
1813                &SaltCounter::new().unwrap(),
1814            )
1815            .expect("encryption failed");
1816
1817        // Decrypt with wrong key
1818        let wrong_decrypted = wrong_priv_key
1819            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1820            .expect("decryption should succeed cryptographically");
1821
1822        // Wrong key should produce garbage (not the original plaintext)
1823        assert_ne!(
1824            wrong_decrypted.as_ref(),
1825            plaintext,
1826            "wrong key should NOT produce the original plaintext"
1827        );
1828
1829        // Correct key should work
1830        let correct_decrypted = correct_priv_key
1831            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1832            .expect("correct key decryption failed");
1833        assert_eq!(correct_decrypted.as_ref(), plaintext);
1834    }
1835
1836    #[test]
1837    fn test_aes192_wrong_key_produces_garbage() {
1838        let correct_key = vec![
1839            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1840            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
1841        ];
1842        let wrong_key = vec![
1843            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2,
1844            0xF1, 0xF0, 0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8,
1845        ];
1846
1847        let correct_priv_key =
1848            PrivKey::from_bytes(PrivProtocol::Aes192Blumenthal, correct_key).unwrap();
1849        let wrong_priv_key =
1850            PrivKey::from_bytes(PrivProtocol::Aes192Blumenthal, wrong_key).unwrap();
1851
1852        let plaintext = b"Secret AES-192 message data!";
1853        let engine_boots = 300u32;
1854        let engine_time = 67890u32;
1855
1856        let (ciphertext, priv_params) = correct_priv_key
1857            .encrypt_aes(
1858                plaintext,
1859                engine_boots,
1860                engine_time,
1861                &SaltCounter::new().unwrap(),
1862            )
1863            .expect("encryption failed");
1864
1865        let wrong_decrypted = wrong_priv_key
1866            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1867            .expect("decryption should succeed cryptographically");
1868
1869        assert_ne!(
1870            wrong_decrypted.as_ref(),
1871            plaintext,
1872            "wrong key should NOT produce the original plaintext"
1873        );
1874    }
1875
1876    #[test]
1877    fn test_aes256_wrong_key_produces_garbage() {
1878        let correct_key = vec![
1879            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
1880            0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
1881            0x1d, 0x1e, 0x1f, 0x20,
1882        ];
1883        let wrong_key = vec![
1884            0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3, 0xF2,
1885            0xF1, 0xF0, 0xEF, 0xEE, 0xED, 0xEC, 0xEB, 0xEA, 0xE9, 0xE8, 0xE7, 0xE6, 0xE5, 0xE4,
1886            0xE3, 0xE2, 0xE1, 0xE0,
1887        ];
1888
1889        let correct_priv_key =
1890            PrivKey::from_bytes(PrivProtocol::Aes256Blumenthal, correct_key).unwrap();
1891        let wrong_priv_key =
1892            PrivKey::from_bytes(PrivProtocol::Aes256Blumenthal, wrong_key).unwrap();
1893
1894        let plaintext = b"Secret AES-256 message data!";
1895        let engine_boots = 400u32;
1896        let engine_time = 11111u32;
1897
1898        let (ciphertext, priv_params) = correct_priv_key
1899            .encrypt_aes(
1900                plaintext,
1901                engine_boots,
1902                engine_time,
1903                &SaltCounter::new().unwrap(),
1904            )
1905            .expect("encryption failed");
1906
1907        let wrong_decrypted = wrong_priv_key
1908            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
1909            .expect("decryption should succeed cryptographically");
1910
1911        assert_ne!(
1912            wrong_decrypted.as_ref(),
1913            plaintext,
1914            "wrong key should NOT produce the original plaintext"
1915        );
1916    }
1917
1918    #[cfg(feature = "crypto-rustcrypto")]
1919    #[test]
1920    fn test_des_wrong_priv_params_produces_garbage() {
1921        // Verify that even with the correct key, wrong priv_params (salt/IV)
1922        // produces garbage. This tests the IV derivation logic.
1923        let key = vec![
1924            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
1925            0x17, 0x18,
1926        ];
1927
1928        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, key).unwrap();
1929
1930        let plaintext = b"DES test message";
1931        let engine_boots = 100u32;
1932        let engine_time = 12345u32;
1933
1934        let (ciphertext, correct_priv_params) = priv_key
1935            .encrypt_des_family(plaintext, &des_state(engine_boots))
1936            .expect("encryption failed");
1937
1938        // Use wrong priv_params (different salt)
1939        let wrong_priv_params = [0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x88];
1940
1941        let wrong_decrypted = priv_key
1942            .decrypt(&ciphertext, engine_boots, engine_time, &wrong_priv_params)
1943            .expect("decryption should succeed cryptographically");
1944
1945        // Wrong IV should produce garbage
1946        assert_ne!(
1947            &wrong_decrypted[..plaintext.len()],
1948            plaintext,
1949            "wrong priv_params should NOT produce the original plaintext"
1950        );
1951
1952        // Correct priv_params should work
1953        let correct_decrypted = priv_key
1954            .decrypt(&ciphertext, engine_boots, engine_time, &correct_priv_params)
1955            .expect("correct decryption failed");
1956        assert_eq!(&correct_decrypted[..plaintext.len()], plaintext);
1957    }
1958
1959    /// Test the DES salt/IV composition against RFC 3414 Section 8.1.1.1.
1960    ///
1961    /// Asserts that the returned `privParameters` is exactly `engineBoots (4 bytes,
1962    /// big-endian) || counter (4 bytes, big-endian)` for a controlled `SaltCounter`
1963    /// value, and that the CBC IV used is `pre-IV XOR salt` by round-tripping through
1964    /// `decrypt` and by independently recomputing the expected IV.
1965    #[cfg(feature = "crypto-rustcrypto")]
1966    #[test]
1967    fn test_des_salt_and_iv_composition() {
1968        // Distinctive 16-byte key: bytes 0..8 = DES key, 8..16 = pre-IV.
1969        let key = vec![
1970            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // DES key
1971            0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, // pre-IV
1972        ];
1973        let pre_iv = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22];
1974        let priv_key = PrivKey::from_bytes(PrivProtocol::Des, key).unwrap();
1975
1976        let expected_salt_value: u32 = 0x1001;
1977
1978        let engine_boots: u32 = 0x1234_5678;
1979        let des_state = DesSaltState::with_last_salt_for_test(engine_boots, 0x1000);
1980        let engine_time: u32 = 999;
1981        let plaintext = b"RFC 3414 8.1.1.1 salt/IV composition test";
1982
1983        let (ciphertext, priv_params) = priv_key
1984            .encrypt_des_family(plaintext, &des_state)
1985            .expect("encryption failed");
1986
1987        // 1. Salt composition: privParameters = engineBoots || counter.
1988        assert_eq!(priv_params.len(), 8);
1989        assert_eq!(&priv_params[..4], &engine_boots.to_be_bytes());
1990        assert_eq!(&priv_params[4..8], &expected_salt_value.to_be_bytes());
1991
1992        // 2. IV = pre-IV XOR salt: independently recompute and confirm it is
1993        // non-trivial (the salt actually XORed in, not a passthrough).
1994        let mut expected_iv = [0u8; 8];
1995        for i in 0..8 {
1996            expected_iv[i] = pre_iv[i] ^ priv_params[i];
1997        }
1998        assert_ne!(
1999            expected_iv, pre_iv,
2000            "salt XOR must change the IV relative to the raw pre-IV"
2001        );
2002
2003        // 3. Round-trip proof: decrypt-side IV reconstruction (pre-IV XOR
2004        // priv_params) must match the encrypt-side IV, recovering the plaintext.
2005        let decrypted = priv_key
2006            .decrypt(&ciphertext, engine_boots, engine_time, &priv_params)
2007            .expect("decryption failed");
2008        assert_eq!(&decrypted[..plaintext.len()], plaintext);
2009    }
2010
2011    /// Test that `SaltCounter` never emits duplicate salts under concurrent access.
2012    ///
2013    /// This is a regression test for the two-fetch_add race where two threads
2014    /// could both return 1 after a wraparound left the counter at 0.
2015    #[test]
2016    fn test_salt_counter_no_duplicates_concurrent() {
2017        use std::collections::HashSet;
2018        use std::sync::{Arc, Mutex};
2019        use std::thread;
2020
2021        let counter = Arc::new(SaltCounter::new().unwrap());
2022        let results = Arc::new(Mutex::new(HashSet::new()));
2023        let iterations = 10_000usize;
2024        let threads = 8usize;
2025
2026        let handles: Vec<_> = (0..threads)
2027            .map(|_| {
2028                let counter = Arc::clone(&counter);
2029                let results = Arc::clone(&results);
2030                thread::spawn(move || {
2031                    for _ in 0..iterations {
2032                        let salt = counter.next();
2033                        assert_ne!(salt, 0, "SaltCounter must never return zero");
2034                        let mut set = results.lock().unwrap();
2035                        assert!(set.insert(salt), "SaltCounter emitted duplicate: {salt}");
2036                    }
2037                })
2038            })
2039            .collect();
2040
2041        for h in handles {
2042            h.join().expect("thread panicked");
2043        }
2044    }
2045
2046    #[cfg(feature = "crypto-rustcrypto")]
2047    #[test]
2048    fn test_des_family_shared_state_concurrency_and_exhaustion() {
2049        use std::collections::HashSet;
2050        use std::sync::{Arc, Mutex};
2051        use std::thread;
2052
2053        for (protocol, key_len) in [(PrivProtocol::Des, 16), (PrivProtocol::Des3, 32)] {
2054            let key = Arc::new(PrivKey::from_bytes(protocol, vec![0x5A; key_len]).unwrap());
2055            let state = Arc::new(DesSaltState::with_last_salt_for_test(7, 0));
2056            let portions = Arc::new(Mutex::new(HashSet::new()));
2057
2058            let handles: Vec<_> = (0..4)
2059                .map(|_| {
2060                    let key = Arc::clone(&key);
2061                    let state = Arc::clone(&state);
2062                    let portions = Arc::clone(&portions);
2063                    thread::spawn(move || {
2064                        let (_, params) = key.encrypt_des_family(b"shared", &state).unwrap();
2065                        assert_eq!(&params[..4], &7_u32.to_be_bytes());
2066                        let low = u32::from_be_bytes(params[4..].try_into().unwrap());
2067                        assert_ne!(low, 0);
2068                        assert!(portions.lock().unwrap().insert(low));
2069                    })
2070                })
2071                .collect();
2072
2073            for handle in handles {
2074                handle.join().unwrap();
2075            }
2076            assert_eq!(*portions.lock().unwrap(), HashSet::from([1, 2, 3, 4]));
2077
2078            let exhausted = DesSaltState::with_last_salt_for_test(7, u32::MAX);
2079            assert!(matches!(
2080                key.encrypt_des_family(b"shared", &exhausted),
2081                Err(PrivacyError::DesSaltExhausted { engine_boots: 7 })
2082            ));
2083        }
2084    }
2085
2086    #[test]
2087    fn test_cloned_priv_keys_require_same_explicit_counter() {
2088        let original = PrivKey::from_bytes(PrivProtocol::Aes128, vec![0u8; 16]).unwrap();
2089        let cloned = original.clone();
2090        let counter = SaltCounter::from_value(200);
2091
2092        let (_, salt_orig) = original.encrypt_aes(b"test", 0, 0, &counter).unwrap();
2093        let (_, salt_clone) = cloned.encrypt_aes(b"test", 0, 0, &counter).unwrap();
2094
2095        assert_eq!(u64::from_be_bytes(salt_orig[..].try_into().unwrap()), 201);
2096        assert_eq!(u64::from_be_bytes(salt_clone[..].try_into().unwrap()), 202);
2097    }
2098
2099    #[cfg(all(feature = "crypto-rustcrypto", feature = "crypto-fips"))]
2100    #[test]
2101    fn test_aes128_backends_match_with_deterministic_salt() {
2102        let key = vec![0x5A; PrivProtocol::Aes128.key_len()];
2103        let rust = PrivKey::from_bytes_with_backend(
2104            PrivProtocol::Aes128,
2105            key.clone(),
2106            CryptoBackend::RustCrypto,
2107        )
2108        .unwrap();
2109        let fips =
2110            PrivKey::from_bytes_with_backend(PrivProtocol::Aes128, key, CryptoBackend::AwsLcFips)
2111                .unwrap();
2112        let rust_counter = SaltCounter::from_value(41);
2113        let fips_counter = SaltCounter::from_value(41);
2114
2115        let rust_encrypted = rust
2116            .encrypt_aes(b"shared AES-128 provider KAT", 7, 11, &rust_counter)
2117            .unwrap();
2118        let fips_encrypted = fips
2119            .encrypt_aes(b"shared AES-128 provider KAT", 7, 11, &fips_counter)
2120            .unwrap();
2121
2122        assert_eq!(rust_encrypted, fips_encrypted);
2123    }
2124
2125    #[test]
2126    fn test_aes_wrong_engine_time_produces_garbage() {
2127        // For AES, the IV includes engine_boots and engine_time.
2128        // Wrong values should produce garbage.
2129        let key = vec![
2130            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
2131            0x0f, 0x10,
2132        ];
2133
2134        let priv_key = PrivKey::from_bytes(PrivProtocol::Aes128, key).unwrap();
2135
2136        let plaintext = b"AES test message";
2137        let engine_boots = 200u32;
2138        let engine_time = 54321u32;
2139
2140        let (ciphertext, priv_params) = priv_key
2141            .encrypt_aes(
2142                plaintext,
2143                engine_boots,
2144                engine_time,
2145                &SaltCounter::new().unwrap(),
2146            )
2147            .expect("encryption failed");
2148
2149        // Decrypt with wrong engine_time (IV mismatch)
2150        let wrong_decrypted = priv_key
2151            .decrypt(&ciphertext, engine_boots, engine_time + 1, &priv_params)
2152            .expect("decryption should succeed cryptographically");
2153
2154        assert_ne!(
2155            wrong_decrypted.as_ref(),
2156            plaintext,
2157            "wrong engine_time should NOT produce the original plaintext"
2158        );
2159
2160        // Decrypt with wrong engine_boots (IV mismatch)
2161        let wrong_decrypted2 = priv_key
2162            .decrypt(&ciphertext, engine_boots + 1, engine_time, &priv_params)
2163            .expect("decryption should succeed cryptographically");
2164
2165        assert_ne!(
2166            wrong_decrypted2.as_ref(),
2167            plaintext,
2168            "wrong engine_boots should NOT produce the original plaintext"
2169        );
2170    }
2171}