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-identifiers",
|
||||
"ruma-signatures",
|
||||
"rust-argon2 0.8.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sled",
|
||||
|
@ -926,7 +927,7 @@ checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431"
|
|||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"rust-argon2",
|
||||
"rust-argon2 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1136,6 +1137,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustls"
|
||||
version = "0.16.0"
|
||||
|
|
|
@ -28,3 +28,4 @@ ruma-federation-api = "0.0.1"
|
|||
serde = "1.0.106"
|
||||
tokio = { version = "0.2.16", features = ["macros"] } #rt-threaded
|
||||
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 serde_json::json;
|
||||
use std::{collections::HashMap, convert::TryInto, path::PathBuf, time::Duration};
|
||||
use argon2::{Config, Variant};
|
||||
|
||||
const GUEST_NAME_LENGTH: usize = 10;
|
||||
const DEVICE_ID_LENGTH: usize = 10;
|
||||
|
@ -103,8 +104,20 @@ pub fn register_route(
|
|||
)));
|
||||
}
|
||||
|
||||
// Create user
|
||||
data.user_add(&user_id, body.password.clone());
|
||||
let password = body.password.clone().unwrap_or_default();
|
||||
|
||||
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
|
||||
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());
|
||||
}
|
||||
if let Ok(user_id) = (*username).try_into() {
|
||||
// Check password (this also checks if the user exists
|
||||
if let Some(correct_password) = data.password_get(&user_id) {
|
||||
if password == correct_password {
|
||||
if let Some(hash) = data.password_hash_get(&user_id) {
|
||||
let hash_matches = argon2::verify_encoded(&hash, password.as_bytes())
|
||||
.unwrap_or(false);
|
||||
|
||||
if hash_matches {
|
||||
// Success!
|
||||
user_id
|
||||
} else {
|
||||
|
|
|
@ -36,10 +36,10 @@ impl Data {
|
|||
}
|
||||
|
||||
/// 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
|
||||
.userid_password
|
||||
.insert(user_id.to_string(), &*password.unwrap_or_default())
|
||||
.insert(user_id.to_string(), hash)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
@ -61,8 +61,8 @@ impl Data {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Checks if the given password is equal to the one in the database.
|
||||
pub fn password_get(&self, user_id: &UserId) -> Option<String> {
|
||||
/// Gets password hash for given user id.
|
||||
pub fn password_hash_get(&self, user_id: &UserId) -> Option<String> {
|
||||
self.db
|
||||
.userid_password
|
||||
.get(user_id.to_string())
|
||||
|
|
102
src/test.rs
102
src/test.rs
|
@ -1,5 +1,9 @@
|
|||
use super::*;
|
||||
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 {
|
||||
Database::try_remove("temp");
|
||||
|
@ -14,19 +18,97 @@ async fn register_login() {
|
|||
let client = setup_client();
|
||||
let mut response = client
|
||||
.post("/_matrix/client/r0/register?kind=user")
|
||||
.body(
|
||||
r#"{
|
||||
"username": "cheeky_monkey",
|
||||
"password": "ilovebananas",
|
||||
"device_id": "GHTYAJCE",
|
||||
"initial_device_display_name": "Jungle Phone",
|
||||
"inhibit_login": false
|
||||
}"#,
|
||||
)
|
||||
.body(registration_init())
|
||||
.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!(dbg!(&body["flows"]).as_array().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,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use argon2::{Config, Variant};
|
||||
|
||||
pub fn millis_since_unix_epoch() -> u64 {
|
||||
SystemTime::now()
|
||||
|
@ -39,3 +40,18 @@ pub fn random_string(length: usize) -> String {
|
|||
.take(length)
|
||||
.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