diff --git a/keyserver/addons/rust-node-addon/src/identity_client/login.rs b/keyserver/addons/rust-node-addon/src/identity_client/login.rs index f36b53f3b..9b49fa10c 100644 --- a/keyserver/addons/rust-node-addon/src/identity_client/login.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/login.rs @@ -1,86 +1,103 @@ use super::*; use comm_opaque2::client::Login; use grpc_clients::identity::protos::unauthenticated::{ OpaqueLoginFinishRequest, OpaqueLoginStartRequest, }; use tracing::debug; #[napi] #[instrument(skip_all)] pub async fn login_user( username: String, password: String, signed_identity_keys_blob: SignedIdentityKeysBlob, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, ) -> Result { debug!("Attempting to login user: {}", username); // Set up the gRPC client that will be used to talk to the Identity service let mut identity_client = get_identity_client().await?; // Start OPAQUE registration and send initial registration request let mut client_login = Login::new(); let opaque_login_request = client_login .start(&password) .map_err(|_| Error::from_reason("Failed to create opaque login request"))?; let login_start_request = OpaqueLoginStartRequest { opaque_login_request, username, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: signed_identity_keys_blob.payload, payload_signature: signed_identity_keys_blob.signature, social_proof: None, }), content_upload: Some(PreKey { pre_key: content_prekey, pre_key_signature: content_prekey_signature, }), notif_upload: Some(PreKey { pre_key: notif_prekey, pre_key_signature: notif_prekey_signature, }), one_time_content_prekeys: content_one_time_keys, one_time_notif_prekeys: notif_one_time_keys, device_type: DeviceType::Keyserver.into(), }), }; debug!("Starting login to identity service"); - let login_start_response = identity_client + let response = identity_client .login_password_user_start(login_start_request) .await - .map_err(handle_grpc_error)? - .into_inner(); - + .map_err(handle_grpc_error)?; debug!("Received login response from identity service"); + + // We need to get the load balancer cookie from from the response and send it + // in the subsequent request to ensure it is routed to the same identity + // service instance as the first request + let cookie = response + .metadata() + .get(RESPONSE_METADATA_COOKIE_KEY) + .cloned(); + + let login_start_response = response.into_inner(); + let opaque_login_upload = client_login .finish(&login_start_response.opaque_login_response) .map_err(|_| Error::from_reason("Failed to finish opaque login request"))?; - let login_finish_request = OpaqueLoginFinishRequest { + + let mut login_finish_request = Request::new(OpaqueLoginFinishRequest { session_id: login_start_response.session_id, opaque_login_upload, - }; + }); + + // Cookie won't be available in local dev environments + if let Some(cookie_metadata) = cookie { + login_finish_request + .metadata_mut() + .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); + } debug!("Attempting to finalize opaque login exchange with identity service"); let login_finish_response = identity_client .login_password_user_finish(login_finish_request) .await .map_err(handle_grpc_error)? .into_inner(); debug!("Finished login with identity service"); let user_info = UserLoginInfo { user_id: login_finish_response.user_id, access_token: login_finish_response.access_token, }; Ok(user_info) } diff --git a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs index cd0740526..139f87aac 100644 --- a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs @@ -1,194 +1,197 @@ pub mod add_reserved_usernames; pub mod get_inbound_keys_for_user; pub mod login; pub mod prekey; pub mod register_user; pub mod remove_reserved_usernames; pub mod upload_one_time_keys; use client_proto::identity_client_service_client::IdentityClientServiceClient; use client_proto::{ AddReservedUsernamesRequest, DeviceKeyUpload, DeviceType, IdentityKeyInfo, InboundKeyInfo, PreKey, RegistrationFinishRequest, RegistrationStartRequest, RemoveReservedUsernameRequest, }; use grpc_clients::identity::authenticated::ChainedInterceptedAuthClient; use grpc_clients::identity::protos::authenticated::UploadOneTimeKeysRequest; use grpc_clients::identity::protos::unauthenticated as client_proto; use grpc_clients::identity::shared::CodeVersionLayer; +use grpc_clients::identity::{ + REQUEST_METADATA_COOKIE_KEY, RESPONSE_METADATA_COOKIE_KEY, +}; use lazy_static::lazy_static; use napi::bindgen_prelude::*; use serde::{Deserialize, Serialize}; use std::env::var; use tonic::codegen::InterceptedService; use tonic::{transport::Channel, Request}; use tracing::{self, info, instrument, warn, Level}; use tracing_subscriber::EnvFilter; mod generated { // We get the CODE_VERSION from this generated file include!(concat!(env!("OUT_DIR"), "/version.rs")); } pub use generated::CODE_VERSION; pub const DEVICE_TYPE: &str = "keyserver"; lazy_static! { static ref IDENTITY_SERVICE_CONFIG: IdentityServiceConfig = { let filter = EnvFilter::builder() .with_default_directive(Level::INFO.into()) .with_env_var(EnvFilter::DEFAULT_ENV) .from_env_lossy(); let subscriber = tracing_subscriber::fmt().with_env_filter(filter).finish(); tracing::subscriber::set_global_default(subscriber) .expect("Unable to configure tracing"); let config_json_string = var("COMM_JSONCONFIG_secrets_identity_service_config"); match config_json_string { Ok(json) => serde_json::from_str(&json).unwrap(), Err(_) => IdentityServiceConfig::default(), } }; } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct IdentityServiceConfig { identity_socket_addr: String, } impl Default for IdentityServiceConfig { fn default() -> Self { info!("Using default identity configuration"); Self { identity_socket_addr: "http://[::1]:50054".to_string(), } } } async fn get_identity_client() -> Result< IdentityClientServiceClient>, > { info!("Connecting to identity service"); grpc_clients::identity::get_unauthenticated_client( &IDENTITY_SERVICE_CONFIG.identity_socket_addr, CODE_VERSION, DEVICE_TYPE.to_string(), ) .await .map_err(|_| { Error::new( Status::GenericFailure, "Unable to connect to identity service".to_string(), ) }) } async fn get_authenticated_identity_client( user_id: String, device_id: String, access_token: String, ) -> Result { info!("Connecting to identity service"); grpc_clients::identity::get_auth_client( &IDENTITY_SERVICE_CONFIG.identity_socket_addr, user_id, device_id, access_token, CODE_VERSION, DEVICE_TYPE.to_string(), ) .await .map_err(|_| { Error::new( Status::GenericFailure, "Unable to connect to identity service".to_string(), ) }) } #[napi(object)] pub struct SignedIdentityKeysBlob { pub payload: String, pub signature: String, } #[napi(object)] pub struct UserLoginInfo { pub user_id: String, pub access_token: String, } #[napi(object)] pub struct InboundKeyInfoResponse { pub payload: String, pub payload_signature: String, pub social_proof: Option, pub content_prekey: String, pub content_prekey_signature: String, pub notif_prekey: String, pub notif_prekey_signature: String, } impl TryFrom for InboundKeyInfoResponse { type Error = Error; fn try_from(key_info: InboundKeyInfo) -> Result { let identity_info = key_info .identity_info .ok_or(Error::from_status(Status::GenericFailure))?; let IdentityKeyInfo { payload, payload_signature, social_proof, } = identity_info; let content_prekey = key_info .content_prekey .ok_or(Error::from_status(Status::GenericFailure))?; let PreKey { pre_key: content_prekey_value, pre_key_signature: content_prekey_signature, } = content_prekey; let notif_prekey = key_info .notif_prekey .ok_or(Error::from_status(Status::GenericFailure))?; let PreKey { pre_key: notif_prekey_value, pre_key_signature: notif_prekey_signature, } = notif_prekey; Ok(Self { payload, payload_signature, social_proof, content_prekey: content_prekey_value, content_prekey_signature, notif_prekey: notif_prekey_value, notif_prekey_signature, }) } } pub fn handle_grpc_error(error: tonic::Status) -> napi::Error { warn!("Received error: {}", error.message()); Error::new(Status::GenericFailure, error.message()) } #[cfg(test)] mod tests { use super::CODE_VERSION; #[test] fn test_code_version_exists() { assert!(CODE_VERSION > 0); } } diff --git a/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs b/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs index 638c9f1be..ed7f3d70c 100644 --- a/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/register_user.rs @@ -1,84 +1,101 @@ use super::*; use tracing::{debug, warn}; #[napi] #[instrument(skip_all)] pub async fn register_user( username: String, password: String, signed_identity_keys_blob: SignedIdentityKeysBlob, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, ) -> Result { debug!("Attempting to register user: {}", username); // Set up the gRPC client that will be used to talk to the Identity service let mut identity_client = get_identity_client().await?; // Start OPAQUE registration and send initial registration request let mut opaque_registration = comm_opaque2::client::Registration::new(); let opaque_registration_request = opaque_registration .start(&password) .map_err(|_| Error::from_status(Status::GenericFailure))?; let device_key_upload = DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: signed_identity_keys_blob.payload, payload_signature: signed_identity_keys_blob.signature, social_proof: None, }), content_upload: Some(PreKey { pre_key: content_prekey, pre_key_signature: content_prekey_signature, }), notif_upload: Some(PreKey { pre_key: notif_prekey, pre_key_signature: notif_prekey_signature, }), one_time_content_prekeys: content_one_time_keys, one_time_notif_prekeys: notif_one_time_keys, device_type: DeviceType::Keyserver.into(), }; let registration_start_request = Request::new(RegistrationStartRequest { opaque_registration_request, username, device_key_upload: Some(device_key_upload), }); // Finish OPAQUE registration and send final registration request - let registration_start_response = identity_client + let response = identity_client .register_password_user_start(registration_start_request) .await - .map_err(handle_grpc_error)? - .into_inner(); + .map_err(handle_grpc_error)?; debug!("Received registration start response"); + // We need to get the load balancer cookie from from the response and send it + // in the subsequent request to ensure it is routed to the same identity + // service instance as the first request + let cookie = response + .metadata() + .get(RESPONSE_METADATA_COOKIE_KEY) + .cloned(); + + let registration_start_response = response.into_inner(); + let opaque_registration_upload = opaque_registration .finish( &password, ®istration_start_response.opaque_registration_response, ) .map_err(|_| Error::from_status(Status::GenericFailure))?; - let registration_finish_request = Request::new(RegistrationFinishRequest { - session_id: registration_start_response.session_id, - opaque_registration_upload, - }); + let mut registration_finish_request = + Request::new(RegistrationFinishRequest { + session_id: registration_start_response.session_id, + opaque_registration_upload, + }); + + // Cookie won't be available in local dev environments + if let Some(cookie_metadata) = cookie { + registration_finish_request + .metadata_mut() + .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); + } let registration_response = identity_client .register_password_user_finish(registration_finish_request) .await .map_err(handle_grpc_error)? .into_inner(); let user_info = UserLoginInfo { user_id: registration_response.user_id, access_token: registration_response.access_token, }; Ok(user_info) } diff --git a/native/native_rust_library/src/lib.rs b/native/native_rust_library/src/lib.rs index 3ce8d0f80..d5911ef09 100644 --- a/native/native_rust_library/src/lib.rs +++ b/native/native_rust_library/src/lib.rs @@ -1,839 +1,899 @@ use crate::ffi::{bool_callback, string_callback, void_callback}; use comm_opaque2::client::{Login, Registration}; use comm_opaque2::grpc::opaque_error_to_grpc_status as handle_error; use grpc_clients::identity::protos::authenticated::{ UpdateUserPasswordFinishRequest, UpdateUserPasswordStartRequest, }; use grpc_clients::identity::protos::client::{ outbound_keys_for_user_request::Identifier, DeviceKeyUpload, DeviceType, Empty, IdentityKeyInfo, OpaqueLoginFinishRequest, OpaqueLoginStartRequest, OutboundKeyInfo, OutboundKeysForUserRequest, PreKey, RegistrationFinishRequest, RegistrationStartRequest, WalletLoginRequest, }; -use grpc_clients::identity::{get_auth_client, get_unauthenticated_client}; +use grpc_clients::identity::{ + get_auth_client, get_unauthenticated_client, REQUEST_METADATA_COOKIE_KEY, + RESPONSE_METADATA_COOKIE_KEY, +}; use lazy_static::lazy_static; use serde::Serialize; use std::sync::Arc; use tokio::runtime::{Builder, Runtime}; -use tonic::Status; +use tonic::{Request, Status}; use tracing::instrument; mod argon2_tools; mod backup; mod constants; use argon2_tools::compute_backup_key_str; mod generated { // We get the CODE_VERSION from this generated file include!(concat!(env!("OUT_DIR"), "/version.rs")); } pub use generated::CODE_VERSION; #[cfg(not(feature = "android"))] pub const DEVICE_TYPE: DeviceType = DeviceType::Ios; #[cfg(feature = "android")] pub const DEVICE_TYPE: DeviceType = DeviceType::Android; lazy_static! { pub static ref RUNTIME: Arc = Arc::new(Builder::new_multi_thread().enable_all().build().unwrap()); } use backup::ffi::*; #[cxx::bridge] mod ffi { extern "Rust" { #[cxx_name = "identityRegisterUser"] fn register_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ); #[cxx_name = "identityLoginPasswordUser"] fn login_password_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ); #[cxx_name = "identityLoginWalletUser"] fn login_wallet_user( siwe_message: String, siwe_signature: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, social_proof: String, promise_id: u32, ); #[cxx_name = "identityUpdateUserPassword"] fn update_user_password( user_id: String, device_id: String, access_token: String, password: String, promise_id: u32, ); #[cxx_name = "identityDeleteUser"] fn delete_user( user_id: String, device_id: String, access_token: String, promise_id: u32, ); #[cxx_name = "identityGetOutboundKeysForUserDevice"] fn get_outbound_keys_for_user_device( identifier_type: String, identifier_value: String, device_id: String, promise_id: u32, ); #[cxx_name = "identityGenerateNonce"] fn generate_nonce(promise_id: u32); #[cxx_name = "identityVersionSupported"] fn version_supported(promise_id: u32); // Argon2 #[cxx_name = "compute_backup_key"] fn compute_backup_key_str( password: &str, backup_id: &str, ) -> Result<[u8; 32]>; } unsafe extern "C++" { include!("RustCallback.h"); #[namespace = "comm"] #[cxx_name = "stringCallback"] fn string_callback(error: String, promise_id: u32, ret: String); #[namespace = "comm"] #[cxx_name = "voidCallback"] fn void_callback(error: String, promise_id: u32); #[namespace = "comm"] #[cxx_name = "boolCallback"] fn bool_callback(error: String, promise_id: u32, ret: bool); } // AES cryptography #[namespace = "comm"] unsafe extern "C++" { include!("RustAESCrypto.h"); #[allow(unused)] #[cxx_name = "aesGenerateKey"] fn generate_key(buffer: &mut [u8]) -> Result<()>; /// The first two argument aren't mutated but creation of Java ByteBuffer /// requires the underlying bytes to be mutable. #[allow(unused)] #[cxx_name = "aesEncrypt"] fn encrypt( key: &mut [u8], plaintext: &mut [u8], sealed_data: &mut [u8], ) -> Result<()>; /// The first two argument aren't mutated but creation of Java ByteBuffer /// requires the underlying bytes to be mutable. #[allow(unused)] #[cxx_name = "aesDecrypt"] fn decrypt( key: &mut [u8], sealed_data: &mut [u8], plaintext: &mut [u8], ) -> Result<()>; } // Backup extern "Rust" { #[cxx_name = "createBackup"] fn create_backup_sync( backup_id: String, backup_secret: String, pickle_key: String, pickled_account: String, user_data: String, promise_id: u32, ); #[cxx_name = "restoreBackup"] fn restore_backup_sync( backup_id: String, backup_secret: String, encrypted_user_keys: String, encrypted_user_data: String, promise_id: u32, ); } } fn handle_string_result_as_callback( result: Result, promise_id: u32, ) where E: std::fmt::Display, { match result { Err(e) => string_callback(e.to_string(), promise_id, "".to_string()), Ok(r) => string_callback("".to_string(), promise_id, r), } } fn handle_void_result_as_callback(result: Result<(), E>, promise_id: u32) where E: std::fmt::Display, { match result { Err(e) => void_callback(e.to_string(), promise_id), Ok(_) => void_callback("".to_string(), promise_id), } } fn handle_bool_result_as_callback(result: Result, promise_id: u32) where E: std::fmt::Display, { match result { Err(e) => bool_callback(e.to_string(), promise_id, false), Ok(r) => bool_callback("".to_string(), promise_id, r), } } fn generate_nonce(promise_id: u32) { RUNTIME.spawn(async move { let result = fetch_nonce().await; handle_string_result_as_callback(result, promise_id); }); } async fn fetch_nonce() -> Result { let mut identity_client = get_unauthenticated_client( "http://127.0.0.1:50054", CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let nonce = identity_client .generate_nonce(Empty {}) .await? .into_inner() .nonce; Ok(nonce) } fn version_supported(promise_id: u32) { RUNTIME.spawn(async move { let result = version_supported_helper().await; handle_bool_result_as_callback(result, promise_id); }); } async fn version_supported_helper() -> Result { let mut identity_client = get_unauthenticated_client( "http://127.0.0.1:50054", CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let response = identity_client.ping(Empty {}).await; match response { Ok(_) => Ok(true), Err(e) => { if grpc_clients::error::is_version_unsupported(&e) { Ok(false) } else { Err(e.into()) } } } } struct AuthInfo { user_id: String, device_id: String, access_token: String, } #[instrument] fn register_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ) { RUNTIME.spawn(async move { let password_user_info = PasswordUserInfo { username, password, key_payload, key_payload_signature, content_prekey, content_prekey_signature, notif_prekey, notif_prekey_signature, content_one_time_keys, notif_one_time_keys, }; let result = register_user_helper(password_user_info).await; handle_string_result_as_callback(result, promise_id); }); } struct PasswordUserInfo { username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct UserIDAndDeviceAccessToken { #[serde(rename = "userID")] user_id: String, access_token: String, } async fn register_user_helper( password_user_info: PasswordUserInfo, ) -> Result { let mut client_registration = Registration::new(); let opaque_registration_request = client_registration .start(&password_user_info.password) .map_err(handle_error)?; let registration_start_request = RegistrationStartRequest { opaque_registration_request, username: password_user_info.username, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: password_user_info.key_payload, payload_signature: password_user_info.key_payload_signature, social_proof: None, }), content_upload: Some(PreKey { pre_key: password_user_info.content_prekey, pre_key_signature: password_user_info.content_prekey_signature, }), notif_upload: Some(PreKey { pre_key: password_user_info.notif_prekey, pre_key_signature: password_user_info.notif_prekey_signature, }), one_time_content_prekeys: password_user_info.content_one_time_keys, one_time_notif_prekeys: password_user_info.notif_one_time_keys, device_type: DEVICE_TYPE.into(), }), }; let mut identity_client = get_unauthenticated_client( "http://127.0.0.1:50054", CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; - let registration_start_response = identity_client + let response = identity_client .register_password_user_start(registration_start_request) - .await? - .into_inner(); + .await?; + + // We need to get the load balancer cookie from from the response and send it + // in the subsequent request to ensure it is routed to the same identity + // service instance as the first request + let cookie = response + .metadata() + .get(RESPONSE_METADATA_COOKIE_KEY) + .cloned(); + + let registration_start_response = response.into_inner(); let opaque_registration_upload = client_registration .finish( &password_user_info.password, ®istration_start_response.opaque_registration_response, ) .map_err(handle_error)?; + let registration_finish_request = RegistrationFinishRequest { session_id: registration_start_response.session_id, opaque_registration_upload, }; + let mut finish_request = Request::new(registration_finish_request); + + // Cookie won't be available in local dev environments + if let Some(cookie_metadata) = cookie { + finish_request + .metadata_mut() + .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); + } + let registration_finish_response = identity_client - .register_password_user_finish(registration_finish_request) + .register_password_user_finish(finish_request) .await? .into_inner(); let user_id_and_access_token = UserIDAndDeviceAccessToken { user_id: registration_finish_response.user_id, access_token: registration_finish_response.access_token, }; Ok(serde_json::to_string(&user_id_and_access_token)?) } #[instrument] fn login_password_user( username: String, password: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, promise_id: u32, ) { RUNTIME.spawn(async move { let password_user_info = PasswordUserInfo { username, password, key_payload, key_payload_signature, content_prekey, content_prekey_signature, notif_prekey, notif_prekey_signature, content_one_time_keys, notif_one_time_keys, }; let result = login_password_user_helper(password_user_info).await; handle_string_result_as_callback(result, promise_id); }); } async fn login_password_user_helper( password_user_info: PasswordUserInfo, ) -> Result { let mut client_login = Login::new(); let opaque_login_request = client_login .start(&password_user_info.password) .map_err(handle_error)?; let login_start_request = OpaqueLoginStartRequest { opaque_login_request, username: password_user_info.username, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: password_user_info.key_payload, payload_signature: password_user_info.key_payload_signature, social_proof: None, }), content_upload: Some(PreKey { pre_key: password_user_info.content_prekey, pre_key_signature: password_user_info.content_prekey_signature, }), notif_upload: Some(PreKey { pre_key: password_user_info.notif_prekey, pre_key_signature: password_user_info.notif_prekey_signature, }), one_time_content_prekeys: password_user_info.content_one_time_keys, one_time_notif_prekeys: password_user_info.notif_one_time_keys, device_type: DEVICE_TYPE.into(), }), }; let mut identity_client = get_unauthenticated_client( "http://127.0.0.1:50054", CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; - let login_start_response = identity_client + let response = identity_client .login_password_user_start(login_start_request) - .await? - .into_inner(); + .await?; + + // We need to get the load balancer cookie from from the response and send it + // in the subsequent request to ensure it is routed to the same identity + // service instance as the first request + let cookie = response + .metadata() + .get(RESPONSE_METADATA_COOKIE_KEY) + .cloned(); + + let login_start_response = response.into_inner(); let opaque_login_upload = client_login .finish(&login_start_response.opaque_login_response) .map_err(handle_error)?; + let login_finish_request = OpaqueLoginFinishRequest { session_id: login_start_response.session_id, opaque_login_upload, }; + let mut finish_request = Request::new(login_finish_request); + + // Cookie won't be available in local dev environments + if let Some(cookie_metadata) = cookie { + finish_request + .metadata_mut() + .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); + } + let login_finish_response = identity_client - .login_password_user_finish(login_finish_request) + .login_password_user_finish(finish_request) .await? .into_inner(); let user_id_and_access_token = UserIDAndDeviceAccessToken { user_id: login_finish_response.user_id, access_token: login_finish_response.access_token, }; Ok(serde_json::to_string(&user_id_and_access_token)?) } struct WalletUserInfo { siwe_message: String, siwe_signature: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, social_proof: String, } #[instrument] fn login_wallet_user( siwe_message: String, siwe_signature: String, key_payload: String, key_payload_signature: String, content_prekey: String, content_prekey_signature: String, notif_prekey: String, notif_prekey_signature: String, content_one_time_keys: Vec, notif_one_time_keys: Vec, social_proof: String, promise_id: u32, ) { RUNTIME.spawn(async move { let wallet_user_info = WalletUserInfo { siwe_message, siwe_signature, key_payload, key_payload_signature, content_prekey, content_prekey_signature, notif_prekey, notif_prekey_signature, content_one_time_keys, notif_one_time_keys, social_proof, }; let result = login_wallet_user_helper(wallet_user_info).await; handle_string_result_as_callback(result, promise_id); }); } async fn login_wallet_user_helper( wallet_user_info: WalletUserInfo, ) -> Result { let login_request = WalletLoginRequest { siwe_message: wallet_user_info.siwe_message, siwe_signature: wallet_user_info.siwe_signature, device_key_upload: Some(DeviceKeyUpload { device_key_info: Some(IdentityKeyInfo { payload: wallet_user_info.key_payload, payload_signature: wallet_user_info.key_payload_signature, social_proof: Some(wallet_user_info.social_proof), }), content_upload: Some(PreKey { pre_key: wallet_user_info.content_prekey, pre_key_signature: wallet_user_info.content_prekey_signature, }), notif_upload: Some(PreKey { pre_key: wallet_user_info.notif_prekey, pre_key_signature: wallet_user_info.notif_prekey_signature, }), one_time_content_prekeys: wallet_user_info.content_one_time_keys, one_time_notif_prekeys: wallet_user_info.notif_one_time_keys, device_type: DEVICE_TYPE.into(), }), }; let mut identity_client = get_unauthenticated_client( "http://127.0.0.1:50054", CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let login_response = identity_client .login_wallet_user(login_request) .await? .into_inner(); let user_id_and_access_token = UserIDAndDeviceAccessToken { user_id: login_response.user_id, access_token: login_response.access_token, }; Ok(serde_json::to_string(&user_id_and_access_token)?) } struct UpdatePasswordInfo { user_id: String, device_id: String, access_token: String, password: String, } fn update_user_password( user_id: String, device_id: String, access_token: String, password: String, promise_id: u32, ) { RUNTIME.spawn(async move { let update_password_info = UpdatePasswordInfo { access_token, user_id, device_id, password, }; let result = update_user_password_helper(update_password_info).await; handle_void_result_as_callback(result, promise_id); }); } async fn update_user_password_helper( update_password_info: UpdatePasswordInfo, ) -> Result<(), Error> { let mut client_registration = Registration::new(); let opaque_registration_request = client_registration .start(&update_password_info.password) .map_err(handle_error)?; let update_password_start_request = UpdateUserPasswordStartRequest { opaque_registration_request, }; let mut identity_client = get_auth_client( "http://127.0.0.1:50054", update_password_info.user_id, update_password_info.device_id, update_password_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; - let update_password_start_response = identity_client + let response = identity_client .update_user_password_start(update_password_start_request) - .await? - .into_inner(); + .await?; + + // We need to get the load balancer cookie from from the response and send it + // in the subsequent request to ensure it is routed to the same identity + // service instance as the first request + let cookie = response + .metadata() + .get(RESPONSE_METADATA_COOKIE_KEY) + .cloned(); + + let update_password_start_response = response.into_inner(); let opaque_registration_upload = client_registration .finish( &update_password_info.password, &update_password_start_response.opaque_registration_response, ) .map_err(handle_error)?; + let update_password_finish_request = UpdateUserPasswordFinishRequest { session_id: update_password_start_response.session_id, opaque_registration_upload, }; + let mut finish_request = Request::new(update_password_finish_request); + + // Cookie won't be available in local dev environments + if let Some(cookie_metadata) = cookie { + finish_request + .metadata_mut() + .insert(REQUEST_METADATA_COOKIE_KEY, cookie_metadata); + } + identity_client - .update_user_password_finish(update_password_finish_request) + .update_user_password_finish(finish_request) .await?; Ok(()) } fn delete_user( user_id: String, device_id: String, access_token: String, promise_id: u32, ) { RUNTIME.spawn(async move { let auth_info = AuthInfo { access_token, user_id, device_id, }; let result = delete_user_helper(auth_info).await; handle_void_result_as_callback(result, promise_id); }); } async fn delete_user_helper(auth_info: AuthInfo) -> Result<(), Error> { let mut identity_client = get_auth_client( "http://127.0.0.1:50054", auth_info.user_id, auth_info.device_id, auth_info.access_token, CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; identity_client.delete_user(Empty {}).await?; Ok(()) } struct GetOutboundKeysRequestInfo { identifier_type: String, identifier_value: String, device_id: String, } // This struct should not be altered without also updating // OutboundKeyInfoResponse in lib/types/identity-service-types.js #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct OutboundKeyInfoResponse { pub payload: String, pub payload_signature: String, pub social_proof: Option, pub content_prekey: String, pub content_prekey_signature: String, pub notif_prekey: String, pub notif_prekey_signature: String, pub one_time_content_prekey: Option, pub one_time_notif_prekey: Option, } impl TryFrom for OutboundKeyInfoResponse { type Error = Error; fn try_from(key_info: OutboundKeyInfo) -> Result { let identity_info = key_info.identity_info.ok_or(Error::MissingResponseData)?; let IdentityKeyInfo { payload, payload_signature, social_proof, } = identity_info; let content_prekey = key_info.content_prekey.ok_or(Error::MissingResponseData)?; let PreKey { pre_key: content_prekey_value, pre_key_signature: content_prekey_signature, } = content_prekey; let notif_prekey = key_info.notif_prekey.ok_or(Error::MissingResponseData)?; let PreKey { pre_key: notif_prekey_value, pre_key_signature: notif_prekey_signature, } = notif_prekey; let one_time_content_prekey = key_info.one_time_content_prekey; let one_time_notif_prekey = key_info.one_time_notif_prekey; Ok(Self { payload, payload_signature, social_proof, content_prekey: content_prekey_value, content_prekey_signature, notif_prekey: notif_prekey_value, notif_prekey_signature, one_time_content_prekey, one_time_notif_prekey, }) } } fn get_outbound_keys_for_user_device( identifier_type: String, identifier_value: String, device_id: String, promise_id: u32, ) { RUNTIME.spawn(async move { let get_outbound_keys_request_info = GetOutboundKeysRequestInfo { identifier_type, identifier_value, device_id, }; let result = get_outbound_keys_for_user_device_helper(get_outbound_keys_request_info) .await; handle_string_result_as_callback(result, promise_id); }); } async fn get_outbound_keys_for_user_device_helper( get_outbound_keys_request_info: GetOutboundKeysRequestInfo, ) -> Result { let identifier = match get_outbound_keys_request_info.identifier_type.as_str() { "walletAddress" => Some(Identifier::WalletAddress( get_outbound_keys_request_info.identifier_value, )), "username" => Some(Identifier::Username( get_outbound_keys_request_info.identifier_value, )), _ => { return Err(Error::TonicGRPC(tonic::Status::invalid_argument( "invalid identifier", ))) } }; let mut identity_client = get_unauthenticated_client( "http://127.0.0.1:50054", CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; let mut response = identity_client .get_outbound_keys_for_user(OutboundKeysForUserRequest { identifier }) .await? .into_inner(); let outbound_key_info = OutboundKeyInfoResponse::try_from( response .devices .remove(&get_outbound_keys_request_info.device_id) .ok_or(Error::MissingResponseData)?, )?; Ok(serde_json::to_string(&outbound_key_info)?) } #[derive( Debug, derive_more::Display, derive_more::From, derive_more::Error, )] pub enum Error { #[display(...)] TonicGRPC(Status), #[display(...)] SerdeJson(serde_json::Error), #[display(...)] MissingResponseData, GRPClient(grpc_clients::error::Error), } #[cfg(test)] mod tests { use super::CODE_VERSION; #[test] fn test_code_version_exists() { assert!(CODE_VERSION > 0); } } diff --git a/shared/grpc_clients/src/identity/mod.rs b/shared/grpc_clients/src/identity/mod.rs index 747f99be1..da3565952 100644 --- a/shared/grpc_clients/src/identity/mod.rs +++ b/shared/grpc_clients/src/identity/mod.rs @@ -1,20 +1,21 @@ pub mod authenticated; pub mod device; pub mod shared; pub mod unauthenticated; pub mod protos { // This must be named client for authenticated generated code pub mod client { tonic::include_proto!("identity.client"); } pub use client as unauthenticated; pub mod authenticated { tonic::include_proto!("identity.authenticated"); } } pub use authenticated::get_auth_client; pub use device::DeviceType; +pub use shared::{REQUEST_METADATA_COOKIE_KEY, RESPONSE_METADATA_COOKIE_KEY}; pub use unauthenticated::get_unauthenticated_client; diff --git a/shared/grpc_clients/src/identity/shared.rs b/shared/grpc_clients/src/identity/shared.rs index f74e13ab0..fad5d27b5 100644 --- a/shared/grpc_clients/src/identity/shared.rs +++ b/shared/grpc_clients/src/identity/shared.rs @@ -1,57 +1,60 @@ use tonic::{ metadata::{errors::InvalidMetadataValue, Ascii, MetadataValue}, service::Interceptor, Request, Status, }; +pub const RESPONSE_METADATA_COOKIE_KEY: &str = "set-cookie"; +pub const REQUEST_METADATA_COOKIE_KEY: &str = "cookie"; + pub struct CodeVersionLayer { pub(crate) version: u64, pub(crate) device_type: String, } impl Interceptor for CodeVersionLayer { fn call(&mut self, mut request: Request<()>) -> Result, Status> { let metadata = request.metadata_mut(); metadata.insert("code_version", self.version.parse_to_ascii()?); metadata.insert("device_type", self.device_type.parse_to_ascii()?); Ok(request) } } pub trait ToMetadataValueAscii { fn parse_to_ascii(&self) -> Result, Status>; } impl ToMetadataValueAscii for u64 { fn parse_to_ascii(&self) -> Result, Status> { let ascii_string = self.to_string(); ascii_string.parse().map_err(|e: InvalidMetadataValue| { Status::invalid_argument(format!( "Non-Ascii character present in metadata value: {}", e )) }) } } pub struct ChainedInterceptor where A: Interceptor + Send + Sync + 'static, B: Interceptor + Send + Sync + 'static, { pub(crate) first: A, pub(crate) second: B, } impl Interceptor for ChainedInterceptor where A: Interceptor + Send + Sync + 'static, B: Interceptor + Send + Sync + 'static, { fn call(&mut self, request: Request<()>) -> Result, Status> { let request = self.first.call(request)?; self.second.call(request) } }