First commit

This commit is contained in:
SoniEx2 2018-11-10 01:22:45 -02:00
commit 4e6d01b3db
6 changed files with 437 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

89
Cargo.lock generated Normal file
View File

@ -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"

8
Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "clogsim"
version = "0.1.0"
authors = ["SoniEx2 <endermoneymod@gmail.com>"]
license = "AGPLv3+"
[dependencies]
sha2 = "0.8.0"

34
README.md Normal file
View File

@ -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 <server>`
displays the status of a specific server
- `new <server>`
creates a new server with the specified name
- `kill <server>`
removes the server with the specified name
- `send <server> <message>`
simulates a client sending a message to the specified server
- `recv <server-from> <server-to>`
simulates a server (`server-to`) receiving a message from another server (`server-from`)
the message must have been previously sent with `send <server-from> <message>`
- quit
quits the game

102
clog.md Normal file
View File

@ -0,0 +1,102 @@
CAP prefix/clog
===============
Copyright (c) 2018 Soni L. \<fakedme plus irkv3 at gmail dot com>
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 <tox-compatible public key> <port> <host> <host> <...> -[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.

202
src/main.rs Normal file
View File

@ -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 <server> - Creates a new server\n\
\x20 kill <server> - Removes a server\n\
\x20 send <server> <message> - Sends a message to a server\n\
\x20 recv <from-server> <to-server> - 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<Hash>,
message_queue: HashMap<String, VecDeque<(BTreeSet<Hash>, String)>>, // (source) server name -> messages
}
impl ServerData {
fn on_msg(&mut self, (hashes, msg): (BTreeSet<Hash>, 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<String, ServerData>,
}
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 <server>\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 <server>\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 <server> <message>\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 <from-server> <to-server>\nSynopsis: Sends a message between servers");
}
}
}
fn format_msg(serv_name: &str, msg: &(BTreeSet<Hash>, 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<T, I: Iterator<Item=T>>(mut it: I) -> Option<T> {
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\"."); }
}
}
}