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