implement token service
This commit is contained in:
parent
966d67dbd5
commit
98abc6cc44
|
@ -27,6 +27,8 @@ url = "1.6"
|
|||
lettre_email = "0.8"
|
||||
handlebars = "1.1.0"
|
||||
num_cpus = "1.0"
|
||||
ring = "0.13"
|
||||
base64 = "0.10"
|
||||
|
||||
[dependencies.lettre]
|
||||
version = "0.8"
|
||||
|
|
|
@ -27,9 +27,13 @@ extern crate tempfile;
|
|||
#[cfg(test)]
|
||||
extern crate regex;
|
||||
|
||||
extern crate ring;
|
||||
|
||||
extern crate hagrid_database as database;
|
||||
mod mail;
|
||||
mod web;
|
||||
mod tokens;
|
||||
mod sealed_state;
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = web::serve() {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
use ring::aead::{seal_in_place, open_in_place, Algorithm, AES_256_GCM};
|
||||
use ring::aead::{OpeningKey, SealingKey};
|
||||
use ring::rand::{SecureRandom, SystemRandom};
|
||||
use ring::digest;
|
||||
|
||||
// Keep these in sync, and keep the key len synced with the `private` docs as
|
||||
// well as the `KEYS_INFO` const in secure::Key.
|
||||
static ALGO: &'static Algorithm = &AES_256_GCM;
|
||||
const NONCE_LEN: usize = 12;
|
||||
|
||||
pub struct SealedState {
|
||||
sealing_key: SealingKey,
|
||||
opening_key: OpeningKey,
|
||||
}
|
||||
|
||||
impl SealedState {
|
||||
pub fn new(secret: &str) -> Self {
|
||||
// TODO use KDF
|
||||
let salted_secret = "hagrid".to_owned() + secret;
|
||||
let key = digest::digest(&digest::SHA256, salted_secret.as_bytes());
|
||||
|
||||
let sealing_key = SealingKey::new(ALGO, key.as_ref()).expect("sealing key creation");
|
||||
let opening_key = OpeningKey::new(ALGO, key.as_ref()).expect("sealing key creation");
|
||||
|
||||
SealedState { sealing_key, opening_key }
|
||||
}
|
||||
|
||||
pub fn unseal(&self, mut data: Vec<u8>) -> Result<String, &'static str> {
|
||||
let (nonce, sealed) = data.split_at_mut(NONCE_LEN);
|
||||
let unsealed = open_in_place(&self.opening_key, nonce, &[], 0, sealed)
|
||||
.map_err(|_| "invalid key/nonce/value: bad seal")?;
|
||||
|
||||
::std::str::from_utf8(unsealed)
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|_| "bad unsealed utf8")
|
||||
}
|
||||
|
||||
pub fn seal(&self, input: &str) -> Vec<u8> {
|
||||
let mut data;
|
||||
let output_len = {
|
||||
let overhead = ALGO.tag_len();
|
||||
data = vec![0; NONCE_LEN + input.len() + overhead];
|
||||
|
||||
let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
|
||||
SystemRandom::new().fill(nonce).expect("couldn't random fill nonce");
|
||||
in_out[..input.len()].copy_from_slice(input.as_bytes());
|
||||
|
||||
seal_in_place(&self.sealing_key, nonce, &[], in_out, overhead).expect("in-place seal")
|
||||
};
|
||||
|
||||
data[..(NONCE_LEN + output_len)].to_vec()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let sv = SealedState::new("swag");
|
||||
|
||||
let sealed = sv.seal("test");
|
||||
let unsealed = sv.unseal(sealed).unwrap();
|
||||
|
||||
assert_eq!("test", unsealed);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
use sealed_state::SealedState;
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use database::types::{Fingerprint};
|
||||
use serde_json;
|
||||
|
||||
const REVISION: u8 = 1;
|
||||
|
||||
pub struct Service {
|
||||
sealed_state: SealedState,
|
||||
validity: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize,Deserialize)]
|
||||
struct Token {
|
||||
#[serde(rename = "f")]
|
||||
fpr: Fingerprint,
|
||||
#[serde(rename = "c")]
|
||||
creation: u64,
|
||||
#[serde(rename = "r")]
|
||||
revision: u8,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn init(secret: &str, validity: u64) -> Self {
|
||||
let sealed_state = SealedState::new(secret);
|
||||
Service { sealed_state, validity }
|
||||
}
|
||||
|
||||
pub fn create(&self, fpr: &Fingerprint) -> String {
|
||||
let creation = current_time();
|
||||
let token = Token { fpr: fpr.clone(), creation, revision: REVISION };
|
||||
let token_serialized = serde_json::to_string(&token).unwrap();
|
||||
|
||||
let token_sealed = self.sealed_state.seal(&token_serialized);
|
||||
|
||||
base64::encode_config(&token_sealed, base64::URL_SAFE_NO_PAD)
|
||||
}
|
||||
|
||||
pub fn check(&self, token_encoded: &str) -> Result<Fingerprint, String> {
|
||||
let token_sealed = base64::decode_config(&token_encoded, base64::URL_SAFE_NO_PAD).map_err(|_| "invalid b64")?;
|
||||
let token_str = self.sealed_state.unseal(token_sealed)?;
|
||||
let token: Token = serde_json::from_str(&token_str)
|
||||
.map_err(|_| "failed to deserialize")?;
|
||||
|
||||
let elapsed = current_time() - token.creation;
|
||||
if elapsed > self.validity {
|
||||
Err("Token has expired!")?;
|
||||
}
|
||||
|
||||
Ok(token.fpr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn current_time() -> u64 {
|
||||
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn current_time() -> u64 {
|
||||
12345678
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_check() {
|
||||
let fpr = "D4AB192964F76A7F8F8A9B357BD18320DEADFA11".parse().unwrap();
|
||||
let mt = Service::init("secret", 60);
|
||||
let token = mt.create(&fpr);
|
||||
// println!("{}", &token);
|
||||
// assert!(false);
|
||||
|
||||
let check_result = mt.check(&token);
|
||||
|
||||
assert_eq!(Ok(fpr), check_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ok() {
|
||||
// {"f":"D4AB192964F76A7F8F8A9B357BD18320DEADFA11","c":12345658,"r":1}
|
||||
let fpr = "D4AB192964F76A7F8F8A9B357BD18320DEADFA11".parse().unwrap();
|
||||
let token = "Gpi5wq4ALZSAQ7KaKmCzpgbWP2a7BImNC6H49ztqAD1Tl7qwJdbTIlyFWMEhkMcU-FIbvPkWUkBAP2EB6pP7-pWsIPmUT6sD_NNChwYaiDEMqMIFpcnb0xEPYKBpqZc";
|
||||
let mt = Service::init("secret", 60);
|
||||
|
||||
let check_result = mt.check(token);
|
||||
|
||||
assert_eq!(Ok(fpr), check_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expired() {
|
||||
// {"f":"D4AB192964F76A7F8F8A9B357BD18320DEADFA11","c":12345078,"r":1}
|
||||
let token = "KfbQMVE-U3thjmwrfAo1sdel9ixwd05fALaPfJ-6p_6AhN2_U0DaLUwAEFwLah-R6zTsQ_LNjMf8cu1z-pJnyB1DoSRYdy380HFT8sx6BnEFFXFyaU02bNM0wlv3Uzk";
|
||||
let mt = Service::init("secret", 60);
|
||||
|
||||
let check_result = mt.check(token);
|
||||
|
||||
assert_eq!(Err("Token has expired!".to_owned()), check_result);
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ use std::path::PathBuf;
|
|||
|
||||
pub mod upload;
|
||||
use mail;
|
||||
use tokens;
|
||||
|
||||
use database::{Database, Polymorphic, Query};
|
||||
use database::types::{Email, Fingerprint, KeyID};
|
||||
|
@ -418,6 +419,8 @@ pub fn serve() -> Result<()> {
|
|||
}
|
||||
|
||||
fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let routes = routes![
|
||||
// infra
|
||||
root,
|
||||
|
@ -478,10 +481,16 @@ fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
|
|||
mail::Service::sendmail(from, base_uri, handlebars)?
|
||||
};
|
||||
|
||||
let secret = rocket.config().get_str("token_secret")?.to_string();
|
||||
let validity = rocket.config().get_int("token_validity")?;
|
||||
let validity = u64::try_from(validity)?;
|
||||
let token_service = tokens::Service::init(&secret, validity);
|
||||
|
||||
Ok(rocket
|
||||
.attach(Template::fairing())
|
||||
.manage(state)
|
||||
.manage(mail_service)
|
||||
.manage(token_service)
|
||||
.mount("/", routes)
|
||||
.manage(db))
|
||||
}
|
||||
|
@ -533,6 +542,8 @@ pub mod tests {
|
|||
)
|
||||
.extra("base-URI", BASE_URI)
|
||||
.extra("from", "from")
|
||||
.extra("token_secret", "hagrid")
|
||||
.extra("token_validity", 3600)
|
||||
.extra("filemail_into", filemail.into_os_string().into_string()
|
||||
.expect("path is valid UTF8"))
|
||||
.extra("x-accel-redirect", false)
|
||||
|
|
Loading…
Reference in New Issue