diff --git a/services/identity/src/client_service.rs b/services/identity/src/client_service.rs --- a/services/identity/src/client_service.rs +++ b/services/identity/src/client_service.rs @@ -23,6 +23,7 @@ database::{DatabaseClient, Error as DBError, KeyPayload}, id::generate_uuid, nonce::generate_nonce_data, + token::AccessTokenData, }; use aws_sdk_dynamodb::Error as DynamoDBError; pub use client_proto::identity_client_service_server::{ @@ -40,16 +41,16 @@ #[derive(Clone)] pub struct UserRegistrationInfo { - username: String, - device_id_key: String, - key_payload: String, - key_payload_signature: String, - identity_prekey: String, - identity_prekey_signature: String, - identity_onetime_keys: Vec, - notif_prekey: String, - notif_prekey_signature: String, - notif_onetime_keys: Vec, + pub username: String, + pub device_id_key: String, + pub key_payload: String, + pub key_payload_signature: String, + pub identity_prekey: String, + pub identity_prekey_signature: String, + pub identity_onetime_keys: Vec, + pub notif_prekey: String, + pub notif_prekey_signature: String, + pub notif_onetime_keys: Vec, } #[derive(derive_more::Constructor)] @@ -144,9 +145,51 @@ async fn register_password_user_finish( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { - unimplemented!(); + 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(comm_opaque2::grpc::protocol_error_to_grpc_status)?; + + let device_id = state.device_id_key.clone(); + let user_id = self + .client + .add_user_to_users_table(state, password_file) + .await + .map_err(handle_db_error)?; + + // Create access token + let token = AccessTokenData::new( + message.session_id, + 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( diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs --- a/services/identity/src/constants.rs +++ b/services/identity/src/constants.rs @@ -8,74 +8,65 @@ // DynamoDB // User table information, supporting opaque_ke 2.0 and X3DH information -pub mod opaque2 { - // 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, - // identityPreKey: String, - // identityPreKeySignature: String, - // identityOneTimeKeys: Vec, - // notifPreKey: String, - // notifPreKeySignature: String, - // notifOneTimeKeys: Vec, - // } - // } - // - // 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-opaque2"; - 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_KEY_PAYLOAD_ATTRIBUTE_NAME: &str = - "keyPayload"; - pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME: &str = - "identityPreKey"; - pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME: - &str = "identityPreKeySignature"; - pub const USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_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_ONETIME_KEYS_ATTRIBUTE_NAME: &str = - "notifOneTimeKeys"; - pub const USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE: &str = "walletAddress"; - pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index"; - pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index"; -} + +// 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, +// } +// } +// +// 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 = "pakeRegistrationData"; +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_DEVICE_ATTRIBUTE_NAME: &str = "device"; -pub const USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME: &str = "signingPublicKey"; +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_IDENTITY_PREKEY_ATTRIBUTE_NAME: &str = + "identityPreKey"; +pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME: + &str = "identityPreKeySignature"; +pub const USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_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_ONETIME_KEYS_ATTRIBUTE_NAME: &str = + "notifOneTimeKeys"; pub const USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE: &str = "walletAddress"; pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index"; pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index"; -pub const USERS_TABLE_INITIALIZATION_INFO: &str = "initializationInfo"; pub const ACCESS_TOKEN_TABLE: &str = "identity-tokens"; pub const ACCESS_TOKEN_TABLE_PARTITION_KEY: &str = "userID"; diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs --- a/services/identity/src/database.rs +++ b/services/identity/src/database.rs @@ -6,7 +6,7 @@ use aws_config::SdkConfig; use aws_sdk_dynamodb::model::AttributeValue; use aws_sdk_dynamodb::output::{ - DeleteItemOutput, GetItemOutput, PutItemOutput, QueryOutput, UpdateItemOutput, + DeleteItemOutput, GetItemOutput, PutItemOutput, QueryOutput, }; use aws_sdk_dynamodb::types::Blob; use aws_sdk_dynamodb::{Client, Error as DynamoDBError}; @@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize}; use tracing::{debug, error, info, warn}; +use crate::client_service::UserRegistrationInfo; use crate::config::CONFIG; use crate::constants::{ ACCESS_TOKEN_SORT_KEY, ACCESS_TOKEN_TABLE, @@ -22,12 +23,21 @@ ACCESS_TOKEN_TABLE_PARTITION_KEY, ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE, ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE, NONCE_TABLE, NONCE_TABLE_CREATED_ATTRIBUTE, NONCE_TABLE_PARTITION_KEY, USERS_TABLE, - USERS_TABLE_DEVICES_ATTRIBUTE, USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME, - USERS_TABLE_DEVICE_ATTRIBUTE_NAME, USERS_TABLE_INITIALIZATION_INFO, + USERS_TABLE_DEVICES_ATTRIBUTE, + USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME, + USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_KEYS_ATTRIBUTE_NAME, + USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME, + USERS_TABLE_DEVICES_MAP_IDENTITY_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_ONETIME_KEYS_ATTRIBUTE_NAME, + USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME, + USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME, USERS_TABLE_PARTITION_KEY, USERS_TABLE_REGISTRATION_ATTRIBUTE, USERS_TABLE_USERNAME_ATTRIBUTE, USERS_TABLE_USERNAME_INDEX, USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE, USERS_TABLE_WALLET_ADDRESS_INDEX, }; +use crate::id::generate_uuid; use crate::nonce::NonceData; use crate::token::{AccessTokenData, AuthType}; use comm_opaque::Cipher; @@ -55,6 +65,20 @@ } } +pub enum Device { + Client, + Keyserver, +} + +impl Display for Device { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + match self { + Device::Client => write!(f, "client"), + Device::Keyserver => write!(f, "keyserver"), + } + } +} + #[derive(Clone)] pub struct DatabaseClient { client: Arc, @@ -111,148 +135,87 @@ } } - pub async fn get_session_initialization_info( - &self, - user_id: &str, - ) -> Result>>, Error> { - match self.get_item_from_users_table(user_id).await { - Ok(GetItemOutput { - item: Some(mut item), - .. - }) => parse_devices_attribute(item.remove(USERS_TABLE_DEVICES_ATTRIBUTE)) - .map(Some) - .map_err(Error::Attribute), - Ok(_) => { - info!("No item found for user {} in users table", user_id); - Ok(None) - } - Err(e) => { - error!( - "DynamoDB client failed to get session initialization info for user {}: {}", - user_id, e - ); - Err(e) - } - } - } - - pub async fn update_users_table( - &self, - user_id: String, - signing_public_key: Option, - registration: Option>, - username: Option, - session_initialization_info: Option<&HashMap>, - ) -> Result { - let mut update_expression_parts = Vec::new(); - let mut expression_attribute_names = HashMap::new(); - let mut expression_attribute_values = HashMap::new(); - if let Some(reg) = registration { - update_expression_parts - .push(format!("{} = :r", USERS_TABLE_REGISTRATION_ATTRIBUTE)); - expression_attribute_values.insert( - ":r".to_string(), - AttributeValue::B(Blob::new(reg.serialize())), - ); - }; - if let Some(username) = username { - update_expression_parts - .push(format!("{} = :u", USERS_TABLE_USERNAME_ATTRIBUTE)); - expression_attribute_values - .insert(":u".to_string(), AttributeValue::S(username)); - }; - if let Some(public_key) = signing_public_key { - let device_info = match session_initialization_info { - Some(info) => info - .iter() - .map(|(k, v)| (k.to_string(), AttributeValue::S(v.to_string()))) - .collect(), - None => HashMap::new(), - }; - - // How we construct the update expression will depend on whether the user - // already exists or not - if let GetItemOutput { item: Some(_), .. } = - self.get_item_from_users_table(&user_id).await? - { - update_expression_parts.push(format!( - "{}.#{} = :k", - USERS_TABLE_DEVICES_ATTRIBUTE, USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME, - )); - expression_attribute_names.insert( - format!("#{}", USERS_TABLE_DEVICES_MAP_ATTRIBUTE_NAME), - public_key, - ); - expression_attribute_values - .insert(":k".to_string(), AttributeValue::M(device_info)); - } else { - update_expression_parts - .push(format!("{} = :k", USERS_TABLE_DEVICES_ATTRIBUTE)); - let mut devices = HashMap::new(); - devices.insert(public_key, AttributeValue::M(device_info)); - expression_attribute_values - .insert(":k".to_string(), AttributeValue::M(devices)); - }; - }; - - self - .client - .update_item() - .table_name(USERS_TABLE) - .key(USERS_TABLE_PARTITION_KEY, AttributeValue::S(user_id)) - .update_expression(format!("SET {}", update_expression_parts.join(","))) - .set_expression_attribute_names( - if expression_attribute_names.is_empty() { - None - } else { - Some(expression_attribute_names) - }, - ) - .set_expression_attribute_values( - if expression_attribute_values.is_empty() { - None - } else { - Some(expression_attribute_values) - }, - ) - .send() - .await - .map_err(|e| Error::AwsSdk(e.into())) - } - pub async fn add_user_to_users_table( &self, - user_id: String, - registration: ServerRegistration, - username: String, - signing_public_key: String, - session_initialization_info: &HashMap, - ) -> Result { - let device_info: HashMap = - session_initialization_info - .iter() - .map(|(k, v)| (k.to_string(), AttributeValue::S(v.to_string()))) - .collect(); + registration_state: UserRegistrationInfo, + password_file: Vec, + ) -> Result { + let user_id = generate_uuid(); + let device_info = HashMap::from([ + ( + USERS_TABLE_DEVICES_MAP_DEVICE_TYPE_ATTRIBUTE_NAME.to_string(), + AttributeValue::S(Device::Client.to_string()), + ), + ( + USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_ATTRIBUTE_NAME.to_string(), + AttributeValue::S(registration_state.key_payload), + ), + ( + USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME + .to_string(), + AttributeValue::S(registration_state.key_payload_signature), + ), + ( + USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME.to_string(), + AttributeValue::S(registration_state.identity_prekey), + ), + ( + USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME + .to_string(), + AttributeValue::S(registration_state.identity_prekey_signature), + ), + ( + USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_KEYS_ATTRIBUTE_NAME + .to_string(), + AttributeValue::L( + registration_state + .identity_onetime_keys + .into_iter() + .map(AttributeValue::S) + .collect(), + ), + ), + ( + USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME.to_string(), + AttributeValue::S(registration_state.notif_prekey), + ), + ( + USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME + .to_string(), + AttributeValue::S(registration_state.notif_prekey_signature), + ), + ( + USERS_TABLE_DEVICES_MAP_NOTIF_ONETIME_KEYS_ATTRIBUTE_NAME.to_string(), + AttributeValue::L( + registration_state + .notif_onetime_keys + .into_iter() + .map(AttributeValue::S) + .collect(), + ), + ), + ]); + let devices = HashMap::from([( + registration_state.device_id_key, + AttributeValue::M(device_info), + )]); - let item = HashMap::from([ + let user = HashMap::from([ ( USERS_TABLE_PARTITION_KEY.to_string(), - AttributeValue::S(user_id), + AttributeValue::S(user_id.clone()), ), ( USERS_TABLE_USERNAME_ATTRIBUTE.to_string(), - AttributeValue::S(username), + AttributeValue::S(registration_state.username), ), ( - USERS_TABLE_REGISTRATION_ATTRIBUTE.to_string(), - AttributeValue::B(Blob::new(registration.serialize())), + USERS_TABLE_DEVICES_ATTRIBUTE.to_string(), + AttributeValue::M(devices), ), ( - USERS_TABLE_DEVICES_ATTRIBUTE.to_string(), - AttributeValue::M(HashMap::from([( - signing_public_key, - AttributeValue::M(device_info), - )])), + USERS_TABLE_REGISTRATION_ATTRIBUTE.to_string(), + AttributeValue::B(Blob::new(password_file)), ), ]); @@ -260,12 +223,13 @@ .client .put_item() .table_name(USERS_TABLE) - .set_item(Some(item)) + .set_item(Some(user)) .send() .await - .map_err(|e| Error::AwsSdk(e.into())) - } + .map_err(|e| Error::AwsSdk(e.into()))?; + Ok(user_id) + } pub async fn delete_user( &self, user_id: String, @@ -714,36 +678,6 @@ } } -fn parse_devices_attribute( - attribute: Option, -) -> Result>, DBItemError> { - let mut devices = HashMap::new(); - let ddb_devices = - parse_map_attribute(USERS_TABLE_DEVICES_ATTRIBUTE, attribute)?; - - for (signing_public_key, session_initialization_info) in ddb_devices { - let session_initialization_info_map = parse_map_attribute( - USERS_TABLE_DEVICE_ATTRIBUTE_NAME, - Some(session_initialization_info), - )?; - let mut inner_hash_map = HashMap::new(); - for (initialization_component_name, initialization_component_value) in - session_initialization_info_map - { - let initialization_piece_value_string = parse_string_attribute( - USERS_TABLE_INITIALIZATION_INFO, - Some(initialization_component_value), - )?; - inner_hash_map.insert( - initialization_component_name, - initialization_piece_value_string, - ); - } - devices.insert(signing_public_key, inner_hash_map); - } - Ok(devices) -} - fn parse_map_attribute( attribute_name: &'static str, attribute_value: Option, diff --git a/shared/protos/identity_client.proto b/shared/protos/identity_client.proto --- a/shared/protos/identity_client.proto +++ b/shared/protos/identity_client.proto @@ -132,6 +132,8 @@ } message RegistrationFinishResponse { + // Unique identifier for newly registered user + string userID = 1; // After successful unpacking of user credentials, return token string accessToken = 2; }