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 @@ -18,8 +18,9 @@ error_types }; use crate::database::{ - DBDeviceTypeInt, DatabaseClient, DeviceType, KeyPayload, + DBDeviceTypeInt, DatabaseClient, DeviceType, KeyPayload }; +use crate::device_list::SignedDeviceList; use crate::error::{DeviceListError, Error as DBError}; use crate::grpc_services::authenticated::DeletePasswordUserInfo; use crate::grpc_services::protos::unauth::{ @@ -35,7 +36,7 @@ }; use crate::grpc_services::shared::get_value; use crate::grpc_utils::{ - SignedNonce, DeviceKeyUploadActions, + DeviceKeyUploadActions, RegistrationActions, SignedNonce }; use crate::nonce::generate_nonce_data; use crate::reserved_users::{ @@ -66,6 +67,7 @@ pub flattened_device_key_upload: FlattenedDeviceKeyUpload, pub user_id: Option, pub farcaster_id: Option, + pub initial_device_list: Option, } #[derive(Clone, Serialize, Deserialize)] @@ -439,6 +441,13 @@ let code_version = get_code_version(&request); let message = request.into_inner(); + // WalletAuthRequest is used for both log_in_wallet_user and register_wallet_user + if !message.initial_device_list.is_empty() { + return Err(tonic::Status::invalid_argument( + "unexpected initial device list", + )); + } + let parsed_message = parse_and_verify_siwe_message( &message.siwe_message, &message.siwe_signature, @@ -1129,7 +1138,7 @@ } fn construct_user_registration_info( - message: &impl DeviceKeyUploadActions, + message: &(impl DeviceKeyUploadActions + RegistrationActions), user_id: Option, username: String, farcaster_id: Option, @@ -1141,6 +1150,7 @@ )?, user_id, farcaster_id, + initial_device_list: message.get_and_verify_initial_device_list()?, }) } diff --git a/services/identity/src/device_list.rs b/services/identity/src/device_list.rs --- a/services/identity/src/device_list.rs +++ b/services/identity/src/device_list.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Duration, Utc}; -use std::collections::HashSet; +use std::{collections::HashSet, str::FromStr}; use tracing::{debug, error, warn}; use crate::{ @@ -20,7 +20,7 @@ /// Signed device list payload that is serializable to JSON. /// For the DDB payload, see [`DeviceListUpdate`] -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct SignedDeviceList { /// JSON-stringified [`RawDeviceList`] @@ -88,13 +88,20 @@ impl TryFrom for SignedDeviceList { type Error = tonic::Status; fn try_from(request: UpdateDeviceListRequest) -> Result { - serde_json::from_str(&request.new_device_list).map_err(|err| { + request.new_device_list.parse().map_err(|err| { warn!("Failed to deserialize device list update: {}", err); tonic::Status::invalid_argument("invalid device list payload") }) } } +impl FromStr for SignedDeviceList { + type Err = serde_json::Error; + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} + impl TryFrom for DeviceListUpdate { type Error = tonic::Status; fn try_from(signed_list: SignedDeviceList) -> Result { diff --git a/services/identity/src/grpc_utils.rs b/services/identity/src/grpc_utils.rs --- a/services/identity/src/grpc_utils.rs +++ b/services/identity/src/grpc_utils.rs @@ -2,11 +2,12 @@ use ed25519_dalek::{PublicKey, Signature, Verifier}; use serde::Deserialize; use tonic::Status; +use tracing::warn; use crate::{ - database::DeviceRow, - ddb_utils::DBIdentity, - ddb_utils::Identifier as DBIdentifier, + database::{DeviceListUpdate, DeviceRow, KeyPayload}, + ddb_utils::{DBIdentity, Identifier as DBIdentifier}, + device_list::SignedDeviceList, grpc_services::protos::{ auth::{EthereumIdentity, Identity, InboundKeyInfo, OutboundKeyInfo}, unauth::{ @@ -249,6 +250,68 @@ } } +/// Common functionality for registration request messages +trait RegistrationData { + fn initial_device_list(&self) -> &str; +} + +impl RegistrationData for RegistrationStartRequest { + fn initial_device_list(&self) -> &str { + &self.initial_device_list + } +} +impl RegistrationData for ReservedRegistrationStartRequest { + fn initial_device_list(&self) -> &str { + &self.initial_device_list + } +} +impl RegistrationData for WalletAuthRequest { + fn initial_device_list(&self) -> &str { + &self.initial_device_list + } +} +impl RegistrationData for ReservedWalletRegistrationRequest { + fn initial_device_list(&self) -> &str { + &self.initial_device_list + } +} + +/// Similar to `[DeviceKeyUploadActions]` but only for registration requests +pub trait RegistrationActions { + fn get_and_verify_initial_device_list( + &self, + ) -> Result, tonic::Status>; +} + +impl RegistrationActions for T { + fn get_and_verify_initial_device_list( + &self, + ) -> Result, tonic::Status> { + let payload = self.initial_device_list(); + if payload.is_empty() { + return Ok(None); + } + let signed_list: SignedDeviceList = payload.parse().map_err(|err| { + warn!("Failed to deserialize initial device list: {}", err); + tonic::Status::invalid_argument("invalid device list payload") + })?; + + let key_info = self + .payload()? + .parse::() + .map_err(|_| tonic::Status::invalid_argument("malformed payload"))?; + let primary_device_id = key_info.primary_identity_public_keys.ed25519; + + let update_payload = DeviceListUpdate::try_from(signed_list.clone())?; + crate::device_list::verify_initial_device_list( + &update_payload, + &primary_device_id, + )?; + + Ok(Some(signed_list)) + } +} + impl From for Identity { fn from(value: DBIdentity) -> Self { match value.identifier {