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
gnieto 2020-04-14 22:25:44 +02:00 committed by timo
parent abcce95dd8
commit fa9e127a1e
6 changed files with 148 additions and 21 deletions

15
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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,
})) }))
} }

View File

@ -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())

View File

@ -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()
}

View File

@ -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,
)
}