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