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