tor_client_lib/
auth.rs

1use crate::control_connection::TorControlConnection;
2use crate::error::TorError;
3use hex;
4use hmac::{Hmac, Mac};
5use lazy_static::lazy_static;
6use rand;
7use regex::Regex;
8use sha2::Sha256;
9
10// Create alias for HMAC-SHA256
11type HmacSha256 = Hmac<Sha256>;
12
13fn parse_authchallenge_response(response: &str) -> Result<(Vec<u8>, Vec<u8>), TorError> {
14    // Parse the controller response
15    lazy_static! {
16        static ref RE: Regex =
17            Regex::new(r"^AUTHCHALLENGE SERVERHASH=(?P<server_hash>[0-9A-F]*) SERVERNONCE=(?P<server_nonce>[0-9A-F]*)")
18                .unwrap();
19    }
20    match RE.captures(response) {
21        Some(captures) => {
22            let server_hash_hex = &captures["server_hash"];
23            let server_nonce_hex = &captures["server_nonce"];
24            let server_hash = match hex::decode(server_hash_hex) {
25                Ok(hash) => hash,
26                Err(error) => {
27                    return Err(TorError::protocol_error(&format!(
28                        "Error decoding base64 server hash from server: {}",
29                        error
30                    )))
31                }
32            };
33            let server_nonce = match hex::decode(server_nonce_hex) {
34                Ok(nonce) => nonce,
35                Err(error) => {
36                    return Err(TorError::protocol_error(&format!(
37                        "Error decoding base64 server nonce from server: {}",
38                        error
39                    )))
40                }
41            };
42            Ok((server_hash, server_nonce))
43        }
44        None => Err(TorError::protocol_error(&format!(
45            "Unexpected response from AUTHCHALLENGE: {}",
46            response
47        ))),
48    }
49}
50
51fn generate_hmac(key: &[u8], message: &[u8]) -> Vec<u8> {
52    let mut hmac = HmacSha256::new_from_slice(key).unwrap();
53    hmac.update(message);
54    hmac.finalize().into_bytes().to_vec()
55}
56
57fn validate_server_hash(
58    safe_cookie_value: &[u8],
59    client_nonce: &mut Vec<u8>,
60    server_nonce: &mut Vec<u8>,
61    server_hash: &[u8],
62) -> Result<Vec<u8>, TorError> {
63    // Generate HMAC
64    let mut message = safe_cookie_value.to_vec();
65    message.append(client_nonce);
66    message.append(server_nonce);
67    let hash = generate_hmac(
68        b"Tor safe cookie authentication server-to-controller hash",
69        &message,
70    );
71
72    if hash.as_slice() == server_hash {
73        Ok(message)
74    } else {
75        Err(TorError::authentication_error(
76            "Server hash didn't validate",
77        ))
78    }
79}
80
81async fn safe_cookie_authentication(
82    cookie: &[u8],
83    connection: &mut TorControlConnection,
84) -> Result<(), TorError> {
85    let mut client_nonce = vec![rand::random::<u8>(), rand::random::<u8>()];
86    let client_nonce_hex = hex::encode(&client_nonce);
87    let response = connection
88        .send_command(
89            "AUTHCHALLENGE",
90            Some(&format!("SAFECOOKIE {}", client_nonce_hex)),
91        )
92        .await?;
93
94    // Handle the response
95    if response.status_code != 250 {
96        return Err(TorError::protocol_error(&format!(
97            "Expected status code 250, got {}",
98            response.status_code
99        )));
100    }
101    let (server_hash, mut server_nonce) = parse_authchallenge_response(&response.reply)?;
102
103    // Validate server hash
104    // Note: we reuse the message from the HMAC
105    let message = validate_server_hash(cookie, &mut client_nonce, &mut server_nonce, &server_hash)?;
106
107    // Generate authentication string
108    let auth_string = hex::encode(generate_hmac(
109        b"Tor safe cookie authentication controller-to-server hash",
110        &message,
111    ));
112
113    // Send authentication request
114    let response = connection
115        .send_command("AUTHENTICATE", Some(&auth_string))
116        .await?;
117    if response.status_code != 250 {
118        return Err(TorError::protocol_error(&format!(
119            "Expected status code 250, got {}",
120            response.status_code
121        )));
122    }
123    Ok(())
124}
125
126/// Methods for Tor authentication:
127/// - Null - No authentication
128/// - SafeCookie - `SAFECOOKIE` authentication
129/// - HashedPassword - pass the hashed password to authenticate
130///
131/// With `SafeCookie` auth, you can either pass in the cookie value as a binary vector, or, if you
132/// pass in "None", it will call the PROTOCOLINFO command to get the location of the cookie file,
133/// and attempt to read that and pass the value to authenticate.
134///
135/// Note that we don't support plain `COOKIE` authentication, since that's been determmined to be
136/// unsafe.
137#[derive(strum_macros::Display)]
138pub enum TorAuthentication {
139    Null,
140    SafeCookie(Option<Vec<u8>>), // Cookie String
141    HashedPassword(String),      // Password
142}
143
144lazy_static! {
145    static ref NULL_AUTH_NAME: String = "NULL".to_string();
146    static ref HASHED_PASSWORD_NAME: String = "HASHEDPASSWORD".to_string();
147    static ref SAFE_COOKIE_NAME: String = "SAFECOOKIE".to_string();
148}
149
150impl TorAuthentication {
151    /// Authenticate using this method to the server
152    pub async fn authenticate(
153        &self,
154        connection: &mut TorControlConnection,
155    ) -> Result<(), TorError> {
156        let protocol_info = connection.get_protocol_info().await?;
157        match self {
158            TorAuthentication::Null => {
159                if !protocol_info.auth_methods.contains(&*NULL_AUTH_NAME) {
160                    return Err(TorError::authentication_error(
161                        "Null auth not allowed by this server",
162                    ));
163                }
164                match connection.send_command("AUTHENTICATE", None).await {
165                    Ok(_) => Ok(()),
166                    Err(TorError::ProtocolError(error)) => {
167                        Err(TorError::AuthenticationError(error))
168                    }
169                    Err(error) => Err(error),
170                }
171            }
172            TorAuthentication::HashedPassword(password) => {
173                if !protocol_info.auth_methods.contains(&*HASHED_PASSWORD_NAME) {
174                    return Err(TorError::authentication_error(
175                        "HashedPassword auth not allowed by this server",
176                    ));
177                }
178                match connection
179                    .send_command("AUTHENTICATE", Some(&format!("\"{}\"", password)))
180                    .await
181                {
182                    Ok(_) => Ok(()),
183                    Err(TorError::ProtocolError(error)) => {
184                        Err(TorError::AuthenticationError(error))
185                    }
186                    Err(error) => Err(error),
187                }
188            }
189            TorAuthentication::SafeCookie(cookie) => {
190                if !protocol_info.auth_methods.contains(&*SAFE_COOKIE_NAME) {
191                    return Err(TorError::authentication_error(
192                        "SafeCookie auth not allowed by this server",
193                    ));
194                }
195                match cookie {
196                    // Authenticate using the passed-in cookie
197                    Some(cookie) => safe_cookie_authentication(cookie, connection).await,
198
199                    // None means to read the cookie from the cookie file as defined in the
200                    // server's protocol_info
201                    None => match protocol_info.cookie_file {
202                        Some(cookie_file) => match std::fs::read(cookie_file.clone()) {
203                            Ok(cookie) => safe_cookie_authentication(&cookie, connection).await,
204                            Err(error) => Err(TorError::authentication_error(&format!(
205                                "Error reading cookie file {}: {}",
206                                cookie_file, error
207                            ))),
208                        },
209                        None => Err(TorError::authentication_error(
210                            "No cookie file provided in tor protocol info",
211                        )),
212                    },
213                }
214            }
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_auth_display() {
225        assert_eq!("Null", format!("{}", TorAuthentication::Null));
226        assert_eq!(
227            "HashedPassword",
228            format!("{}", TorAuthentication::HashedPassword("foobar".into()))
229        );
230        assert_eq!(
231            "SafeCookie",
232            format!(
233                "{}",
234                TorAuthentication::SafeCookie(Some("foobar".as_bytes().to_vec()))
235            )
236        );
237    }
238}