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 @@
-
-
+
+
-
-
-
-
+
-
+
+
@@ -47,6 +48,7 @@
+
@@ -96,7 +98,7 @@
1599728150675
-
+
@@ -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)
+}