diff --git a/services/commtest/src/identity/device.rs b/services/commtest/src/identity/device.rs index dbc89abd8..885f13abb 100644 --- a/services/commtest/src/identity/device.rs +++ b/services/commtest/src/identity/device.rs @@ -1,235 +1,235 @@ use comm_opaque2::client::{Login, Registration}; use grpc_clients::identity::{ get_auth_client, get_unauthenticated_client, PlatformMetadata, }; use rand::{distributions::Alphanumeric, Rng}; use std::borrow::Cow; use crate::identity::olm_account_infos::generate_random_olm_key; use crate::identity::olm_account_infos::ClientPublicKeys; use crate::service_addr; use grpc_clients::identity::protos::unauth::{ DeviceKeyUpload, DeviceType, Empty, IdentityKeyInfo, OpaqueLoginFinishRequest, OpaqueLoginStartRequest, Prekey, RegistrationFinishRequest, RegistrationStartRequest, VerifyUserAccessTokenRequest, }; pub const PLACEHOLDER_CODE_VERSION: u64 = 0; pub const DEVICE_TYPE: &str = "service"; const PASSWORD: &str = "pass"; pub struct DeviceInfo { pub username: String, pub user_id: String, pub device_id: String, pub access_token: String, } impl From<&DeviceInfo> for VerifyUserAccessTokenRequest { fn from(value: &DeviceInfo) -> Self { Self { user_id: value.user_id.to_string(), device_id: value.device_id.to_string(), access_token: value.access_token.to_string(), } } } /// Register a new user with a device. /// - Gives random username (returned by function). /// - Device type defaults to keyserver. /// - Device ID taken from `keys` (ed25519), see [`DEFAULT_CLIENT_KEYS`] pub async fn register_user_device( keys: Option<&ClientPublicKeys>, device_type: Option, ) -> DeviceInfo { register_user_device_with_device_list(keys, device_type, None).await } /// Same as [`register_user_device`] but with third param being a /// stringified signed device list JSON pub async fn register_user_device_with_device_list( keys: Option<&ClientPublicKeys>, device_type: Option, initial_device_list: Option, ) -> DeviceInfo { let username: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(7) .map(char::from) .collect(); let device_keys = keys.map(Cow::Borrowed).unwrap_or_default(); let example_payload = serde_json::to_string(&device_keys) .expect("Failed to serialize example payload"); // The ed25519 value from the olm payload - let device_id = &device_keys.primary_identity_public_keys.ed25519; + let device_id = device_keys.device_id(); let device_type = device_type.unwrap_or(DeviceType::Keyserver); let mut client_registration = Registration::new(); let opaque_registration_request = client_registration.start(PASSWORD).unwrap(); let registration_start_request = RegistrationStartRequest { opaque_registration_request, username: username.to_string(), device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: example_payload.to_string(), payload_signature: "foo".to_string(), }), content_upload: Some(Prekey { prekey: generate_random_olm_key(), prekey_signature: "content_prekey_sig".to_string(), }), notif_upload: Some(Prekey { prekey: generate_random_olm_key(), prekey_signature: "notif_prekey_sig".to_string(), }), one_time_content_prekeys: Vec::new(), one_time_notif_prekeys: Vec::new(), device_type: device_type.into(), }), farcaster_id: None, initial_device_list: initial_device_list.unwrap_or_default(), }; let mut identity_client = get_unauthenticated_client( &service_addr::IDENTITY_GRPC.to_string(), PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); let registration_start_response = identity_client .register_password_user_start(registration_start_request) .await .unwrap() .into_inner(); let opaque_registration_upload = client_registration .finish( PASSWORD, ®istration_start_response.opaque_registration_response, ) .unwrap(); let registration_finish_request = RegistrationFinishRequest { session_id: registration_start_response.session_id, opaque_registration_upload, }; let registration_finish_response = identity_client .register_password_user_finish(registration_finish_request) .await .unwrap() .into_inner(); DeviceInfo { username: username.to_string(), device_id: device_id.to_string(), user_id: registration_finish_response.user_id, access_token: registration_finish_response.access_token, } } /// Log in existing user with a device. /// - Tries to log in with given username (it has to be already registered) /// - Device type defaults to keyserver. /// - Device ID taken from `keys` (ed25519), see [`DEFAULT_CLIENT_KEYS`] pub async fn login_user_device( username: &str, keys: Option<&ClientPublicKeys>, device_type: Option, force: bool, ) -> DeviceInfo { let device_keys = keys.cloned().unwrap_or_default(); let example_payload = serde_json::to_string(&device_keys) .expect("Failed to serialize example payload"); // The ed25519 value from the olm payload - let device_id = &device_keys.primary_identity_public_keys.ed25519; + let device_id = device_keys.device_id(); let device_type = device_type.unwrap_or(DeviceType::Keyserver); let mut client_login = Login::new(); let opaque_login_request = client_login.start(PASSWORD).unwrap(); let login_start_request = OpaqueLoginStartRequest { opaque_login_request, username: username.to_string(), device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: example_payload.to_string(), payload_signature: "foo".to_string(), }), content_upload: Some(Prekey { prekey: generate_random_olm_key(), prekey_signature: "content_prekey_sig".to_string(), }), notif_upload: Some(Prekey { prekey: generate_random_olm_key(), prekey_signature: "notif_prekey_sig".to_string(), }), one_time_content_prekeys: Vec::new(), one_time_notif_prekeys: Vec::new(), device_type: device_type.into(), }), force: Some(force), }; let mut identity_client = get_unauthenticated_client( &service_addr::IDENTITY_GRPC.to_string(), PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); let login_start_response = identity_client .log_in_password_user_start(login_start_request) .await .unwrap() .into_inner(); let opaque_login_upload = client_login .finish(&login_start_response.opaque_login_response) .unwrap(); let login_finish_request = OpaqueLoginFinishRequest { session_id: login_start_response.session_id, opaque_login_upload, }; let login_finish_response = identity_client .log_in_password_user_finish(login_finish_request) .await .unwrap() .into_inner(); DeviceInfo { username: username.to_string(), device_id: device_id.to_string(), user_id: login_finish_response.user_id, access_token: login_finish_response.access_token, } } pub async fn logout_user_device(device_info: DeviceInfo) { let DeviceInfo { user_id, device_id, access_token, .. } = device_info; let mut client = get_auth_client( &service_addr::IDENTITY_GRPC.to_string(), user_id, device_id, access_token, PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldnt connect to auth identity service"); client .log_out_user(Empty {}) .await .expect("Failed to logout user"); } diff --git a/services/commtest/src/identity/olm_account_infos.rs b/services/commtest/src/identity/olm_account_infos.rs index cba747dae..394ac76b1 100644 --- a/services/commtest/src/identity/olm_account_infos.rs +++ b/services/commtest/src/identity/olm_account_infos.rs @@ -1,84 +1,84 @@ use lazy_static::lazy_static; use rand::{distributions::Alphanumeric, Rng}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct IdentityPublicKeys { pub ed25519: String, pub curve25519: String, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ClientPublicKeys { pub primary_identity_public_keys: IdentityPublicKeys, pub notification_identity_public_keys: IdentityPublicKeys, } impl ClientPublicKeys { /// Generates random keys with given `ed25519` primary account /// signing public key. Use [`ClientPublicKeys::default`] for random key. pub fn new(primary_signing_public_key: impl Into) -> Self { Self { primary_identity_public_keys: IdentityPublicKeys { ed25519: primary_signing_public_key.into(), curve25519: generate_random_olm_key(), }, notification_identity_public_keys: IdentityPublicKeys { ed25519: generate_random_olm_key(), curve25519: generate_random_olm_key(), }, } } - pub fn device_id(&self) -> String { - self.primary_identity_public_keys.ed25519.clone() + pub fn device_id(&self) -> &str { + &self.primary_identity_public_keys.ed25519 } } impl Default for ClientPublicKeys { fn default() -> Self { Self::new(generate_random_olm_key()) } } lazy_static! { pub static ref DEFAULT_CLIENT_KEYS: ClientPublicKeys = ClientPublicKeys { primary_identity_public_keys: IdentityPublicKeys { ed25519: "cSlL+VLLJDgtKSPlIwoCZg0h0EmHlQoJC08uV/O+jvg".to_string(), curve25519: "Y4ZIqzpE1nv83kKGfvFP6rifya0itRg2hifqYtsISnk".to_string(), }, notification_identity_public_keys: IdentityPublicKeys { ed25519: "D0BV2Y7Qm36VUtjwyQTJJWYAycN7aMSJmhEsRJpW2mk".to_string(), curve25519: "DYmV8VdkjwG/VtC8C53morogNJhpTPT/4jzW0/cxzQo".to_string(), } }; pub static ref MOCK_CLIENT_KEYS_1: ClientPublicKeys = ClientPublicKeys { primary_identity_public_keys: IdentityPublicKeys { ed25519: "lbp5cS9fH5NnWIJbZ57wGBzDBGvmjoq6gMBHsIyXfJ4".to_string(), curve25519: "x74rEeVzfTcjm+B2yLN/wgfvHEzEtphQ/JeQfIrzPzQ".to_string(), }, notification_identity_public_keys: IdentityPublicKeys { ed25519: "+mi3TltiSK2883cm0TK2mkSKPcQb+WVfshltTSVgA2Y".to_string(), curve25519: "GI8V9FwOYIqxB2TzQN31nXKR8y3/B3k+ZOCgxkTlUlI".to_string(), }, }; pub static ref MOCK_CLIENT_KEYS_2: ClientPublicKeys = ClientPublicKeys { primary_identity_public_keys: IdentityPublicKeys { ed25519: "ZXx1ADCFxFm6P+UmVhX0A1tuqUoBU7lYjig/gMzSEJI".to_string(), curve25519: "zHfP5eeD3slrgidtNRknHw3NKtJ7hA+vinaT3ACIhRA".to_string(), }, notification_identity_public_keys: IdentityPublicKeys { ed25519: "TqzVFQLnJvt9JfMVU54d6InEd/wQV3DCplBuj5axTlU".to_string(), curve25519: "nRVVaf+Iz2MfEFtQtzrvV/EmTivqKpOeHlCt9OWYUxM".to_string(), }, }; } pub fn generate_random_olm_key() -> String { rand::thread_rng() .sample_iter(&Alphanumeric) .take(43) .map(char::from) .collect() } diff --git a/services/commtest/tests/identity_device_list_tests.rs b/services/commtest/tests/identity_device_list_tests.rs index 2d54aca19..12d86552e 100644 --- a/services/commtest/tests/identity_device_list_tests.rs +++ b/services/commtest/tests/identity_device_list_tests.rs @@ -1,565 +1,565 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; use commtest::identity::device::{ login_user_device, logout_user_device, register_user_device, register_user_device_with_device_list, DEVICE_TYPE, PLACEHOLDER_CODE_VERSION, }; use commtest::identity::SigningCapableAccount; use commtest::service_addr; use grpc_clients::identity::authenticated::ChainedInterceptedAuthClient; use grpc_clients::identity::protos::auth::{ PeersDeviceListsRequest, UpdateDeviceListRequest, }; use grpc_clients::identity::protos::authenticated::GetDeviceListRequest; use grpc_clients::identity::DeviceType; use grpc_clients::identity::{get_auth_client, PlatformMetadata}; use serde::{Deserialize, Serialize}; // 1. register user with android device // 2. register a web device // 3. remove android device // 4. register ios device // 5. get device list - should have 4 updates: // - [android] // - [android, web] // - [web] // - [ios, web] - mobile should be first #[tokio::test] async fn test_device_list_rotation() { use commtest::identity::olm_account_infos::{ DEFAULT_CLIENT_KEYS as DEVICE_KEYS_ANDROID, MOCK_CLIENT_KEYS_1 as DEVICE_KEYS_WEB, MOCK_CLIENT_KEYS_2 as DEVICE_KEYS_IOS, }; // Create viewer (user that doesn't change devices) let viewer = register_user_device(None, None).await; let mut auth_client = get_auth_client( &service_addr::IDENTITY_GRPC.to_string(), viewer.user_id.clone(), viewer.device_id, viewer.access_token, PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); let android_device_id = &DEVICE_KEYS_ANDROID.primary_identity_public_keys.ed25519; let web_device_id = &DEVICE_KEYS_WEB.primary_identity_public_keys.ed25519; let ios_device_id = &DEVICE_KEYS_IOS.primary_identity_public_keys.ed25519; // 1. Register user with primary Android device let android = register_user_device(Some(&DEVICE_KEYS_ANDROID), Some(DeviceType::Android)) .await; let user_id = android.user_id.clone(); let username = android.username.clone(); // 2. Log in a web device let _web = login_user_device( &username, Some(&DEVICE_KEYS_WEB), Some(DeviceType::Web), false, ) .await; // 3. Remove android device logout_user_device(android).await; // 4. Log in an iOS device let _ios = login_user_device( &username, Some(&DEVICE_KEYS_IOS), Some(DeviceType::Ios), false, ) .await; // Get device list updates for the user let device_lists_response: Vec> = get_raw_device_list_history(&mut auth_client, &user_id) .await .into_iter() .map(|device_list| device_list.devices) .collect(); let expected_device_list: Vec> = vec![ vec![android_device_id.into()], vec![android_device_id.into(), web_device_id.into()], vec![web_device_id.into()], vec![ios_device_id.into(), web_device_id.into()], ]; assert_eq!(device_lists_response, expected_device_list); } #[tokio::test] async fn test_update_device_list_rpc() { // Register user with primary device let primary_device = register_user_device(None, None).await; let mut auth_client = get_auth_client( &service_addr::IDENTITY_GRPC.to_string(), primary_device.user_id.clone(), primary_device.device_id, primary_device.access_token, PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); // Initial device list check let initial_device_list = get_raw_device_list_history(&mut auth_client, &primary_device.user_id) .await .into_iter() .map(|device_list| device_list.devices) .next() .expect("Expected to get single device list update"); assert!(initial_device_list.len() == 1, "Expected single device"); let primary_device_id = initial_device_list[0].clone(); // perform update by adding a new device let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); let devices_payload = vec![primary_device_id, "device2".to_string()]; let update_payload = SignedDeviceList::from_raw_unsigned(&RawDeviceList { devices: devices_payload.clone(), timestamp: now.as_millis() as i64, }); let update_request = UpdateDeviceListRequest::from(&update_payload); auth_client .update_device_list(update_request) .await .expect("Update device list RPC failed"); // get device list again let last_device_list = get_raw_device_list_history(&mut auth_client, &primary_device.user_id) .await; let last_device_list = last_device_list .last() .expect("Failed to get last device list update"); // check that the device list is properly updated assert_eq!(last_device_list.devices, devices_payload); assert_eq!(last_device_list.timestamp, now.as_millis() as i64); } #[tokio::test] async fn test_device_list_signatures() { // device list history as list of tuples: (signature, devices) type DeviceListHistoryItem = (Option, Vec); // Register user with primary device let mut primary_account = SigningCapableAccount::new(); let primary_device_keys = primary_account.public_keys(); let primary_device_id = primary_device_keys.device_id(); let user = register_user_device(Some(&primary_device_keys), Some(DeviceType::Ios)) .await; let mut auth_client = get_auth_client( &service_addr::IDENTITY_GRPC.to_string(), user.user_id.clone(), user.device_id, user.access_token, PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); // Perform unsigned update (add a new device) let first_update: DeviceListHistoryItem = { let update_payload = SignedDeviceList::from_raw_unsigned(&RawDeviceList::new(vec![ - primary_device_id.clone(), + primary_device_id.to_string(), "device2".to_string(), ])); let update_request = UpdateDeviceListRequest::from(&update_payload); auth_client .update_device_list(update_request) .await .expect("Unsigned device list update failed"); ( update_payload.cur_primary_signature.clone(), update_payload.into_raw().devices, ) }; // now perform a update (remove a device), but sign the device list let second_update: DeviceListHistoryItem = { let update_payload = SignedDeviceList::create_signed( - &RawDeviceList::new(vec![primary_device_id.clone()]), + &RawDeviceList::new(vec![primary_device_id.to_string()]), &mut primary_account, None, ); let update_request = UpdateDeviceListRequest::from(&update_payload); auth_client .update_device_list(update_request) .await .expect("Signed device list update failed"); ( update_payload.cur_primary_signature.clone(), update_payload.into_raw().devices, ) }; // now perform a signed update (add a device), but with invalid signature { let mut update_payload = SignedDeviceList::create_signed( &RawDeviceList::new(vec![ - primary_device_id.clone(), + primary_device_id.to_string(), "device3".to_string(), ]), &mut primary_account, None, ); // malfolm signature by replacing first characters update_payload .cur_primary_signature .as_mut() .expect("signature should be present") .replace_range(0..3, "foo"); let update_request = UpdateDeviceListRequest::from(&update_payload); auth_client .update_device_list(update_request) .await .expect_err("RPC should fail for invalid signature"); } // check the history to make sure our updates are correct let device_list_history = get_device_list_history(&mut auth_client, &user.user_id).await; let expected_devices_lists: Vec = vec![ - (None, vec![primary_device_id.clone()]), // auto-generated during registration + (None, vec![primary_device_id.to_string()]), // auto-generated during registration first_update, second_update, ]; let actual_device_lists: Vec = device_list_history .into_iter() .map(|list| (list.cur_primary_signature.clone(), list.into_raw().devices)) .collect(); assert_eq!(actual_device_lists, expected_devices_lists); } #[tokio::test] async fn test_keyserver_force_login() { use commtest::identity::olm_account_infos::{ DEFAULT_CLIENT_KEYS as DEVICE_KEYS_ANDROID, MOCK_CLIENT_KEYS_1 as DEVICE_KEYS_KEYSERVER_1, MOCK_CLIENT_KEYS_2 as DEVICE_KEYS_KEYSERVER_2, }; // Create viewer (user that doesn't change devices) let viewer = register_user_device(None, None).await; let mut auth_client = get_auth_client( &service_addr::IDENTITY_GRPC.to_string(), viewer.user_id.clone(), viewer.device_id, viewer.access_token, PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); let android_device_id = &DEVICE_KEYS_ANDROID.primary_identity_public_keys.ed25519; let keyserver_1_device_id = &DEVICE_KEYS_KEYSERVER_1.primary_identity_public_keys.ed25519; let keyserver_2_device_id = &DEVICE_KEYS_KEYSERVER_2.primary_identity_public_keys.ed25519; // 1. Register user with primary Android device let android = register_user_device(Some(&DEVICE_KEYS_ANDROID), Some(DeviceType::Android)) .await; let user_id = android.user_id.clone(); let username = android.username.clone(); // 2. Log in on keyserver 1 let _keyserver_1 = login_user_device( &username, Some(&DEVICE_KEYS_KEYSERVER_1), Some(DeviceType::Keyserver), false, ) .await; // 3. Log in on keyserver 2 with force = true let _keyserver_2 = login_user_device( &username, Some(&DEVICE_KEYS_KEYSERVER_2), Some(DeviceType::Keyserver), true, ) .await; // Get device list updates for the user let device_lists_response: Vec> = get_raw_device_list_history(&mut auth_client, &user_id) .await .into_iter() .map(|device_list| device_list.devices) .collect(); let expected_device_list: Vec> = vec![ vec![android_device_id.into()], vec![android_device_id.into(), keyserver_1_device_id.into()], vec![android_device_id.into()], vec![android_device_id.into(), keyserver_2_device_id.into()], ]; assert_eq!(device_lists_response, expected_device_list); } #[tokio::test] async fn test_device_list_multifetch() { // Create viewer (user that only auths request) let viewer = register_user_device(None, None).await; let mut auth_client = get_auth_client( &service_addr::IDENTITY_GRPC.to_string(), viewer.user_id.clone(), viewer.device_id, viewer.access_token, PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); // Register users and prepare expected device lists let mut expected_device_lists = HashMap::new(); for _ in 0..5 { let user = register_user_device(None, None).await; expected_device_lists.insert(user.user_id, vec![user.device_id]); } // Fetch device lists from server let user_ids: Vec<_> = expected_device_lists.keys().cloned().collect(); let request = PeersDeviceListsRequest { user_ids }; let response_device_lists = auth_client .get_device_lists_for_users(request) .await .expect("GetDeviceListsForUser RPC failed") .into_inner() .users_device_lists; // verify if response has the same user IDs as request let expected_user_ids: HashSet = expected_device_lists.keys().cloned().collect(); let response_user_ids: HashSet = response_device_lists.keys().cloned().collect(); let difference: HashSet<_> = expected_user_ids .symmetric_difference(&response_user_ids) .collect(); assert!(difference.is_empty(), "User IDs differ: {:?}", difference); // verify device list for each user for (user_id, expected_devices) in expected_device_lists { let response_payload = response_device_lists.get(&user_id).unwrap(); let returned_devices = SignedDeviceList::from_str(response_payload) .expect("failed to deserialize signed device list") .into_raw() .devices; assert_eq!( returned_devices, expected_devices, "Device list differs for user: {}, Expected {:?}, but got {:?}", user_id, expected_devices, returned_devices ); } } #[tokio::test] async fn test_initial_device_list() { // create signing account let mut primary_account = SigningCapableAccount::new(); let primary_device_keys = primary_account.public_keys(); - let primary_device_id = primary_device_keys.device_id(); + let primary_device_id = primary_device_keys.device_id().to_string(); // create initial device list let raw_device_list = RawDeviceList::new(vec![primary_device_id]); let signed_list = SignedDeviceList::create_signed( &raw_device_list, &mut primary_account, None, ); // register user with initial list let user = register_user_device_with_device_list( Some(&primary_device_keys), Some(DeviceType::Ios), Some(signed_list.as_json_string()), ) .await; let mut auth_client = get_auth_client( &service_addr::IDENTITY_GRPC.to_string(), user.user_id.clone(), user.device_id, user.access_token, PlatformMetadata::new(PLACEHOLDER_CODE_VERSION, DEVICE_TYPE), ) .await .expect("Couldn't connect to identity service"); let mut history = get_device_list_history(&mut auth_client, &user.user_id).await; let received_list = history.pop().expect("Received empty device list history"); assert!( history.is_empty(), "Device list history should have no more updates" ); assert_eq!( received_list.cur_primary_signature, signed_list.cur_primary_signature, "Signature mismatch" ); assert!(received_list.last_primary_signature.is_none()); assert_eq!(received_list.into_raw(), raw_device_list); } // See GetDeviceListResponse in identity_authenticated.proto // for details on the response format. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] #[allow(unused)] struct RawDeviceList { devices: Vec, timestamp: i64, } #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct SignedDeviceList { raw_device_list: String, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] cur_primary_signature: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] last_primary_signature: Option, } impl RawDeviceList { fn new(devices: Vec) -> Self { let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); RawDeviceList { devices, timestamp: now.as_millis() as i64, } } fn as_json_string(&self) -> String { serde_json::to_string(self).expect("Failed to serialize RawDeviceList") } } impl SignedDeviceList { fn from_raw_unsigned(raw: &RawDeviceList) -> Self { Self { raw_device_list: raw.as_json_string(), cur_primary_signature: None, last_primary_signature: None, } } fn create_signed( raw: &RawDeviceList, cur_primary_account: &mut SigningCapableAccount, last_primary_account: Option<&mut SigningCapableAccount>, ) -> Self { let raw_device_list = raw.as_json_string(); let cur_primary_signature = cur_primary_account.sign_message(&raw_device_list); let last_primary_signature = last_primary_account .map(|account| account.sign_message(&raw_device_list)); Self { raw_device_list, cur_primary_signature: Some(cur_primary_signature), last_primary_signature, } } fn into_raw(self) -> RawDeviceList { self .raw_device_list .parse() .expect("Failed to parse raw device list") } fn as_json_string(&self) -> String { serde_json::to_string(self).expect("Failed to serialize SignedDeviceList") } } impl FromStr for SignedDeviceList { type Err = serde_json::Error; fn from_str(s: &str) -> Result { serde_json::from_str(s) } } impl FromStr for RawDeviceList { type Err = serde_json::Error; fn from_str(s: &str) -> 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(&s.replace(r#"\""#, r#"""#)) } } impl From<&SignedDeviceList> for UpdateDeviceListRequest { fn from(value: &SignedDeviceList) -> Self { Self { new_device_list: value.as_json_string(), } } } async fn get_device_list_history( client: &mut ChainedInterceptedAuthClient, user_id: &str, ) -> Vec { let request = GetDeviceListRequest { user_id: user_id.to_string(), since_timestamp: None, }; let response = client .get_device_list_for_user(request) .await .expect("Get device list request failed") .into_inner(); response .device_list_updates .into_iter() .map(|update| { SignedDeviceList::from_str(&update) .expect("Failed to parse device list update") }) .collect() } async fn get_raw_device_list_history( client: &mut ChainedInterceptedAuthClient, user_id: &str, ) -> Vec { get_device_list_history(client, user_id) .await .into_iter() .map(|signed| signed.into_raw()) .collect() }