diff --git a/Cargo.lock b/Cargo.lock index 8f94fa8..8288eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,46 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "342258dd14006105c2b75ab1bd7543a03bdf0cfc94383303ac212a04939dff6f" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-wincon", + "concolor-override", + "concolor-query", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ea9e81bd02e310c216d080f6223c179012256e5151c41db88d12c88a1684d2" + +[[package]] +name = "anstyle-parse" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7d1bb534e9efed14f3e5f44e7dd1a4f709384023a4165199a4241e18dff0116" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-wincon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3127af6145b149f3287bb9a0d10ad9c5692dba8c53ad48285e5bec4063834fa" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.70" @@ -186,12 +226,47 @@ checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", - "clap_lex", + "clap_lex 0.2.4", "indexmap", "strsim", "termcolor", "textwrap 0.16.0", - "yaml-rust", +] + +[[package]] +name = "clap" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046ae530c528f252094e4a77886ee1374437744b2bff1497aa898bbddbbb29b3" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex 0.4.1", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.14", ] [[package]] @@ -203,6 +278,12 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + [[package]] name = "cmake" version = "0.1.49" @@ -212,6 +293,21 @@ dependencies = [ "cc", ] +[[package]] +name = "concolor-override" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a855d4a1978dc52fb0536a04d384c2c0c1aa273597f08b77c8c4d3b2eec6037f" + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -429,6 +525,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -590,12 +692,6 @@ dependencies = [ "zip", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -656,6 +752,18 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + [[package]] name = "nom" version = "7.1.3" @@ -778,18 +886,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -869,18 +977,21 @@ version = "0.1.2-rc.4" dependencies = [ "anyhow", "base64", - "clap 3.2.23", + "clap 4.2.1", "criterion", "env_logger 0.10.0", "lazy_static", "libsodium-sys-stable", "log", "memoffset 0.6.5", + "mio", "oqs-sys", "paste", + "serde", "static_assertions", "test_bin", "thiserror", + "toml", ] [[package]] @@ -954,9 +1065,12 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.152" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_cbor" @@ -970,13 +1084,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.152" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -990,6 +1104,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "1.1.0" @@ -1025,6 +1148,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tar" version = "0.4.38" @@ -1083,7 +1217,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1111,6 +1245,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-bidi" version = "0.3.10" @@ -1170,6 +1338,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1187,6 +1361,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.84" @@ -1208,7 +1388,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -1230,7 +1410,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1378,6 +1558,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "xattr" version = "0.2.3" @@ -1387,15 +1576,6 @@ dependencies = [ "libc", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "zip" version = "0.6.4" diff --git a/Cargo.toml b/Cargo.toml index 84a3131..dfcc8ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ harness = false [dependencies] anyhow = { version = "1.0.52", features = ["backtrace"] } base64 = "0.13.0" -clap = { version = "3.0.0", features = ["yaml"] } static_assertions = "1.1.0" memoffset = "0.6.5" libsodium-sys-stable = { version = "1.19.26", features = ["use-pkg-config"] } @@ -26,6 +25,10 @@ thiserror = "1.0.38" paste = "1.0.11" log = { version = "0.4.17", optional = true } env_logger = { version = "0.10.0", optional = true } +serde = { version = "1.0.160", features = ["derive"] } +toml = "0.7.3" +clap = { version = "4.2.1", features = ["derive"] } +mio = { version = "0.8.6", features = ["net", "os-poll"] } [build-dependencies] anyhow = "1.0.70" diff --git a/config-examples/.gitignore b/config-examples/.gitignore new file mode 100644 index 0000000..8ddab45 --- /dev/null +++ b/config-examples/.gitignore @@ -0,0 +1,2 @@ +peer-*-*-key +peer-*-out \ No newline at end of file diff --git a/config-examples/peer-a-config.toml b/config-examples/peer-a-config.toml new file mode 100644 index 0000000..a768406 --- /dev/null +++ b/config-examples/peer-a-config.toml @@ -0,0 +1,18 @@ +public_key = "peer-a-public-key" +secret_key = "peer-a-secret-key" +listen = ["[::]:10001"] +verbosity = "Quiet" + +[[peers]] +public_key = "peer-b-public-key" +endpoint = "localhost:10002" +key_out = "peer-a-rp-out-key" +# exchange_command = [ +# "wg", +# "set", +# "wg0", +# "peer", +# "", +# "preshared-key", +# "/dev/stdin", +# ] diff --git a/config-examples/peer-b-config.toml b/config-examples/peer-b-config.toml new file mode 100644 index 0000000..7b647db --- /dev/null +++ b/config-examples/peer-b-config.toml @@ -0,0 +1,18 @@ +public_key = "peer-b-public-key" +secret_key = "peer-b-secret-key" +listen = ["[::]:10002"] +verbosity = "Quiet" + +[[peers]] +public_key = "peer-a-public-key" +endpoint = "localhost:10001" +key_out = "peer-b-rp-out-key" +# exchange_command = [ +# "wg", +# "set", +# "wg0", +# "peer", +# "", +# "preshared-key", +# "/dev/stdin", +# ] diff --git a/src/app_server.rs b/src/app_server.rs new file mode 100644 index 0000000..e12e14c --- /dev/null +++ b/src/app_server.rs @@ -0,0 +1,442 @@ +use anyhow::bail; + +use anyhow::Result; +use log::{error, info}; +use mio::Interest; +use mio::Token; + +use std::io::Write; + +use std::io::ErrorKind; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::net::SocketAddr; +use std::net::SocketAddrV4; +use std::net::SocketAddrV6; +use std::path::PathBuf; +use std::process::Command; +use std::process::Stdio; +use std::time::Duration; + +use crate::util::fopen_w; +use crate::{ + config::Verbosity, + protocol::{CryptoServer, MsgBuf, PeerPtr, SPk, SSk, SymKey, Timing}, + util::{b64_writer, fmt_b64}, +}; + +#[derive(Default, Debug)] +pub struct AppPeer { + pub outfile: Option, + pub outwg: Option, // TODO make this a generic command + pub tx_addr: Option, +} + +#[derive(Default, Debug)] +pub struct WireguardOut { + // impl KeyOutput + pub dev: String, + pub pk: String, + pub extra_params: Vec, +} + +/// Holds the state of the application, namely the external IO +/// +/// Responsible for file IO, network IO +// TODO add user control via unix domain socket and stdin/stdout +#[derive(Debug)] +pub struct AppServer { + pub crypt: CryptoServer, + pub sockets: Vec, + pub events: mio::Events, + pub mio_poll: mio::Poll, + pub peers: Vec, + pub verbosity: Verbosity, + pub all_sockets_drained: bool, +} + +/// Index based pointer to a Peer +#[derive(Debug)] +pub struct AppPeerPtr(pub usize); + +impl AppPeerPtr { + /// Takes an index based handle and returns the actual peer + pub fn lift(p: PeerPtr) -> Self { + Self(p.0) + } + + /// Returns an index based handle to one Peer + pub fn lower(&self) -> PeerPtr { + PeerPtr(self.0) + } + + pub fn get_app<'a>(&self, srv: &'a AppServer) -> &'a AppPeer { + &srv.peers[self.0] + } + + pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer { + &mut srv.peers[self.0] + } +} + +#[derive(Debug)] +pub enum AppPollResult { + DeleteKey(AppPeerPtr), + SendInitiation(AppPeerPtr), + SendRetransmission(AppPeerPtr), + ReceivedMessage(usize, SocketAddr), +} + +#[derive(Debug)] +pub enum KeyOutputReason { + Exchanged, + Stale, +} + +impl AppServer { + pub fn new( + sk: SSk, + pk: SPk, + addrs: Vec, + verbosity: Verbosity, + ) -> anyhow::Result { + // setup mio + let mio_poll = mio::Poll::new()?; + let events = mio::Events::with_capacity(8); + + // bind each SocketAddr to a socket + let maybe_sockets: Result, _> = + addrs.into_iter().map(mio::net::UdpSocket::bind).collect(); + let mut sockets = maybe_sockets?; + + // if there is no socket, just listen to anything + if sockets.is_empty() { + // port 0 means the OS can pick any free port + let port = 0; + + let ipv4_any = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port)); + + let ipv6_any = SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), + port, + 0, + 0, + )); + + // bind to IPv4 + sockets.push(mio::net::UdpSocket::bind(ipv4_any)?); + + // and try to bind to IPv6, just in case + match mio::net::UdpSocket::bind(ipv6_any) { + Ok(socket) => sockets.push(socket), + Err(e) if e.kind() == ErrorKind::AddrInUse => { /* shrugs, seems to be a IPv4/IPv6 dual stack OS */ + } + Err(e) => return Err(e.into()), + } + } + + // register all sockets to mio + for (i, socket) in sockets.iter_mut().enumerate() { + mio_poll + .registry() + .register(socket, Token(i), Interest::READABLE)?; + } + + // TODO use mio::net::UnixStream together with std::os::unix::net::UnixStream for linux + + Ok(Self { + crypt: CryptoServer::new(sk, pk), + peers: Vec::new(), + verbosity, + sockets, + events, + mio_poll, + all_sockets_drained: false, + }) + } + + pub fn verbose(&self) -> bool { + matches!(self.verbosity, Verbosity::Verbose) + } + + pub fn add_peer( + &mut self, + psk: Option, + pk: SPk, + outfile: Option, + outwg: Option, + tx_addr: Option, + ) -> anyhow::Result { + let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?; + assert!(pn == self.peers.len()); + self.peers.push(AppPeer { + outfile, + outwg, + tx_addr, + }); + Ok(AppPeerPtr(pn)) + } + + pub fn listen_loop(&mut self) -> anyhow::Result<()> { + const INIT_SLEEP: f64 = 0.01; + const MAX_FAILURES: i32 = 10; + let mut failure_cnt = 0; + + loop { + let msgs_processed = 0usize; + let err = match self.event_loop() { + Ok(()) => return Ok(()), + Err(e) => e, + }; + + // This should not happen… + failure_cnt = if msgs_processed > 0 { + 0 + } else { + failure_cnt + 1 + }; + let sleep = INIT_SLEEP * 2.0f64.powf(f64::from(failure_cnt - 1)); + let tries_left = MAX_FAILURES - (failure_cnt - 1); + error!( + "unexpected error after processing {} messages: {:?} {}", + msgs_processed, + err, + err.backtrace() + ); + if tries_left > 0 { + error!("reinitializing networking in {sleep}! {tries_left} tries left."); + std::thread::sleep(self.crypt.timebase.dur(sleep)); + continue; + } + + bail!("too many network failures"); + } + } + + pub fn event_loop(&mut self) -> anyhow::Result<()> { + let (mut rx, mut tx) = (MsgBuf::zero(), MsgBuf::zero()); + + /// if socket address for peer is known, call closure + /// assumes that closure leaves a message in `tx` + /// assumes that closure returns the length of message in bytes + macro_rules! tx_maybe_with { + ($peer:expr, $fn:expr) => { + attempt!({ + let p = $peer.get_app(self); + if let Some(addr) = p.tx_addr { + let len = $fn()?; + self.try_send(&tx[..len], addr)?; + } + Ok(()) + }) + }; + } + + loop { + use crate::protocol::HandleMsgResult; + use AppPollResult::*; + use KeyOutputReason::*; + match self.poll(&mut *rx)? { + SendInitiation(peer) => tx_maybe_with!(peer, || self + .crypt + .initiate_handshake(peer.lower(), &mut *tx))?, + SendRetransmission(peer) => tx_maybe_with!(peer, || self + .crypt + .retransmit_handshake(peer.lower(), &mut *tx))?, + DeleteKey(peer) => self.output_key(peer, Stale, &SymKey::random())?, + + ReceivedMessage(len, addr) => { + match self.crypt.handle_msg(&rx[..len], &mut *tx) { + Err(ref e) => { + self.verbose().then(|| { + info!( + "error processing incoming message from {:?}: {:?} {}", + addr, + e, + e.backtrace() + ); + }); + } + + Ok(HandleMsgResult { + resp, + exchanged_with, + .. + }) => { + if let Some(len) = resp { + self.try_send(&tx[0..len], addr)?; + } + + if let Some(p) = exchanged_with { + let ap = AppPeerPtr::lift(p); + ap.get_app_mut(self).tx_addr = Some(addr); + + // TODO: Maybe we should rather call the key "rosenpass output"? + self.output_key(ap, Exchanged, &self.crypt.osk(p)?)?; + } + } + } + } + }; + } + } + + pub fn output_key( + &self, + peer: AppPeerPtr, + why: KeyOutputReason, + key: &SymKey, + ) -> anyhow::Result<()> { + let peerid = peer.lower().get(&self.crypt).pidt()?; + let ap = peer.get_app(self); + + if self.verbose() { + let msg = match why { + KeyOutputReason::Exchanged => "Exchanged key with peer", + KeyOutputReason::Stale => "Erasing outdated key from peer", + }; + info!("{} {}", msg, fmt_b64(&*peerid)); + } + + if let Some(of) = ap.outfile.as_ref() { + // This might leave some fragments of the secret on the stack; + // in practice this is likely not a problem because the stack likely + // will be overwritten by something else soon but this is not exactly + // guaranteed. It would be possible to remedy this, but since the secret + // data will linger in the linux page cache anyways with the current + // implementation, going to great length to erase the secret here is + // not worth it right now. + b64_writer(fopen_w(of)?).write_all(key.secret())?; + let why = match why { + KeyOutputReason::Exchanged => "exchanged", + KeyOutputReason::Stale => "stale", + }; + + // this is intentionally writing to stdout instead of stderr, because + // it is meant to allow external detection of a succesful key-exchange + println!( + "output-key peer {} key-file {of:?} {why}", + fmt_b64(&*peerid) + ); + } + + if let Some(owg) = ap.outwg.as_ref() { + let child = Command::new("wg") + .arg("set") + .arg(&owg.dev) + .arg("peer") + .arg(&owg.pk) + .arg("preshared-key") + .arg("/dev/stdin") + .stdin(Stdio::piped()) + .args(&owg.extra_params) + .spawn()?; + b64_writer(child.stdin.unwrap()).write_all(key.secret())?; + } + + Ok(()) + } + + pub fn poll(&mut self, rx_buf: &mut [u8]) -> anyhow::Result { + use crate::protocol::PollResult as C; + use AppPollResult as A; + loop { + return Ok(match self.crypt.poll()? { + C::DeleteKey(PeerPtr(no)) => A::DeleteKey(AppPeerPtr(no)), + C::SendInitiation(PeerPtr(no)) => A::SendInitiation(AppPeerPtr(no)), + C::SendRetransmission(PeerPtr(no)) => A::SendRetransmission(AppPeerPtr(no)), + C::Sleep(timeout) => match self.try_recv(rx_buf, timeout)? { + Some((len, addr)) => A::ReceivedMessage(len, addr), + None => continue, + }, + }); + } + } + + /// Tries to receive a new message + /// + /// - might wait for an duration up to `timeout` + /// - returns immediately if an error occurs + /// - returns immediately if a new message is received + pub fn try_recv( + &mut self, + buf: &mut [u8], + timeout: Timing, + ) -> anyhow::Result> { + let timeout = Duration::from_secs_f64(timeout); + + // if there is no time to wait on IO, well, then, lets not waste any time! + if timeout.is_zero() { + return Ok(None); + } + + // NOTE when using mio::Poll, there are some finickies (taken from + // https://docs.rs/mio/latest/mio/struct.Poll.html): + // + // - poll() might return readiness, even if nothing is ready + // - in this case, a WouldBlock error is returned from actual IO operations + // - after receiving readiness for a source, it must be drained until a WouldBlock + // is received + // + // This would ususally require us to maintain the drainage status of each socket; + // a socket would only become drained when it returned WouldBlock and only + // non-drained when receiving a readiness event from mio for it. Then, only the + // ready sockets should be worked on, ideally without requiring an O(n) search + // through all sockets for checking their drained status. However, our use-case + // is primarily heaving one or two sockets (if IPv4 and IPv6 IF_ANY listen is + // desired on a non-dual-stack OS), thus just checking every socket after any + // readiness event seems to be good enough™ for now. + + // only poll if we drained all sockets before + if self.all_sockets_drained { + self.mio_poll.poll(&mut self.events, Some(timeout))?; + } + + let mut would_block_count = 0; + for socket in &mut self.sockets { + match socket.recv_from(buf) { + Ok(x) => { + // at least one socket was not drained... + self.all_sockets_drained = false; + return Ok(Some(x)); + } + Err(e) if e.kind() == ErrorKind::WouldBlock => { + would_block_count += 1; + } + // TODO if one socket continuesly returns an error, then we never poll, thus we never wait for a timeout, thus we have a spin-lock + Err(e) => return Err(e.into()), + } + } + + // if each socket returned WouldBlock, then we drained them all at least once indeed + self.all_sockets_drained = would_block_count == self.sockets.len(); + + Ok(None) + } + + /// Try to send a message + /// + /// Every available socket is tried once + // TODO cache what socket worked last time + // TODO cache what socket we received from last time for that addr + pub fn try_send(&mut self, buf: &[u8], addr: SocketAddr) -> anyhow::Result<()> { + for socket in &self.sockets { + return match socket.send_to(&buf, addr) { + Ok(_) => Ok(()), + + // TODO replace this by + // Err(e) if e.kind() == io::ErrorKind::NetworkUnreachable => continue, + // once https://github.com/rust-lang/rust/issues/86442 lands + Err(e) + if e.to_string() + .starts_with("Address family not supported by protocol") => + { + continue + } + Err(e) => Err(e.into()), + }; + } + + bail!("none of our sockets matched the address family {}", addr); + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..4ff793b --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,265 @@ +use anyhow::{bail, ensure}; +use clap::Parser; +use std::net::ToSocketAddrs; +use std::path::{Path, PathBuf}; + +use crate::app_server::AppServer; +use crate::util::{LoadValue, LoadValueB64}; +use crate::{ + // app_server::{AppServer, LoadValue, LoadValueB64}, + coloring::Secret, + pqkem::{StaticKEM, KEM}, + protocol::{SPk, SSk, SymKey}, +}; + +use super::config; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about)] +pub enum Cli { + /// Start Rosenpass in server mode and carry on with the key exchange + /// + /// This will parse the configuration file and perform the key exchange + /// with the specified peers. If a peer's endpoint is specified, this + /// Rosenpass instance will try to initiate a key exchange with the peer, + /// otherwise only initiation attempts from the peer will be responded to. + ExchangeConfig { config_file: PathBuf }, + + /// Start in daemon mode, performing key exchanges + /// + /// The configuration is read from the command line. The `peer` token + /// always separates multiple peers, e. g. if the token `peer` appears + /// in the WIREGUARD_EXTRA_ARGS it terminates is not put into the + /// WireGuard arguments but instead a new peer is created. + /* Explanation: `first_arg` and `rest_of_args` are combined into one + * `Vec`. They are only used to trick clap into displaying some + * guidance on the CLI usage. + */ + Exchange { + /// public-key secret-key [listen :] [verbose] + #[clap(value_name = "OWN_CONFIG")] + first_arg: String, + + /// peer public-key [ENDPOINT] [PSK] [OUTFILE] [WG] + /// + /// ENDPOINT := [endpoint :] + /// + /// PSK := [preshared-key ] + /// + /// OUTFILE := [outfile ] + /// + /// WG := [wireguard [WIREGUARD_EXTRA_ARGS]...] + #[clap(value_names = [ +"peer", "public-key", "", "[ENDPOINT]" ,"[PSK]", "[OUTFILE]", "[WG]" + ])] + rest_of_args: Vec, + + /// Save the parsed configuration to a file before starting the daemon + #[clap(short, long)] + config_file: Option, + }, + + /// Generate a demo config file + GenConfig { + config_file: PathBuf, + + /// Forecefully overwrite existing config file + #[clap(short, long)] + force: bool, + }, + + /// Generate the keys mentioned in a configFile + /// + /// Generates secret- & public-key to their destination. If a config file + /// is provided then the key file destination is taken from there. + /// Otherwise the + GenKeys { + config_file: Option, + + /// where to write public-key to + #[clap(short, long)] + public_key: Option, + + /// where to write secret-key to + #[clap(short, long)] + secret_key: Option, + + /// Forecefully overwrite public- & secret-key file + #[clap(short, long)] + force: bool, + }, + + /// Validate a configuration + Validate { config_files: Vec }, + + /// Show the rosenpass manpage + // TODO make this the default, but only after the manpage has been adjusted once the CLI stabilizes + Man, +} + +impl Cli { + pub fn run() -> anyhow::Result<()> { + let cli = Self::parse(); + + use Cli::*; + match cli { + Man => { + let _man_cmd = std::process::Command::new("man") + .args(["1", "rosenpass"]) + .status(); + } + GenConfig { config_file, force } => { + ensure!( + force || !config_file.exists(), + "config file {config_file:?} already exists" + ); + + config::Rosenpass::example_config().store(config_file)?; + } + + GenKeys { + config_file, + public_key, + secret_key, + force, + } => { + // figure out where the key file is specified, in the config file or directly as flag? + let (pkf, skf) = match (config_file, public_key, secret_key) { + (Some(config_file), _, _) => { + ensure!( + config_file.exists(), + "config file {config_file:?} does not exist" + ); + + let config = config::Rosenpass::load(config_file)?; + + (config.public_key, config.secret_key) + } + (_, Some(pkf), Some(skf)) => (pkf, skf), + _ => { + bail!("either a config-file or both public-key and secret-key file are required") + } + }; + + // check that we are not overriding something unintentionally + let mut problems = vec![]; + if !force && pkf.is_file() { + problems.push(format!( + "public-key file {pkf:?} exist, refusing to overwrite it" + )); + } + if !force && skf.is_file() { + problems.push(format!( + "secret-key file {skf:?} exist, refusing to overwrite it" + )); + } + if !problems.is_empty() { + bail!(problems.join("\n")); + } + + // generate the keys and store them in files + let mut ssk = crate::protocol::SSk::random(); + let mut spk = crate::protocol::SPk::random(); + + unsafe { + StaticKEM::keygen(ssk.secret_mut(), spk.secret_mut())?; + ssk.store_secret(skf)?; + spk.store_secret(pkf)?; + } + } + + ExchangeConfig { config_file } => { + ensure!( + config_file.exists(), + "config file '{config_file:?}' does not exist" + ); + + let config = config::Rosenpass::load(config_file)?; + config.validate()?; + Self::event_loop(config)?; + } + + Exchange { + first_arg, + mut rest_of_args, + config_file, + } => { + rest_of_args.insert(0, first_arg); + let args = rest_of_args; + let mut config = config::Rosenpass::parse_args(args)?; + + if let Some(p) = config_file { + config.store(&p)?; + config.config_file_path = p; + } + config.validate()?; + Self::event_loop(config)?; + } + + Validate { config_files } => { + for file in config_files { + match config::Rosenpass::load(&file) { + Ok(config) => { + eprintln!("{file:?} is valid TOML and conforms to the expected schema"); + match config.validate() { + Ok(_) => eprintln!("{file:?} is passed all logical checks"), + Err(_) => eprintln!("{file:?} contains logical errors"), + } + } + Err(e) => eprintln!("{file:?} is not valid: {e}"), + } + } + } + } + + Ok(()) + } + + fn event_loop(config: config::Rosenpass) -> anyhow::Result<()> { + // dump config + eprintln!("{config:#?}"); + + // load own keys + let sk = SSk::load(&config.secret_key)?; + let pk = SPk::load(&config.public_key)?; + + // start an application server + let mut srv = std::boxed::Box::::new(AppServer::new( + sk, + pk, + config.listen, + config.verbosity, + )?); + + for cfg_peer in config.peers { + let endpoint = cfg_peer + .endpoint + .as_ref() + .map(ToSocketAddrs::to_socket_addrs) + .transpose()? + .and_then(|mut i| i.next()); + + srv.add_peer( + // psk, pk, outfile, outwg, tx_addr + cfg_peer.pre_shared_key.map(SymKey::load_b64).transpose()?, + SPk::load(&cfg_peer.public_key)?, + cfg_peer.key_out, + None, // TODO remove this argument + endpoint, + )?; + } + + srv.event_loop() + } +} + +trait StoreSecret { + unsafe fn store_secret>(&self, path: P) -> anyhow::Result<()>; +} + +impl StoreSecret for Secret { + unsafe fn store_secret>(&self, path: P) -> anyhow::Result<()> { + std::fs::write(path, self.secret())?; + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..154edab --- /dev/null +++ b/src/config.rs @@ -0,0 +1,449 @@ +use std::{ + collections::HashSet, + fs, + io::Write, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}, + path::{Path, PathBuf}, +}; + +use anyhow::{bail, ensure}; +use serde::{Deserialize, Serialize}; + +use crate::util::fopen_w; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Rosenpass { + pub public_key: PathBuf, + + pub secret_key: PathBuf, + + pub listen: Vec, + + #[serde(default)] + pub verbosity: Verbosity, + pub peers: Vec, + + #[serde(skip)] + pub config_file_path: PathBuf, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Verbosity { + Quiet, + Verbose, +} + +#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct RosenpassPeer { + pub public_key: PathBuf, + pub endpoint: Option, + pub pre_shared_key: Option, + + #[serde(default)] + pub key_out: Option, + + // TODO make sure failure does not crash but is logged + #[serde(default)] + pub exchange_command: Vec, + + // TODO make this field only available on binary builds, not on library builds + #[serde(flatten)] + pub wg: Option, +} + +#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct WireGuard { + device: String, + peer: String, + extra_params: Vec, +} + +impl Rosenpass { + /// Load a config file from a file path + /// + /// no validation is conducted + pub fn load>(p: P) -> anyhow::Result { + let mut config: Self = toml::from_str(&fs::read_to_string(&p)?)?; + + config.config_file_path = p.as_ref().to_owned(); + Ok(config) + } + + /// Write a config to a file + pub fn store>(&self, p: P) -> anyhow::Result<()> { + let serialized_config = + toml::to_string_pretty(&self).expect("unable to serialize the default config"); + fs::write(p, serialized_config)?; + Ok(()) + } + + /// Commit the configuration to where it came from, overwriting the original file + pub fn commit(&self) -> anyhow::Result<()> { + let mut f = fopen_w(&self.config_file_path)?; + f.write_all(toml::to_string_pretty(&self)?.as_bytes())?; + + self.store(&self.config_file_path) + } + + /// Validate a configuration + pub fn validate(&self) -> anyhow::Result<()> { + // check the public-key file exists + ensure!( + self.public_key.is_file(), + "public-key file {:?} does not exist", + self.public_key + ); + + // check the secret-key file exists + ensure!( + self.secret_key.is_file(), + "secret-key file {:?} does not exist", + self.secret_key + ); + + for (i, peer) in self.peers.iter().enumerate() { + // check peer's public-key file exists + ensure!( + peer.public_key.is_file(), + "peer {i} public-key file {:?} does not exist", + peer.public_key + ); + + // check endpoint is usable + if let Some(addr) = peer.endpoint.as_ref() { + ensure!( + addr.to_socket_addrs().is_ok(), + "peer {i} endpoint {} can not be parsed to a socket address", + addr + ); + } + + // TODO warn if neither out_key nor exchange_command is defined + } + + Ok(()) + } + + /// Creates a new configuration + pub fn new, P2: AsRef>(public_key: P1, secret_key: P2) -> Self { + Self { + public_key: PathBuf::from(public_key.as_ref()), + secret_key: PathBuf::from(secret_key.as_ref()), + listen: vec![], + verbosity: Verbosity::Quiet, + peers: vec![], + config_file_path: PathBuf::new(), + } + } + + /// Add IPv4 __and__ IPv6 IF_ANY address to the listen interfaces + pub fn add_if_any(&mut self, port: u16) { + let ipv4_any = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), port)); + let ipv6_any = SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), + port, + 0, + 0, + )); + self.listen.push(ipv4_any); + self.listen.push(ipv6_any); + } + + /// from chaotic args + /// Quest: the grammar is undecideable, what do we do here? + pub fn parse_args(args: Vec) -> anyhow::Result { + let mut config = Self::new("", ""); + + #[derive(Debug, Hash, PartialEq, Eq)] + enum State { + Own, + OwnPublicKey, + OwnSecretKey, + OwnListen, + Peer, + PeerPsk, + PeerPublicKey, + PeerEndpoint, + PeerOutfile, + PeerWireguardDev, + PeerWireguardPeer, + PeerWireguardExtraArgs, + } + + let mut already_set = HashSet::new(); + + // TODO idea: use config.peers.len() to give index of peer with conflicting argument + use State::*; + let mut state = Own; + let mut current_peer = None; + let p_exists = "a peer should exist by now"; + let wg_exists = "a peer wireguard should exist by now"; + for arg in args { + state = match (state, arg.as_str(), &mut current_peer) { + (Own, "public-key", None) => OwnPublicKey, + (Own, "secret-key", None) => OwnSecretKey, + (Own, "listen", None) => OwnListen, + (Own, "verbose", None) => { + config.verbosity = Verbosity::Verbose; + Own + } + (Own, "peer", None) => { + ensure!( + already_set.contains(&OwnPublicKey), + "public-key file must be set" + ); + ensure!( + already_set.contains(&OwnSecretKey), + "secret-key file must be set" + ); + + already_set.clear(); + current_peer = Some(RosenpassPeer::default()); + + Peer + } + (OwnPublicKey, pk, None) => { + ensure!( + already_set.insert(OwnPublicKey), + "public-key was already set" + ); + config.public_key = pk.into(); + Own + } + (OwnSecretKey, sk, None) => { + ensure!( + already_set.insert(OwnSecretKey), + "secret-key was already set" + ); + config.secret_key = sk.into(); + Own + } + (OwnListen, l, None) => { + already_set.insert(OwnListen); // multiple listen directives are allowed + for socket_addr in l.to_socket_addrs()? { + config.listen.push(socket_addr); + } + + Own + } + (Peer | PeerWireguardExtraArgs, "peer", maybe_peer @ Some(_)) => { + // TODO check current peer + // commit current peer, create a new one + config.peers.push(maybe_peer.take().expect(p_exists)); + + already_set.clear(); + current_peer = Some(RosenpassPeer::default()); + + Peer + } + (Peer, "public-key", Some(_)) => PeerPublicKey, + (Peer, "endpoint", Some(_)) => PeerEndpoint, + (Peer, "preshared-key", Some(_)) => PeerPsk, + (Peer, "outfile", Some(_)) => PeerOutfile, + (Peer, "wireguard", Some(_)) => PeerWireguardDev, + (PeerPublicKey, pk, Some(peer)) => { + ensure!( + already_set.insert(PeerPublicKey), + "public-key was already set" + ); + peer.public_key = pk.into(); + Peer + } + (PeerEndpoint, e, Some(peer)) => { + ensure!(already_set.insert(PeerEndpoint), "endpoint was already set"); + peer.endpoint = Some(e.to_owned()); + Peer + } + (PeerPsk, psk, Some(peer)) => { + ensure!(already_set.insert(PeerEndpoint), "peer psk was already set"); + peer.pre_shared_key = Some(psk.into()); + Peer + } + (PeerOutfile, of, Some(peer)) => { + ensure!( + already_set.insert(PeerOutfile), + "peer outfile was already set" + ); + peer.key_out = Some(of.into()); + Peer + } + (PeerWireguardDev, dev, Some(peer)) => { + ensure!( + already_set.insert(PeerWireguardDev), + "peer wireguard-dev was already set" + ); + assert!(peer.wg.is_none()); + peer.wg = Some(WireGuard { + device: dev.to_string(), + ..Default::default() + }); + + PeerWireguardPeer + } + (PeerWireguardPeer, p, Some(peer)) => { + ensure!( + already_set.insert(PeerWireguardPeer), + "peer wireguard-peer was already set" + ); + peer.wg.as_mut().expect(wg_exists).peer = p.to_string(); + PeerWireguardExtraArgs + } + (PeerWireguardExtraArgs, arg, Some(peer)) => { + peer.wg + .as_mut() + .expect(wg_exists) + .extra_params + .push(arg.to_string()); + PeerWireguardExtraArgs + } + + // error cases + (Own, x, None) => { + bail!("unrecognised argument {x}"); + } + (Own | OwnPublicKey | OwnSecretKey | OwnListen, _, Some(_)) => { + panic!("current_peer is not None while in Own* state, this must never happen") + } + + (State::Peer, arg, Some(_)) => { + bail!("unrecongnised argument {arg}"); + } + ( + Peer + | PeerEndpoint + | PeerOutfile + | PeerPublicKey + | PeerPsk + | PeerWireguardDev + | PeerWireguardPeer + | PeerWireguardExtraArgs, + _, + None, + ) => { + panic!("got peer options but no peer was created") + } + }; + } + + if let Some(p) = current_peer { + // TODO ensure peer is propagated with sufficient information + config.peers.push(p); + } + + Ok(config) + } +} + +impl Rosenpass { + /// Generate an example configuration + pub fn example_config() -> Self { + let peer = RosenpassPeer { + public_key: "rp-peer-public-key".into(), + endpoint: Some("my-peer.test:9999".into()), + exchange_command: [ + "wg", + "set", + "wg0", + "peer", + "", + "preshared-key", + "/dev/stdin", + ] + .into_iter() + .map(|x| x.to_string()) + .collect(), + key_out: Some("rp-key-out".into()), + pre_shared_key: None, + wg: None, + }; + + Self { + public_key: "rp-public-key".into(), + secret_key: "rp-secret-key".into(), + peers: vec![peer], + ..Self::new("", "") + } + } +} + +impl Default for Verbosity { + fn default() -> Self { + Self::Quiet + } +} + +#[cfg(test)] +mod test { + use std::net::IpAddr; + + use super::*; + + fn split_str(s: &str) -> Vec { + s.split(" ").map(|s| s.to_string()).collect() + } + + #[test] + fn test_simple_cli_parse() { + let args = split_str( + "public-key /my/public-key secret-key /my/secret-key verbose \ + listen 0.0.0.0:9999 peer public-key /peer/public-key endpoint \ + peer.test:9999 outfile /peer/rp-out", + ); + + let config = Rosenpass::parse_args(args).unwrap(); + + assert_eq!(config.public_key, PathBuf::from("/my/public-key")); + assert_eq!(config.secret_key, PathBuf::from("/my/secret-key")); + assert_eq!(config.verbosity, Verbosity::Verbose); + assert_eq!( + &config.listen, + &vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 9999)] + ); + assert_eq!( + config.peers, + vec![RosenpassPeer { + public_key: PathBuf::from("/peer/public-key"), + endpoint: Some("peer.test:9999".into()), + pre_shared_key: None, + key_out: Some(PathBuf::from("/peer/rp-out")), + ..Default::default() + }] + ) + } + + #[test] + fn test_cli_parse_multiple_peers() { + let args = split_str( + "public-key /my/public-key secret-key /my/secret-key verbose \ + peer public-key /peer-a/public-key endpoint \ + peer.test:9999 outfile /peer-a/rp-out \ + peer public-key /peer-b/public-key outfile /peer-b/rp-out", + ); + + let config = Rosenpass::parse_args(args).unwrap(); + + assert_eq!(config.public_key, PathBuf::from("/my/public-key")); + assert_eq!(config.secret_key, PathBuf::from("/my/secret-key")); + assert_eq!(config.verbosity, Verbosity::Verbose); + assert!(&config.listen.is_empty()); + assert_eq!( + config.peers, + vec![ + RosenpassPeer { + public_key: PathBuf::from("/peer-a/public-key"), + endpoint: Some("peer.test:9999".into()), + pre_shared_key: None, + key_out: Some(PathBuf::from("/peer-a/rp-out")), + ..Default::default() + }, + RosenpassPeer { + public_key: PathBuf::from("/peer-b/public-key"), + endpoint: None, + pre_shared_key: None, + key_out: Some(PathBuf::from("/peer-b/rp-out")), + ..Default::default() + } + ] + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index bf5c514..46bc1ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,9 @@ pub mod sodium; pub mod coloring; #[rustfmt::skip] pub mod labeled_prf; +pub mod app_server; +pub mod cli; +pub mod config; pub mod msgs; pub mod pqkem; pub mod prftree; diff --git a/src/main.rs b/src/main.rs index 2e8acd8..f4af8ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,261 +1,11 @@ -use anyhow::{bail, ensure, Context, Result}; -use log::{error, info}; -use rosenpass::{ - attempt, - coloring::{Public, Secret}, - pqkem::{StaticKEM, KEM}, - protocol::{CryptoServer, MsgBuf, PeerPtr, SPk, SSk, SymKey, Timing}, - sodium::sodium_init, - util::{b64_reader, b64_writer, fmt_b64}, -}; -use std::{ - fs::{File, OpenOptions}, - io::{ErrorKind, Read, Write}, - net::{SocketAddr, ToSocketAddrs, UdpSocket}, - path::Path, - process::{exit, Command, Stdio}, - time::Duration, -}; - -/// Open a file writable -pub fn fopen_w>(path: P) -> Result { - Ok(OpenOptions::new() - .read(false) - .write(true) - .create(true) - .truncate(true) - .open(path)?) -} -/// Open a file readable -pub fn fopen_r>(path: P) -> Result { - Ok(OpenOptions::new() - .read(true) - .write(false) - .create(false) - .truncate(false) - .open(path)?) -} - -pub trait ReadExactToEnd { - fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()>; -} - -impl ReadExactToEnd for R { - fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()> { - let mut dummy = [0u8; 8]; - self.read_exact(buf)?; - ensure!(self.read(&mut dummy)? == 0, "File too long!"); - Ok(()) - } -} - -pub trait LoadValue { - fn load>(path: P) -> Result - where - Self: Sized; -} - -pub trait LoadValueB64 { - fn load_b64>(path: P) -> Result - where - Self: Sized; -} - -trait StoreValue { - fn store>(&self, path: P) -> Result<()>; -} - -trait StoreSecret { - unsafe fn store_secret>(&self, path: P) -> Result<()>; -} - -impl StoreSecret for T { - unsafe fn store_secret>(&self, path: P) -> Result<()> { - self.store(path) - } -} - -impl LoadValue for Secret { - fn load>(path: P) -> Result { - let mut v = Self::random(); - let p = path.as_ref(); - fopen_r(p)? - .read_exact_to_end(v.secret_mut()) - .with_context(|| format!("Could not load file {p:?}"))?; - Ok(v) - } -} - -impl LoadValueB64 for Secret { - fn load_b64>(path: P) -> Result { - let mut v = Self::random(); - let p = path.as_ref(); - // This might leave some fragments of the secret on the stack; - // in practice this is likely not a problem because the stack likely - // will be overwritten by something else soon but this is not exactly - // guaranteed. It would be possible to remedy this, but since the secret - // data will linger in the linux page cache anyways with the current - // implementation, going to great length to erase the secret here is - // not worth it right now. - b64_reader(&mut fopen_r(p)?) - .read_exact(v.secret_mut()) - .with_context(|| format!("Could not load base64 file {p:?}"))?; - Ok(v) - } -} - -impl StoreSecret for Secret { - unsafe fn store_secret>(&self, path: P) -> Result<()> { - std::fs::write(path, self.secret())?; - Ok(()) - } -} - -impl LoadValue for Public { - fn load>(path: P) -> Result { - let mut v = Self::random(); - fopen_r(path)?.read_exact_to_end(&mut *v)?; - Ok(v) - } -} - -impl StoreValue for Public { - fn store>(&self, path: P) -> Result<()> { - std::fs::write(path, **self)?; - Ok(()) - } -} - -macro_rules! bail_usage { - ($args:expr, $($pt:expr),*) => {{ - error!($($pt),*); - cmd_help()?; - exit(1); - }} -} - -macro_rules! ensure_usage { - ($args:expr, $ck:expr, $($pt:expr),*) => {{ - if !$ck { - bail_usage!($args, $($pt),*); - } - }} -} - -macro_rules! mandatory_opt { - ($args:expr, $val:expr, $name:expr) => {{ - ensure_usage!($args, $val.is_some(), "{0} option is mandatory", $name) - }}; -} - -pub struct ArgsWalker { - pub argv: Vec, - pub off: usize, -} - -impl ArgsWalker { - pub fn get(&self) -> Option<&str> { - self.argv.get(self.off).map(|s| s as &str) - } - - pub fn prev(&mut self) -> Option<&str> { - assert!(self.off > 0); - self.off -= 1; - self.get() - } - - #[allow(clippy::should_implement_trait)] - pub fn next(&mut self) -> Option<&str> { - assert!(self.todo() > 0); - self.off += 1; - self.get() - } - - pub fn opt(&mut self, dst: &mut Option) -> Result<()> { - let cmd = &self.argv[self.off - 1]; - ensure_usage!(&self, self.todo() > 0, "Option {} takes a value", cmd); - ensure_usage!(&self, dst.is_none(), "Cannot set {} multiple times.", cmd); - *dst = Some(String::from(self.next().unwrap())); - Ok(()) - } - - fn todo(&self) -> usize { - self.argv.len() - self.off - } -} - -#[derive(Default, Debug)] -pub struct WireguardOut { - // impl KeyOutput - dev: String, - pk: String, - extra_params: Vec, -} - -#[derive(Default, Debug)] -pub struct AppPeer { - pub outfile: Option, - pub outwg: Option, - pub tx_addr: Option, -} - -#[derive(Debug)] -pub enum Verbosity { - Quiet, - Verbose, -} - -/// Holds the state of the application, namely the external IO -#[derive(Debug)] -pub struct AppServer { - pub crypt: CryptoServer, - pub sock: UdpSocket, - pub peers: Vec, - pub verbosity: Verbosity, -} - -/// Index based pointer to a Peer -#[derive(Debug)] -pub struct AppPeerPtr(pub usize); - -impl AppPeerPtr { - /// Takes an index based handle and returns the actual peer - pub fn lift(p: PeerPtr) -> Self { - Self(p.0) - } - - /// Returns an index based handle to one Peer - pub fn lower(&self) -> PeerPtr { - PeerPtr(self.0) - } - - pub fn get_app<'a>(&self, srv: &'a AppServer) -> &'a AppPeer { - &srv.peers[self.0] - } - - pub fn get_app_mut<'a>(&self, srv: &'a mut AppServer) -> &'a mut AppPeer { - &mut srv.peers[self.0] - } -} - -#[derive(Debug)] -pub enum AppPollResult { - DeleteKey(AppPeerPtr), - SendInitiation(AppPeerPtr), - SendRetransmission(AppPeerPtr), - ReceivedMessage(usize, SocketAddr), -} - -#[derive(Debug)] -pub enum KeyOutputReason { - Exchanged, - Stale, -} +use log::error; +use rosenpass::{cli::Cli, sodium::sodium_init}; +use std::process::exit; /// Catches errors, prints them through the logger, then exits pub fn main() { env_logger::init(); - match rosenpass_main() { + match sodium_init().and_then(|()| Cli::run()) { Ok(_) => {} Err(e) => { error!("{e}"); @@ -263,402 +13,3 @@ pub fn main() { } } } - -/// Entry point to the whole program -pub fn rosenpass_main() -> Result<()> { - sodium_init()?; - - let mut args = ArgsWalker { - argv: std::env::args().collect(), - off: 0, // skipping executable path - }; - - // Command parsing - match args.next() { - Some("help") | Some("-h") | Some("-help") | Some("--help") => cmd_help()?, - Some("keygen") => cmd_keygen(args)?, - Some("exchange") => cmd_exchange(args)?, - Some(cmd) => bail_usage!(&args, "No such command {}", cmd), - None => bail_usage!(&args, "Expected a command!"), - }; - - Ok(()) -} - -/// Print the usage information -pub fn cmd_help() -> Result<()> { - let man_cmd = Command::new("man").args(["1", "rosenpass"]).status(); - if man_cmd.is_ok() && man_cmd.unwrap().success() { - return Ok(()); - } - - // Print the compiled manual - eprint!(include_str!(env!("ROSENPASS_MAN"))); - Ok(()) -} - -/// Generate a keypair -pub fn cmd_keygen(mut args: ArgsWalker) -> Result<()> { - let mut sf: Option = None; - let mut pf: Option = None; - - // Arg parsing - loop { - match args.next() { - Some("private-key") => args.opt(&mut sf)?, - Some("public-key") => args.opt(&mut pf)?, - Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt), - None => break, - }; - } - - mandatory_opt!(&args, sf, "private-key"); - mandatory_opt!(&args, pf, "private-key"); - - // Cmd - let (mut ssk, mut spk) = (SSk::random(), SPk::random()); - unsafe { - StaticKEM::keygen(ssk.secret_mut(), spk.secret_mut())?; - ssk.store_secret(sf.unwrap())?; - spk.store_secret(pf.unwrap())?; - } - - Ok(()) -} - -pub fn cmd_exchange(mut args: ArgsWalker) -> Result<()> { - // Argument parsing - let mut sf: Option = None; - let mut pf: Option = None; - let mut listen: Option = None; - let mut verbosity = Verbosity::Quiet; - - // Global parameters - loop { - match args.next() { - Some("private-key") => args.opt(&mut sf)?, - Some("public-key") => args.opt(&mut pf)?, - Some("listen") => args.opt(&mut listen)?, - Some("verbose") => { - verbosity = Verbosity::Verbose; - } - Some("peer") => { - args.prev(); - break; - } - Some(opt) => bail_usage!(&args, "Unknown option `{}`", opt), - None => break, - }; - } - - mandatory_opt!(&args, sf, "private-key"); - mandatory_opt!(&args, pf, "public-key"); - - let mut srv = std::boxed::Box::::new(AppServer::new( - // sk, pk, addr - SSk::load(&sf.unwrap())?, - SPk::load(&pf.unwrap())?, - listen.as_deref().unwrap_or("[0::0]:0"), - verbosity, - )?); - - // Peer parameters - '_parseAllPeers: while args.todo() > 0 { - let mut pf: Option = None; - let mut outfile: Option = None; - let mut outwg: Option = None; - let mut endpoint: Option = None; - let mut pskf: Option = None; - - args.next(); // skip "peer" starter itself - - 'parseOnePeer: loop { - match args.next() { - // Done with this peer - Some("peer") => { - args.prev(); - break 'parseOnePeer; - } - None => break 'parseOnePeer, - // Options - Some("public-key") => args.opt(&mut pf)?, - Some("endpoint") => args.opt(&mut endpoint)?, - Some("preshared-key") => args.opt(&mut pskf)?, - Some("outfile") => args.opt(&mut outfile)?, - // Wireguard out - Some("wireguard") => { - ensure_usage!( - &args, - outwg.is_none(), - "Cannot set wireguard output for the same peer multiple times." - ); - ensure_usage!(&args, args.todo() >= 2, "Option wireguard takes to values"); - let dev = String::from(args.next().unwrap()); - let pk = String::from(args.next().unwrap()); - let wg = outwg.insert(WireguardOut { - dev, - pk, - extra_params: Vec::new(), - }); - '_parseWgOutExtra: loop { - match args.next() { - Some("peer") => { - args.prev(); - break 'parseOnePeer; - } - None => break 'parseOnePeer, - Some(xtra) => wg.extra_params.push(xtra.to_string()), - }; - } - } - // Invalid - Some(opt) => bail_usage!(&args, "Unknown peer option `{}`", opt), - }; - } - - mandatory_opt!(&args, pf, "private-key"); - ensure_usage!( - &args, - outfile.is_some() || outwg.is_some(), - "Either of the outfile or wireguard option is mandatory" - ); - - let tx_addr = endpoint - .map(|e| { - e.to_socket_addrs()? - .next() - .context("Expected address in endpoint parameter") - }) - .transpose()?; - - srv.add_peer( - // psk, pk, outfile, outwg, tx_addr - pskf.map(SymKey::load_b64).transpose()?, - SPk::load(&pf.unwrap())?, - outfile, - outwg, - tx_addr, - )?; - } - - srv.listen_loop() -} - -impl AppServer { - pub fn new(sk: SSk, pk: SPk, addr: A, verbosity: Verbosity) -> Result { - Ok(Self { - crypt: CryptoServer::new(sk, pk), - sock: UdpSocket::bind(addr)?, - peers: Vec::new(), - verbosity, - }) - } - - pub fn verbose(&self) -> bool { - matches!(self.verbosity, Verbosity::Verbose) - } - - pub fn add_peer( - &mut self, - psk: Option, - pk: SPk, - outfile: Option, - outwg: Option, - tx_addr: Option, - ) -> Result { - let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?; - assert!(pn == self.peers.len()); - self.peers.push(AppPeer { - outfile, - outwg, - tx_addr, - }); - Ok(AppPeerPtr(pn)) - } - - pub fn listen_loop(&mut self) -> Result<()> { - const INIT_SLEEP: f64 = 0.01; - const MAX_FAILURES: i32 = 10; - let mut failure_cnt = 0; - - loop { - let msgs_processed = 0usize; - let err = match self.event_loop() { - Ok(()) => return Ok(()), - Err(e) => e, - }; - - // This should not happen… - failure_cnt = if msgs_processed > 0 { - 0 - } else { - failure_cnt + 1 - }; - let sleep = INIT_SLEEP * 2.0f64.powf(f64::from(failure_cnt - 1)); - let tries_left = MAX_FAILURES - (failure_cnt - 1); - error!( - "unexpected error after processing {} messages: {:?} {}", - msgs_processed, - err, - err.backtrace() - ); - if tries_left > 0 { - error!("reinitializing networking in {sleep}! {tries_left} tries left."); - std::thread::sleep(self.crypt.timebase.dur(sleep)); - continue; - } - - bail!("too many network failures"); - } - } - - pub fn event_loop(&mut self) -> Result<()> { - let (mut rx, mut tx) = (MsgBuf::zero(), MsgBuf::zero()); - - /// if socket address for peer is known, call closure - /// assumes that closure leaves a message in `tx` - /// assumes that closure returns the length of message in bytes - macro_rules! tx_maybe_with { - ($peer:expr, $fn:expr) => { - attempt!({ - let p = $peer.get_app(self); - if let Some(addr) = p.tx_addr { - let len = $fn()?; - self.sock.send_to(&tx[..len], addr)?; - } - Ok(()) - }) - }; - } - - loop { - use rosenpass::protocol::HandleMsgResult; - use AppPollResult::*; - use KeyOutputReason::*; - match self.poll(&mut *rx)? { - SendInitiation(peer) => tx_maybe_with!(peer, || self - .crypt - .initiate_handshake(peer.lower(), &mut *tx))?, - SendRetransmission(peer) => tx_maybe_with!(peer, || self - .crypt - .retransmit_handshake(peer.lower(), &mut *tx))?, - DeleteKey(peer) => self.output_key(peer, Stale, &SymKey::random())?, - - ReceivedMessage(len, addr) => { - match self.crypt.handle_msg(&rx[..len], &mut *tx) { - Err(ref e) => { - self.verbose().then(|| { - info!( - "error processing incoming message from {:?}: {:?} {}", - addr, - e, - e.backtrace() - ); - }); - } - - Ok(HandleMsgResult { - resp, - exchanged_with, - .. - }) => { - if let Some(len) = resp { - self.sock.send_to(&tx[0..len], addr)?; - } - - if let Some(p) = exchanged_with { - let ap = AppPeerPtr::lift(p); - ap.get_app_mut(self).tx_addr = Some(addr); - - // TODO: Maybe we should rather call the key "rosenpass output"? - self.output_key(ap, Exchanged, &self.crypt.osk(p)?)?; - } - } - } - } - }; - } - } - - pub fn output_key(&self, peer: AppPeerPtr, why: KeyOutputReason, key: &SymKey) -> Result<()> { - let peerid = peer.lower().get(&self.crypt).pidt()?; - let ap = peer.get_app(self); - - if self.verbose() { - let msg = match why { - KeyOutputReason::Exchanged => "Exchanged key with peer", - KeyOutputReason::Stale => "Erasing outdated key from peer", - }; - info!("{} {}", msg, fmt_b64(&*peerid)); - } - - if let Some(of) = ap.outfile.as_ref() { - // This might leave some fragments of the secret on the stack; - // in practice this is likely not a problem because the stack likely - // will be overwritten by something else soon but this is not exactly - // guaranteed. It would be possible to remedy this, but since the secret - // data will linger in the linux page cache anyways with the current - // implementation, going to great length to erase the secret here is - // not worth it right now. - b64_writer(fopen_w(of)?).write_all(key.secret())?; - let why = match why { - KeyOutputReason::Exchanged => "exchanged", - KeyOutputReason::Stale => "stale", - }; - println!( - "output-key peer {} key-file {} {}", - fmt_b64(&*peerid), - of, - why - ); - } - - if let Some(owg) = ap.outwg.as_ref() { - let child = Command::new("wg") - .arg("set") - .arg(&owg.dev) - .arg("peer") - .arg(&owg.pk) - .arg("preshared-key") - .arg("/dev/stdin") - .stdin(Stdio::piped()) - .args(&owg.extra_params) - .spawn()?; - b64_writer(child.stdin.unwrap()).write_all(key.secret())?; - } - - Ok(()) - } - - pub fn poll(&mut self, rx_buf: &mut [u8]) -> Result { - use rosenpass::protocol::PollResult as C; - use AppPollResult as A; - loop { - return Ok(match self.crypt.poll()? { - C::DeleteKey(PeerPtr(no)) => A::DeleteKey(AppPeerPtr(no)), - C::SendInitiation(PeerPtr(no)) => A::SendInitiation(AppPeerPtr(no)), - C::SendRetransmission(PeerPtr(no)) => A::SendRetransmission(AppPeerPtr(no)), - C::Sleep(timeout) => match self.try_recv(rx_buf, timeout)? { - Some((len, addr)) => A::ReceivedMessage(len, addr), - None => continue, - }, - }); - } - } - - pub fn try_recv(&self, buf: &mut [u8], timeout: Timing) -> Result> { - if timeout == 0.0 { - return Ok(None); - } - self.sock - .set_read_timeout(Some(Duration::from_secs_f64(timeout)))?; - match self.sock.recv_from(buf) { - Ok(x) => Ok(Some(x)), - Err(e) => match e.kind() { - ErrorKind::WouldBlock => Ok(None), - ErrorKind::TimedOut => Ok(None), - _ => Err(anyhow::Error::new(e)), - }, - } - } -} diff --git a/src/protocol.rs b/src/protocol.rs index 6c4e88d..70e145f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -28,7 +28,7 @@ //! // always init libsodium before anything //! rosenpass::sodium::sodium_init().unwrap(); //! -//! // initialize public and private key for peer a ... +//! // initialize secret and public key for peer a ... //! let (mut peer_a_sk, mut peer_a_pk) = (SSk::zero(), SPk::zero()); //! StaticKEM::keygen(peer_a_sk.secret_mut(), peer_a_pk.secret_mut())?; //! @@ -249,18 +249,13 @@ impl HandshakeRole { } } -#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Copy, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum HandshakeStateMachine { + #[default] RespHello, RespConf, } -impl Default for HandshakeStateMachine { - fn default() -> Self { - HandshakeStateMachine::RespHello - } -} - #[derive(Debug)] pub struct InitiatorHandshake { pub created_at: Timing, @@ -1704,7 +1699,7 @@ mod test { // always init libsodium before anything crate::sodium::sodium_init().unwrap(); - // initialize public and private key for the crypto server + // initialize secret and public key for the crypto server let (mut sk, mut pk) = (SSk::zero(), SPk::zero()); StaticKEM::keygen(sk.secret_mut(), pk.secret_mut()).expect("unable to generate keys"); diff --git a/src/usage.md b/src/usage.md deleted file mode 100644 index 84c07bb..0000000 --- a/src/usage.md +++ /dev/null @@ -1,48 +0,0 @@ -NAME - - {0} – Perform post-quantum secure key exchanges for wireguard and other services. - -SYNOPSIS - - {0} [ COMMAND ] [ OPTIONS ]... [ ARGS ]... - -DESCRIPTION - {0} performs cryptographic key exchanges that are secure against quantum-computers and outputs the keys. - These keys can then be passed to various services such as wireguard or other vpn services - as pre-shared-keys to achieve security against attackers with quantum computers. - -COMMANDS - - keygen private-key public-key - Generate a keypair to use in the exchange command later. Send the public-key file to your communication partner - and keep the private-key file a secret! - - exchange private-key public-key [ OPTIONS ]... PEER...\n" - Start a process to exchange keys with the specified peers. You should specify at least one peer. - - OPTIONS - listen [:] - Instructs {0} to listen on the specified interface and port. By default {0} will listen on all interfaces and select a random port. - - verbose - Extra logging - - PEER := peer public-key [endpoint [:]] [preshared-key ] [outfile ] [wireguard ] - Instructs {0} to exchange keys with the given peer and write the resulting PSK into the given output file. - You must either specify the outfile or wireguard output option. - - endpoint [:] - Specifies the address where the peer can be reached. This will be automatically updated after the first successful - key exchange with the peer. If this is unspecified, the peer must initiate the connection. - - preshared-key - You may specify a pre-shared key which will be mixed into the final secret. - - outfile - You may specify a file to write the exchanged keys to. If this option is specified, {0} will - write a notification to standard out every time the key is updated. - - wireguard - This allows you to directly specify a wireguard peer to deploy the pre-shared-key to. - You may specify extra parameters you would pass to `wg set` besides the preshared-key parameter which is used by {0}. - This makes it possible to add peers entirely from {0}. diff --git a/src/util.rs b/src/util.rs index a637542..e5d71f5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,5 @@ //! Helper functions and macros - +use anyhow::{ensure, Context, Result}; use base64::{ display::Base64Display as B64Display, read::DecoderReader as B64Reader, write::EncoderWriter as B64Writer, @@ -7,10 +7,14 @@ use base64::{ use std::{ borrow::{Borrow, BorrowMut}, cmp::min, + fs::{File, OpenOptions}, io::{Read, Write}, + path::Path, time::{Duration, Instant}, }; +use crate::coloring::{Public, Secret}; + #[inline] pub fn xor_into(a: &mut [u8], b: &[u8]) { assert!(a.len() == b.len()); @@ -115,3 +119,114 @@ where f(&v); v } + +/// load'n store + +/// Open a file writable +pub fn fopen_w>(path: P) -> Result { + Ok(OpenOptions::new() + .read(false) + .write(true) + .create(true) + .truncate(true) + .open(path)?) +} +/// Open a file readable +pub fn fopen_r>(path: P) -> Result { + Ok(OpenOptions::new() + .read(true) + .write(false) + .create(false) + .truncate(false) + .open(path)?) +} + +pub trait ReadExactToEnd { + fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()>; +} + +impl ReadExactToEnd for R { + fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<()> { + let mut dummy = [0u8; 8]; + self.read_exact(buf)?; + ensure!(self.read(&mut dummy)? == 0, "File too long!"); + Ok(()) + } +} + +pub trait LoadValue { + fn load>(path: P) -> Result + where + Self: Sized; +} + +pub trait LoadValueB64 { + fn load_b64>(path: P) -> Result + where + Self: Sized; +} + +trait StoreValue { + fn store>(&self, path: P) -> Result<()>; +} + +trait StoreSecret { + unsafe fn store_secret>(&self, path: P) -> Result<()>; +} + +impl StoreSecret for T { + unsafe fn store_secret>(&self, path: P) -> Result<()> { + self.store(path) + } +} + +impl LoadValue for Secret { + fn load>(path: P) -> Result { + let mut v = Self::random(); + let p = path.as_ref(); + fopen_r(p)? + .read_exact_to_end(v.secret_mut()) + .with_context(|| format!("Could not load file {p:?}"))?; + Ok(v) + } +} + +impl LoadValueB64 for Secret { + fn load_b64>(path: P) -> Result { + let mut v = Self::random(); + let p = path.as_ref(); + // This might leave some fragments of the secret on the stack; + // in practice this is likely not a problem because the stack likely + // will be overwritten by something else soon but this is not exactly + // guaranteed. It would be possible to remedy this, but since the secret + // data will linger in the linux page cache anyways with the current + // implementation, going to great length to erase the secret here is + // not worth it right now. + b64_reader(&mut fopen_r(p)?) + .read_exact(v.secret_mut()) + .with_context(|| format!("Could not load base64 file {p:?}"))?; + Ok(v) + } +} + +impl StoreSecret for Secret { + unsafe fn store_secret>(&self, path: P) -> Result<()> { + std::fs::write(path, self.secret())?; + Ok(()) + } +} + +impl LoadValue for Public { + fn load>(path: P) -> Result { + let mut v = Self::random(); + fopen_r(path)?.read_exact_to_end(&mut *v)?; + Ok(v) + } +} + +impl StoreValue for Public { + fn store>(&self, path: P) -> Result<()> { + std::fs::write(path, **self)?; + Ok(()) + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 32a90d3..a953d66 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -8,21 +8,21 @@ fn generate_keys() { let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("keygen"); fs::create_dir_all(&tmpdir).unwrap(); - let priv_key_path = tmpdir.join("private-key"); - let pub_key_path = tmpdir.join("public-key"); + let secret_key_path = tmpdir.join("secret-key"); + let public_key_path = tmpdir.join("public-key"); let output = test_bin::get_test_bin(BIN) - .args(["keygen", "private-key"]) - .arg(&priv_key_path) - .arg("public-key") - .arg(&pub_key_path) + .args(["gen-keys", "--secret-key"]) + .arg(&secret_key_path) + .arg("--public-key") + .arg(&public_key_path) .output() .expect("Failed to start {BIN}"); assert_eq!(String::from_utf8_lossy(&output.stdout), ""); - assert!(priv_key_path.is_file()); - assert!(pub_key_path.is_file()); + assert!(secret_key_path.is_file()); + assert!(public_key_path.is_file()); // cleanup fs::remove_dir_all(&tmpdir).unwrap(); @@ -46,22 +46,22 @@ fn check_exchange() { let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange"); fs::create_dir_all(&tmpdir).unwrap(); - let priv_key_paths = [tmpdir.join("private-key-0"), tmpdir.join("private-key-1")]; - let pub_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")]; + let secret_key_paths = [tmpdir.join("secret-key-0"), tmpdir.join("secret-key-1")]; + let public_key_paths = [tmpdir.join("public-key-0"), tmpdir.join("public-key-1")]; let shared_key_paths = [tmpdir.join("shared-key-0"), tmpdir.join("shared-key-1")]; // generate key pairs - for (priv_key_path, pub_key_path) in priv_key_paths.iter().zip(pub_key_paths.iter()) { + for (secret_key_path, pub_key_path) in secret_key_paths.iter().zip(public_key_paths.iter()) { let output = test_bin::get_test_bin(BIN) - .args(["keygen", "private-key"]) - .arg(&priv_key_path) - .arg("public-key") + .args(["gen-keys", "--secret-key"]) + .arg(&secret_key_path) + .arg("--public-key") .arg(&pub_key_path) .output() .expect("Failed to start {BIN}"); assert_eq!(String::from_utf8_lossy(&output.stdout), ""); - assert!(priv_key_path.is_file()); + assert!(secret_key_path.is_file()); assert!(pub_key_path.is_file()); } @@ -69,12 +69,12 @@ fn check_exchange() { let port = find_udp_socket(); let listen_addr = format!("localhost:{port}"); let mut server = test_bin::get_test_bin(BIN) - .args(["exchange", "private-key"]) - .arg(&priv_key_paths[0]) + .args(["exchange", "secret-key"]) + .arg(&secret_key_paths[0]) .arg("public-key") - .arg(&pub_key_paths[0]) + .arg(&public_key_paths[0]) .args(["listen", &listen_addr, "verbose", "peer", "public-key"]) - .arg(&pub_key_paths[1]) + .arg(&public_key_paths[1]) .arg("outfile") .arg(&shared_key_paths[0]) .stdout(Stdio::null()) @@ -82,14 +82,16 @@ fn check_exchange() { .spawn() .expect("Failed to start {BIN}"); + std::thread::sleep(Duration::from_millis(500)); + // start second process, the client let mut client = test_bin::get_test_bin(BIN) - .args(["exchange", "private-key"]) - .arg(&priv_key_paths[1]) + .args(["exchange", "secret-key"]) + .arg(&secret_key_paths[1]) .arg("public-key") - .arg(&pub_key_paths[1]) + .arg(&public_key_paths[1]) .args(["verbose", "peer", "public-key"]) - .arg(&pub_key_paths[0]) + .arg(&public_key_paths[0]) .args(["endpoint", &listen_addr]) .arg("outfile") .arg(&shared_key_paths[1])