Skip to main content

async_snmp/v3/
auth.rs

1//! Authentication key derivation and HMAC operations (RFC 3414).
2//!
3//! This module implements:
4//! - Password-to-key derivation (1MB expansion + hash)
5//! - Key localization (binding key to engine ID)
6//! - HMAC authentication for message integrity
7//!
8//! # Two-Level Key Derivation
9//!
10//! `SNMPv3` key derivation is a two-step process:
11//!
12//! 1. **Password to Master Key** (~850μs for SHA-256): Expand password to 1MB
13//!    by repetition and hash it. This produces a protocol-specific master key.
14//!
15//! 2. **Localization** (~1μs): Bind the master key to a specific engine ID by
16//!    computing `H(master_key || engine_id || master_key)`.
17//!
18//! When polling many engines with the same credentials, cache the [`MasterKey`]
19//! and call [`MasterKey::localize`] for each engine ID. This avoids repeating
20//! the expensive 1MB expansion for every engine.
21//!
22//! ```rust
23//! use async_snmp::{AuthProtocol, MasterKey};
24//!
25//! // Expensive: ~850μs - do once per password
26//! let master = MasterKey::from_password(AuthProtocol::Sha256, b"authpassword").unwrap();
27//!
28//! // Cheap: ~1μs each - do per engine
29//! let key1 = master.localize(b"\x80\x00\x1f\x88\x80...").unwrap();
30//! let key2 = master.localize(b"\x80\x00\x1f\x88\x81...").unwrap();
31//! ```
32
33use zeroize::{Zeroize, ZeroizeOnDrop};
34
35use super::AuthProtocol;
36use super::crypto::{CryptoProvider, CryptoResult};
37
38/// Minimum password length required for password-based key derivation.
39///
40/// RFC 3414 Section 11.2 requires passwords of at least 8 octets, and net-snmp
41/// rejects shorter passwords with `USM_PASSWORDTOOSHORT`. Password-based key
42/// derivation entry points reject passwords shorter than this with
43/// [`CryptoError::PasswordTooShort`](super::CryptoError::PasswordTooShort).
44pub const MIN_PASSWORD_LENGTH: usize = 8;
45
46/// Master authentication key (Ku) before engine localization.
47///
48/// This is the intermediate result of the RFC 3414 password-to-key algorithm,
49/// computed by expanding the password to 1MB and hashing it. This step is
50/// computationally expensive (~850μs for SHA-256) but can be cached and reused
51/// across multiple engines that share the same credentials.
52///
53/// # Performance
54///
55/// | Operation | Time |
56/// |-----------|------|
57/// | `MasterKey::from_password` (SHA-256) | ~850 μs |
58/// | `MasterKey::localize` | ~1 μs |
59///
60/// For applications polling many engines with shared credentials, caching the
61/// `MasterKey` provides significant performance benefits.
62///
63/// # Security
64///
65/// Key material is automatically zeroed from memory when dropped, using the
66/// `zeroize` crate. This provides defense-in-depth against memory scraping.
67///
68/// # Example
69///
70/// ```rust
71/// use async_snmp::{AuthProtocol, MasterKey};
72///
73/// // Derive master key once (expensive)
74/// let master = MasterKey::from_password(AuthProtocol::Sha256, b"authpassword").unwrap();
75///
76/// // Localize to different engines (cheap)
77/// let engine1_id = b"\x80\x00\x1f\x88\x80\xe9\xb1\x04\x61\x73\x61\x00\x00\x00";
78/// let engine2_id = b"\x80\x00\x1f\x88\x80\xe9\xb1\x04\x61\x73\x61\x00\x00\x01";
79///
80/// let key1 = master.localize(engine1_id).unwrap();
81/// let key2 = master.localize(engine2_id).unwrap();
82/// ```
83#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct MasterKey {
85    key: Vec<u8>,
86    #[zeroize(skip)]
87    protocol: AuthProtocol,
88}
89
90impl MasterKey {
91    /// Derive a master key from a password.
92    ///
93    /// This implements RFC 3414 Section A.2.1: expand the password to 1MB by
94    /// repetition, then hash the result. This is computationally expensive
95    /// (~850μs for SHA-256) but only needs to be done once per password.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
100    /// backend does not support the requested authentication protocol.
101    ///
102    /// # Empty and Short Passwords
103    ///
104    /// Passwords shorter than [`MIN_PASSWORD_LENGTH`] (8 octets) are rejected
105    /// with [`CryptoError::PasswordTooShort`](super::CryptoError::PasswordTooShort),
106    /// matching RFC 3414 Section 11.2 and net-snmp's `USM_PASSWORDTOOSHORT`.
107    /// This does not affect pre-derived key constructors such as
108    /// [`from_bytes`](Self::from_bytes), which take key material rather than a
109    /// plaintext password.
110    pub fn from_password(protocol: AuthProtocol, password: &[u8]) -> CryptoResult<Self> {
111        if password.len() < MIN_PASSWORD_LENGTH {
112            return Err(super::CryptoError::PasswordTooShort);
113        }
114        let key = password_to_key(protocol, password)?;
115        Ok(Self { key, protocol })
116    }
117
118    /// Derive a master key from a string password.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
123    /// backend does not support the requested authentication protocol.
124    pub fn from_str_password(protocol: AuthProtocol, password: &str) -> CryptoResult<Self> {
125        Self::from_password(protocol, password.as_bytes())
126    }
127
128    /// Create a master key from raw bytes.
129    ///
130    /// Use this if you already have a master key (e.g., from configuration).
131    /// The bytes should be the raw digest output from the 1MB password expansion.
132    pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
133        Self {
134            key: key.into(),
135            protocol,
136        }
137    }
138
139    /// Localize this master key to a specific engine ID.
140    ///
141    /// This implements RFC 3414 Section A.2.2:
142    /// `localized_key = H(master_key || engine_id || master_key)`
143    ///
144    /// This operation is cheap (~1μs) compared to master key derivation.
145    ///
146    /// # Errors
147    ///
148    /// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
149    /// backend does not support the key's authentication protocol.
150    pub fn localize(&self, engine_id: &[u8]) -> CryptoResult<LocalizedKey> {
151        let localized = localize_key(self.protocol, &self.key, engine_id)?;
152        Ok(LocalizedKey {
153            key: localized,
154            protocol: self.protocol,
155        })
156    }
157
158    /// Get the protocol this key is for.
159    #[must_use]
160    pub fn protocol(&self) -> AuthProtocol {
161        self.protocol
162    }
163
164    /// Get the raw key bytes.
165    #[must_use]
166    pub fn as_bytes(&self) -> &[u8] {
167        &self.key
168    }
169}
170
171impl std::fmt::Debug for MasterKey {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.debug_struct("MasterKey")
174            .field("protocol", &self.protocol)
175            .field("key", &"[REDACTED]")
176            .finish()
177    }
178}
179
180impl AsRef<[u8]> for MasterKey {
181    fn as_ref(&self) -> &[u8] {
182        self.as_bytes()
183    }
184}
185
186/// Localized authentication key.
187///
188/// A key that has been derived from a password and bound to a specific engine ID.
189/// This key can be used for HMAC operations on messages to/from that engine.
190///
191/// # Security
192///
193/// Key material is automatically zeroed from memory when the key is dropped,
194/// using the `zeroize` crate. This provides defense-in-depth against memory
195/// scraping attacks.
196#[derive(Clone, Zeroize, ZeroizeOnDrop)]
197pub struct LocalizedKey {
198    key: Vec<u8>,
199    #[zeroize(skip)]
200    protocol: AuthProtocol,
201}
202
203impl LocalizedKey {
204    /// Derive a localized key from a password and engine ID.
205    ///
206    /// This implements the key localization algorithm from RFC 3414 Section A.2:
207    /// 1. Expand password to 1MB by repetition
208    /// 2. Hash the expansion to get the master key
209    /// 3. Hash (`master_key` || `engine_id` || `master_key`) to get the localized key
210    ///
211    /// # Performance Note
212    ///
213    /// This method performs the full key derivation (~850μs for SHA-256). When
214    /// polling many engines with shared credentials, use [`MasterKey`] to cache
215    /// the intermediate result and call [`MasterKey::localize`] for each engine.
216    ///
217    /// # Empty and Short Passwords
218    ///
219    /// Passwords shorter than [`MIN_PASSWORD_LENGTH`] (8 octets) are rejected
220    /// with [`CryptoError::PasswordTooShort`](super::CryptoError::PasswordTooShort),
221    /// matching RFC 3414 Section 11.2 and net-snmp's `USM_PASSWORDTOOSHORT`.
222    pub fn from_password(
223        protocol: AuthProtocol,
224        password: &[u8],
225        engine_id: &[u8],
226    ) -> CryptoResult<Self> {
227        MasterKey::from_password(protocol, password)?.localize(engine_id)
228    }
229
230    /// Derive a localized key from a string password and engine ID.
231    ///
232    /// This is a convenience method that converts the string to bytes and calls
233    /// [`from_password`](Self::from_password).
234    pub fn from_str_password(
235        protocol: AuthProtocol,
236        password: &str,
237        engine_id: &[u8],
238    ) -> CryptoResult<Self> {
239        Self::from_password(protocol, password.as_bytes(), engine_id)
240    }
241
242    /// Create a localized key from a master key and engine ID.
243    ///
244    /// This is the efficient path when you have a cached [`MasterKey`].
245    /// Equivalent to calling [`MasterKey::localize`].
246    pub fn from_master_key(master: &MasterKey, engine_id: &[u8]) -> CryptoResult<Self> {
247        master.localize(engine_id)
248    }
249
250    /// Create a localized key from raw bytes.
251    ///
252    /// Use this if you already have a localized key (e.g., from configuration).
253    pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
254        Self {
255            key: key.into(),
256            protocol,
257        }
258    }
259
260    /// Get the protocol this key is for.
261    #[must_use]
262    pub fn protocol(&self) -> AuthProtocol {
263        self.protocol
264    }
265
266    /// Get the raw key bytes.
267    #[must_use]
268    pub fn as_bytes(&self) -> &[u8] {
269        &self.key
270    }
271
272    /// Get the MAC length for this key's protocol.
273    #[must_use]
274    pub fn mac_len(&self) -> usize {
275        self.protocol.mac_len()
276    }
277
278    /// Compute HMAC over a message and return the truncated MAC.
279    ///
280    /// The returned MAC is truncated to the appropriate length for the protocol
281    /// (12 bytes for MD5/SHA-1, variable for SHA-2).
282    ///
283    /// # Errors
284    ///
285    /// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
286    /// backend does not support the key's authentication protocol.
287    pub fn compute_hmac(&self, data: &[u8]) -> CryptoResult<Vec<u8>> {
288        compute_hmac(self.protocol, &self.key, data)
289    }
290
291    /// Verify an HMAC.
292    ///
293    /// Returns `true` if the MAC matches, `false` otherwise.
294    ///
295    /// # Errors
296    ///
297    /// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
298    /// backend does not support the key's authentication protocol.
299    pub fn verify_hmac(&self, data: &[u8], expected: &[u8]) -> CryptoResult<bool> {
300        let computed = self.compute_hmac(data)?;
301        // Constant-time comparison
302        if computed.len() != expected.len() {
303            return Ok(false);
304        }
305        let mut result = 0u8;
306        for (a, b) in computed.iter().zip(expected.iter()) {
307            result |= a ^ b;
308        }
309        Ok(result == 0)
310    }
311}
312
313impl std::fmt::Debug for LocalizedKey {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        f.debug_struct("LocalizedKey")
316            .field("protocol", &self.protocol)
317            .field("key", &"[REDACTED]")
318            .finish()
319    }
320}
321
322impl AsRef<[u8]> for LocalizedKey {
323    fn as_ref(&self) -> &[u8] {
324        self.as_bytes()
325    }
326}
327
328/// Password to key transformation (RFC 3414 Section A.2.1).
329///
330/// Routes through the active [`CryptoProvider`](super::crypto::CryptoProvider).
331fn password_to_key(protocol: AuthProtocol, password: &[u8]) -> CryptoResult<Vec<u8>> {
332    super::crypto::provider().password_to_key(protocol, password)
333}
334
335/// Key localization (RFC 3414 Section A.2.2).
336///
337/// Routes through the active [`CryptoProvider`](super::crypto::CryptoProvider).
338fn localize_key(
339    protocol: AuthProtocol,
340    master_key: &[u8],
341    engine_id: &[u8],
342) -> CryptoResult<Vec<u8>> {
343    super::crypto::provider().localize_key(protocol, master_key, engine_id)
344}
345
346/// Compute HMAC with the appropriate algorithm.
347///
348/// Routes through the active [`CryptoProvider`](super::crypto::CryptoProvider).
349fn compute_hmac(protocol: AuthProtocol, key: &[u8], data: &[u8]) -> CryptoResult<Vec<u8>> {
350    super::crypto::provider().compute_hmac(protocol, key, &[data], protocol.mac_len())
351}
352
353/// HMAC computation over multiple data slices (avoids concatenation allocation).
354///
355/// Routes through the active [`CryptoProvider`](super::crypto::CryptoProvider).
356fn compute_hmac_slices(
357    protocol: AuthProtocol,
358    key: &[u8],
359    slices: &[&[u8]],
360) -> CryptoResult<Vec<u8>> {
361    super::crypto::provider().compute_hmac(protocol, key, slices, protocol.mac_len())
362}
363
364/// Authenticate an outgoing message by computing and inserting the HMAC.
365///
366/// The message must already have placeholder zeros in the auth params field.
367/// This function computes the HMAC over the entire message (with zeros in place)
368/// and returns the message with the actual HMAC inserted.
369///
370/// # Errors
371///
372/// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
373/// backend does not support the key's authentication protocol.
374///
375/// Returns [`CryptoError::CipherError`](super::CryptoError::CipherError) if the auth-parameter
376/// offset/length is out of bounds for the message; leaving the message unsigned would be a
377/// silent failure, so an out-of-bounds offset is rejected rather than returning `Ok(())`.
378pub fn authenticate_message(
379    key: &LocalizedKey,
380    message: &mut [u8],
381    auth_offset: usize,
382    auth_len: usize,
383) -> CryptoResult<()> {
384    let end = match auth_offset.checked_add(auth_len) {
385        Some(e) if e <= message.len() => e,
386        _ => return Err(super::CryptoError::CipherError),
387    };
388
389    // Compute HMAC over the message with zeros in auth params position
390    let mac = key.compute_hmac(message)?;
391
392    // Replace zeros with actual MAC
393    message[auth_offset..end].copy_from_slice(&mac);
394    Ok(())
395}
396
397/// Verify the authentication of an incoming message.
398///
399/// Returns `true` if the MAC is valid, `false` otherwise.
400///
401/// # Errors
402///
403/// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
404/// backend does not support the key's authentication protocol.
405pub fn verify_message(
406    key: &LocalizedKey,
407    message: &[u8],
408    auth_offset: usize,
409    auth_len: usize,
410) -> CryptoResult<bool> {
411    const MAX_MAC_LEN: usize = 48; // SHA-512
412
413    // No supported protocol produces a MAC longer than MAX_MAC_LEN; a larger
414    // auth_len can never verify and would overrun the zeros buffer below.
415    if auth_len > MAX_MAC_LEN {
416        return Ok(false);
417    }
418    let end = match auth_offset.checked_add(auth_len) {
419        Some(e) if e <= message.len() => e,
420        _ => return Ok(false),
421    };
422
423    // Extract the received MAC
424    let received_mac = &message[auth_offset..end];
425
426    // Compute HMAC over the message with zeros in the auth position,
427    // feeding three slices to avoid copying the entire message.
428    let computed = {
429        let zeros: [u8; MAX_MAC_LEN] = [0u8; MAX_MAC_LEN];
430        compute_hmac_slices(
431            key.protocol,
432            key.as_bytes(),
433            &[&message[..auth_offset], &zeros[..auth_len], &message[end..]],
434        )?
435    };
436
437    // Constant-time comparison
438    if computed.len() != received_mac.len() {
439        return Ok(false);
440    }
441    let mut result = 0u8;
442    for (a, b) in computed.iter().zip(received_mac.iter()) {
443        result |= a ^ b;
444    }
445    Ok(result == 0)
446}
447
448/// Pre-computed master keys for `SNMPv3` authentication and privacy.
449///
450/// This struct caches the expensive password-to-key derivation results for
451/// both authentication and privacy passwords. When polling many engines with
452/// shared credentials, create a `MasterKeys` once and use it with
453/// [`UsmBuilder`](crate::UsmBuilder) to avoid repeating the ~850μs key derivation for each engine.
454///
455/// # Example
456///
457/// ```rust
458/// use async_snmp::{AuthProtocol, PrivProtocol, MasterKeys};
459///
460/// // Create master keys once (expensive)
461/// let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword").unwrap()
462///     .with_privacy(PrivProtocol::Aes128, b"privpassword").unwrap();
463///
464/// // Use with multiple clients - localization is cheap (~1μs per engine)
465/// ```
466#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq, Eq, PartialOrd, Ord, Hash)]
467pub struct MasterKeys {
468    /// Master key for authentication (and base for privacy key derivation)
469    auth_master: MasterKey,
470    /// Optional separate master key for privacy password
471    /// If None, the `auth_master` is used for privacy (common case: same password)
472    #[zeroize(skip)]
473    priv_protocol: Option<super::PrivProtocol>,
474    priv_master: Option<MasterKey>,
475}
476
477impl MasterKeys {
478    /// Create master keys with just authentication.
479    ///
480    /// # Errors
481    ///
482    /// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
483    /// backend does not support the requested authentication protocol.
484    ///
485    /// # Example
486    ///
487    /// ```rust
488    /// use async_snmp::{AuthProtocol, MasterKeys};
489    ///
490    /// let keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword").unwrap();
491    /// ```
492    pub fn new(auth_protocol: AuthProtocol, auth_password: &[u8]) -> CryptoResult<Self> {
493        Ok(Self {
494            auth_master: MasterKey::from_password(auth_protocol, auth_password)?,
495            priv_protocol: None,
496            priv_master: None,
497        })
498    }
499
500    /// Add privacy with the same password as authentication.
501    ///
502    /// This is the common case where auth and priv passwords are identical.
503    /// The same master key is reused, avoiding duplicate derivation.
504    #[must_use]
505    pub fn with_privacy_same_password(mut self, priv_protocol: super::PrivProtocol) -> Self {
506        self.priv_protocol = Some(priv_protocol);
507        // priv_master stays None - we'll use auth_master for priv key derivation
508        self
509    }
510
511    /// Add privacy with a different password than authentication.
512    ///
513    /// Use this when auth and priv passwords differ. A separate master key
514    /// derivation is performed for the privacy password.
515    ///
516    /// # Errors
517    ///
518    /// Returns [`CryptoError::UnsupportedAlgorithm`](super::CryptoError::UnsupportedAlgorithm) if the active crypto
519    /// backend does not support the authentication protocol used for key
520    /// derivation.
521    pub fn with_privacy(
522        mut self,
523        priv_protocol: super::PrivProtocol,
524        priv_password: &[u8],
525    ) -> CryptoResult<Self> {
526        self.priv_protocol = Some(priv_protocol);
527        // Use the auth protocol for priv key derivation (per RFC 3826 Section 1.2)
528        self.priv_master = Some(MasterKey::from_password(
529            self.auth_master.protocol(),
530            priv_password,
531        )?);
532        Ok(self)
533    }
534
535    /// Get the authentication master key.
536    #[must_use]
537    pub fn auth_master(&self) -> &MasterKey {
538        &self.auth_master
539    }
540
541    /// Get the privacy master key, if configured.
542    ///
543    /// Returns the separate priv master key if set, otherwise returns the
544    /// auth master key (for same-password case).
545    #[must_use]
546    pub fn priv_master(&self) -> Option<&MasterKey> {
547        if self.priv_protocol.is_some() {
548            Some(self.priv_master.as_ref().unwrap_or(&self.auth_master))
549        } else {
550            None
551        }
552    }
553
554    /// Get the configured privacy protocol.
555    #[must_use]
556    pub fn priv_protocol(&self) -> Option<super::PrivProtocol> {
557        self.priv_protocol
558    }
559
560    /// Get the authentication protocol.
561    #[must_use]
562    pub fn auth_protocol(&self) -> AuthProtocol {
563        self.auth_master.protocol()
564    }
565
566    /// Derive localized keys for a specific engine ID.
567    ///
568    /// Returns (`auth_key`, `priv_key`) where `priv_key` is None if no privacy
569    /// was configured.
570    ///
571    /// Key extension is automatically applied when needed based on the auth/priv
572    /// protocol combination:
573    ///
574    /// - AES-192/256 with SHA-1 or MD5: Blumenthal extension (draft-blumenthal-aes-usm-04)
575    /// - 3DES with SHA-1 or MD5: Reeder extension (draft-reeder-snmpv3-usm-3desede-00)
576    ///
577    /// # Example
578    ///
579    /// ```rust
580    /// use async_snmp::{AuthProtocol, MasterKeys, PrivProtocol};
581    ///
582    /// let keys = MasterKeys::new(AuthProtocol::Sha1, b"authpassword").unwrap()
583    ///     .with_privacy_same_password(PrivProtocol::Aes256);
584    ///
585    /// let engine_id = [0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04];
586    ///
587    /// // SHA-1 only produces 20 bytes, but AES-256 needs 32.
588    /// // Blumenthal extension is automatically applied.
589    /// let (auth, priv_key) = keys.localize(&engine_id).unwrap();
590    /// ```
591    pub fn localize(
592        &self,
593        engine_id: &[u8],
594    ) -> CryptoResult<(LocalizedKey, Option<crate::v3::PrivKey>)> {
595        let auth_key = self.auth_master.localize(engine_id)?;
596
597        let priv_key = self
598            .priv_protocol
599            .map(|priv_protocol| {
600                let master = self.priv_master.as_ref().unwrap_or(&self.auth_master);
601                crate::v3::PrivKey::from_master_key(master, priv_protocol, engine_id)
602            })
603            .transpose()?;
604
605        Ok((auth_key, priv_key))
606    }
607}
608
609impl std::fmt::Debug for MasterKeys {
610    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611        f.debug_struct("MasterKeys")
612            .field("auth_protocol", &self.auth_master.protocol())
613            .field("priv_protocol", &self.priv_protocol)
614            .field("has_separate_priv_password", &self.priv_master.is_some())
615            .finish()
616    }
617}
618
619/// Extend a localized key to the required length using the Blumenthal algorithm.
620///
621/// This implements the key extension algorithm from draft-blumenthal-aes-usm-04
622/// Section 3.1.2.1, which allows AES-192/256 to be used with authentication
623/// protocols that produce shorter digests (e.g., SHA-1 with AES-256).
624///
625/// The algorithm iteratively appends hash digests:
626/// ```text
627/// Kul' = Kul || H(Kul) || H(Kul || H(Kul)) || ...
628/// ```
629///
630/// Where `H()` is the hash function of the authentication protocol.
631pub(crate) fn extend_key(
632    protocol: AuthProtocol,
633    key: &[u8],
634    target_len: usize,
635) -> CryptoResult<Vec<u8>> {
636    // If we already have enough bytes, just truncate
637    if key.len() >= target_len {
638        return Ok(key[..target_len].to_vec());
639    }
640
641    let provider = super::crypto::provider();
642    let mut result = key.to_vec();
643
644    // Keep appending H(result) until we have enough bytes
645    while result.len() < target_len {
646        let hash = provider.hash(protocol, &result)?;
647        result.extend_from_slice(&hash);
648    }
649
650    // Truncate to exact length
651    result.truncate(target_len);
652    Ok(result)
653}
654
655/// Extend a localized key using the Reeder key extension algorithm.
656///
657/// This implements the key extension algorithm from draft-reeder-snmpv3-usm-3desede-00
658/// Section 2.1. Unlike Blumenthal, this algorithm re-runs the full password-to-key (P2K)
659/// algorithm using the current localized key as the "passphrase":
660///
661/// ```text
662/// K1 = P2K(passphrase, engine_id)   // Original localized key (input)
663/// K2 = P2K(K1, engine_id)           // Run full P2K with K1 as passphrase
664/// localized_key = K1 || K2
665/// K3 = P2K(K2, engine_id)           // If more bytes needed
666/// localized_key = K1 || K2 || K3
667/// ... and so on
668/// ```
669///
670/// # Performance Warning
671///
672/// This is approximately 1000x slower than [`extend_key`] (Blumenthal) because each
673/// iteration requires the full 1MB password expansion.
674pub(crate) fn extend_key_reeder(
675    protocol: AuthProtocol,
676    key: &[u8],
677    engine_id: &[u8],
678    target_len: usize,
679) -> CryptoResult<Vec<u8>> {
680    // If we already have enough bytes, just truncate
681    if key.len() >= target_len {
682        return Ok(key[..target_len].to_vec());
683    }
684
685    let mut result = key.to_vec();
686    let mut current_kul = key.to_vec();
687
688    // Keep extending until we have enough bytes
689    while result.len() < target_len {
690        // Run full password-to-key using current Kul as the "passphrase"
691        // This is the expensive 1MB expansion step
692        let ku = password_to_key(protocol, &current_kul)?;
693
694        // Localize the new Ku to get Kul
695        let new_kul = localize_key(protocol, &ku, engine_id)?;
696
697        // Append as many bytes as we need (or all of them)
698        let bytes_needed = target_len - result.len();
699        let bytes_to_copy = bytes_needed.min(new_kul.len());
700        result.extend_from_slice(&new_kul[..bytes_to_copy]);
701
702        // The next iteration uses the new Kul as input
703        current_kul = new_kul;
704    }
705
706    Ok(result)
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712    use crate::format::hex::{decode as decode_hex, encode as encode_hex};
713
714    #[cfg(feature = "crypto-rustcrypto")]
715    #[test]
716    fn test_password_to_key_md5() {
717        // Test vector from RFC 3414 Appendix A.3.1
718        // Password: "maplesyrup"
719        // Expected Ku (hex): 9faf 3283 884e 9283 4ebc 9847 d8ed d963
720        let password = b"maplesyrup";
721        let key = password_to_key(AuthProtocol::Md5, password).unwrap();
722
723        assert_eq!(key.len(), 16);
724        assert_eq!(encode_hex(&key), "9faf3283884e92834ebc9847d8edd963");
725    }
726
727    #[test]
728    fn test_password_to_key_sha1() {
729        // Test vector from RFC 3414 Appendix A.3.2
730        // Password: "maplesyrup"
731        // Expected Ku (hex): 9fb5 cc03 8149 7b37 9352 8939 ff78 8d5d 7914 5211
732        let password = b"maplesyrup";
733        let key = password_to_key(AuthProtocol::Sha1, password).unwrap();
734
735        assert_eq!(key.len(), 20);
736        assert_eq!(encode_hex(&key), "9fb5cc0381497b3793528939ff788d5d79145211");
737    }
738
739    #[cfg(feature = "crypto-rustcrypto")]
740    #[test]
741    fn test_localize_key_md5() {
742        // Test vector from RFC 3414 Appendix A.3.1
743        // Master key from "maplesyrup"
744        // Engine ID: 00 00 00 00 00 00 00 00 00 00 00 02
745        // Expected Kul (hex): 526f 5eed 9fcc e26f 8964 c293 0787 d82b
746        let password = b"maplesyrup";
747        let engine_id = decode_hex("000000000000000000000002").unwrap();
748
749        let key = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id).unwrap();
750
751        assert_eq!(key.as_bytes().len(), 16);
752        assert_eq!(
753            encode_hex(key.as_bytes()),
754            "526f5eed9fcce26f8964c2930787d82b"
755        );
756    }
757
758    #[test]
759    fn test_localize_key_sha1() {
760        // Test vector from RFC 3414 Appendix A.3.2
761        // Engine ID: 00 00 00 00 00 00 00 00 00 00 00 02
762        // Expected Kul (hex): 6695 febc 9288 e362 8223 5fc7 151f 1284 97b3 8f3f
763        let password = b"maplesyrup";
764        let engine_id = decode_hex("000000000000000000000002").unwrap();
765
766        let key = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
767
768        assert_eq!(key.as_bytes().len(), 20);
769        assert_eq!(
770            encode_hex(key.as_bytes()),
771            "6695febc9288e36282235fc7151f128497b38f3f"
772        );
773    }
774
775    #[cfg(feature = "crypto-rustcrypto")]
776    #[test]
777    fn test_hmac_computation() {
778        let key = LocalizedKey::from_bytes(
779            AuthProtocol::Md5,
780            vec![
781                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
782                0x0f, 0x10,
783            ],
784        );
785
786        let data = b"test message";
787        let mac = key.compute_hmac(data).unwrap();
788
789        // HMAC-MD5-96: 12 bytes
790        assert_eq!(mac.len(), 12);
791
792        // Verify returns true for correct MAC
793        assert!(key.verify_hmac(data, &mac).unwrap());
794
795        // Verify returns false for wrong MAC
796        let mut wrong_mac = mac.clone();
797        wrong_mac[0] ^= 0xFF;
798        assert!(!key.verify_hmac(data, &wrong_mac).unwrap());
799    }
800
801    #[cfg(feature = "crypto-rustcrypto")]
802    #[test]
803    fn test_verify_message_oversized_auth_len() {
804        let key = LocalizedKey::from_bytes(
805            AuthProtocol::Md5,
806            vec![
807                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
808                0x0f, 0x10,
809            ],
810        );
811
812        // auth_len larger than the biggest supported MAC (48 bytes for
813        // SHA-512) but still within the message bounds must be rejected,
814        // not panic on the internal zeros buffer.
815        let message = vec![0u8; 128];
816        let result = verify_message(&key, &message, 10, 49).unwrap();
817        assert!(!result);
818    }
819
820    #[cfg(feature = "crypto-rustcrypto")]
821    #[test]
822    fn test_authenticate_message_oob_offset_errors() {
823        let key = LocalizedKey::from_bytes(
824            AuthProtocol::Md5,
825            vec![
826                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
827                0x0f, 0x10,
828            ],
829        );
830
831        // An auth-parameter offset/length reaching past the message end must be
832        // rejected rather than silently leaving the message unsigned.
833        let mut message = vec![0u8; 32];
834        assert_eq!(
835            authenticate_message(&key, &mut message, 30, 12),
836            Err(super::super::CryptoError::CipherError)
837        );
838
839        // Overflow of offset + len must also be rejected.
840        let mut message = vec![0u8; 32];
841        assert_eq!(
842            authenticate_message(&key, &mut message, usize::MAX, 12),
843            Err(super::super::CryptoError::CipherError)
844        );
845    }
846
847    #[cfg(feature = "crypto-rustcrypto")]
848    #[test]
849    fn test_empty_password_rejected() {
850        // RFC 3414 §11.2 / net-snmp USM_PASSWORDTOOSHORT: an empty password
851        // must be rejected rather than deriving an all-zero key.
852        assert_eq!(
853            password_to_key(AuthProtocol::Md5, b""),
854            Err(super::super::CryptoError::PasswordTooShort)
855        );
856    }
857
858    #[test]
859    fn test_short_password_rejected() {
860        // A 7-octet password is below the RFC 3414 minimum of 8 and must be
861        // rejected at every plaintext-password derivation entry point.
862        let engine_id = decode_hex("000000000000000000000002").unwrap();
863        let short = b"1234567"; // 7 octets
864
865        assert_eq!(
866            MasterKey::from_password(AuthProtocol::Sha1, short),
867            Err(super::super::CryptoError::PasswordTooShort)
868        );
869        assert_eq!(
870            LocalizedKey::from_password(AuthProtocol::Sha1, short, &engine_id).err(),
871            Some(super::super::CryptoError::PasswordTooShort)
872        );
873        assert_eq!(
874            MasterKeys::new(AuthProtocol::Sha1, short),
875            Err(super::super::CryptoError::PasswordTooShort)
876        );
877    }
878
879    #[test]
880    fn test_min_length_password_accepted() {
881        // An exactly-8-octet password meets the minimum and must succeed.
882        let engine_id = decode_hex("000000000000000000000002").unwrap();
883        let ok = b"12345678"; // 8 octets
884
885        assert!(MasterKey::from_password(AuthProtocol::Sha1, ok).is_ok());
886        assert!(LocalizedKey::from_password(AuthProtocol::Sha1, ok, &engine_id).is_ok());
887        assert!(MasterKeys::new(AuthProtocol::Sha1, ok).is_ok());
888    }
889
890    #[test]
891    fn test_from_bytes_bypasses_length_check() {
892        // Pre-derived key constructors take key material, not a plaintext
893        // password, and must remain unaffected by the length check.
894        let short_key = vec![0xAAu8; 4];
895        let master = MasterKey::from_bytes(AuthProtocol::Sha1, short_key.clone());
896        assert_eq!(master.as_bytes(), short_key.as_slice());
897        let localized = LocalizedKey::from_bytes(AuthProtocol::Sha1, short_key.clone());
898        assert_eq!(localized.as_bytes(), short_key.as_slice());
899    }
900
901    #[test]
902    fn test_from_str_password() {
903        // Verify from_str_password produces same result as from_password with bytes
904        let engine_id = decode_hex("000000000000000000000002").unwrap();
905
906        let key_from_bytes =
907            LocalizedKey::from_password(AuthProtocol::Sha1, b"maplesyrup", &engine_id).unwrap();
908        let key_from_str =
909            LocalizedKey::from_str_password(AuthProtocol::Sha1, "maplesyrup", &engine_id).unwrap();
910
911        assert_eq!(key_from_bytes.as_bytes(), key_from_str.as_bytes());
912        assert_eq!(key_from_bytes.protocol(), key_from_str.protocol());
913    }
914
915    #[cfg(feature = "crypto-rustcrypto")]
916    #[test]
917    fn test_master_key_localize_md5() {
918        // Verify MasterKey produces same result as LocalizedKey::from_password
919        let password = b"maplesyrup";
920        let engine_id = decode_hex("000000000000000000000002").unwrap();
921
922        let master = MasterKey::from_password(AuthProtocol::Md5, password).unwrap();
923        let localized_via_master = master.localize(&engine_id).unwrap();
924        let localized_direct =
925            LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id).unwrap();
926
927        assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
928        assert_eq!(localized_via_master.protocol(), localized_direct.protocol());
929
930        // Verify the master key itself matches RFC 3414 test vector
931        assert_eq!(
932            encode_hex(master.as_bytes()),
933            "9faf3283884e92834ebc9847d8edd963"
934        );
935    }
936
937    #[test]
938    fn test_master_key_localize_sha1() {
939        let password = b"maplesyrup";
940        let engine_id = decode_hex("000000000000000000000002").unwrap();
941
942        let master = MasterKey::from_password(AuthProtocol::Sha1, password).unwrap();
943        let localized_via_master = master.localize(&engine_id).unwrap();
944        let localized_direct =
945            LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
946
947        assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
948
949        // Verify the master key itself matches RFC 3414 test vector
950        assert_eq!(
951            encode_hex(master.as_bytes()),
952            "9fb5cc0381497b3793528939ff788d5d79145211"
953        );
954    }
955
956    #[test]
957    fn test_master_key_reuse_for_multiple_engines() {
958        // Demonstrate that a single MasterKey can localize to multiple engines
959        let password = b"maplesyrup";
960        let engine_id_1 = decode_hex("000000000000000000000001").unwrap();
961        let engine_id_2 = decode_hex("000000000000000000000002").unwrap();
962
963        let master = MasterKey::from_password(AuthProtocol::Sha256, password).unwrap();
964
965        let key1 = master.localize(&engine_id_1).unwrap();
966        let key2 = master.localize(&engine_id_2).unwrap();
967
968        // Keys should be different for different engines
969        assert_ne!(key1.as_bytes(), key2.as_bytes());
970
971        // Each key should match what from_password produces
972        let direct1 =
973            LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_1).unwrap();
974        let direct2 =
975            LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_2).unwrap();
976
977        assert_eq!(key1.as_bytes(), direct1.as_bytes());
978        assert_eq!(key2.as_bytes(), direct2.as_bytes());
979    }
980
981    #[test]
982    fn test_from_master_key() {
983        let password = b"maplesyrup";
984        let engine_id = decode_hex("000000000000000000000002").unwrap();
985
986        let master = MasterKey::from_password(AuthProtocol::Sha256, password).unwrap();
987        let key_via_localize = master.localize(&engine_id).unwrap();
988        let key_via_from_master = LocalizedKey::from_master_key(&master, &engine_id).unwrap();
989
990        assert_eq!(key_via_localize.as_bytes(), key_via_from_master.as_bytes());
991    }
992
993    #[test]
994    fn test_master_keys_auth_only() {
995        let engine_id = decode_hex("000000000000000000000002").unwrap();
996        let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword").unwrap();
997
998        assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
999        assert!(master_keys.priv_protocol().is_none());
1000        assert!(master_keys.priv_master().is_none());
1001
1002        let (auth_key, priv_key) = master_keys.localize(&engine_id).unwrap();
1003        assert!(priv_key.is_none());
1004        assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
1005    }
1006
1007    #[test]
1008    fn test_master_keys_with_privacy_same_password() {
1009        use crate::v3::PrivProtocol;
1010
1011        let engine_id = decode_hex("000000000000000000000002").unwrap();
1012        let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"sharedpassword")
1013            .unwrap()
1014            .with_privacy_same_password(PrivProtocol::Aes128);
1015
1016        assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
1017        assert_eq!(master_keys.priv_protocol(), Some(PrivProtocol::Aes128));
1018
1019        let (auth_key, priv_key) = master_keys.localize(&engine_id).unwrap();
1020        assert!(priv_key.is_some());
1021        assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
1022    }
1023
1024    #[test]
1025    fn test_master_keys_with_privacy_different_password() {
1026        use crate::v3::PrivProtocol;
1027
1028        let engine_id = decode_hex("000000000000000000000002").unwrap();
1029        let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
1030            .unwrap()
1031            .with_privacy(PrivProtocol::Aes128, b"privpassword")
1032            .unwrap();
1033
1034        let (_auth_key, priv_key) = master_keys.localize(&engine_id).unwrap();
1035        assert!(priv_key.is_some());
1036
1037        // Verify that different passwords produce different keys
1038        let same_password_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
1039            .unwrap()
1040            .with_privacy_same_password(PrivProtocol::Aes128);
1041        let (_, priv_key_same) = same_password_keys.localize(&engine_id).unwrap();
1042
1043        // The priv keys should differ when using different passwords
1044        // (auth keys are the same since they use same auth password)
1045        assert_ne!(
1046            priv_key.as_ref().unwrap().encryption_key(),
1047            priv_key_same.as_ref().unwrap().encryption_key()
1048        );
1049    }
1050
1051    // Known-Answer Tests (KAT) for Reeder key extension algorithm
1052    // Test vectors from draft-reeder-snmpv3-usm-3desede-00 Appendix B
1053
1054    #[cfg(feature = "crypto-rustcrypto")]
1055    #[test]
1056    fn test_reeder_extend_key_md5_kat() {
1057        // Test vector from draft-reeder Appendix B.1
1058        // Password: "maplesyrup"
1059        // Engine ID: 00 00 00 00 00 00 00 00 00 00 00 02
1060        // Expected 32-byte localized key:
1061        //   52 6f 5e ed 9f cc e2 6f 89 64 c2 93 07 87 d8 2b   (first 16 bytes = K1)
1062        //   79 ef f4 4a 90 65 0e e0 a3 a4 0a bf ac 5a cc 12   (next 16 bytes = K2)
1063        let password = b"maplesyrup";
1064        let engine_id = decode_hex("000000000000000000000002").unwrap();
1065
1066        // Get the standard localized key (K1)
1067        let k1 = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id).unwrap();
1068        assert_eq!(
1069            encode_hex(k1.as_bytes()),
1070            "526f5eed9fcce26f8964c2930787d82b"
1071        );
1072
1073        // Extend using Reeder algorithm to 32 bytes
1074        let extended = extend_key_reeder(AuthProtocol::Md5, k1.as_bytes(), &engine_id, 32).unwrap();
1075        assert_eq!(extended.len(), 32);
1076        assert_eq!(
1077            encode_hex(&extended),
1078            "526f5eed9fcce26f8964c2930787d82b79eff44a90650ee0a3a40abfac5acc12"
1079        );
1080    }
1081
1082    #[test]
1083    fn test_reeder_extend_key_sha1_kat() {
1084        // Test vector from draft-reeder Appendix B.2
1085        // Password: "maplesyrup"
1086        // Engine ID: 00 00 00 00 00 00 00 00 00 00 00 02
1087        // Expected 40-byte localized key:
1088        //   66 95 fe bc 92 88 e3 62 82 23 5f c7 15 1f 12 84 97 b3 8f 3f  (first 20 bytes = K1)
1089        //   9b 8b 6d 78 93 6b a6 e7 d1 9d fd 9c d2 d5 06 55 47 74 3f b5  (next 20 bytes = K2)
1090        let password = b"maplesyrup";
1091        let engine_id = decode_hex("000000000000000000000002").unwrap();
1092
1093        // Get the standard localized key (K1)
1094        let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
1095        assert_eq!(
1096            encode_hex(k1.as_bytes()),
1097            "6695febc9288e36282235fc7151f128497b38f3f"
1098        );
1099
1100        // Extend using Reeder algorithm to 40 bytes
1101        let extended =
1102            extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 40).unwrap();
1103        assert_eq!(extended.len(), 40);
1104        assert_eq!(
1105            encode_hex(&extended),
1106            "6695febc9288e36282235fc7151f128497b38f3f9b8b6d78936ba6e7d19dfd9cd2d5065547743fb5"
1107        );
1108    }
1109
1110    #[test]
1111    fn test_reeder_extend_key_sha1_to_32_bytes() {
1112        // Extending SHA-1 key to 32 bytes (for AES-256)
1113        // Should be the first 32 bytes of the 40-byte result
1114        let password = b"maplesyrup";
1115        let engine_id = decode_hex("000000000000000000000002").unwrap();
1116
1117        let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
1118        let extended =
1119            extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 32).unwrap();
1120
1121        assert_eq!(extended.len(), 32);
1122        // First 20 bytes = K1, next 12 bytes = first 12 bytes of K2
1123        assert_eq!(
1124            encode_hex(&extended),
1125            "6695febc9288e36282235fc7151f128497b38f3f9b8b6d78936ba6e7d19dfd9c"
1126        );
1127    }
1128
1129    #[test]
1130    fn test_reeder_extend_key_truncation() {
1131        // When key is already long enough, should truncate
1132        let long_key = vec![0xAAu8; 64];
1133        let engine_id = decode_hex("000000000000000000000002").unwrap();
1134
1135        let extended = extend_key_reeder(AuthProtocol::Sha256, &long_key, &engine_id, 32).unwrap();
1136        assert_eq!(extended.len(), 32);
1137        assert_eq!(extended, vec![0xAAu8; 32]);
1138    }
1139
1140    #[test]
1141    fn test_reeder_vs_blumenthal_differ() {
1142        // Verify that Reeder and Blumenthal produce different results
1143        let password = b"maplesyrup";
1144        let engine_id = decode_hex("000000000000000000000002").unwrap();
1145
1146        let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
1147
1148        let reeder = extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 32).unwrap();
1149        let blumenthal = extend_key(AuthProtocol::Sha1, k1.as_bytes(), 32).unwrap();
1150
1151        assert_eq!(reeder.len(), 32);
1152        assert_eq!(blumenthal.len(), 32);
1153
1154        // First 20 bytes should be identical (both start with K1)
1155        assert_eq!(&reeder[..20], &blumenthal[..20]);
1156        // Extension bytes should differ (different algorithms)
1157        assert_ne!(&reeder[20..], &blumenthal[20..]);
1158    }
1159}