diff --git a/services/commtest/src/identity/device.rs b/services/commtest/src/identity/device.rs index b0c48c112..653569e28 100644 --- a/services/commtest/src/identity/device.rs +++ b/services/commtest/src/identity/device.rs @@ -1,229 +1,239 @@ use comm_opaque2::client::{Login, Registration}; use grpc_clients::identity::{get_auth_client, get_unauthenticated_client}; use rand::{distributions::Alphanumeric, Rng}; use crate::identity::olm_account_infos::generate_random_olm_key; use crate::identity::olm_account_infos::{ ClientPublicKeys, DEFAULT_CLIENT_KEYS, }; 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(); // TODO: Generate dynamic valid olm account info let keys = keys.unwrap_or_else(|| &DEFAULT_CLIENT_KEYS); let example_payload = serde_json::to_string(&keys).expect("Failed to serialize example payload"); // The ed25519 value from the olm payload let device_id = &keys.primary_identity_public_keys.ed25519; 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: "".to_string(), + initial_device_list: initial_device_list.unwrap_or_default(), }; let mut identity_client = get_unauthenticated_client( &service_addr::IDENTITY_GRPC.to_string(), PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .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 { // TODO: Generate dynamic valid olm account info let keys = keys.unwrap_or_else(|| &DEFAULT_CLIENT_KEYS); let example_payload = serde_json::to_string(&keys).expect("Failed to serialize example payload"); // The ed25519 value from the olm payload let device_id = &keys.primary_identity_public_keys.ed25519; 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(), PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .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, PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .await .expect("Couldnt connect to auth identity service"); client .log_out_user(Empty {}) .await .expect("Failed to logout user"); } diff --git a/services/commtest/tests/identity_device_list_tests.rs b/services/commtest/tests/identity_device_list_tests.rs index 6d223961c..af6f6c608 100644 --- a/services/commtest/tests/identity_device_list_tests.rs +++ b/services/commtest/tests/identity_device_list_tests.rs @@ -1,516 +1,571 @@ 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, DEVICE_TYPE, - PLACEHOLDER_CODE_VERSION, + 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::get_auth_client; use grpc_clients::identity::protos::auth::{ PeersDeviceListsRequest, UpdateDeviceListRequest, }; use grpc_clients::identity::protos::authenticated::GetDeviceListRequest; use grpc_clients::identity::DeviceType; 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, PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .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, PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .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, PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .await .expect("Couldn't connect to identity service"); // Perform unsigned update (add a new device) let first_update: DeviceListHistoryItem = { - let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - let update_payload = SignedDeviceList::from_raw_unsigned(&RawDeviceList { - devices: vec![primary_device_id.clone(), "device2".to_string()], - timestamp: now.as_millis() as i64, - }); + let update_payload = + SignedDeviceList::from_raw_unsigned(&RawDeviceList::new(vec![ + primary_device_id.clone(), + "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 now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); let update_payload = SignedDeviceList::create_signed( - &RawDeviceList { - devices: vec![primary_device_id.clone()], - timestamp: now.as_millis() as i64, - }, + &RawDeviceList::new(vec![primary_device_id.clone()]), &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 now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); let mut update_payload = SignedDeviceList::create_signed( - &RawDeviceList { - devices: vec![primary_device_id.clone(), "device3".to_string()], - timestamp: now.as_millis() as i64, - }, + &RawDeviceList::new(vec![ + primary_device_id.clone(), + "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 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, PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .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, PLACEHOLDER_CODE_VERSION, DEVICE_TYPE.to_string(), ) .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(); + + // 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, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), + ) + .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, Serialize, Deserialize)] +#[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() }