freechat/server-api/src/auth.rs
shockrah ab12283507 + Computing encoding key once in a lazy_static
+ unit test verifying is extremely sus.jpg but it looks like it passes
! bs detectors on max rn so i'll keep this under close watch for now
2020-12-29 23:57:11 -08:00

276 lines
7.9 KiB
Rust

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<u8> = {
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<u8> = 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<String> {
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<value> -> 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::<Claim>(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<hyper::Body>, 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::<super::Claim>(
&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<String> = 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);
}
}