Adding REM Mixer Concept + Stub Code

trunk
Sarah Jamie Lewis 2 years ago
parent 0bdaa94dd5
commit c2a3518e96
  1. 1
      .gitignore
  2. 8
      .idea/.gitignore
  3. 8
      .idea/modules.xml
  4. 15
      .idea/niwl.iml
  5. 6
      .idea/sqldialects.xml
  6. 6
      .idea/vcs.xml
  7. 71
      Cargo.lock
  8. 2
      Cargo.toml
  9. 94
      README.md
  10. 2
      niwl-client/Cargo.toml
  11. 114
      niwl-client/src/main.rs
  12. 19
      niwl-rem/Cargo.toml
  13. 58
      niwl-rem/src/lib.rs
  14. 135
      niwl-rem/src/main.rs
  15. 5
      niwl-server/Cargo.toml
  16. 9
      niwl-server/sql/create.sql
  17. 150
      niwl-server/src/main.rs
  18. 8
      niwl/Cargo.toml
  19. 120
      niwl/src/encrypt.rs
  20. 194
      niwl/src/lib.rs
  21. BIN
      references/heartbeat.pdf

1
.gitignore vendored

@ -1,3 +1,4 @@
/target
*.profile
*.sqlite
*.niwl

8
.idea/.gitignore vendored

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

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/niwl.iml" filepath="$PROJECT_DIR$/.idea/niwl.iml" />
</modules>
</component>
</project>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/niwl-client/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/niwl-rem/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/niwl-server/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/niwl/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/niwl-server/sql/create.sql" dialect="SQLite" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

71
Cargo.lock generated

@ -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]]
@ -1055,13 +1070,31 @@ dependencies = [
]
[[package]]
name = "niwl-server"
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 = [
"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"

@ -1,2 +1,2 @@
[workspace]
members=["./niwl", "niwl-client","niwl-server"]
members=["niwl", "niwl-client","niwl-server","niwl-rem"]

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

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

@ -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 <sarah@openprivacy.ca>")]
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,46 +50,79 @@ 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()
.enable_all()
.build()
.unwrap()
.block_on(async {
let result = profile.tag_and_send(server, contact, &cmd.message).await;
println!("{}", result.unwrap().text().await.unwrap());
});
}
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_send(server, contact).await;
let result = profile
.tag_and_mix(server, mix, contact, &cmd.message)
.await;
println!("{}", result.unwrap().text().await.unwrap());
});
},
SubCommand::Detect(cmd) => {
let mut profile = get_profile(&opts.profile_filename);
}
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)
}
_ => {}
}
}
}
}

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

@ -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<Local>),
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<MixMessage> {
// The plaintext can either be a TaggedCiphertext OR a HeartBeat
let message: serde_json::Result<TaggedCiphertext> =
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<MixMessage> =
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<Local>) -> Option<MixMessage> {
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()
}
}

@ -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 <sarah@openprivacy.ca>")]
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;
}
});
}
}
}

@ -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"
serde_json = "1.0.61"

@ -1,4 +1,5 @@
create table if not exists tags (
id text primary key,
tag blob not null unique
)
CREATE TABLE IF NOT EXISTS tags (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
tag BLOB NOT NULL UNIQUE,
message TEXT NOT NULL
)

@ -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 = "<tag>")]
fn new(conn:TagsDbConn, tag: Json<Tag<24>>) -> 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("/new", format = "application/json", data = "<post_message_request>")]
fn new(conn: TagsDbConn, post_message_request: Json<PostMessageRequest>) -> 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"})
}
}
}
#[post("/tags", format = "application/json", data = "<detection_key>")]
fn tags(conn:TagsDbConn, detection_key: Json<DetectionKey<24>>) -> 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<u8> = row.get(0);
let tag = Tag::<24>::decompress(tag_bytes.as_slice()).unwrap();
tag
}).unwrap();
let mut detected_tags : Vec<Tag<24>> = vec![];
for tag in selected_tags {
let tag : Tag<24> = tag.unwrap();
if detection_key.0.test_tag(&tag) {
detected_tags.push(tag);
#[post("/tags", format = "application/json", data = "<fetch_message_request>")]
fn tags(conn: TagsDbConn, fetch_message_request: Json<FetchMessagesRequest>) -> 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<u8> = 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<u8> = 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})
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();
}

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

@ -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<u8>,
}
/// 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<String> {
// 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"))
}
}

@ -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<String, TaggingKey<24>>,
pub root_secret: RootSecret<24>,
pub private_key: PrivateKey,
tagging_keys: HashMap<String, (TaggingKey<24>, PublicKey)>,
detection_key_length: usize,
last_seen_tag: Option<Tag<24>>,
}
#[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<Tag<24>>,
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<Tag<24>>,
// 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 {
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 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,
tagging_keys: Default::default()
private_key,
tagging_keys: Default::default(),
detection_key_length,
last_seen_tag: None,
}
}
pub fn human_readable_tagging_key(&self) -> HumanOrientedTaggingKey {
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<Tag<24>, 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<HumanOrientedTaggingKey, bincode::Error> = bincode::deserialize(&data);
let tagging_key_result: Result<KeySet, bincode::Error> =
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<Response, NiwlError> {
pub async fn tag_and_mix(
&self,
server: String,
mix: String,
contact: String,
message: &String,
) -> Result<Response, NiwlError> {
match self.generate_tag(&contact) {
Ok(tag) => {
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 send_to_self(
&self,
server: &String,
message: &String,
) -> Result<Response, NiwlError> {
let client = reqwest::Client::new();
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<Response, NiwlError> {
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<Response, NiwlError> {
let client = reqwest::Client::new();
match self.generate_tag(&contact) {
Ok(tag) => {
let result = client.
post(&String::from(server + "/new"))
.json(&tag)
.send().await;
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