From af21afb2976d27b54f5967bca15c3711b324b12a Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Sun, 31 Jan 2021 23:50:59 -0800 Subject: [PATCH] initial commit --- .gitignore | 3 ++ Cargo.toml | 15 +++++++++ README.md | 29 +++++++++++++++++ rustfmt.toml | 66 +++++++++++++++++++++++++++++++++++++++ src/main.rs | 72 ++++++++++++++++++++++++++++++++++++++++++ src/parties.rs | 46 +++++++++++++++++++++++++++ src/server.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 316 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 rustfmt.toml create mode 100644 src/main.rs create mode 100644 src/parties.rs create mode 100644 src/server.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e04901 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.idea/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..71d4ee7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fuzzytags-sim" +version = "0.1.0" +authors = ["Sarah Jamie Lewis "] +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" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2fbf5e --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b355306 --- /dev/null +++ b/rustfmt.toml @@ -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 diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1628850 --- /dev/null +++ b/src/main.rs @@ -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 ")] +struct Opts { + /// Sets a custom config file. Could have been an Option 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(); + } +} diff --git a/src/parties.rs b/src/parties.rs new file mode 100644 index 0000000..6c24e93 --- /dev/null +++ b/src/parties.rs @@ -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, +} + +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(&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(&self, server: &mut SimulatedServer, rng: &mut R, distribution: D) + where + D: Distribution, + 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); + } + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..75dac9b --- /dev/null +++ b/src/server.rs @@ -0,0 +1,85 @@ +use fuzzytags::{FuzzyDetectionKey, FuzzyPublicKey, FuzzyTag}; +use hashbrown::HashMap; + +pub struct SimulatedServer { + keybase: Vec<(FuzzyDetectionKey, FuzzyPublicKey)>, + messages: Vec, + tags_to_keys_cache: HashMap>, + keys_to_tags_cache: HashMap>, +} + +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 { + 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 + } +}