diff --git a/native/native_rust_library/src/lib.rs b/native/native_rust_library/src/lib.rs index fa6289bde..7c4d8ff0b 100644 --- a/native/native_rust_library/src/lib.rs +++ b/native/native_rust_library/src/lib.rs @@ -1,840 +1,842 @@ 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::get_unauthenticated_client; +use grpc_clients::identity::protos::authenticated::{ + UpdateUserPasswordFinishRequest, UpdateUserPasswordStartRequest, +}; use grpc_clients::identity::protos::client::{ outbound_keys_for_user_request::Identifier, DeleteUserRequest, DeviceKeyUpload, DeviceType, Empty, IdentityKeyInfo, OpaqueLoginFinishRequest, OpaqueLoginStartRequest, OutboundKeyInfo, OutboundKeysForUserRequest, PreKey, RegistrationFinishRequest, - RegistrationStartRequest, UpdateUserPasswordFinishRequest, - UpdateUserPasswordStartRequest, WalletLoginRequest, + RegistrationStartRequest, WalletLoginRequest, }; +use grpc_clients::identity::{get_auth_client, get_unauthenticated_client}; use lazy_static::lazy_static; use serde::Serialize; use std::sync::Arc; use tokio::runtime::{Builder, Runtime}; use tonic::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 .register_password_user_start(registration_start_request) .await? .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 registration_finish_response = identity_client .register_password_user_finish(registration_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 .login_password_user_start(login_start_request) .await? .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 login_finish_response = identity_client .login_password_user_finish(login_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, - access_token: update_password_info.access_token, - user_id: update_password_info.user_id, - device_id_key: update_password_info.device_id, }; - let mut identity_client = get_unauthenticated_client( + 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 .update_user_password_start(update_password_start_request) .await? .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, }; identity_client .update_user_password_finish(update_password_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 delete_user_request = DeleteUserRequest { access_token: auth_info.access_token, user_id: auth_info.user_id, device_id_key: auth_info.device_id, }; let mut identity_client = get_unauthenticated_client( "http://127.0.0.1:50054", CODE_VERSION, DEVICE_TYPE.as_str_name().to_lowercase(), ) .await?; identity_client.delete_user(delete_user_request).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/services/identity/src/client_service.rs b/services/identity/src/client_service.rs index c11405b3e..c86e8470f 100644 --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -1,1022 +1,946 @@ use std::collections::HashMap; // Standard library imports use std::str::FromStr; // External crate imports use aws_sdk_dynamodb::Error as DynamoDBError; use comm_opaque2::grpc::protocol_error_to_grpc_status; use moka::future::Cache; use rand::rngs::OsRng; use siwe::eip55; use tonic::Response; use tracing::{debug, error}; // Workspace crate imports use crate::client_service::client_proto::{ inbound_keys_for_user_request, outbound_keys_for_user_request, AddReservedUsernamesRequest, DeleteUserRequest, Empty, GenerateNonceResponse, InboundKeyInfo, InboundKeysForUserRequest, InboundKeysForUserResponse, LogoutRequest, OpaqueLoginFinishRequest, OpaqueLoginFinishResponse, OpaqueLoginStartRequest, OpaqueLoginStartResponse, OutboundKeyInfo, OutboundKeysForUserRequest, OutboundKeysForUserResponse, RegistrationFinishRequest, RegistrationFinishResponse, RegistrationStartRequest, RegistrationStartResponse, RemoveReservedUsernameRequest, ReservedRegistrationStartRequest, - ReservedWalletLoginRequest, UpdateUserPasswordFinishRequest, - UpdateUserPasswordStartRequest, UpdateUserPasswordStartResponse, - VerifyUserAccessTokenRequest, VerifyUserAccessTokenResponse, - WalletLoginRequest, WalletLoginResponse, + ReservedWalletLoginRequest, VerifyUserAccessTokenRequest, + VerifyUserAccessTokenResponse, WalletLoginRequest, WalletLoginResponse, }; use crate::config::CONFIG; use crate::database::{ DBDeviceTypeInt, DatabaseClient, DeviceType, KeyPayload, }; use crate::error::Error as DBError; use crate::grpc_utils::{DeviceInfoWithAuth, DeviceKeyUploadActions}; use crate::id::generate_uuid; use crate::nonce::generate_nonce_data; use crate::reserved_users::{ validate_account_ownership_message_and_get_user_id, validate_add_reserved_usernames_message, validate_remove_reserved_username_message, }; use crate::siwe::{is_valid_ethereum_address, parse_and_verify_siwe_message}; use crate::token::{AccessTokenData, AuthType}; pub use client_proto::identity_client_service_server::{ IdentityClientService, IdentityClientServiceServer, }; pub mod client_proto { tonic::include_proto!("identity.client"); } #[derive(Clone)] pub enum WorkflowInProgress { Registration(Box), Login(Box), Update(UpdateState), } #[derive(Clone)] pub struct UserRegistrationInfo { pub username: String, pub flattened_device_key_upload: FlattenedDeviceKeyUpload, pub user_id: Option, } #[derive(Clone)] pub struct UserLoginInfo { pub user_id: String, pub flattened_device_key_upload: FlattenedDeviceKeyUpload, pub opaque_server_login: comm_opaque2::server::Login, } #[derive(Clone)] pub struct UpdateState { pub user_id: String, } #[derive(Clone)] pub struct FlattenedDeviceKeyUpload { pub device_id_key: String, pub key_payload: String, pub key_payload_signature: String, pub content_prekey: String, pub content_prekey_signature: String, pub content_one_time_keys: Vec, pub notif_prekey: String, pub notif_prekey_signature: String, pub notif_one_time_keys: Vec, pub device_type: DeviceType, } #[derive(derive_more::Constructor)] pub struct ClientService { client: DatabaseClient, cache: Cache, } #[tonic::async_trait] impl IdentityClientService for ClientService { async fn register_password_user_start( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); debug!("Received registration request for: {}", message.username); self.check_username_taken(&message.username).await?; let username_in_reserved_usernames_table = self .client .username_in_reserved_usernames_table(&message.username) .await .map_err(handle_db_error)?; if username_in_reserved_usernames_table { return Err(tonic::Status::already_exists("username already exists")); } if CONFIG.reserved_usernames.contains(&message.username) || is_valid_ethereum_address(&message.username) { return Err(tonic::Status::invalid_argument("username reserved")); } let registration_state = construct_user_registration_info( &message, None, message.username.clone(), )?; let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start( &CONFIG.server_setup, &message.opaque_registration_request, message.username.as_bytes(), ) .map_err(protocol_error_to_grpc_status)?; let session_id = self .cache .insert_with_uuid_key(WorkflowInProgress::Registration(Box::new( registration_state, ))) .await; let response = RegistrationStartResponse { session_id, opaque_registration_response: server_message, }; Ok(Response::new(response)) } async fn register_reserved_password_user_start( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); self.check_username_taken(&message.username).await?; if CONFIG.reserved_usernames.contains(&message.username) { return Err(tonic::Status::invalid_argument("username reserved")); } let username_in_reserved_usernames_table = self .client .username_in_reserved_usernames_table(&message.username) .await .map_err(handle_db_error)?; if !username_in_reserved_usernames_table { return Err(tonic::Status::permission_denied("username not reserved")); } let user_id = validate_account_ownership_message_and_get_user_id( &message.username, &message.keyserver_message, &message.keyserver_signature, )?; let registration_state = construct_user_registration_info( &message, Some(user_id), message.username.clone(), )?; let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start( &CONFIG.server_setup, &message.opaque_registration_request, message.username.as_bytes(), ) .map_err(protocol_error_to_grpc_status)?; let session_id = self .cache .insert_with_uuid_key(WorkflowInProgress::Registration(Box::new( registration_state, ))) .await; let response = RegistrationStartResponse { session_id, opaque_registration_response: server_message, }; Ok(Response::new(response)) } async fn register_password_user_finish( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); if let Some(WorkflowInProgress::Registration(state)) = self.cache.get(&message.session_id) { self.cache.invalidate(&message.session_id).await; let server_registration = comm_opaque2::server::Registration::new(); let password_file = server_registration .finish(&message.opaque_registration_upload) .map_err(protocol_error_to_grpc_status)?; let device_id = state.flattened_device_key_upload.device_id_key.clone(); let user_id = self .client .add_password_user_to_users_table(*state, password_file) .await .map_err(handle_db_error)?; // Create access token let token = AccessTokenData::new( user_id.clone(), device_id, crate::token::AuthType::Password, &mut OsRng, ); let access_token = token.access_token.clone(); self .client .put_access_token_data(token) .await .map_err(handle_db_error)?; let response = RegistrationFinishResponse { user_id, access_token, }; Ok(Response::new(response)) } else { Err(tonic::Status::not_found("session not found")) } } - async fn update_user_password_start( - &self, - request: tonic::Request, - ) -> Result, tonic::Status> - { - let message = request.into_inner(); - - let token_is_valid = self - .client - .verify_access_token( - message.user_id.clone(), - message.device_id_key, - message.access_token, - ) - .await - .map_err(handle_db_error)?; - - if !token_is_valid { - return Err(tonic::Status::permission_denied("bad token")); - } - - let server_registration = comm_opaque2::server::Registration::new(); - let server_message = server_registration - .start( - &CONFIG.server_setup, - &message.opaque_registration_request, - message.user_id.as_bytes(), - ) - .map_err(protocol_error_to_grpc_status)?; - - let update_state = UpdateState { - user_id: message.user_id, - }; - let session_id = self - .cache - .insert_with_uuid_key(WorkflowInProgress::Update(update_state)) - .await; - - let response = UpdateUserPasswordStartResponse { - session_id, - opaque_registration_response: server_message, - }; - Ok(Response::new(response)) - } - - async fn update_user_password_finish( - &self, - request: tonic::Request, - ) -> Result, tonic::Status> { - let message = request.into_inner(); - - if let Some(WorkflowInProgress::Update(state)) = - self.cache.get(&message.session_id) - { - self.cache.invalidate(&message.session_id).await; - - let server_registration = comm_opaque2::server::Registration::new(); - let password_file = server_registration - .finish(&message.opaque_registration_upload) - .map_err(protocol_error_to_grpc_status)?; - - self - .client - .update_user_password(state.user_id, password_file) - .await - .map_err(handle_db_error)?; - - let response = Empty {}; - Ok(Response::new(response)) - } else { - Err(tonic::Status::not_found("session not found")) - } - } - async fn login_password_user_start( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); debug!("Attempting to login user: {:?}", &message.username); let user_id_and_password_file = self .client .get_user_id_and_password_file_from_username(&message.username) .await .map_err(handle_db_error)?; let (user_id, password_file_bytes) = if let Some(data) = user_id_and_password_file { data } else { // It's possible that the user attempting login is already registered // on Ashoat's keyserver. If they are, we should send back a gRPC status // code instructing them to get a signed message from Ashoat's keyserver // in order to claim their username and register with the Identity // service. let username_in_reserved_usernames_table = self .client .username_in_reserved_usernames_table(&message.username) .await .map_err(handle_db_error)?; if username_in_reserved_usernames_table { return Err(tonic::Status::failed_precondition( "need keyserver message to claim username", )); } return Err(tonic::Status::not_found("user not found")); }; let mut server_login = comm_opaque2::server::Login::new(); let server_response = server_login .start( &CONFIG.server_setup, &password_file_bytes, &message.opaque_login_request, message.username.as_bytes(), ) .map_err(protocol_error_to_grpc_status)?; let login_state = construct_user_login_info(&message, user_id, server_login)?; let session_id = self .cache .insert_with_uuid_key(WorkflowInProgress::Login(Box::new(login_state))) .await; let response = Response::new(OpaqueLoginStartResponse { session_id, opaque_login_response: server_response, }); Ok(response) } async fn login_password_user_finish( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); if let Some(WorkflowInProgress::Login(state)) = self.cache.get(&message.session_id) { self.cache.invalidate(&message.session_id).await; let mut server_login = state.opaque_server_login.clone(); server_login .finish(&message.opaque_login_upload) .map_err(protocol_error_to_grpc_status)?; self .client .add_password_user_device_to_users_table( state.user_id.clone(), state.flattened_device_key_upload.clone(), ) .await .map_err(handle_db_error)?; // Create access token let token = AccessTokenData::new( state.user_id.clone(), state.flattened_device_key_upload.device_id_key, crate::token::AuthType::Password, &mut OsRng, ); let access_token = token.access_token.clone(); self .client .put_access_token_data(token) .await .map_err(handle_db_error)?; let response = OpaqueLoginFinishResponse { user_id: state.user_id, access_token, }; Ok(Response::new(response)) } else { Err(tonic::Status::not_found("session not found")) } } async fn login_wallet_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let parsed_message = parse_and_verify_siwe_message( &message.siwe_message, &message.siwe_signature, )?; match self .client .get_nonce_from_nonces_table(&parsed_message.nonce) .await .map_err(handle_db_error)? { None => return Err(tonic::Status::invalid_argument("invalid nonce")), Some(_) => self .client .remove_nonce_from_nonces_table(&parsed_message.nonce) .await .map_err(handle_db_error)?, }; let wallet_address = eip55(&parsed_message.address); let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; let social_proof = message .social_proof()? .ok_or_else(|| tonic::Status::invalid_argument("malformed payload"))?; let user_id = match self .client .get_user_id_from_user_info(wallet_address.clone(), &AuthType::Wallet) .await .map_err(handle_db_error)? { Some(id) => { // User already exists, so we should update the DDB item self .client .add_wallet_user_device_to_users_table( id.clone(), flattened_device_key_upload.clone(), social_proof, ) .await .map_err(handle_db_error)?; id } None => { // It's possible that the user attempting login is already registered // on Ashoat's keyserver. If they are, we should send back a gRPC status // code instructing them to get a signed message from Ashoat's keyserver // in order to claim their wallet address and register with the Identity // service. let username_in_reserved_usernames_table = self .client .username_in_reserved_usernames_table(&wallet_address) .await .map_err(handle_db_error)?; if username_in_reserved_usernames_table { return Err(tonic::Status::failed_precondition( "need keyserver message to claim username", )); } // User doesn't exist yet and wallet address isn't reserved, so we // should add a new user in DDB self .client .add_wallet_user_to_users_table( flattened_device_key_upload.clone(), wallet_address, social_proof, None, ) .await .map_err(handle_db_error)? } }; // Create access token let token = AccessTokenData::new( user_id.clone(), flattened_device_key_upload.device_id_key, crate::token::AuthType::Password, &mut OsRng, ); let access_token = token.access_token.clone(); self .client .put_access_token_data(token) .await .map_err(handle_db_error)?; let response = WalletLoginResponse { user_id, access_token, }; Ok(Response::new(response)) } async fn login_reserved_wallet_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let parsed_message = parse_and_verify_siwe_message( &message.siwe_message, &message.siwe_signature, )?; match self .client .get_nonce_from_nonces_table(&parsed_message.nonce) .await .map_err(handle_db_error)? { None => return Err(tonic::Status::invalid_argument("invalid nonce")), Some(_) => self .client .remove_nonce_from_nonces_table(&parsed_message.nonce) .await .map_err(handle_db_error)?, }; let wallet_address = eip55(&parsed_message.address); self.check_wallet_address_taken(&wallet_address).await?; let wallet_address_in_reserved_usernames_table = self .client .username_in_reserved_usernames_table(&wallet_address) .await .map_err(handle_db_error)?; if !wallet_address_in_reserved_usernames_table { return Err(tonic::Status::permission_denied( "wallet address not reserved", )); } let user_id = validate_account_ownership_message_and_get_user_id( &wallet_address, &message.keyserver_message, &message.keyserver_signature, )?; let flattened_device_key_upload = construct_flattened_device_key_upload(&message)?; let social_proof = message .social_proof()? .ok_or_else(|| tonic::Status::invalid_argument("malformed payload"))?; self .client .add_wallet_user_to_users_table( flattened_device_key_upload.clone(), wallet_address, social_proof, Some(user_id.clone()), ) .await .map_err(handle_db_error)?; let token = AccessTokenData::new( user_id.clone(), flattened_device_key_upload.device_id_key, crate::token::AuthType::Password, &mut OsRng, ); let access_token = token.access_token.clone(); self .client .put_access_token_data(token) .await .map_err(handle_db_error)?; let response = WalletLoginResponse { user_id, access_token, }; Ok(Response::new(response)) } async fn log_out_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let token_is_valid = self .client .verify_access_token( message.user_id.clone(), message.device_id_key.clone(), message.access_token, ) .await .map_err(handle_db_error)?; if !token_is_valid { return Err(tonic::Status::permission_denied("bad token")); } self .client .remove_device_from_users_table( message.user_id.clone(), message.device_id_key.clone(), ) .await .map_err(handle_db_error)?; self .client .delete_access_token_data(message.user_id, message.device_id_key) .await .map_err(handle_db_error)?; let response = Empty {}; Ok(Response::new(response)) } async fn delete_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let token_is_valid = self .client .verify_access_token( message.user_id.clone(), message.device_id_key, message.access_token, ) .await .map_err(handle_db_error)?; if !token_is_valid { return Err(tonic::Status::permission_denied("bad token")); } self .client .delete_user(message.user_id) .await .map_err(handle_db_error)?; let response = Empty {}; Ok(Response::new(response)) } async fn generate_nonce( &self, _request: tonic::Request, ) -> Result, tonic::Status> { let nonce_data = generate_nonce_data(&mut OsRng); match self .client .add_nonce_to_nonces_table(nonce_data.clone()) .await { Ok(_) => Ok(Response::new(GenerateNonceResponse { nonce: nonce_data.nonce, })), Err(e) => Err(handle_db_error(e)), } } async fn get_outbound_keys_for_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); use outbound_keys_for_user_request::Identifier; let (user_ident, auth_type) = match message.identifier { None => { return Err(tonic::Status::invalid_argument("no identifier provided")) } Some(Identifier::Username(username)) => (username, AuthType::Password), Some(Identifier::WalletAddress(address)) => (address, AuthType::Wallet), }; let devices_map = self .client .get_keys_for_user(user_ident, &auth_type, true) .await .map_err(handle_db_error)? .ok_or_else(|| match auth_type { AuthType::Password => tonic::Status::not_found("username not found"), AuthType::Wallet => { tonic::Status::not_found("wallet address not found") } })?; let transformed_devices = devices_map .into_iter() .filter_map(|(key, device_info)| { let device_info_with_auth = DeviceInfoWithAuth { device_info, auth_type: &auth_type, }; match OutboundKeyInfo::try_from(device_info_with_auth) { Ok(key_info) => Some((key, key_info)), Err(_) => { error!("Failed to transform device info for key {}", key); None } } }) .collect::>(); Ok(tonic::Response::new(OutboundKeysForUserResponse { devices: transformed_devices, })) } async fn get_inbound_keys_for_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); use inbound_keys_for_user_request::Identifier; let (user_ident, auth_type) = match message.identifier { None => { return Err(tonic::Status::invalid_argument("no identifier provided")) } Some(Identifier::Username(username)) => (username, AuthType::Password), Some(Identifier::WalletAddress(address)) => (address, AuthType::Wallet), }; let devices_map = self .client .get_keys_for_user(user_ident, &auth_type, false) .await .map_err(handle_db_error)? .ok_or_else(|| match auth_type { AuthType::Password => tonic::Status::not_found("username not found"), AuthType::Wallet => { tonic::Status::not_found("wallet address not found") } })?; let transformed_devices = devices_map .into_iter() .filter_map(|(key, device_info)| { let device_info_with_auth = DeviceInfoWithAuth { device_info, auth_type: &auth_type, }; match InboundKeyInfo::try_from(device_info_with_auth) { Ok(key_info) => Some((key, key_info)), Err(_) => { error!("Failed to transform device info for key {}", key); None } } }) .collect::>(); Ok(tonic::Response::new(InboundKeysForUserResponse { devices: transformed_devices, })) } async fn verify_user_access_token( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); debug!("Verifying device: {}", &message.signing_public_key); let token_valid = self .client .verify_access_token( message.user_id, message.signing_public_key.clone(), message.access_token, ) .await .map_err(handle_db_error)?; let response = Response::new(VerifyUserAccessTokenResponse { token_valid }); debug!( "device {} was verified: {}", &message.signing_public_key, token_valid ); Ok(response) } async fn add_reserved_usernames( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let usernames = validate_add_reserved_usernames_message( &message.message, &message.signature, )?; let filtered_usernames = self .client .filter_out_taken_usernames(usernames) .await .map_err(handle_db_error)?; self .client .add_usernames_to_reserved_usernames_table(filtered_usernames) .await .map_err(handle_db_error)?; let response = Response::new(Empty {}); Ok(response) } async fn remove_reserved_username( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let username = validate_remove_reserved_username_message( &message.message, &message.signature, )?; self .client .delete_username_from_reserved_usernames_table(username) .await .map_err(handle_db_error)?; let response = Response::new(Empty {}); Ok(response) } async fn ping( &self, _request: tonic::Request, ) -> Result, tonic::Status> { let response = Response::new(Empty {}); Ok(response) } } impl ClientService { async fn check_username_taken( &self, username: &str, ) -> Result<(), tonic::Status> { let username_taken = self .client .username_taken(username.to_string()) .await .map_err(handle_db_error)?; if username_taken { return Err(tonic::Status::already_exists("username already exists")); } Ok(()) } async fn check_wallet_address_taken( &self, wallet_address: &str, ) -> Result<(), tonic::Status> { let wallet_address_taken = self .client .wallet_address_taken(wallet_address.to_string()) .await .map_err(handle_db_error)?; if wallet_address_taken { return Err(tonic::Status::already_exists( "wallet address already exists", )); } Ok(()) } } #[tonic::async_trait] pub trait CacheExt { async fn insert_with_uuid_key(&self, value: T) -> String; } #[tonic::async_trait] impl CacheExt for Cache where T: Clone + Send + Sync + 'static, { async fn insert_with_uuid_key(&self, value: T) -> String { let session_id = generate_uuid(); self.insert(session_id.clone(), value).await; session_id } } pub fn handle_db_error(db_error: DBError) -> tonic::Status { match db_error { DBError::AwsSdk(DynamoDBError::InternalServerError(_)) | DBError::AwsSdk(DynamoDBError::ProvisionedThroughputExceededException( _, )) | DBError::AwsSdk(DynamoDBError::RequestLimitExceeded(_)) => { tonic::Status::unavailable("please retry") } e => { error!("Encountered an unexpected error: {}", e); tonic::Status::failed_precondition("unexpected error") } } } fn construct_user_registration_info( message: &impl DeviceKeyUploadActions, user_id: Option, username: String, ) -> Result { Ok(UserRegistrationInfo { username, flattened_device_key_upload: construct_flattened_device_key_upload( message, )?, user_id, }) } fn construct_user_login_info( message: &impl DeviceKeyUploadActions, user_id: String, opaque_server_login: comm_opaque2::server::Login, ) -> Result { Ok(UserLoginInfo { user_id, flattened_device_key_upload: construct_flattened_device_key_upload( message, )?, opaque_server_login, }) } fn construct_flattened_device_key_upload( message: &impl DeviceKeyUploadActions, ) -> Result { let key_info = KeyPayload::from_str(&message.payload()?) .map_err(|_| tonic::Status::invalid_argument("malformed payload"))?; let flattened_device_key_upload = FlattenedDeviceKeyUpload { device_id_key: key_info.primary_identity_public_keys.ed25519, key_payload: message.payload()?, key_payload_signature: message.payload_signature()?, content_prekey: message.content_prekey()?, content_prekey_signature: message.content_prekey_signature()?, content_one_time_keys: message.one_time_content_prekeys()?, notif_prekey: message.notif_prekey()?, notif_prekey_signature: message.notif_prekey_signature()?, notif_one_time_keys: message.one_time_notif_prekeys()?, device_type: DeviceType::try_from(DBDeviceTypeInt(message.device_type()?)) .map_err(handle_db_error)?, }; Ok(flattened_device_key_upload) } diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs index 0b4966a2a..b2443347d 100644 --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -1,197 +1,267 @@ +use crate::config::CONFIG; use crate::{ - client_service::handle_db_error, constants::request_metadata, - database::DatabaseClient, grpc_services::shared::get_value, token::AuthType, + client_service::{ + handle_db_error, CacheExt, UpdateState, WorkflowInProgress, + }, + constants::request_metadata, + database::DatabaseClient, + grpc_services::shared::get_value, + token::AuthType, }; +use comm_opaque2::grpc::protocol_error_to_grpc_status; +use moka::future::Cache; use tonic::{Request, Response, Status}; // This must be named client, because generated code from the authenticated // protobuf file references message structs from the client protobuf file // with the client:: namespace pub mod client { tonic::include_proto!("identity.client"); } pub mod auth_proto { tonic::include_proto!("identity.authenticated"); } use auth_proto::{ find_user_id_request, identity_client_service_server::IdentityClientService, FindUserIdRequest, FindUserIdResponse, KeyserverKeysResponse, OutboundKeyInfo, OutboundKeysForUserRequest, RefreshUserPreKeysRequest, UploadOneTimeKeysRequest, }; use client::{Empty, IdentityKeyInfo}; use tracing::debug; #[derive(derive_more::Constructor)] pub struct AuthenticatedService { db_client: DatabaseClient, + cache: Cache, } fn get_auth_info(req: &Request<()>) -> Option<(String, String, String)> { debug!("Retrieving auth info for request: {:?}", req); let user_id = get_value(req, request_metadata::USER_ID)?; let device_id = get_value(req, request_metadata::DEVICE_ID)?; let access_token = get_value(req, request_metadata::ACCESS_TOKEN)?; Some((user_id, device_id, access_token)) } pub fn auth_interceptor( req: Request<()>, db_client: &DatabaseClient, ) -> Result, Status> { debug!("Intercepting request to check auth info: {:?}", req); let (user_id, device_id, access_token) = get_auth_info(&req) .ok_or_else(|| Status::unauthenticated("Missing credentials"))?; let handle = tokio::runtime::Handle::current(); let new_db_client = db_client.clone(); // This function cannot be `async`, yet must call the async db call // Force tokio to resolve future in current thread without an explicit .await let valid_token = tokio::task::block_in_place(move || { handle .block_on(new_db_client.verify_access_token( user_id, device_id, access_token, )) .map_err(handle_db_error) })?; if !valid_token { return Err(Status::aborted("Bad Credentials")); } Ok(req) } pub fn get_user_and_device_id( request: &Request, ) -> Result<(String, String), Status> { let user_id = get_value(request, request_metadata::USER_ID) .ok_or_else(|| Status::unauthenticated("Missing user_id field"))?; let device_id = get_value(request, request_metadata::DEVICE_ID) .ok_or_else(|| Status::unauthenticated("Missing device_id field"))?; Ok((user_id, device_id)) } #[tonic::async_trait] impl IdentityClientService for AuthenticatedService { async fn refresh_user_pre_keys( &self, request: Request, ) -> Result, Status> { let (user_id, device_id) = get_user_and_device_id(&request)?; let message = request.into_inner(); debug!("Refreshing prekeys for user: {}", user_id); let content_keys = message .new_content_pre_keys .ok_or_else(|| Status::invalid_argument("Missing content keys"))?; let notif_keys = message .new_notif_pre_keys .ok_or_else(|| Status::invalid_argument("Missing notification keys"))?; self .db_client .set_prekey( user_id, device_id, content_keys.pre_key, content_keys.pre_key_signature, notif_keys.pre_key, notif_keys.pre_key_signature, ) .await .map_err(handle_db_error)?; let response = Response::new(Empty {}); Ok(response) } async fn get_keyserver_keys( &self, request: Request, ) -> Result, Status> { let message = request.into_inner(); let inner_response = self .db_client .get_keyserver_keys_for_user(&message.user_id) .await .map_err(handle_db_error)? .map(|db_keys| OutboundKeyInfo { identity_info: Some(IdentityKeyInfo { payload: db_keys.key_payload, payload_signature: db_keys.key_payload_signature, social_proof: db_keys.social_proof, }), content_prekey: Some(client::PreKey { pre_key: db_keys.content_prekey.prekey, pre_key_signature: db_keys.content_prekey.prekey_signature, }), notif_prekey: Some(client::PreKey { pre_key: db_keys.notif_prekey.prekey, pre_key_signature: db_keys.notif_prekey.prekey_signature, }), one_time_content_prekey: db_keys.content_one_time_key, one_time_notif_prekey: db_keys.notif_one_time_key, }); let response = Response::new(KeyserverKeysResponse { keyserver_info: inner_response, }); return Ok(response); } async fn upload_one_time_keys( &self, request: tonic::Request, ) -> Result, tonic::Status> { let (user_id, device_id) = get_user_and_device_id(&request)?; let message = request.into_inner(); debug!("Attempting to update one time keys for user: {}", user_id); self .db_client .append_one_time_prekeys( device_id, message.content_one_time_pre_keys, message.notif_one_time_pre_keys, ) .await .map_err(handle_db_error)?; Ok(tonic::Response::new(Empty {})) } async fn find_user_id( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); use find_user_id_request::Identifier; let (user_ident, auth_type) = match message.identifier { None => { return Err(tonic::Status::invalid_argument("no identifier provided")) } Some(Identifier::Username(username)) => (username, AuthType::Password), Some(Identifier::WalletAddress(address)) => (address, AuthType::Wallet), }; let user_id = self .db_client .get_user_id_from_user_info(user_ident, &auth_type) .await .map_err(handle_db_error)?; Ok(Response::new(FindUserIdResponse { user_id })) } + + async fn update_user_password_start( + &self, + request: tonic::Request, + ) -> Result< + tonic::Response, + tonic::Status, + > { + let (user_id, _) = get_user_and_device_id(&request)?; + let message = request.into_inner(); + + let server_registration = comm_opaque2::server::Registration::new(); + let server_message = server_registration + .start( + &CONFIG.server_setup, + &message.opaque_registration_request, + user_id.as_bytes(), + ) + .map_err(protocol_error_to_grpc_status)?; + + let update_state = UpdateState { user_id }; + let session_id = self + .cache + .insert_with_uuid_key(WorkflowInProgress::Update(update_state)) + .await; + + let response = auth_proto::UpdateUserPasswordStartResponse { + session_id, + opaque_registration_response: server_message, + }; + Ok(Response::new(response)) + } + + async fn update_user_password_finish( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let message = request.into_inner(); + + let Some(WorkflowInProgress::Update(state)) = + self.cache.get(&message.session_id) + else { + return Err(tonic::Status::not_found("session not found")); + }; + + self.cache.invalidate(&message.session_id).await; + + let server_registration = comm_opaque2::server::Registration::new(); + let password_file = server_registration + .finish(&message.opaque_registration_upload) + .map_err(protocol_error_to_grpc_status)?; + + self + .db_client + .update_user_password(state.user_id, password_file) + .await + .map_err(handle_db_error)?; + + let response = Empty {}; + Ok(Response::new(response)) + } } diff --git a/services/identity/src/main.rs b/services/identity/src/main.rs index 9c62178e4..eed1d766e 100644 --- a/services/identity/src/main.rs +++ b/services/identity/src/main.rs @@ -1,110 +1,110 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use database::DatabaseClient; use moka::future::Cache; use tonic::transport::Server; use tonic_web::GrpcWebLayer; mod client_service; mod config; pub mod constants; mod cors; mod database; pub mod ddb_utils; pub mod error; mod grpc_services; mod grpc_utils; mod id; mod keygen; mod nonce; mod reserved_users; mod siwe; mod token; mod tunnelbroker; use config::load_config; use constants::{IDENTITY_SERVICE_SOCKET_ADDR, SECRETS_DIRECTORY}; use cors::cors_layer; use keygen::generate_and_persist_keypair; use tracing::{self, info, Level}; use tracing_subscriber::EnvFilter; use client_service::{ClientService, IdentityClientServiceServer}; use grpc_services::authenticated::auth_proto::identity_client_service_server::IdentityClientServiceServer as AuthServer; use grpc_services::authenticated::AuthenticatedService; #[derive(Parser)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] struct Cli { #[clap(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Runs the server Server, /// Generates and persists a keypair to use for PAKE registration and login Keygen { #[clap(short, long)] #[clap(default_value_t = String::from(SECRETS_DIRECTORY))] dir: String, }, /// Populates the `identity-users` table in DynamoDB from MySQL PopulateDB, } #[tokio::main] async fn main() -> Result<(), Box> { 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)?; let cli = Cli::parse(); match &cli.command { Commands::Keygen { dir } => { generate_and_persist_keypair(dir)?; } Commands::Server => { load_config(); let addr = IDENTITY_SERVICE_SOCKET_ADDR.parse()?; let aws_config = aws_config::from_env().region("us-east-2").load().await; let database_client = DatabaseClient::new(&aws_config); let workflow_cache = Cache::builder() .time_to_live(Duration::from_secs(10)) .build(); let inner_client_service = - ClientService::new(database_client.clone(), workflow_cache); + ClientService::new(database_client.clone(), workflow_cache.clone()); let client_service = IdentityClientServiceServer::with_interceptor( inner_client_service, grpc_services::shared::version_interceptor, ); let inner_auth_service = - AuthenticatedService::new(database_client.clone()); + AuthenticatedService::new(database_client.clone(), workflow_cache); let auth_service = AuthServer::with_interceptor(inner_auth_service, move |req| { grpc_services::authenticated::auth_interceptor(req, &database_client) .and_then(grpc_services::shared::version_interceptor) }); info!("Listening to gRPC traffic on {}", addr); Server::builder() .accept_http1(true) .layer(cors_layer()) .layer(GrpcWebLayer::new()) .add_service(client_service) .add_service(auth_service) .serve(addr) .await?; } Commands::PopulateDB => unimplemented!(), } Ok(()) } diff --git a/shared/protos/identity_authenticated.proto b/shared/protos/identity_authenticated.proto index 1758f4825..e30330b9b 100644 --- a/shared/protos/identity_authenticated.proto +++ b/shared/protos/identity_authenticated.proto @@ -1,81 +1,111 @@ syntax = "proto3"; import "identity_client.proto"; package identity.authenticated; // RPCs from a client (iOS, Android, or web) to identity service // // This service will assert authenticity of a device by verifying the access // token through an interceptor, thus avoiding the need to explicitly pass // the credentials on every request service IdentityClientService { // Replenish one-time preKeys rpc UploadOneTimeKeys(UploadOneTimeKeysRequest) returns (identity.client.Empty) {} // Rotate a device's preKey and preKey signature // Rotated for deniability of older messages rpc RefreshUserPreKeys(RefreshUserPreKeysRequest) returns (identity.client.Empty) {} + // Called by user to update password and receive new access token + rpc UpdateUserPasswordStart(UpdateUserPasswordStartRequest) returns + (UpdateUserPasswordStartResponse) {} + rpc UpdateUserPasswordFinish(UpdateUserPasswordFinishRequest) returns + (identity.client.Empty) {} + // Called by clients to get required keys for opening a connection // to a user's keyserver rpc GetKeyserverKeys(OutboundKeysForUserRequest) returns (KeyserverKeysResponse) {} // Returns userID for given username or wallet address rpc FindUserID(FindUserIDRequest) returns (FindUserIDResponse) {} } // Helper types // UploadOneTimeKeys // As OPKs get exhausted, they need to be refreshed message UploadOneTimeKeysRequest { repeated string contentOneTimePreKeys = 1; repeated string notifOneTimePreKeys = 2; } // RefreshUserPreKeys message RefreshUserPreKeysRequest { identity.client.PreKey newContentPreKeys = 1; identity.client.PreKey newNotifPreKeys = 2; } // Information needed when establishing communication to someone else's device message OutboundKeyInfo { identity.client.IdentityKeyInfo identityInfo = 1; identity.client.PreKey contentPrekey = 2; identity.client.PreKey notifPrekey = 3; optional string oneTimeContentPrekey = 4; optional string oneTimeNotifPrekey = 5; } message KeyserverKeysResponse { optional OutboundKeyInfo keyserverInfo = 1; } // Information needed by a device to establish communcation when responding // to a request. // The device receiving a request only needs the content key and prekey. message OutboundKeysForUserRequest { string userID = 1; } // FindUserID message FindUserIDRequest { oneof identifier { string username = 1; string walletAddress = 2; } } message FindUserIDResponse { // none if user not found optional string userID = 1; } + +// UpdateUserPassword + +// Request for updating a user, similar to registration but need a +// access token to validate user before updating password +message UpdateUserPasswordStartRequest { + // Message sent to initiate PAKE registration (step 1) + bytes opaqueRegistrationRequest = 1; +} + +// Do a user registration, but overwrite the existing credentials +// after validation of user +message UpdateUserPasswordFinishRequest { + // Identifier used to correlate start and finish request + string sessionID = 1; + // Opaque client registration upload (step 3) + bytes opaqueRegistrationUpload = 2; +} + +message UpdateUserPasswordStartResponse { + // Identifier used to correlate start request with finish request + string sessionID = 1; + bytes opaqueRegistrationResponse = 2; +} diff --git a/shared/protos/identity_client.proto b/shared/protos/identity_client.proto index c395c0944..a0c8f47a1 100644 --- a/shared/protos/identity_client.proto +++ b/shared/protos/identity_client.proto @@ -1,367 +1,332 @@ syntax = "proto3"; package identity.client; // RPCs from a client (iOS, Android, or web) to identity service service IdentityClientService { // Account actions // Called by user to register with the Identity Service (PAKE only) // Due to limitations of grpc-web, the Opaque challenge+response // needs to be split up over two unary requests // Start/Finish is used here to align with opaque protocol rpc RegisterPasswordUserStart(RegistrationStartRequest) returns ( RegistrationStartResponse) {} rpc RegisterReservedPasswordUserStart(ReservedRegistrationStartRequest) returns (RegistrationStartResponse) {} rpc RegisterPasswordUserFinish(RegistrationFinishRequest) returns ( RegistrationFinishResponse) {} - // Called by user to update password and receive new access token - rpc UpdateUserPasswordStart(UpdateUserPasswordStartRequest) returns - (UpdateUserPasswordStartResponse) {} - rpc UpdateUserPasswordFinish(UpdateUserPasswordFinishRequest) returns - (Empty) {} // Called by user to register device and get an access token rpc LoginPasswordUserStart(OpaqueLoginStartRequest) returns (OpaqueLoginStartResponse) {} rpc LoginPasswordUserFinish(OpaqueLoginFinishRequest) returns (OpaqueLoginFinishResponse) {} rpc LoginWalletUser(WalletLoginRequest) returns (WalletLoginResponse) {} rpc LoginReservedWalletUser(ReservedWalletLoginRequest) returns (WalletLoginResponse) {} // Called by user to log out (clears device's keys and access token) rpc LogOutUser(LogoutRequest) returns (Empty) {} // Called by a user to delete their own account rpc DeleteUser(DeleteUserRequest) returns (Empty) {} // Sign-In with Ethereum actions // Called by clients to get a nonce for a Sign-In with Ethereum message rpc GenerateNonce(Empty) returns (GenerateNonceResponse) {} // X3DH actions // Called by clients to get all device keys associated with a user in order // to open a new channel of communication on any of their devices. // Specially, this will return the following per device: // - Identity keys (both Content and Notif Keys) // - PreKey (including preKey signature) // - One-time PreKey rpc GetOutboundKeysForUser(OutboundKeysForUserRequest) returns (OutboundKeysForUserResponse) {} // Called by receivers of a communication request. The reponse will only // return identity keys (both content and notif keys) and related prekeys per // device, but will not contain one-time keys. rpc GetInboundKeysForUser(InboundKeysForUserRequest) returns (InboundKeysForUserResponse) {} // Service actions // Called by other services to verify a user's access token rpc VerifyUserAccessToken(VerifyUserAccessTokenRequest) returns (VerifyUserAccessTokenResponse) {} // Ashoat's keyserver actions // Called by Ashoat's keyserver to add usernames to the Identity service's // reserved list rpc AddReservedUsernames(AddReservedUsernamesRequest) returns (Empty) {} // Called by Ashoat's keyserver to remove usernames from the Identity // service's reserved list rpc RemoveReservedUsername(RemoveReservedUsernameRequest) returns (Empty) {} // Miscellaneous actions // Called by users periodically to check if their code version is supported rpc Ping(Empty) returns (Empty) {} } // Helper types message Empty {} message PreKey { string preKey = 1; string preKeySignature = 2; } // Key information needed for starting a X3DH session message IdentityKeyInfo { // JSON payload containing Olm keys // Sessions for users will contain both ContentKeys and NotifKeys // For keyservers, this will only contain ContentKeys string payload = 1; // Payload signed with the signing ed25519 key string payloadSignature = 2; // Signed message used for SIWE // This correlates a given wallet with a device's content key optional string socialProof = 3; } // RegisterUser // Ephemeral information provided so others can create initial message // to this device // // Prekeys are generally rotated periodically // One-time Prekeys are "consumed" after first use, so many need to // be provide to avoid exhausting them. enum DeviceType { Keyserver = 0; Web = 1; // iOS doesn't leave a good option for title to camel case renaming Ios = 2; Android = 3; Windows = 4; MacOS = 5; } // Bundle of information needed for creating an initial message using X3DH message DeviceKeyUpload { IdentityKeyInfo deviceKeyInfo = 1; PreKey contentUpload = 2; PreKey notifUpload = 3; repeated string oneTimeContentPrekeys = 4; repeated string oneTimeNotifPrekeys = 5; DeviceType deviceType = 6; } // Request for registering a new user message RegistrationStartRequest { // Message sent to initiate PAKE registration (step 1) bytes opaqueRegistrationRequest = 1; string username = 2; // Information needed to open a new channel to current user's device DeviceKeyUpload deviceKeyUpload = 3; } message ReservedRegistrationStartRequest { // Message sent to initiate PAKE registration (step 1) bytes opaqueRegistrationRequest = 1; string username = 2; // Information needed to open a new channel to current user's device DeviceKeyUpload deviceKeyUpload = 3; // Message from Ashoat's keyserver attesting that a given user has ownership // of a given username string keyserverMessage = 4; // Above message signed with Ashoat's keyserver's signing ed25519 key string keyserverSignature = 5; } // Messages sent from a client to Identity Service message RegistrationFinishRequest { // Identifier to correlate RegisterStart session string sessionID = 1; // Final message in PAKE registration bytes opaqueRegistrationUpload = 2; } // Messages sent from Identity Service to client message RegistrationStartResponse { // Identifier used to correlate start request with finish request string sessionID = 1; // sent to the user upon reception of the PAKE registration attempt // (step 2) bytes opaqueRegistrationResponse = 2; } message RegistrationFinishResponse { // Unique identifier for newly registered user string userID = 1; // After successful unpacking of user credentials, return token string accessToken = 2; } -// UpdateUserPassword - -// Request for updating a user, similar to registration but need a -// access token to validate user before updating password -message UpdateUserPasswordStartRequest { - // Message sent to initiate PAKE registration (step 1) - bytes opaqueRegistrationRequest = 1; - // Used to validate user, before attempting to update password - string accessToken = 2; - string userID = 3; - // Public ed25519 key used for signing. We need this to look up a device's - // access token - string deviceIDKey = 4; -} - -// Do a user registration, but overwrite the existing credentials -// after validation of user -message UpdateUserPasswordFinishRequest { - // Identifier used to correlate start and finish request - string sessionID = 1; - // Opaque client registration upload (step 3) - bytes opaqueRegistrationUpload = 2; -} - -message UpdateUserPasswordStartResponse { - // Identifier used to correlate start request with finish request - string sessionID = 1; - bytes opaqueRegistrationResponse = 2; -} - // LoginUser message OpaqueLoginStartRequest { string username = 1; // Message sent to initiate PAKE login (step 1) bytes opaqueLoginRequest = 2; // Information specific to a user's device needed to open a new channel of // communication with this user DeviceKeyUpload deviceKeyUpload = 3; } message OpaqueLoginFinishRequest { // Identifier used to correlate start request with finish request string sessionID = 1; // Message containing client's reponse to server challenge. // Used to verify that client holds password secret (Step 3) bytes opaqueLoginUpload = 2; } message OpaqueLoginStartResponse { // Identifier used to correlate start request with finish request string sessionID = 1; // Opaque challenge sent from server to client attempting to login (Step 2) bytes opaqueLoginResponse = 2; } message OpaqueLoginFinishResponse { string userID = 1; // Mint and return a new access token upon successful login string accessToken = 2; } message WalletLoginRequest { string siweMessage = 1; string siweSignature = 2; // Information specific to a user's device needed to open a new channel of // communication with this user DeviceKeyUpload deviceKeyUpload = 3; } message ReservedWalletLoginRequest { string siweMessage = 1; string siweSignature = 2; // Information specific to a user's device needed to open a new channel of // communication with this user DeviceKeyUpload deviceKeyUpload = 3; // Message from Ashoat's keyserver attesting that a given user has ownership // of a given wallet address string keyserverMessage = 4; // Above message signed with Ashoat's keyserver's signing ed25519 key string keyserverSignature = 5; } message WalletLoginResponse { string userID = 1; string accessToken = 2; } // LogOutUser message LogoutRequest { string accessToken = 1; string userID = 2; // Public ed25519 key used for signing. We need this to look up a device's // access token string deviceIDKey = 3; } // DeleteUser message DeleteUserRequest { string accessToken = 1; string userID = 2; // Public ed25519 key used for signing. We need this to look up a device's // access token string deviceIDKey = 3; } // GenerateNonce message GenerateNonceResponse{ string nonce = 1; } // GetOutboundKeysForUser // Information needed when establishing communication to someone else's device message OutboundKeyInfo { IdentityKeyInfo identityInfo = 1; PreKey contentPrekey = 2; PreKey notifPrekey = 3; optional string oneTimeContentPrekey = 4; optional string oneTimeNotifPrekey = 5; } // Information needed by a device to establish communcation when responding // to a request. // The device receiving a request only needs the content key and prekey. message OutboundKeysForUserRequest { oneof identifier { string username = 1; string walletAddress = 2; } } message OutboundKeysForUserResponse { // Map is keyed on devices' public ed25519 key used for signing map devices = 1; } // GetInboundKeysForUser message InboundKeyInfo { IdentityKeyInfo identityInfo = 1; PreKey contentPrekey = 2; PreKey notifPrekey = 3; } message InboundKeysForUserRequest { oneof identifier { string username = 1; string walletAddress = 2; } } message InboundKeysForUserResponse { // Map is keyed on devices' public ed25519 key used for signing map devices = 1; } // VerifyUserAccessToken message VerifyUserAccessTokenRequest { string userID = 1; // signing ed25519 key for the given user's device string signingPublicKey = 2; string accessToken = 3; } message VerifyUserAccessTokenResponse { bool tokenValid = 1; } // AddReservedUsernames message AddReservedUsernamesRequest { // Message from Ashoat's keyserver containing the username to be added string message = 1; // Above message signed with Ashoat's keyserver's signing ed25519 key string signature = 2; } // RemoveReservedUsername message RemoveReservedUsernameRequest { // Message from Ashoat's keyserver containing the username to be removed string message = 1; // Above message signed with Ashoat's keyserver's signing ed25519 key string signature = 2; }