From 52926d6152686ccf864fe281c03482b62fb6a749 Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Thu, 9 Mar 2023 14:23:40 +0100 Subject: gossip encryption (fix #4) --- Cargo.lock | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/main.rs | 64 +++++++++++++++++--- 3 files changed, 251 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2a75ae..eb28c58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -17,6 +27,18 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "attohttpc" version = "0.16.3" @@ -55,6 +77,29 @@ dependencies = [ "serde", ] +[[package]] +name = "blake3" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytes" version = "1.4.0" @@ -67,12 +112,66 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "constant_time_eq" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -107,6 +206,16 @@ version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "get_if_addrs" version = "0.5.3" @@ -208,6 +317,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "itoa" version = "1.0.6" @@ -235,12 +353,29 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "percent-encoding" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -328,6 +463,15 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "serde" version = "1.0.154" @@ -357,6 +501,12 @@ dependencies = [ "serde", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.109" @@ -426,6 +576,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-bidi" version = "0.3.11" @@ -447,6 +603,16 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.3.1" @@ -458,6 +624,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -470,12 +642,14 @@ version = "0.1.0" dependencies = [ "anyhow", "bincode", + "blake3", "get_if_addrs", "igd", "log", "pretty_env_logger", "serde", "toml", + "xsalsa20poly1305", "xxhash-rust", ] @@ -546,8 +720,27 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "xsalsa20poly1305" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "472c385ee974833d7e59979eeb74175d56774be3768b5bcc581337e21396bda3" +dependencies = [ + "aead", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + [[package]] name = "xxhash-rust" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "735a71d46c4d68d71d4b24d03fdc2b98e38cea81730595801db779c04fe80d70" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml index f722bf3..7a04067 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ pretty_env_logger = "0.4.0" serde = { version = "1.0", features = ["derive"] } bincode = "1.3" toml = { version = "0.7", default-features = false, features = ["parse"] } +xsalsa20poly1305 = "0.9" +blake3 = "1.3" igd = { version = "0.12", default-features = false } get_if_addrs = "0.5" diff --git a/src/main.rs b/src/main.rs index b913068..1730edc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,9 @@ struct Config { interface: Pubkey, /// The port to use for gossip inside the Wireguard mesh (must be the same on all nodes) gossip_port: u16, + /// The secret to use to authenticate nodes between them + gossip_secret: Option, + gossip_secret_file: Option, /// Enable LAN discovery #[serde(default)] @@ -71,11 +74,18 @@ fn main() -> Result<()> { ), }; - let config: Config = { + let mut config: Config = { let config_str = std::fs::read_to_string(config_path)?; toml::from_str(&config_str)? }; + if let Some(f) = &config.gossip_secret_file { + if config.gossip_secret.is_some() { + bail!("both gossip_secret and gossip_secret_file are given in config file"); + } + config.gossip_secret = Some(std::fs::read_to_string(f)?); + } + Daemon::new(config)?.run() } @@ -96,6 +106,11 @@ fn fasthash(data: &[u8]) -> u64 { h.digest() } +fn kdf(secret: &str) -> xsalsa20poly1305::Key { + let hash = blake3::hash(format!("wgautomesh: {}", secret).as_bytes()); + hash.as_bytes().clone().into() +} + fn wg_dump(config: &Config) -> Result<(Pubkey, u16, Vec<(Pubkey, Option, u64)>)> { let output = Command::new("wg") .args(["show", &config.interface, "dump"]) @@ -134,6 +149,7 @@ fn wg_dump(config: &Config) -> Result<(Pubkey, u16, Vec<(Pubkey, Option Result { + let gossip_key = kdf(config.gossip_secret.as_deref().unwrap_or_default()); + let (our_pubkey, listen_port, _peers) = wg_dump(&config)?; let socket = UdpSocket::bind(SocketAddr::new("0.0.0.0".parse()?, config.gossip_port))?; socket.set_broadcast(true)?; Ok(Daemon { config, + gossip_key, our_pubkey, listen_port, socket, @@ -186,7 +205,7 @@ impl Daemon { error!("Error initializing wireguard peers: {}", e); } - let request = bincode::serialize(&Gossip::Request)?; + let request = self.make_packet(&Gossip::Request)?; for peer in self.config.peers.iter() { let addr = SocketAddr::new(peer.address, self.config.gossip_port); if let Err(e) = self.socket.send_to(&request, addr) { @@ -264,7 +283,7 @@ impl Daemon { } Gossip::Request => { for (pubkey, endpoints) in state.gossip.iter() { - let packet = bincode::serialize(&Gossip::Announce { + let packet = self.make_packet(&Gossip::Announce { pubkey: pubkey.clone(), endpoints: endpoints.clone(), })?; @@ -287,12 +306,24 @@ impl Daemon { } fn recv_gossip(&self) -> Result<(SocketAddr, Gossip)> { + use xsalsa20poly1305::{ + aead::{Aead, KeyInit}, + XSalsa20Poly1305, NONCE_SIZE, + }; + let mut buf = vec![0u8; 1500]; let (amt, src) = self.socket.recv_from(&mut buf)?; - if !self.config.peers.iter().any(|x| x.address == src.ip()) { - bail!("Received message from unexpected peer: {}", src); + + if amt < NONCE_SIZE { + bail!("invalid packet"); } - let gossip = bincode::deserialize(&buf[..amt])?; + + let cipher = XSalsa20Poly1305::new(&self.gossip_key); + let plaintext = cipher + .decrypt(buf[..NONCE_SIZE].try_into().unwrap(), &buf[NONCE_SIZE..amt]) + .map_err(|e| anyhow!("decrypt error: {}", e))?; + + let gossip = bincode::deserialize(&plaintext)?; debug!("RECV {}\t{:?}", src, gossip); Ok((src, gossip)) } @@ -309,7 +340,7 @@ impl Daemon { } fn lan_broadcast_iter(&self) -> Result<()> { - let packet = bincode::serialize(&Gossip::LanBroadcast { + let packet = self.make_packet(&Gossip::LanBroadcast { pubkey: self.our_pubkey.clone(), listen_port: self.listen_port, })?; @@ -365,6 +396,23 @@ impl Daemon { Ok(()) } + + fn make_packet(&self, gossip: &Gossip) -> Result> { + use xsalsa20poly1305::{ + aead::{Aead, KeyInit, OsRng}, + XSalsa20Poly1305, + }; + + let plaintext = bincode::serialize(&gossip)?; + + let cipher = XSalsa20Poly1305::new(&self.gossip_key); + let nonce = XSalsa20Poly1305::generate_nonce(&mut OsRng); + let ciphertext = cipher + .encrypt(&nonce, &plaintext[..]) + .map_err(|e| anyhow!("encrypt error: {}", e))?; + + Ok([&nonce[..], &ciphertext[..]].concat()) + } } struct State { @@ -374,7 +422,7 @@ struct State { impl State { fn send_gossip(&self, daemon: &Daemon, gossip: Gossip) -> Result<()> { - let packet = bincode::serialize(&gossip)?; + let packet = daemon.make_packet(&gossip)?; let now = time(); -- cgit v1.2.3