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}