diff --git a/.env.test b/.env.test index a9be4ee..b3ed7c5 100644 --- a/.env.test +++ b/.env.test @@ -1,2 +1,4 @@ SFRS_ENV=development -DATABASE_URL=./db/database.test.db \ No newline at end of file +DATABASE_URL=./db/database.test.db +SYNC_TOKEN_SECRET=awesome_password +SYNC_TOKEN_SALT=awesome_salt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3888690..180df7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,6 +334,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" + [[package]] name = "hmac" version = "0.7.1" @@ -1094,9 +1100,11 @@ dependencies = [ "diesel", "diesel_migrations", "dotenv", + "hex", "itertools", "lazy_static", "regex 1.3.4", + "ring", "rocket", "rocket_contrib", "rocket_cors", diff --git a/Cargo.toml b/Cargo.toml index 071be2a..52384f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,6 @@ uuid = { version = "0.8", features = ["v4"] } chrono = "0.4" serde_json = "1.0" regex = "1" -itertools = "0.8" \ No newline at end of file +itertools = "0.8" +ring = "0.13" +hex = "0.4" \ No newline at end of file diff --git a/src/api.rs b/src/api.rs index de5a758..7999aa9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -217,7 +217,7 @@ fn items_sync( // so all that can change the current_max_id for the current user // is operations later in this function. let new_sync_token = match item::SyncItem::get_current_max_id(&db.0, &u) { - Ok(Some(id)) => Some(id.to_string()), + Ok(Some(id)) => Some(crate::sync_tokens::max_id_to_token(id)), Ok(None) => None, Err(item::ItemOpError(e)) => return error_resp(Status::InternalServerError, vec![e]) @@ -237,11 +237,19 @@ fn items_sync( // If the client provides cursor_token, // then, we return all records // until sync_token (the head of the last sync) - cursor_token.parse().ok() + match crate::sync_tokens::token_to_max_id(&cursor_token) { + Err(()) => + return error_resp(Status::InternalServerError, vec!["Invalid cursor_token".into()]), + Ok(id) => Some(id) + } } else if let Some(sync_token) = inner_params.sync_token { // If there is no cursor_token, then we are doing // a normal sync, so just return all records from sync_token - sync_token.parse().ok() + match crate::sync_tokens::token_to_max_id(&sync_token) { + Err(()) => + return error_resp(Status::InternalServerError, vec!["Invalid sync_token".into()]), + Ok(id) => Some(id) + } } else { None }; @@ -263,7 +271,7 @@ fn items_sync( if let Some(limit) = inner_params.limit { if items.len() as i64 == limit { // We may still have something to fetch - resp.cursor_token = Some(next_from.to_string()); + resp.cursor_token = Some(crate::sync_tokens::max_id_to_token(next_from)); } } } @@ -326,7 +334,7 @@ fn items_sync( // LATEST known state of the system by the client, // but it MAY still need to fill in a bit of history // (that's where `cursor_token` comes into play) - resp.sync_token = Some(last_id.to_string()); + resp.sync_token = Some(crate::sync_tokens::max_id_to_token(last_id)); } // Remove conflicted items from retrieved items diff --git a/src/main.rs b/src/main.rs index 851e279..58e3a31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ extern crate lazy_static; mod db; mod schema; +mod sync_tokens; mod api; mod tokens; mod user; diff --git a/src/sync_tokens.rs b/src/sync_tokens.rs new file mode 100644 index 0000000..2dd025a --- /dev/null +++ b/src/sync_tokens.rs @@ -0,0 +1,58 @@ +use ring::aead::*; +use ring::digest::*; +use ring::pbkdf2::*; +use ring::rand::{SecureRandom, SystemRandom}; + +// In the API endpoint `/items/sync`, we use `max_id` of the +// current user as the sync token. However, this may be prone +// to side-channel leakage since all users in database share +// the same auto-incrementing ID. An attacker may be able to +// call `/items/sync` with one update each time and extract +// what others' are doing based on changes in ID. +// Therefore, we should at least not send the ID as a token +// in plain-text to the client. + +lazy_static! { + static ref TOKEN_KEY: [u8; 32] = get_token_key(); +} + +pub fn get_token_key() -> [u8; 32] { + let pwd = std::env::var("SYNC_TOKEN_SECRET") + .expect("Please set SYNC_TOKEN_SECRET").into_bytes(); + let salt = std::env::var("SYNC_TOKEN_SALT") + .expect("Please set SYNC_TOKEN_SALT").into_bytes(); + let mut ret = [0; 32]; + derive(&SHA256, 100, &salt, &pwd, &mut ret); + ret +} + +pub fn max_id_to_token(max_id: i64) -> String { + let sealing_key = SealingKey::new(&CHACHA20_POLY1305, &*TOKEN_KEY).unwrap(); + let mut nonce = [0u8; 12]; + SystemRandom::new().fill(&mut nonce).unwrap(); + let mut id_str = max_id.to_string().as_bytes().to_vec(); + id_str.resize(id_str.len() + CHACHA20_POLY1305.tag_len(), 0); + let out_len = seal_in_place(&sealing_key, &nonce, &[], &mut id_str, CHACHA20_POLY1305.tag_len()) + .unwrap(); + let mut out = id_str[0..out_len].to_vec(); + out.extend_from_slice(&nonce); + hex::encode(out) +} + +pub fn token_to_max_id(token: &str) -> Result { + let opening_key = OpeningKey::new(&CHACHA20_POLY1305, &*TOKEN_KEY).unwrap(); + let data = hex::decode(token).map_err(|_| ())?; + let len = data.len(); + if len <= 12 { + return Err(()); + } + + let mut id_str = (&data[0..(len - 12)]).to_vec(); + let nonce = &data[(len - 12)..len]; + let decrypted = open_in_place(&opening_key, nonce, &[], 0, &mut id_str) + .map_err(|_| ())?; + String::from_utf8(decrypted.to_vec()) + .map_err(|_| ())? + .parse() + .map_err(|_| ()) +} \ No newline at end of file