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,10 +1,109 @@ use chrono::{DateTime, Duration, Utc}; use std::collections::HashSet; +use tracing::{error, warn}; use crate::{ - constants::DEVICE_LIST_TIMESTAMP_VALID_FOR, error::DeviceListError, + constants::DEVICE_LIST_TIMESTAMP_VALID_FOR, + database::{DeviceListRow, DeviceListUpdate}, + ddb_utils::DateTimeExt, + error::DeviceListError, + grpc_services::protos::auth::UpdateDeviceListRequest, }; +// raw device list that can be serialized to JSON (and then signed in the future) +#[derive(serde::Serialize, serde::Deserialize)] +pub struct RawDeviceList { + devices: Vec, + timestamp: i64, +} + +impl From for RawDeviceList { + fn from(row: DeviceListRow) -> Self { + Self { + devices: row.device_ids, + timestamp: row.timestamp.timestamp_millis(), + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SignedDeviceList { + /// JSON-stringified [`RawDeviceList`] + raw_device_list: String, + /// Current primary device signature. + /// NOTE: Present only when the payload is received from primary device. + /// It's `None` for Identity-generated device-lists + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + cur_primary_signature: Option, + /// Previous primary device signature. Present only + /// if primary device has changed since last update. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + last_primary_signature: Option, +} + +impl SignedDeviceList { + /// Serialize (and sign in the future) a [`RawDeviceList`] + pub fn try_from_raw(raw: RawDeviceList) -> Result { + let stringified_list = serde_json::to_string(&raw).map_err(|err| { + error!("Failed to serialize raw device list: {}", err); + tonic::Status::failed_precondition("unexpected error") + })?; + + Ok(Self { + raw_device_list: stringified_list, + cur_primary_signature: None, + last_primary_signature: None, + }) + } + + fn as_raw(&self) -> Result { + // The device list payload is sent as an escaped JSON payload. + // Escaped double quotes need to be trimmed before attempting to deserialize + serde_json::from_str(&self.raw_device_list.replace(r#"\""#, r#"""#)) + .map_err(|err| { + warn!("Failed to deserialize raw device list: {}", err); + tonic::Status::invalid_argument("invalid device list payload") + }) + } + + /// Serializes the signed device list to a JSON string + pub fn as_json_string(&self) -> Result { + serde_json::to_string(self).map_err(|err| { + error!("Failed to serialize device list updates: {}", err); + tonic::Status::failed_precondition("unexpected error") + }) + } +} + +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| { + warn!("Failed to deserialize device list update: {}", err); + tonic::Status::invalid_argument("invalid device list payload") + }) + } +} + +impl TryFrom for DeviceListUpdate { + type Error = tonic::Status; + fn try_from(signed_list: SignedDeviceList) -> Result { + let RawDeviceList { + devices, + timestamp: raw_timestamp, + } = signed_list.as_raw()?; + let timestamp = DateTime::::from_utc_timestamp_millis(raw_timestamp) + .ok_or_else(|| { + error!("Failed to parse RawDeviceList timestamp!"); + tonic::Status::invalid_argument("invalid timestamp") + })?; + Ok(DeviceListUpdate::new(devices, timestamp)) + } +} + /// Returns `true` if given timestamp is valid. The timestamp is considered /// valid under the following condition: /// - `new_timestamp` is greater than `previous_timestamp` (if provided) @@ -191,6 +290,114 @@ mod tests { use super::*; + #[test] + fn deserialize_device_list_signature() { + let payload_with_signature = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}","curPrimarySignature":"foo"}"#; + let payload_without_signatures = r#"{"rawDeviceList":"{\"devices\":[\"device1\",\"device2\"],\"timestamp\":222222222}"}"#; + + let list_with_signature: SignedDeviceList = + serde_json::from_str(payload_with_signature).unwrap(); + let list_without_signatures: SignedDeviceList = + serde_json::from_str(payload_without_signatures).unwrap(); + + assert_eq!( + list_with_signature.cur_primary_signature, + Some("foo".to_string()) + ); + assert!(list_with_signature.last_primary_signature.is_none()); + + assert!(list_without_signatures.cur_primary_signature.is_none()); + assert!(list_without_signatures.last_primary_signature.is_none()); + } + + #[test] + fn serialize_device_list_signatures() { + let raw_list = r#"{"devices":["device1"],"timestamp":111111111}"#; + + let expected_payload_without_signatures = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}"}"#; + let device_list_without_signature = SignedDeviceList { + raw_device_list: raw_list.to_string(), + cur_primary_signature: None, + last_primary_signature: None, + }; + assert_eq!( + device_list_without_signature.as_json_string().unwrap(), + expected_payload_without_signatures + ); + + let expected_payload_with_signature = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}","curPrimarySignature":"foo"}"#; + let device_list_with_cur_signature = SignedDeviceList { + raw_device_list: raw_list.to_string(), + cur_primary_signature: Some("foo".to_string()), + last_primary_signature: None, + }; + assert_eq!( + device_list_with_cur_signature.as_json_string().unwrap(), + expected_payload_with_signature + ); + } + + #[test] + fn serialize_device_list_updates() { + let raw_updates = vec![ + RawDeviceList { + devices: vec!["device1".into()], + timestamp: 111111111, + }, + RawDeviceList { + devices: vec!["device1".into(), "device2".into()], + timestamp: 222222222, + }, + ]; + + let expected_raw_list1 = r#"{"devices":["device1"],"timestamp":111111111}"#; + let expected_raw_list2 = + r#"{"devices":["device1","device2"],"timestamp":222222222}"#; + + let signed_updates = raw_updates + .into_iter() + .map(SignedDeviceList::try_from_raw) + .collect::, _>>() + .expect("signing device list updates failed"); + + assert_eq!(signed_updates[0].raw_device_list, expected_raw_list1); + assert_eq!(signed_updates[1].raw_device_list, expected_raw_list2); + + let stringified_updates = signed_updates + .iter() + .map(serde_json::to_string) + .collect::, _>>() + .expect("serialize signed device lists failed"); + + let expected_stringified_list1 = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}"}"#; + let expected_stringified_list2 = r#"{"rawDeviceList":"{\"devices\":[\"device1\",\"device2\"],\"timestamp\":222222222}"}"#; + + assert_eq!(stringified_updates[0], expected_stringified_list1); + assert_eq!(stringified_updates[1], expected_stringified_list2); + } + + #[test] + fn deserialize_device_list_update() { + let raw_payload = r#"{"rawDeviceList":"{\"devices\":[\"device1\",\"device2\"],\"timestamp\":123456789}"}"#; + let request = UpdateDeviceListRequest { + new_device_list: raw_payload.to_string(), + }; + + let signed_list = SignedDeviceList::try_from(request) + .expect("Failed to parse SignedDeviceList"); + let update = DeviceListUpdate::try_from(signed_list) + .expect("Failed to parse DeviceListUpdate from signed list"); + + let expected_timestamp = + DateTime::::from_utc_timestamp_millis(123456789).unwrap(); + + assert_eq!(update.timestamp, expected_timestamp); + assert_eq!( + update.devices, + vec!["device1".to_string(), "device2".to_string()] + ); + } + #[test] fn test_timestamp_validation() { let valid_timestamp = Utc::now() - Duration::milliseconds(100); diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use crate::config::CONFIG; -use crate::database::{DeviceListRow, DeviceListUpdate}; +use crate::database::DeviceListUpdate; +use crate::device_list::{RawDeviceList, SignedDeviceList}; use crate::{ client_service::{handle_db_error, UpdateState, WorkflowInProgress}, constants::request_metadata, @@ -606,100 +607,6 @@ } } -// raw device list that can be serialized to JSON (and then signed in the future) -#[derive(serde::Serialize, serde::Deserialize)] -struct RawDeviceList { - devices: Vec, - timestamp: i64, -} - -impl From for RawDeviceList { - fn from(row: DeviceListRow) -> Self { - Self { - devices: row.device_ids, - timestamp: row.timestamp.timestamp_millis(), - } - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SignedDeviceList { - /// JSON-stringified [`RawDeviceList`] - raw_device_list: String, - /// Current primary device signature. - /// NOTE: Present only when the payload is received from primary device. - /// It's `None` for Identity-generated device-lists - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - cur_primary_signature: Option, - /// Previous primary device signature. Present only - /// if primary device has changed since last update. - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - last_primary_signature: Option, -} - -impl SignedDeviceList { - /// Serialize (and sign in the future) a [`RawDeviceList`] - fn try_from_raw(raw: RawDeviceList) -> Result { - let stringified_list = serde_json::to_string(&raw).map_err(|err| { - error!("Failed to serialize raw device list: {}", err); - tonic::Status::failed_precondition("unexpected error") - })?; - - Ok(Self { - raw_device_list: stringified_list, - cur_primary_signature: None, - last_primary_signature: None, - }) - } - - fn as_raw(&self) -> Result { - // The device list payload is sent as an escaped JSON payload. - // Escaped double quotes need to be trimmed before attempting to deserialize - serde_json::from_str(&self.raw_device_list.replace(r#"\""#, r#"""#)) - .map_err(|err| { - warn!("Failed to deserialize raw device list: {}", err); - tonic::Status::invalid_argument("invalid device list payload") - }) - } - - /// Serializes the signed device list to a JSON string - fn as_json_string(&self) -> Result { - serde_json::to_string(self).map_err(|err| { - error!("Failed to serialize device list updates: {}", err); - tonic::Status::failed_precondition("unexpected error") - }) - } -} - -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| { - warn!("Failed to deserialize device list update: {}", err); - tonic::Status::invalid_argument("invalid device list payload") - }) - } -} - -impl TryFrom for DeviceListUpdate { - type Error = tonic::Status; - fn try_from(signed_list: SignedDeviceList) -> Result { - let RawDeviceList { - devices, - timestamp: raw_timestamp, - } = signed_list.as_raw()?; - let timestamp = DateTime::::from_utc_timestamp_millis(raw_timestamp) - .ok_or_else(|| { - error!("Failed to parse RawDeviceList timestamp!"); - tonic::Status::invalid_argument("invalid timestamp") - })?; - Ok(DeviceListUpdate::new(devices, timestamp)) - } -} - #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct DeletePasswordUserInfo { pub opaque_server_login: comm_opaque2::server::Login, @@ -712,116 +619,3 @@ opaque_server_login, } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn deserialize_device_list_signature() { - let payload_with_signature = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}","curPrimarySignature":"foo"}"#; - let payload_without_signatures = r#"{"rawDeviceList":"{\"devices\":[\"device1\",\"device2\"],\"timestamp\":222222222}"}"#; - - let list_with_signature: SignedDeviceList = - serde_json::from_str(payload_with_signature).unwrap(); - let list_without_signatures: SignedDeviceList = - serde_json::from_str(payload_without_signatures).unwrap(); - - assert_eq!( - list_with_signature.cur_primary_signature, - Some("foo".to_string()) - ); - assert!(list_with_signature.last_primary_signature.is_none()); - - assert!(list_without_signatures.cur_primary_signature.is_none()); - assert!(list_without_signatures.last_primary_signature.is_none()); - } - - #[test] - fn serialize_device_list_signatures() { - let raw_list = r#"{"devices":["device1"],"timestamp":111111111}"#; - - let expected_payload_without_signatures = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}"}"#; - let device_list_without_signature = SignedDeviceList { - raw_device_list: raw_list.to_string(), - cur_primary_signature: None, - last_primary_signature: None, - }; - assert_eq!( - device_list_without_signature.as_json_string().unwrap(), - expected_payload_without_signatures - ); - - let expected_payload_with_signature = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}","curPrimarySignature":"foo"}"#; - let device_list_with_cur_signature = SignedDeviceList { - raw_device_list: raw_list.to_string(), - cur_primary_signature: Some("foo".to_string()), - last_primary_signature: None, - }; - assert_eq!( - device_list_with_cur_signature.as_json_string().unwrap(), - expected_payload_with_signature - ); - } - - #[test] - fn serialize_device_list_updates() { - let raw_updates = vec![ - RawDeviceList { - devices: vec!["device1".into()], - timestamp: 111111111, - }, - RawDeviceList { - devices: vec!["device1".into(), "device2".into()], - timestamp: 222222222, - }, - ]; - - let expected_raw_list1 = r#"{"devices":["device1"],"timestamp":111111111}"#; - let expected_raw_list2 = - r#"{"devices":["device1","device2"],"timestamp":222222222}"#; - - let signed_updates = raw_updates - .into_iter() - .map(SignedDeviceList::try_from_raw) - .collect::, _>>() - .expect("signing device list updates failed"); - - assert_eq!(signed_updates[0].raw_device_list, expected_raw_list1); - assert_eq!(signed_updates[1].raw_device_list, expected_raw_list2); - - let stringified_updates = signed_updates - .iter() - .map(serde_json::to_string) - .collect::, _>>() - .expect("serialize signed device lists failed"); - - let expected_stringified_list1 = r#"{"rawDeviceList":"{\"devices\":[\"device1\"],\"timestamp\":111111111}"}"#; - let expected_stringified_list2 = r#"{"rawDeviceList":"{\"devices\":[\"device1\",\"device2\"],\"timestamp\":222222222}"}"#; - - assert_eq!(stringified_updates[0], expected_stringified_list1); - assert_eq!(stringified_updates[1], expected_stringified_list2); - } - - #[test] - fn deserialize_device_list_update() { - let raw_payload = r#"{"rawDeviceList":"{\"devices\":[\"device1\",\"device2\"],\"timestamp\":123456789}"}"#; - let request = UpdateDeviceListRequest { - new_device_list: raw_payload.to_string(), - }; - - let signed_list = SignedDeviceList::try_from(request) - .expect("Failed to parse SignedDeviceList"); - let update = DeviceListUpdate::try_from(signed_list) - .expect("Failed to parse DeviceListUpdate from signed list"); - - let expected_timestamp = - DateTime::::from_utc_timestamp_millis(123456789).unwrap(); - - assert_eq!(update.timestamp, expected_timestamp); - assert_eq!( - update.devices, - vec!["device1".to_string(), "device2".to_string()] - ); - } -}