From 31f99d77fbdee2cc1d3bdbb8c49d098148bebdb3 Mon Sep 17 00:00:00 2001 From: trivernis Date: Thu, 10 Sep 2020 15:56:31 +0200 Subject: [PATCH] Add token functions (create, refresh, validate) Signed-off-by: trivernis --- .idea/inspectionProfiles/Project_Default.xml | 10 ++ .idea/workspace.xml | 42 ++++---- Cargo.lock | 35 +++++++ Cargo.toml | 4 +- src/database/mod.rs | 3 + src/database/models.rs | 5 +- src/database/redis_operations.rs | 5 + src/database/role_permissions.rs | 1 - src/database/roles.rs | 2 +- src/database/tokens.rs | 91 ++++++++++++++++ src/database/user_roles.rs | 2 +- src/database/users.rs | 103 +++++++++++++++++-- src/main.rs | 11 -- src/server/rpc_methods.rs | 1 + src/utils.rs | 21 +++- 15 files changed, 289 insertions(+), 47 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 src/database/redis_operations.rs create mode 100644 src/database/tokens.rs diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..e6c90c3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 49f3395..e516944 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,5 +1,8 @@ + + @@ -13,19 +16,17 @@ - - + + - - - - + - + + + @@ -96,7 +98,7 @@ @@ -108,22 +110,22 @@ - + - - + + - - + + - - + + - + @@ -144,17 +146,17 @@ - + - + - + - + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index df5977a..55acba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,6 +305,7 @@ checksum = "5c85295147490b8fcf2ea3d104080a105a8b2c63f9c319e82c02d8e952388919" name = "flotte-user-management" version = "0.1.0" dependencies = [ + "byteorder", "dotenv", "msgrpc", "postgres", @@ -313,6 +314,7 @@ dependencies = [ "scrypt", "serde", "serde_postgres", + "zeroize", ] [[package]] @@ -1130,6 +1132,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "tinyvec" version = "0.3.4" @@ -1384,3 +1398,24 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] + +[[package]] +name = "zeroize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index c89ad00..a20e745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,6 @@ dotenv = "0.15.0" redis = "0.17.0" serde = {version = "1.0.115", features = ["serde_derive"]} rand = "0.7.3" -scrypt = "0.4.1" \ No newline at end of file +scrypt = "0.4.1" +zeroize = {version = "1.1.0", features = ["zeroize_derive"]} +byteorder = "1.3.4" \ No newline at end of file diff --git a/src/database/mod.rs b/src/database/mod.rs index 6e8e1cd..4d12e16 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -10,8 +10,10 @@ use std::sync::{Arc, Mutex}; pub mod models; pub mod permissions; +pub mod redis_operations; pub mod role_permissions; pub mod roles; +pub mod tokens; pub mod user_roles; pub mod users; @@ -40,6 +42,7 @@ pub enum Error { RecordExists, ScryptError, DeserializeError(serde_postgres::DeError), + GenericError(String), } pub type DatabaseError = Error; diff --git a/src/database/models.rs b/src/database/models.rs index f8873a4..2220f3d 100644 --- a/src/database/models.rs +++ b/src/database/models.rs @@ -1,7 +1,8 @@ use postgres::Row; -use serde::Deserialize; +use zeroize::Zeroize; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Zeroize)] +#[zeroize(drop)] pub struct UserRecord { pub id: i32, pub name: String, diff --git a/src/database/redis_operations.rs b/src/database/redis_operations.rs new file mode 100644 index 0000000..cd24757 --- /dev/null +++ b/src/database/redis_operations.rs @@ -0,0 +1,5 @@ +pub const SET: &str = "SET"; +pub const EXPIRE: &str = "EXPIRE"; +pub const TTL: &str = "TTL"; +pub const EX: &str = "EX"; +pub const GET: &str = "GET"; diff --git a/src/database/role_permissions.rs b/src/database/role_permissions.rs index 85f630e..feb21c2 100644 --- a/src/database/role_permissions.rs +++ b/src/database/role_permissions.rs @@ -1,5 +1,4 @@ use crate::database::{DatabaseClient, DatabaseError, DatabaseResult, RedisConnection, Table}; -use postgres::{Client, Error}; use std::sync::{Arc, Mutex}; #[derive(Clone)] diff --git a/src/database/roles.rs b/src/database/roles.rs index 98508ad..ceb5a73 100644 --- a/src/database/roles.rs +++ b/src/database/roles.rs @@ -1,6 +1,6 @@ use crate::database::role_permissions::RolePermissions; use crate::database::{DatabaseError, DatabaseResult, RedisConnection, Table}; -use postgres::{Client, Error}; +use postgres::Client; use std::sync::{Arc, Mutex}; #[derive(Clone)] diff --git a/src/database/tokens.rs b/src/database/tokens.rs new file mode 100644 index 0000000..3e91859 --- /dev/null +++ b/src/database/tokens.rs @@ -0,0 +1,91 @@ +use crate::database::redis_operations::{EX, GET, SET}; +use crate::database::RedisConnection; +use crate::utils::create_user_token; +use byteorder::{BigEndian, ByteOrder}; +use redis::RedisResult; +use zeroize::Zeroize; + +const REQUEST_TOKEN_EXPIRE_SECONDS: usize = 60 * 10; +const REFRESH_TOKEN_EXPIRE_SECONDS: usize = 60 * 60 * 24; + +#[derive(Clone, Debug, Zeroize)] +#[zeroize(drop)] +pub struct SessionTokens { + pub request_token: [u8; 32], + pub refresh_token: [u8; 32], +} + +impl SessionTokens { + pub fn new(user_id: i32) -> Self { + Self { + request_token: create_user_token(user_id), + refresh_token: create_user_token(user_id), + } + } + + pub fn from_tokens(request_token: [u8; 32], refresh_token: [u8; 32]) -> Self { + Self { + request_token, + refresh_token, + } + } + + pub fn retrieve(user_id: i32, redis_connection: &mut RedisConnection) -> RedisResult { + let redis_request_key = format!("user-{}_request", user_id); + let request_token_vec: Vec = redis::cmd(GET) + .arg(redis_request_key) + .query(redis_connection)?; + let redis_refresh_key = format!("user-{}_refresh", user_id); + let refresh_token_vec: Vec = redis::cmd(GET) + .arg(redis_refresh_key) + .query(redis_connection)?; + + let mut request_token = [0u8; 32]; + let mut refresh_token = [0u8; 32]; + if request_token_vec.len() == 32 { + request_token.copy_from_slice(&request_token_vec); + } + if refresh_token_vec.len() == 32 { + refresh_token.copy_from_slice(&refresh_token_vec); + } + + Ok(Self { + request_token, + refresh_token, + }) + } + + pub fn refresh(&mut self) { + self.request_token = create_user_token(self.get_user_id()); + self.refresh_token = create_user_token(self.get_user_id()); + } + + /// Returns the user id that is stored in the first four bytes of the refresh token + pub fn get_user_id(&self) -> i32 { + BigEndian::read_i32(&self.refresh_token[0..4]) + } + + /// Saves the tokens into the database + pub fn store(&self, redis_connection: &mut RedisConnection) -> RedisResult<()> { + let id = self.get_user_id(); + + let redis_request_key = format!("user-{}_request", id); + redis::cmd(SET) + .arg(&redis_request_key) + .arg(&self.request_token) + .arg(EX) + .arg(REQUEST_TOKEN_EXPIRE_SECONDS) + .query(&mut *redis_connection)?; + + let redis_refresh_key = format!("user-{}_refresh", id); + + redis::cmd(SET) + .arg(&redis_refresh_key) + .arg(&self.refresh_token) + .arg(EX) + .arg(REFRESH_TOKEN_EXPIRE_SECONDS) + .query(&mut *redis_connection)?; + + Ok(()) + } +} diff --git a/src/database/user_roles.rs b/src/database/user_roles.rs index db6b2fb..cc71767 100644 --- a/src/database/user_roles.rs +++ b/src/database/user_roles.rs @@ -1,5 +1,5 @@ use crate::database::{DatabaseError, DatabaseResult, RedisConnection, Table}; -use postgres::{Client, Error}; +use postgres::Client; use std::sync::{Arc, Mutex}; #[derive(Clone)] diff --git a/src/database/users.rs b/src/database/users.rs index b3a3d69..834a697 100644 --- a/src/database/users.rs +++ b/src/database/users.rs @@ -1,10 +1,12 @@ use crate::database::models::UserRecord; +use crate::database::tokens::SessionTokens; use crate::database::user_roles::UserRoles; use crate::database::{DatabaseError, DatabaseResult, RedisConnection, Table}; -use crate::utils::create_salt; -use postgres::{Client, Error}; +use crate::utils::{create_salt, get_user_id_from_token, TOKEN_LENGTH}; +use postgres::Client; use scrypt::ScryptParams; use std::sync::{Arc, Mutex}; +use zeroize::{Zeroize, Zeroizing}; #[derive(Clone)] pub struct Users { @@ -13,8 +15,6 @@ pub struct Users { user_roles: UserRoles, } -const SALT_LENGTH: usize = 16; - impl Table for Users { fn new( database_connection: Arc>, @@ -55,6 +55,7 @@ impl Users { password: String, ) -> DatabaseResult { let mut connection = self.database_connection.lock().unwrap(); + let mut password = Zeroizing::new(password); if !connection .query("SELECT email FROM users WHERE email = $1", &[&email]) @@ -63,19 +64,105 @@ impl Users { { return Err(DatabaseError::RecordExists); } - let salt = create_salt(SALT_LENGTH); - let mut pw_hash = [0u8; 32]; + let salt = Zeroizing::new(create_salt()); + let mut pw_hash = Zeroizing::new([0u8; 32]); scrypt::scrypt( password.as_bytes(), - &salt, + &*salt, &ScryptParams::recommended(), - &mut pw_hash, + &mut *pw_hash, ) .map_err(|_| DatabaseError::ScryptError)?; + password.zeroize(); let row = connection.query_one(" INSERT INTO users (name, email, password_hash, salt) VALUES ($1, $2, $3, $4) RETURNING *; ", &[&name, &email, &pw_hash.to_vec(), &salt.to_vec()]).map_err(|e|DatabaseError::Postgres(e))?; Ok(UserRecord::from_ordered_row(&row)) } + + pub fn create_token(&self, email: String, password: String) -> DatabaseResult { + if self.validate_login(&email, password)? { + let mut connection = self.database_connection.lock().unwrap(); + let row = connection + .query_one("SELECT id FROM users WHERE email = $1", &[&email]) + .map_err(|e| DatabaseError::Postgres(e))?; + let id: i32 = row.get(0); + let mut redis_connection = self.redis_connection.lock().unwrap(); + + let tokens = SessionTokens::new(id); + tokens + .store(&mut redis_connection) + .map_err(|e| DatabaseError::Redis(e))?; + + Ok(tokens) + } else { + Err(DatabaseError::GenericError("Invalid password".to_string())) + } + } + + pub fn validate_request_token(&self, token: &[u8; TOKEN_LENGTH]) -> DatabaseResult { + let id = get_user_id_from_token(token); + let mut redis_connection = self.redis_connection.lock().unwrap(); + let tokens = SessionTokens::retrieve(id, &mut redis_connection) + .map_err(|e| DatabaseError::Redis(e))?; + + Ok(tokens.request_token == *token) + } + + pub fn validate_refresh_token(&self, token: &[u8; TOKEN_LENGTH]) -> DatabaseResult { + let id = get_user_id_from_token(token); + let mut redis_connection = self.redis_connection.lock().unwrap(); + let tokens = SessionTokens::retrieve(id, &mut redis_connection) + .map_err(|e| DatabaseError::Redis(e))?; + + Ok(tokens.refresh_token == *token) + } + + pub fn refresh_tokens( + &self, + refresh_token: &[u8; TOKEN_LENGTH], + ) -> DatabaseResult { + let id = get_user_id_from_token(refresh_token); + let mut redis_connection = self.redis_connection.lock().unwrap(); + let mut tokens = SessionTokens::retrieve(id, &mut redis_connection) + .map_err(|e| DatabaseError::Redis(e))?; + + if tokens.refresh_token == *refresh_token { + tokens.refresh(); + tokens + .store(&mut redis_connection) + .map_err(|e| DatabaseError::Redis(e))?; + + Ok(tokens) + } else { + Err(DatabaseError::GenericError( + "Invalid refresh token!".to_string(), + )) + } + } + + fn validate_login(&self, email: &String, password: String) -> DatabaseResult { + let password = Zeroizing::new(password); + let mut connection = self.database_connection.lock().unwrap(); + let row = connection + .query_one( + "SELECT password_hash, salt FROM users WHERE email = $1", + &[&email], + ) + .map_err(|e| DatabaseError::Postgres(e))?; + let original_pw_hash: Zeroizing> = Zeroizing::new(row.get(0)); + let salt: Zeroizing> = Zeroizing::new(row.get(1)); + let mut pw_hash = Zeroizing::new([0u8; 32]); + + scrypt::scrypt( + password.as_bytes(), + &*salt, + &ScryptParams::recommended(), + &mut *pw_hash, + ) + .map_err(|_| DatabaseError::ScryptError)?; + + Ok(*pw_hash == *original_pw_hash.as_slice()) + } } diff --git a/src/main.rs b/src/main.rs index 7cc46a7..bea523c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,15 +3,4 @@ use flotte_user_management::database::Database; fn main() { let database = Database::new().unwrap(); database.init().unwrap(); - println!( - "{:?}", - database - .users - .create_user( - "John Doe".to_string(), - "johndoe@protonmail.com".to_string(), - "ttest".to_string() - ) - .unwrap() - ) } diff --git a/src/server/rpc_methods.rs b/src/server/rpc_methods.rs index 0f4346f..29a7e09 100644 --- a/src/server/rpc_methods.rs +++ b/src/server/rpc_methods.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] const NULL: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; const VALIDATE_TOKEN: [u8; 4] = [0x56, 0x41, 0x4c, 0x49]; const GET_ROLES: [u8; 4] = [0x52, 0x4f, 0x4c, 0x45]; diff --git a/src/utils.rs b/src/utils.rs index 77e9b13..e1a4784 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,26 @@ +use byteorder::{BigEndian, ByteOrder}; use rand::Rng; -pub fn create_salt(length: usize) -> [u8; 16] { +pub const TOKEN_LENGTH: usize = 32; +const SALT_LENGTH: usize = 16; + +pub fn create_salt() -> [u8; SALT_LENGTH] { let mut rng = rand::thread_rng(); - let mut salt = [0u8; 16]; + let mut salt = [0u8; SALT_LENGTH]; rng.fill(&mut salt); salt } + +pub fn create_user_token(user_id: i32) -> [u8; TOKEN_LENGTH] { + let mut rng = rand::thread_rng(); + let mut value = [0u8; TOKEN_LENGTH]; + rng.fill(&mut value); + BigEndian::write_i32(&mut value, user_id); + + value +} + +pub fn get_user_id_from_token(token: &[u8]) -> i32 { + BigEndian::read_i32(token) +}