tor_interface/
legacy_tor_process.rs

1// standard
2use std::default::Default;
3use std::fs;
4use std::fs::File;
5use std::io::{BufRead, BufReader, Read, Write};
6use std::net::SocketAddr;
7use std::ops::Drop;
8use std::path::Path;
9use std::process;
10use std::process::{Child, ChildStdout, Command, Stdio};
11use std::str::FromStr;
12use std::string::ToString;
13use std::sync::{Arc, Mutex};
14use std::time::{Duration, Instant};
15
16// extern crates
17use data_encoding::HEXUPPER;
18use rand::RngCore;
19use sha1::{Digest, Sha1};
20
21// internal crates
22use crate::tor_crypto::generate_password;
23
24#[derive(thiserror::Error, Debug)]
25pub enum Error {
26    #[error("failed to read control port file")]
27    ControlPortFileReadFailed(#[source] std::io::Error),
28
29    #[error("provided control port file '{0}' larger than expected ({1} bytes)")]
30    ControlPortFileTooLarge(String, u64),
31
32    #[error("failed to parse '{0}' as control port file")]
33    ControlPortFileContentsInvalid(String),
34
35    #[error("provided tor bin path '{0}' must be an absolute path")]
36    TorBinPathNotAbsolute(String),
37
38    #[error("provided data directory '{0}' must be an absolute path")]
39    TorDataDirectoryPathNotAbsolute(String),
40
41    #[error("failed to create data directory")]
42    DataDirectoryCreationFailed(#[source] std::io::Error),
43
44    #[error("file exists in provided data directory path '{0}'")]
45    DataDirectoryPathExistsAsFile(String),
46
47    #[error("failed to create default_torrc file")]
48    DefaultTorrcFileCreationFailed(#[source] std::io::Error),
49
50    #[error("failed to write default_torrc file")]
51    DefaultTorrcFileWriteFailed(#[source] std::io::Error),
52
53    #[error("failed to create torrc file")]
54    TorrcFileCreationFailed(#[source] std::io::Error),
55
56    #[error("failed to remove control_port file")]
57    ControlPortFileDeleteFailed(#[source] std::io::Error),
58
59    #[error("failed to start legacy tor process")]
60    LegacyTorProcessStartFailed(#[source] std::io::Error),
61
62    #[error("failed to read control addr from control_file '{0}'")]
63    ControlPortFileMissing(String),
64
65    #[error("unable to take legacy tor process stdout")]
66    LegacyTorProcessStdoutTakeFailed(),
67
68    #[error("failed to spawn tor process stdout read thread")]
69    StdoutReadThreadSpawnFailed(#[source] std::io::Error),
70}
71
72fn read_control_port_file(control_port_file: &Path) -> Result<SocketAddr, Error> {
73    // open file
74    let mut file = File::open(control_port_file).map_err(Error::ControlPortFileReadFailed)?;
75
76    // bail if the file is larger than expected
77    let metadata = file.metadata().map_err(Error::ControlPortFileReadFailed)?;
78    if metadata.len() >= 1024 {
79        return Err(Error::ControlPortFileTooLarge(
80            format!("{}", control_port_file.display()),
81            metadata.len(),
82        ));
83    }
84
85    // read contents to string
86    let mut contents = String::new();
87    file.read_to_string(&mut contents)
88        .map_err(Error::ControlPortFileReadFailed)?;
89
90    if contents.starts_with("PORT=") {
91        let addr_string = &contents.trim_end()["PORT=".len()..];
92        if let Ok(addr) = SocketAddr::from_str(addr_string) {
93            return Ok(addr);
94        }
95    }
96    Err(Error::ControlPortFileContentsInvalid(format!(
97        "{}",
98        control_port_file.display()
99    )))
100}
101
102// Encapsulates the tor daemon process
103pub(crate) struct LegacyTorProcess {
104    control_addr: SocketAddr,
105    process: Child,
106    password: String,
107    // stdout data
108    stdout_lines: Arc<Mutex<Vec<String>>>,
109}
110
111impl LegacyTorProcess {
112    const S2K_RFC2440_SPECIFIER_LEN: usize = 9;
113
114    fn hash_tor_password_with_salt(
115        salt: &[u8; Self::S2K_RFC2440_SPECIFIER_LEN],
116        password: &str,
117    ) -> String {
118        assert_eq!(salt[Self::S2K_RFC2440_SPECIFIER_LEN - 1], 0x60);
119
120        // tor-specific rfc 2440 constants
121        const EXPBIAS: u8 = 6u8;
122        const C: u8 = 0x60; // salt[S2K_RFC2440_SPECIFIER_LEN - 1]
123        const COUNT: usize = (16usize + ((C & 15u8) as usize)) << ((C >> 4) + EXPBIAS);
124
125        // squash together our hash input
126        let mut input: Vec<u8> = Default::default();
127        // append salt (sans the 'C' constant')
128        input.extend_from_slice(&salt[0..Self::S2K_RFC2440_SPECIFIER_LEN - 1]);
129        // append password bytes
130        input.extend_from_slice(password.as_bytes());
131
132        let input = input.as_slice();
133        let input_len = input.len();
134
135        let mut sha1 = Sha1::new();
136        let mut count = COUNT;
137        while count > 0 {
138            if count > input_len {
139                sha1.update(input);
140                count -= input_len;
141            } else {
142                sha1.update(&input[0..count]);
143                break;
144            }
145        }
146
147        let key = sha1.finalize();
148
149        let mut hash = "16:".to_string();
150        HEXUPPER.encode_append(salt, &mut hash);
151        HEXUPPER.encode_append(&key, &mut hash);
152
153        hash
154    }
155
156    fn hash_tor_password(password: &str) -> String {
157        let mut salt = [0x00u8; Self::S2K_RFC2440_SPECIFIER_LEN];
158        let csprng = &mut tor_llcrypto::rng::CautiousRng;
159        csprng.fill_bytes(&mut salt);
160        salt[Self::S2K_RFC2440_SPECIFIER_LEN - 1] = 0x60u8;
161
162        Self::hash_tor_password_with_salt(&salt, password)
163    }
164
165    pub fn get_control_addr(&self) -> &SocketAddr {
166        &self.control_addr
167    }
168
169    pub fn get_password(&self) -> &String {
170        &self.password
171    }
172
173    pub fn new(tor_bin_path: &Path, data_directory: &Path) -> Result<LegacyTorProcess, Error> {
174        if tor_bin_path.is_relative() {
175            return Err(Error::TorBinPathNotAbsolute(format!(
176                "{}",
177                tor_bin_path.display()
178            )));
179        }
180        if data_directory.is_relative() {
181            return Err(Error::TorDataDirectoryPathNotAbsolute(format!(
182                "{}",
183                data_directory.display()
184            )));
185        }
186
187        // create data directory if it doesn't exist
188        if !data_directory.exists() {
189            fs::create_dir_all(data_directory).map_err(Error::DataDirectoryCreationFailed)?;
190        } else if data_directory.is_file() {
191            return Err(Error::DataDirectoryPathExistsAsFile(format!(
192                "{}",
193                data_directory.display()
194            )));
195        }
196
197        // construct paths to torrc files
198        let default_torrc = data_directory.join("default_torrc");
199        let torrc = data_directory.join("torrc");
200        let control_port_file = data_directory.join("control_port");
201
202        // TODO: should we nuke the existing torrc between runs? Do we want
203        // users setting custom nonsense in there?
204        // construct default torrc
205        //  - daemon determines socks port
206        //  - minimize writes to disk
207        //  - start with network disabled by default
208        if !default_torrc.exists() {
209            const DEFAULT_TORRC_CONTENT: &str = "SocksPort auto\n\
210            AvoidDiskWrites 1\n\
211            DisableNetwork 1\n";
212
213            let mut default_torrc_file =
214                File::create(&default_torrc).map_err(Error::DefaultTorrcFileCreationFailed)?;
215            default_torrc_file
216                .write_all(DEFAULT_TORRC_CONTENT.as_bytes())
217                .map_err(Error::DefaultTorrcFileWriteFailed)?;
218        }
219
220        // create empty torrc for user
221        if !torrc.exists() {
222            let _ = File::create(&torrc).map_err(Error::TorrcFileCreationFailed)?;
223        }
224
225        // remove any existing control_port_file
226        if control_port_file.exists() {
227            fs::remove_file(&control_port_file).map_err(Error::ControlPortFileDeleteFailed)?;
228        }
229
230        const CONTROL_PORT_PASSWORD_LENGTH: usize = 32usize;
231        let password = generate_password(CONTROL_PORT_PASSWORD_LENGTH);
232        let password_hash = Self::hash_tor_password(&password);
233
234        let mut process = Command::new(tor_bin_path.as_os_str())
235            .stdout(Stdio::piped())
236            .stdin(Stdio::null())
237            .stderr(Stdio::null())
238            // set working directory to data directory
239            .current_dir(data_directory)
240            // point to our above written torrc file
241            .arg("--defaults-torrc")
242            .arg(default_torrc)
243            // location of torrc
244            .arg("--torrc-file")
245            .arg(torrc)
246            // root data directory
247            .arg("DataDirectory")
248            .arg(data_directory)
249            // daemon will assign us a port, and we will
250            // read it from the control port file
251            .arg("ControlPort")
252            .arg("auto")
253            // control port file destination
254            .arg("ControlPortWriteToFile")
255            .arg(control_port_file.clone())
256            // use password authentication to prevent other apps
257            // from modifying our daemon's settings
258            .arg("HashedControlPassword")
259            .arg(password_hash)
260            // tor process will shut down after this process shuts down
261            // to avoid orphaned tor daemon
262            .arg("__OwningControllerProcess")
263            .arg(process::id().to_string())
264            .spawn()
265            .map_err(Error::LegacyTorProcessStartFailed)?;
266
267        let mut control_addr = None;
268        let start = Instant::now();
269
270        // try and read the control port from the control port file
271        // or abort after 5 seconds
272        // TODO: make this timeout configurable?
273        while control_addr.is_none() && start.elapsed() < Duration::from_secs(5) {
274            if control_port_file.exists() {
275                control_addr = Some(read_control_port_file(control_port_file.as_path())?);
276                fs::remove_file(&control_port_file).map_err(Error::ControlPortFileDeleteFailed)?;
277            }
278        }
279
280        let control_addr = match control_addr {
281            Some(control_addr) => control_addr,
282            None => {
283                return Err(Error::ControlPortFileMissing(format!(
284                    "{}",
285                    control_port_file.display()
286                )))
287            }
288        };
289
290        let stdout_lines: Arc<Mutex<Vec<String>>> = Default::default();
291
292        {
293            let stdout_lines = Arc::downgrade(&stdout_lines);
294            let stdout = BufReader::new(match process.stdout.take() {
295                Some(stdout) => stdout,
296                None => return Err(Error::LegacyTorProcessStdoutTakeFailed()),
297            });
298
299            std::thread::Builder::new()
300                .name("tor_stdout_reader".to_string())
301                .spawn(move || {
302                    LegacyTorProcess::read_stdout_task(&stdout_lines, stdout);
303                })
304                .map_err(Error::StdoutReadThreadSpawnFailed)?;
305        }
306
307        Ok(LegacyTorProcess {
308            control_addr,
309            process,
310            password,
311            stdout_lines,
312        })
313    }
314
315    fn read_stdout_task(
316        stdout_lines: &std::sync::Weak<Mutex<Vec<String>>>,
317        mut stdout: BufReader<ChildStdout>,
318    ) {
319        while let Some(stdout_lines) = stdout_lines.upgrade() {
320            let mut line = String::default();
321            // read line
322            if stdout.read_line(&mut line).is_ok() {
323                // remove trailing '\n'
324                line.pop();
325                // then acquire the lock on the line buffer
326                let mut stdout_lines = match stdout_lines.lock() {
327                    Ok(stdout_lines) => stdout_lines,
328                    Err(_) => unreachable!(),
329                };
330                stdout_lines.push(line);
331            }
332        }
333    }
334
335    pub fn wait_log_lines(&mut self) -> Vec<String> {
336        let mut lines = match self.stdout_lines.lock() {
337            Ok(lines) => lines,
338            Err(_) => unreachable!(),
339        };
340        std::mem::take(&mut lines)
341    }
342}
343
344impl Drop for LegacyTorProcess {
345    fn drop(&mut self) {
346        let _ = self.process.kill();
347    }
348}
349
350#[test]
351fn test_password_hash() -> Result<(), anyhow::Error> {
352    let salt1: [u8; LegacyTorProcess::S2K_RFC2440_SPECIFIER_LEN] = [
353        0xbeu8, 0x2au8, 0x25u8, 0x1du8, 0xe6u8, 0x2cu8, 0xb2u8, 0x7au8, 0x60u8,
354    ];
355    let hash1 = LegacyTorProcess::hash_tor_password_with_salt(&salt1, "abcdefghijklmnopqrstuvwxyz");
356    assert_eq!(
357        hash1,
358        "16:BE2A251DE62CB27A60AC9178A937990E8ED0AB662FA82A5C7DE3EBB23A"
359    );
360
361    let salt2: [u8; LegacyTorProcess::S2K_RFC2440_SPECIFIER_LEN] = [
362        0x36u8, 0x73u8, 0x0eu8, 0xefu8, 0xd1u8, 0x8cu8, 0x60u8, 0xd6u8, 0x60u8,
363    ];
364    let hash2 = LegacyTorProcess::hash_tor_password_with_salt(&salt2, "password");
365    assert_eq!(
366        hash2,
367        "16:36730EEFD18C60D66052E7EA535438761C0928D316EEA56A190C99B50A"
368    );
369
370    // ensure same password is hashed to different things
371    assert_ne!(
372        LegacyTorProcess::hash_tor_password("password"),
373        LegacyTorProcess::hash_tor_password("password")
374    );
375
376    Ok(())
377}