feat: user interactive authentication
This commit is contained in:
		
							parent
							
								
									88d091fca1
								
							
						
					
					
						commit
						c85d363d71
					
				
					 3 changed files with 249 additions and 15 deletions
				
			
		|  | @ -62,9 +62,9 @@ use serde_json::{json, value::RawValue}; | |||
| 
 | ||||
| const GUEST_NAME_LENGTH: usize = 10; | ||||
| const DEVICE_ID_LENGTH: usize = 10; | ||||
| const SESSION_ID_LENGTH: usize = 256; | ||||
| const TOKEN_LENGTH: usize = 256; | ||||
| const MXC_LENGTH: usize = 256; | ||||
| const SESSION_ID_LENGTH: usize = 256; | ||||
| 
 | ||||
| #[get("/_matrix/client/versions")] | ||||
| pub fn get_supported_versions_route() -> MatrixResult<get_supported_versions::Response> { | ||||
|  | @ -117,18 +117,6 @@ pub fn register_route( | |||
|     db: State<'_, Database>, | ||||
|     body: Ruma<register::Request>, | ||||
| ) -> MatrixResult<register::Response, UiaaResponse> { | ||||
|     if body.auth.is_none() { | ||||
|         return MatrixResult(Err(UiaaResponse::AuthResponse(UiaaInfo { | ||||
|             flows: vec![AuthFlow { | ||||
|                 stages: vec!["m.login.dummy".to_owned()], | ||||
|             }], | ||||
|             completed: vec![], | ||||
|             params: RawValue::from_string("{}".to_owned()).unwrap(), | ||||
|             session: Some(utils::random_string(SESSION_ID_LENGTH)), | ||||
|             auth_error: None, | ||||
|         }))); | ||||
|     } | ||||
| 
 | ||||
|     // Validate user id
 | ||||
|     let user_id = match UserId::parse_with_server_name( | ||||
|         body.username | ||||
|  | @ -161,6 +149,32 @@ pub fn register_route( | |||
|         }))); | ||||
|     } | ||||
| 
 | ||||
|     // UIAA
 | ||||
|     let uiaainfo = UiaaInfo { | ||||
|         flows: vec![AuthFlow { | ||||
|             stages: vec!["m.login.dummy".to_owned()], | ||||
|         }], | ||||
|         completed: Vec::new(), | ||||
|         params: Default::default(), | ||||
|         session: Some(utils::random_string(SESSION_ID_LENGTH)), | ||||
|         auth_error: None, | ||||
|     }; | ||||
| 
 | ||||
|     if let Some(auth) = &body.auth { | ||||
|         let (worked, uiaainfo) = db | ||||
|             .uiaa | ||||
|             .try_auth(&user_id, &"".to_owned(), auth, &uiaainfo, &db.users, &db.globals) | ||||
|             .unwrap(); | ||||
|         if !worked { | ||||
|             return MatrixResult(Err(UiaaResponse::AuthResponse(uiaainfo))); | ||||
|         } | ||||
|         // Success!
 | ||||
|     } else { | ||||
|         db.uiaa.create(&user_id, &"".to_owned(), &uiaainfo).unwrap(); | ||||
| 
 | ||||
|         return MatrixResult(Err(UiaaResponse::AuthResponse(uiaainfo))); | ||||
|     } | ||||
| 
 | ||||
|     let password = body.password.clone().unwrap_or_default(); | ||||
| 
 | ||||
