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>
This commit is contained in:
		
							parent
							
								
									abcce95dd8
								
							
						
					
					
						commit
						fa9e127a1e
					
				
					 6 changed files with 148 additions and 21 deletions
				
			
		
							
								
								
									
										15
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							|  | @ -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 { | ||||
|  | @ -930,4 +945,4 @@ pub fn options_route(_segments: PathBuf) -> MatrixResult<create_message_event::R | |||
|         message: "This is the options route.".to_owned(), | ||||
|         status_code: http::StatusCode::OK, | ||||
|     })) | ||||
| } | ||||
| } | ||||
|  | @ -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 a new issue