diff --git a/.gitignore b/.gitignore index 026fb6d..a1fa12a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /target Cargo.lock .idea/* +examples/client_data_dir +examples/server_data_dir +*_rc diff --git a/Cargo.toml b/Cargo.toml index b5865e4..e03149d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,16 @@ [package] name = "tapir-cwtch" -version = "0.1.7" +version = "0.1.8" authors = ["Sarah Jamie Lewis "] edition = "2018" license = "MIT" description = "Tapir is a small library for building p2p applications over anonymous communication systems" repository = "https://git.openprivacy.ca/sarah/tapir-rs" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + [features] + +# Compile with support for launching v3 onion services onionv3 = [] [[test]] @@ -23,6 +25,7 @@ merlin = "2.0.0" hex = "0.4.2" base32 = "0.4.0" base64 = "0.13.0" +sha1 = "0.6.0" sha3 = "0.9.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.61" @@ -32,3 +35,4 @@ integer-encoding = "2.1.1" secretbox = "0.1.2" subtle = "2.3.0" hashbrown = "0.9.1" +which = "4.0.2" diff --git a/examples/simple_client.rs b/examples/simple_client.rs index 1b20432..0d777b4 100644 --- a/examples/simple_client.rs +++ b/examples/simple_client.rs @@ -1,4 +1,7 @@ +use rand::{thread_rng, Rng}; use std::sync::Arc; +use tapir_cwtch::acns::tor::run::TorRunner; +use tapir_cwtch::acns::tor::torrc::TorrcGenerator; use tapir_cwtch::applications::authentication_app::{AuthenicationApp, AUTHENTICATION_CAPABILITY}; use tapir_cwtch::connections::service::Service; use tapir_cwtch::connections::{Connection, ConnectionInterface, OutboundConnection}; @@ -9,7 +12,19 @@ fn main() { let identity = Arc::new(Identity::initialize_ephemeral_identity()); println!("Setup: {}", identity.hostname()); - let mut service = Service::init(identity.clone()); + let tor_path = which::which("tor"); + let mut rng = thread_rng(); + let socks_port = rng.gen_range(9052, 9100); + let mut tor_runner = TorRunner::run( + TorrcGenerator::new().with_socks_port(socks_port), + "./example_client_rc", + tor_path.unwrap().to_str().unwrap(), + "./examples/client_data_dir/", + ) + .unwrap(); + tor_runner.wait_until_bootstrapped(); + + let mut service = Service::init(identity.clone(), socks_port); let identity = identity.clone(); let outbound_identity = identity.clone(); let outbound_service = |conn: Connection| { @@ -24,7 +39,7 @@ fn main() { } } }; - match service.connect("qz4o3mqhzw6ye3jjadbi3l7g2rldbnkidf255iwsl4vfvrzsl26drfad", outbound_service.clone()) { + match service.connect("srg3w256xrpcs6o25i5qlo6iviwsybnfw66nwelrbah7c5e6knb32xyd", outbound_service.clone()) { _ => {} } loop {} diff --git a/examples/simple_server.rs b/examples/simple_server.rs index 44210ae..c7c22ec 100644 --- a/examples/simple_server.rs +++ b/examples/simple_server.rs @@ -1,5 +1,8 @@ +use rand::{thread_rng, Rng}; use std::sync::Arc; use tapir_cwtch::acns::tor::authentication::HashedPassword; +use tapir_cwtch::acns::tor::run::TorRunner; +use tapir_cwtch::acns::tor::torrc::TorrcGenerator; use tapir_cwtch::acns::tor::TorProcess; use tapir_cwtch::applications::authentication_app::AuthenicationApp; use tapir_cwtch::connections::service::Service; @@ -8,7 +11,23 @@ use tapir_cwtch::primitives::identity::Identity; use tapir_cwtch::primitives::transcript::Transcript; fn main() { - let mut auth_control_port = TorProcess::connect(9051) + let tor_path = which::which("tor"); + let mut rng = thread_rng(); + let socks_port = rng.gen_range(10052, 11100); + let control_port = rng.gen_range(9052, 9100); + let mut tor_runner = TorRunner::run( + TorrcGenerator::new() + .with_socks_port(socks_port) + .with_control_port(control_port) + .with_hashed_control_password("examplehashedpassword"), + "./example_server_rc", + tor_path.unwrap().to_str().unwrap(), + "./examples/server_data_dir/", + ) + .unwrap(); + tor_runner.wait_until_bootstrapped(); + + let mut auth_control_port = TorProcess::connect(control_port) .unwrap() .authenticate(Box::new(HashedPassword::new(String::from("examplehashedpassword")))) .unwrap(); @@ -20,7 +39,7 @@ fn main() { println!("Service Id: {}", service_id); println!("Setup: {}", identity.hostname()); - let service = Service::init(identity.clone()); + let service = Service::init(identity.clone(), 0); let inbound_service = |conn: Connection| { let mut transcript = Transcript::new_transcript("tapir-transcript"); diff --git a/src/acns/mod.rs b/src/acns/mod.rs index 8df1701..60b2a81 100644 --- a/src/acns/mod.rs +++ b/src/acns/mod.rs @@ -1,7 +1,9 @@ +#[cfg(any(feature = "onionv3"))] pub mod tor; #[derive(Debug)] pub enum ACNError { + ConfigurationError(String), AuthenticationError(String), ServiceSetupError(String), } diff --git a/src/acns/tor/mod.rs b/src/acns/tor/mod.rs index 0e473cb..ab5fb6c 100644 --- a/src/acns/tor/mod.rs +++ b/src/acns/tor/mod.rs @@ -7,6 +7,8 @@ use std::io::{BufRead, BufReader, Error}; use std::net::TcpStream; pub mod authentication; +pub mod run; +pub mod torrc; #[derive(Debug)] pub struct TorDisconnected(()); diff --git a/src/acns/tor/run.rs b/src/acns/tor/run.rs new file mode 100644 index 0000000..de96e11 --- /dev/null +++ b/src/acns/tor/run.rs @@ -0,0 +1,75 @@ +use crate::acns::tor::torrc::TorrcGenerator; +use crate::acns::ACNError; +use std::io::{BufRead, BufReader}; +use std::process::{Child, Command, Stdio}; + +#[derive(Debug)] +pub struct TorRunner { + process: Box, +} + +impl TorRunner { + /// run Tor at the given tor_path with the given torrc + pub fn run(torrc: TorrcGenerator, torrc_path: &str, tor_path: &str, tor_data_dir: &str) -> Result { + match torrc.build(torrc_path) { + Ok(()) => { + let child = Command::new(tor_path) + .arg("-f") + .arg(torrc_path) + .arg("--DataDirectory") + .arg(tor_data_dir) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + Ok(TorRunner { process: Box::new(child) }) + } + Err(e) => Err(e), + } + } + + pub fn wait_until_bootstrapped(&mut self) { + let child_stdout = self.process.stdout.as_mut().unwrap(); + let mut reader = BufReader::new(child_stdout); + loop { + let mut line = String::new(); + reader.read_line(&mut line); + if line.is_empty() == false { + if line.contains("Bootstrapped 100% (done): Done") { + return; + } + } + } + } +} + +/// Kill Tor when the TorRunner drops out of scope +impl Drop for TorRunner { + fn drop(&mut self) { + match self.process.kill() { + Ok(()) => eprintln!("Killing Tor...{:?}", self.process.wait()), + Err(_err) => eprintln!("Could not Kill Tor..."), + } + } +} + +#[cfg(test)] +mod tests { + use crate::acns::tor::run::TorRunner; + use crate::acns::tor::torrc::TorrcGenerator; + use std::fs::remove_file; + use std::thread::sleep; + use std::time::Duration; + + #[test] + fn test_run_tor() { + match TorRunner::run(TorrcGenerator::new().with_socks_port(9055), "./torrc", "/usr/sbin/tor") { + Ok(runner) => { + sleep(Duration::new(5, 0)); + assert!(remove_file("./torrc").is_ok()); + println!("Runner {:?}", runner) + } + _ => panic!("tor did not run"), + } + sleep(Duration::new(10, 0)); + } +} diff --git a/src/acns/tor/torrc.rs b/src/acns/tor/torrc.rs new file mode 100644 index 0000000..e7dfffe --- /dev/null +++ b/src/acns/tor/torrc.rs @@ -0,0 +1,130 @@ +use crate::acns::ACNError; +use crate::acns::ACNError::ConfigurationError; +use std::fs::File; +use std::io::Write; + +/// Build a Torrc File with a set of configured options. Note: This is not a complete Torrc builder +/// although it might one day grow to be one. As it stands it only allows the configuration options +/// most necessary for Tor v3 Onion Service clients/servers as they related to Tapir/Cwtch. +/// We use the Builder patter to allow easy programmatic construction for different Tor configurations. +pub struct TorrcGenerator { + socks_port: u16, + control_port: u16, + hashed_password: String, +} + +impl TorrcGenerator { + /// Generate a new, empty Torrc Generator. + pub fn new() -> TorrcGenerator { + TorrcGenerator { + socks_port: 0, + control_port: 0, + hashed_password: String::new(), + } + } + + /// Configure Tor to open a port to connect to external servers over Tor via SOCKS5 + pub fn with_socks_port(mut self, socks_port: u16) -> Self { + self.socks_port = socks_port; + self + } + + /// Configure Tor to have an open Control Port for configuration. + pub fn with_control_port(mut self, control_port: u16) -> Self { + self.control_port = control_port; + self + } + + /// Force the tor control port to accept a hashed password as authentication, the given password + /// is hashed with a random salt + /// From the Tor Docs: If the 'HashedControlPassword' option is set, it must contain the salted + /// hash of a secret password. The salted hash is computed according to the + /// S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier. + /// This is then encoded in hexadecimal, prefixed by the indicator sequence + /// "16:". + pub fn with_hashed_control_password(mut self, password: &str) -> Self { + let salt: [u8; 8] = rand::random(); + self.hashed_password = TorrcGenerator::generate_hashed_password(&salt, password); + self + } + + /// generate_hashed_password calculates a hash in the same way tha tor --hash-password does + /// this function takes a salt as input which is not great from an api-misuse perspective, but + /// we make it private. + fn generate_hashed_password(salt: &[u8; 8], password: &str) -> String { + let c = 96; + let mut count = (16 + (c & 15)) << ((c >> 4) + 6); + let mut tmp = vec![]; + tmp.extend_from_slice(salt); + tmp.extend_from_slice(password.as_bytes()); + let slen = tmp.len(); + let mut hash = sha1::Sha1::new(); + while count != 0 { + if count > slen { + hash.update(tmp.as_slice()); + count -= slen + } else { + let tmp_slice = tmp.as_slice(); + hash.update(tmp_slice.split_at(count).0); + count = 0 + } + } + let hashed = hash.digest().bytes(); + return format!("16:{}{:X}{}", hex::encode_upper(salt), c, hex::encode_upper(hashed)); + } + + /// Compile the final torrc file and write it to the file system. + pub fn build(&self, file_name: &str) -> Result<(), ACNError> { + let mut file = match File::create(file_name) { + Err(why) => return Err(ConfigurationError(why.to_string())), + Ok(file) => file, + }; + let mut lines = vec![]; + + if self.socks_port != 0 { + lines.push(format!("SocksPort {}", self.socks_port)); + } + + if self.control_port != 0 { + lines.push(format!("ControlPort {}", self.control_port)); + } + + if self.hashed_password.is_empty() == false { + lines.push(format!("HashedControlPassword {}", self.hashed_password)); + } + + match file.write_all(lines.join("\n").as_bytes()) { + Ok(_) => Ok(()), + Err(why) => Err(ConfigurationError(why.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use crate::acns::tor::torrc::TorrcGenerator; + use std::fs::{read_to_string, remove_file}; + use std::path::Path; + + #[test] + fn generate_hashed_password() { + let salt: [u8; 8] = [0xC1, 0x53, 0x05, 0xF9, 0x77, 0x89, 0x41, 0x4B]; + assert_eq!( + String::from("16:C15305F97789414B601259E3EC5E76B8E55FC56A9F562B713F3D2BA257"), + TorrcGenerator::generate_hashed_password(&salt, "examplehashedpassword") + ); + } + + #[test] + fn generate_torrc() { + let gen = TorrcGenerator::new().with_control_port(9051).with_hashed_control_password("examplehashedpassword"); + match gen.build("./torrc") { + Ok(()) => { + assert!(Path::new("./torrc").exists()); + println!("{}", read_to_string("./torrc").unwrap()); + assert!(remove_file("./torrc").is_ok()); + } + _ => assert!(false), + } + } +} diff --git a/src/connections/service.rs b/src/connections/service.rs index 092d378..dbbb7c6 100644 --- a/src/connections/service.rs +++ b/src/connections/service.rs @@ -13,6 +13,7 @@ pub struct ApplicationListenService(()); pub struct Service { identity: Arc, + socks_port: u16, listen_service: ListenService, } @@ -21,7 +22,7 @@ impl Service { where F: FnOnce(Connection) + Send + Clone + 'static, { - let conn = Socks5Stream::connect(format!("127.0.0.1:9050"), Domain(format!("{}.onion", hostname), 9878)); + let conn = Socks5Stream::connect(format!("127.0.0.1:{}", self.socks_port), Domain(format!("{}.onion", hostname), 9878)); match conn { Ok(conn) => { let application = application.clone(); @@ -34,9 +35,10 @@ impl Service { } impl Service { - pub fn init(identity: Arc) -> Service { + pub fn init(identity: Arc, socks_port: u16) -> Service { Service { identity, + socks_port, listen_service: NoListenService(()), } } @@ -66,6 +68,7 @@ impl Service { Ok(Service { identity: self.identity, + socks_port: self.socks_port, listen_service: jh, }) } @@ -94,7 +97,7 @@ mod tests { let keypair = ed25519_dalek::Keypair::generate(&mut csprng); let _secret_key = SecretKey::from_bytes(&keypair.secret.to_bytes()); let identity = Identity::initialize(keypair); - let _service = Service::init(identity); + let _service = Service::init(identity, 9051); //let mut listen_service = service.listen(10000, TranscriptApp::new_instance()).unwrap_or_else(|_| panic!()); // this will not compile! wish we could test that service.connect(Hostname{},TranscriptApp::new_instance()); } @@ -105,7 +108,7 @@ mod tests { let keypair = ed25519_dalek::Keypair::generate(&mut csprng); let _secret_key = SecretKey::from_bytes(&keypair.secret.to_bytes()); let identity = Identity::initialize(keypair); - let _service = Service::init(identity); + let _service = Service::init(identity, 9051); //let listen_service = service.listen(1000, TranscriptApp::new_instance()).unwrap_or_else(|_| panic!()); // TODO use trybuild to test that this fails: service.connect(Hostname{},TranscriptApp::new_instance()); }