stem_rs/
auth.rs

1//! Authentication methods for Tor control protocol.
2//!
3//! This module provides functions for authenticating to Tor's control interface.
4//! All control connections must authenticate before they can be used, even if
5//! Tor hasn't been configured to require any authentication.
6//!
7//! # Overview
8//!
9//! Tor supports four authentication methods, tried in this order by [`authenticate`]:
10//!
11//! 1. **NONE** - No authentication required (open control port)
12//! 2. **SAFECOOKIE** - Challenge-response authentication with HMAC (preferred for local connections)
13//! 3. **COOKIE** - Cookie file authentication (fallback for older Tor versions)
14//! 4. **PASSWORD** - Password authentication using `HashedControlPassword`
15//!
16//! # Conceptual Role
17//!
18//! The authentication module handles the security handshake between a client and
19//! Tor's control interface. It queries Tor for supported authentication methods
20//! via [`get_protocol_info`], then attempts authentication using the most secure
21//! available method.
22//!
23//! # Security Considerations
24//!
25//! - **SAFECOOKIE** is preferred over **COOKIE** because it uses HMAC challenge-response,
26//!   preventing replay attacks where an attacker captures and reuses the cookie value.
27//! - Cookie files should have restrictive permissions (readable only by the Tor user).
28//! - Passwords are hex-encoded before transmission but are not encrypted on the wire.
29//! - The module uses constant-time comparison for cryptographic values to prevent
30//!   timing attacks.
31//!
32//! # Example
33//!
34//! ```rust,no_run
35//! use stem_rs::auth::{authenticate, get_protocol_info};
36//! use stem_rs::ControlSocket;
37//!
38//! # async fn example() -> Result<(), stem_rs::Error> {
39//! let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
40//!
41//! // Query available authentication methods
42//! let protocol_info = get_protocol_info(&mut socket).await?;
43//! println!("Tor version: {}", protocol_info.tor_version);
44//! println!("Auth methods: {:?}", protocol_info.auth_methods);
45//!
46//! // Authenticate (auto-detects best method)
47//! authenticate(&mut socket, None).await?;
48//! # Ok(())
49//! # }
50//! ```
51//!
52//! # This Compiles But Is Wrong
53//!
54//! ```rust,no_run
55//! use stem_rs::auth::authenticate;
56//! use stem_rs::ControlSocket;
57//!
58//! # async fn example() -> Result<(), stem_rs::Error> {
59//! let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
60//!
61//! // WRONG: Don't call authenticate twice on the same connection!
62//! // Tor may reject or disconnect after the first successful authentication.
63//! authenticate(&mut socket, None).await?;
64//! // authenticate(&mut socket, None).await?; // This would fail!
65//! # Ok(())
66//! # }
67//! ```
68//!
69//! # See Also
70//!
71//! - [`socket`](crate::socket): Low-level control socket communication
72//! - [`controller`](crate::controller): High-level Controller API (handles auth automatically)
73
74use std::path::{Path, PathBuf};
75
76use crate::protocol::ControlLine;
77use crate::socket::{ControlMessage, ControlSocket};
78use crate::version::Version;
79use crate::{AuthError, Error};
80
81/// HMAC key for server-to-controller hash in SAFECOOKIE authentication.
82const SAFECOOKIE_SERVER_TO_CONTROLLER: &[u8] =
83    b"Tor safe cookie authentication server-to-controller hash";
84
85/// HMAC key for controller-to-server hash in SAFECOOKIE authentication.
86const SAFECOOKIE_CONTROLLER_TO_SERVER: &[u8] =
87    b"Tor safe cookie authentication controller-to-server hash";
88
89/// Authentication methods supported by Tor's control protocol.
90///
91/// These methods are reported by Tor in response to a `PROTOCOLINFO` query.
92/// The [`authenticate`] function tries methods in order of security preference:
93/// NONE → SAFECOOKIE → COOKIE → PASSWORD.
94///
95/// # Security Comparison
96///
97/// | Method | Security Level | Use Case |
98/// |--------|---------------|----------|
99/// | [`None`](AuthMethod::None) | Lowest | Testing only, never in production |
100/// | [`Password`](AuthMethod::Password) | Medium | Remote access with strong password |
101/// | [`Cookie`](AuthMethod::Cookie) | High | Local access, older Tor versions |
102/// | [`SafeCookie`](AuthMethod::SafeCookie) | Highest | Local access, Tor 0.2.3+ |
103///
104/// # Example
105///
106/// ```rust
107/// use stem_rs::auth::AuthMethod;
108///
109/// let methods = vec![AuthMethod::Cookie, AuthMethod::SafeCookie];
110/// assert!(methods.contains(&AuthMethod::SafeCookie));
111/// ```
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub enum AuthMethod {
114    /// No authentication required.
115    ///
116    /// This method is available when Tor's control port is open without any
117    /// authentication configured. This is insecure and should only be used
118    /// for testing or when the control port is bound to localhost and the
119    /// system is trusted.
120    ///
121    /// Corresponds to `NULL` in the PROTOCOLINFO response.
122    None,
123
124    /// Password authentication using `HashedControlPassword`.
125    ///
126    /// Requires a password that matches the hash configured in Tor's
127    /// `HashedControlPassword` torrc option. The password is hex-encoded
128    /// before transmission.
129    ///
130    /// Corresponds to `HASHEDPASSWORD` in the PROTOCOLINFO response.
131    Password,
132
133    /// Cookie file authentication using `CookieAuthentication`.
134    ///
135    /// Authenticates by presenting the contents of Tor's authentication
136    /// cookie file (typically 32 bytes). The cookie path is provided in
137    /// the PROTOCOLINFO response.
138    ///
139    /// Corresponds to `COOKIE` in the PROTOCOLINFO response.
140    Cookie,
141
142    /// HMAC challenge-response authentication (Tor 0.2.3+).
143    ///
144    /// A more secure variant of cookie authentication that uses HMAC-SHA256
145    /// challenge-response to prevent replay attacks. The client sends a
146    /// random nonce, receives a server nonce and hash, verifies the server's
147    /// response, then sends its own hash.
148    ///
149    /// Corresponds to `SAFECOOKIE` in the PROTOCOLINFO response.
150    SafeCookie,
151}
152
153impl AuthMethod {
154    /// Parses an authentication method from its PROTOCOLINFO string representation.
155    ///
156    /// # Arguments
157    ///
158    /// * `s` - The method string from PROTOCOLINFO (case-insensitive)
159    ///
160    /// # Returns
161    ///
162    /// `Some(AuthMethod)` if recognized, `None` for unknown methods.
163    ///
164    /// # Examples
165    ///
166    /// ```rust
167    /// use stem_rs::auth::AuthMethod;
168    ///
169    /// assert_eq!(AuthMethod::parse("NULL"), Some(AuthMethod::None));
170    /// assert_eq!(AuthMethod::parse("HASHEDPASSWORD"), Some(AuthMethod::Password));
171    /// assert_eq!(AuthMethod::parse("cookie"), Some(AuthMethod::Cookie));
172    /// assert_eq!(AuthMethod::parse("UNKNOWN"), None);
173    /// ```
174    pub fn parse(s: &str) -> Option<Self> {
175        match s.to_uppercase().as_str() {
176            "NULL" => Some(AuthMethod::None),
177            "HASHEDPASSWORD" => Some(AuthMethod::Password),
178            "COOKIE" => Some(AuthMethod::Cookie),
179            "SAFECOOKIE" => Some(AuthMethod::SafeCookie),
180            _ => None,
181        }
182    }
183}
184
185/// Information about Tor's control protocol and authentication requirements.
186///
187/// This struct contains the response from a `PROTOCOLINFO` query, which must
188/// be issued before authentication. It provides:
189///
190/// - The protocol version supported by Tor
191/// - The Tor software version
192/// - Available authentication methods
193/// - The path to the authentication cookie (if cookie auth is available)
194///
195/// # Invariants
196///
197/// - `protocol_version` is typically 1 for all current Tor versions
198/// - `cookie_path` is `Some` only when [`AuthMethod::Cookie`] or
199///   [`AuthMethod::SafeCookie`] is in `auth_methods`
200/// - `auth_methods` may be empty if Tor is misconfigured
201///
202/// # Example
203///
204/// ```rust,no_run
205/// use stem_rs::auth::{get_protocol_info, AuthMethod};
206/// use stem_rs::ControlSocket;
207///
208/// # async fn example() -> Result<(), stem_rs::Error> {
209/// let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
210/// let info = get_protocol_info(&mut socket).await?;
211///
212/// println!("Protocol version: {}", info.protocol_version);
213/// println!("Tor version: {}", info.tor_version);
214///
215/// if info.auth_methods.contains(&AuthMethod::SafeCookie) {
216///     println!("SafeCookie auth available at: {:?}", info.cookie_path);
217/// }
218/// # Ok(())
219/// # }
220/// ```
221#[derive(Debug, Clone)]
222pub struct ProtocolInfo {
223    /// The control protocol version (typically 1).
224    ///
225    /// This indicates the version of the control protocol that Tor speaks.
226    /// Currently, only version 1 is defined.
227    pub protocol_version: u32,
228
229    /// The version of the Tor software.
230    ///
231    /// This can be used to check for feature availability or known bugs
232    /// in specific Tor versions.
233    pub tor_version: Version,
234
235    /// Authentication methods accepted by this Tor instance.
236    ///
237    /// The methods are listed in the order they appear in the PROTOCOLINFO
238    /// response. Use [`authenticate`] to automatically select the best method.
239    pub auth_methods: Vec<AuthMethod>,
240
241    /// Path to the authentication cookie file, if available.
242    ///
243    /// This is `Some` when cookie-based authentication ([`AuthMethod::Cookie`]
244    /// or [`AuthMethod::SafeCookie`]) is available. The path may be absolute
245    /// or relative to Tor's data directory.
246    ///
247    /// # Security Note
248    ///
249    /// The cookie file should be readable only by the user running Tor.
250    /// If you're in a chroot environment, you may need to adjust this path.
251    pub cookie_path: Option<PathBuf>,
252}
253
254impl ProtocolInfo {
255    /// Parses a PROTOCOLINFO response from Tor.
256    ///
257    /// This function extracts authentication information from a raw control
258    /// protocol response. It handles the multi-line PROTOCOLINFO format and
259    /// extracts all relevant fields.
260    ///
261    /// # Arguments
262    ///
263    /// * `message` - The control message containing the PROTOCOLINFO response
264    ///
265    /// # Returns
266    ///
267    /// A `ProtocolInfo` struct with the parsed information.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`Error::Protocol`] if:
272    /// - The response status code indicates failure
273    /// - The response format is malformed
274    ///
275    /// # Example
276    ///
277    /// ```rust
278    /// use stem_rs::auth::{ProtocolInfo, AuthMethod};
279    /// use stem_rs::socket::ControlMessage;
280    ///
281    /// let message = ControlMessage {
282    ///     status_code: 250,
283    ///     lines: vec![
284    ///         "PROTOCOLINFO 1".to_string(),
285    ///         "AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/var/run/tor/control.authcookie\"".to_string(),
286    ///         "VERSION Tor=\"0.4.7.1\"".to_string(),
287    ///         "OK".to_string(),
288    ///     ],
289    /// };
290    ///
291    /// let info = ProtocolInfo::parse(&message).unwrap();
292    /// assert_eq!(info.protocol_version, 1);
293    /// assert!(info.auth_methods.contains(&AuthMethod::Cookie));
294    /// ```
295    pub fn parse(message: &ControlMessage) -> Result<Self, Error> {
296        if !message.is_ok() {
297            return Err(Error::Protocol(format!(
298                "PROTOCOLINFO failed: {} {}",
299                message.status_code,
300                message.content()
301            )));
302        }
303
304        let mut protocol_version: Option<u32> = None;
305        let mut tor_version: Option<Version> = None;
306        let mut auth_methods: Vec<AuthMethod> = Vec::new();
307        let mut cookie_path: Option<PathBuf> = None;
308
309        for line_content in &message.lines {
310            let line = ControlLine::new(line_content);
311            if line.is_empty() {
312                continue;
313            }
314
315            if line_content.starts_with("PROTOCOLINFO ") {
316                let version_str = line_content.strip_prefix("PROTOCOLINFO ").unwrap_or("1");
317                protocol_version = version_str.trim().parse().ok();
318            } else if line_content.starts_with("AUTH ") {
319                let auth_part = line_content.strip_prefix("AUTH ").unwrap_or("");
320                let mut auth_line = ControlLine::new(auth_part);
321
322                while !auth_line.is_empty() {
323                    if auth_line.is_next_mapping(Some("METHODS"), false) {
324                        let (_, methods_str) = auth_line.pop_mapping(false, false)?;
325                        for method in methods_str.split(',') {
326                            if let Some(m) = AuthMethod::parse(method.trim()) {
327                                if !auth_methods.contains(&m) {
328                                    auth_methods.push(m);
329                                }
330                            }
331                        }
332                    } else if auth_line.is_next_mapping(Some("COOKIEFILE"), false) {
333                        let (_, path_str) = auth_line.pop_mapping(true, true)?;
334                        cookie_path = Some(PathBuf::from(path_str));
335                    } else {
336                        let _ = auth_line.pop(false, false);
337                    }
338                }
339            } else if line_content.starts_with("VERSION ") {
340                let version_part = line_content.strip_prefix("VERSION ").unwrap_or("");
341                let mut version_line = ControlLine::new(version_part);
342
343                while !version_line.is_empty() {
344                    if version_line.is_next_mapping(Some("Tor"), false) {
345                        let (_, ver_str) = version_line.pop_mapping(true, true)?;
346                        tor_version = Version::parse(&ver_str).ok();
347                    } else {
348                        let _ = version_line.pop(false, false);
349                    }
350                }
351            }
352        }
353
354        Ok(ProtocolInfo {
355            protocol_version: protocol_version.unwrap_or(1),
356            tor_version: tor_version.unwrap_or_else(|| Version::new(0, 0, 0)),
357            auth_methods,
358            cookie_path,
359        })
360    }
361}
362
363/// Queries Tor for protocol and authentication information.
364///
365/// Issues a `PROTOCOLINFO 1` command to the control socket and parses the
366/// response. This must be called before authentication to determine which
367/// authentication methods are available.
368///
369/// # Preconditions
370///
371/// - The socket must be connected but not yet authenticated
372/// - No prior commands should have been sent on this connection
373///
374/// # Postconditions
375///
376/// - On success: Returns protocol information including available auth methods
377/// - On failure: The socket state is undefined; reconnection may be required
378///
379/// # Arguments
380///
381/// * `socket` - A connected control socket
382///
383/// # Errors
384///
385/// Returns an error if:
386/// - [`Error::Socket`](crate::Error::Socket): Connection failed or was closed
387/// - [`Error::Protocol`](crate::Error::Protocol): Response was malformed or indicated failure
388///
389/// # Example
390///
391/// ```rust,no_run
392/// use stem_rs::auth::get_protocol_info;
393/// use stem_rs::ControlSocket;
394///
395/// # async fn example() -> Result<(), stem_rs::Error> {
396/// let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
397/// let info = get_protocol_info(&mut socket).await?;
398///
399/// println!("Connected to Tor {}", info.tor_version);
400/// for method in &info.auth_methods {
401///     println!("  Auth method: {:?}", method);
402/// }
403/// # Ok(())
404/// # }
405/// ```
406///
407/// # Protocol Details
408///
409/// The PROTOCOLINFO command returns a multi-line response:
410/// ```text
411/// 250-PROTOCOLINFO 1
412/// 250-AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE="/path/to/cookie"
413/// 250-VERSION Tor="0.4.7.1"
414/// 250 OK
415/// ```
416pub async fn get_protocol_info(socket: &mut ControlSocket) -> Result<ProtocolInfo, Error> {
417    socket.send("PROTOCOLINFO 1").await?;
418    let response = socket.recv().await?;
419    ProtocolInfo::parse(&response)
420}
421
422/// Authenticates to Tor using the best available method.
423///
424/// This function queries Tor for supported authentication methods via
425/// [`get_protocol_info`], then attempts authentication using the most secure
426/// available method in this order:
427///
428/// 1. **NONE** - If no authentication is required
429/// 2. **SAFECOOKIE** - HMAC challenge-response (most secure)
430/// 3. **COOKIE** - Cookie file contents
431/// 4. **PASSWORD** - If a password is provided
432///
433/// # Preconditions
434///
435/// - The socket must be connected but not yet authenticated
436/// - For password authentication, `password` must be `Some`
437///
438/// # Postconditions
439///
440/// - On success: The socket is authenticated and ready for commands
441/// - On failure: The socket state is undefined; reconnection is recommended
442///
443/// # Arguments
444///
445/// * `socket` - A connected control socket
446/// * `password` - Optional password for PASSWORD authentication
447///
448/// # Errors
449///
450/// Returns [`Error::Authentication`] with specific [`AuthError`] variants:
451///
452/// - [`AuthError::NoMethods`](crate::AuthError::NoMethods): No compatible auth methods available
453/// - [`AuthError::MissingPassword`](crate::AuthError::MissingPassword): PASSWORD auth required but no password provided
454/// - [`AuthError::IncorrectPassword`](crate::AuthError::IncorrectPassword): PASSWORD auth failed
455/// - [`AuthError::CookieUnreadable`](crate::AuthError::CookieUnreadable): Cannot read cookie file
456/// - [`AuthError::IncorrectCookie`](crate::AuthError::IncorrectCookie): COOKIE auth failed
457/// - [`AuthError::ChallengeFailed`](crate::AuthError::ChallengeFailed): SAFECOOKIE challenge failed
458///
459/// # Example
460///
461/// ```rust,no_run
462/// use stem_rs::auth::authenticate;
463/// use stem_rs::ControlSocket;
464///
465/// # async fn example() -> Result<(), stem_rs::Error> {
466/// let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
467///
468/// // Auto-detect authentication method (no password)
469/// authenticate(&mut socket, None).await?;
470///
471/// // Or with a password
472/// // authenticate(&mut socket, Some("my_password")).await?;
473/// # Ok(())
474/// # }
475/// ```
476///
477/// # Security
478///
479/// - Passwords are hex-encoded before transmission but not encrypted
480/// - Cookie comparison uses constant-time algorithm to prevent timing attacks
481/// - SAFECOOKIE nonces are cryptographically random (32 bytes)
482pub async fn authenticate(socket: &mut ControlSocket, password: Option<&str>) -> Result<(), Error> {
483    let protocol_info = get_protocol_info(socket).await?;
484
485    if protocol_info.auth_methods.is_empty() {
486        return Err(Error::Authentication(AuthError::NoMethods));
487    }
488
489    if protocol_info.auth_methods.contains(&AuthMethod::None) {
490        return authenticate_none(socket).await;
491    }
492
493    if protocol_info.auth_methods.contains(&AuthMethod::SafeCookie) {
494        if let Some(ref cookie_path) = protocol_info.cookie_path {
495            if authenticate_safecookie(socket, cookie_path).await.is_ok() {
496                return Ok(());
497            }
498        }
499    }
500
501    if protocol_info.auth_methods.contains(&AuthMethod::Cookie) {
502        if let Some(ref cookie_path) = protocol_info.cookie_path {
503            if authenticate_cookie(socket, cookie_path).await.is_ok() {
504                return Ok(());
505            }
506        }
507    }
508
509    if protocol_info.auth_methods.contains(&AuthMethod::Password) {
510        if let Some(pw) = password {
511            return authenticate_password(socket, pw).await;
512        }
513        return Err(Error::Authentication(AuthError::MissingPassword));
514    }
515
516    Err(Error::Authentication(AuthError::NoMethods))
517}
518
519/// Authenticates to an open control socket (no credentials required).
520///
521/// This function sends an empty `AUTHENTICATE` command, which succeeds when
522/// Tor's control port is configured without any authentication requirements.
523///
524/// # Preconditions
525///
526/// - The socket must be connected
527/// - Tor must be configured with no authentication (NULL method)
528///
529/// # Postconditions
530///
531/// - On success: The socket is authenticated
532/// - On failure: The socket may be disconnected by Tor
533///
534/// # Arguments
535///
536/// * `socket` - A connected control socket
537///
538/// # Errors
539///
540/// Returns [`Error::Authentication`] with [`AuthError::SecurityFailure`] if
541/// Tor rejects the authentication attempt.
542///
543/// # Security Warning
544///
545/// Using NONE authentication is insecure and should only be used for:
546/// - Local testing environments
547/// - Trusted localhost connections
548/// - Development purposes
549///
550/// Never use NONE authentication in production or when the control port
551/// is accessible from untrusted networks.
552///
553/// # Example
554///
555/// ```rust,no_run
556/// use stem_rs::auth::authenticate_none;
557/// use stem_rs::ControlSocket;
558///
559/// # async fn example() -> Result<(), stem_rs::Error> {
560/// let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
561/// authenticate_none(&mut socket).await?;
562/// // Socket is now authenticated
563/// # Ok(())
564/// # }
565/// ```
566pub async fn authenticate_none(socket: &mut ControlSocket) -> Result<(), Error> {
567    socket.send("AUTHENTICATE").await?;
568    let response = socket.recv().await?;
569
570    if response.is_ok() {
571        Ok(())
572    } else {
573        Err(Error::Authentication(AuthError::SecurityFailure))
574    }
575}
576
577/// Authenticates using a password.
578///
579/// Sends an `AUTHENTICATE` command with the password hex-encoded. The password
580/// must match the hash configured in Tor's `HashedControlPassword` torrc option.
581///
582/// # Preconditions
583///
584/// - The socket must be connected
585/// - Tor must be configured with `HashedControlPassword`
586/// - The password must match the configured hash
587///
588/// # Postconditions
589///
590/// - On success: The socket is authenticated
591/// - On failure: Tor may disconnect the socket
592///
593/// # Arguments
594///
595/// * `socket` - A connected control socket
596/// * `password` - The plaintext password to authenticate with
597///
598/// # Errors
599///
600/// Returns [`Error::Authentication`] with [`AuthError::IncorrectPassword`] if
601/// the password doesn't match the configured hash.
602///
603/// # Security Considerations
604///
605/// - The password is hex-encoded but transmitted in cleartext over the socket
606/// - For TCP connections, consider using a secure tunnel or localhost only
607/// - Unix domain sockets provide better security for local connections
608/// - The password is not stored after the function returns
609///
610/// # Example
611///
612/// ```rust,no_run
613/// use stem_rs::auth::authenticate_password;
614/// use stem_rs::ControlSocket;
615///
616/// # async fn example() -> Result<(), stem_rs::Error> {
617/// let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
618/// authenticate_password(&mut socket, "my_secure_password").await?;
619/// # Ok(())
620/// # }
621/// ```
622///
623/// # Protocol Details
624///
625/// The password is hex-encoded before sending:
626/// ```text
627/// AUTHENTICATE 6D795F7061737377 (hex of "my_passw")
628/// ```
629pub async fn authenticate_password(
630    socket: &mut ControlSocket,
631    password: &str,
632) -> Result<(), Error> {
633    let hex_password = hex_encode(password.as_bytes());
634    let command = format!("AUTHENTICATE {}", hex_password);
635    socket.send(&command).await?;
636    let response = socket.recv().await?;
637
638    if response.is_ok() {
639        Ok(())
640    } else {
641        Err(Error::Authentication(AuthError::IncorrectPassword))
642    }
643}
644
645/// Authenticates using a cookie file.
646///
647/// Reads the authentication cookie from the specified path and sends its
648/// contents (hex-encoded) to Tor. The cookie file is typically 32 bytes
649/// and is created by Tor when `CookieAuthentication` is enabled.
650///
651/// # Preconditions
652///
653/// - The socket must be connected
654/// - Tor must be configured with `CookieAuthentication 1`
655/// - The cookie file must exist and be readable
656/// - The cookie file must be exactly 32 bytes
657///
658/// # Postconditions
659///
660/// - On success: The socket is authenticated
661/// - On failure: Tor may disconnect the socket
662///
663/// # Arguments
664///
665/// * `socket` - A connected control socket
666/// * `path` - Path to the authentication cookie file
667///
668/// # Errors
669///
670/// Returns [`Error::Authentication`] with:
671/// - [`AuthError::CookieUnreadable`]: Cannot read the cookie file (permissions, not found)
672/// - [`AuthError::IncorrectCookieSize`]: Cookie file is not 32 bytes
673/// - [`AuthError::IncorrectCookie`]: Cookie value was rejected by Tor
674///
675/// # Security Considerations
676///
677/// - The cookie file should have restrictive permissions (e.g., 0600)
678/// - Only the user running Tor should be able to read the cookie
679/// - The cookie is transmitted in cleartext (hex-encoded) over the socket
680/// - Consider using [`authenticate_safecookie`] for better security
681///
682/// # Example
683///
684/// ```rust,no_run
685/// use stem_rs::auth::authenticate_cookie;
686/// use stem_rs::ControlSocket;
687/// use std::path::Path;
688///
689/// # async fn example() -> Result<(), stem_rs::Error> {
690/// let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
691/// let cookie_path = Path::new("/var/run/tor/control.authcookie");
692/// authenticate_cookie(&mut socket, cookie_path).await?;
693/// # Ok(())
694/// # }
695/// ```
696///
697/// # Why 32 Bytes?
698///
699/// The cookie size is validated to prevent a malicious Tor instance from
700/// tricking the client into reading arbitrary files. Without this check,
701/// an attacker could claim that `~/.ssh/id_rsa` is the cookie file.
702pub async fn authenticate_cookie(socket: &mut ControlSocket, path: &Path) -> Result<(), Error> {
703    let cookie = read_cookie_file(path)?;
704    let hex_cookie = hex_encode(&cookie);
705    let command = format!("AUTHENTICATE {}", hex_cookie);
706    socket.send(&command).await?;
707    let response = socket.recv().await?;
708
709    if response.is_ok() {
710        Ok(())
711    } else {
712        Err(Error::Authentication(AuthError::IncorrectCookie))
713    }
714}
715
716/// Authenticates using the SAFECOOKIE challenge-response protocol.
717///
718/// This is the most secure cookie-based authentication method, available in
719/// Tor 0.2.3+. It uses HMAC-SHA256 challenge-response to prevent replay attacks
720/// where an attacker captures and reuses the cookie value.
721///
722/// # Protocol Steps
723///
724/// 1. Client generates a random 32-byte nonce
725/// 2. Client sends `AUTHCHALLENGE SAFECOOKIE <client_nonce>`
726/// 3. Server responds with `SERVERHASH` and `SERVERNONCE`
727/// 4. Client verifies `SERVERHASH` using HMAC-SHA256
728/// 5. Client computes its own hash and sends `AUTHENTICATE <client_hash>`
729///
730/// # Preconditions
731///
732/// - The socket must be connected
733/// - Tor must support SAFECOOKIE (version 0.2.3+)
734/// - The cookie file must exist and be readable
735/// - The cookie file must be exactly 32 bytes
736///
737/// # Postconditions
738///
739/// - On success: The socket is authenticated
740/// - On failure: Tor may disconnect the socket
741///
742/// # Arguments
743///
744/// * `socket` - A connected control socket
745/// * `path` - Path to the authentication cookie file
746///
747/// # Errors
748///
749/// Returns [`Error::Authentication`] with:
750/// - [`AuthError::CookieUnreadable`]: Cannot read the cookie file
751/// - [`AuthError::IncorrectCookieSize`]: Cookie file is not 32 bytes
752/// - [`AuthError::ChallengeUnsupported`]: Tor doesn't support AUTHCHALLENGE
753/// - [`AuthError::ChallengeFailed`]: Server hash verification failed or auth rejected
754///
755/// # Security Advantages
756///
757/// Unlike plain cookie authentication, SAFECOOKIE:
758/// - Prevents replay attacks (nonces are unique per session)
759/// - Provides mutual authentication (client verifies server)
760/// - Uses constant-time comparison for cryptographic values
761///
762/// # Example
763///
764/// ```rust,no_run
765/// use stem_rs::auth::authenticate_safecookie;
766/// use stem_rs::ControlSocket;
767/// use std::path::Path;
768///
769/// # async fn example() -> Result<(), stem_rs::Error> {
770/// let mut socket = ControlSocket::connect_port("127.0.0.1:9051".parse()?).await?;
771/// let cookie_path = Path::new("/var/run/tor/control.authcookie");
772/// authenticate_safecookie(&mut socket, cookie_path).await?;
773/// # Ok(())
774/// # }
775/// ```
776///
777/// # HMAC Details
778///
779/// The HMAC keys are fixed strings defined by the Tor specification:
780/// - Server hash: `"Tor safe cookie authentication server-to-controller hash"`
781/// - Client hash: `"Tor safe cookie authentication controller-to-server hash"`
782pub async fn authenticate_safecookie(socket: &mut ControlSocket, path: &Path) -> Result<(), Error> {
783    let cookie = read_cookie_file(path)?;
784    let client_nonce = generate_nonce();
785    let client_nonce_hex = hex_encode(&client_nonce);
786
787    let challenge_command = format!("AUTHCHALLENGE SAFECOOKIE {}", client_nonce_hex);
788    socket.send(&challenge_command).await?;
789    let response = socket.recv().await?;
790
791    if !response.is_ok() {
792        return Err(Error::Authentication(AuthError::ChallengeUnsupported));
793    }
794
795    let (server_hash, server_nonce) = parse_authchallenge_response(&response)?;
796    let expected_server_hash = compute_hmac(
797        SAFECOOKIE_SERVER_TO_CONTROLLER,
798        &cookie,
799        &client_nonce,
800        &server_nonce,
801    );
802
803    if !crate::util::secure_compare(&server_hash, &expected_server_hash) {
804        return Err(Error::Authentication(AuthError::ChallengeFailed));
805    }
806
807    let client_hash = compute_hmac(
808        SAFECOOKIE_CONTROLLER_TO_SERVER,
809        &cookie,
810        &client_nonce,
811        &server_nonce,
812    );
813
814    let auth_command = format!("AUTHENTICATE {}", hex_encode(&client_hash));
815    socket.send(&auth_command).await?;
816    let auth_response = socket.recv().await?;
817
818    if auth_response.is_ok() {
819        Ok(())
820    } else {
821        Err(Error::Authentication(AuthError::ChallengeFailed))
822    }
823}
824
825/// Reads and validates an authentication cookie file.
826///
827/// # Security
828///
829/// The cookie size is validated to exactly 32 bytes to prevent a malicious
830/// server from tricking the client into reading arbitrary files.
831fn read_cookie_file(path: &Path) -> Result<Vec<u8>, Error> {
832    let cookie = std::fs::read(path).map_err(|e| {
833        Error::Authentication(AuthError::CookieUnreadable(format!(
834            "{}: {}",
835            path.display(),
836            e
837        )))
838    })?;
839
840    if cookie.len() != 32 {
841        return Err(Error::Authentication(AuthError::IncorrectCookieSize));
842    }
843
844    Ok(cookie)
845}
846
847/// Generates a cryptographically secure random 32-byte nonce.
848fn generate_nonce() -> [u8; 32] {
849    let mut nonce = [0u8; 32];
850    getrandom::fill(&mut nonce).expect("failed to generate random nonce");
851    nonce
852}
853
854/// Parses an AUTHCHALLENGE response to extract server hash and nonce.
855fn parse_authchallenge_response(response: &ControlMessage) -> Result<(Vec<u8>, Vec<u8>), Error> {
856    let content = response.content();
857    let mut line = ControlLine::new(content);
858
859    if !line.is_empty() {
860        let first = line.pop(false, false)?;
861        if first != "AUTHCHALLENGE" {
862            return Err(Error::Protocol(
863                "expected AUTHCHALLENGE response".to_string(),
864            ));
865        }
866    }
867
868    let mut server_hash: Option<Vec<u8>> = None;
869    let mut server_nonce: Option<Vec<u8>> = None;
870
871    while !line.is_empty() {
872        if line.is_next_mapping(Some("SERVERHASH"), false) {
873            let (_, hash_hex) = line.pop_mapping(false, false)?;
874            server_hash = Some(hex_decode(&hash_hex)?);
875        } else if line.is_next_mapping(Some("SERVERNONCE"), false) {
876            let (_, nonce_hex) = line.pop_mapping(false, false)?;
877            server_nonce = Some(hex_decode(&nonce_hex)?);
878        } else {
879            let _ = line.pop(false, false);
880        }
881    }
882
883    let server_hash =
884        server_hash.ok_or_else(|| Error::Protocol("missing SERVERHASH".to_string()))?;
885    let server_nonce =
886        server_nonce.ok_or_else(|| Error::Protocol("missing SERVERNONCE".to_string()))?;
887
888    Ok((server_hash, server_nonce))
889}
890
891/// Computes HMAC-SHA256 for SAFECOOKIE authentication.
892///
893/// The HMAC is computed over: cookie || client_nonce || server_nonce
894fn compute_hmac(
895    key_prefix: &[u8],
896    cookie: &[u8],
897    client_nonce: &[u8],
898    server_nonce: &[u8],
899) -> Vec<u8> {
900    use hmac::{Hmac, Mac};
901    use sha2::Sha256;
902
903    type HmacSha256 = Hmac<Sha256>;
904
905    let mut mac = HmacSha256::new_from_slice(key_prefix).expect("HMAC can take key of any size");
906    mac.update(cookie);
907    mac.update(client_nonce);
908    mac.update(server_nonce);
909    mac.finalize().into_bytes().to_vec()
910}
911
912/// Encodes bytes as uppercase hexadecimal string.
913fn hex_encode(data: &[u8]) -> String {
914    data.iter().map(|b| format!("{:02X}", b)).collect()
915}
916
917/// Decodes a hexadecimal string to bytes.
918fn hex_decode(s: &str) -> Result<Vec<u8>, Error> {
919    if !s.len().is_multiple_of(2) {
920        return Err(Error::Protocol("invalid hex string length".to_string()));
921    }
922
923    (0..s.len())
924        .step_by(2)
925        .map(|i| {
926            u8::from_str_radix(&s[i..i + 2], 16)
927                .map_err(|_| Error::Protocol(format!("invalid hex character at position {}", i)))
928        })
929        .collect()
930}
931
932#[cfg(test)]
933mod tests {
934    use super::*;
935
936    #[test]
937    fn test_auth_method_from_str() {
938        assert_eq!(AuthMethod::parse("NULL"), Some(AuthMethod::None));
939        assert_eq!(
940            AuthMethod::parse("HASHEDPASSWORD"),
941            Some(AuthMethod::Password)
942        );
943        assert_eq!(AuthMethod::parse("COOKIE"), Some(AuthMethod::Cookie));
944        assert_eq!(
945            AuthMethod::parse("SAFECOOKIE"),
946            Some(AuthMethod::SafeCookie)
947        );
948        assert_eq!(AuthMethod::parse("null"), Some(AuthMethod::None));
949        assert_eq!(AuthMethod::parse("UNKNOWN"), None);
950    }
951
952    #[test]
953    fn test_hex_encode() {
954        assert_eq!(hex_encode(&[0x00, 0xFF, 0xAB]), "00FFAB");
955        assert_eq!(hex_encode(&[]), "");
956        assert_eq!(hex_encode(&[0x12, 0x34]), "1234");
957    }
958
959    #[test]
960    fn test_hex_decode() {
961        assert_eq!(hex_decode("00FFAB").unwrap(), vec![0x00, 0xFF, 0xAB]);
962        assert_eq!(hex_decode("").unwrap(), vec![]);
963        assert_eq!(hex_decode("1234").unwrap(), vec![0x12, 0x34]);
964        assert_eq!(hex_decode("abcd").unwrap(), vec![0xAB, 0xCD]);
965    }
966
967    #[test]
968    fn test_hex_decode_invalid() {
969        assert!(hex_decode("123").is_err());
970        assert!(hex_decode("GHIJ").is_err());
971    }
972
973    #[test]
974    fn test_protocol_info_parse_simple() {
975        let message = ControlMessage {
976            status_code: 250,
977            lines: vec![
978                "PROTOCOLINFO 1".to_string(),
979                "AUTH METHODS=NULL".to_string(),
980                "VERSION Tor=\"0.4.7.1\"".to_string(),
981                "OK".to_string(),
982            ],
983        };
984
985        let info = ProtocolInfo::parse(&message).unwrap();
986        assert_eq!(info.protocol_version, 1);
987        assert_eq!(info.tor_version, Version::parse("0.4.7.1").unwrap());
988        assert_eq!(info.auth_methods, vec![AuthMethod::None]);
989        assert!(info.cookie_path.is_none());
990    }
991
992    #[test]
993    fn test_protocol_info_parse_with_cookie() {
994        let message = ControlMessage {
995            status_code: 250,
996            lines: vec![
997                "PROTOCOLINFO 1".to_string(),
998                "AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/var/run/tor/control.authcookie\""
999                    .to_string(),
1000                "VERSION Tor=\"0.4.8.0\"".to_string(),
1001                "OK".to_string(),
1002            ],
1003        };
1004
1005        let info = ProtocolInfo::parse(&message).unwrap();
1006        assert_eq!(info.protocol_version, 1);
1007        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1008        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1009        assert_eq!(
1010            info.cookie_path,
1011            Some(PathBuf::from("/var/run/tor/control.authcookie"))
1012        );
1013    }
1014
1015    #[test]
1016    fn test_protocol_info_parse_password() {
1017        let message = ControlMessage {
1018            status_code: 250,
1019            lines: vec![
1020                "PROTOCOLINFO 1".to_string(),
1021                "AUTH METHODS=HASHEDPASSWORD".to_string(),
1022                "VERSION Tor=\"0.4.7.1\"".to_string(),
1023                "OK".to_string(),
1024            ],
1025        };
1026
1027        let info = ProtocolInfo::parse(&message).unwrap();
1028        assert_eq!(info.auth_methods, vec![AuthMethod::Password]);
1029    }
1030
1031    #[test]
1032    fn test_protocol_info_parse_multiple_methods() {
1033        let message = ControlMessage {
1034            status_code: 250,
1035            lines: vec![
1036                "PROTOCOLINFO 1".to_string(),
1037                "AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE=\"/tmp/cookie\""
1038                    .to_string(),
1039                "VERSION Tor=\"0.4.7.1\"".to_string(),
1040                "OK".to_string(),
1041            ],
1042        };
1043
1044        let info = ProtocolInfo::parse(&message).unwrap();
1045        assert_eq!(info.auth_methods.len(), 3);
1046        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1047        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1048        assert!(info.auth_methods.contains(&AuthMethod::Password));
1049    }
1050
1051    #[test]
1052    fn test_protocol_info_parse_error() {
1053        let message = ControlMessage {
1054            status_code: 515,
1055            lines: vec!["Authentication required".to_string()],
1056        };
1057
1058        assert!(ProtocolInfo::parse(&message).is_err());
1059    }
1060
1061    #[test]
1062    fn test_parse_authchallenge_response() {
1063        let message = ControlMessage {
1064            status_code: 250,
1065            lines: vec![
1066                "AUTHCHALLENGE SERVERHASH=ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234 SERVERNONCE=1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF".to_string(),
1067            ],
1068        };
1069
1070        let (server_hash, server_nonce) = parse_authchallenge_response(&message).unwrap();
1071        assert_eq!(server_hash.len(), 32);
1072        assert_eq!(server_nonce.len(), 32);
1073    }
1074
1075    #[test]
1076    fn test_protocol_info_parse_empty_methods() {
1077        let message = ControlMessage {
1078            status_code: 250,
1079            lines: vec![
1080                "PROTOCOLINFO 1".to_string(),
1081                "AUTH METHODS=NULL".to_string(),
1082                "VERSION Tor=\"0.4.7.1\"".to_string(),
1083                "OK".to_string(),
1084            ],
1085        };
1086
1087        let info = ProtocolInfo::parse(&message).unwrap();
1088        assert!(info.auth_methods.contains(&AuthMethod::None));
1089    }
1090
1091    #[test]
1092    fn test_protocol_info_parse_version_without_quotes() {
1093        let message = ControlMessage {
1094            status_code: 250,
1095            lines: vec![
1096                "PROTOCOLINFO 1".to_string(),
1097                "AUTH METHODS=NULL".to_string(),
1098                "VERSION Tor=\"0.4.8.0-alpha-dev\"".to_string(),
1099                "OK".to_string(),
1100            ],
1101        };
1102
1103        let info = ProtocolInfo::parse(&message).unwrap();
1104        assert_eq!(info.tor_version.major, 0);
1105        assert_eq!(info.tor_version.minor, 4);
1106        assert_eq!(info.tor_version.micro, 8);
1107    }
1108
1109    #[test]
1110    fn test_protocol_info_no_auth() {
1111        let message = ControlMessage {
1112            status_code: 250,
1113            lines: vec![
1114                "PROTOCOLINFO 1".to_string(),
1115                "AUTH METHODS=NULL".to_string(),
1116                "VERSION Tor=\"0.2.1.30\"".to_string(),
1117                "OK".to_string(),
1118            ],
1119        };
1120
1121        let info = ProtocolInfo::parse(&message).unwrap();
1122        assert_eq!(info.protocol_version, 1);
1123        assert_eq!(info.tor_version, Version::parse("0.2.1.30").unwrap());
1124        assert_eq!(info.auth_methods, vec![AuthMethod::None]);
1125        assert!(info.cookie_path.is_none());
1126    }
1127
1128    #[test]
1129    fn test_protocol_info_password_auth() {
1130        let message = ControlMessage {
1131            status_code: 250,
1132            lines: vec![
1133                "PROTOCOLINFO 1".to_string(),
1134                "AUTH METHODS=HASHEDPASSWORD".to_string(),
1135                "VERSION Tor=\"0.2.1.30\"".to_string(),
1136                "OK".to_string(),
1137            ],
1138        };
1139
1140        let info = ProtocolInfo::parse(&message).unwrap();
1141        assert_eq!(info.auth_methods, vec![AuthMethod::Password]);
1142    }
1143
1144    #[test]
1145    fn test_protocol_info_cookie_auth_with_escape() {
1146        let message = ControlMessage {
1147            status_code: 250,
1148            lines: vec![
1149                "PROTOCOLINFO 1".to_string(),
1150                "AUTH METHODS=COOKIE COOKIEFILE=\"/tmp/my data\\\\\\\"dir//control_auth_cookie\""
1151                    .to_string(),
1152                "VERSION Tor=\"0.2.1.30\"".to_string(),
1153                "OK".to_string(),
1154            ],
1155        };
1156
1157        let info = ProtocolInfo::parse(&message).unwrap();
1158        assert_eq!(info.auth_methods, vec![AuthMethod::Cookie]);
1159        assert!(info.cookie_path.is_some());
1160        let path = info.cookie_path.unwrap();
1161        assert_eq!(
1162            path.to_str().unwrap(),
1163            "/tmp/my data\\\"dir//control_auth_cookie"
1164        );
1165    }
1166
1167    #[test]
1168    fn test_protocol_info_multiple_auth_methods() {
1169        let message = ControlMessage {
1170            status_code: 250,
1171            lines: vec![
1172                "PROTOCOLINFO 1".to_string(),
1173                "AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE=\"/home/atagar/.tor/control_auth_cookie\"".to_string(),
1174                "VERSION Tor=\"0.2.1.30\"".to_string(),
1175                "OK".to_string(),
1176            ],
1177        };
1178
1179        let info = ProtocolInfo::parse(&message).unwrap();
1180        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1181        assert!(info.auth_methods.contains(&AuthMethod::Password));
1182        assert_eq!(
1183            info.cookie_path,
1184            Some(PathBuf::from("/home/atagar/.tor/control_auth_cookie"))
1185        );
1186    }
1187
1188    #[test]
1189    fn test_protocol_info_minimum_response() {
1190        let message = ControlMessage {
1191            status_code: 250,
1192            lines: vec!["PROTOCOLINFO 5".to_string(), "OK".to_string()],
1193        };
1194
1195        let info = ProtocolInfo::parse(&message).unwrap();
1196        assert_eq!(info.protocol_version, 5);
1197        assert_eq!(info.tor_version, Version::new(0, 0, 0));
1198        assert!(info.auth_methods.is_empty());
1199        assert!(info.cookie_path.is_none());
1200    }
1201
1202    #[test]
1203    fn test_protocol_info_safecookie_auth() {
1204        let message = ControlMessage {
1205            status_code: 250,
1206            lines: vec![
1207                "PROTOCOLINFO 1".to_string(),
1208                "AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/var/run/tor/control.authcookie\""
1209                    .to_string(),
1210                "VERSION Tor=\"0.4.2.6\"".to_string(),
1211                "OK".to_string(),
1212            ],
1213        };
1214
1215        let info = ProtocolInfo::parse(&message).unwrap();
1216        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1217        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1218        assert_eq!(
1219            info.cookie_path,
1220            Some(PathBuf::from("/var/run/tor/control.authcookie"))
1221        );
1222    }
1223
1224    #[test]
1225    fn test_protocol_info_all_auth_methods() {
1226        let message = ControlMessage {
1227            status_code: 250,
1228            lines: vec![
1229                "PROTOCOLINFO 1".to_string(),
1230                "AUTH METHODS=NULL,HASHEDPASSWORD,COOKIE,SAFECOOKIE COOKIEFILE=\"/tmp/cookie\""
1231                    .to_string(),
1232                "VERSION Tor=\"0.4.7.1\"".to_string(),
1233                "OK".to_string(),
1234            ],
1235        };
1236
1237        let info = ProtocolInfo::parse(&message).unwrap();
1238        assert_eq!(info.auth_methods.len(), 4);
1239        assert!(info.auth_methods.contains(&AuthMethod::None));
1240        assert!(info.auth_methods.contains(&AuthMethod::Password));
1241        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1242        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1243    }
1244}
1245
1246#[cfg(test)]
1247mod proptests {
1248    use super::*;
1249    use proptest::prelude::*;
1250
1251    fn auth_method_strategy() -> impl Strategy<Value = Vec<AuthMethod>> {
1252        proptest::collection::vec(
1253            prop_oneof![
1254                Just(AuthMethod::None),
1255                Just(AuthMethod::Password),
1256                Just(AuthMethod::Cookie),
1257                Just(AuthMethod::SafeCookie),
1258            ],
1259            1..=4,
1260        )
1261        .prop_map(|mut methods| {
1262            methods.sort_by_key(|m| match m {
1263                AuthMethod::None => 0,
1264                AuthMethod::Password => 1,
1265                AuthMethod::Cookie => 2,
1266                AuthMethod::SafeCookie => 3,
1267            });
1268            methods.dedup();
1269            methods
1270        })
1271    }
1272
1273    fn version_strategy() -> impl Strategy<Value = Version> {
1274        (0u32..10, 0u32..10, 0u32..10, proptest::option::of(0u32..10)).prop_map(
1275            |(major, minor, micro, patch)| Version {
1276                major,
1277                minor,
1278                micro,
1279                patch,
1280                status: None,
1281            },
1282        )
1283    }
1284
1285    fn methods_to_string(methods: &[AuthMethod]) -> String {
1286        methods
1287            .iter()
1288            .map(|m| match m {
1289                AuthMethod::None => "NULL",
1290                AuthMethod::Password => "HASHEDPASSWORD",
1291                AuthMethod::Cookie => "COOKIE",
1292                AuthMethod::SafeCookie => "SAFECOOKIE",
1293            })
1294            .collect::<Vec<_>>()
1295            .join(",")
1296    }
1297
1298    proptest! {
1299        #![proptest_config(ProptestConfig::with_cases(100))]
1300
1301        #[test]
1302        fn prop_protocol_info_parsing_consistency(
1303            protocol_version in 1u32..3,
1304            tor_version in version_strategy(),
1305            auth_methods in auth_method_strategy()
1306        ) {
1307            let methods_str = methods_to_string(&auth_methods);
1308            let version_str = tor_version.to_string();
1309
1310            let message = ControlMessage {
1311                status_code: 250,
1312                lines: vec![
1313                    format!("PROTOCOLINFO {}", protocol_version),
1314                    format!("AUTH METHODS={}", methods_str),
1315                    format!("VERSION Tor=\"{}\"", version_str),
1316                    "OK".to_string(),
1317                ],
1318            };
1319
1320            let info = ProtocolInfo::parse(&message).expect("should parse");
1321            prop_assert_eq!(info.protocol_version, protocol_version);
1322            prop_assert_eq!(info.tor_version.major, tor_version.major);
1323            prop_assert_eq!(info.tor_version.minor, tor_version.minor);
1324            prop_assert_eq!(info.tor_version.micro, tor_version.micro);
1325
1326            for method in &auth_methods {
1327                prop_assert!(
1328                    info.auth_methods.contains(method),
1329                    "missing auth method: {:?}", method
1330                );
1331            }
1332        }
1333
1334        #[test]
1335        fn prop_protocol_info_with_cookie_path(
1336            protocol_version in 1u32..3,
1337            path_suffix in "[a-z]{5,15}"
1338        ) {
1339            let cookie_path = format!("/var/run/tor/{}.cookie", path_suffix);
1340            let message = ControlMessage {
1341                status_code: 250,
1342                lines: vec![
1343                    format!("PROTOCOLINFO {}", protocol_version),
1344                    format!("AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"{}\"", cookie_path),
1345                    "VERSION Tor=\"0.4.7.1\"".to_string(),
1346                    "OK".to_string(),
1347                ],
1348            };
1349
1350            let info = ProtocolInfo::parse(&message).expect("should parse");
1351            prop_assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1352            prop_assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1353            prop_assert_eq!(info.cookie_path, Some(PathBuf::from(&cookie_path)));
1354        }
1355
1356        #[test]
1357        fn prop_hex_encode_decode_roundtrip(data in proptest::collection::vec(any::<u8>(), 0..32)) {
1358            let encoded = hex_encode(&data);
1359            let decoded = hex_decode(&encoded).expect("should decode");
1360            prop_assert_eq!(data, decoded);
1361        }
1362    }
1363}