diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs index 2b25dd6fa..7034dced0 100644 --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -1,773 +1,770 @@ // 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::{ +use crate::config::CONFIG; +use crate::database::{ + DBDeviceTypeInt, DatabaseClient, DeviceType, KeyPayload, +}; +use crate::error::Error as DBError; +use crate::grpc_services::protos::unauth::{ AddReservedUsernamesRequest, Empty, GenerateNonceResponse, OpaqueLoginFinishRequest, OpaqueLoginFinishResponse, OpaqueLoginStartRequest, OpaqueLoginStartResponse, RegistrationFinishRequest, RegistrationFinishResponse, RegistrationStartRequest, RegistrationStartResponse, RemoveReservedUsernameRequest, ReservedRegistrationStartRequest, 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::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"); -} +pub use crate::grpc_services::protos::client::identity_client_service_server::{ + IdentityClientService, IdentityClientServiceServer, + }; #[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 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 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 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 66ed032ef..fcfec499d 100644 --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -1,378 +1,370 @@ use std::collections::HashMap; use crate::config::CONFIG; use crate::grpc_utils::DeviceInfoWithAuth; use crate::{ 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}; +use tracing::{debug, error}; -// 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 -use crate::client_service::client_proto as client; - -pub mod auth_proto { - tonic::include_proto!("identity.authenticated"); -} -use auth_proto::{ +use super::protos::auth::{ find_user_id_request, identity_client_service_server::IdentityClientService, FindUserIdRequest, FindUserIdResponse, InboundKeyInfo, InboundKeysForUserRequest, InboundKeysForUserResponse, KeyserverKeysResponse, OutboundKeyInfo, OutboundKeysForUserRequest, OutboundKeysForUserResponse, - RefreshUserPreKeysRequest, UploadOneTimeKeysRequest, + RefreshUserPreKeysRequest, UpdateUserPasswordFinishRequest, + UpdateUserPasswordStartRequest, UpdateUserPasswordStartResponse, + UploadOneTimeKeysRequest, }; -use client::{Empty, IdentityKeyInfo}; -use tracing::{debug, error}; +use super::protos::client::{Empty, IdentityKeyInfo, PreKey}; #[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_outbound_keys_for_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let devices_map = self .db_client .get_keys_for_user_id(&message.user_id, true) .await .map_err(handle_db_error)? .ok_or_else(|| tonic::Status::not_found("user not found"))?; let transformed_devices = devices_map .into_iter() .filter_map(|(key, device_info)| { let device_info_with_auth = DeviceInfoWithAuth { device_info, auth_type: None, }; 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(); let devices_map = self .db_client .get_keys_for_user_id(&message.user_id, false) .await .map_err(handle_db_error)? .ok_or_else(|| tonic::Status::not_found("user not found"))?; let transformed_devices = devices_map .into_iter() .filter_map(|(key, device_info)| { let device_info_with_auth = DeviceInfoWithAuth { device_info, auth_type: None, }; 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 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 { + content_prekey: Some(PreKey { pre_key: db_keys.content_prekey.prekey, pre_key_signature: db_keys.content_prekey.prekey_signature, }), - notif_prekey: Some(client::PreKey { + notif_prekey: Some(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, - > { + request: tonic::Request, + ) -> Result, 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 { + let response = UpdateUserPasswordStartResponse { session_id, opaque_registration_response: server_message, }; Ok(Response::new(response)) } async fn update_user_password_finish( &self, - request: tonic::Request, + 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)) } async fn log_out_user( &self, request: tonic::Request, ) -> Result, tonic::Status> { let (user_id, device_id) = get_user_and_device_id(&request)?; self .db_client .remove_device_from_users_table(user_id.clone(), device_id.clone()) .await .map_err(handle_db_error)?; self .db_client .delete_access_token_data(user_id, device_id) .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 (user_id, _) = get_user_and_device_id(&request)?; self .db_client .delete_user(user_id) .await .map_err(handle_db_error)?; let response = Empty {}; Ok(Response::new(response)) } } diff --git a/services/identity/src/grpc_services/mod.rs b/services/identity/src/grpc_services/mod.rs index 657fbecee..3c31cbfb9 100644 --- a/services/identity/src/grpc_services/mod.rs +++ b/services/identity/src/grpc_services/mod.rs @@ -1,2 +1,16 @@ pub mod authenticated; pub mod shared; + +pub mod protos { + pub mod unauth { + tonic::include_proto!("identity.client"); + } + pub mod auth { + tonic::include_proto!("identity.authenticated"); + } + + // 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 use self::unauth as client; +} diff --git a/services/identity/src/grpc_utils.rs b/services/identity/src/grpc_utils.rs index 2f5a4b8ca..16ff3befa 100644 --- a/services/identity/src/grpc_utils.rs +++ b/services/identity/src/grpc_utils.rs @@ -1,256 +1,258 @@ use std::collections::HashMap; use tonic::Status; use tracing::error; use crate::{ - client_service::client_proto::{ - DeviceKeyUpload, IdentityKeyInfo, OpaqueLoginStartRequest, PreKey, - RegistrationStartRequest, ReservedRegistrationStartRequest, - ReservedWalletLoginRequest, WalletLoginRequest, - }, constants::{ CONTENT_ONE_TIME_KEY, NOTIF_ONE_TIME_KEY, USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_SIGNATURE_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_SOCIAL_PROOF_ATTRIBUTE_NAME, }, database::DeviceKeys, - grpc_services::authenticated::auth_proto::{InboundKeyInfo, OutboundKeyInfo}, + grpc_services::protos::{ + auth::{InboundKeyInfo, OutboundKeyInfo}, + unauth::{ + DeviceKeyUpload, IdentityKeyInfo, OpaqueLoginStartRequest, PreKey, + RegistrationStartRequest, ReservedRegistrationStartRequest, + ReservedWalletLoginRequest, WalletLoginRequest, + }, + }, token::AuthType, }; pub struct DeviceInfoWithAuth<'a> { pub device_info: HashMap, pub auth_type: Option<&'a AuthType>, } impl TryFrom> for InboundKeyInfo { type Error = Status; fn try_from(data: DeviceInfoWithAuth) -> Result { let mut device_info = data.device_info; let identity_info = extract_identity_info(&mut device_info, data.auth_type)?; Ok(InboundKeyInfo { identity_info: Some(identity_info), content_prekey: Some(create_prekey( &mut device_info, USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_SIGNATURE_ATTRIBUTE_NAME, )?), notif_prekey: Some(create_prekey( &mut device_info, USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME, )?), }) } } impl TryFrom> for OutboundKeyInfo { type Error = Status; fn try_from(data: DeviceInfoWithAuth) -> Result { let mut device_info = data.device_info; let identity_info = extract_identity_info(&mut device_info, data.auth_type)?; let content_one_time_key = device_info.remove(CONTENT_ONE_TIME_KEY); let notif_one_time_key = device_info.remove(NOTIF_ONE_TIME_KEY); Ok(OutboundKeyInfo { identity_info: Some(identity_info), content_prekey: Some(create_prekey( &mut device_info, USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_SIGNATURE_ATTRIBUTE_NAME, )?), notif_prekey: Some(create_prekey( &mut device_info, USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME, USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME, )?), one_time_content_prekey: content_one_time_key, one_time_notif_prekey: notif_one_time_key, }) } } fn extract_key( device_info: &mut DeviceKeys, key: &str, ) -> Result { device_info.remove(key).ok_or_else(|| { error!("{} missing from device info", key); Status::failed_precondition("Database item malformed") }) } fn extract_identity_info( device_info: &mut HashMap, auth_type: Option<&AuthType>, ) -> Result { let payload = extract_key( device_info, USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME, )?; let payload_signature = extract_key( device_info, USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME, )?; let social_proof = device_info.remove(USERS_TABLE_DEVICES_MAP_SOCIAL_PROOF_ATTRIBUTE_NAME); if social_proof.is_none() && auth_type == Some(&AuthType::Wallet) { error!("Social proof missing for wallet user"); return Err(Status::failed_precondition("Database item malformed")); } Ok(IdentityKeyInfo { payload, payload_signature, social_proof, }) } fn create_prekey( device_info: &mut HashMap, key_attr: &str, signature_attr: &str, ) -> Result { Ok(PreKey { pre_key: extract_key(device_info, key_attr)?, pre_key_signature: extract_key(device_info, signature_attr)?, }) } pub trait DeviceKeyUploadData { fn device_key_upload(&self) -> Option<&DeviceKeyUpload>; } impl DeviceKeyUploadData for RegistrationStartRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for ReservedRegistrationStartRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for OpaqueLoginStartRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for WalletLoginRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for ReservedWalletLoginRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } pub trait DeviceKeyUploadActions { fn payload(&self) -> Result; fn payload_signature(&self) -> Result; fn content_prekey(&self) -> Result; fn content_prekey_signature(&self) -> Result; fn notif_prekey(&self) -> Result; fn notif_prekey_signature(&self) -> Result; fn one_time_content_prekeys(&self) -> Result, Status>; fn one_time_notif_prekeys(&self) -> Result, Status>; fn device_type(&self) -> Result; fn social_proof(&self) -> Result, Status>; } impl DeviceKeyUploadActions for T { fn payload(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.device_key_info.as_ref()) .map(|info| info.payload.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn payload_signature(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.device_key_info.as_ref()) .map(|info| info.payload_signature.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn content_prekey(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.content_upload.as_ref()) .map(|prekey| prekey.pre_key.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn content_prekey_signature(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.content_upload.as_ref()) .map(|prekey| prekey.pre_key_signature.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn notif_prekey(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.notif_upload.as_ref()) .map(|prekey| prekey.pre_key.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn notif_prekey_signature(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.notif_upload.as_ref()) .map(|prekey| prekey.pre_key_signature.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn one_time_content_prekeys(&self) -> Result, Status> { self .device_key_upload() .map(|upload| upload.one_time_content_prekeys.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn one_time_notif_prekeys(&self) -> Result, Status> { self .device_key_upload() .map(|upload| upload.one_time_notif_prekeys.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn device_type(&self) -> Result { self .device_key_upload() .map(|upload| upload.device_type) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn social_proof(&self) -> Result, Status> { self .device_key_upload() .and_then(|upload| upload.device_key_info.as_ref()) .map(|info| info.social_proof.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } } diff --git a/services/identity/src/main.rs b/services/identity/src/main.rs index 2bb99b7e9..c047f1754 100644 --- a/services/identity/src/main.rs +++ b/services/identity/src/main.rs @@ -1,86 +1,86 @@ use std::time::Duration; use config::Command; 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 constants::IDENTITY_SERVICE_SOCKET_ADDR; 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; +use grpc_services::protos::auth::identity_client_service_server::IdentityClientServiceServer as AuthServer; #[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)?; match config::parse_cli_command() { Command::Keygen { dir } => { generate_and_persist_keypair(dir)?; } Command::Server => { config::load_server_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.clone()); let client_service = IdentityClientServiceServer::with_interceptor( inner_client_service, grpc_services::shared::version_interceptor, ); let inner_auth_service = 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?; } Command::PopulateDB => unimplemented!(), } Ok(()) }