diff --git a/src/client_server.rs b/src/client_server.rs index 057b473..03be7bf 100644 --- a/src/client_server.rs +++ b/src/client_server.rs @@ -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 { @@ -117,18 +117,6 @@ pub fn register_route( db: State<'_, Database>, body: Ruma, ) -> MatrixResult { - 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, device_id: DeviceId, -) -> MatrixResult { +) -> MatrixResult { 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, -) -> MatrixResult { +) -> MatrixResult { 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() } diff --git a/src/database.rs b/src/database.rs index dc78ba9..492f880 100644 --- a/src/database.rs +++ b/src/database.rs @@ -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 diff --git a/src/database/uiaa.rs b/src/database/uiaa.rs new file mode 100644 index 0000000..f1de476 --- /dev/null +++ b/src/database/uiaa.rs @@ -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 { + 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::( + &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) + } +}