Initial Commit

This commit is contained in:
Sarah Jamie Lewis 2021-02-10 19:37:16 -08:00
commit 0bdaa94dd5
13 changed files with 2680 additions and 0 deletions

3
.gitignore vendored Normal file
View File

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

2283
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

2
Cargo.toml Normal file
View File

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

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# niwl - a prototype system for metadata resistant notifications
**niwl** (_/nɪu̯l/_) - fog, mist or haze (Welsh).
Privacy preserving applications often require a mechanism for providing notifications to parties that an
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
**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.
For a more detailed overview please check out each individual crate.
# Example
niwl-client alice.profile generate "alice"
Tagging Key: auaaaaaaaaaaaylmnfrwkgaaaaaaaaaaadlbii3y7r6vmc7upbxa4myohaqmr5xl22bdxeed4abkotnovlmakzdo5stq2ibtjewm4rnkgzqwglrt72zfeyomvdpxqnu4ci4hwebyiseyn7pqfxypnvef7a3flu2hby7gdluh6wocxa5mvmimi2xorydcqaca2p2aevmue4cwyxnw2h7fkps7e6grgls66zgohbnwjibt6nlsdqjbrdjrzlsc3at3f43jyniz2i67ng6xdty5pr3elzedhjlvefhd6pjfc7g4owrz3dkq5xt2hhh3vvctkywqkcwriguayyx3pourepfs7s76bekrjgcjgj6zyid3ixmeh5ewqhkhxhzevf3uogvscxtpbksaclhccht7pj2fungnztfghshd6lsmegmysiiuyav6schtmyxmne2vfi4j4cxllm2crj3cqofsxjlxov3ms2zgtjzyxtubwtnwspc4jhijz4kufm6r3qkhpcyibx7ulceckx2a4g23tkhtgshtxq3fga7ptbhq5gcebwiq6cfolt4zbn72gbmtc43nw63vd4soxf4bnbhrykaoudfs3mh6laap6iwbngo4ylocs4w5hgd4t22yrtrmhkewsc2eytsosxyhaiuaww24mszscsojm2bcoldpokwuxbnfx7lgnzdcuae3y55zoen47noltjqgcpuqzl6upjcvutgvvro6nu2uyl36rcqmw2by2e45uqtsdnolbispxv2e5aeeuz5gytuf5f5e44nldmywtmxkfqfljml5gye6tj3qswmz6d36f2k4v7fbiuv7jzplzmghsgxvmq7fo3qp655obysbggkd3iqpk76p5umbpc2tk64oiklrponulkqf3v337aaxyn6nvzz2rpj3o374tftscsr7oilzkah63xpe2jc45dd4fuwxvlg3c33zgkminemqqfz7jdjtnawy77vpxxgnosbw4fwadhhggofmipboiqo55xygojdnfdkuzgfe4455sdqv5ytzdl55yuzlbdgsnwtgnfakmoyjhblzbuwohq7esayfxe72yqgci5dappiad7bc3ikfsydv5b7stifajkxuosu345upxg2hwzajj4uu7lxaykxgo22pslkxnidaoyevn3gamx63ec4fkhzguhbu6jt7pukr4rpafx24vd622f5wzux4corlxthjuhi2ewiu6laxx3aqfkzv2d2hhqzsac25vycmmxy
niwl-client bob.profile generate "bob"
Tagging Key: auaaaaaaaaaaaylmnfrwkgaaaaaaaaaaadlbii3y7r6vmc7upbxa4myohaqmr5xl22bdxeed4abkotnovlmakzdo5stq2ibtjewm4rnkgzqwglrt72zfeyomvdpxqnu4ci4hwebyiseyn7pqfxypnvef7a3flu2hby7gdluh6wocxa5mvmimi2xorydcqaca2p2aevmue4cwyxnw2h7fkps7e6grgls66zgohbnwjibt6nlsdqjbrdjrzlsc3at3f43jyniz2i67ng6xdty5pr3elzedhjlvefhd6pjfc7g4owrz3dkq5xt2hhh3vvctkywqkcwriguayyx3pourepfs7s76bekrjgcjgj6zyid3ixmeh5ewqhkhxhzevf3uogvscxtpbksaclhccht7pj2fungnztfghshd6lsmegmysiiuyav6schtmyxmne2vfi4j4cxllm2crj3cqofsxjlxov3ms2zgtjzyxtubwtnwspc4jhijz4kufm6r3qkhpcyibx7ulceckx2a4g23tkhtgshtxq3fga7ptbhq5gcebwiq6cfolt4zbn72gbmtc43nw63vd4soxf4bnbhrykaoudfs3mh6laap6iwbngo4ylocs4w5hgd4t22yrtrmhkewsc2eytsosxyhaiuaww24mszscsojm2bcoldpokwuxbnfx7lgnzdcuae3y55zoen47noltjqgcpuqzl6upjcvutgvvro6nu2uyl36rcqmw2by2e45uqtsdnolbispxv2e5aeeuz5gytuf5f5e44nldmywtmxkfqfljml5gye6tj3qswmz6d36f2k4v7fbiuv7jzplzmghsgxvmq7fo3qp655obysbggkd3iqpk76p5umbpc2tk64oiklrponulkqf3v337aaxyn6nvzz2rpj3o374tftscsr7oilzkah63xpe2jc45dd4fuwxvlg3c33zgkminemqqfz7jdjtnawy77vpxxgnosbw4fwadhhggofmipboiqo55xygojdnfdkuzgfe4455sdqv5ytzdl55yuzlbdgsnwtgnfakmoyjhblzbuwohq7esayfxe72yqgci5dappiad7bc3ikfsydv5b7stifajkxuosu345upxg2hwzajj4uu7lxaykxgo22pslkxnidaoyevn3gamx63ec4fkhzguhbu6jt7pukr4rpafx24vd622f5wzux4corlxthjuhi2ewiu6laxx3aqfkzv2d2hhqzsac25vycmmxy
niwl-client alice.profile import-tagging-key amaaaaaaaaaaaytpmimaaaaaaaaaaafaukgy7543bcnjyq4jbthaovnfxtdnya3jajmbwa4t5gmihqgudbta4nzigrhzirkekers23ng2lr4zbjspthybajjj7vbwn6wnied27e2jvuipqbinru2q7eumgbt62spztz3rpslymv4iwsujozb7ylcfr7ugroilpgxzrjniussojm4q3kun247o4kqjzcrec4ohcuiyaiinourb7h7j4qjv4ne46xhnptwsfjr5s7yz2igqsbpvrqeiy5u6khmxwpi2jzxrnk5qlixewjcbe3zzy4qpxnl7ybdds6tld522amonc2dxncff2ihribsdnd5fc5dozqu2eqqxqmyvnd5pdngozhqikdc6ovj4uzf2ttabckbr4sim6z3fkl7kd5wqjjdaosahqsi67gy47q3vd3ubtu5btx2lmgkmyzm2wuupvwxxvc65lcxghm43bu4yah76jb3u36kg4nzdemuxewxcswofymuvdxh24uqyyhn7ymlr6mnuuk6g6acy4bcu7gsiacu3am6qwfve7s5wckgbaqc6veafbzynjmv6wubkleas2ghkirnl3pdznf37pyz62hkjssiqzqlduhkcyghdkdzccrtnesdkob447zlxaj2qz24chuxpy7hkffx64fi7aqzkpujifagrkcxvroq43wl2hme7udqcwpdjdtqm7yhnnanazuahtqlvf3ux7kvmidevorrgaiephptm7qgk7ezw6aa7o3fjyra7m3xknbmpniqa4dnwg44cfgbj2ln6kcecgat4d5cokabzk64jjhfq4m6upoptya4bjy2chdhged4jsqvp646u4klk5zk2fmhgf7l365k2g4z4i2u2dveajldebgnwc4esdrmequawk5sk6oskurfsuj6i4v7jzeqcd5hbujllka4dq3cn3hidx67jspqefnwj6yr7fvwjvvtjo3ri5wcqqd3m5kjrmhjuxbho7sl5xnhzntdtha7ldwml6xkwg3x7scsmhikvkrzfenggxccunpyp6cmsslblfbdrwvl4qurnrdg7wqtlgy6pfhjrqwqej26e7ddxwax34fagkgqksupdutbaq2juvicn7rq3xrxs6bi6a2ozdfdyfbtun5ckz6ln2jxyu254appisier35e3d6nmicywn5p7m32cvcct3ilub4ovk4jpuizegyudyrcgoud2rmuyei6fdgrbvtdqv7gqy6a
Got: bob: 4211f14667b425649390eee099c4e84bf758a1a4e6375e23b50c6de347c25654
niwl-client alice.profile tag-and-send bob
Tag for bob 7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da
niwl-client bob.profile detect 10
7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da

