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