Page MenuHomePhabricator

No OneTemporary

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<String>,
pub notif_prekey: String,
pub notif_prekey_signature: String,
pub notif_onetime_keys: Vec<String>,
pub device_type: database::Device,
}
#[derive(derive_more::Constructor)]
pub struct ClientService {
client: DatabaseClient,
cache: Cache<String, WorkflowInProgress>,
}
#[tonic::async_trait]
impl IdentityClientService for ClientService {
async fn register_password_user_start(
&self,
request: tonic::Request<RegistrationStartRequest>,
) -> Result<tonic::Response<RegistrationStartResponse>, 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, &register_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<ReservedRegistrationStartRequest>,
) -> Result<tonic::Response<RegistrationStartResponse>, 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, &register_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<RegistrationFinishRequest>,
) -> Result<tonic::Response<RegistrationFinishResponse>, 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<UpdateUserPasswordStartRequest>,
) -> Result<tonic::Response<UpdateUserPasswordStartResponse>, 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<UpdateUserPasswordFinishRequest>,
) -> Result<tonic::Response<Empty>, 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<OpaqueLoginStartRequest>,
) -> Result<tonic::Response<OpaqueLoginStartResponse>, 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<OpaqueLoginFinishRequest>,
) -> Result<tonic::Response<OpaqueLoginFinishResponse>, 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<WalletLoginRequest>,
) -> Result<tonic::Response<WalletLoginResponse>, 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<LogoutRequest>,
+ ) -> Result<tonic::Response<Empty>, 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<DeleteUserRequest>,
) -> Result<tonic::Response<Empty>, 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<Empty>,
) -> Result<tonic::Response<GenerateNonceResponse>, 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<OutboundKeysForUserRequest>,
) -> Result<tonic::Response<OutboundKeysForUserResponse>, tonic::Status> {
unimplemented!();
}
async fn get_inbound_keys_for_user(
&self,
_request: tonic::Request<InboundKeysForUserRequest>,
) -> Result<tonic::Response<InboundKeysForUserResponse>, tonic::Status> {
unimplemented!();
}
async fn upload_one_time_keys(
&self,
request: tonic::Request<UploadOneTimeKeysRequest>,
) -> Result<tonic::Response<Empty>, 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<RefreshUserPreKeysRequest>,
) -> Result<tonic::Response<Empty>, tonic::Status> {
unimplemented!();
}
async fn verify_user_access_token(
&self,
request: tonic::Request<VerifyUserAccessTokenRequest>,
) -> Result<tonic::Response<VerifyUserAccessTokenResponse>, 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<AddReservedUsernamesRequest>,
) -> Result<tonic::Response<Empty>, 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<RemoveReservedUsernameRequest>,
) -> Result<tonic::Response<Empty>, 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<string, OutboundKeyInfo> 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<string, InboundKeyInfo> 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;
}

File Metadata

Mime Type
text/x-diff
Expires
Wed, Dec 25, 6:02 PM (7 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2700652
Default Alt Text
(41 KB)

Event Timeline