diff --git a/Cargo.toml b/Cargo.toml index 12e7742..fa24354 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,16 +8,27 @@ edition = "2021" [dependencies] array-bytes = "6.2.0" bincode = "1" +chrono = "0.4" +clap = { version = "4.4.14", features = ["derive"] } curve25519-dalek = { version = "4", default-features = false, features = ["serde", "rand_core", "digest"] } ed25519-dalek = { version = "2", features = ["serde", "rand_core"] } +http-body-util = "0.1" +hyper = { version = "1", features = ["full"] } +hyper-rustls = "0.26.0" +hyper-util = { version = "0.1", features = ["full"] } +julianday = "1.2.0" lazy_static = "1" lox-library = { git = "https://gitlab.torproject.org/vecna/lox.git", version = "0.1.0" } +#scraper = "0.18" +select = "0.6.0" serde = "1.0.195" serde_json = "1.0" serde_with = {version = "3.4.0", features = ["json"]} sha1 = "0.10" sha3 = "0.10" +sled = "0.34.7" time = "0.3.30" +tokio = { version = "1", features = ["full"] } # probably not needed once I can query an API rand = { version = "0.8", features = ["std_rng"]} diff --git a/src/extra_info.rs b/src/extra_info.rs new file mode 100644 index 0000000..00604b0 --- /dev/null +++ b/src/extra_info.rs @@ -0,0 +1,195 @@ +/*! Fields we need from the extra-info documents for bridges... +Note, this is NOT a complete implementation of the document format. +(https://spec.torproject.org/dir-spec/extra-info-document-format.html) */ + +use chrono::DateTime; +use http_body_util::{BodyExt, Empty}; +use hyper::body::Bytes; +use hyper_util::{client::legacy::Client, rt::TokioExecutor}; +use julianday::JulianDay; +use select::{document::Document, predicate::Name}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + fs::File, + io::{prelude::*, BufReader, Write}, + path::Path, +}; + +/// Directory where we store these files +pub const DIRECTORY: &str = "extra_infos"; + +/// Fields we need from extra-info document +#[derive(Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct ExtraInfo { + /// Bridge nickname, probably unused + pub nickname: String, + /// Bridge fingerprint, a SHA-1 hash of the bridge ID + pub fingerprint: [u8; 20], + /// Date (in UTC) that this document was published, stored as a Julian + /// date because we don't need to know more precisely than the day. + pub published: u32, + /// Map of country codes and how many users (rounded up to a multiple of + /// 8) have connected to that bridge during the day. + /// Uses BTreeMap instead of HashMap so ExtraInfo can implement Hash. + pub bridge_ips: BTreeMap, // TODO: What size for count? +} + +fn get_extra_info_or_error(entry: &HashMap) -> Result { + if !entry.contains_key("nickname") || !entry.contains_key("fingerprint") { + // How did we get here?? + return Err("Cannot parse extra-info: Missing nickname or fingerprint".to_string()); + } + if !entry.contains_key("published") || !entry.contains_key("bridge-ips") { + // Some extra-infos are missing data on connecting IPs... + // But we can't do anything in that case. + return Err(format!( + "Failed to parse extra-info for {} {}", + entry.get("nickname").unwrap(), + entry.get("fingerprint").unwrap() + )); + } + let nickname = entry.get("nickname").unwrap().to_string(); + let fingerprint_str = entry.get("fingerprint").unwrap(); + if fingerprint_str.len() != 40 { + return Err("Fingerprint must be 20 bytes".to_string()); + } + let fingerprint = array_bytes::hex2array(fingerprint_str).unwrap(); + let published: u32 = JulianDay::from( + DateTime::parse_from_str( + &(entry.get("published").unwrap().to_owned() + " +0000"), + "%F %T %z", + ) + .unwrap() + .date_naive(), + ) + .inner() + .try_into() + .unwrap(); + let bridge_ips_str = entry.get("bridge-ips").unwrap(); + let mut bridge_ips: BTreeMap = BTreeMap::new(); + let countries: Vec<&str> = bridge_ips_str.split(',').collect(); + for country in countries { + if country != "" { + // bridge-ips may be empty + let (cc, count) = country.split_once('=').unwrap(); + bridge_ips.insert(cc.to_string(), count.parse::().unwrap()); + } + } + + Ok(ExtraInfo { + nickname, + fingerprint, + published, + bridge_ips, + }) +} + +pub fn add_extra_infos<'a>(filename: &str, set: &mut HashSet) { + let infile = File::open(format!("{}/{}", DIRECTORY, filename)).unwrap(); + let reader = BufReader::new(infile); + + let mut entry = HashMap::::new(); + for line in reader.lines() { + let line = line.unwrap(); + if line.starts_with("@type bridge-extra-info ") { + if !entry.is_empty() { + let extra_info = get_extra_info_or_error(&entry); + if extra_info.is_ok() { + set.insert(extra_info.unwrap()); + } else { + // Just print the error and continue. + println!("{}", extra_info.err().unwrap()); + } + entry = HashMap::::new(); + } + } else { + if line.starts_with("extra-info ") { + // extra-info line has format: + // extra-info + let line_split: Vec<&str> = line.split(' ').collect(); + if line_split.len() != 3 { + println!("Misformed extra-info line"); + } else { + entry.insert("nickname".to_string(), line_split[1].to_string()); + entry.insert("fingerprint".to_string(), line_split[2].to_string()); + } + } else { + let (key, value) = match line.split_once(' ') { + Some((k, v)) => (k, v), + None => (line.as_str(), ""), + }; + entry.insert(key.to_string(), value.to_string()); + } + } + } + // Do for the last one + let extra_info = get_extra_info_or_error(&entry); + if extra_info.is_ok() { + set.insert(extra_info.unwrap()); + } else { + println!("{}", extra_info.err().unwrap()); + } +} + +/// Download new extra-infos files and save them in DIRECTORY. This function +/// returns the set of newly downloaded filenames. +pub async fn download_extra_infos( +) -> Result, Box> { + // Download directory of recent extra-infos + let base_url = "https://collector.torproject.org/recent/bridge-descriptors/extra-infos/"; + let url = base_url.parse().unwrap(); + let https = hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() // TODO: Pin certificate? Is this data signed/verifiable? + .expect("no native root CA certificates found") + .https_only() + .enable_http1() + .build(); + + let client: Client<_, Empty> = Client::builder(TokioExecutor::new()).build(https); + + println!("Downloading {}", base_url); + let mut res = client.get(url).await?; + + assert_eq!(res.status(), hyper::StatusCode::OK); + let mut body_str = String::from(""); + while let Some(next) = res.frame().await { + let frame = next?; + if let Some(chunk) = frame.data_ref() { + body_str.push_str(&String::from_utf8(chunk.to_vec())?); + } + } + + let doc = Document::from(body_str.as_str()); + + // Create extra-infos directory if it doesn't exist + std::fs::create_dir_all(&DIRECTORY)?; + + let mut new_files = HashSet::::new(); + + // Go through all the links in the page and download new files + let links = doc.find(Name("a")).filter_map(|n| n.attr("href")); + for link in links { + if link.ends_with("-extra-infos") { + let filename = format!("{}/{}", DIRECTORY, link); + + // Download file if it's not already downloaded + if !Path::new(&filename).exists() { + let extra_infos_url = format!("{}{}", base_url, link); + println!("Downloading {}", extra_infos_url); + let mut res = client.get(extra_infos_url.parse().unwrap()).await?; + assert_eq!(res.status(), hyper::StatusCode::OK); + let mut file = std::fs::File::create(filename).unwrap(); + while let Some(next) = res.frame().await { + let frame = next?; + if let Some(chunk) = frame.data_ref() { + file.write_all(&chunk)?; + } + } + new_files.insert(link.to_string()); + } + } + } + + Ok(new_files) +} diff --git a/src/lib.rs b/src/lib.rs index 0c14a5d..c4f028d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,32 +1,53 @@ -use curve25519_dalek::scalar::Scalar; -use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; use lazy_static::lazy_static; -use lox_library::bridge_table::{BridgeLine, MAX_BRIDGES_PER_BUCKET}; -use lox_library::cred::Lox; -use lox_library::proto::positive_report; -use lox_library::IssuerPubKey; -use serde::de::{self, Deserializer, MapAccess, SeqAccess, Unexpected, Visitor}; use serde::{Deserialize, Serialize}; -use sha1::{Digest, Sha1}; -use sha3::Sha3_256; -use std::collections::HashSet; -use std::fmt; -use std::option::Option; +use sled::Db; +use std::{ + collections::{BTreeMap, HashMap, HashSet}, + fmt, + fs::File, + io::BufReader, +}; -// for generating ed25519 keys during initial development -use rand::rngs::OsRng; +pub mod extra_info; +pub mod negative_report; +pub mod positive_report; -// TODO: These should be loaded from config file -pub const REQUIRE_BRIDGE_TOKEN: bool = false; +use extra_info::*; +use negative_report::*; +use positive_report::*; -lazy_static! { - // known country codes, TODO: Verify these are correct - pub static ref COUNTRY_CODES: HashSet<&'static str> = HashSet::from(["ac","af","ax","al","dz","ad","ao","ai","aq","ag","ar","am","aw","au","at","az","bs","bh","bd","bb","by","be","bz","bj","bm","bt","bo","ba","bw","bv","br","io","vg","bn","bg","bf","bi","kh","cm","ca","cv","ky","cf","td","cl","cn","cx","cc","co","km","cg","cd","ck","cr","ci","hr","cu","cy","cz","dk","dj","dm","do","tp","ec","eg","sv","gq","ee","et","fk","fo","fj","fi","fr","fx","gf","pf","tf","ga","gm","ge","de","gh","gi","gr","gl","gd","gp","gu","gt","gn","gw","gy","ht","hm","hn","hk","hu","is","in","id","ir","iq","ie","im","il","it","jm","jp","jo","kz","ke","ki","kp","kr","kw","kg","la","lv","lb","ls","lr","ly","li","lt","lu","mo","mk","mg","mw","my","mv","ml","mt","mh","mq","mr","mu","yt","mx","fm","md","mc","mn","me","ms","ma","mz","mm","na","nr","np","an","nl","nc","nz","ni","ne","ng","nu","nf","mp","no","om","pk","pw","ps","pa","pg","py","pe","ph","pn","pl","pt","pr","qa","re","ro","ru","rw","ws","sm","st","sa","uk","sn","rs","sc","sl","sg","sk","si","sb","so","as","za","gs","su","es","lk","sh","kn","lc","pm","vc","sd","sr","sj","sz","se","ch","sy","tw","tj","tz","th","tg","tk","to","tt","tn","tr","tm","tc","tv","ug","ua","ae","gb","uk","us","um","uy","uz","vu","va","ve","vn","vi","wf","eh","ye","zm","zw"]); +#[derive(Debug, Deserialize)] +pub struct Config { + pub db: DbConfig, + require_bridge_token: bool, } -/// The minimum trust level a Lox credential must have to be allowed to -/// submit a positive report -pub const PR_MIN_TRUST_LEVEL: u32 = 3; +#[derive(Debug, Deserialize)] +pub struct DbConfig { + // The path for the server database, default is "server_db" + pub db_path: String, +} + +impl Default for DbConfig { + fn default() -> DbConfig { + DbConfig { + db_path: "server_db".to_owned(), + } + } +} + +lazy_static! { + // known country codes based on Tor geoIP database + // Produced with `cat /usr/share/tor/geoip{,6} | grep -v ^# | grep -o ..$ | sort | uniq | tr '[:upper:]' '[:lower:]' | tr '\n' ',' | sed 's/,/","/g'` + pub static ref COUNTRY_CODES: HashSet<&'static str> = HashSet::from(["??","ad","ae","af","ag","ai","al","am","ao","ap","aq","ar","as","at","au","aw","ax","az","ba","bb","bd","be","bf","bg","bh","bi","bj","bl","bm","bn","bo","bq","br","bs","bt","bv","bw","by","bz","ca","cc","cd","cf","cg","ch","ci","ck","cl","cm","cn","co","cr","cs","cu","cv","cw","cx","cy","cz","de","dj","dk","dm","do","dz","ec","ee","eg","eh","er","es","et","eu","fi","fj","fk","fm","fo","fr","ga","gb","gd","ge","gf","gg","gh","gi","gl","gm","gn","gp","gq","gr","gs","gt","gu","gw","gy","hk","hm","hn","hr","ht","hu","id","ie","il","im","in","io","iq","ir","is","it","je","jm","jo","jp","ke","kg","kh","ki","km","kn","kp","kr","kw","ky","kz","la","lb","lc","li","lk","lr","ls","lt","lu","lv","ly","ma","mc","md","me","mf","mg","mh","mk","ml","mm","mn","mo","mp","mq","mr","ms","mt","mu","mv","mw","mx","my","mz","na","nc","ne","nf","ng","ni","nl","no","np","nr","nu","nz","om","pa","pe","pf","pg","ph","pk","pl","pm","pn","pr","ps","pt","pw","py","qa","re","ro","rs","ru","rw","sa","sb","sc","sd","se","sg","sh","si","sj","sk","sl","sm","sn","so","sr","ss","st","sv","sx","sy","sz","tc","td","tf","tg","th","tj","tk","tl","tm","tn","to","tr","tt","tv","tw","tz","ua","ug","um","us","uy","uz","va","vc","ve","vg","vi","vn","vu","wf","ws","ye","yt","za","zm","zw"]); + + // read config data at run time + pub static ref CONFIG: Config = serde_json::from_reader( + BufReader::new( + File::open("config.json").expect("Could not read config file") // TODO: Make config filename configurable + ) + ).expect("Reading config file from JSON failed"); +} /// Get Julian date pub fn get_date() -> u32 { @@ -37,606 +58,147 @@ pub fn get_date() -> u32 { .unwrap() } -/// Get bridge line for a bridge, requires some oracle -pub fn get_bridge_line(fingerprint: &[u8; 20]) -> BridgeLine { - // TODO - // for now just return empty bridgeline - BridgeLine::default() -} - -/// Get verifying key for a bridge, requires some oracle -pub fn get_bridge_signing_pubkey(fingerprint: &[u8; 20]) -> VerifyingKey { - // TODO - // for now just return new pubkey - let mut csprng = OsRng {}; - let keypair = SigningKey::generate(&mut csprng); - keypair.verifying_key() -} - -/// Get bucket from hash of bucket ID, requires some oracle -pub fn get_bucket(beta_hash: &[u8; 32]) -> [BridgeLine; MAX_BRIDGES_PER_BUCKET] { - // TODO - // for now just return bucket of empty bridgelines - [ - BridgeLine::default(), - BridgeLine::default(), - BridgeLine::default(), - ] -} - -/// Proof that the user knows (and should be able to access) a given bridge +/// All the info for a bridge, to be stored in the database #[derive(Serialize, Deserialize)] -pub enum ProofOfBridgeKnowledge { - /// Hash of bridge line as proof of knowledge of bridge line - HashOfBridgeLine(HashOfBridgeLine), - /// Hash of bucket ID for Lox user - HashOfBucket(HashOfBucket), -} - -impl ProofOfBridgeKnowledge { - pub fn verify(&self, bridge_fingerprint: [u8; 20]) -> bool { - // TODO: It seems like there ought to be a cleaner way to do this? - match self { - ProofOfBridgeKnowledge::HashOfBridgeLine(bl_hash) => bl_hash.verify(bridge_fingerprint), - ProofOfBridgeKnowledge::HashOfBucket(bucket_hash) => { - bucket_hash.verify(bridge_fingerprint) - } - } - } -} - -/// Hash of bridge line to prove knowledge of that bridge -#[derive(PartialEq, Serialize, Deserialize)] -pub struct HashOfBridgeLine { - hash: [u8; 32], -} - -impl HashOfBridgeLine { - pub fn new(bl: BridgeLine) -> Self { - let mut hasher = Sha3_256::new(); - hasher.update(bincode::serialize(&bl).unwrap()); - let hash: [u8; 32] = hasher.finalize().into(); - Self { hash } - } - - pub fn verify(&self, bridge_fingerprint: [u8; 20]) -> bool { - let bl = get_bridge_line(&bridge_fingerprint); - self == &HashOfBridgeLine::new(bl) - } -} - -/// Hash of bucket ID to prove knowledge of bridges in that bucket -#[derive(PartialEq, Serialize, Deserialize)] -pub struct HashOfBucket { - hash: [u8; 32], -} - -impl HashOfBucket { - pub fn new(bucket: Scalar) -> Self { - let mut hasher = Sha3_256::new(); - hasher.update(bucket.to_bytes()); - let hash: [u8; 32] = hasher.finalize().into(); - Self { hash } - } - - pub fn verify(&self, bridge_fingerprint: [u8; 20]) -> bool { - let bucket = get_bucket(&self.hash); - for bl in bucket { - let mut hasher = Sha1::new(); - hasher.update(bl.uid_fingerprint.to_le_bytes()); - if bridge_fingerprint == <[u8; 20]>::from(hasher.finalize()) { - return true; - } - } - false - } -} - -/// Reports from users about whether or not their bridges are blocked -#[derive(Serialize, Deserialize)] -pub enum Report { - /// Negative report indicating user was unable to connect - NegativeReport(NegativeReport), - /// Positive report indicating user was able to connect - PositiveReport(PositiveReport), -} - -impl Report { - fn verify(&self) -> bool { - match self { - Report::NegativeReport(report) => report.verify(), - Report::PositiveReport(report) => report.verify(), - } - } -} - -/// A report that the user was unable to connect to the bridge -#[derive(Serialize)] -pub struct NegativeReport { +pub struct BridgeInfo { /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) pub fingerprint: [u8; 20], - /// some way to prove knowledge of bridge - bridge_pok: ProofOfBridgeKnowledge, - /// user's country code, may be an empty string - pub country: String, - /// today's Julian date - pub date: u32, + /// nickname of bridge (probably not necessary) + pub nickname: String, + /// flag indicating whether the bridge is believed to be blocked + pub is_blocked: bool, + /// map of dates to data for that day + pub info_by_day: HashMap, } -// Ensure public fields are legal while deserializing -// Based on https://serde.rs/deserialize-struct.html -impl<'de> Deserialize<'de> for NegativeReport { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - enum Field { - Fingerprint, - BridgePOK, - Country, - Date, +impl BridgeInfo { + pub fn new(fingerprint: [u8; 20], nickname: String) -> Self { + Self { + fingerprint: fingerprint, + nickname: nickname, + is_blocked: false, + info_by_day: HashMap::::new(), } + } +} - impl<'de> Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct FieldVisitor; - - impl<'de> Visitor<'de> for FieldVisitor { - type Value = Field; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("`fingerprint`, `bridge_pok`, `country`, or `date`") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - match value { - "fingerprint" => Ok(Field::Fingerprint), - "bridge_pok" => Ok(Field::BridgePOK), - "country" => Ok(Field::Country), - "date" => Ok(Field::Date), - _ => Err(de::Error::unknown_field(value, FIELDS)), - } - } - } - - deserializer.deserialize_identifier(FieldVisitor) +impl fmt::Display for BridgeInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut str = format!( + "fingerprint:{}\n", + array_bytes::bytes2hex("", self.fingerprint).as_str() + ); + str.push_str(format!("nickname: {}\n", self.nickname).as_str()); + str.push_str(format!("is_blocked: {}\n", self.is_blocked).as_str()); + str.push_str("info_by_day:"); + for day in self.info_by_day.keys() { + str.push_str(format!("\n day: {}", day).as_str()); + let daily_info = self.info_by_day.get(day).unwrap(); + for line in daily_info.to_string().lines() { + str.push_str(format!("\n {}", line).as_str()); } } + write!(f, "{}", str) + } +} - struct NegativeReportVisitor; +// TODO: Should this be an enum to make it easier to implement different +// versions for plugins? - impl<'de> de::Visitor<'de> for NegativeReportVisitor { - type Value = NegativeReport; +/// Information about bridge reachability, gathered daily +#[derive(Serialize, Deserialize)] +pub struct DailyBridgeInfo { + /// Map of country codes and how many users (rounded up to a multiple of + /// 8) have connected to that bridge during the day. + pub bridge_ips: BTreeMap, + /// Set of negative reports received during this day + pub negative_reports: Vec, + /// Set of positive reports received during this day + pub positive_reports: Vec, + // We don't care about ordering of the reports, but I'm using vectors for + // reports because we don't want a set to deduplicate our reports, and + // I don't want to implement Hash or Ord. Another possibility might be a + // map of the report to the number of that exact report we received. + // Positive reports include a Lox proof and should be unique, but negative + // reports could be deduplicated. +} - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "negative report with valid country code and date no later than today", - ) - } +impl DailyBridgeInfo { + pub fn new() -> Self { + Self { + bridge_ips: BTreeMap::::new(), + negative_reports: Vec::::new(), + positive_reports: Vec::::new(), + } + } +} - fn visit_seq(self, mut seq: V) -> Result - where - V: SeqAccess<'de>, - { - let fingerprint = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(0, &self))?; - let bridge_pok = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(1, &self))?; - let country = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(2, &self))?; - let date = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(3, &self))?; - Ok(NegativeReport { - fingerprint, - bridge_pok, +impl fmt::Display for DailyBridgeInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut str = String::from("bridge_ips:"); + for country in self.bridge_ips.keys() { + str.push_str( + format!( + "\n cc: {}, connections: {}", country, - date, - }) - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut fingerprint = None; - let mut bridge_pok = None; - let mut country: Option = None; - let mut date = None; - while let Some(key) = map.next_key()? { - match key { - Field::Fingerprint => { - if fingerprint.is_some() { - return Err(de::Error::duplicate_field("fingerprint")); - } - fingerprint = Some(map.next_value()?); - } - Field::BridgePOK => { - if bridge_pok.is_some() { - return Err(de::Error::duplicate_field("bridge_pok")); - } - bridge_pok = Some(map.next_value()?); - } - Field::Country => { - if country.is_some() { - return Err(de::Error::duplicate_field("country")); - } - country = Some(map.next_value()?); - } - Field::Date => { - if date.is_some() { - return Err(de::Error::duplicate_field("date")); - } - date = Some(map.next_value()?); - } - } - } - let fingerprint = - fingerprint.ok_or_else(|| de::Error::missing_field("fingerprint"))?; - let bridge_pok = - bridge_pok.ok_or_else(|| de::Error::missing_field("bridge_pok"))?; - let country = country.ok_or_else(|| de::Error::missing_field("country"))?; - if country != "" && !COUNTRY_CODES.contains(country.as_str()) { - return Err(de::Error::invalid_value( - Unexpected::Str(&country), - &"a country code or empty string", - )); - } - let date = date.ok_or_else(|| de::Error::missing_field("date"))?; - if date > get_date().into() { - return Err(de::Error::invalid_value( - Unexpected::Unsigned(date), - &"report date no later than today", - )); - } - Ok(NegativeReport { - fingerprint: fingerprint, - bridge_pok: bridge_pok, - country: country.to_string(), - date: date.try_into().unwrap(), - }) - } + self.bridge_ips.get(country).unwrap() + ) + .as_str(), + ); } - const FIELDS: &'static [&'static str] = &["fingerprint", "bridge_pok", "country", "date"]; - deserializer.deserialize_struct("NegativeReport", FIELDS, NegativeReportVisitor) + write!(f, "{}", str) } } -impl NegativeReport { - pub fn new(bridge_id: [u8; 20], bridge_pok: ProofOfBridgeKnowledge, country: String) -> Self { - let mut hasher = Sha1::new(); - hasher.update(bridge_id); - let fingerprint: [u8; 20] = hasher.finalize().into(); - let date = get_date(); - Self { - fingerprint, - bridge_pok, - country, - date, - } - } - - pub fn from_bridgeline(bridge_id: [u8; 20], bridgeline: BridgeLine, country: String) -> Self { - let bridge_pok = - ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(bridgeline)); - NegativeReport::new(bridge_id, bridge_pok, country) - } - - pub fn from_bucket(bridge_id: [u8; 20], bucket: Scalar, country: String) -> Self { - let mut hasher = Sha3_256::new(); - hasher.update(bucket.to_bytes()); - let bucket_hash: [u8; 32] = hasher.finalize().into(); - let bridge_pok = ProofOfBridgeKnowledge::HashOfBucket(HashOfBucket { hash: bucket_hash }); - NegativeReport::new(bridge_id, bridge_pok, country) - } - - pub fn from_lox_credential(bridge_id: [u8; 20], cred: Lox, country: String) -> Self { - NegativeReport::from_bucket(bridge_id, cred.bucket, country) - } - - fn verify(&self) -> bool { - self.bridge_pok.verify(self.fingerprint) - } -} - -/// A report that the user was able to connect to the bridge -#[derive(Serialize)] -pub struct PositiveReport { - /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) - pub fingerprint: [u8; 20], - /// token from the bridge indicating it was reached - bridge_token: Option, - // proof of Lox cred with level >= 3 and this bridge - lox_proof: positive_report::Request, - /// user's country code, may be an empty string - pub country: String, - /// today's Julian date - pub date: u32, -} - -// Ensure public fields are legal while deserializing -// Based on https://serde.rs/deserialize-struct.html -impl<'de> Deserialize<'de> for PositiveReport { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - enum Field { - Fingerprint, - BridgeToken, - LoxProof, - Country, - Date, - } - - impl<'de> Deserialize<'de> for Field { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct FieldVisitor; - - impl<'de> Visitor<'de> for FieldVisitor { - type Value = Field; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "`fingerprint`, `bridge_token`, `lox_proof`, `country`, or `date`", - ) - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, +/// Adds the extra-info data for a single bridge to the database. If the +/// database already contains an extra-info for this bridge for thid date, +/// but this extra-info contains different data for some reason, use the +/// greater count of connections from each country. +pub fn add_extra_info_to_db(db: &Db, extra_info: ExtraInfo) { + let fingerprint = extra_info.fingerprint; + let mut bridge_info = match db.get(&fingerprint).unwrap() { + Some(v) => bincode::deserialize(&v).unwrap(), + None => BridgeInfo::new(fingerprint, extra_info.nickname), + }; + // If we already have an entry, compare it with the new one. For each + // country:count mapping, use the greater of the two counts. + if bridge_info.info_by_day.contains_key(&extra_info.published) { + let daily_bridge_info = bridge_info + .info_by_day + .get_mut(&extra_info.published) + .unwrap(); + if extra_info.bridge_ips != daily_bridge_info.bridge_ips { + for country in extra_info.bridge_ips.keys() { + if daily_bridge_info.bridge_ips.contains_key(country) { + // Use greatest value we've seen today + if daily_bridge_info.bridge_ips.get(country).unwrap() + < extra_info.bridge_ips.get(country).unwrap() { - match value { - "fingerprint" => Ok(Field::Fingerprint), - "bridge_token" => Ok(Field::BridgeToken), - "lox_proof" => Ok(Field::LoxProof), - "country" => Ok(Field::Country), - "date" => Ok(Field::Date), - _ => Err(de::Error::unknown_field(value, FIELDS)), - } + daily_bridge_info.bridge_ips.insert( + country.to_string(), + *extra_info.bridge_ips.get(country).unwrap(), + ); } + } else { + daily_bridge_info.bridge_ips.insert( + country.to_string(), + *extra_info.bridge_ips.get(country).unwrap(), + ); } - - deserializer.deserialize_identifier(FieldVisitor) } } - - struct PositiveReportVisitor; - - impl<'de> de::Visitor<'de> for PositiveReportVisitor { - type Value = PositiveReport; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "positive report with valid country code and date no later than today", - ) - } - - fn visit_seq(self, mut seq: V) -> Result - where - V: SeqAccess<'de>, - { - let fingerprint = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(0, &self))?; - let bridge_token = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(1, &self))?; - let lox_proof = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(2, &self))?; - let country = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(3, &self))?; - let date = seq - .next_element()? - .ok_or_else(|| de::Error::invalid_length(4, &self))?; - Ok(PositiveReport { - fingerprint, - bridge_token, - lox_proof, - country, - date, - }) - } - - fn visit_map(self, mut map: V) -> Result - where - V: MapAccess<'de>, - { - let mut fingerprint = None; - let mut bridge_token: Option> = None; - let mut lox_proof = None; - let mut country: Option = None; - let mut date = None; - while let Some(key) = map.next_key()? { - match key { - Field::Fingerprint => { - if fingerprint.is_some() { - return Err(de::Error::duplicate_field("fingerprint")); - } - fingerprint = Some(map.next_value()?); - } - Field::BridgeToken => { - if bridge_token.is_some() { - return Err(de::Error::duplicate_field("bridge_token")); - } - bridge_token = Some(map.next_value()?); - } - Field::LoxProof => { - if lox_proof.is_some() { - return Err(de::Error::duplicate_field("lox_proof")); - } - lox_proof = Some(map.next_value()?); - } - Field::Country => { - if country.is_some() { - return Err(de::Error::duplicate_field("country")); - } - country = Some(map.next_value()?); - } - Field::Date => { - if date.is_some() { - return Err(de::Error::duplicate_field("date")); - } - date = Some(map.next_value()?); - } - } - } - let fingerprint = - fingerprint.ok_or_else(|| de::Error::missing_field("fingerprint"))?; - let bridge_token = - bridge_token.ok_or_else(|| de::Error::missing_field("bridge_token"))?; - if REQUIRE_BRIDGE_TOKEN && bridge_token.is_none() { - return Err(de::Error::invalid_value( - Unexpected::Option, - &"a bridge token (mandatory, per system configuration)", - )); - } - let lox_proof = lox_proof.ok_or_else(|| de::Error::missing_field("lox_proof"))?; - let country = country.ok_or_else(|| de::Error::missing_field("country"))?; - if country != "" && !COUNTRY_CODES.contains(country.as_str()) { - return Err(de::Error::invalid_value( - Unexpected::Str(&country), - &"a country code or empty string", - )); - } - let date = date.ok_or_else(|| de::Error::missing_field("date"))?; - if date > get_date().into() { - return Err(de::Error::invalid_value( - Unexpected::Unsigned(date), - &"report date no later than today", - )); - } - Ok(PositiveReport { - fingerprint: fingerprint, - bridge_token: bridge_token, - lox_proof: lox_proof, - country: country.to_string(), - date: date.try_into().unwrap(), - }) - } - } - const FIELDS: &'static [&'static str] = &[ - "fingerprint", - "bridge_token", - "lox_proof", - "country", - "date", - ]; - deserializer.deserialize_struct("PositiveReport", FIELDS, PositiveReportVisitor) - } -} - -impl PositiveReport { - pub fn new( - bridge_id: [u8; 20], - bridge_token: Option, - lox_proof: positive_report::Request, - country: String, - ) -> Self { - if REQUIRE_BRIDGE_TOKEN && bridge_token.is_none() { - panic!("Bridge tokens are required for positive reports."); - } - let mut hasher = Sha1::new(); - hasher.update(bridge_id); - let fingerprint: [u8; 20] = hasher.finalize().into(); - let date = get_date(); - Self { - fingerprint, - bridge_token, - lox_proof, - country, - date, - } - } - - pub fn from_lox_credential( - bridge_id: [u8; 20], - bridge_token: Option, - lox_cred: &Lox, - lox_pub: &IssuerPubKey, - country: String, - ) -> Self { - let lox_proof = positive_report::request(lox_cred, lox_pub).unwrap(); - PositiveReport::new(bridge_id, bridge_token, lox_proof, country) - } - - fn verify(&self) -> bool { - !REQUIRE_BRIDGE_TOKEN || { - if self.bridge_token.is_none() { - false - } else { - let bt = self.bridge_token.as_ref().unwrap(); - bt.verify() - } - } - } -} - -/// An unsigned token which indicates that the bridge was reached -#[derive(Serialize, Deserialize)] -pub struct UnsignedBridgeToken { - /// hashed fingerprint (SHA-1 hash of 20-byte bridge ID) - pub fingerprint: [u8; 20], - /// client's country code - pub country: String, - /// today's Julian date - pub date: u32, -} - -impl UnsignedBridgeToken { - pub fn new(bridge_id: [u8; 20], country: String) -> Self { - let mut hasher = Sha1::new(); - hasher.update(bridge_id); - let fingerprint: [u8; 20] = hasher.finalize().into(); - let date = get_date(); - Self { - fingerprint, - country, - date, - } - } -} - -/// A signed token which indicates that the bridge was reached -#[derive(Serialize, Deserialize)] -pub struct BridgeToken { - /// the unsigned version of this token - pub unsigned_bridge_token: UnsignedBridgeToken, - /// signature from bridge's ed25519 key - pub sig: Signature, -} - -impl BridgeToken { - pub fn new(unsigned_bridge_token: UnsignedBridgeToken, keypair: SigningKey) -> Self { - let sig = keypair.sign(&bincode::serialize(&unsigned_bridge_token).unwrap()); - Self { - unsigned_bridge_token, - sig, - } - } - - pub fn verify(&self) -> bool { - let pubkey = get_bridge_signing_pubkey(&self.unsigned_bridge_token.fingerprint); - self.unsigned_bridge_token.date <= get_date() - && pubkey - .verify( - &bincode::serialize(&self.unsigned_bridge_token).unwrap(), - &self.sig, - ) - .is_ok() + } else { + // No existing entry; make a new one. + let daily_bridge_info = DailyBridgeInfo { + bridge_ips: extra_info.bridge_ips, + negative_reports: Vec::::new(), + positive_reports: Vec::::new(), + }; + bridge_info + .info_by_day + .insert(extra_info.published, daily_bridge_info); } + // Commit changes to database + db.insert(fingerprint, bincode::serialize(&bridge_info).unwrap()) + .unwrap(); }