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}