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