Encrypt negative reports

This commit is contained in:
Vecna 2024-04-16 01:11:47 -04:00
parent bd4bc1b7b8
commit 43228e18c9
7 changed files with 238 additions and 18 deletions

View File

@ -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"

View File

@ -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>) {

75
src/crypto.rs Normal file
View File

@ -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()),
}
}
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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>({

View File

@ -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();