stem_rs/response/
authchallenge.rs

1//! AUTHCHALLENGE response parsing.
2//!
3//! This module parses responses from the `AUTHCHALLENGE` command, which is used
4//! during SAFECOOKIE authentication. SAFECOOKIE is the most secure authentication
5//! method for local Tor connections, using HMAC-SHA256 challenge-response.
6//!
7//! # Protocol Overview
8//!
9//! SAFECOOKIE authentication works as follows:
10//!
11//! 1. Client sends `AUTHCHALLENGE SAFECOOKIE <client_nonce>`
12//! 2. Server responds with `SERVERHASH` and `SERVERNONCE`
13//! 3. Client computes `HMAC-SHA256(cookie || client_nonce || server_nonce)`
14//! 4. Client sends `AUTHENTICATE <computed_hash>`
15//!
16//! # Response Format
17//!
18//! ```text
19//! 250 AUTHCHALLENGE SERVERHASH=<64_hex_chars> SERVERNONCE=<64_hex_chars>
20//! ```
21//!
22//! Both values are 32-byte (256-bit) values encoded as 64 hexadecimal characters.
23//!
24//! # Example
25//!
26//! ```rust
27//! use stem_rs::response::{ControlMessage, AuthChallengeResponse};
28//!
29//! let response_text = "250 AUTHCHALLENGE \
30//!     SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5 \
31//!     SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85\r\n";
32//! let msg = ControlMessage::from_str(response_text, None, false).unwrap();
33//! let response = AuthChallengeResponse::from_message(&msg).unwrap();
34//!
35//! assert_eq!(response.server_hash.len(), 32);
36//! assert_eq!(response.server_nonce.len(), 32);
37//! ```
38//!
39//! # Security Considerations
40//!
41//! - The server hash proves the server knows the cookie file contents
42//! - The server nonce prevents replay attacks
43//! - Both values should be used exactly once per authentication attempt
44//! - Failed authentication should trigger a new challenge with fresh nonces
45//!
46//! # See Also
47//!
48//! - [`crate::auth::authenticate_safecookie`]: High-level SAFECOOKIE authentication
49//! - [`crate::response::ProtocolInfoResponse`]: Determines available auth methods
50//! - [Tor Control Protocol: AUTHCHALLENGE](https://spec.torproject.org/control-spec/commands.html#authchallenge)
51
52use super::ControlMessage;
53use crate::Error;
54
55/// Parsed response from the AUTHCHALLENGE command.
56///
57/// Contains the server's challenge values needed to complete SAFECOOKIE
58/// authentication. These values are used to compute the HMAC-SHA256
59/// authentication token.
60///
61/// # Fields
62///
63/// - `server_hash`: HMAC proving the server knows the cookie
64/// - `server_nonce`: Random value to prevent replay attacks
65///
66/// # Security
67///
68/// Both values are cryptographically significant:
69///
70/// - **server_hash**: Computed as `HMAC-SHA256(cookie, "Tor safe cookie authentication server-to-controller hash" || client_nonce || server_nonce)`
71/// - **server_nonce**: 32 random bytes generated by Tor for this challenge
72///
73/// The client must verify the server_hash before sending its authentication
74/// response to prevent man-in-the-middle attacks.
75///
76/// # Example
77///
78/// ```rust
79/// use stem_rs::response::{ControlMessage, AuthChallengeResponse};
80///
81/// // Parse a typical AUTHCHALLENGE response
82/// let msg = ControlMessage::from_str(
83///     "250 AUTHCHALLENGE \
84///      SERVERHASH=B16F72DACD4B5ED1531F3FCC04B593D46A1E30267E636EA7C7F8DD7A2B7BAA05 \
85///      SERVERNONCE=653574272ABBB49395BD1060D642D653CFB7A2FCE6A4955BCFED819703A9998C\r\n",
86///     None,
87///     false
88/// ).unwrap();
89///
90/// let response = AuthChallengeResponse::from_message(&msg).unwrap();
91///
92/// // Both values are 32 bytes (256 bits)
93/// assert_eq!(response.server_hash.len(), 32);
94/// assert_eq!(response.server_nonce.len(), 32);
95///
96/// // First bytes match the hex encoding
97/// assert_eq!(response.server_hash[0], 0xB1);
98/// assert_eq!(response.server_nonce[0], 0x65);
99/// ```
100#[derive(Debug, Clone)]
101pub struct AuthChallengeResponse {
102    /// The server's HMAC-SHA256 hash proving knowledge of the cookie.
103    ///
104    /// This 32-byte value is computed by the server using the cookie file
105    /// contents, the client nonce, and the server nonce. The client should
106    /// verify this hash before proceeding with authentication.
107    ///
108    /// # Verification
109    ///
110    /// To verify, compute:
111    /// ```text
112    /// HMAC-SHA256(cookie, "Tor safe cookie authentication server-to-controller hash"
113    ///             || client_nonce || server_nonce)
114    /// ```
115    /// and compare with this value using constant-time comparison.
116    pub server_hash: Vec<u8>,
117
118    /// The server's random nonce for this authentication attempt.
119    ///
120    /// This 32-byte random value is generated fresh for each AUTHCHALLENGE
121    /// request. It prevents replay attacks by ensuring each authentication
122    /// attempt uses unique values.
123    ///
124    /// # Usage
125    ///
126    /// Include this nonce when computing the client's authentication hash:
127    /// ```text
128    /// HMAC-SHA256(cookie, "Tor safe cookie authentication controller-to-server hash"
129    ///             || client_nonce || server_nonce)
130    /// ```
131    pub server_nonce: Vec<u8>,
132}
133
134impl AuthChallengeResponse {
135    /// Parses an AUTHCHALLENGE response from a control message.
136    ///
137    /// Extracts the server hash and server nonce from the response, converting
138    /// them from hexadecimal strings to raw bytes.
139    ///
140    /// # Arguments
141    ///
142    /// * `message` - The control message to parse
143    ///
144    /// # Errors
145    ///
146    /// Returns [`Error::Protocol`](crate::Error::Protocol) if:
147    /// - The response status is not OK (2xx)
148    /// - The response contains multiple lines (should be single-line)
149    /// - The response is empty
150    /// - The first word is not "AUTHCHALLENGE"
151    /// - The SERVERHASH mapping is missing or invalid
152    /// - The SERVERNONCE mapping is missing or invalid
153    /// - Either value is not exactly 64 hexadecimal characters
154    ///
155    /// # Example
156    ///
157    /// ```rust
158    /// use stem_rs::response::{ControlMessage, AuthChallengeResponse};
159    ///
160    /// // Valid response
161    /// let msg = ControlMessage::from_str(
162    ///     "250 AUTHCHALLENGE \
163    ///      SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5 \
164    ///      SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85\r\n",
165    ///     None,
166    ///     false
167    /// ).unwrap();
168    ///
169    /// let response = AuthChallengeResponse::from_message(&msg).unwrap();
170    /// assert_eq!(response.server_hash.len(), 32);
171    ///
172    /// // Invalid: missing SERVERNONCE
173    /// let bad_msg = ControlMessage::from_str(
174    ///     "250 AUTHCHALLENGE \
175    ///      SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5\r\n",
176    ///     None,
177    ///     false
178    /// ).unwrap();
179    ///
180    /// assert!(AuthChallengeResponse::from_message(&bad_msg).is_err());
181    /// ```
182    ///
183    /// # Security
184    ///
185    /// After parsing, the caller should:
186    /// 1. Verify the server_hash matches the expected HMAC
187    /// 2. Use the server_nonce to compute the client's authentication response
188    /// 3. Clear sensitive data from memory after use
189    pub fn from_message(message: &ControlMessage) -> Result<Self, Error> {
190        if !message.is_ok() {
191            return Err(Error::Protocol(format!(
192                "AUTHCHALLENGE response didn't have an OK status:\n{}",
193                message
194            )));
195        }
196
197        if message.len() > 1 {
198            return Err(Error::Protocol(format!(
199                "Received multiline AUTHCHALLENGE response:\n{}",
200                message
201            )));
202        }
203
204        let mut line = message
205            .get(0)
206            .ok_or_else(|| Error::Protocol("Empty AUTHCHALLENGE response".to_string()))?;
207
208        let first_word = line.pop(false, false)?;
209        if first_word != "AUTHCHALLENGE" {
210            return Err(Error::Protocol(format!(
211                "Message is not an AUTHCHALLENGE response ({})",
212                message
213            )));
214        }
215
216        let server_hash = if line.is_next_mapping(Some("SERVERHASH"), false, false) {
217            let (_, value) = line.pop_mapping(false, false)?;
218            if !is_hex_digits(&value, 64) {
219                return Err(Error::Protocol(format!(
220                    "SERVERHASH has an invalid value: {}",
221                    value
222                )));
223            }
224            hex_decode(&value)?
225        } else {
226            return Err(Error::Protocol(format!(
227                "Missing SERVERHASH mapping: {}",
228                line
229            )));
230        };
231
232        let server_nonce = if line.is_next_mapping(Some("SERVERNONCE"), false, false) {
233            let (_, value) = line.pop_mapping(false, false)?;
234            if !is_hex_digits(&value, 64) {
235                return Err(Error::Protocol(format!(
236                    "SERVERNONCE has an invalid value: {}",
237                    value
238                )));
239            }
240            hex_decode(&value)?
241        } else {
242            return Err(Error::Protocol(format!(
243                "Missing SERVERNONCE mapping: {}",
244                line
245            )));
246        };
247
248        Ok(Self {
249            server_hash,
250            server_nonce,
251        })
252    }
253}
254
255/// Checks if a string contains exactly the expected number of hexadecimal digits.
256///
257/// # Arguments
258///
259/// * `s` - The string to check
260/// * `expected_len` - The expected length in characters
261///
262/// # Returns
263///
264/// `true` if the string has exactly `expected_len` characters and all are
265/// valid hexadecimal digits (0-9, a-f, A-F).
266fn is_hex_digits(s: &str, expected_len: usize) -> bool {
267    s.len() == expected_len && s.chars().all(|c| c.is_ascii_hexdigit())
268}
269
270/// Decodes a hexadecimal string into bytes.
271///
272/// # Arguments
273///
274/// * `s` - The hexadecimal string to decode (must have even length)
275///
276/// # Errors
277///
278/// Returns [`Error::Protocol`](crate::Error::Protocol) if:
279/// - The string has odd length
280/// - Any character is not a valid hexadecimal digit
281fn hex_decode(s: &str) -> Result<Vec<u8>, Error> {
282    if !s.len().is_multiple_of(2) {
283        return Err(Error::Protocol("invalid hex string length".to_string()));
284    }
285
286    (0..s.len())
287        .step_by(2)
288        .map(|i| {
289            u8::from_str_radix(&s[i..i + 2], 16)
290                .map_err(|_| Error::Protocol(format!("invalid hex character at position {}", i)))
291        })
292        .collect()
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    fn create_message(lines: Vec<&str>) -> ControlMessage {
300        let parsed: Vec<(String, char, Vec<u8>)> = lines
301            .iter()
302            .enumerate()
303            .map(|(i, line)| {
304                let divider = if i == lines.len() - 1 { ' ' } else { '-' };
305                ("250".to_string(), divider, line.as_bytes().to_vec())
306            })
307            .collect();
308        let raw = lines.join("\r\n");
309        ControlMessage::new(parsed, raw.into_bytes(), None).unwrap()
310    }
311
312    #[test]
313    fn test_authchallenge_basic() {
314        let msg = create_message(vec![
315            "AUTHCHALLENGE SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5 SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85",
316        ]);
317        let response = AuthChallengeResponse::from_message(&msg).unwrap();
318        assert_eq!(response.server_hash.len(), 32);
319        assert_eq!(response.server_nonce.len(), 32);
320    }
321
322    #[test]
323    fn test_authchallenge_missing_serverhash() {
324        let msg = create_message(vec![
325            "AUTHCHALLENGE SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85",
326        ]);
327        assert!(AuthChallengeResponse::from_message(&msg).is_err());
328    }
329
330    #[test]
331    fn test_authchallenge_missing_servernonce() {
332        let msg = create_message(vec![
333            "AUTHCHALLENGE SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5",
334        ]);
335        assert!(AuthChallengeResponse::from_message(&msg).is_err());
336    }
337
338    #[test]
339    fn test_authchallenge_invalid_hex() {
340        let msg = create_message(vec![
341            "AUTHCHALLENGE SERVERHASH=GHIJ73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5 SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85",
342        ]);
343        assert!(AuthChallengeResponse::from_message(&msg).is_err());
344    }
345
346    #[test]
347    fn test_authchallenge_wrong_length() {
348        let msg = create_message(vec![
349            "AUTHCHALLENGE SERVERHASH=680A73C9 SERVERNONCE=F8EA4B1F",
350        ]);
351        assert!(AuthChallengeResponse::from_message(&msg).is_err());
352    }
353
354    #[test]
355    fn test_authchallenge_not_authchallenge() {
356        let msg = create_message(vec![
357            "SOMETHING_ELSE SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5 SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85",
358        ]);
359        assert!(AuthChallengeResponse::from_message(&msg).is_err());
360    }
361
362    #[test]
363    fn test_hex_decode() {
364        assert_eq!(hex_decode("00FFAB").unwrap(), vec![0x00, 0xFF, 0xAB]);
365        assert_eq!(hex_decode("").unwrap(), vec![]);
366        assert_eq!(hex_decode("abcd").unwrap(), vec![0xAB, 0xCD]);
367    }
368
369    #[test]
370    fn test_is_hex_digits() {
371        assert!(is_hex_digits("0123456789ABCDEF", 16));
372        assert!(is_hex_digits("abcdef", 6));
373        assert!(!is_hex_digits("ghij", 4));
374        assert!(!is_hex_digits("abc", 4));
375    }
376
377    #[test]
378    fn test_authchallenge_valid_bytes_conversion() {
379        let msg = create_message(vec![
380            "AUTHCHALLENGE SERVERHASH=B16F72DACD4B5ED1531F3FCC04B593D46A1E30267E636EA7C7F8DD7A2B7BAA05 SERVERNONCE=653574272ABBB49395BD1060D642D653CFB7A2FCE6A4955BCFED819703A9998C",
381        ]);
382        let response = AuthChallengeResponse::from_message(&msg).unwrap();
383
384        assert_eq!(response.server_hash[0], 0xB1);
385        assert_eq!(response.server_hash[1], 0x6F);
386        assert_eq!(response.server_hash[2], 0x72);
387
388        assert_eq!(response.server_nonce[0], 0x65);
389        assert_eq!(response.server_nonce[1], 0x35);
390        assert_eq!(response.server_nonce[2], 0x74);
391    }
392
393    #[test]
394    fn test_authchallenge_multiline_error() {
395        let parsed = vec![
396            ("250".to_string(), '-', "AUTHCHALLENGE SERVERHASH=680A73C9836C4F557314EA1C4EDE54C285DB9DC89C83627401AEF9D7D27A95D5".as_bytes().to_vec()),
397            ("250".to_string(), ' ', "SERVERNONCE=F8EA4B1F2C8B40EF1AF68860171605B910E3BBCABADF6FC3DB1FA064F4690E85".as_bytes().to_vec()),
398        ];
399        let msg = ControlMessage::new(parsed, "multiline".into(), None).unwrap();
400        assert!(AuthChallengeResponse::from_message(&msg).is_err());
401    }
402
403    #[test]
404    fn test_authchallenge_lowercase_hex() {
405        let msg = create_message(vec![
406            "AUTHCHALLENGE SERVERHASH=b16f72dacd4b5ed1531f3fcc04b593d46a1e30267e636ea7c7f8dd7a2b7baa05 SERVERNONCE=653574272abbb49395bd1060d642d653cfb7a2fce6a4955bcfed819703a9998c",
407        ]);
408        let response = AuthChallengeResponse::from_message(&msg).unwrap();
409        assert_eq!(response.server_hash.len(), 32);
410        assert_eq!(response.server_nonce.len(), 32);
411    }
412
413    #[test]
414    fn test_authchallenge_not_ok_status() {
415        let parsed = vec![(
416            "515".to_string(),
417            ' ',
418            "Authentication required".as_bytes().to_vec(),
419        )];
420        let msg = ControlMessage::new(parsed, "515 Authentication required".into(), None).unwrap();
421        assert!(AuthChallengeResponse::from_message(&msg).is_err());
422    }
423}