|     if let Ok(hash) = utils::calculate_hash(&password) { | ||||
|  | @ -2867,8 +2881,35 @@ pub fn delete_device_route( | |||
|     db: State<'_, Database>, | ||||
|     body: Ruma<delete_device::Request>, | ||||
|     device_id: DeviceId, | ||||
| ) -> MatrixResult<delete_device::Response> { | ||||
| ) -> MatrixResult<delete_device::Response, UiaaResponse> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     // UIAA
 | ||||
|     let uiaainfo = UiaaInfo { | ||||
|         flows: vec![AuthFlow { | ||||
|             stages: vec!["m.login.password".to_owned()], | ||||
|         }], | ||||
|         completed: Vec::new(), | ||||
|         params: Default::default(), | ||||
|         session: Some(utils::random_string(SESSION_ID_LENGTH)), | ||||
|         auth_error: None, | ||||
|     }; | ||||
| 
 | ||||
|     if let Some(auth) = &body.auth { | ||||
|         let (worked, uiaainfo) = db | ||||
|             .uiaa | ||||
|             .try_auth(&user_id, &"".to_owned(), auth, &uiaainfo, &db.users, &db.globals) | ||||
|             .unwrap(); | ||||
|         if !worked { | ||||
|             return MatrixResult(Err(UiaaResponse::AuthResponse(uiaainfo))); | ||||
|         } | ||||
|         // Success!
 | ||||
|     } else { | ||||
|         db.uiaa.create(&user_id, &"".to_owned(), &uiaainfo).unwrap(); | ||||
| 
 | ||||
|         return MatrixResult(Err(UiaaResponse::AuthResponse(uiaainfo))); | ||||
|     } | ||||
| 
 | ||||
|     db.users.remove_device(&user_id, &device_id).unwrap(); | ||||
| 
 | ||||
|     MatrixResult(Ok(delete_device::Response)) | ||||
|  | @ -2878,8 +2919,35 @@ pub fn delete_device_route( | |||
| pub fn delete_devices_route( | ||||
|     db: State<'_, Database>, | ||||
|     body: Ruma<delete_devices::Request>, | ||||
| ) -> MatrixResult<delete_devices::Response> { | ||||
| ) -> MatrixResult<delete_devices::Response, UiaaResponse> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     // UIAA
 | ||||
|     let uiaainfo = UiaaInfo { | ||||
|         flows: vec![AuthFlow { | ||||
|             stages: vec!["m.login.password".to_owned()], | ||||
|         }], | ||||
|         completed: Vec::new(), | ||||
|         params: Default::default(), | ||||
|         session: Some(utils::random_string(SESSION_ID_LENGTH)), | ||||
|         auth_error: None, | ||||
|     }; | ||||
| 
 | ||||
|     if let Some(auth) = &body.auth { | ||||
|         let (worked, uiaainfo) = db | ||||
|             .uiaa | ||||
|             .try_auth(&user_id, &"".to_owned(), auth, &uiaainfo, &db.users, &db.globals) | ||||
|             .unwrap(); | ||||
|         if !worked { | ||||
|             return MatrixResult(Err(UiaaResponse::AuthResponse(uiaainfo))); | ||||
|         } | ||||
|         // Success!
 | ||||
|     } else { | ||||
|         db.uiaa.create(&user_id, &"".to_owned(), &uiaainfo).unwrap(); | ||||
| 
 | ||||
|         return MatrixResult(Err(UiaaResponse::AuthResponse(uiaainfo))); | ||||
|     } | ||||
| 
 | ||||
|     for device_id in &body.devices { | ||||
|         db.users.remove_device(&user_id, &device_id).unwrap() | ||||
|     } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ pub(self) mod global_edus; | |||
| pub(self) mod globals; | ||||
| pub(self) mod media; | ||||
| pub(self) mod rooms; | ||||
| pub(self) mod uiaa; | ||||
| pub(self) mod users; | ||||
| 
 | ||||
| use directories::ProjectDirs; | ||||
|  | @ -13,6 +14,7 @@ use rocket::Config; | |||
| pub struct Database { | ||||
|     pub globals: globals::Globals, | ||||
|     pub users: users::Users, | ||||
|     pub uiaa: uiaa::Uiaa, | ||||
|     pub rooms: rooms::Rooms, | ||||
|     pub account_data: account_data::AccountData, | ||||
|     pub global_edus: global_edus::GlobalEdus, | ||||
|  | @ -66,6 +68,9 @@ impl Database { | |||
|                 devicekeychangeid_userid: db.open_tree("devicekeychangeid_userid").unwrap(), | ||||
|                 todeviceid_events: db.open_tree("todeviceid_events").unwrap(), | ||||
|             }, | ||||
|             uiaa: uiaa::Uiaa { | ||||
|                 userdeviceid_uiaainfo: db.open_tree("userdeviceid_uiaainfo").unwrap(), | ||||
|             }, | ||||
|             rooms: rooms::Rooms { | ||||
|                 edus: rooms::RoomEdus { | ||||
|                     roomuserid_lastread: db.open_tree("roomuserid_lastread").unwrap(), // "Private" read receipt
 | ||||
|  |  | |||
							
								
								
									
										161
									
								
								src/database/uiaa.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								src/database/uiaa.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,161 @@ | |||
| use crate::{utils, Error, Result}; | ||||
| use js_int::UInt; | ||||
| use log::debug; | ||||
| use ruma::{ | ||||
|     api::client::{ | ||||
|         error::ErrorKind, | ||||
|         r0::{ | ||||
|             device::Device, | ||||
|             keys::{AlgorithmAndDeviceId, DeviceKeys, KeyAlgorithm, OneTimeKey}, | ||||
|             uiaa::{AuthData, AuthFlow, UiaaInfo, UiaaResponse}, | ||||
|         }, | ||||
|     }, | ||||
|     events::{to_device::AnyToDeviceEvent, EventJson, EventType}, | ||||
|     identifiers::{DeviceId, UserId}, | ||||
| }; | ||||
| use serde_json::value::RawValue; | ||||
| use std::{collections::BTreeMap, convert::TryFrom, time::SystemTime}; | ||||
| 
 | ||||
| pub struct Uiaa { | ||||
|     pub(super) userdeviceid_uiaainfo: sled::Tree, // User-interactive authentication
 | ||||
| } | ||||
| 
 | ||||
| impl Uiaa { | ||||
|     /// Creates a new Uiaa session. Make sure the session token is unique.
 | ||||
|     pub fn create(&self, user_id: &UserId, device_id: &str, uiaainfo: &UiaaInfo) -> Result<()> { | ||||
|         self.update_uiaa_session(user_id, device_id, Some(uiaainfo)) | ||||
|     } | ||||
| 
 | ||||
|     pub fn try_auth( | ||||
|         &self, | ||||
|         user_id: &UserId, | ||||
|         device_id: &DeviceId, | ||||
|         auth: &AuthData, | ||||
|         uiaainfo: &UiaaInfo, | ||||
|         users: &super::users::Users, | ||||
|         globals: &super::globals::Globals, | ||||
|     ) -> Result<(bool, UiaaInfo)> { | ||||
|         if let AuthData::DirectRequest { | ||||
|             kind, | ||||
|             session, | ||||
|             auth_parameters, | ||||
|         } = &auth | ||||
|         { | ||||
|             let mut uiaainfo = session | ||||
|                 .as_ref() | ||||
|                 .map(|session| { | ||||
|                     Ok::<_, Error>(self.get_uiaa_session(&user_id, &"".to_owned(), session)?) | ||||
|                 }) | ||||
|                 .unwrap_or(Ok(uiaainfo.clone()))?; | ||||
| 
 | ||||
|             // Find out what the user completed
 | ||||
|             match &**kind { | ||||
|                 "m.login.password" => { | ||||
|                     if auth_parameters["identifier"]["type"] != "m.id.user" { | ||||
|                         panic!("identifier not supported"); | ||||
|                     } | ||||
| 
 | ||||
|                     let user_id = UserId::parse_with_server_name( | ||||
|                         auth_parameters["identifier"]["user"].as_str().unwrap(), | ||||
|                         globals.server_name(), | ||||
|                     )?; | ||||
|                     let password = auth_parameters["password"].as_str().unwrap(); | ||||
| 
 | ||||
|                     // Check if password is correct
 | ||||
|                     if let Some(hash) = users.password_hash(&user_id)? { | ||||
|                         let hash_matches = | ||||
|                             argon2::verify_encoded(&hash, password.as_bytes()).unwrap_or(false); | ||||
| 
 | ||||
|                         if !hash_matches { | ||||
|                             debug!("Invalid password."); | ||||
|                             uiaainfo.auth_error = Some(ruma::api::client::error::ErrorBody { | ||||
|                                 kind: ErrorKind::Forbidden, | ||||
|                                 message: "Invalid username or password.".to_owned(), | ||||
|                             }); | ||||
|                             return Ok((false, uiaainfo)); | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     // Password was correct! Let's add it to `completed`
 | ||||
|                     uiaainfo.completed.push("m.login.password".to_owned()); | ||||
|                 } | ||||
|                 "m.login.dummy" => { | ||||
|                     uiaainfo.completed.push("m.login.dummy".to_owned()); | ||||
|                 } | ||||
|                 k => panic!("type not supported: {}", k), | ||||
|             } | ||||
| 
 | ||||
|             // Check if a flow now succeeds
 | ||||
|             let mut completed = false; | ||||
|             'flows: for flow in &mut uiaainfo.flows { | ||||
|                 for stage in &flow.stages { | ||||
|                     if !uiaainfo.completed.contains(stage) { | ||||
|                         continue 'flows; | ||||
|                     } | ||||
|                 } | ||||
|                 // We didn't break, so this flow succeeded!
 | ||||
|                 completed = true; | ||||
|             } | ||||
| 
 | ||||
|             if !completed { | ||||
|                 self.update_uiaa_session(user_id, device_id, Some(&uiaainfo))?; | ||||
|                 return Ok((false, uiaainfo)); | ||||
|             } | ||||
| 
 | ||||
|             // UIAA was successful! Remove this session and return true
 | ||||
|             self.update_uiaa_session(user_id, device_id, None)?; | ||||
|             return Ok((true, uiaainfo)); | ||||
|         } else { | ||||
|             panic!("FallbackAcknowledgement is not supported yet"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn update_uiaa_session( | ||||
|         &self, | ||||
|         user_id: &UserId, | ||||
|         device_id: &str, | ||||
|         uiaainfo: Option<&UiaaInfo>, | ||||
|     ) -> Result<()> { | ||||
|         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||
|         userdeviceid.push(0xff); | ||||
|         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||
| 
 | ||||
|         if let Some(uiaainfo) = uiaainfo { | ||||
|             self.userdeviceid_uiaainfo | ||||
|                 .insert(&userdeviceid, &*serde_json::to_string(&uiaainfo)?)?; | ||||
|         } else { | ||||
|             self.userdeviceid_uiaainfo.remove(&userdeviceid)?; | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     fn get_uiaa_session( | ||||
|         &self, | ||||
|         user_id: &UserId, | ||||
|         device_id: &str, | ||||
|         session: &str, | ||||
|     ) -> Result<UiaaInfo> { | ||||
|         let mut userdeviceid = user_id.to_string().as_bytes().to_vec(); | ||||
|         userdeviceid.push(0xff); | ||||
|         userdeviceid.extend_from_slice(device_id.as_bytes()); | ||||
| 
 | ||||
|         let uiaainfo = serde_json::from_slice::<UiaaInfo>( | ||||
|             &self | ||||
|                 .userdeviceid_uiaainfo | ||||
|                 .get(&userdeviceid)? | ||||
|                 .ok_or(Error::BadRequest("session does not exist"))?, | ||||
|         )?; | ||||
| 
 | ||||
|         if uiaainfo | ||||
|             .session | ||||
|             .as_ref() | ||||
|             .filter(|&s| s == session) | ||||
|             .is_none() | ||||
|         { | ||||
|             return Err(Error::BadRequest("wrong session token")); | ||||
|         } | ||||
| 
 | ||||
|         Ok(uiaainfo) | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in a new issue