First commit
This commit is contained in:
commit
4e6d01b3db
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
**/*.rs.bk
|
|
@ -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"
|
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "clogsim"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["SoniEx2 <endermoneymod@gmail.com>"]
|
||||||
|
license = "AGPLv3+"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sha2 = "0.8.0"
|
|
@ -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
|
|
@ -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.
|
|
@ -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\"."); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue