initial commit

This commit is contained in:
Sarah Jamie Lewis 2021-01-31 23:50:59 -08:00
commit af21afb297
7 changed files with 316 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
Cargo.lock
.idea/

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "fuzzytags-sim"
version = "0.1.0"
authors = ["Sarah Jamie Lewis <sarah@openprivacy.ca>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
fuzzytags = {path="../../fuzzymetatag/"}
rand = "0.8.3"
rand_distr = "0.4.0"
hashbrown = "0.9.1"
termcolor = "1.1.2"
clap = "3.0.0-beta.2"

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# fuzzytags-sim
A playground simulator for [fuzzytags](https://crates.io/crates/fuzzytags) designed to demonstrate the impact of
integrating fuzzytags into an application with poor parameter choices (and aid in choosing good parameters).
## Setup
Run `cargo build --release`, then the binary will be at `./target/release/fuzzytags-sim`
By default, the simulator will run a single-round experiment with 10 parties (`--num-parties`). Messages for parties will
be drawn randomly from a pareto distribution (scale: 1.0, shape: 1.0) i.e. a few parties will receive
many messages, the rest will receive a small number (or zero). This can be adjusted by the `--samples-per-round`
argument.
Once all the parties and messages have been generated, the server will test all messages against all parties.
Finally, the server will produce statistics for the round, including the ideal false positive rate for each party,
the observed match rate for each party, the skew between the ideal false positive rate and the observed rate, and the
number of trivial attributions it has made to that party in this round.
Note: A *trivial attribution* corresponds to the server receiving a tag which only matches a single party.
## Future Extensions
- Multiple Rounds / Aggregate Statistics
- Differential Attacks e.g. leak that a set of messages all belong to the same party and have the server guess which party
they belong to.
- Simulate poisson distribution arrival (and bidirectional conversation) and allow the server to make timing correlations to
aid in breaking anonymity

66
rustfmt.toml Normal file
View File

@ -0,0 +1,66 @@
max_width = 200
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"
use_small_heuristics = "Default"
indent_style = "Block"
wrap_comments = false
format_code_in_doc_comments = false
comment_width = 80
normalize_comments = false
normalize_doc_attributes = false
license_template_path = ""
format_strings = false
format_macro_matchers = false
format_macro_bodies = true
empty_item_single_line = true
struct_lit_single_line = true
fn_single_line = false
where_single_line = false
imports_indent = "Block"
imports_layout = "Mixed"
merge_imports = false
reorder_imports = true
reorder_modules = true
reorder_impl_items = false
type_punctuation_density = "Wide"
space_before_colon = false
space_after_colon = true
spaces_around_ranges = false
binop_separator = "Front"
remove_nested_parens = true
combine_control_expr = true
overflow_delimited_expr = false
struct_field_align_threshold = 0
enum_discrim_align_threshold = 0
match_arm_blocks = true
force_multiline_blocks = false
fn_args_layout = "Tall"
brace_style = "SameLineWhere"
control_brace_style = "AlwaysSameLine"
trailing_semicolon = true
trailing_comma = "Vertical"
match_block_trailing_comma = false
blank_lines_upper_bound = 1
blank_lines_lower_bound = 0
edition = "2015"
version = "One"
inline_attribute_width = 0
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
condense_wildcard_suffixes = false
color = "Auto"
required_version = "1.4.21"
unstable_features = false
disable_all_formatting = false
skip_children = false
hide_parse_errors = false
error_on_line_overflow = false
error_on_unformatted = false
report_todo = "Never"
report_fixme = "Never"
ignore = []
emit_mode = "Files"
make_backup = false

72
src/main.rs Normal file
View File

@ -0,0 +1,72 @@
use crate::parties::SimulatedParties;
use crate::server::SimulatedServer;
use rand_distr::Pareto;
use std::io::Write;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
mod parties;
mod server;
use clap::Clap;
#[derive(Clap)]
#[clap(version = "1.0", author = "Sarah Jamie Lewis <sarah@opneprivacy.ca>")]
struct Opts {
/// Sets a custom config file. Could have been an Option<T> with no default too
#[clap(short, long, default_value = "24")]
gamma: usize,
/// the number of parties to simulate
#[clap(short, long, default_value = "10")]
num_parties: usize,
/// samples per round
#[clap(short, long, default_value = "10")]
samples_per_round: usize,
/// minimum false positive rate
#[clap(short, long, default_value = "1")]
min_p: usize,
/// maximum false positive rate
#[clap(short, long, default_value = "8")]
max_p: usize,
}
fn main() {
let opts: Opts = Opts::parse();
let mut rng = rand::thread_rng();
let mut server = SimulatedServer::new();
println!("Generating {} Parties...", opts.num_parties);
let simulated_parties = SimulatedParties::new_simulation(opts.num_parties, opts.gamma);
simulated_parties.register_with_server(&mut server, &mut rng, opts.min_p, opts.max_p);
let pareto = Pareto::new(1.0, 1.0).unwrap();
println!("Simulating message sends using {} samples from a pareto distribution...", opts.samples_per_round);
(0..opts.samples_per_round).for_each(|_i| simulated_parties.sample_traffic(&mut server, &mut rng, pareto));
println!("Simulating Adversarial Server Processing Messages..");
server.test_messages();
let round_statistics = server.statistics();
let mut stdout = StandardStream::stdout(ColorChoice::Always);
for (party, stats) in round_statistics.iter() {
if stats.trivial_breaks > 0 {
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red))).unwrap();
} else {
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green))).unwrap();
}
writeln!(
&mut stdout,
"Party {} | Ideal: {} | Observed: {} ({:.2}) | Skew: {} ({:.2}) | Trivial Attributions this Round: {}",
party,
stats.ideal_rate,
stats.observed_messages,
100.0 * stats.observed_rate,
stats.observed_skew_messages,
stats.observed_skew,
stats.trivial_breaks
)
.unwrap();
}
}

46
src/parties.rs Normal file
View File

@ -0,0 +1,46 @@
use crate::server::SimulatedServer;
use fuzzytags::FuzzyTagKeyPair;
use rand::distributions::Distribution;
use rand::Rng;
use rand_distr::num_traits::ToPrimitive;
pub struct SimulatedParties {
parties: Vec<FuzzyTagKeyPair>,
}
impl SimulatedParties {
pub fn new_simulation(num_parties: usize, gamma: usize) -> SimulatedParties {
let mut parties = vec![];
for _p in 0..num_parties {
let key = FuzzyTagKeyPair::generate(gamma);
parties.push(key);
}
SimulatedParties { parties }
}
pub fn register_with_server<R>(&self, server: &mut SimulatedServer, rng: &mut R, min_p: usize, max_p: usize)
where
R: Rng,
{
for party in self.parties.iter() {
let n = rng.gen_range(min_p..max_p);
let detection_key = party.secret_key.extract(n);
server.register_key(&detection_key, &party.public_key);
}
}
pub fn sample_traffic<R, D>(&self, server: &mut SimulatedServer, rng: &mut R, distribution: D)
where
D: Distribution<f64>,
R: Rng,
{
let v = distribution.sample(rng).to_u16().unwrap();
let receiver = rng.gen_range(0..self.parties.len());
println!("[Oracle] {} received {} messages", self.parties.get(receiver).unwrap().public_key.id(), v);
for _i in 0..v {
let tag = self.parties.get(receiver).unwrap().public_key.generate_tag();
server.add_message(tag);
//message_oracle.push(receiver);
}
}
}

85
src/server.rs Normal file
View File

@ -0,0 +1,85 @@
use fuzzytags::{FuzzyDetectionKey, FuzzyPublicKey, FuzzyTag};
use hashbrown::HashMap;
pub struct SimulatedServer {
keybase: Vec<(FuzzyDetectionKey, FuzzyPublicKey)>,
messages: Vec<FuzzyTag>,
tags_to_keys_cache: HashMap<String, Vec<FuzzyDetectionKey>>,
keys_to_tags_cache: HashMap<String, Vec<FuzzyTag>>,
}
pub struct PartyStatistics {
pub ideal_rate: f64,
pub expected_messages: f64,
pub observed_messages: usize,
pub observed_rate: f64,
pub observed_skew_messages: f64,
pub observed_skew: f64,
pub trivial_breaks: usize,
}
impl SimulatedServer {
pub fn new() -> SimulatedServer {
SimulatedServer {
keybase: vec![],
messages: vec![],
tags_to_keys_cache: HashMap::new(),
keys_to_tags_cache: HashMap::new(),
}
}
pub fn register_key(&mut self, detection_key: &FuzzyDetectionKey, public_key: &FuzzyPublicKey) {
self.keybase.push((detection_key.clone(), public_key.clone()));
self.keys_to_tags_cache.insert(detection_key.id(), vec![]);
}
pub fn add_message(&mut self, tag: FuzzyTag) {
self.messages.push(tag.clone());
self.tags_to_keys_cache.insert(tag.to_string(), vec![]);
}
pub fn test_messages(&mut self) {
for message in self.messages.iter() {
for (detection_key, _) in self.keybase.iter() {
if detection_key.test_tag(message) {
self.tags_to_keys_cache.get_mut(message.to_string().as_str()).unwrap().push((*detection_key).clone());
self.keys_to_tags_cache.get_mut(detection_key.id().as_str()).unwrap().push((*message).clone());
}
}
}
}
pub fn statistics(&self) -> HashMap<String, PartyStatistics> {
let mut stats = HashMap::new();
for (party, pub_key) in self.keybase.iter() {
let matched = self.keys_to_tags_cache[party.id().as_str()].clone();
let observed_messages = matched.len();
let ideal_rate = party.false_positive_probability();
let observed_rate = (observed_messages as f64) / (self.messages.len() as f64);
let expected_messages = ideal_rate * (self.messages.len() as f64);
let observed_skew_messages = expected_messages - (observed_messages as f64);
let observed_skew = observed_rate / ideal_rate;
let mut trivial_breaks = 0;
for tag in matched.iter() {
if self.tags_to_keys_cache[tag.to_string().as_str()].len() == 1 {
trivial_breaks += 1;
}
}
stats.insert(
pub_key.id(),
PartyStatistics {
ideal_rate,
expected_messages,
observed_messages,
observed_rate,
observed_skew_messages,
observed_skew,
trivial_breaks,
},
);
}
stats
}
}