diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs index 25a75b630..263887ca4 100644 --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -1,894 +1,923 @@ pub mod client_proto { tonic::include_proto!("identity.client"); } use std::str::FromStr; use crate::database::{self, Device}; use crate::error::Error as DBError; use crate::{ client_service::client_proto::{ AddReservedUsernamesRequest, DeleteUserRequest, Empty, GenerateNonceResponse, InboundKeysForUserRequest, - InboundKeysForUserResponse, OpaqueLoginFinishRequest, + InboundKeysForUserResponse, LogoutRequest, OpaqueLoginFinishRequest, OpaqueLoginFinishResponse, OpaqueLoginStartRequest, OpaqueLoginStartResponse, OutboundKeysForUserRequest, OutboundKeysForUserResponse, RefreshUserPreKeysRequest, RegistrationFinishRequest, RegistrationFinishResponse, RegistrationStartRequest, RegistrationStartResponse, RemoveReservedUsernameRequest, ReservedRegistrationStartRequest, UpdateUserPasswordFinishRequest, UpdateUserPasswordStartRequest, UpdateUserPasswordStartResponse, UploadOneTimeKeysRequest, VerifyUserAccessTokenRequest, VerifyUserAccessTokenResponse, WalletLoginRequest, WalletLoginResponse, }, config::CONFIG, database::{DatabaseClient, KeyPayload}, id::generate_uuid, nonce::generate_nonce_data, reserved_users::{ validate_add_reserved_usernames_message, validate_remove_reserved_username_message, validate_signed_account_ownership_message, }, siwe::parse_and_verify_siwe_message, token::{AccessTokenData, AuthType}, }; use aws_sdk_dynamodb::Error as DynamoDBError; pub use client_proto::identity_client_service_server::{ IdentityClientService, IdentityClientServiceServer, }; use comm_opaque2::grpc::protocol_error_to_grpc_status; -use constant_time_eq::constant_time_eq; use moka::future::Cache; use rand::rngs::OsRng; use tonic::Response; use tracing::{debug, error}; #[derive(Clone)] pub enum WorkflowInProgress { Registration(UserRegistrationInfo), Login(UserLoginInfo), 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_onetime_keys: Vec, pub notif_prekey: String, pub notif_prekey_signature: String, pub notif_onetime_keys: Vec, pub device_type: database::Device, } #[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) { 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, }), onetime_content_prekeys, onetime_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_onetime_keys: onetime_content_prekeys, notif_prekey, notif_prekey_signature, notif_onetime_keys: onetime_notif_prekeys, device_type: Device::try_from(device_type) .map_err(handle_db_error)?, }, }; let session_id = generate_uuid(); self .cache .insert( session_id.clone(), WorkflowInProgress::Registration(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, }), onetime_content_prekeys, onetime_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_onetime_keys: onetime_content_prekeys, notif_prekey, notif_prekey_signature, notif_onetime_keys: onetime_notif_prekeys, device_type: Device::try_from(device_type) .map_err(handle_db_error)?, }, }; let session_id = generate_uuid(); self .cache .insert( session_id.clone(), WorkflowInProgress::Registration(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 access_token = self + let token_is_valid = self .client - .get_access_token_data(message.user_id.clone(), message.device_id_key) + .verify_access_token( + message.user_id.clone(), + message.device_id_key, + message.access_token, + ) .await .map_err(handle_db_error)?; - if let Some(token) = access_token { - if !token.is_valid() - || !constant_time_eq( - token.access_token.as_bytes(), - message.access_token.as_bytes(), - ) - { - return Err(tonic::Status::permission_denied("bad token")); - } + 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 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 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)) - } else { - Err(tonic::Status::permission_denied("bad token")) - } + 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, }), onetime_content_prekeys, onetime_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_onetime_keys: onetime_content_prekeys, notif_prekey, notif_prekey_signature, notif_onetime_keys: onetime_notif_prekeys, device_type: Device::try_from(device_type) .map_err(handle_db_error)?, }, }; let session_id = generate_uuid(); self .cache .insert(session_id.clone(), WorkflowInProgress::Login(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 wallet_address = parse_and_verify_siwe_message( &message.siwe_message, &message.siwe_signature, )?; 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, }), onetime_content_prekeys, onetime_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_onetime_keys: onetime_content_prekeys, notif_prekey, notif_prekey_signature, notif_onetime_keys: onetime_notif_prekeys, device_type: Device::try_from(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 access_token = self + let token_is_valid = self .client - .get_access_token_data(message.user_id.clone(), message.device_id_key) + .verify_access_token( + message.user_id.clone(), + message.device_id_key, + message.access_token, + ) .await .map_err(handle_db_error)?; - if let Some(token) = access_token { - if !token.is_valid() - || !constant_time_eq( - token.access_token.as_bytes(), - message.access_token.as_bytes(), - ) - { - return Err(tonic::Status::permission_denied("bad token")); - } + 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)?; + self + .client + .delete_user(message.user_id) + .await + .map_err(handle_db_error)?; - let response = Empty {}; + let response = Empty {}; - Ok(Response::new(response)) - } else { - Err(tonic::Status::permission_denied("bad 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 get_outbound_keys_for_user( &self, _request: tonic::Request, ) -> Result, tonic::Status> { unimplemented!(); } async fn get_inbound_keys_for_user( &self, _request: tonic::Request, ) -> Result, tonic::Status> { unimplemented!(); } 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.user_id, 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(); let token_valid = self .client .verify_access_token( message.user_id, message.signing_public_key, message.access_token, ) .await .map_err(handle_db_error)?; let response = Response::new(VerifyUserAccessTokenResponse { 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) } } 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/shared/protos/identity_client.proto b/shared/protos/identity_client.proto index 6cc91ac4e..dd58b053e 100644 --- a/shared/protos/identity_client.proto +++ b/shared/protos/identity_client.proto @@ -1,354 +1,366 @@ 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) {} } // 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; Native = 1; Web = 2; } // 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; }