Page MenuHomePhabricator

D11278.diff
No OneTemporary

D11278.diff

diff --git a/keyserver/addons/rust-node-addon/src/identity_client/login.rs b/keyserver/addons/rust-node-addon/src/identity_client/login.rs
--- a/keyserver/addons/rust-node-addon/src/identity_client/login.rs
+++ b/keyserver/addons/rust-node-addon/src/identity_client/login.rs
@@ -51,6 +51,7 @@
one_time_notif_prekeys: notif_one_time_keys,
device_type: DeviceType::Keyserver.into(),
}),
+ force: None,
};
debug!("Starting login to identity service");
diff --git a/native/native_rust_library/src/lib.rs b/native/native_rust_library/src/lib.rs
--- a/native/native_rust_library/src/lib.rs
+++ b/native/native_rust_library/src/lib.rs
@@ -701,6 +701,7 @@
one_time_notif_prekeys: password_user_info.notif_one_time_keys,
device_type: DEVICE_TYPE.into(),
}),
+ force: None,
};
let mut identity_client = get_unauthenticated_client(
diff --git a/services/commtest/src/identity/device.rs b/services/commtest/src/identity/device.rs
--- a/services/commtest/src/identity/device.rs
+++ b/services/commtest/src/identity/device.rs
@@ -24,8 +24,8 @@
pub access_token: String,
}
-/// Log in existing user with a device.
-/// - Tries to log in with given username (it has to be already registered)
+/// 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(
@@ -111,14 +111,15 @@
}
}
-/// Register a new user with a device.
-/// - Gives random username (returned by function).
+/// 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<DeviceType>,
+ force: bool,
) -> DeviceInfo {
// TODO: Generate dynamic valid olm account info
let keys = keys.unwrap_or_else(|| &DEFAULT_CLIENT_KEYS);
@@ -151,6 +152,7 @@
one_time_notif_prekeys: Vec::new(),
device_type: device_type.into(),
}),
+ force: Some(force),
};
let mut identity_client = get_unauthenticated_client(
diff --git a/services/commtest/tests/identity_device_list_tests.rs b/services/commtest/tests/identity_device_list_tests.rs
--- a/services/commtest/tests/identity_device_list_tests.rs
+++ b/services/commtest/tests/identity_device_list_tests.rs
@@ -11,7 +11,7 @@
use grpc_clients::identity::protos::authenticated::GetDeviceListRequest;
use grpc_clients::identity::DeviceType;
use serde::Deserialize;
-use serde_json::{from_str, json};
+use serde_json::json;
// 1. register user with android device
// 2. register a web device
@@ -56,17 +56,25 @@
let username = android.username.clone();
// 2. Log in a web device
- let _web =
- login_user_device(&username, Some(&DEVICE_KEYS_WEB), Some(DeviceType::Web))
- .await;
+ 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))
- .await;
+ 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<Vec<String>> =
@@ -144,6 +152,77 @@
);
}
+#[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<Vec<String>> =
+ get_device_list_history(&mut auth_client, &user_id)
+ .await
+ .into_iter()
+ .map(|device_list| device_list.devices)
+ .collect();
+
+ let expected_device_list: Vec<Vec<String>> = 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);
+}
+
// See GetDeviceListResponse in identity_authenticated.proto
// for details on the response format.
#[derive(Deserialize)]
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
@@ -9,7 +9,7 @@
use serde::{Deserialize, Serialize};
use siwe::eip55;
use tonic::Response;
-use tracing::{debug, error, warn};
+use tracing::{debug, error, info, warn};
// Workspace crate imports
use crate::config::CONFIG;
@@ -66,6 +66,7 @@
pub user_id: String,
pub flattened_device_key_upload: FlattenedDeviceKeyUpload,
pub opaque_server_login: comm_opaque2::server::Login,
+ pub device_to_remove: Option<String>,
}
#[derive(Clone, Serialize, Deserialize)]
@@ -265,7 +266,7 @@
) -> Result<tonic::Response<OpaqueLoginStartResponse>, tonic::Status> {
let message = request.into_inner();
- debug!("Attempting to login user: {:?}", &message.username);
+ debug!("Attempting to log in user: {:?}", &message.username);
let user_id_and_password_file = self
.client
.get_user_id_and_password_file_from_username(&message.username)
@@ -296,6 +297,18 @@
return Err(tonic::Status::not_found("user not found"));
};
+ let flattened_device_key_upload =
+ construct_flattened_device_key_upload(&message)?;
+
+ let maybe_device_to_remove = self
+ .get_keyserver_device_to_remove(
+ &user_id,
+ &flattened_device_key_upload.device_id_key,
+ message.force.unwrap_or(false),
+ &flattened_device_key_upload.device_type,
+ )
+ .await?;
+
let mut server_login = comm_opaque2::server::Login::new();
let server_response = server_login
.start(
@@ -306,8 +319,12 @@
)
.map_err(protocol_error_to_grpc_status)?;
- let login_state =
- construct_user_login_info(&message, user_id, server_login)?;
+ let login_state = construct_user_login_info(
+ user_id,
+ server_login,
+ flattened_device_key_upload,
+ maybe_device_to_remove,
+ )?;
let session_id = self
.client
@@ -340,6 +357,14 @@
.finish(&message.opaque_login_upload)
.map_err(protocol_error_to_grpc_status)?;
+ if let Some(device_to_remove) = state.device_to_remove {
+ self
+ .client
+ .remove_device(state.user_id.clone(), device_to_remove)
+ .await
+ .map_err(handle_db_error)?;
+ }
+
let login_time = chrono::Utc::now();
self
.client
@@ -896,6 +921,44 @@
};
Ok(())
}
+
+ async fn get_keyserver_device_to_remove(
+ &self,
+ user_id: &str,
+ new_keyserver_device_id: &str,
+ force: bool,
+ device_type: &DeviceType,
+ ) -> Result<Option<String>, tonic::Status> {
+ if device_type != &DeviceType::Keyserver {
+ return Ok(None);
+ }
+
+ let maybe_keyserver_device_id = self
+ .client
+ .get_keyserver_device_id_for_user(user_id)
+ .await
+ .map_err(handle_db_error)?;
+
+ let Some(existing_keyserver_device_id) = maybe_keyserver_device_id else {
+ return Ok(None);
+ };
+
+ if new_keyserver_device_id == existing_keyserver_device_id {
+ return Ok(None);
+ }
+
+ if force {
+ info!(
+ "keyserver {} will be removed from the device list",
+ existing_keyserver_device_id
+ );
+ Ok(Some(existing_keyserver_device_id))
+ } else {
+ Err(tonic::Status::already_exists(
+ "user already has a keyserver",
+ ))
+ }
+ }
}
pub fn handle_db_error(db_error: DBError) -> tonic::Status {
@@ -932,16 +995,16 @@
}
fn construct_user_login_info(
- message: &impl DeviceKeyUploadActions,
user_id: String,
opaque_server_login: comm_opaque2::server::Login,
+ flattened_device_key_upload: FlattenedDeviceKeyUpload,
+ device_to_remove: Option<String>,
) -> Result<UserLoginInfo, tonic::Status> {
Ok(UserLoginInfo {
user_id,
- flattened_device_key_upload: construct_flattened_device_key_upload(
- message,
- )?,
+ flattened_device_key_upload,
opaque_server_login,
+ device_to_remove,
})
}
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
@@ -390,6 +390,21 @@
Ok(Some(outbound_payload))
}
+ pub async fn get_keyserver_device_id_for_user(
+ &self,
+ user_id: &str,
+ ) -> Result<Option<String>, Error> {
+ use crate::grpc_services::protos::unauth::DeviceType as GrpcDeviceType;
+
+ 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)
+ .map(|device| device.device_id);
+
+ Ok(maybe_keyserver_device_id)
+ }
+
/// Will "mint" a single one-time key by attempting to successfully delete a
/// key
pub async fn get_one_time_key(
diff --git a/shared/protos/identity_unauth.proto b/shared/protos/identity_unauth.proto
--- a/shared/protos/identity_unauth.proto
+++ b/shared/protos/identity_unauth.proto
@@ -164,6 +164,10 @@
// Information specific to a user's device needed to open a new channel of
// communication with this user
DeviceKeyUpload device_key_upload = 3;
+ // If set to true, the user's existing keyserver will be deleted from the
+ // identity service and replaced with this one. This field has no effect if
+ // the device is not a keyserver
+ optional bool force = 4;
}
message OpaqueLoginFinishRequest {
diff --git a/web/protobufs/identity-unauth-structs.cjs b/web/protobufs/identity-unauth-structs.cjs
--- a/web/protobufs/identity-unauth-structs.cjs
+++ b/web/protobufs/identity-unauth-structs.cjs
@@ -2457,7 +2457,8 @@
var f, obj = {
username: jspb.Message.getFieldWithDefault(msg, 1, ""),
opaqueLoginRequest: msg.getOpaqueLoginRequest_asB64(),
- deviceKeyUpload: (f = msg.getDeviceKeyUpload()) && proto.identity.unauth.DeviceKeyUpload.toObject(includeInstance, f)
+ deviceKeyUpload: (f = msg.getDeviceKeyUpload()) && proto.identity.unauth.DeviceKeyUpload.toObject(includeInstance, f),
+ force: jspb.Message.getBooleanFieldWithDefault(msg, 4, false)
};
if (includeInstance) {
@@ -2507,6 +2508,10 @@
reader.readMessage(value,proto.identity.unauth.DeviceKeyUpload.deserializeBinaryFromReader);
msg.setDeviceKeyUpload(value);
break;
+ case 4:
+ var value = /** @type {boolean} */ (reader.readBool());
+ msg.setForce(value);
+ break;
default:
reader.skipField();
break;
@@ -2558,6 +2563,13 @@
proto.identity.unauth.DeviceKeyUpload.serializeBinaryToWriter
);
}
+ f = /** @type {boolean} */ (jspb.Message.getField(message, 4));
+ if (f != null) {
+ writer.writeBool(
+ 4,
+ f
+ );
+ }
};
@@ -2658,6 +2670,42 @@
};
+/**
+ * optional bool force = 4;
+ * @return {boolean}
+ */
+proto.identity.unauth.OpaqueLoginStartRequest.prototype.getForce = function() {
+ return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 4, false));
+};
+
+
+/**
+ * @param {boolean} value
+ * @return {!proto.identity.unauth.OpaqueLoginStartRequest} returns this
+ */
+proto.identity.unauth.OpaqueLoginStartRequest.prototype.setForce = function(value) {
+ return jspb.Message.setField(this, 4, value);
+};
+
+
+/**
+ * Clears the field making it undefined.
+ * @return {!proto.identity.unauth.OpaqueLoginStartRequest} returns this
+ */
+proto.identity.unauth.OpaqueLoginStartRequest.prototype.clearForce = function() {
+ return jspb.Message.setField(this, 4, undefined);
+};
+
+
+/**
+ * Returns whether this field is set.
+ * @return {boolean}
+ */
+proto.identity.unauth.OpaqueLoginStartRequest.prototype.hasForce = function() {
+ return jspb.Message.getField(this, 4) != null;
+};
+
+
diff --git a/web/protobufs/identity-unauth-structs.cjs.flow b/web/protobufs/identity-unauth-structs.cjs.flow
--- a/web/protobufs/identity-unauth-structs.cjs.flow
+++ b/web/protobufs/identity-unauth-structs.cjs.flow
@@ -250,6 +250,11 @@
hasDeviceKeyUpload(): boolean;
clearDeviceKeyUpload(): OpaqueLoginStartRequest;
+ getForce(): boolean;
+ setForce(value: boolean): OpaqueLoginStartRequest;
+ hasForce(): boolean;
+ clearForce(): OpaqueLoginStartRequest;
+
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): OpaqueLoginStartRequestObject;
static toObject(includeInstance: boolean, msg: OpaqueLoginStartRequest): OpaqueLoginStartRequestObject;
@@ -262,6 +267,7 @@
username: string,
opaqueLoginRequest: Uint8Array | string,
deviceKeyUpload?: DeviceKeyUploadObject,
+ force?: boolean,
};
declare export class OpaqueLoginFinishRequest extends Message {

File Metadata

Mime Type
text/plain
Expires
Sat, Dec 21, 12:22 PM (20 h, 21 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2687655
Default Alt Text
D11278.diff (14 KB)

Event Timeline