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 @@ -141,6 +141,12 @@ message.username.clone(), message.farcaster_id.clone(), )?; + self + .check_device_id_taken( + ®istration_state.flattened_device_key_upload, + None, + ) + .await?; let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start( @@ -197,6 +203,12 @@ message.username.clone(), None, )?; + self + .check_device_id_taken( + ®istration_state.flattened_device_key_upload, + None, + ) + .await?; let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start( @@ -322,6 +334,9 @@ let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; let maybe_device_to_remove = self .get_keyserver_device_to_remove( @@ -484,6 +499,10 @@ return Err(tonic::Status::not_found("user not found")); }; + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; + self .client .add_user_device( @@ -573,6 +592,9 @@ let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; + self + .check_device_id_taken(&flattened_device_key_upload, None) + .await?; let login_time = chrono::Utc::now(); @@ -658,6 +680,9 @@ let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; + self + .check_device_id_taken(&flattened_device_key_upload, None) + .await?; let initial_device_list = message.get_and_verify_initial_device_list()?; let social_proof = @@ -722,6 +747,10 @@ let nonce = challenge_response.verify_and_get_nonce(&device_id)?; self.verify_and_remove_nonce(&nonce).await?; + self + .check_device_id_taken(&flattened_device_key_upload, Some(&user_id)) + .await?; + let user_identity = self .client .get_user_identity(&user_id) @@ -1048,6 +1077,38 @@ Ok(()) } + async fn check_device_id_taken( + &self, + key_upload: &FlattenedDeviceKeyUpload, + requesting_user_id: Option<&str>, + ) -> Result<(), tonic::Status> { + let device_id = key_upload.device_id_key.as_str(); + let Some(existing_device_user_id) = self + .client + .find_user_id_for_device(device_id) + .await + .map_err(handle_db_error)? + else { + // device ID doesn't exist + return Ok(()); + }; + + // allow already-existing device ID for the same user + match requesting_user_id { + Some(user_id) if user_id == existing_device_user_id => { + debug!( + "Found already-existing device {} for user {}", + device_id, user_id + ); + Ok(()) + } + _ => { + warn!("Device ID already exists: {device_id}"); + Err(tonic::Status::already_exists("device_already_exists")) + } + } + } + async fn verify_and_remove_nonce( &self, nonce: &str, diff --git a/services/identity/src/database/device_list.rs b/services/identity/src/database/device_list.rs --- a/services/identity/src/database/device_list.rs +++ b/services/identity/src/database/device_list.rs @@ -746,11 +746,13 @@ Ok(user_devices_keys) } + /// Find owner's user ID for given device ID. Useful for finding + /// devices table partition key. #[tracing::instrument(skip_all)] - pub async fn find_device_by_id( + pub async fn find_user_id_for_device( &self, device_id: &str, - ) -> Result, Error> { + ) -> Result, Error> { let response = self .client .query() @@ -785,15 +787,23 @@ return Err(Error::IllegalState); } - let Some(user_id) = results + let user_id = results .pop() .map(|mut attrs| attrs.take_attr::(devices_table::ATTR_USER_ID)) - .transpose()? - else { + .transpose()?; + + Ok(user_id) + } + + #[tracing::instrument(skip_all)] + pub async fn find_device_by_id( + &self, + device_id: &str, + ) -> Result, Error> { + let Some(user_id) = self.find_user_id_for_device(device_id).await? else { debug!("No device found with ID: {}", device_id); return Ok(None); }; - self.get_device_data(user_id, device_id).await }