Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3505132
D11278.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
14 KB
Referenced Files
None
Subscribers
None
D11278.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D11278: [identity] add optional force param to password login RPC
Attached
Detach File
Event Timeline
Log In to Comment