From 4e6d01b3dbbc46df8074cb94d9f3a7edf6025e81 Mon Sep 17 00:00:00 2001 From: SoniEx2 Date: Sat, 10 Nov 2018 01:22:45 -0200 Subject: [PATCH] First commit --- .gitignore | 2 + Cargo.lock | 89 +++++++++++++++++++++++ Cargo.toml | 8 +++ README.md | 34 +++++++++ clog.md | 102 ++++++++++++++++++++++++++ src/main.rs | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 437 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 clog.md create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a7f0af0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,89 @@ +[[package]] +name = "block-buffer" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-padding 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "block-padding" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "byte-tools" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "byteorder" +version = "1.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "clogsim" +version = "0.1.0" +dependencies = [ + "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "digest" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "generic-array" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "opaque-debug" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "sha2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typenum" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum block-buffer 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49665c62e0e700857531fa5d3763e91b539ff1abeebd56808d378b495870d60d" +"checksum block-padding 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4fc4358306e344bf9775d0197fd00d2603e5afb0771bb353538630f022068ea3" +"checksum byte-tools 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "980479e6fde23246dfb54d47580d66b4e99202e7579c5eaa9fe10ecb5ebd2182" +"checksum byteorder 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "94f88df23a25417badc922ab0f5716cc1330e87f71ddd9203b3a3ccd9cedf75d" +"checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +"checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592" +"checksum opaque-debug 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "51ecbcb821e1bd256d456fe858aaa7f380b63863eab2eb86eee1bd9f33dd6682" +"checksum sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4d8bfd0e469f417657573d8451fb33d16cfe0989359b93baf3a1ffc639543d" +"checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9389ea0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "clogsim" +version = "0.1.0" +authors = ["SoniEx2 "] +license = "AGPLv3+" + +[dependencies] +sha2 = "0.8.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..cce9d8b --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +This is a simple CLI-based "game" that simulates the clog idea. See clog.md. + +The game has in-game help, just type "help". + +The game has the following commands: + +- `help` + + shows help text +- `status` + + displays the status of all servers in the game + + this displays the server name, the hashes the server knows of, and the pending messages the server hasn't seen yet +- `status ` + + displays the status of a specific server +- `new ` + + creates a new server with the specified name +- `kill ` + + removes the server with the specified name +- `send ` + + simulates a client sending a message to the specified server +- `recv ` + + simulates a server (`server-to`) receiving a message from another server (`server-from`) + + the message must have been previously sent with `send ` +- quit + + quits the game diff --git a/clog.md b/clog.md new file mode 100644 index 0000000..f8792b2 --- /dev/null +++ b/clog.md @@ -0,0 +1,102 @@ +CAP prefix/clog +=============== + +Copyright (c) 2018 Soni L. \ + +This capability provides a mechanism for identifying and conveying a cryptographically secured list of messages to IRC clients. + +This specification also suggests a mechanism by which IRC clients can sync logs, using the information provided by this capability. + +Cap syntax +---------- + +The capability shall be specified as + + prefix/clog=hash_type,hash_type/tag,tag,tag + +Example: + + prefix/clog=sha1,sha256/server-time,prefix/hash + +`server-time` and `prefix/hash` are always implicitly specified, and should be omitted. + +The `HASH` S2C command +---------------------- + +The `HASH` command shall be sent for hashes associated with a channel. + +Each hash must be a cryptographic hash. Non-cryptographic hashes must be ignored. Broken hash algorithms should be avoided. MD5 is explicitly disallowed and must not be used. + +The counter must be able to store numbers in the `0..2^62-1` range. + +These hashes specify the current "heads" of the clog. This is similar to git heads, if you're familiar with them. + +Syntax: + + HASH #channel :hash_type=counter/hash,type=counter/hash,... + +Example: + + >>> JOIN #channel + <<< JOIN #channel + <<< NAMES etc + <<< HASH #channel :sha1=20,something_long + <<< HASH #channel :sha1=60,something_else sha256=70,another + +Optionally, the server may include a server name with the HASH command: + + <<< :server1.example.com HASH #channel :etc + <<< :server2.example.com HASH #channel :other + +The `hash` message tag +---------------------- + +The `hash` mesaage tag shall be sent with every `PRIVMSG`, `TOPIC` and `NOTICE`. + +Syntax: + + prefix/hash=type=counter/short_hash,type=counter/short_hash,... + +The use of `short_hash` lowers bandwidth requirements. Consult your cryptography expert for best practices on using cryptography. + +These hashes specify the previous "head(s)" of the clog. "Merges" are just messages with hashes from different sources. + +The hash encompasses the message tags specified by the capability (e.g. `server-time` and `hash`), sorted according to UTF-8 byte order, and the contents of the IRC message, as seen in the following format: + + @prefix/hash=...;server-time=... :nick!user@host PRIVMSG #channel :message + +(This line is what gets hashed) + +Examples: + + TODO + +Security Considerations +----------------------- + +Clogs are meant for channels that want public logging. An example is the Rust IRC channel. As such, anything in a clog should be assumed public. + +It's possible to recover deleted clogs by setting up a separate IRC network and sending the right hashes on the right channel. However, you'd still need to know the hashes. +Thus, this isn't a vulnerability, because if you had the hashes, you could just request the logs directly, without going through the process of setting up an IRC network. + +The network could be made to sign all hashes, but you'd need to share the signing key across all servers for it to work correctly. This still doesn't prevent someone +from getting access to an intercepted, correctly-signed hash, and by sharing the signing key you increase the attack surface. + +The server can and should be able to "undo" hashes. This is useful if, for example, someone posts child pornography - you likely don't want that permanently recorded in a channel's history. + +[FIXME there may be more] + +The `SYNC` CTCP (informative) +----------------------------- + +*This section is informative.* + +The `SYNC` CTCP is a hypothetical CTCP for syncing logs. + +Syntax: + + SYNC <...> -[nospam+checksum] + +This is a highly flexible command supporting ipv4, ipv6, hostname, tor, i2p and tox ID. + +Messages sent over non-tox channels must be encrypted with the tox-compatible public key. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..59af07e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,202 @@ +extern crate sha2; + +use sha2::{Sha256, Digest}; + +use std::io; +use std::io::prelude::*; + +use std::collections::VecDeque; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::collections::HashMap; + +use std::time::{SystemTime, UNIX_EPOCH}; + +use std::fmt; + +const INTRO: &'static str = "This is a small program that demonstrates some concepts of clog.\n\ + To begin, type \"help\"."; +const HELP: &'static str = "Available commands:\n\ + \x20 help - Prints this help text\n\ + \x20 status [server] - Prints queues, buffers, details, etc\n\ + \x20 new - Creates a new server\n\ + \x20 kill - Removes a server\n\ + \x20 send - Sends a message to a server\n\ + \x20 recv - Sends a message between servers\n\ + \x20 quit - Exits this program\n\ + "; + +#[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Default, Debug)] +struct Hash { + ty: String, + depth: u64, + hash: String, +} + +impl Hash { + fn of(s: &str, d: u64) -> Hash { + Hash { + ty: "sha256".into(), + depth: d + 1, + hash: format!("{:x}", Sha256::digest(s.as_bytes())), + } + } +} + +impl fmt::Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}={}/{}", self.ty, self.depth, self.hash) + } +} + +#[derive(Default)] +struct ServerData { + hashes: BTreeSet, + message_queue: HashMap, String)>>, // (source) server name -> messages +} + +impl ServerData { + fn on_msg(&mut self, (hashes, msg): (BTreeSet, String)) { + let new_hashes = self.hashes.difference(&hashes).cloned().collect(); + self.hashes = new_hashes; + let mut h_msg = format!("hash="); + let mut depth: u64 = 0; + for hash in hashes { + h_msg.push_str(&hash.to_string()); + depth = depth.max(hash.depth); + } + h_msg.push_str(&format!(" {}", msg)); + let next_hash: Hash = Hash::of(&h_msg, depth); + self.hashes.insert(next_hash); + } +} + +#[derive(Default)] +struct Game { + servers: BTreeMap, +} + +impl Game { + fn new(&mut self, s: &str) { + if let Some(server_name) = singular(s.trim().split_whitespace()) { + let mut server_data = ServerData::default(); + // add all known hashes, skip duplicates + server_data.hashes.extend(self.servers.values().flat_map(|serv| &serv.hashes).cloned()); + self.servers.insert(server_name.to_owned(), server_data); + println!("Added server: {}", server_name); + } else { + println!("Syntax: new \nSynopsis: Creates a new server"); + } + } + fn print_status(&self, s: &str) { + if let Some(server_name) = singular(s.trim().split_whitespace()) { + if let Some(server) = self.servers.get(server_name) { + println!("{}:", server_name); + println!(" Hashes:"); + for hash in &server.hashes { + println!(" {}", hash); + } + println!(" Messages:"); + for msg in server.message_queue.iter().flat_map(|(k, v)| v.iter().map(move |v| format_msg(k, v))) { + println!(" {}", msg); + } + } else { + println!("Unknown server: {}", server_name); + } + } else { + for server in self.servers.keys() { + self.print_status(server); + } + if self.servers.is_empty() { + println!("No servers"); + } + } + } + fn kill(&mut self, s: &str) { + if let Some(server_name) = singular(s.trim().split_whitespace()) { + if let Some(_server) = self.servers.remove(server_name) { + println!("Removed server: {}", server_name); + } else { + println!("Unknown server: {}", server_name); + } + } else { + println!("Syntax: kill \nSynopsis: Removes a server"); + } + } + fn send(&mut self, s: &str) { + let mut it = s.splitn(2, " "); + if let (Some(server_name), Some(msg)) = (it.next().and_then(|sn| singular(sn.trim().split_whitespace())), it.next().map(|m| m.trim())) { + let msg = format!("[{}] {}", SystemTime::now().duration_since(UNIX_EPOCH).expect("time before unix epoch").as_secs(), msg); + if let Some(hashes) = self.servers.get(server_name).map(|serv| serv.hashes.clone()) { + for server in self.servers.values_mut() { + server.message_queue.entry(server_name.to_owned()).or_default().push_back((hashes.clone(), msg.to_owned())); + } + } + if let Some(server) = self.servers.get_mut(server_name) { + if let Some(msg) = server.message_queue.get_mut(server_name).and_then(|msgs| msgs.pop_front()) { + server.on_msg(msg); + } else { + panic!("Somehow got a None in a place you're not supposed to get a None"); + } + } else { + println!("Unknown server: {}", server_name); + } + } else { + println!("Syntax: send \nSynopsis: Sends a message to a server"); + } + } + fn recv(&mut self, s: &str) { + let mut it = s.splitn(2, " "); + if let (Some(from), Some(to)) = (it.next().and_then(|sn| singular(sn.trim().split_whitespace())), it.next().and_then(|sn| singular(sn.trim().split_whitespace()))) { + if let Some(server) = self.servers.get_mut(to) { + if let Some(msg) = server.message_queue.get_mut(from).and_then(|msgs| msgs.pop_front()) { + server.on_msg(msg); + } else { + println!("No message from server: {}", from); + } + } else { + println!("Unknown server: {}", to); + } + } else { + println!("Syntax: recv \nSynopsis: Sends a message between servers"); + } + } +} + +fn format_msg(serv_name: &str, msg: &(BTreeSet, String)) -> String { + let mut s = format!("[{}] hash=", serv_name); + for hash in msg.0.iter() { + s.push_str(&format!("{},", hash)); + } + s.push_str(&format!(" {}", msg.1)); + s +} + +fn singular>(mut it: I) -> Option { + let res = it.next(); + res.filter(|_| it.next().is_none()) +} + +fn main() { + println!("{}", INTRO); + let stdin = io::stdin(); + let mut game = Game::default(); + loop { + print!("> "); + let _ = io::stdout().flush(); + let mut line = String::new(); + if stdin.read_line(&mut line).is_ok() { + let mut it = line.trim().splitn(2, " "); + if let Some(s) = it.next() { match s { + "help" => println!("{}", HELP), + "quit" => return, + "status" => game.print_status(it.next().unwrap_or("")), + "new" => game.new(it.next().unwrap_or("")), + "kill" => game.kill(it.next().unwrap_or("")), + "send" => game.send(it.next().unwrap_or("")), + "recv" => game.recv(it.next().unwrap_or("")), + s => println!("Unknown command: {}", s), + } } else { println!("No input. Try \"help\"."); } + } + } +}