Encrypt negative reports
This commit is contained in:
parent
bd4bc1b7b8
commit
43228e18c9
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
aes-gcm = "0.10"
|
||||
array-bytes = "6.2.0"
|
||||
bincode = "1"
|
||||
chrono = "0.4"
|
||||
|
@ -13,6 +14,7 @@ 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"] }
|
||||
futures = "0.3.30"
|
||||
hkdf = "0.12"
|
||||
http = "1"
|
||||
http-body-util = "0.1"
|
||||
hyper = { version = "0.14.28", features = ["full"] }
|
||||
|
@ -34,6 +36,7 @@ statrs = "0.16"
|
|||
time = "0.3.30"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-cron = "0.1.2"
|
||||
x25519-dalek = { version = "2", features = ["serde", "static_secrets"] }
|
||||
|
||||
[dev-dependencies]
|
||||
base64 = "0.21.7"
|
||||
|
|
|
@ -90,6 +90,9 @@ async fn update_daily_info(
|
|||
confidence,
|
||||
);
|
||||
report_blockages(&distributors, new_blockages).await;
|
||||
|
||||
// Generate tomorrow's key if we don't already have it
|
||||
new_negative_report_key(&db, get_date() + 1);
|
||||
}
|
||||
|
||||
async fn run_updater(updater_tx: mpsc::Sender<Command>) {
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
// Minimal implementation of ECIES with x25519_dalek
|
||||
use aes_gcm::{
|
||||
aead::{Aead, AeadCore},
|
||||
Aes256Gcm, Key, KeyInit, Nonce,
|
||||
};
|
||||
use hkdf::Hkdf;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha3::Sha3_256;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
|
||||
|
||||
const SALT_STRING: &str = "ECIES symmetric key for Troll Patrol";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EciesCiphertext {
|
||||
pubkey: PublicKey,
|
||||
nonce: Vec<u8>,
|
||||
ct: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EciesCiphertext {
|
||||
pub fn encrypt(message: &[u8], receiver_pubkey: &PublicKey) -> Result<Self, String> {
|
||||
// Compute shared secret based on new ephemeral ECDH key
|
||||
let mut rng = rand::thread_rng();
|
||||
let secret = EphemeralSecret::random_from_rng(&mut rng);
|
||||
let client_pubkey = PublicKey::from(&secret);
|
||||
let shared_secret = secret.diffie_hellman(&receiver_pubkey);
|
||||
|
||||
// Compute key
|
||||
let hk = Hkdf::<Sha3_256>::new(None, shared_secret.as_bytes());
|
||||
let mut symmetric_key = [0u8; 32];
|
||||
if hk
|
||||
.expand(SALT_STRING.as_bytes(), &mut symmetric_key)
|
||||
.is_err()
|
||||
{
|
||||
return Err("Failed to encrypt".to_string());
|
||||
}
|
||||
|
||||
// Encrypt with key
|
||||
let key: Key<Aes256Gcm> = symmetric_key.into();
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut rng);
|
||||
match cipher.encrypt(&nonce, &*message) {
|
||||
Ok(ct) => Ok(EciesCiphertext {
|
||||
pubkey: client_pubkey,
|
||||
nonce: nonce.to_vec(),
|
||||
ct: ct,
|
||||
}),
|
||||
Err(_) => Err("Failed to encrypt".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrypt(self, secret: &StaticSecret) -> Result<Vec<u8>, String> {
|
||||
// Compute shared secret
|
||||
let shared_secret = secret.diffie_hellman(&self.pubkey);
|
||||
|
||||
// Compute key
|
||||
let hk = Hkdf::<Sha3_256>::new(None, shared_secret.as_bytes());
|
||||
let mut symmetric_key = [0u8; 32];
|
||||
if hk
|
||||
.expand(SALT_STRING.as_bytes(), &mut symmetric_key)
|
||||
.is_err()
|
||||
{
|
||||
return Err("Failed to decrypt".to_string());
|
||||
}
|
||||
|
||||
// Decrypt with key
|
||||
let key: Key<Aes256Gcm> = symmetric_key.into();
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = Nonce::from_slice(&self.nonce);
|
||||
match cipher.decrypt(nonce, &*self.ct) {
|
||||
Ok(m) => Ok(m),
|
||||
Err(_) => Err("Failed to decrypt".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
51
src/lib.rs
51
src/lib.rs
|
@ -10,9 +10,11 @@ use std::{
|
|||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fmt,
|
||||
};
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
pub mod analysis;
|
||||
pub mod bridge_verification_info;
|
||||
pub mod crypto;
|
||||
pub mod extra_info;
|
||||
pub mod negative_report;
|
||||
pub mod positive_report;
|
||||
|
@ -318,6 +320,55 @@ pub async fn update_extra_infos(
|
|||
|
||||
// Process negative reports
|
||||
|
||||
/// If there is already a negative report ECDH key for this date, return None.
|
||||
/// Otherwise, generate a new keypair, save the secret part in the db, and
|
||||
/// return the public part.
|
||||
pub fn new_negative_report_key(db: &Db, date: u32) -> Option<PublicKey> {
|
||||
let mut nr_keys = if !db.contains_key("nr-keys").unwrap() {
|
||||
BTreeMap::<u32, StaticSecret>::new()
|
||||
} else {
|
||||
match bincode::deserialize(&db.get("nr-keys").unwrap().unwrap()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => BTreeMap::<u32, StaticSecret>::new(),
|
||||
}
|
||||
};
|
||||
if nr_keys.contains_key(&date) {
|
||||
None
|
||||
} else {
|
||||
let mut rng = rand::thread_rng();
|
||||
let secret = StaticSecret::random_from_rng(&mut rng);
|
||||
let public = PublicKey::from(&secret);
|
||||
nr_keys.insert(date, secret);
|
||||
db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap())
|
||||
.unwrap();
|
||||
Some(public)
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive an encrypted negative report. Attempt to decrypt it and if
|
||||
/// successful, add it to the database to be processed later.
|
||||
pub fn handle_encrypted_negative_report(db: &Db, enc_report: EncryptedNegativeReport) {
|
||||
if db.contains_key("nr-keys").unwrap() {
|
||||
let nr_keys: BTreeMap<u32, StaticSecret> =
|
||||
match bincode::deserialize(&db.get("nr-keys").unwrap().unwrap()) {
|
||||
Ok(map) => map,
|
||||
Err(_) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if nr_keys.contains_key(&enc_report.date) {
|
||||
let secret = nr_keys.get(&enc_report.date).unwrap();
|
||||
let nr = match enc_report.decrypt(&secret) {
|
||||
Ok(nr) => nr,
|
||||
Err(_) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
save_negative_report_to_process(&db, nr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// We store to-be-processed negative reports as a vector. Add this NR
|
||||
/// to that vector (or create a new vector if necessary)
|
||||
pub fn save_negative_report_to_process(db: &Db, nr: NegativeReport) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
bridge_verification_info::BridgeVerificationInfo, get_date, BridgeDistributor, COUNTRY_CODES,
|
||||
MAX_BACKDATE,
|
||||
bridge_verification_info::BridgeVerificationInfo, crypto::EciesCiphertext, get_date,
|
||||
BridgeDistributor, COUNTRY_CODES, MAX_BACKDATE,
|
||||
};
|
||||
|
||||
use curve25519_dalek::scalar::Scalar;
|
||||
|
@ -9,12 +9,14 @@ use rand::RngCore;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sha1::{Digest, Sha1};
|
||||
use sha3::Sha3_256;
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum NegativeReportError {
|
||||
DateInFuture,
|
||||
DateInPast, // report is more than MAX_BACKDATE days old
|
||||
FailedToDeserialize, // couldn't deserialize to SerializableNegativeReport
|
||||
FailedToDecrypt, // couldn't decrypt to SerializableNegativeReport
|
||||
FailedToDeserialize, // couldn't deserialize to NegativeReport
|
||||
InvalidCountryCode,
|
||||
MissingCountryCode,
|
||||
}
|
||||
|
@ -108,6 +110,17 @@ impl NegativeReport {
|
|||
NegativeReport::from_lox_bucket(bridge_id, cred.bucket, country)
|
||||
}
|
||||
|
||||
pub fn encrypt(self, server_pub: &PublicKey) -> EncryptedNegativeReport {
|
||||
EncryptedNegativeReport {
|
||||
date: self.date,
|
||||
ciphertext: EciesCiphertext::encrypt(
|
||||
&bincode::serialize(&self.to_serializable_report()).unwrap(),
|
||||
server_pub,
|
||||
)
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert report to a serializable version
|
||||
pub fn to_serializable_report(self) -> SerializableNegativeReport {
|
||||
SerializableNegativeReport {
|
||||
|
@ -199,6 +212,28 @@ impl SerializableNegativeReport {
|
|||
}
|
||||
}
|
||||
|
||||
/// Negative reports should be sent encrypted. This struct provides an
|
||||
/// encrypted serializable negative report.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EncryptedNegativeReport {
|
||||
/// The date field in the report. This is used to determine which key to use
|
||||
/// to decrypt the report.
|
||||
pub date: u32,
|
||||
ciphertext: EciesCiphertext,
|
||||
}
|
||||
|
||||
impl EncryptedNegativeReport {
|
||||
pub fn decrypt(self, secret: &StaticSecret) -> Result<NegativeReport, NegativeReportError> {
|
||||
match self.ciphertext.decrypt(&secret) {
|
||||
Ok(m) => match bincode::deserialize::<SerializableNegativeReport>(&m) {
|
||||
Ok(ser_report) => ser_report.to_report(),
|
||||
Err(_) => Err(NegativeReportError::FailedToDeserialize),
|
||||
},
|
||||
Err(_) => Err(NegativeReportError::FailedToDecrypt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Proof that the user knows (and should be able to access) a given bridge
|
||||
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
pub enum ProofOfBridgeKnowledge {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{negative_report::NegativeReport, positive_report::PositiveReport, *};
|
||||
use crate::{negative_report::EncryptedNegativeReport, positive_report::PositiveReport, *};
|
||||
use hyper::{body, header::HeaderValue, Body, Method, Request, Response, StatusCode};
|
||||
use serde_json::json;
|
||||
use sled::Db;
|
||||
|
@ -17,15 +17,17 @@ pub async fn handle(db: &Db, req: Request<Body>) -> Result<Response<Body>, Infal
|
|||
_ => match (req.method(), req.uri().path()) {
|
||||
(&Method::POST, "/negativereport") => Ok::<_, Infallible>({
|
||||
let bytes = body::to_bytes(req.into_body()).await.unwrap();
|
||||
let nr = match NegativeReport::from_slice(&bytes) {
|
||||
Ok(nr) => nr,
|
||||
// We cannot depend on the transport layer providing E2EE, so
|
||||
// positive reports should be separately encrypted.
|
||||
let enr: EncryptedNegativeReport = match bincode::deserialize(&bytes) {
|
||||
Ok(enr) => enr,
|
||||
Err(e) => {
|
||||
let response = json!({"error": e});
|
||||
let response = json!({"error": e.to_string()});
|
||||
let val = serde_json::to_string(&response).unwrap();
|
||||
return Ok(prepare_header(val));
|
||||
}
|
||||
};
|
||||
save_negative_report_to_process(&db, nr);
|
||||
handle_encrypted_negative_report(&db, enr);
|
||||
prepare_header("OK".to_string())
|
||||
}),
|
||||
(&Method::POST, "/positivereport") => Ok::<_, Infallible>({
|
||||
|
|
71
src/tests.rs
71
src/tests.rs
|
@ -20,6 +20,7 @@ use std::{
|
|||
collections::{BTreeMap, HashMap, HashSet},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
struct TestHarness {
|
||||
bdb: BridgeDb,
|
||||
|
@ -333,6 +334,7 @@ fn test_negative_reports() {
|
|||
assert!(!invalid_report_5.verify(&bridge_info_2));
|
||||
|
||||
// Test that reports with duplicate nonces are rejected
|
||||
// (Also test encryption and decryption.)
|
||||
|
||||
// Open test database
|
||||
let db: Db = sled::open("test_db").unwrap();
|
||||
|
@ -354,6 +356,24 @@ fn test_negative_reports() {
|
|||
BridgeDistributor::Lox,
|
||||
);
|
||||
|
||||
let valid_report_1_copy_1 = NegativeReport::new(
|
||||
bridges[0].fingerprint,
|
||||
ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)),
|
||||
"ru".to_string(),
|
||||
date,
|
||||
nonce,
|
||||
BridgeDistributor::Lox,
|
||||
);
|
||||
|
||||
let valid_report_1_copy_2 = NegativeReport::new(
|
||||
bridges[0].fingerprint,
|
||||
ProofOfBridgeKnowledge::HashOfBridgeLine(HashOfBridgeLine::new(&bridges[0], date, nonce)),
|
||||
"ru".to_string(),
|
||||
date,
|
||||
nonce,
|
||||
BridgeDistributor::Lox,
|
||||
);
|
||||
|
||||
// Report which reuses this nonce
|
||||
let invalid_report_1 = NegativeReport::new(
|
||||
bridges[0].fingerprint,
|
||||
|
@ -410,13 +430,40 @@ fn test_negative_reports() {
|
|||
"ru".to_string(),
|
||||
date
|
||||
);
|
||||
save_negative_report_to_process(&db, valid_report_1);
|
||||
|
||||
// Generate key for today
|
||||
let secret = StaticSecret::random_from_rng(&mut rng);
|
||||
let public = PublicKey::from(&secret);
|
||||
let secret_yesterday = StaticSecret::random_from_rng(&mut rng);
|
||||
let public_yesterday = PublicKey::from(&secret_yesterday);
|
||||
assert!(!db.contains_key("nr-keys").unwrap());
|
||||
|
||||
// Fail to add to database because we can't decrypt
|
||||
handle_encrypted_negative_report(&db, valid_report_1_copy_1.encrypt(&public));
|
||||
assert!(!db.contains_key("nrs-to-process").unwrap());
|
||||
|
||||
// Store yesterday's key but not today's
|
||||
let mut nr_keys = BTreeMap::<u32, StaticSecret>::new();
|
||||
nr_keys.insert(date - 1, secret_yesterday);
|
||||
db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap())
|
||||
.unwrap();
|
||||
|
||||
// Fail to add to database because we still can't decrypt
|
||||
handle_encrypted_negative_report(&db, valid_report_1_copy_2.encrypt(&public));
|
||||
assert!(!db.contains_key("nrs-to-process").unwrap());
|
||||
|
||||
// Store today's key
|
||||
nr_keys.insert(date, secret);
|
||||
db.insert("nr-keys", bincode::serialize(&nr_keys).unwrap())
|
||||
.unwrap();
|
||||
|
||||
handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public));
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_1).unwrap();
|
||||
assert_eq!(negative_reports.len(), 1);
|
||||
|
||||
save_negative_report_to_process(&db, invalid_report_1); // no change
|
||||
handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_1).unwrap();
|
||||
|
@ -428,7 +475,7 @@ fn test_negative_reports() {
|
|||
"ru".to_string(),
|
||||
date
|
||||
);
|
||||
save_negative_report_to_process(&db, invalid_report_2); // no change
|
||||
handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
assert!(!nrs_to_process.contains_key(&map_key_2));
|
||||
|
@ -439,13 +486,13 @@ fn test_negative_reports() {
|
|||
"ru".to_string(),
|
||||
date - 1
|
||||
);
|
||||
save_negative_report_to_process(&db, valid_report_2);
|
||||
handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday));
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_3).unwrap();
|
||||
assert_eq!(negative_reports.len(), 1);
|
||||
|
||||
save_negative_report_to_process(&db, valid_report_3);
|
||||
handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public));
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_1).unwrap();
|
||||
|
@ -457,6 +504,10 @@ fn test_negative_reports() {
|
|||
db.clear().unwrap();
|
||||
assert!(!db.contains_key("nrs-to-process").unwrap());
|
||||
|
||||
// Re-generate keys and save in database
|
||||
let public = new_negative_report_key(&db, date).unwrap();
|
||||
let public_yesterday = new_negative_report_key(&db, date - 1).unwrap();
|
||||
|
||||
let mut nonce = [0; 32];
|
||||
rng.fill_bytes(&mut nonce);
|
||||
|
||||
|
@ -522,13 +573,13 @@ fn test_negative_reports() {
|
|||
"ru".to_string(),
|
||||
date
|
||||
);
|
||||
save_negative_report_to_process(&db, valid_report_1);
|
||||
handle_encrypted_negative_report(&db, valid_report_1.encrypt(&public));
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_1).unwrap();
|
||||
assert_eq!(negative_reports.len(), 1);
|
||||
|
||||
save_negative_report_to_process(&db, invalid_report_1); // no change
|
||||
handle_encrypted_negative_report(&db, invalid_report_1.encrypt(&public)); // no change
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_1).unwrap();
|
||||
|
@ -540,7 +591,7 @@ fn test_negative_reports() {
|
|||
"ru".to_string(),
|
||||
date
|
||||
);
|
||||
save_negative_report_to_process(&db, invalid_report_2); // no change
|
||||
handle_encrypted_negative_report(&db, invalid_report_2.encrypt(&public)); // no change
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
assert!(!nrs_to_process.contains_key(&map_key_2));
|
||||
|
@ -551,13 +602,13 @@ fn test_negative_reports() {
|
|||
"ru".to_string(),
|
||||
date - 1
|
||||
);
|
||||
save_negative_report_to_process(&db, valid_report_2);
|
||||
handle_encrypted_negative_report(&db, valid_report_2.encrypt(&public_yesterday));
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_3).unwrap();
|
||||
assert_eq!(negative_reports.len(), 1);
|
||||
|
||||
save_negative_report_to_process(&db, valid_report_3);
|
||||
handle_encrypted_negative_report(&db, valid_report_3.encrypt(&public));
|
||||
let nrs_to_process: BTreeMap<String, Vec<SerializableNegativeReport>> =
|
||||
bincode::deserialize(&db.get("nrs-to-process").unwrap().unwrap()).unwrap();
|
||||
let negative_reports = nrs_to_process.get(&map_key_1).unwrap();
|
||||
|
|
Loading…
Reference in New Issue