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`]: Connection failed or was closed
387/// - [`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`]: No compatible auth methods available
453/// - [`AuthError::MissingPassword`]: PASSWORD auth required but no password provided
454/// - [`AuthError::IncorrectPassword`]: PASSWORD auth failed
455/// - [`AuthError::CookieUnreadable`]: Cannot read cookie file
456/// - [`AuthError::IncorrectCookie`]: COOKIE auth failed
457/// - [`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    use rand::RngCore;
851    let mut rng = rand::rng();
852    rng.fill_bytes(&mut nonce);
853    nonce
854}
855
856/// Parses an AUTHCHALLENGE response to extract server hash and nonce.
857fn parse_authchallenge_response(response: &ControlMessage) -> Result<(Vec<u8>, Vec<u8>), Error> {
858    let content = response.content();
859    let mut line = ControlLine::new(content);
860
861    if !line.is_empty() {
862        let first = line.pop(false, false)?;
863        if first != "AUTHCHALLENGE" {
864            return Err(Error::Protocol(
865                "expected AUTHCHALLENGE response".to_string(),
866            ));
867        }
868    }
869
870    let mut server_hash: Option<Vec<u8>> = None;
871    let mut server_nonce: Option<Vec<u8>> = None;
872
873    while !line.is_empty() {
874        if line.is_next_mapping(Some("SERVERHASH"), false) {
875            let (_, hash_hex) = line.pop_mapping(false, false)?;
876            server_hash = Some(hex_decode(&hash_hex)?);
877        } else if line.is_next_mapping(Some("SERVERNONCE"), false) {
878            let (_, nonce_hex) = line.pop_mapping(false, false)?;
879            server_nonce = Some(hex_decode(&nonce_hex)?);
880        } else {
881            let _ = line.pop(false, false);
882        }
883    }
884
885    let server_hash =
886        server_hash.ok_or_else(|| Error::Protocol("missing SERVERHASH".to_string()))?;
887    let server_nonce =
888        server_nonce.ok_or_else(|| Error::Protocol("missing SERVERNONCE".to_string()))?;
889
890    Ok((server_hash, server_nonce))
891}
892
893/// Computes HMAC-SHA256 for SAFECOOKIE authentication.
894///
895/// The HMAC is computed over: cookie || client_nonce || server_nonce
896fn compute_hmac(
897    key_prefix: &[u8],
898    cookie: &[u8],
899    client_nonce: &[u8],
900    server_nonce: &[u8],
901) -> Vec<u8> {
902    use hmac::{Hmac, Mac};
903    use sha2::Sha256;
904
905    type HmacSha256 = Hmac<Sha256>;
906
907    let mut mac = HmacSha256::new_from_slice(key_prefix).expect("HMAC can take key of any size");
908    mac.update(cookie);
909    mac.update(client_nonce);
910    mac.update(server_nonce);
911    mac.finalize().into_bytes().to_vec()
912}
913
914/// Encodes bytes as uppercase hexadecimal string.
915fn hex_encode(data: &[u8]) -> String {
916    data.iter().map(|b| format!("{:02X}", b)).collect()
917}
918
919/// Decodes a hexadecimal string to bytes.
920fn hex_decode(s: &str) -> Result<Vec<u8>, Error> {
921    if !s.len().is_multiple_of(2) {
922        return Err(Error::Protocol("invalid hex string length".to_string()));
923    }
924
925    (0..s.len())
926        .step_by(2)
927        .map(|i| {
928            u8::from_str_radix(&s[i..i + 2], 16)
929                .map_err(|_| Error::Protocol(format!("invalid hex character at position {}", i)))
930        })
931        .collect()
932}
933
934#[cfg(test)]
935mod tests {
936    use super::*;
937
938    #[test]
939    fn test_auth_method_from_str() {
940        assert_eq!(AuthMethod::parse("NULL"), Some(AuthMethod::None));
941        assert_eq!(
942            AuthMethod::parse("HASHEDPASSWORD"),
943            Some(AuthMethod::Password)
944        );
945        assert_eq!(AuthMethod::parse("COOKIE"), Some(AuthMethod::Cookie));
946        assert_eq!(
947            AuthMethod::parse("SAFECOOKIE"),
948            Some(AuthMethod::SafeCookie)
949        );
950        assert_eq!(AuthMethod::parse("null"), Some(AuthMethod::None));
951        assert_eq!(AuthMethod::parse("UNKNOWN"), None);
952    }
953
954    #[test]
955    fn test_hex_encode() {
956        assert_eq!(hex_encode(&[0x00, 0xFF, 0xAB]), "00FFAB");
957        assert_eq!(hex_encode(&[]), "");
958        assert_eq!(hex_encode(&[0x12, 0x34]), "1234");
959    }
960
961    #[test]
962    fn test_hex_decode() {
963        assert_eq!(hex_decode("00FFAB").unwrap(), vec![0x00, 0xFF, 0xAB]);
964        assert_eq!(hex_decode("").unwrap(), vec![]);
965        assert_eq!(hex_decode("1234").unwrap(), vec![0x12, 0x34]);
966        assert_eq!(hex_decode("abcd").unwrap(), vec![0xAB, 0xCD]);
967    }
968
969    #[test]
970    fn test_hex_decode_invalid() {
971        assert!(hex_decode("123").is_err());
972        assert!(hex_decode("GHIJ").is_err());
973    }
974
975    #[test]
976    fn test_protocol_info_parse_simple() {
977        let message = ControlMessage {
978            status_code: 250,
979            lines: vec![
980                "PROTOCOLINFO 1".to_string(),
981                "AUTH METHODS=NULL".to_string(),
982                "VERSION Tor=\"0.4.7.1\"".to_string(),
983                "OK".to_string(),
984            ],
985        };
986
987        let info = ProtocolInfo::parse(&message).unwrap();
988        assert_eq!(info.protocol_version, 1);
989        assert_eq!(info.tor_version, Version::parse("0.4.7.1").unwrap());
990        assert_eq!(info.auth_methods, vec![AuthMethod::None]);
991        assert!(info.cookie_path.is_none());
992    }
993
994    #[test]
995    fn test_protocol_info_parse_with_cookie() {
996        let message = ControlMessage {
997            status_code: 250,
998            lines: vec![
999                "PROTOCOLINFO 1".to_string(),
1000                "AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/var/run/tor/control.authcookie\""
1001                    .to_string(),
1002                "VERSION Tor=\"0.4.8.0\"".to_string(),
1003                "OK".to_string(),
1004            ],
1005        };
1006
1007        let info = ProtocolInfo::parse(&message).unwrap();
1008        assert_eq!(info.protocol_version, 1);
1009        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1010        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1011        assert_eq!(
1012            info.cookie_path,
1013            Some(PathBuf::from("/var/run/tor/control.authcookie"))
1014        );
1015    }
1016
1017    #[test]
1018    fn test_protocol_info_parse_password() {
1019        let message = ControlMessage {
1020            status_code: 250,
1021            lines: vec![
1022                "PROTOCOLINFO 1".to_string(),
1023                "AUTH METHODS=HASHEDPASSWORD".to_string(),
1024                "VERSION Tor=\"0.4.7.1\"".to_string(),
1025                "OK".to_string(),
1026            ],
1027        };
1028
1029        let info = ProtocolInfo::parse(&message).unwrap();
1030        assert_eq!(info.auth_methods, vec![AuthMethod::Password]);
1031    }
1032
1033    #[test]
1034    fn test_protocol_info_parse_multiple_methods() {
1035        let message = ControlMessage {
1036            status_code: 250,
1037            lines: vec![
1038                "PROTOCOLINFO 1".to_string(),
1039                "AUTH METHODS=COOKIE,SAFECOOKIE,HASHEDPASSWORD COOKIEFILE=\"/tmp/cookie\""
1040                    .to_string(),
1041                "VERSION Tor=\"0.4.7.1\"".to_string(),
1042                "OK".to_string(),
1043            ],
1044        };
1045
1046        let info = ProtocolInfo::parse(&message).unwrap();
1047        assert_eq!(info.auth_methods.len(), 3);
1048        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1049        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1050        assert!(info.auth_methods.contains(&AuthMethod::Password));
1051    }
1052
1053    #[test]
1054    fn test_protocol_info_parse_error() {
1055        let message = ControlMessage {
1056            status_code: 515,
1057            lines: vec!["Authentication required".to_string()],
1058        };
1059
1060        assert!(ProtocolInfo::parse(&message).is_err());
1061    }
1062
1063    #[test]
1064    fn test_parse_authchallenge_response() {
1065        let message = ControlMessage {
1066            status_code: 250,
1067            lines: vec![
1068                "AUTHCHALLENGE SERVERHASH=ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234 SERVERNONCE=1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF".to_string(),
1069            ],
1070        };
1071
1072        let (server_hash, server_nonce) = parse_authchallenge_response(&message).unwrap();
1073        assert_eq!(server_hash.len(), 32);
1074        assert_eq!(server_nonce.len(), 32);
1075    }
1076
1077    #[test]
1078    fn test_protocol_info_parse_empty_methods() {
1079        let message = ControlMessage {
1080            status_code: 250,
1081            lines: vec![
1082                "PROTOCOLINFO 1".to_string(),
1083                "AUTH METHODS=NULL".to_string(),
1084                "VERSION Tor=\"0.4.7.1\"".to_string(),
1085                "OK".to_string(),
1086            ],
1087        };
1088
1089        let info = ProtocolInfo::parse(&message).unwrap();
1090        assert!(info.auth_methods.contains(&AuthMethod::None));
1091    }
1092
1093    #[test]
1094    fn test_protocol_info_parse_version_without_quotes() {
1095        let message = ControlMessage {
1096            status_code: 250,
1097            lines: vec![
1098                "PROTOCOLINFO 1".to_string(),
1099                "AUTH METHODS=NULL".to_string(),
1100                "VERSION Tor=\"0.4.8.0-alpha-dev\"".to_string(),
1101                "OK".to_string(),
1102            ],
1103        };
1104
1105        let info = ProtocolInfo::parse(&message).unwrap();
1106        assert_eq!(info.tor_version.major, 0);
1107        assert_eq!(info.tor_version.minor, 4);
1108        assert_eq!(info.tor_version.micro, 8);
1109    }
1110
1111    #[test]
1112    fn test_protocol_info_no_auth() {
1113        let message = ControlMessage {
1114            status_code: 250,
1115            lines: vec![
1116                "PROTOCOLINFO 1".to_string(),
1117                "AUTH METHODS=NULL".to_string(),
1118                "VERSION Tor=\"0.2.1.30\"".to_string(),
1119                "OK".to_string(),
1120            ],
1121        };
1122
1123        let info = ProtocolInfo::parse(&message).unwrap();
1124        assert_eq!(info.protocol_version, 1);
1125        assert_eq!(info.tor_version, Version::parse("0.2.1.30").unwrap());
1126        assert_eq!(info.auth_methods, vec![AuthMethod::None]);
1127        assert!(info.cookie_path.is_none());
1128    }
1129
1130    #[test]
1131    fn test_protocol_info_password_auth() {
1132        let message = ControlMessage {
1133            status_code: 250,
1134            lines: vec![
1135                "PROTOCOLINFO 1".to_string(),
1136                "AUTH METHODS=HASHEDPASSWORD".to_string(),
1137                "VERSION Tor=\"0.2.1.30\"".to_string(),
1138                "OK".to_string(),
1139            ],
1140        };
1141
1142        let info = ProtocolInfo::parse(&message).unwrap();
1143        assert_eq!(info.auth_methods, vec![AuthMethod::Password]);
1144    }
1145
1146    #[test]
1147    fn test_protocol_info_cookie_auth_with_escape() {
1148        let message = ControlMessage {
1149            status_code: 250,
1150            lines: vec![
1151                "PROTOCOLINFO 1".to_string(),
1152                "AUTH METHODS=COOKIE COOKIEFILE=\"/tmp/my data\\\\\\\"dir//control_auth_cookie\""
1153                    .to_string(),
1154                "VERSION Tor=\"0.2.1.30\"".to_string(),
1155                "OK".to_string(),
1156            ],
1157        };
1158
1159        let info = ProtocolInfo::parse(&message).unwrap();
1160        assert_eq!(info.auth_methods, vec![AuthMethod::Cookie]);
1161        assert!(info.cookie_path.is_some());
1162        let path = info.cookie_path.unwrap();
1163        assert_eq!(
1164            path.to_str().unwrap(),
1165            "/tmp/my data\\\"dir//control_auth_cookie"
1166        );
1167    }
1168
1169    #[test]
1170    fn test_protocol_info_multiple_auth_methods() {
1171        let message = ControlMessage {
1172            status_code: 250,
1173            lines: vec![
1174                "PROTOCOLINFO 1".to_string(),
1175                "AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE=\"/home/atagar/.tor/control_auth_cookie\"".to_string(),
1176                "VERSION Tor=\"0.2.1.30\"".to_string(),
1177                "OK".to_string(),
1178            ],
1179        };
1180
1181        let info = ProtocolInfo::parse(&message).unwrap();
1182        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1183        assert!(info.auth_methods.contains(&AuthMethod::Password));
1184        assert_eq!(
1185            info.cookie_path,
1186            Some(PathBuf::from("/home/atagar/.tor/control_auth_cookie"))
1187        );
1188    }
1189
1190    #[test]
1191    fn test_protocol_info_minimum_response() {
1192        let message = ControlMessage {
1193            status_code: 250,
1194            lines: vec!["PROTOCOLINFO 5".to_string(), "OK".to_string()],
1195        };
1196
1197        let info = ProtocolInfo::parse(&message).unwrap();
1198        assert_eq!(info.protocol_version, 5);
1199        assert_eq!(info.tor_version, Version::new(0, 0, 0));
1200        assert!(info.auth_methods.is_empty());
1201        assert!(info.cookie_path.is_none());
1202    }
1203
1204    #[test]
1205    fn test_protocol_info_safecookie_auth() {
1206        let message = ControlMessage {
1207            status_code: 250,
1208            lines: vec![
1209                "PROTOCOLINFO 1".to_string(),
1210                "AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"/var/run/tor/control.authcookie\""
1211                    .to_string(),
1212                "VERSION Tor=\"0.4.2.6\"".to_string(),
1213                "OK".to_string(),
1214            ],
1215        };
1216
1217        let info = ProtocolInfo::parse(&message).unwrap();
1218        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1219        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1220        assert_eq!(
1221            info.cookie_path,
1222            Some(PathBuf::from("/var/run/tor/control.authcookie"))
1223        );
1224    }
1225
1226    #[test]
1227    fn test_protocol_info_all_auth_methods() {
1228        let message = ControlMessage {
1229            status_code: 250,
1230            lines: vec![
1231                "PROTOCOLINFO 1".to_string(),
1232                "AUTH METHODS=NULL,HASHEDPASSWORD,COOKIE,SAFECOOKIE COOKIEFILE=\"/tmp/cookie\""
1233                    .to_string(),
1234                "VERSION Tor=\"0.4.7.1\"".to_string(),
1235                "OK".to_string(),
1236            ],
1237        };
1238
1239        let info = ProtocolInfo::parse(&message).unwrap();
1240        assert_eq!(info.auth_methods.len(), 4);
1241        assert!(info.auth_methods.contains(&AuthMethod::None));
1242        assert!(info.auth_methods.contains(&AuthMethod::Password));
1243        assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1244        assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1245    }
1246}
1247
1248#[cfg(test)]
1249mod proptests {
1250    use super::*;
1251    use proptest::prelude::*;
1252
1253    fn auth_method_strategy() -> impl Strategy<Value = Vec<AuthMethod>> {
1254        proptest::collection::vec(
1255            prop_oneof![
1256                Just(AuthMethod::None),
1257                Just(AuthMethod::Password),
1258                Just(AuthMethod::Cookie),
1259                Just(AuthMethod::SafeCookie),
1260            ],
1261            1..=4,
1262        )
1263        .prop_map(|mut methods| {
1264            methods.sort_by_key(|m| match m {
1265                AuthMethod::None => 0,
1266                AuthMethod::Password => 1,
1267                AuthMethod::Cookie => 2,
1268                AuthMethod::SafeCookie => 3,
1269            });
1270            methods.dedup();
1271            methods
1272        })
1273    }
1274
1275    fn version_strategy() -> impl Strategy<Value = Version> {
1276        (0u32..10, 0u32..10, 0u32..10, proptest::option::of(0u32..10)).prop_map(
1277            |(major, minor, micro, patch)| Version {
1278                major,
1279                minor,
1280                micro,
1281                patch,
1282                status: None,
1283            },
1284        )
1285    }
1286
1287    fn methods_to_string(methods: &[AuthMethod]) -> String {
1288        methods
1289            .iter()
1290            .map(|m| match m {
1291                AuthMethod::None => "NULL",
1292                AuthMethod::Password => "HASHEDPASSWORD",
1293                AuthMethod::Cookie => "COOKIE",
1294                AuthMethod::SafeCookie => "SAFECOOKIE",
1295            })
1296            .collect::<Vec<_>>()
1297            .join(",")
1298    }
1299
1300    proptest! {
1301        #![proptest_config(ProptestConfig::with_cases(100))]
1302
1303        #[test]
1304        fn prop_protocol_info_parsing_consistency(
1305            protocol_version in 1u32..3,
1306            tor_version in version_strategy(),
1307            auth_methods in auth_method_strategy()
1308        ) {
1309            let methods_str = methods_to_string(&auth_methods);
1310            let version_str = tor_version.to_string();
1311
1312            let message = ControlMessage {
1313                status_code: 250,
1314                lines: vec![
1315                    format!("PROTOCOLINFO {}", protocol_version),
1316                    format!("AUTH METHODS={}", methods_str),
1317                    format!("VERSION Tor=\"{}\"", version_str),
1318                    "OK".to_string(),
1319                ],
1320            };
1321
1322            let info = ProtocolInfo::parse(&message).expect("should parse");
1323            prop_assert_eq!(info.protocol_version, protocol_version);
1324            prop_assert_eq!(info.tor_version.major, tor_version.major);
1325            prop_assert_eq!(info.tor_version.minor, tor_version.minor);
1326            prop_assert_eq!(info.tor_version.micro, tor_version.micro);
1327
1328            for method in &auth_methods {
1329                prop_assert!(
1330                    info.auth_methods.contains(method),
1331                    "missing auth method: {:?}", method
1332                );
1333            }
1334        }
1335
1336        #[test]
1337        fn prop_protocol_info_with_cookie_path(
1338            protocol_version in 1u32..3,
1339            path_suffix in "[a-z]{5,15}"
1340        ) {
1341            let cookie_path = format!("/var/run/tor/{}.cookie", path_suffix);
1342            let message = ControlMessage {
1343                status_code: 250,
1344                lines: vec![
1345                    format!("PROTOCOLINFO {}", protocol_version),
1346                    format!("AUTH METHODS=COOKIE,SAFECOOKIE COOKIEFILE=\"{}\"", cookie_path),
1347                    "VERSION Tor=\"0.4.7.1\"".to_string(),
1348                    "OK".to_string(),
1349                ],
1350            };
1351
1352            let info = ProtocolInfo::parse(&message).expect("should parse");
1353            prop_assert!(info.auth_methods.contains(&AuthMethod::Cookie));
1354            prop_assert!(info.auth_methods.contains(&AuthMethod::SafeCookie));
1355            prop_assert_eq!(info.cookie_path, Some(PathBuf::from(&cookie_path)));
1356        }
1357
1358        #[test]
1359        fn prop_hex_encode_decode_roundtrip(data in proptest::collection::vec(any::<u8>(), 0..32)) {
1360            let encoded = hex_encode(&data);
1361            let decoded = hex_decode(&encoded).expect("should decode");
1362            prop_assert_eq!(data, decoded);
1363        }
1364    }
1365}