19
niwl-client/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "niwl-client"
version = "0.1.0"
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
niwl = {path="../niwl"}
fuzzytags = {path="../../fuzzymetatag"}
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"

114
niwl-client/src/main.rs Normal file
View File

@ -0,0 +1,114 @@
use clap::Clap;
use std::fs;
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,
#[clap(default_value = "http://localhost:8000")]
niwl_server: String,
#[clap(subcommand)]
subcmd: SubCommand,
}
#[derive(Clap)]
enum SubCommand {
Generate(Generate),
ImportTaggingKey(ImportTaggingKey),
TagAndSend(TagAndSend),
Detect(Detect)
}
/// Generate a new niwl.profile file
#[derive(Clap)]
struct Generate {
name: String
}
/// Import a friends tagging key into this profile so you can send messages to them
#[derive(Clap)]
struct ImportTaggingKey {
key: String
}
/// Connect to a server and check for new notifications
#[derive(Clap)]
struct Detect {
#[clap(default_value = "2")]
length: u8
}
/// Send a message to a friend tagged with their niwl key
#[derive(Clap)]
struct TagAndSend {
/// the id of the friend e.g. "alice"
id: String,
/// the message you want to send.
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);
}
}
}
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);
}
SubCommand::ImportTaggingKey(cmd) => {
let mut profile = get_profile(&opts.profile_filename);
profile.import_tagging_key(&cmd.key);
profile.save(&opts.profile_filename);
},
SubCommand::TagAndSend(cmd) => {
let mut profile = get_profile(&opts.profile_filename);
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).await;
println!("{}", result.unwrap().text().await.unwrap());
});
},
SubCommand::Detect(cmd) => {
let mut 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 {
match profile.detect_tags(server).await {
Ok(detected_tags) => {
for tag in detected_tags.detected_tags {
println!("{}", tag);
}
},
Err(err) => {
println!("Error: {}", err)
}
}
});
}
}
}

