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