diff --git a/.gitignore b/.gitignore index 8c68b38..21b757b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target *.profile *.sqlite +*.niwl diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..692fc1b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/niwl.iml b/.idea/niwl.iml new file mode 100644 index 0000000..7af36cb --- /dev/null +++ b/.idea/niwl.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..95e4605 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e9aabd7..c9c57e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aead" version = "0.2.0" @@ -219,6 +221,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi 0.3.9", ] @@ -287,6 +290,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-mac" version = "0.7.0" @@ -495,7 +504,9 @@ dependencies = [ [[package]] name = "fuzzytags" -version = "0.4.0" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621e31a90168e36ecbba544f42ec8f4078db50272abdbbe172a1af9f5ea5e28" dependencies = [ "bit-vec", "curve25519-dalek", @@ -1031,11 +1042,15 @@ version = "0.1.0" dependencies = [ "base32", "bincode", + "curve25519-dalek", "fuzzytags", "hex", + "rand 0.7.3", "reqwest", + "secretbox", "serde", "serde_json", + "sha3", ] [[package]] @@ -1054,14 +1069,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "niwl-rem" +version = "0.1.0" +dependencies = [ + "base32", + "bincode", + "chrono", + "clap", + "fuzzytags", + "hex", + "niwl", + "reqwest", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "niwl-server" version = "0.1.0" dependencies = [ - "chrono", "fuzzytags", + "niwl", "rocket", "rocket_contrib", + "serde_json", ] [[package]] @@ -1619,6 +1652,12 @@ dependencies = [ "time", ] +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + [[package]] name = "ryu" version = "1.0.5" @@ -1665,6 +1704,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "secretbox" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55405b834101c4f14b3a1c35c0bc426ee41ff31ffe04f18a0575786b5c807c7" +dependencies = [ + "rand 0.7.3", + "uint", +] + [[package]] name = "security-framework" version = "2.0.0" @@ -1784,6 +1833,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -1989,6 +2044,18 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +[[package]] +name = "uint" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9db035e67dfaf7edd9aebfe8676afcd63eed53c8a4044fed514c8cccf1835177" +dependencies = [ + "byteorder", + "crunchy", + "rustc-hex", + "static_assertions", +] + [[package]] name = "unicase" version = "1.4.2" diff --git a/Cargo.toml b/Cargo.toml index b2645c7..adeca61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members=["./niwl", "niwl-client","niwl-server"] \ No newline at end of file +members=["niwl", "niwl-client","niwl-server","niwl-rem"] \ No newline at end of file diff --git a/README.md b/README.md index 5316ac7..8b2faf7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# niwl - a prototype system for metadata resistant notifications +# niwl - a prototype system for open, decentralized, metadata resistant communication **niwl** (_/nɪu̯l/_) - fog, mist or haze (Welsh). @@ -8,13 +8,97 @@ event has happened e.g. a new group message, a payment etc. **niwl** provides a set of libraries, clients and servers to provide this in a metadata resistant, bandwidth efficient way based on [fuzzytags](https://crates.io/crates/fuzzytags). -# Overview +# How Niwl Works + +A Niwl system relies on a single, untrusted routing server that acts as a bulletin board. + +Niwl clients can post and fetch messages to and from the server. When posting a message a client attaches a fuzzytag +generated for the receiver that allows the receiver to not only identify the message, but also to restrict the number +of other messages they have to download (see [Fuzzytags](https://docs.openprivacy.ca/fuzzytags-book/introduction.html) and [Fuzzy Message Detection](https://eprint.iacr.org/2021/089)) + +In order to provide statistical anonymity , the above base functionality is extended by a special class of client +called `random ejection mixers` or `REMs` for short. + +`REMs` reinforce the anonymity of the system in two ways: + +1. `REMs` download all the of messages from the server. Thus providing cover for receivers who download only a fraction + of the messages. A Niwl server cannot distinguish between a message intended for a REM from a message intended for an + ordinary client. + +2. Clients can wrap messages to other clients in a message that is first forwarded to a `REM`. The `REM` then decrypts + the message and adds it to a store of messages - ejecting a previously stored message (at random) first to make space. + + +## Random Ejection Mixers (REMs) + +A REM starts with a store of `n` randomly generated messages with randomly generated fuzzytags. These messages are +for all intents and purposes "noise". Each REM also generates a TaggingKey that it can provide (publicly or privately) +to other clients who wish to use the REMs services. + +Each REM constantly checks the Niwl Server for messages. It checks each message it downloads against its RootSecret +and if the FuzzyTag verifies then it proceeds to decrypt the message. + +The primary service a REM provides is anonymous mixing. A decrypted mixpacket contains 2 fields: + +1. The fuzzytag of the message to forward. +2. The message itself, which we will assume to be encrypted by some out-of-scope process. + +Once a message is decrypted, an existing message from the store is randomly chosen to be ejected by the mix - and is +posted to the Niwl Server. The new decrypted message takes its place in the message store. + +### On the Privacy of REMs + +Fuzzytags themselves can only be linked to receivers via those in position of a RootSecret *or* Niwl Servers who +possess the `VerificationKey` - as such, assuming that there is no collusion between a particular REM and a Niwl Server +there is no mechanism through which a REM can associate message with a (set of) receiver(s). + +Further, (again assuming no collusion between a particular REM and a Niwl Server), there is no mechanism for a REM to associate +a message with a particular sender. + +Finally, and perhaps most importantly, there is no limit on the number of REMs permitted in a particular system. Different +parties can select different REMs with different trust valuations. REMs can join the system at any time without permission +from any other entity. In other words, unlike traditional mixnets or onion routing, the system does not rely on consensus +regarding the mixing entities to ensure privacy. + +### On the Security of REMS + +`n-1 attacks` / `flooding attacks` and other active attacks on mixers are a valid concern with any mixing strategy. + +This broad genre of attacks can be generalized as follows: + +1. REMs start with a pool of randomly generated messages, this protected initial messages sent to the REM. +2. Over time this pool is probabilistically replaced by messages from the network. +3. A malicious Niwl server, having identified a REM, can flood the REM with its own messages. +4. At a certain number of messages, the probability that a REM store contains only messages from the Niwl server approaches 1.0. +5. A Niwl server can then delay every other message sent to it by other clients one-by-one. + 1. If the message isn't for the REM then nothing will happen. + 2. If the message is for the REM then the REM will either eject a message known to the Niwl Server, or it will eject + an unknown message than the Niwl Server can then correlate with a Sender and a set of Receivers. + +First, we should note that Niwl is less prone to these kinds of attacks because: + +1. REMs are not, a-priori, known to the Niwl Server and such are more difficult to target than mixers in traditional mixnets. +2. Different parties can rely on different REMs without compromising metadata privacy. + +As such targeting a particular mix is not an effective strategy for undermining the anonymity set of the entire system. + +Further, REMs employ [heartbeat messages](references/heartbeat.pdf) (messages periodically sent to the Niwl server addressed to the REM) +to detect such attacks. If a REM does not receive its own heartbeat message shortly after it is sent, it begins injecting random messages + into its pool to thwart mixers. It can also display this status publicly and/or include the status in legitimate messages alerting + other clients to the malicious Niwl Server + + +# Code Overview + +**niwl** provides common library functions useful to all other packages. **niwl-server** provides a web server with a json API for posting new tags and querying the tags database. **niwl-client** provides a command-line application for managing secrets, tagging keys of parties and posting / querying for new tags. +**niwl-rem** provides an implementation of the random ejection mixer. + For a more detailed overview please check out each individual crate. # Example @@ -32,4 +116,8 @@ For a more detailed overview please check out each individual crate. Tag for bob 7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da niwl-client bob.profile detect 10 - 7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da \ No newline at end of file + 7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da + +## References + +* Danezis, George, and Len Sassaman. "Heartbeat traffic to counter (n-1) attacks: red-green-black mixes." Proceedings of the 2003 ACM workshop on Privacy in the electronic society. 2003. \ No newline at end of file diff --git a/niwl-client/Cargo.toml b/niwl-client/Cargo.toml index cf03c4c..2661cbe 100644 --- a/niwl-client/Cargo.toml +++ b/niwl-client/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" [dependencies] niwl = {path="../niwl"} -fuzzytags = {path="../../fuzzymetatag"} +fuzzytags = "0.4.2" clap = "3.0.0-beta.2" serde = {version="1.0.123", features=["derive"]} serde_json = "1.0.61" diff --git a/niwl-client/src/main.rs b/niwl-client/src/main.rs index 40da673..5c5be6a 100644 --- a/niwl-client/src/main.rs +++ b/niwl-client/src/main.rs @@ -1,13 +1,11 @@ use clap::Clap; -use std::fs; -use niwl::{Profile}; +use niwl::Profile; #[derive(Clap)] #[clap(version = "1.0", author = "Sarah Jamie Lewis ")] struct Opts { - #[clap(default_value = "niwl.profile")] - profile_filename: String, + profile: String, #[clap(default_value = "http://localhost:8000")] niwl_server: String, @@ -21,27 +19,27 @@ enum SubCommand { Generate(Generate), ImportTaggingKey(ImportTaggingKey), TagAndSend(TagAndSend), - Detect(Detect) + TagAndMix(TagAndMix), + Detect(Detect), } /// Generate a new niwl.profile file #[derive(Clap)] struct Generate { - name: String + name: String, + #[clap(default_value = "2")] + length: usize, } /// Import a friends tagging key into this profile so you can send messages to them #[derive(Clap)] struct ImportTaggingKey { - key: String + key: String, } /// Connect to a server and check for new notifications #[derive(Clap)] -struct Detect { - #[clap(default_value = "2")] - length: u8 -} +struct Detect {} /// Send a message to a friend tagged with their niwl key #[derive(Clap)] @@ -52,33 +50,50 @@ struct TagAndSend { message: String, } -fn get_profile(profile_filename: &String) -> Profile { - match fs::read_to_string(profile_filename) { - Ok(json) => serde_json::from_str(json.as_str()).unwrap(), - Err(why) => { - panic!("couldn't read orb.profile : {}", why); - } - } +/// Send a message to a friend tagged with their niwl key +#[derive(Clap)] +struct TagAndMix { + /// the id of the mix + mix: String, + /// the id of the friend e.g. "alice" + id: String, + /// the message you want to send. + message: String, } - - fn main() { let opts: Opts = Opts::parse(); match opts.subcmd { SubCommand::Generate(g) => { - let profile = Profile::new(g.name.clone()); - let hotk = profile.human_readable_tagging_key(); - println!("Tagging Key: {}", base32::encode(base32::Alphabet::RFC4648{padding:false} ,bincode::serialize(&hotk).unwrap().as_slice()).to_ascii_lowercase()); - profile.save(&opts.profile_filename); + let profile = Profile::new(g.name.clone(), g.length); + let hotk = profile.keyset(); + println!( + "Tagging Key: {}", + base32::encode( + base32::Alphabet::RFC4648 { padding: false }, + bincode::serialize(&hotk).unwrap().as_slice() + ) + .to_ascii_lowercase() + ); + match profile.save(&opts.profile) { + Err(e) => { + println!("[ERROR] {}", e) + } + _ => {} + } } SubCommand::ImportTaggingKey(cmd) => { - let mut profile = get_profile(&opts.profile_filename); + let mut profile = Profile::get_profile(&opts.profile); profile.import_tagging_key(&cmd.key); - profile.save(&opts.profile_filename); - }, + match profile.save(&opts.profile) { + Err(e) => { + println!("[ERROR] {}", e) + } + _ => {} + } + } SubCommand::TagAndSend(cmd) => { - let mut profile = get_profile(&opts.profile_filename); + let profile = Profile::get_profile(&opts.profile); let server = opts.niwl_server.clone(); let contact = cmd.id.clone(); tokio::runtime::Builder::new_current_thread() @@ -86,12 +101,28 @@ fn main() { .build() .unwrap() .block_on(async { - let result = profile.tag_and_send(server, contact).await; + let result = profile.tag_and_send(server, contact, &cmd.message).await; println!("{}", result.unwrap().text().await.unwrap()); }); - }, - SubCommand::Detect(cmd) => { - let mut profile = get_profile(&opts.profile_filename); + } + SubCommand::TagAndMix(cmd) => { + let profile = Profile::get_profile(&opts.profile); + let server = opts.niwl_server.clone(); + let contact = cmd.id.clone(); + let mix = cmd.mix.clone(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let result = profile + .tag_and_mix(server, mix, contact, &cmd.message) + .await; + println!("{}", result.unwrap().text().await.unwrap()); + }); + } + SubCommand::Detect(_cmd) => { + let mut profile = Profile::get_profile(&opts.profile); let server = opts.niwl_server.clone(); tokio::runtime::Builder::new_current_thread() .enable_all() @@ -100,15 +131,28 @@ fn main() { .block_on(async { match profile.detect_tags(server).await { Ok(detected_tags) => { - for tag in detected_tags.detected_tags { - println!("{}", tag); + for (tag, ciphertext) in detected_tags.detected_tags.iter() { + match profile.private_key.decrypt(ciphertext) { + Some(message) => { + println!("message: {}", message) + } + _ => {} + } + profile.update_previously_seen_tag(tag); } - }, + } Err(err) => { println!("Error: {}", err) } } }); + + match profile.save(&opts.profile) { + Err(e) => { + println!("[ERROR] {}", e) + } + _ => {} + } } } } diff --git a/niwl-rem/Cargo.toml b/niwl-rem/Cargo.toml new file mode 100644 index 0000000..30886bd --- /dev/null +++ b/niwl-rem/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "niwl-rem" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +niwl = {path="../niwl"} +fuzzytags = "0.4.2" +clap = "3.0.0-beta.2" +serde = {version="1.0.123", features=["derive"]} +serde_json = "1.0.61" +bincode = "1.3.1" +hex = "0.4.2" +base32 = "0.4.0" +reqwest = {version="0.11.0", features=["json"]} +tokio = "1.2.0" +chrono = {version="0.4.19", features=["serde"]} \ No newline at end of file diff --git a/niwl-rem/src/lib.rs b/niwl-rem/src/lib.rs new file mode 100644 index 0000000..f72d92b --- /dev/null +++ b/niwl-rem/src/lib.rs @@ -0,0 +1,58 @@ +use crate::MixMessage::{Forward, Heartbeat}; +use chrono::{DateTime, Duration, Local, NaiveDateTime}; +use fuzzytags::{RootSecret, Tag, TaggingKey}; +use niwl::encrypt::{PrivateKey, TaggedCiphertext}; +use niwl::Profile; +use serde::{Deserialize, Serialize}; +use std::fmt::Error; + +#[derive(Serialize, Deserialize)] +pub enum MixMessage { + Heartbeat(Tag<24>, DateTime), + Forward(TaggedCiphertext), +} + +pub struct RandomEjectionMix { + heartbeat_id: Tag<24>, +} + +impl RandomEjectionMix { + pub fn init(tag: Tag<24>) -> RandomEjectionMix { + RandomEjectionMix { heartbeat_id: tag } + } + + pub fn push(&mut self, tag: &Tag<24>, plaintext: &String) -> Option { + // The plaintext can either be a TaggedCiphertext OR a HeartBeat + let message: serde_json::Result = + serde_json::from_str(plaintext.as_str()); + match &message { + Ok(ciphertext) => return Some(Forward(self.random_ejection_mix(ciphertext))), + Err(_) => { + // Assume this is a Mix Message + let message: serde_json::Result = + serde_json::from_str(plaintext.as_str()); + match &message { + Ok(mixMessage) => match mixMessage { + Heartbeat(id, time) => self.process_heartbeat(id, time), + _ => None, + }, + Err(_) => None, + } + } + } + } + + fn process_heartbeat(&self, tag: &Tag<24>, heartbeat: &DateTime) -> Option { + if tag == &self.heartbeat_id { + println!("Received HeartBeat @ {}", heartbeat); + let new_heartbeat = Heartbeat(self.heartbeat_id.clone(), Local::now()); + return Some(new_heartbeat); + } + None + } + + // Actually do the Random Ejection Mixing... + fn random_ejection_mix(&mut self, ciphertext: &TaggedCiphertext) -> TaggedCiphertext { + ciphertext.clone() + } +} diff --git a/niwl-rem/src/main.rs b/niwl-rem/src/main.rs new file mode 100644 index 0000000..52b0226 --- /dev/null +++ b/niwl-rem/src/main.rs @@ -0,0 +1,135 @@ +use chrono::Local; +use clap::Clap; +use niwl::Profile; +use niwl_rem::MixMessage::Heartbeat; +use niwl_rem::{MixMessage, RandomEjectionMix}; +use std::time::Duration; + +#[derive(Clap)] +#[clap(version = "1.0", author = "Sarah Jamie Lewis ")] +struct Opts { + #[clap(default_value = "niwl.profile")] + profile_filename: String, + + #[clap(default_value = "http://localhost:8000")] + niwl_server: String, + + #[clap(subcommand)] + subcmd: SubCommand, +} + +#[derive(Clap)] +enum SubCommand { + Generate(Generate), + Run(Run), +} + +/// Generate a new niwl.profile file +#[derive(Clap)] +struct Generate { + name: String, +} + +/// Run a Random Ejection Mix +#[derive(Clap)] +struct Run {} + +fn main() { + let opts: Opts = Opts::parse(); + match opts.subcmd { + SubCommand::Generate(g) => { + let profile = Profile::new(g.name.clone(), 0); + let hotk = profile.keyset(); + println!( + "Tagging Key: {}", + base32::encode( + base32::Alphabet::RFC4648 { padding: false }, + bincode::serialize(&hotk).unwrap().as_slice() + ) + .to_ascii_lowercase() + ); + profile.save(&opts.profile_filename); + } + SubCommand::Run(_cmd) => { + let mut profile = Profile::get_profile(&opts.profile_filename); + let server = opts.niwl_server.clone(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + let random_tag = profile.root_secret.tagging_key().generate_tag(); + let mut rem = RandomEjectionMix::init(random_tag.clone()); + println!("kicking off initial heartbeat..."); + profile + .send_to_self( + &server, + &serde_json::to_string(&Heartbeat(random_tag.clone(), Local::now())) + .unwrap(), + ) + .await; + println!("starting.."); + let detection_key = profile.root_secret.extract_detection_key(24); + + loop { + match profile.detect_tags(&server).await { + Ok(detected_tags) => { + let mut latest_tag = None; + for (tag, ciphertext) in detected_tags.detected_tags.iter() { + if detection_key.test_tag(&tag) { + let plaintext = profile.private_key.decrypt(ciphertext); + match plaintext { + Some(plaintext) => match rem.push(tag, &plaintext) { + None => {} + Some(message) => { + let response = match &message { + MixMessage::Heartbeat(_, _) => { + profile + .send_to_self( + &server, + &serde_json::to_string( + &message, + ) + .unwrap(), + ) + .await + } + MixMessage::Forward(ciphertext) => { + profile + .forward(&server, ciphertext) + .await + } + }; + + match response { + Err(err) => { + println!("[ERROR] {:?}", err); + } + _ => {} + } + } + }, + _ => {} + } + } + latest_tag = Some(tag.clone()); + } + println!("Updating..."); + match &latest_tag { + Some(tag) => { + profile.update_previously_seen_tag(tag); + } + _ => {} + } + } + Err(err) => { + println!("Error: {}", err) + } + } + println!("sleeping.."); + tokio::time::sleep(Duration::new(5, 0)).await; + } + }); + } + } +} diff --git a/niwl-server/Cargo.toml b/niwl-server/Cargo.toml index 3bae9bf..0a3e93f 100644 --- a/niwl-server/Cargo.toml +++ b/niwl-server/Cargo.toml @@ -7,7 +7,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fuzzytags = {path="../../fuzzymetatag"} +niwl = {path="../niwl"} +fuzzytags = "0.4.2" rocket = "0.4.6" rocket_contrib = {version="0.4.6", features=["sqlite_pool"]} -chrono = "0.4.19" \ No newline at end of file +serde_json = "1.0.61" \ No newline at end of file diff --git a/niwl-server/sql/create.sql b/niwl-server/sql/create.sql index 67b6d0b..72f00db 100644 --- a/niwl-server/sql/create.sql +++ b/niwl-server/sql/create.sql @@ -1,4 +1,5 @@ -create table if not exists tags ( - id text primary key, - tag blob not null unique - ) \ No newline at end of file +CREATE TABLE IF NOT EXISTS tags ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + tag BLOB NOT NULL UNIQUE, + message TEXT NOT NULL +) \ No newline at end of file diff --git a/niwl-server/src/main.rs b/niwl-server/src/main.rs index 6c37c59..f9c017b 100644 --- a/niwl-server/src/main.rs +++ b/niwl-server/src/main.rs @@ -1,55 +1,131 @@ #![feature(proc_macro_hygiene, decl_macro)] -#[macro_use] extern crate rocket; -#[macro_use] extern crate rocket_contrib; +#[macro_use] +extern crate rocket; +#[macro_use] +extern crate rocket_contrib; -use rocket_contrib::json; use fuzzytags::{DetectionKey, Tag}; -use rocket_contrib::json::{Json, JsonValue}; +use niwl::encrypt::TaggedCiphertext; +use niwl::{FetchMessagesRequest, PostMessageRequest}; use rocket_contrib::databases::rusqlite; use rocket_contrib::databases::rusqlite::types::ToSql; -use chrono::{Utc, Duration}; -use std::ops::Sub; +use rocket_contrib::json; +use rocket_contrib::json::{Json, JsonValue}; #[database("tags")] struct TagsDbConn(rusqlite::Connection); -#[post("/new", format = "application/json", data = "")] -fn new(conn:TagsDbConn, tag: Json>) -> JsonValue { - conn.0.execute( - "INSERT INTO tags (id, tag) VALUES (strftime('%Y-%m-%d %H:%M:%S:%f', 'now'), ?1)", - &[&tag.0.compress() as &dyn ToSql], - ).unwrap(); - json!({"tag" : tag.to_string()}) -} - -#[post("/tags", format = "application/json", data = "")] -fn tags(conn:TagsDbConn, detection_key: Json>) -> JsonValue { - - let mut stmt = conn.0.prepare( - "SELECT tag FROM tags WHERE id > (?1) AND id < (?2)", - ).unwrap(); - - let now = Utc::now(); - let after = now.sub(Duration::days(1)).format("%Y-%m-%d %H:%M:%S:%f").to_string(); - let before = now.format("%Y-%m-%d %H:%M:%S:%f").to_string(); - let selected_tags = stmt.query_map(&[&after, &before], |row| { - let tag_bytes : Vec = row.get(0); - let tag = Tag::<24>::decompress(tag_bytes.as_slice()).unwrap(); - tag - }).unwrap(); - - let mut detected_tags : Vec> = vec![]; - for tag in selected_tags { - let tag : Tag<24> = tag.unwrap(); - if detection_key.0.test_tag(&tag) { - detected_tags.push(tag); +#[post("/new", format = "application/json", data = "")] +fn new(conn: TagsDbConn, post_message_request: Json) -> JsonValue { + match serde_json::to_string(&post_message_request.ciphertext) { + Ok(ciphertext) => { + match conn.0.execute( + "INSERT INTO tags (tag, message) VALUES (?1, ?2);", + &[ + &post_message_request.tag.compress() as &dyn ToSql, + &ciphertext, + ], + ) { + Ok(_) => { + json!({"tag" : post_message_request.tag.to_string()}) + } + Err(_) => { + json!({"tag" : "error"}) + } + } + } + _ => { + json!({"tag" : "error"}) } } +} - json!({"detected_tags" : detected_tags}) +#[post("/tags", format = "application/json", data = "")] +fn tags(conn: TagsDbConn, fetch_message_request: Json) -> JsonValue { + let mut detected_tags: Vec<(Tag<24>, TaggedCiphertext)> = vec![]; + + let mut select = conn + .0 + .prepare("SELECT tag,message FROM tags WHERE id>(SELECT id FROM tags WHERE tag=(?));") + .unwrap(); + + let mut select_all = conn.0.prepare("SELECT tag,message FROM tags;").unwrap(); + + let all = match &fetch_message_request.reference_tag { + Some(tag) => { + let mut stmt = conn + .0 + .prepare("SELECT COUNT(*) FROM tags WHERE tag=(?);") + .unwrap(); + let count = stmt.query_row(&[&tag.compress() as &dyn ToSql], |row| { + let count: i32 = row.get(0); + return count; + }); + match count { + Ok(count) => count == 0, + _ => true, + } + } + None => true, + }; + + match all { + false => { + let ref_tag = fetch_message_request.reference_tag.clone().unwrap(); + let selected_tags = select + .query_map(&[&ref_tag.compress() as &dyn ToSql], |row| { + let tag_bytes: Vec = row.get(0); + let tag = Tag::<24>::decompress(tag_bytes.as_slice()).unwrap(); + + let ciphertext_json: String = row.get(1); + let message: TaggedCiphertext = + serde_json::from_str(ciphertext_json.as_str()).unwrap(); + (tag, message) + }) + .unwrap(); + for result in selected_tags { + match result { + Ok((tag, ciphertext)) => { + if fetch_message_request.detection_key.test_tag(&tag) { + detected_tags.push((tag, ciphertext)); + } + } + _ => {} + } + } + } + true => { + let selected_tags = select_all + .query_map(&[], |row| { + let tag_bytes: Vec = row.get(0); + let tag = Tag::<24>::decompress(tag_bytes.as_slice()).unwrap(); + + let ciphertext_json: String = row.get(1); + let message: TaggedCiphertext = + serde_json::from_str(ciphertext_json.as_str()).unwrap(); + (tag, message) + }) + .unwrap(); + for result in selected_tags { + match result { + Ok((tag, ciphertext)) => { + if fetch_message_request.detection_key.test_tag(&tag) { + detected_tags.push((tag, ciphertext)); + } + } + _ => {} + } + } + } + }; + + json!({ "detected_tags": detected_tags }) } fn main() { - rocket::ignite().attach(TagsDbConn::fairing()).mount("/", routes![tags, new]).launch(); + rocket::ignite() + .attach(TagsDbConn::fairing()) + .mount("/", routes![tags, new]) + .launch(); } diff --git a/niwl/Cargo.toml b/niwl/Cargo.toml index d962810..295d1b7 100644 --- a/niwl/Cargo.toml +++ b/niwl/Cargo.toml @@ -7,10 +7,14 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -fuzzytags = {path="../../fuzzymetatag"} +fuzzytags = "0.4.2" serde = {version="1.0.123", features=["derive"]} serde_json = "1.0.61" bincode = "1.3.1" hex = "0.4.2" base32 = "0.4.0" -reqwest = {version="0.11.0", features=["json"]} \ No newline at end of file +rand = "0.7.3" +curve25519-dalek = {version="3.0.0", features=["serde"]} +sha3 = "0.9.1" +reqwest = {version="0.11.0", features=["json"]} +secretbox = {version="0.1.2"} \ No newline at end of file diff --git a/niwl/src/encrypt.rs b/niwl/src/encrypt.rs new file mode 100644 index 0000000..7dad9b1 --- /dev/null +++ b/niwl/src/encrypt.rs @@ -0,0 +1,120 @@ +use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT; +use curve25519_dalek::digest::Digest; +use curve25519_dalek::ristretto::RistrettoPoint; +use curve25519_dalek::scalar::Scalar; +use fuzzytags::Tag; +use rand::rngs::OsRng; +use secretbox::CipherType::Salsa20; +use secretbox::SecretBox; +use serde::{Deserialize, Serialize}; +use std::ops::Mul; + +/// TaggedCiphertext is a wrapper around a Tag and an encrypted payload (in addition to a +/// nonce value). +#[derive(Serialize, Deserialize, Clone)] +pub struct TaggedCiphertext { + pub tag: Tag<24>, + nonce: RistrettoPoint, + ciphertext: Vec, +} + +/// A Private Key used when encrypting to a niwl client +#[derive(Serialize, Deserialize)] +pub struct PrivateKey(Scalar); + +/// A Public Key derived from a niwl PrivateKey +#[derive(Serialize, Deserialize)] +pub struct PublicKey(RistrettoPoint); + +impl PublicKey { + /// Encrypt to Tag provides uni-directional encrypted + pub fn encrypt(&self, tag: &Tag<24>, message: &String) -> TaggedCiphertext { + // Generate a random point. We will use the public part as a nonce + // And the private part to generate a key. + let mut rng = OsRng::default(); + let r = Scalar::random(&mut rng); + let z = RISTRETTO_BASEPOINT_POINT.mul(r); + + // Compile our (public) nonce...we derive a new random nonce by hashing + // the public z parameter with the tag. + let mut nonce_hash = sha3::Sha3_256::new(); + nonce_hash.update(z.compress().as_bytes()); + nonce_hash.update(tag.compress()); + let mut nonce = [0u8; 24]; + nonce[..].copy_from_slice(&nonce_hash.finalize().as_slice()[0..24]); + + // Calculate the key by multiplying part of the tagging key by our private 'r' + let mut hash = sha3::Sha3_256::new(); + hash.update(self.0.mul(r).compress().as_bytes()); + hash.update(tag.compress()); + let key = hash.finalize().to_vec(); + let secret_box = SecretBox::new(key, Salsa20).unwrap(); + + let ciphertext = secret_box.seal(message.as_bytes(), nonce); + TaggedCiphertext { + tag: tag.clone(), + nonce: z, + ciphertext, + } + } +} + +impl PrivateKey { + pub fn generate() -> PrivateKey { + let mut rng = OsRng::default(); + let r = Scalar::random(&mut rng); + PrivateKey { 0: r } + } + + pub fn public_key(&self) -> PublicKey { + PublicKey { + 0: RISTRETTO_BASEPOINT_POINT.mul(self.0), + } + } + + /// Decrypt a tagged ciphertext + pub fn decrypt(&self, ciphertext: &TaggedCiphertext) -> Option { + // Derive the public nonce... + let mut nonce_hash = sha3::Sha3_256::new(); + nonce_hash.update(ciphertext.nonce.compress().as_bytes()); + nonce_hash.update(ciphertext.tag.compress()); + let mut nonce = [0u8; 24]; + nonce[..].copy_from_slice(&nonce_hash.finalize().as_slice()[0..24]); + + // Calculate the key by multiplying the public point with our private 'x' + let mut hash = sha3::Sha3_256::new(); + hash.update(ciphertext.nonce.mul(self.0).compress().as_bytes()); + hash.update(ciphertext.tag.compress()); + let key = hash.finalize().to_vec(); + + let secret_box = SecretBox::new(key, Salsa20).unwrap(); + match secret_box.unseal(ciphertext.ciphertext.as_slice(), nonce) { + Some(plaintext) => match String::from_utf8(plaintext) { + Ok(plaintext) => Some(plaintext), + Err(_) => None, + }, + None => None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::encrypt::PrivateKey; + use fuzzytags::RootSecret; + + #[test] + fn test_encrypt_to_tag() { + let secret = PrivateKey::generate(); + let public_key = secret.public_key(); + + let root_secret = RootSecret::<24>::generate(); + let tagging_key = root_secret.tagging_key(); + + let ciphertext = + public_key.encrypt(&tagging_key.generate_tag(), &String::from("Hello World")); + + let plaintext = secret.decrypt(&ciphertext); + assert_eq!(plaintext.unwrap(), String::from("Hello World")) + } +} diff --git a/niwl/src/lib.rs b/niwl/src/lib.rs index 58d37a8..27e14d1 100644 --- a/niwl/src/lib.rs +++ b/niwl/src/lib.rs @@ -1,51 +1,88 @@ #![feature(into_future)] -use fuzzytags::{RootSecret, TaggingKey, Tag}; -use std::fs::File; -use std::io::Write; +use crate::encrypt::{PrivateKey, PublicKey, TaggedCiphertext}; +use fuzzytags::{DetectionKey, RootSecret, Tag, TaggingKey}; +use reqwest::{Error, Response}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use reqwest::{Response, Error}; -use std::future::{Future, IntoFuture}; +use std::fs; +use std::fs::File; +use std::io::Write; + +pub mod encrypt; #[derive(Debug)] pub enum NiwlError { NoKnownContactError(String), - RemoteServerError(String) + RemoteServerError(String), } -#[derive(Serialize,Deserialize)] +#[derive(Serialize, Deserialize)] pub struct Profile { profile_name: String, - root_secret: RootSecret<24>, - tagging_keys: HashMap>, + pub root_secret: RootSecret<24>, + pub private_key: PrivateKey, + tagging_keys: HashMap, PublicKey)>, + detection_key_length: usize, + last_seen_tag: Option>, } -#[derive(Serialize,Deserialize)] -pub struct HumanOrientedTaggingKey { +#[derive(Serialize, Deserialize)] +pub struct KeySet { profile_name: String, tagging_key: TaggingKey<24>, + public_key: PublicKey, } #[derive(Deserialize)] pub struct DetectedTags { - pub detected_tags: Vec>, + pub detected_tags: Vec<(Tag<24>, TaggedCiphertext)>, +} + +#[derive(Serialize, Deserialize)] +pub struct FetchMessagesRequest { + // The last tag this client downloaded to use as a reference when fetching new messages + // If None, then the server will check *all* messages. + pub reference_tag: Option>, + // The detection key to use to fetch new messages + pub detection_key: DetectionKey<24>, +} + +#[derive(Serialize, Deserialize)] +pub struct PostMessageRequest { + pub tag: Tag<24>, + pub ciphertext: TaggedCiphertext, } impl Profile { - pub fn new(profile_name: String) -> Profile { - let root_secret = RootSecret::<24>::generate(); - Profile { - profile_name, - root_secret, - tagging_keys: Default::default() + pub fn get_profile(profile_filename: &String) -> Profile { + match fs::read_to_string(profile_filename) { + Ok(json) => serde_json::from_str(json.as_str()).unwrap(), + Err(why) => { + panic!("couldn't read orb.profile : {}", why); + } } } - pub fn human_readable_tagging_key(&self) -> HumanOrientedTaggingKey { + pub fn new(profile_name: String, detection_key_length: usize) -> Profile { + let root_secret = RootSecret::<24>::generate(); + let private_key = PrivateKey::generate(); + Profile { + profile_name, + root_secret, + private_key, + tagging_keys: Default::default(), + detection_key_length, + last_seen_tag: None, + } + } + + pub fn keyset(&self) -> KeySet { let tagging_key = self.root_secret.tagging_key(); - HumanOrientedTaggingKey { + let public_key = self.private_key.public_key(); + KeySet { profile_name: self.profile_name.clone(), - tagging_key + tagging_key, + public_key, } } @@ -60,62 +97,141 @@ impl Profile { pub fn generate_tag(&self, id: &String) -> Result, NiwlError> { if self.tagging_keys.contains_key(id) { - let tag = self.tagging_keys[id].generate_tag(); + let tag = self.tagging_keys[id].0.generate_tag(); println!("Tag for {} {}", id, tag.to_string()); - return Ok(tag) + return Ok(tag); } - Err(NiwlError::NoKnownContactError(format!("No known friend {}. Perhaps you need to import-tagging-key first?", id))) + Err(NiwlError::NoKnownContactError(format!( + "No known friend {}. Perhaps you need to import-tagging-key first?", + id + ))) } pub fn import_tagging_key(&mut self, key: &String) { match base32::decode(base32::Alphabet::RFC4648 { padding: false }, key.as_str()) { Some(data) => { - let tagging_key_result: Result = bincode::deserialize(&data); + let tagging_key_result: Result = + bincode::deserialize(&data); match tagging_key_result { Ok(hotk) => { println!("Got: {}: {}", hotk.profile_name, hotk.tagging_key.id()); if self.tagging_keys.contains_key(&hotk.profile_name) == false { - self.tagging_keys.insert(hotk.profile_name, hotk.tagging_key); + self.tagging_keys + .insert(hotk.profile_name, (hotk.tagging_key, hotk.public_key)); } else { println!("There is already an entry for {}", hotk.profile_name) } - return + return; } Err(err) => { println!("Error: {}", err.to_string()); } } - }, + } _ => {} }; println!("Error Reporting Tagging Key") } - pub async fn tag_and_send(&self, server: String, contact: String) -> Result { - let client = reqwest::Client::new(); + pub async fn tag_and_mix( + &self, + server: String, + mix: String, + contact: String, + message: &String, + ) -> Result { match self.generate_tag(&contact) { Ok(tag) => { - let result = client. - post(&String::from(server + "/new")) - .json(&tag) - .send().await; - match result { - Ok(response) => Ok(response), - Err(err) => Err(NiwlError::RemoteServerError(err.to_string())) - } - } - Err(err) => { - Err(err) + let ciphertext = self.tagging_keys[&contact].1.encrypt(&tag, message); + let ciphertext_json = serde_json::to_string(&ciphertext).unwrap(); + return self.tag_and_send(&server, mix, &ciphertext_json).await; } + Err(err) => Err(err), } } - pub async fn detect_tags(&mut self, server: String) -> Result { + pub async fn send_to_self( + &self, + server: &String, + message: &String, + ) -> Result { let client = reqwest::Client::new(); - let detection_key = self.root_secret.extract_detection_key(1); - let result = client.post(&String::from(server + "/tags")) - .json(&detection_key) - .send().await; + let tag = self.root_secret.tagging_key().generate_tag(); + let ciphertext = self.private_key.public_key().encrypt(&tag, message); + + let result = client + .post(&format!("{}/new", server)) + .json(&PostMessageRequest { tag, ciphertext }) + .send() + .await; + match result { + Ok(response) => Ok(response), + Err(err) => Err(NiwlError::RemoteServerError(err.to_string())), + } + } + + pub async fn forward( + &self, + server: &String, + message: &TaggedCiphertext, + ) -> Result { + let client = reqwest::Client::new(); + let tag = message.tag.clone(); + let ciphertext = message.clone(); + + let result = client + .post(&format!("{}/new", server)) + .json(&PostMessageRequest { tag, ciphertext }) + .send() + .await; + match result { + Ok(response) => Ok(response), + Err(err) => Err(NiwlError::RemoteServerError(err.to_string())), + } + } + + pub async fn tag_and_send( + &self, + server: &String, + contact: String, + message: &String, + ) -> Result { + let client = reqwest::Client::new(); + match self.generate_tag(&contact) { + Ok(tag) => { + let ciphertext = self.tagging_keys[&contact].1.encrypt(&tag, message); + + let result = client + .post(&format!("{}/new", server)) + .json(&PostMessageRequest { tag, ciphertext }) + .send() + .await; + match result { + Ok(response) => Ok(response), + Err(err) => Err(NiwlError::RemoteServerError(err.to_string())), + } + } + Err(err) => Err(err), + } + } + + pub async fn detect_tags(&mut self, server: &String) -> Result { + let client = reqwest::Client::new(); + let detection_key = self + .root_secret + .extract_detection_key(self.detection_key_length); + let result = client + .post(&format!("{}/tags", server)) + .json(&FetchMessagesRequest { + reference_tag: self.last_seen_tag.clone(), + detection_key, + }) + .send() + .await; result.unwrap().json().await } -} \ No newline at end of file + + pub fn update_previously_seen_tag(&mut self, tag: &Tag<24>) { + self.last_seen_tag = Some(tag.clone()); + } +} diff --git a/references/heartbeat.pdf b/references/heartbeat.pdf new file mode 100644 index 0000000..745a0e2 Binary files /dev/null and b/references/heartbeat.pdf differ