forked from openprivacy/niwl
Adding REM Mixer Concept + Stub Code
This commit is contained in:
parent
0bdaa94dd5
commit
c2a3518e96
|
@ -1,3 +1,4 @@
|
||||||
/target
|
/target
|
||||||
*.profile
|
*.profile
|
||||||
*.sqlite
|
*.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.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aead"
|
name = "aead"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -219,6 +221,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
@ -287,6 +290,12 @@ version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-mac"
|
name = "crypto-mac"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -495,7 +504,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fuzzytags"
|
name = "fuzzytags"
|
||||||
version = "0.4.0"
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0621e31a90168e36ecbba544f42ec8f4078db50272abdbbe172a1af9f5ea5e28"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
|
@ -1031,11 +1042,15 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base32",
|
"base32",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"curve25519-dalek",
|
||||||
"fuzzytags",
|
"fuzzytags",
|
||||||
"hex",
|
"hex",
|
||||||
|
"rand 0.7.3",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"secretbox",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1054,14 +1069,32 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "niwl-rem"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"base32",
|
||||||
|
"bincode",
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"fuzzytags",
|
||||||
|
"hex",
|
||||||
|
"niwl",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "niwl-server"
|
name = "niwl-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
|
||||||
"fuzzytags",
|
"fuzzytags",
|
||||||
|
"niwl",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_contrib",
|
"rocket_contrib",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1619,6 +1652,12 @@ dependencies = [
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hex"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
|
@ -1665,6 +1704,16 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
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]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -1784,6 +1833,12 @@ version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483"
|
checksum = "3015a7d0a5fd5105c91c3710d42f9ccf0abfb287d62206484dcc67f9569a6483"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -1989,6 +2044,18 @@ version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
|
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]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "1.4.2"
|
version = "1.4.2"
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
[workspace]
|
[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).
|
**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
|
**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).
|
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-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
|
**niwl-client** provides a command-line application for managing secrets, tagging keys of parties and posting / querying
|
||||||
for new tags.
|
for new tags.
|
||||||
|
|
||||||
|
**niwl-rem** provides an implementation of the random ejection mixer.
|
||||||
|
|
||||||
For a more detailed overview please check out each individual crate.
|
For a more detailed overview please check out each individual crate.
|
||||||
|
|
||||||
# Example
|
# Example
|
||||||
|
@ -32,4 +116,8 @@ For a more detailed overview please check out each individual crate.
|
||||||
Tag for bob 7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da
|
Tag for bob 7e441275a5c3f88606c34c3451a44eaeaa025680cfcb3d9db53992501cc22134 4f7a7f961bc19297fee98da5f8601aa8373429b80b10c55dbe8116aa8c497a0e 71d8da
|
||||||
|
|
||||||
niwl-client bob.profile detect 10
|
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]
|
[dependencies]
|
||||||
niwl = {path="../niwl"}
|
niwl = {path="../niwl"}
|
||||||
fuzzytags = {path="../../fuzzymetatag"}
|
fuzzytags = "0.4.2"
|
||||||
clap = "3.0.0-beta.2"
|
clap = "3.0.0-beta.2"
|
||||||
serde = {version="1.0.123", features=["derive"]}
|
serde = {version="1.0.123", features=["derive"]}
|
||||||
serde_json = "1.0.61"
|
serde_json = "1.0.61"
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use clap::Clap;
|
use clap::Clap;
|
||||||
use std::fs;
|
use niwl::Profile;
|
||||||
use niwl::{Profile};
|
|
||||||
|
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
#[clap(version = "1.0", author = "Sarah Jamie Lewis <sarah@openprivacy.ca>")]
|
#[clap(version = "1.0", author = "Sarah Jamie Lewis <sarah@openprivacy.ca>")]
|
||||||
struct Opts {
|
struct Opts {
|
||||||
|
|
||||||
#[clap(default_value = "niwl.profile")]
|
#[clap(default_value = "niwl.profile")]
|
||||||
profile_filename: String,
|
profile: String,
|
||||||
|
|
||||||
#[clap(default_value = "http://localhost:8000")]
|
#[clap(default_value = "http://localhost:8000")]
|
||||||
niwl_server: String,
|
niwl_server: String,
|
||||||
|
@ -21,27 +19,27 @@ enum SubCommand {
|
||||||
Generate(Generate),
|
Generate(Generate),
|
||||||
ImportTaggingKey(ImportTaggingKey),
|
ImportTaggingKey(ImportTaggingKey),
|
||||||
TagAndSend(TagAndSend),
|
TagAndSend(TagAndSend),
|
||||||
Detect(Detect)
|
TagAndMix(TagAndMix),
|
||||||
|
Detect(Detect),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a new niwl.profile file
|
/// Generate a new niwl.profile file
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
struct Generate {
|
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
|
/// Import a friends tagging key into this profile so you can send messages to them
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
struct ImportTaggingKey {
|
struct ImportTaggingKey {
|
||||||
key: String
|
key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to a server and check for new notifications
|
/// Connect to a server and check for new notifications
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
struct Detect {
|
struct Detect {}
|
||||||
#[clap(default_value = "2")]
|
|
||||||
length: u8
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a message to a friend tagged with their niwl key
|
/// Send a message to a friend tagged with their niwl key
|
||||||
#[derive(Clap)]
|
#[derive(Clap)]
|
||||||
|
@ -52,33 +50,50 @@ struct TagAndSend {
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_profile(profile_filename: &String) -> Profile {
|
/// Send a message to a friend tagged with their niwl key
|
||||||
match fs::read_to_string(profile_filename) {
|
#[derive(Clap)]
|
||||||
Ok(json) => serde_json::from_str(json.as_str()).unwrap(),
|
struct TagAndMix {
|
||||||
Err(why) => {
|
/// the id of the mix
|
||||||
panic!("couldn't read orb.profile : {}", why);
|
mix: String,
|
||||||
}
|
/// the id of the friend e.g. "alice"
|
||||||
}
|
id: String,
|
||||||
|
/// the message you want to send.
|
||||||
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let opts: Opts = Opts::parse();
|
let opts: Opts = Opts::parse();
|
||||||
match opts.subcmd {
|
match opts.subcmd {
|
||||||
SubCommand::Generate(g) => {
|
SubCommand::Generate(g) => {
|
||||||
let profile = Profile::new(g.name.clone());
|
let profile = Profile::new(g.name.clone(), g.length);
|
||||||
let hotk = profile.human_readable_tagging_key();
|
let hotk = profile.keyset();
|
||||||
println!("Tagging Key: {}", base32::encode(base32::Alphabet::RFC4648{padding:false} ,bincode::serialize(&hotk).unwrap().as_slice()).to_ascii_lowercase());
|
println!(
|
||||||
profile.save(&opts.profile_filename);
|
"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) => {
|
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.import_tagging_key(&cmd.key);
|
||||||
profile.save(&opts.profile_filename);
|
match profile.save(&opts.profile) {
|
||||||
},
|
Err(e) => {
|
||||||
|
println!("[ERROR] {}", e)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
SubCommand::TagAndSend(cmd) => {
|
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 server = opts.niwl_server.clone();
|
||||||
let contact = cmd.id.clone();
|
let contact = cmd.id.clone();
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
@ -86,12 +101,28 @@ fn main() {
|
||||||
.build()
|
.build()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.block_on(async {
|
.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());
|
println!("{}", result.unwrap().text().await.unwrap());
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
SubCommand::Detect(cmd) => {
|
SubCommand::TagAndMix(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();
|
||||||
|
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();
|
let server = opts.niwl_server.clone();
|
||||||
tokio::runtime::Builder::new_current_thread()
|
tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
|
@ -100,15 +131,28 @@ fn main() {
|
||||||
.block_on(async {
|
.block_on(async {
|
||||||
match profile.detect_tags(server).await {
|
match profile.detect_tags(server).await {
|
||||||
Ok(detected_tags) => {
|
Ok(detected_tags) => {
|
||||||
for tag in detected_tags.detected_tags {
|
for (tag, ciphertext) in detected_tags.detected_tags.iter() {
|
||||||
println!("{}", tag);
|
match profile.private_key.decrypt(ciphertext) {
|
||||||
|
Some(message) => {
|
||||||
|
println!("message: {}", message)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
profile.update_previously_seen_tag(tag);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error: {}", 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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fuzzytags = {path="../../fuzzymetatag"}
|
niwl = {path="../niwl"}
|
||||||
|
fuzzytags = "0.4.2"
|
||||||
rocket = "0.4.6"
|
rocket = "0.4.6"
|
||||||
rocket_contrib = {version="0.4.6", features=["sqlite_pool"]}
|
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 (
|
CREATE TABLE IF NOT EXISTS tags (
|
||||||
id text primary key,
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
tag blob not null unique
|
tag BLOB NOT NULL UNIQUE,
|
||||||
)
|
message TEXT NOT NULL
|
||||||
|
)
|
|
@ -1,55 +1,131 @@
|
||||||
#![feature(proc_macro_hygiene, decl_macro)]
|
#![feature(proc_macro_hygiene, decl_macro)]
|
||||||
|
|
||||||
#[macro_use] extern crate rocket;
|
#[macro_use]
|
||||||
#[macro_use] extern crate rocket_contrib;
|
extern crate rocket;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rocket_contrib;
|
||||||
|
|
||||||
use rocket_contrib::json;
|
|
||||||
use fuzzytags::{DetectionKey, Tag};
|
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;
|
||||||
use rocket_contrib::databases::rusqlite::types::ToSql;
|
use rocket_contrib::databases::rusqlite::types::ToSql;
|
||||||
use chrono::{Utc, Duration};
|
use rocket_contrib::json;
|
||||||
use std::ops::Sub;
|
use rocket_contrib::json::{Json, JsonValue};
|
||||||
|
|
||||||
#[database("tags")]
|
#[database("tags")]
|
||||||
struct TagsDbConn(rusqlite::Connection);
|
struct TagsDbConn(rusqlite::Connection);
|
||||||
|
|
||||||
#[post("/new", format = "application/json", data = "<tag>")]
|
#[post("/new", format = "application/json", data = "<post_message_request>")]
|
||||||
fn new(conn:TagsDbConn, tag: Json<Tag<24>>) -> JsonValue {
|
fn new(conn: TagsDbConn, post_message_request: Json<PostMessageRequest>) -> JsonValue {
|
||||||
conn.0.execute(
|
match serde_json::to_string(&post_message_request.ciphertext) {
|
||||||
"INSERT INTO tags (id, tag) VALUES (strftime('%Y-%m-%d %H:%M:%S:%f', 'now'), ?1)",
|
Ok(ciphertext) => {
|
||||||
&[&tag.0.compress() as &dyn ToSql],
|
match conn.0.execute(
|
||||||
).unwrap();
|
"INSERT INTO tags (tag, message) VALUES (?1, ?2);",
|
||||||
json!({"tag" : tag.to_string()})
|
&[
|
||||||
}
|
&post_message_request.tag.compress() as &dyn ToSql,
|
||||||
|
&ciphertext,
|
||||||
#[post("/tags", format = "application/json", data = "<detection_key>")]
|
],
|
||||||
fn tags(conn:TagsDbConn, detection_key: Json<DetectionKey<24>>) -> JsonValue {
|
) {
|
||||||
|
Ok(_) => {
|
||||||
let mut stmt = conn.0.prepare(
|
json!({"tag" : post_message_request.tag.to_string()})
|
||||||
"SELECT tag FROM tags WHERE id > (?1) AND id < (?2)",
|
}
|
||||||
).unwrap();
|
Err(_) => {
|
||||||
|
json!({"tag" : "error"})
|
||||||
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);
|
json!({"tag" : "error"})
|
||||||
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})
|
#[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() {
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fuzzytags = {path="../../fuzzymetatag"}
|
fuzzytags = "0.4.2"
|
||||||
serde = {version="1.0.123", features=["derive"]}
|
serde = {version="1.0.123", features=["derive"]}
|
||||||
serde_json = "1.0.61"
|
serde_json = "1.0.61"
|
||||||
bincode = "1.3.1"
|
bincode = "1.3.1"
|
||||||
hex = "0.4.2"
|
hex = "0.4.2"
|
||||||
base32 = "0.4.0"
|
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)]
|
#![feature(into_future)]
|
||||||
use fuzzytags::{RootSecret, TaggingKey, Tag};
|
use crate::encrypt::{PrivateKey, PublicKey, TaggedCiphertext};
|
||||||
use std::fs::File;
|
use fuzzytags::{DetectionKey, RootSecret, Tag, TaggingKey};
|
||||||
use std::io::Write;
|
use reqwest::{Error, Response};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use reqwest::{Response, Error};
|
use std::fs;
|
||||||
use std::future::{Future, IntoFuture};
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
pub mod encrypt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum NiwlError {
|
pub enum NiwlError {
|
||||||
NoKnownContactError(String),
|
NoKnownContactError(String),
|
||||||
RemoteServerError(String)
|
RemoteServerError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize,Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
root_secret: RootSecret<24>,
|
pub root_secret: RootSecret<24>,
|
||||||
tagging_keys: HashMap<String, TaggingKey<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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct HumanOrientedTaggingKey {
|
pub struct KeySet {
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
tagging_key: TaggingKey<24>,
|
tagging_key: TaggingKey<24>,
|
||||||
|
public_key: PublicKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DetectedTags {
|
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 {
|
impl Profile {
|
||||||
pub fn new(profile_name: String) -> Profile {
|
pub fn get_profile(profile_filename: &String) -> Profile {
|
||||||
let root_secret = RootSecret::<24>::generate();
|
match fs::read_to_string(profile_filename) {
|
||||||
Profile {
|
Ok(json) => serde_json::from_str(json.as_str()).unwrap(),
|
||||||
profile_name,
|
Err(why) => {
|
||||||
root_secret,
|
panic!("couldn't read orb.profile : {}", why);
|
||||||
tagging_keys: Default::default()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
let tagging_key = self.root_secret.tagging_key();
|
||||||
HumanOrientedTaggingKey {
|
let public_key = self.private_key.public_key();
|
||||||
|
KeySet {
|
||||||
profile_name: self.profile_name.clone(),
|
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> {
|
pub fn generate_tag(&self, id: &String) -> Result<Tag<24>, NiwlError> {
|
||||||
if self.tagging_keys.contains_key(id) {
|
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());
|
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) {
|
pub fn import_tagging_key(&mut self, key: &String) {
|
||||||
match base32::decode(base32::Alphabet::RFC4648 { padding: false }, key.as_str()) {
|
match base32::decode(base32::Alphabet::RFC4648 { padding: false }, key.as_str()) {
|
||||||
Some(data) => {
|
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 {
|
match tagging_key_result {
|
||||||
Ok(hotk) => {
|
Ok(hotk) => {
|
||||||
println!("Got: {}: {}", hotk.profile_name, hotk.tagging_key.id());
|
println!("Got: {}: {}", hotk.profile_name, hotk.tagging_key.id());
|
||||||
if self.tagging_keys.contains_key(&hotk.profile_name) == false {
|
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 {
|
} else {
|
||||||
println!("There is already an entry for {}", hotk.profile_name)
|
println!("There is already an entry for {}", hotk.profile_name)
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("Error: {}", err.to_string());
|
println!("Error: {}", err.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
println!("Error Reporting Tagging Key")
|
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(
|
||||||
let client = reqwest::Client::new();
|
&self,
|
||||||
|
server: String,
|
||||||
|
mix: String,
|
||||||
|
contact: String,
|
||||||
|
message: &String,
|
||||||
|
) -> Result<Response, NiwlError> {
|
||||||
match self.generate_tag(&contact) {
|
match self.generate_tag(&contact) {
|
||||||
Ok(tag) => {
|
Ok(tag) => {
|
||||||
let result = client.
|
let ciphertext = self.tagging_keys[&contact].1.encrypt(&tag, message);
|
||||||
post(&String::from(server + "/new"))
|
let ciphertext_json = serde_json::to_string(&ciphertext).unwrap();
|
||||||
.json(&tag)
|
return self.tag_and_send(&server, mix, &ciphertext_json).await;
|
||||||
.send().await;
|
|
||||||
match result {
|
|
||||||
Ok(response) => Ok(response),
|
|
||||||
Err(err) => Err(NiwlError::RemoteServerError(err.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
Err(err)
|
|
||||||
}
|
}
|
||||||
|
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 client = reqwest::Client::new();
|
||||||
let detection_key = self.root_secret.extract_detection_key(1);
|
let tag = self.root_secret.tagging_key().generate_tag();
|
||||||
let result = client.post(&String::from(server + "/tags"))
|
let ciphertext = self.private_key.public_key().encrypt(&tag, message);
|
||||||
.json(&detection_key)
|
|
||||||
.send().await;
|
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
|
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