initial commit
This commit is contained in:
commit
af21afb297
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
.idea/
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue