encrypt sync_token and cursor_token
This commit is contained in:
parent
2eaf7d24e0
commit
a340eb8c4f
|
@ -1,2 +1,4 @@
|
||||||
SFRS_ENV=development
|
SFRS_ENV=development
|
||||||
DATABASE_URL=./db/database.test.db
|
DATABASE_URL=./db/database.test.db
|
||||||
|
SYNC_TOKEN_SECRET=awesome_password
|
||||||
|
SYNC_TOKEN_SALT=awesome_salt
|
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -334,6 +334,12 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -1094,9 +1100,11 @@ dependencies = [
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
|
"hex",
|
||||||
"itertools",
|
"itertools",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"regex 1.3.4",
|
"regex 1.3.4",
|
||||||
|
"ring",
|
||||||
"rocket",
|
"rocket",
|
||||||
"rocket_contrib",
|
"rocket_contrib",
|
||||||
"rocket_cors",
|
"rocket_cors",
|
||||||
|
|
|
@ -19,3 +19,5 @@ chrono = "0.4"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
itertools = "0.8"
|
itertools = "0.8"
|
||||||
|
ring = "0.13"
|
||||||
|
hex = "0.4"
|
18
src/api.rs
18
src/api.rs
|
@ -217,7 +217,7 @@ fn items_sync(
|
||||||
// so all that can change the current_max_id for the current user
|
// so all that can change the current_max_id for the current user
|
||||||
// is operations later in this function.
|
// is operations later in this function.
|
||||||
let new_sync_token = match item::SyncItem::get_current_max_id(&db.0, &u) {
|
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,
|
Ok(None) => None,
|
||||||
Err(item::ItemOpError(e)) =>
|
Err(item::ItemOpError(e)) =>
|
||||||
return error_resp(Status::InternalServerError, vec![e])
|
return error_resp(Status::InternalServerError, vec![e])
|
||||||
|
@ -237,11 +237,19 @@ fn items_sync(
|
||||||
// If the client provides cursor_token,
|
// If the client provides cursor_token,
|
||||||
// then, we return all records
|
// then, we return all records
|
||||||
// until sync_token (the head of the last sync)
|
// 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 {
|
} else if let Some(sync_token) = inner_params.sync_token {
|
||||||
// If there is no cursor_token, then we are doing
|
// If there is no cursor_token, then we are doing
|
||||||
// a normal sync, so just return all records from sync_token
|
// 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 {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -263,7 +271,7 @@ fn items_sync(
|
||||||
if let Some(limit) = inner_params.limit {
|
if let Some(limit) = inner_params.limit {
|
||||||
if items.len() as i64 == limit {
|
if items.len() as i64 == limit {
|
||||||
// We may still have something to fetch
|
// 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,
|
// LATEST known state of the system by the client,
|
||||||
// but it MAY still need to fill in a bit of history
|
// but it MAY still need to fill in a bit of history
|
||||||
// (that's where `cursor_token` comes into play)
|
// (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
|
// Remove conflicted items from retrieved items
|
||||||
|
|
|
@ -15,6 +15,7 @@ extern crate lazy_static;
|
||||||
|
|
||||||
mod db;
|
mod db;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
mod sync_tokens;
|
||||||
mod api;
|
mod api;
|
||||||
mod tokens;
|
mod tokens;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
58
src/sync_tokens.rs
Normal file
58
src/sync_tokens.rs
Normal file
|
@ -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<i64, ()> {
|
||||||
|
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(|_| ())
|
||||||
|
}
|
Loading…
Reference in a new issue