13
niwl-server/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "niwl-server"
version = "0.1.0"
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fuzzytags = {path="../../fuzzymetatag"}
rocket = "0.4.6"
rocket_contrib = {version="0.4.6", features=["sqlite_pool"]}
chrono = "0.4.19"

13
niwl-server/README.md Normal file
View File

@ -0,0 +1,13 @@
## Building
Requires `libsqlite3`
sudo apt install libsqlite3-dev
## Running
First setup the database:
cat sql/create.sql | sqlite3 tags.sqlite

2
niwl-server/Rocket.toml Normal file
View File

@ -0,0 +1,2 @@
[global.databases]
tags = { url = "./tags.sqlite", pool_size = 20 }

View File

@ -0,0 +1,4 @@
create table if not exists tags (
id text primary key,
tag blob not null unique
)

55
niwl-server/src/main.rs Normal file
View File

@ -0,0 +1,55 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[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 rocket_contrib::databases::rusqlite;
use rocket_contrib::databases::rusqlite::types::ToSql;
use chrono::{Utc, Duration};
use std::ops::Sub;
#[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("/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);
}
}
json!({"detected_tags" : detected_tags})
}
fn main() {
rocket::ignite().attach(TagsDbConn::fairing()).mount("/", routes![tags, new]).launch();
}

16
niwl/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "niwl"
version = "0.1.0"
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fuzzytags = {path="../../fuzzymetatag"}
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"]}

121
niwl/src/lib.rs Normal file
View File

@ -0,0 +1,121 @@
#![feature(into_future)]
use fuzzytags::{RootSecret, TaggingKey, Tag};
use std::fs::File;
use std::io::Write;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use reqwest::{Response, Error};
use std::future::{Future, IntoFuture};
#[derive(Debug)]
pub enum NiwlError {
NoKnownContactError(String),
RemoteServerError(String)
}
#[derive(Serialize,Deserialize)]
pub struct Profile {
profile_name: String,
root_secret: RootSecret<24>,
tagging_keys: HashMap<String, TaggingKey<24>>,
}
#[derive(Serialize,Deserialize)]
pub struct HumanOrientedTaggingKey {
profile_name: String,
tagging_key: TaggingKey<24>,
}
#[derive(Deserialize)]
pub struct DetectedTags {
pub detected_tags: Vec<Tag<24>>,
}
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 human_readable_tagging_key(&self) -> HumanOrientedTaggingKey {
let tagging_key = self.root_secret.tagging_key();
HumanOrientedTaggingKey {
profile_name: self.profile_name.clone(),
tagging_key
}
}
pub fn save(&self, profile_filename: &String) -> std::io::Result<()> {
let j = serde_json::to_string(&self);
let mut file = match File::create(profile_filename) {
Err(why) => panic!("couldn't create : {}", why),
Ok(file) => file,
};
file.write_all(j.unwrap().as_bytes())
}
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();
println!("Tag for {} {}", id, tag.to_string());
return Ok(tag)
}
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);
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);
} else {
println!("There is already an entry for {}", hotk.profile_name)
}
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> {
let client = reqwest::Client::new();
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)
}
}
}
pub async fn detect_tags(&mut self, server: String) -> Result<DetectedTags, Error> {
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;
result.unwrap().json().await
}
}