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}