diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs index 9158c1fb8..a13241b74 100644 --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -1,1039 +1,1040 @@ 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, - RefreshUserPreKeysRequest, RegistrationFinishRequest, - RegistrationFinishResponse, RegistrationStartRequest, - RegistrationStartResponse, RemoveReservedUsernameRequest, - ReservedRegistrationStartRequest, UpdateUserPasswordFinishRequest, - UpdateUserPasswordStartRequest, UpdateUserPasswordStartResponse, - UploadOneTimeKeysRequest, VerifyUserAccessTokenRequest, - VerifyUserAccessTokenResponse, WalletLoginRequest, WalletLoginResponse, + RegistrationFinishRequest, RegistrationFinishResponse, + RegistrationStartRequest, RegistrationStartResponse, + RemoveReservedUsernameRequest, ReservedRegistrationStartRequest, + UpdateUserPasswordFinishRequest, UpdateUserPasswordStartRequest, + UpdateUserPasswordStartResponse, UploadOneTimeKeysRequest, + 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; use crate::id::generate_uuid; use crate::nonce::generate_nonce_data; use crate::reserved_users::{ is_valid_ethereum_address, validate_add_reserved_usernames_message, validate_remove_reserved_username_message, validate_signed_account_ownership_message, }; use crate::siwe::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, } #[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); let username_taken = self .client .username_taken(message.username.clone()) .await .map_err(handle_db_error)?; let username_in_reserved_usernames_table = self .client .username_in_reserved_usernames_table(&message.username) .await .map_err(handle_db_error)?; if username_taken || 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")); } if let client_proto::RegistrationStartRequest { opaque_registration_request: register_message, username, device_key_upload: Some(client_proto::DeviceKeyUpload { device_key_info: Some(client_proto::IdentityKeyInfo { payload, payload_signature, social_proof: _social_proof, }), content_upload: Some(client_proto::PreKey { pre_key: content_prekey, pre_key_signature: content_prekey_signature, }), notif_upload: Some(client_proto::PreKey { pre_key: notif_prekey, pre_key_signature: notif_prekey_signature, }), one_time_content_prekeys, one_time_notif_prekeys, device_type, }), } = message { let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start(&CONFIG.server_setup, ®ister_message, username.as_bytes()) .map_err(protocol_error_to_grpc_status)?; let key_info = KeyPayload::from_str(&payload) .map_err(|_| tonic::Status::invalid_argument("malformed payload"))?; let registration_state = UserRegistrationInfo { username, flattened_device_key_upload: FlattenedDeviceKeyUpload { device_id_key: key_info.primary_identity_public_keys.ed25519, key_payload: payload, key_payload_signature: payload_signature, content_prekey, content_prekey_signature, content_one_time_keys: one_time_content_prekeys, notif_prekey, notif_prekey_signature, notif_one_time_keys: one_time_notif_prekeys, device_type: DeviceType::try_from(DBDeviceTypeInt(device_type)) .map_err(handle_db_error)?, }, }; let session_id = generate_uuid(); self .cache .insert( session_id.clone(), WorkflowInProgress::Registration(Box::new(registration_state)), ) .await; let response = RegistrationStartResponse { session_id, opaque_registration_response: server_message, }; Ok(Response::new(response)) } else { Err(tonic::Status::invalid_argument("unexpected message data")) } } async fn register_reserved_password_user_start( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); let username_taken = self .client .username_taken(message.username.clone()) .await .map_err(handle_db_error)?; if username_taken { return Err(tonic::Status::already_exists("username already exists")); } 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 { validate_signed_account_ownership_message( &message.username, &message.keyserver_message, &message.keyserver_signature, )?; } else { return Err(tonic::Status::permission_denied("username not reserved")); } if let client_proto::ReservedRegistrationStartRequest { opaque_registration_request: register_message, username, device_key_upload: Some(client_proto::DeviceKeyUpload { device_key_info: Some(client_proto::IdentityKeyInfo { payload, payload_signature, social_proof: _social_proof, }), content_upload: Some(client_proto::PreKey { pre_key: content_prekey, pre_key_signature: content_prekey_signature, }), notif_upload: Some(client_proto::PreKey { pre_key: notif_prekey, pre_key_signature: notif_prekey_signature, }), one_time_content_prekeys, one_time_notif_prekeys, device_type, }), .. } = message { let server_registration = comm_opaque2::server::Registration::new(); let server_message = server_registration .start(&CONFIG.server_setup, ®ister_message, username.as_bytes()) .map_err(protocol_error_to_grpc_status)?; let key_info = KeyPayload::from_str(&payload) .map_err(|_| tonic::Status::invalid_argument("malformed payload"))?; let registration_state = UserRegistrationInfo { username, flattened_device_key_upload: FlattenedDeviceKeyUpload { device_id_key: key_info.primary_identity_public_keys.ed25519, key_payload: payload, key_payload_signature: payload_signature, content_prekey, content_prekey_signature, content_one_time_keys: one_time_content_prekeys, notif_prekey, notif_prekey_signature, notif_one_time_keys: one_time_notif_prekeys, device_type: DeviceType::try_from(DBDeviceTypeInt(device_type)) .map_err(handle_db_error)?, }, }; let session_id = generate_uuid(); self .cache .insert( session_id.clone(), WorkflowInProgress::Registration(Box::new(registration_state)), ) .await; let response = RegistrationStartResponse { session_id, opaque_registration_response: server_message, }; Ok(Response::new(response)) } else { Err(tonic::Status::invalid_argument("unexpected message data")) } } 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 = generate_uuid(); self .cache .insert(session_id.clone(), 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")); }; if let client_proto::OpaqueLoginStartRequest { opaque_login_request: login_message, username, device_key_upload: Some(client_proto::DeviceKeyUpload { device_key_info: Some(client_proto::IdentityKeyInfo { payload, payload_signature, social_proof: _social_proof, }), content_upload: Some(client_proto::PreKey { pre_key: content_prekey, pre_key_signature: content_prekey_signature, }), notif_upload: Some(client_proto::PreKey { pre_key: notif_prekey, pre_key_signature: notif_prekey_signature, }), one_time_content_prekeys, one_time_notif_prekeys, device_type, }), } = message { let mut server_login = comm_opaque2::server::Login::new(); let server_response = server_login .start( &CONFIG.server_setup, &password_file_bytes, &login_message, username.as_bytes(), ) .map_err(protocol_error_to_grpc_status)?; let key_info = KeyPayload::from_str(&payload) .map_err(|_| tonic::Status::invalid_argument("malformed payload"))?; let login_state = UserLoginInfo { user_id, opaque_server_login: server_login, flattened_device_key_upload: FlattenedDeviceKeyUpload { device_id_key: key_info.primary_identity_public_keys.ed25519, key_payload: payload, key_payload_signature: payload_signature, content_prekey, content_prekey_signature, content_one_time_keys: one_time_content_prekeys, notif_prekey, notif_prekey_signature, notif_one_time_keys: one_time_notif_prekeys, device_type: DeviceType::try_from(DBDeviceTypeInt(device_type)) .map_err(handle_db_error)?, }, }; let session_id = generate_uuid(); self .cache .insert( session_id.clone(), WorkflowInProgress::Login(Box::new(login_state)), ) .await; let response = Response::new(OpaqueLoginStartResponse { session_id, opaque_login_response: server_response, }); Ok(response) } else { Err(tonic::Status::invalid_argument("unexpected message data")) } } 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, social_proof) = if let client_proto::WalletLoginRequest { siwe_message: _, siwe_signature: _, device_key_upload: Some(client_proto::DeviceKeyUpload { device_key_info: Some(client_proto::IdentityKeyInfo { payload, payload_signature, social_proof: Some(social_proof), }), content_upload: Some(client_proto::PreKey { pre_key: content_prekey, pre_key_signature: content_prekey_signature, }), notif_upload: Some(client_proto::PreKey { pre_key: notif_prekey, pre_key_signature: notif_prekey_signature, }), one_time_content_prekeys, one_time_notif_prekeys, device_type, }), } = message { let key_info = KeyPayload::from_str(&payload) .map_err(|_| tonic::Status::invalid_argument("malformed payload"))?; ( FlattenedDeviceKeyUpload { device_id_key: key_info.primary_identity_public_keys.ed25519, key_payload: payload, key_payload_signature: payload_signature, content_prekey, content_prekey_signature, content_one_time_keys: one_time_content_prekeys, notif_prekey, notif_prekey_signature, notif_one_time_keys: one_time_notif_prekeys, device_type: DeviceType::try_from(DBDeviceTypeInt(device_type)) .map_err(handle_db_error)?, }, social_proof, ) } else { return Err(tonic::Status::invalid_argument("unexpected message data")); }; 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 => { // User doesn't exist yet, 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, ) .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 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 upload_one_time_keys( &self, request: tonic::Request, ) -> Result, tonic::Status> { let message = request.into_inner(); debug!("Validating token: {:?}", message); let token_valid = self .client .verify_access_token( message.user_id.clone(), message.device_id.clone(), message.access_token, ) .await .map_err(handle_db_error)?; if !token_valid { return Err(tonic::Status::unauthenticated("Invalid token")); } debug!( "Attempting to update one time keys for user: {}", message.user_id ); self .client .append_one_time_prekeys( message.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 refresh_user_pre_keys( - &self, - _request: tonic::Request, - ) -> Result, tonic::Status> { - unimplemented!(); - } - 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) + } } 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") } } } diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs index 3b9d8d3db..6670f71de 100644 --- a/services/identity/src/constants.rs +++ b/services/identity/src/constants.rs @@ -1,143 +1,147 @@ // Secrets pub const SECRETS_DIRECTORY: &str = "secrets"; pub const SECRETS_SETUP_FILE: &str = "server_setup.txt"; // DynamoDB // User table information, supporting opaque_ke 2.0 and X3DH information // Users can sign in either through username+password or Eth wallet. // // This structure should be aligned with the messages defined in // shared/protos/identity_client.proto // // Structure for a user should be: // { // userID: String, // opaqueRegistrationData: Option, // username: Option, // walletAddress: Option, // devices: HashMap // } // // A device is defined as: // { // deviceType: String, # client or keyserver // keyPayload: String, // keyPayloadSignature: String, // identityPreKey: String, // identityPreKeySignature: String, // identityOneTimeKeys: Vec, // notifPreKey: String, // notifPreKeySignature: String, // notifOneTimeKeys: Vec, // socialProof: Option // } // } // // Additional context: // "devices" uses the signing public identity key of the device as a key for the devices map // "keyPayload" is a JSON encoded string containing identity and notif keys (both signature and verification) // if "deviceType" == "keyserver", then the device will not have any notif key information pub const USERS_TABLE: &str = "identity-users"; pub const USERS_TABLE_PARTITION_KEY: &str = "userID"; pub const USERS_TABLE_REGISTRATION_ATTRIBUTE: &str = "opaqueRegistrationData"; pub const USERS_TABLE_USERNAME_ATTRIBUTE: &str = "username"; pub const USERS_TABLE_DEVICES_ATTRIBUTE: &str = "devices"; pub const USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME: &str = "deviceType"; pub const USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME: &str = "keyPayload"; pub const USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME: &str = "keyPayloadSignature"; pub const USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_ATTRIBUTE_NAME: &str = "identityPreKey"; pub const USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_SIGNATURE_ATTRIBUTE_NAME: &str = "identityPreKeySignature"; pub const USERS_TABLE_DEVICES_MAP_CONTENT_ONE_TIME_KEYS_ATTRIBUTE_NAME: &str = "identityOneTimeKeys"; pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME: &str = "preKey"; pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME: &str = "preKeySignature"; pub const USERS_TABLE_DEVICES_MAP_NOTIF_ONE_TIME_KEYS_ATTRIBUTE_NAME: &str = "notifOneTimeKeys"; pub const USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE: &str = "walletAddress"; pub const USERS_TABLE_DEVICES_MAP_SOCIAL_PROOF_ATTRIBUTE_NAME: &str = "socialProof"; pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index"; pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index"; pub const ACCESS_TOKEN_TABLE: &str = "identity-tokens"; pub const ACCESS_TOKEN_TABLE_PARTITION_KEY: &str = "userID"; pub const ACCESS_TOKEN_SORT_KEY: &str = "signingPublicKey"; pub const ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE: &str = "created"; pub const ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE: &str = "authType"; pub const ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE: &str = "valid"; pub const ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE: &str = "token"; pub const NONCE_TABLE: &str = "identity-nonces"; pub const NONCE_TABLE_PARTITION_KEY: &str = "nonce"; pub const NONCE_TABLE_CREATED_ATTRIBUTE: &str = "created"; pub const NONCE_TABLE_EXPIRATION_TIME_ATTRIBUTE: &str = "expirationTime"; pub const NONCE_TABLE_EXPIRATION_TIME_UNIX_ATTRIBUTE: &str = "expirationTimeUnix"; // Usernames reserved because they exist in Ashoat's keyserver already pub const RESERVED_USERNAMES_TABLE: &str = "identity-reserved-usernames"; pub const RESERVED_USERNAMES_TABLE_PARTITION_KEY: &str = "username"; // One time keys table, which need to exist in their own table to ensure // atomicity of additions and removals pub mod one_time_keys_table { // The `PARTITION_KEY` will contain "notification_${deviceID}" or // "content_${deviceID}" to allow for both key sets to coexist in the same table pub const NAME: &str = "identity-one-time-keys"; pub const PARTITION_KEY: &str = "deviceID"; pub const DEVICE_ID: &str = PARTITION_KEY; pub const SORT_KEY: &str = "oneTimeKey"; pub const ONE_TIME_KEY: &str = SORT_KEY; } // One-time key constants for device info map pub const CONTENT_ONE_TIME_KEY: &str = "contentOneTimeKey"; pub const NOTIF_ONE_TIME_KEY: &str = "notifOneTimeKey"; // Tokio pub const MPSC_CHANNEL_BUFFER_CAPACITY: usize = 1; pub const IDENTITY_SERVICE_SOCKET_ADDR: &str = "[::]:50054"; // Token pub const ACCESS_TOKEN_LENGTH: usize = 512; // Temporary config pub const AUTH_TOKEN: &str = "COMM_IDENTITY_SERVICE_AUTH_TOKEN"; pub const KEYSERVER_PUBLIC_KEY: &str = "KEYSERVER_PUBLIC_KEY"; // Nonce pub const NONCE_LENGTH: usize = 17; pub const NONCE_TTL_DURATION: i64 = 30; // LocalStack pub const LOCALSTACK_ENDPOINT: &str = "LOCALSTACK_ENDPOINT"; // OPAQUE Server Setup pub const OPAQUE_SERVER_SETUP: &str = "OPAQUE_SERVER_SETUP"; // Tunnelbroker pub const TUNNELBROKER_GRPC_ENDPOINT: &str = "TUNNELBROKER_GRPC_ENDPOINT"; pub const DEFAULT_TUNNELBROKER_ENDPOINT: &str = "http://localhost:50051"; // X3DH key management // Threshold for requesting more one_time keys pub const ONE_TIME_KEY_MINIMUM_THRESHOLD: usize = 5; // Number of keys to be refreshed when below the threshold pub const ONE_TIME_KEY_REFRESH_NUMBER: u32 = 5; + +// Minimum supported code versions + +pub const MIN_SUPPORTED_NATIVE_VERSION: u64 = 270; diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs index ba159c270..b2d7012d9 100644 --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -1,174 +1,172 @@ -use crate::{client_service::handle_db_error, database::DatabaseClient}; +use crate::{ + client_service::handle_db_error, database::DatabaseClient, + grpc_services::shared::get_value, +}; 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::{ identity_client_service_server::IdentityClientService, KeyserverKeysResponse, OutboundKeyInfo, OutboundKeysForUserRequest, RefreshUserPreKeysRequest, UploadOneTimeKeysRequest, }; use client::{Empty, IdentityKeyInfo}; use tracing::debug; #[derive(derive_more::Constructor)] pub struct AuthenticatedService { db_client: DatabaseClient, } -fn get_value(req: &Request, key: &str) -> Option { - let raw_value = req.metadata().get(key)?; - raw_value.to_str().ok().map(|s| s.to_string()) -} - fn get_auth_info(req: &Request<()>) -> Option<(String, String, String)> { debug!("Retrieving auth info for request: {:?}", req); let user_id = get_value(req, "user_id")?; let device_id = get_value(req, "device_id")?; let access_token = get_value(req, "access_token")?; Some((user_id, device_id, access_token)) } -pub fn auth_intercept( +pub fn auth_interceptor( req: Request<()>, db_client: &DatabaseClient, ) -> Result, Status> { - println!("Intercepting request: {:?}", req); + 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, "user_id") .ok_or_else(|| Status::unauthenticated("Missing user_id field"))?; let device_id = get_value(request, "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 {})) } } diff --git a/services/identity/src/grpc_services/mod.rs b/services/identity/src/grpc_services/mod.rs index 3aaf49e4a..657fbecee 100644 --- a/services/identity/src/grpc_services/mod.rs +++ b/services/identity/src/grpc_services/mod.rs @@ -1 +1,2 @@ pub mod authenticated; +pub mod shared; diff --git a/services/identity/src/grpc_services/shared.rs b/services/identity/src/grpc_services/shared.rs new file mode 100644 index 000000000..6a0c17fb6 --- /dev/null +++ b/services/identity/src/grpc_services/shared.rs @@ -0,0 +1,33 @@ +use grpc_clients::error::unsupported_version; +use tonic::{Request, Status}; +use tracing::debug; + +use crate::constants::MIN_SUPPORTED_NATIVE_VERSION; + +pub fn version_interceptor(req: Request<()>) -> Result, Status> { + debug!("Intercepting request to check version: {:?}", req); + + match get_version_info(&req) { + Some((version, platform)) + if (platform == "ios" || platform == "android") + && version < MIN_SUPPORTED_NATIVE_VERSION => + { + Err(unsupported_version()) + } + _ => Ok(req), + } +} + +fn get_version_info(req: &Request<()>) -> Option<(u64, String)> { + debug!("Retrieving version info for request: {:?}", req); + + let code_version: u64 = get_value(req, "code_version")?.parse().ok()?; + let device_type = get_value(req, "device_type")?; + + Some((code_version, device_type)) +} + +pub fn get_value(req: &Request, key: &str) -> Option { + let raw_value = req.metadata().get(key)?; + raw_value.to_str().ok().map(|s| s.to_string()) +} diff --git a/services/identity/src/grpc_utils.rs b/services/identity/src/grpc_utils.rs index eddc2c601..ba2d84159 100644 --- a/services/identity/src/grpc_utils.rs +++ b/services/identity/src/grpc_utils.rs @@ -1,129 +1,129 @@ use std::collections::HashMap; use tonic::Status; use tracing::error; use crate::{ client_service::client_proto::{ IdentityKeyInfo, InboundKeyInfo, OutboundKeyInfo, PreKey, }, 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, token::AuthType, }; pub struct DeviceInfoWithAuth<'a> { pub device_info: HashMap, pub auth_type: &'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)?; + 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)?; + 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: &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 == &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)?, }) } diff --git a/services/identity/src/main.rs b/services/identity/src/main.rs index f6f05e508..4debf89aa 100644 --- a/services/identity/src/main.rs +++ b/services/identity/src/main.rs @@ -1,100 +1,105 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use database::DatabaseClient; use moka::future::Cache; use tonic::transport::Server; mod client_service; mod config; pub mod constants; 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 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 client_service = IdentityClientServiceServer::new( - ClientService::new(database_client.clone(), workflow_cache), + let inner_client_service = + ClientService::new(database_client.clone(), workflow_cache); + let client_service = IdentityClientServiceServer::with_interceptor( + inner_client_service, + grpc_services::shared::version_interceptor, ); - let raw_auth_service = AuthenticatedService::new(database_client.clone()); + let inner_auth_service = + AuthenticatedService::new(database_client.clone()); let auth_service = - AuthServer::with_interceptor(raw_auth_service, move |req| { - grpc_services::authenticated::auth_intercept(req, &database_client) + 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) .add_service(tonic_web::enable(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 ef2ec00c2..e74c03925 100644 --- a/shared/protos/identity_authenticated.proto +++ b/shared/protos/identity_authenticated.proto @@ -1,66 +1,64 @@ 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 devices preKey and preKey signature + // Rotate a device's preKey and preKey signature // Rotated for deniability of older messages rpc RefreshUserPreKeys(RefreshUserPreKeysRequest) 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) {} } // 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; } - - diff --git a/shared/protos/identity_client.proto b/shared/protos/identity_client.proto index 6e8044750..6340fe3be 100644 --- a/shared/protos/identity_client.proto +++ b/shared/protos/identity_client.proto @@ -1,370 +1,365 @@ 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) {} // 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) {} // Replenish one-time preKeys rpc UploadOneTimeKeys(UploadOneTimeKeysRequest) returns (Empty) {} - // Rotate a devices preKey and preKey signature - // Rotated for deniability of older messages - rpc RefreshUserPreKeys(RefreshUserPreKeysRequest) returns (Empty) {} // 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 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; } // UploadOneTimeKeys // As OPKs get exhausted, they need to be refreshed message UploadOneTimeKeysRequest { string userID = 1; string deviceID = 2; string accessToken = 3; repeated string contentOneTimePreKeys = 4; repeated string notifOneTimePreKeys = 5; } -// RefreshUserPreKeys - -message RefreshUserPreKeysRequest { - string accessToken = 1; - PreKey newPreKeys = 2; -} - // 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; }