Store hashed passwords (#7)
Use if let instead of unwrap Default to invalid password if could not calculate Move hash password methdo and return Result Rename get_password method Default to empty password when no pwd is received Store hashed passwords Store passwords hashed with Argon2 and verify password with that stored hash. Co-authored-by: Guillem Nieto <gnieto.talo@gmail.com>next
parent
abcce95dd8
commit
fa9e127a1e
|
@ -155,6 +155,7 @@ dependencies = [
|
||||||
"ruma-federation-api",
|
"ruma-federation-api",
|
||||||
"ruma-identifiers",
|
"ruma-identifiers",
|
||||||
"ruma-signatures",
|
"ruma-signatures",
|
||||||
|
"rust-argon2 0.8.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sled",
|
"sled",
|
||||||
|
@ -926,7 +927,7 @@ checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"redox_syscall",
|
"redox_syscall",
|
||||||
"rust-argon2",
|
"rust-argon2 0.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1136,6 +1137,18 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-argon2"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.12.0",
|
||||||
|
"blake2b_simd",
|
||||||
|
"constant_time_eq",
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
|
|
@ -28,3 +28,4 @@ ruma-federation-api = "0.0.1"
|
||||||
serde = "1.0.106"
|
serde = "1.0.106"
|
||||||
tokio = { version = "0.2.16", features = ["macros"] } #rt-threaded
|
tokio = { version = "0.2.16", features = ["macros"] } #rt-threaded
|
||||||
rand = "0.7.3"
|
rand = "0.7.3"
|
||||||
|
rust-argon2 = "0.8.2"
|
|
@ -36,6 +36,7 @@ use ruma_events::{collections::only::Event as EduEvent, EventType};
|
||||||
use ruma_identifiers::{RoomId, RoomIdOrAliasId, UserId};
|
use ruma_identifiers::{RoomId, RoomIdOrAliasId, UserId};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{collections::HashMap, convert::TryInto, path::PathBuf, time::Duration};
|
use std::{collections::HashMap, convert::TryInto, path::PathBuf, time::Duration};
|
||||||
|
use argon2::{Config, Variant};
|
||||||
|
|
||||||
const GUEST_NAME_LENGTH: usize = 10;
|
const GUEST_NAME_LENGTH: usize = 10;
|
||||||
const DEVICE_ID_LENGTH: usize = 10;
|
const DEVICE_ID_LENGTH: usize = 10;
|
||||||
|
@ -103,8 +104,20 @@ pub fn register_route(
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
let password = body.password.clone().unwrap_or_default();
|
||||||
data.user_add(&user_id, body.password.clone());
|
|
||||||
|
if let Ok(hash) = utils::calculate_hash(&password) {
|
||||||
|
// Create user
|
||||||
|
data.user_add(&user_id, &hash);
|
||||||
|
} else {
|
||||||
|
return MatrixResult(Err(UserInteractiveAuthenticationResponse::MatrixError(
|
||||||
|
Error {
|
||||||
|
kind: ErrorKind::InvalidParam,
|
||||||
|
message: "Password did not met requirements".to_owned(),
|
||||||
|
status_code: http::StatusCode::BAD_REQUEST,
|
||||||
|
},
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
// Generate new device id if the user didn't specify one
|
// Generate new device id if the user didn't specify one
|
||||||
let device_id = body
|
let device_id = body
|
||||||
|
@ -144,9 +157,11 @@ pub fn login_route(data: State<Data>, body: Ruma<login::Request>) -> MatrixResul
|
||||||
username = format!("@{}:{}", username, data.hostname());
|
username = format!("@{}:{}", username, data.hostname());
|
||||||
}
|
}
|
||||||
if let Ok(user_id) = (*username).try_into() {
|
if let Ok(user_id) = (*username).try_into() {
|
||||||
// Check password (this also checks if the user exists
|
if let Some(hash) = data.password_hash_get(&user_id) {
|
||||||
if let Some(correct_password) = data.password_get(&user_id) {
|
let hash_matches = argon2::verify_encoded(&hash, password.as_bytes())
|
||||||
if password == correct_password {
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if hash_matches {
|
||||||
// Success!
|
// Success!
|
||||||
user_id
|
user_id
|
||||||
} else {
|
} else {
|
||||||
|
@ -930,4 +945,4 @@ pub fn options_route(_segments: PathBuf) -> MatrixResult<create_message_event::R
|
||||||
message: "This is the options route.".to_owned(),
|
message: "This is the options route.".to_owned(),
|
||||||
status_code: http::StatusCode::OK,
|
status_code: http::StatusCode::OK,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
|
@ -36,10 +36,10 @@ impl Data {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new user account by assigning them a password.
|
/// Create a new user account by assigning them a password.
|
||||||
pub fn user_add(&self, user_id: &UserId, password: Option<String>) {
|
pub fn user_add(&self, user_id: &UserId, hash: &str) {
|
||||||
self.db
|
self.db
|
||||||
.userid_password
|
.userid_password
|
||||||
.insert(user_id.to_string(), &*password.unwrap_or_default())
|
.insert(user_id.to_string(), hash)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ impl Data {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the given password is equal to the one in the database.
|
/// Gets password hash for given user id.
|
||||||
pub fn password_get(&self, user_id: &UserId) -> Option<String> {
|
pub fn password_hash_get(&self, user_id: &UserId) -> Option<String> {
|
||||||
self.db
|
self.db
|
||||||
.userid_password
|
.userid_password
|
||||||
.get(user_id.to_string())
|
.get(user_id.to_string())
|
||||||
|
|
102
src/test.rs
102
src/test.rs
|
@ -1,5 +1,9 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use rocket::{local::Client, http::Status};
|
use rocket::{local::Client, http::Status};
|
||||||
|
use serde_json::Value;
|
||||||
|
use serde_json::json;
|
||||||
|
use ruma_client_api::error::ErrorKind;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
fn setup_client() -> Client {
|
fn setup_client() -> Client {
|
||||||
Database::try_remove("temp");
|
Database::try_remove("temp");
|
||||||
|
@ -14,19 +18,97 @@ async fn register_login() {
|
||||||
let client = setup_client();
|
let client = setup_client();
|
||||||
let mut response = client
|
let mut response = client
|
||||||
.post("/_matrix/client/r0/register?kind=user")
|
.post("/_matrix/client/r0/register?kind=user")
|
||||||
.body(
|
.body(registration_init())
|
||||||
r#"{
|
|
||||||
"username": "cheeky_monkey",
|
|
||||||
"password": "ilovebananas",
|
|
||||||
"device_id": "GHTYAJCE",
|
|
||||||
"initial_device_display_name": "Jungle Phone",
|
|
||||||
"inhibit_login": false
|
|
||||||
}"#,
|
|
||||||
)
|
|
||||||
.dispatch().await;
|
.dispatch().await;
|
||||||
let body = serde_json::to_value(&response.body_string().await.unwrap()).unwrap();
|
let body = serde_json::from_str::<Value>(&response.body_string().await.unwrap()).unwrap();
|
||||||
|
|
||||||
assert_eq!(response.status().code, 401);
|
assert_eq!(response.status().code, 401);
|
||||||
assert!(dbg!(&body["flows"]).as_array().unwrap().len() > 0);
|
assert!(dbg!(&body["flows"]).as_array().unwrap().len() > 0);
|
||||||
assert!(body["session"].as_str().unwrap().len() > 0);
|
assert!(body["session"].as_str().unwrap().len() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn login_after_register_correct_password() {
|
||||||
|
let client = setup_client();
|
||||||
|
let mut response = client
|
||||||
|
.post("/_matrix/client/r0/register?kind=user")
|
||||||
|
.body(registration_init())
|
||||||
|
.dispatch().await;
|
||||||
|
let body = serde_json::from_str::<Value>(&response.body_string().await.unwrap()).unwrap();
|
||||||
|
let session = body["session"].clone();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post("/_matrix/client/r0/register?kind=user")
|
||||||
|
.body(registration(session.as_str().unwrap()))
|
||||||
|
.dispatch().await;
|
||||||
|
assert_eq!(response.status().code, 200);
|
||||||
|
|
||||||
|
let login_response = client
|
||||||
|
.post("/_matrix/client/r0/login")
|
||||||
|
.body(login_with_password("ilovebananas"))
|
||||||
|
.dispatch()
|
||||||
|
.await;
|
||||||
|
assert_eq!(login_response.status().code, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn login_after_register_incorrect_password() {
|
||||||
|
let client = setup_client();
|
||||||
|
let mut response = client
|
||||||
|
.post("/_matrix/client/r0/register?kind=user")
|
||||||
|
.body(registration_init())
|
||||||
|
.dispatch().await;
|
||||||
|
let body = serde_json::from_str::<Value>(&response.body_string().await.unwrap()).unwrap();
|
||||||
|
let session = body["session"].clone();
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post("/_matrix/client/r0/register?kind=user")
|
||||||
|
.body(registration(session.as_str().unwrap()))
|
||||||
|
.dispatch().await;
|
||||||
|
assert_eq!(response.status().code, 200);
|
||||||
|
|
||||||
|
let mut login_response = client
|
||||||
|
.post("/_matrix/client/r0/login")
|
||||||
|
.body(login_with_password("idontlovebananas"))
|
||||||
|
.dispatch()
|
||||||
|
.await;
|
||||||
|
let body = serde_json::from_str::<Value>(&login_response.body_string().await.unwrap()).unwrap();
|
||||||
|
assert_eq!(body.as_object().unwrap().get("errcode").unwrap().as_str().unwrap(), "M_FORBIDDEN");
|
||||||
|
assert_eq!(login_response.status().code, 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registration_init() -> &'static str {
|
||||||
|
r#"{
|
||||||
|
"username": "cheeky_monkey",
|
||||||
|
"password": "ilovebananas",
|
||||||
|
"device_id": "GHTYAJCE",
|
||||||
|
"initial_device_display_name": "Jungle Phone",
|
||||||
|
"inhibit_login": false
|
||||||
|
}"#
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registration(session: &str) -> String {
|
||||||
|
json!({
|
||||||
|
"auth": {
|
||||||
|
"session": session,
|
||||||
|
"type": "m.login.dummy"
|
||||||
|
},
|
||||||
|
"username": "cheeky_monkey",
|
||||||
|
"password": "ilovebananas",
|
||||||
|
"device_id": "GHTYAJCE",
|
||||||
|
"initial_device_display_name": "Jungle Phone",
|
||||||
|
"inhibit_login": false
|
||||||
|
}).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login_with_password(password: &str) -> String {
|
||||||
|
json!({
|
||||||
|
"type": "m.login.password",
|
||||||
|
"identifier": {
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": "cheeky_monkey"
|
||||||
|
},
|
||||||
|
"password": password,
|
||||||
|
"initial_device_display_name": "Jungle Phone"
|
||||||
|
}).to_string()
|
||||||
|
}
|
16
src/utils.rs
16
src/utils.rs
|
@ -3,6 +3,7 @@ use std::{
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
use argon2::{Config, Variant};
|
||||||
|
|
||||||
pub fn millis_since_unix_epoch() -> u64 {
|
pub fn millis_since_unix_epoch() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
|
@ -39,3 +40,18 @@ pub fn random_string(length: usize) -> String {
|
||||||
.take(length)
|
.take(length)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate a new hash for the given password
|
||||||
|
pub fn calculate_hash(password: &str) -> Result<String, argon2::Error> {
|
||||||
|
let hashing_config = Config {
|
||||||
|
variant: Variant::Argon2id,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let salt = random_string(32);
|
||||||
|
argon2::hash_encoded(
|
||||||
|
password.as_bytes(),
|
||||||
|
salt.as_bytes(),
|
||||||
|
&hashing_config,
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue