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