forked from openprivacy/niwl
Adding REM Mixer Concept + Stub Code
This commit is contained in:
parent
0bdaa94dd5
commit
c2a3518e96
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
*.profile
|
||||
*.sqlite
|
||||
*.niwl
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[workspace]
|
||||
members=["./niwl", "niwl-client","niwl-server"]
|
||||
members=["niwl", "niwl-client","niwl-server","niwl-rem"]
|
94
README.md
94
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
|
||||
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,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)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("/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("/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"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json!({"detected_tags" : detected_tags})
|
||||
#[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 })
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
210
niwl/src/lib.rs
210
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<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 {
|
||||
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<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> {
|
||||
let client = reqwest::Client::new();
|
||||
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 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<DetectedTags, Error> {
|
||||
pub async fn send_to_self(
|
||||
&self,
|
||||
server: &String,
|
||||
message: &String,
|
||||
) -> Result<Response, NiwlError> {
|
||||
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<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 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<DetectedTags, Error> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_previously_seen_tag(&mut self, tag: &Tag<24>) {
|
||||
self.last_seen_tag = Some(tag.clone());
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue