diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use siwe::eip55; use tonic::Response; -use tracing::{debug, error, warn}; +use tracing::{debug, error, info, warn}; // Workspace crate imports use crate::config::CONFIG; @@ -66,6 +66,7 @@ pub user_id: String, pub flattened_device_key_upload: FlattenedDeviceKeyUpload, pub opaque_server_login: comm_opaque2::server::Login, + pub device_to_remove: Option, } #[derive(Clone, Serialize, Deserialize)] @@ -265,7 +266,7 @@ ) -> Result, tonic::Status> { let message = request.into_inner(); - debug!("Attempting to login user: {:?}", &message.username); + debug!("Attempting to log in user: {:?}", &message.username); let user_id_and_password_file = self .client .get_user_id_and_password_file_from_username(&message.username) @@ -296,6 +297,18 @@ return Err(tonic::Status::not_found("user not found")); }; + let flattened_device_key_upload = + construct_flattened_device_key_upload(&message)?; + + let maybe_device_to_remove = self + .get_keyserver_device_to_remove( + &user_id, + &flattened_device_key_upload.device_id_key, + &message.force, + &flattened_device_key_upload.device_type, + ) + .await?; + let mut server_login = comm_opaque2::server::Login::new(); let server_response = server_login .start( @@ -306,8 +319,12 @@ ) .map_err(protocol_error_to_grpc_status)?; - let login_state = - construct_user_login_info(&message, user_id, server_login)?; + let login_state = construct_user_login_info( + user_id, + server_login, + flattened_device_key_upload, + maybe_device_to_remove, + )?; let session_id = self .client @@ -340,6 +357,14 @@ .finish(&message.opaque_login_upload) .map_err(protocol_error_to_grpc_status)?; + if let Some(device_to_remove) = state.device_to_remove { + self + .client + .remove_device(state.user_id.clone(), device_to_remove) + .await + .map_err(handle_db_error)?; + } + let login_time = chrono::Utc::now(); self .client @@ -896,6 +921,45 @@ }; Ok(()) } + + async fn get_keyserver_device_to_remove( + &self, + user_id: &str, + new_keyserver_device_id: &str, + force: &Option, + device_type: &DeviceType, + ) -> Result, tonic::Status> { + if device_type != &DeviceType::Keyserver { + return Ok(None); + } + + let maybe_keyserver_device_id = self + .client + .get_keyserver_device_id_for_user(user_id) + .await + .map_err(handle_db_error)?; + + let Some(existing_keyserver_device_id) = maybe_keyserver_device_id else { + return Ok(None); + }; + + if new_keyserver_device_id == existing_keyserver_device_id { + return Ok(None); + } + + match force { + Some(true) => { + info!( + "keyserver {} will be removed from the device list", + existing_keyserver_device_id + ); + Ok(Some(existing_keyserver_device_id)) + } + _ => Err(tonic::Status::already_exists( + "user already has a keyserver", + )), + } + } } pub fn handle_db_error(db_error: DBError) -> tonic::Status { @@ -932,16 +996,16 @@ } fn construct_user_login_info( - message: &impl DeviceKeyUploadActions, user_id: String, opaque_server_login: comm_opaque2::server::Login, + flattened_device_key_upload: FlattenedDeviceKeyUpload, + device_to_remove: Option, ) -> Result { Ok(UserLoginInfo { user_id, - flattened_device_key_upload: construct_flattened_device_key_upload( - message, - )?, + flattened_device_key_upload, opaque_server_login, + device_to_remove, }) } diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs --- a/services/identity/src/database.rs +++ b/services/identity/src/database.rs @@ -390,6 +390,21 @@ Ok(Some(outbound_payload)) } + pub async fn get_keyserver_device_id_for_user( + &self, + user_id: &str, + ) -> Result, Error> { + use crate::grpc_services::protos::unauth::DeviceType as GrpcDeviceType; + + let user_devices = self.get_current_devices(user_id).await?; + let maybe_keyserver_device_id = user_devices + .into_iter() + .find(|device| device.device_type == GrpcDeviceType::Keyserver) + .map(|device| device.device_id); + + Ok(maybe_keyserver_device_id) + } + /// Will "mint" a single one-time key by attempting to successfully delete a /// key pub async fn get_one_time_key( diff --git a/shared/protos/identity_unauth.proto b/shared/protos/identity_unauth.proto --- a/shared/protos/identity_unauth.proto +++ b/shared/protos/identity_unauth.proto @@ -164,6 +164,10 @@ // Information specific to a user's device needed to open a new channel of // communication with this user DeviceKeyUpload device_key_upload = 3; + // If set to true, the user's existing keyserver will be deleted from the + // identity service and replaced with this one. This field has no effect if + // the device is not a keyserver + optional bool force = 4; } message OpaqueLoginFinishRequest { diff --git a/web/protobufs/identity-unauth-structs.cjs b/web/protobufs/identity-unauth-structs.cjs --- a/web/protobufs/identity-unauth-structs.cjs +++ b/web/protobufs/identity-unauth-structs.cjs @@ -2457,7 +2457,8 @@ var f, obj = { username: jspb.Message.getFieldWithDefault(msg, 1, ""), opaqueLoginRequest: msg.getOpaqueLoginRequest_asB64(), - deviceKeyUpload: (f = msg.getDeviceKeyUpload()) && proto.identity.unauth.DeviceKeyUpload.toObject(includeInstance, f) + deviceKeyUpload: (f = msg.getDeviceKeyUpload()) && proto.identity.unauth.DeviceKeyUpload.toObject(includeInstance, f), + force: jspb.Message.getBooleanFieldWithDefault(msg, 4, false) }; if (includeInstance) { @@ -2507,6 +2508,10 @@ reader.readMessage(value,proto.identity.unauth.DeviceKeyUpload.deserializeBinaryFromReader); msg.setDeviceKeyUpload(value); break; + case 4: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setForce(value); + break; default: reader.skipField(); break; @@ -2558,6 +2563,13 @@ proto.identity.unauth.DeviceKeyUpload.serializeBinaryToWriter ); } + f = /** @type {boolean} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeBool( + 4, + f + ); + } }; @@ -2658,6 +2670,42 @@ }; +/** + * optional bool force = 4; + * @return {boolean} + */ +proto.identity.unauth.OpaqueLoginStartRequest.prototype.getForce = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 4, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.identity.unauth.OpaqueLoginStartRequest} returns this + */ +proto.identity.unauth.OpaqueLoginStartRequest.prototype.setForce = function(value) { + return jspb.Message.setField(this, 4, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.identity.unauth.OpaqueLoginStartRequest} returns this + */ +proto.identity.unauth.OpaqueLoginStartRequest.prototype.clearForce = function() { + return jspb.Message.setField(this, 4, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.identity.unauth.OpaqueLoginStartRequest.prototype.hasForce = function() { + return jspb.Message.getField(this, 4) != null; +}; + + diff --git a/web/protobufs/identity-unauth-structs.cjs.flow b/web/protobufs/identity-unauth-structs.cjs.flow --- a/web/protobufs/identity-unauth-structs.cjs.flow +++ b/web/protobufs/identity-unauth-structs.cjs.flow @@ -250,6 +250,11 @@ hasDeviceKeyUpload(): boolean; clearDeviceKeyUpload(): OpaqueLoginStartRequest; + getForce(): boolean; + setForce(value: boolean): OpaqueLoginStartRequest; + hasForce(): boolean; + clearForce(): OpaqueLoginStartRequest; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): OpaqueLoginStartRequestObject; static toObject(includeInstance: boolean, msg: OpaqueLoginStartRequest): OpaqueLoginStartRequestObject; @@ -262,6 +267,7 @@ username: string, opaqueLoginRequest: Uint8Array | string, deviceKeyUpload?: DeviceKeyUploadObject, + force?: boolean, }; declare export class OpaqueLoginFinishRequest extends Message {