use serde::{Serialize, Deserialize}; use bcrypt::{self, BcryptResult}; use mysql_async::Pool; use chrono::{Utc, Duration}; use crate::routes; use db::{member::Member, common::FromDB}; use db::Response; use jsonwebtoken::EncodingKey; lazy_static! { static ref HMAC_SECRET: Vec = { std::fs::read("hmac.secret").expect("Couldn't get HMAC secret") }; static ref ENCODING_KEY: EncodingKey = { EncodingKey::from_secret(&HMAC_SECRET) }; } #[derive(Debug, Serialize, Deserialize)] struct Claim { sub: db::UBigInt, // user id exp: db::BigInt, // expiry date cookie: String, // unique cookie value } impl Claim { pub fn new(id: db::UBigInt) -> Claim { Claim { sub: id, exp: Utc::now() .checked_add_signed(Duration::weeks(1)) .expect("Couldn't generate an expirey date") .timestamp(), cookie: generate_cookie() } } } // used when we create a new users for the first time #[derive(Debug)] pub enum AuthReason { Good, //passed regular check OpenAuth, // route does not require auth NoKey, // key missing BadKey, // key is bad LoginValid, // used only to access the login route which is also our refresh ServerIssue(String) // for well 500's } fn valid_secret(given_pass: &str, hash: &str) -> bool { let result = bcrypt::verify(given_pass, hash); return match result { Ok(result) => result, Err(e) => { eprintln!("{}", e); return false; } } } fn valid_perms(member: Member, path: &str) -> bool { use crate::perms; // if there are perms on the current path make sure the user has them if let Some(p) = perms::get_perm_mask(path) { return (p & member.permissions) == p; } // if no perms then we don't care else { return true; } } fn rng_secret(length: usize) -> String { use getrandom::getrandom; use base64::{encode_config, URL_SAFE}; let mut buf: Vec = vec![0;length]; getrandom(&mut buf).unwrap(); encode_config(buf,URL_SAFE) } pub fn generate_secret() -> String { /* * Generates a url-safe-plaintext secret for our db * */ return rng_secret(64); } pub fn generate_cookie() -> String { return rng_secret(32) } pub fn encrypt_secret(raw: &str) -> BcryptResult { const BCRYPT_COST: u32 = 14; return bcrypt::hash(raw, BCRYPT_COST); } fn jwt_from_serde(params: &serde_json::Value) -> Option<&str> { // gets the `token` from the parameters // option -> some(value) -> string return params.get("jwt")?.as_str(); } async fn valid_jwt(p: &Pool, token: &str) -> AuthReason { use jsonwebtoken::{ decode, DecodingKey, Validation, Algorithm }; // crypto things that should prolly not fail assuming we're configured correctly let algo = Algorithm::HS512; let dk = DecodingKey::from_secret(&HMAC_SECRET); if let Ok(decoded) = decode::(token, &dk, &Validation::new(algo)) { // subject used for querying speed NOT security let listed = db::auth::listed_jwt(p, decoded.claims.sub, token).await.unwrap(); let active = Utc::now().timestamp() < decoded.claims.exp; return match listed && active { true => AuthReason::Good, false => AuthReason::BadKey }; } else { return AuthReason::BadKey; } } fn login_params_from_serde(params: &serde_json::Value) -> Option<(db::UBigInt, &str)> { let id_v = params.get("id"); let secret_v = params.get("secret"); return match (id_v, secret_v) { (Some(id_v), Some(secret_v)) => { match (id_v.as_u64(), secret_v.as_str()) { (Some(id), Some(secret)) => Some((id, secret)), _ => None } }, _ => None } } pub async fn wall_entry<'path, 'pool, 'params>( path: &'path str, pool: &'pool Pool, params: &'params serde_json::Value) -> AuthReason { // Dont need to auth if it's not required let open_path = routes::is_open(path); let jwt = jwt_from_serde(params); if open_path { // ignore the parameters since they're irelevant return AuthReason::OpenAuth; } if let Some(jwt) = jwt { // get the headers here return valid_jwt(pool, jwt).await; } if let Some((id, secret)) = login_params_from_serde(params) { // Last chance we might be hitting the /login route so we have to do the heavy auth flow if path != routes::AUTH_LOGIN { return AuthReason::BadKey; } else { return match Member::get(pool, id).await { Response::Row(user) => { if valid_secret(secret, &user.secret) && valid_perms(user, path){ AuthReason::LoginValid } else { AuthReason::BadKey } }, Response::Empty => AuthReason::BadKey, Response::Other(err) => AuthReason::ServerIssue(err), _ => AuthReason::ServerIssue("db-lib returned garbage".into()) } } } return AuthReason::NoKey; } pub async fn login_get_jwt(p: &Pool, response: &mut hyper::Response, params: serde_json::Value) { // basically this route generates a jwt for the user and returns via the jwt key // in the json response use jsonwebtoken::{ Header, Algorithm, encode }; use hyper::header::HeaderValue; let id = params.get("id").unwrap().as_u64().unwrap(); // only route where we have the "id is there guarantee" let claim = Claim::new(id); let header = Header::new(Algorithm::HS512); let encoded = encode( &header, &claim, &ENCODING_KEY).unwrap(); match db::auth::add_jwt(p, id, &encoded).await { Ok(_) => { response.headers_mut().insert("Content-Type", HeaderValue::from_static("application/json")); let payload = serde_json::json!({ "jwt": encoded }); *response.body_mut() = hyper::Body::from(payload.to_string()); }, Err(e) => { eprintln!("{}", e); *response.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR; } }; } #[cfg(test)] mod auth_tests { use jsonwebtoken::{ Header, encode, Algorithm }; #[test] fn validity_check() { use bcrypt::{hash, DEFAULT_COST}; let plain = super::generate_secret(); match hash(&plain, DEFAULT_COST) { Ok(hash) => assert_eq!(super::valid_secret(&plain, &hash), true), Err(err) => panic!("{}", err) } } #[test] fn verify_jwt() { let claim = super::Claim::new(123); // example claim that we send out let header = Header::new(Algorithm::HS512); // header that basically all clients get let encoded = encode( &header, &claim, &super::ENCODING_KEY).unwrap(); use jsonwebtoken::{decode, DecodingKey, Validation}; let dc = decode::( &encoded, &DecodingKey::from_secret(&super::HMAC_SECRET), &Validation::new(Algorithm::HS512) ); // decoding works yet fails on the debugger assert_eq!(dc.is_ok(), true); println!("{:?}", dc); println!("{}", encoded); let mut parts:Vec = Vec::new(); for i in encoded.split('.') { parts.push(i.to_string()); } let head = &parts[0]; let claim = &parts[1]; let sig = &parts[2]; let res = jsonwebtoken::crypto::verify( sig, &format!("{}.{}", head, claim), &DecodingKey::from_secret(&super::HMAC_SECRET), Algorithm::HS512); println!("{:?}", res); } }