diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs --- a/services/identity/src/constants.rs +++ b/services/identity/src/constants.rs @@ -105,10 +105,11 @@ pub const DEVICE_LIST_KEY_PREFIX: &str = "devicelist-"; // device-specific attrs - pub const ATTR_DEVICE_TYPE: &str = "deviceType"; + pub const OLD_ATTR_DEVICE_TYPE: &str = "deviceType"; pub const ATTR_DEVICE_KEY_INFO: &str = "deviceKeyInfo"; pub const ATTR_CONTENT_PREKEY: &str = "contentPreKey"; pub const ATTR_NOTIF_PREKEY: &str = "notifPreKey"; + pub const ATTR_PLATFORM_DETAILS: &str = "platformDetails"; // IdentityKeyInfo constants pub const ATTR_KEY_PAYLOAD: &str = "keyPayload"; @@ -118,6 +119,12 @@ pub const ATTR_PREKEY: &str = "preKey"; pub const ATTR_PREKEY_SIGNATURE: &str = "preKeySignature"; + // PlatformDetails constants + pub const ATTR_DEVICE_TYPE: &str = "deviceType"; + pub const ATTR_CODE_VERSION: &str = "codeVersion"; + pub const ATTR_STATE_VERSION: &str = "stateVersion"; + pub const ATTR_MAJOR_DESKTOP_VERSION: &str = "majorDesktopVersion"; + // device-list-specific attrs pub const ATTR_TIMESTAMP: &str = "timestamp"; pub const ATTR_DEVICE_IDS: &str = "deviceIDs"; @@ -125,7 +132,7 @@ pub const ATTR_LAST_SIGNATURE: &str = "lastPrimarySignature"; // migration-specific attrs - pub const ATTR_CODE_VERSION: &str = "codeVersion"; + pub const OLD_ATTR_CODE_VERSION: &str = "codeVersion"; pub const ATTR_LOGIN_TIME: &str = "loginTime"; // one-time key constants 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 @@ -385,7 +385,7 @@ let user_devices = self.get_current_devices(user_id).await?; let maybe_keyserver_device = user_devices .into_iter() - .find(|device| device.device_type == GrpcDeviceType::Keyserver); + .find(|device| *device.device_type() == GrpcDeviceType::Keyserver); let Some(keyserver) = maybe_keyserver_device else { return Ok(None); @@ -444,7 +444,7 @@ 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) + .find(|device| *device.device_type() == GrpcDeviceType::Keyserver) .map(|device| device.device_id); Ok(maybe_keyserver_device_id) 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 @@ -37,13 +37,11 @@ pub struct DeviceRow { pub user_id: String, pub device_id: String, - pub device_type: DeviceType, pub device_key_info: IdentityKeyInfo, pub content_prekey: Prekey, pub notif_prekey: Prekey, + pub platform_details: PlatformDetails, - // migration-related data - pub code_version: u64, /// Timestamp of last login (access token generation) pub login_time: DateTime, } @@ -72,6 +70,14 @@ pub prekey_signature: String, } +#[derive(Clone, Debug)] +pub struct PlatformDetails { + device_type: DeviceType, + code_version: u64, + state_version: Option, + major_desktop_version: Option, +} + /// A struct representing device list update payload /// issued by the primary device. /// For the JSON payload, see [`crate::device_list::SignedDeviceList`] @@ -104,11 +110,12 @@ ); return Err(Error::InvalidFormat); } + let device_type = DeviceType::from_str_name(upload.device_type.as_str_name()) + .expect("DeviceType conversion failed. Identity client and server protos mismatch"); + let device_row = Self { user_id: user_id.into(), device_id: upload.device_id_key, - device_type: DeviceType::from_str_name(upload.device_type.as_str_name()) - .expect("DeviceType conversion failed. Identity client and server protos mismatch"), device_key_info: IdentityKeyInfo { key_payload: upload.key_payload, key_payload_signature: upload.key_payload_signature, @@ -121,11 +128,20 @@ prekey: upload.notif_prekey, prekey_signature: upload.notif_prekey_signature, }, - code_version, + platform_details: PlatformDetails { + device_type, + code_version, + state_version: None, + major_desktop_version: None, + }, login_time, }; Ok(device_row) } + + pub fn device_type(&self) -> &DeviceType { + &self.platform_details.device_type + } } impl DeviceListRow { @@ -234,16 +250,6 @@ let user_id = attrs.take_attr(ATTR_USER_ID)?; let DeviceIDAttribute(device_id) = attrs.remove(ATTR_ITEM_ID).try_into()?; - let raw_device_type: String = attrs.take_attr(ATTR_DEVICE_TYPE)?; - let device_type = - DeviceType::from_str_name(&raw_device_type).ok_or_else(|| { - DBItemError::new( - ATTR_DEVICE_TYPE.to_string(), - raw_device_type.into(), - DBItemAttributeError::InvalidValue, - ) - })?; - let device_key_info = attrs .take_attr::(ATTR_DEVICE_KEY_INFO) .and_then(IdentityKeyInfo::try_from)?; @@ -256,22 +262,46 @@ .take_attr::(ATTR_NOTIF_PREKEY) .and_then(Prekey::try_from)?; - let code_version = attrs - .remove(ATTR_CODE_VERSION) - .and_then(|attr| attr.as_n().ok().cloned()) - .and_then(|val| val.parse::().ok()) - .unwrap_or_default(); - let login_time: DateTime = attrs.take_attr(ATTR_LOGIN_TIME)?; + // New schema contains PlatformDetails attribute while legacy schema + // contains "deviceType" and "codeVersion" top-level attributes + let platform_details = match attrs + .take_attr::>(ATTR_PLATFORM_DETAILS)? + { + Some(platform_details) => platform_details, + None => { + let raw_device_type: String = attrs.take_attr(OLD_ATTR_DEVICE_TYPE)?; + let device_type = DeviceType::from_str_name(&raw_device_type) + .ok_or_else(|| { + DBItemError::new( + OLD_ATTR_DEVICE_TYPE.to_string(), + raw_device_type.into(), + DBItemAttributeError::InvalidValue, + ) + })?; + let code_version = attrs + .remove(OLD_ATTR_CODE_VERSION) + .and_then(|attr| attr.as_n().ok().cloned()) + .and_then(|val| val.parse::().ok()) + .unwrap_or_default(); + + PlatformDetails { + device_type, + code_version, + state_version: None, + major_desktop_version: None, + } + } + }; + Ok(Self { user_id, device_id, - device_type, device_key_info, content_prekey, notif_prekey, - code_version, + platform_details, login_time, }) } @@ -286,8 +316,8 @@ DeviceIDAttribute(value.device_id).into(), ), ( - ATTR_DEVICE_TYPE.to_string(), - AttributeValue::S(value.device_type.as_str_name().to_string()), + ATTR_PLATFORM_DETAILS.to_string(), + value.platform_details.into(), ), ( ATTR_DEVICE_KEY_INFO.to_string(), @@ -296,10 +326,6 @@ (ATTR_CONTENT_PREKEY.to_string(), value.content_prekey.into()), (ATTR_NOTIF_PREKEY.to_string(), value.notif_prekey.into()), // migration attributes - ( - ATTR_CODE_VERSION.to_string(), - AttributeValue::N(value.code_version.to_string()), - ), ( ATTR_LOGIN_TIME.to_string(), AttributeValue::S(value.login_time.to_rfc3339()), @@ -389,6 +415,81 @@ } } +impl From for AttributeValue { + fn from(value: PlatformDetails) -> Self { + let mut attrs = HashMap::from([ + ( + ATTR_DEVICE_TYPE.to_string(), + AttributeValue::S(value.device_type.as_str_name().to_string()), + ), + ( + ATTR_CODE_VERSION.to_string(), + AttributeValue::N(value.code_version.to_string()), + ), + ]); + if let Some(state_version) = value.state_version { + attrs.insert( + ATTR_STATE_VERSION.to_string(), + AttributeValue::N(state_version.to_string()), + ); + } + if let Some(major_desktop_version) = value.major_desktop_version { + attrs.insert( + ATTR_STATE_VERSION.to_string(), + AttributeValue::N(major_desktop_version.to_string()), + ); + } + + AttributeValue::M(attrs) + } +} + +impl TryFrom for PlatformDetails { + type Error = DBItemError; + fn try_from(mut attrs: AttributeMap) -> Result { + let raw_device_type: String = attrs.take_attr(ATTR_DEVICE_TYPE)?; + let device_type = + DeviceType::from_str_name(&raw_device_type).ok_or_else(|| { + DBItemError::new( + ATTR_DEVICE_TYPE.to_string(), + raw_device_type.into(), + DBItemAttributeError::InvalidValue, + ) + })?; + let code_version = attrs + .remove(ATTR_CODE_VERSION) + .and_then(|attr| attr.as_n().ok().cloned()) + .and_then(|val| val.parse::().ok()) + .unwrap_or_default(); + + let state_version = attrs + .remove(ATTR_STATE_VERSION) + .and_then(|attr| attr.as_n().ok().cloned()) + .and_then(|val| val.parse::().ok()); + let major_desktop_version = attrs + .remove(ATTR_MAJOR_DESKTOP_VERSION) + .and_then(|attr| attr.as_n().ok().cloned()) + .and_then(|val| val.parse::().ok()); + + Ok(Self { + device_type, + code_version, + state_version, + major_desktop_version, + }) + } +} + +impl TryFromAttribute for PlatformDetails { + fn try_from_attr( + attribute_name: impl Into, + attribute: Option, + ) -> Result { + AttributeMap::try_from_attr(attribute_name, attribute) + .and_then(PlatformDetails::try_from) + } +} + impl TryFrom for DeviceListRow { type Error = DBItemError; @@ -1489,13 +1590,16 @@ let mut mobile_devices = devices .iter() .filter(|device| { - device.device_type == DeviceType::Ios - || device.device_type == DeviceType::Android + *device.device_type() == DeviceType::Ios + || *device.device_type() == DeviceType::Android }) .collect::>(); mobile_devices.sort_by(|a, b| { - let code_version_cmp = b.code_version.cmp(&a.code_version); + let code_version_cmp = b + .platform_details + .code_version + .cmp(&a.platform_details.code_version); if code_version_cmp == Ordering::Equal { b.login_time.cmp(&a.login_time) } else { @@ -1674,7 +1778,6 @@ DeviceRow { user_id: "test".into(), device_id: id.into(), - device_type: platform, device_key_info: IdentityKeyInfo { key_payload: "".into(), key_payload_signature: "".into(), @@ -1687,7 +1790,12 @@ prekey: "".into(), prekey_signature: "".into(), }, - code_version, + platform_details: PlatformDetails { + device_type: platform, + code_version, + state_version: None, + major_desktop_version: None, + }, login_time, } }