diff --git a/native/native_rust_library/src/identity_client.rs b/native/native_rust_library/src/identity_client.rs --- a/native/native_rust_library/src/identity_client.rs +++ b/native/native_rust_library/src/identity_client.rs @@ -1,21 +1,88 @@ +use comm_opaque2::client::Registration; +use comm_opaque2::grpc::opaque_error_to_grpc_status as handle_error; +use tonic::transport::Channel; use tonic::Status; +use crate::identity; use crate::IdentityClient; +use identity::identity_client_service_client::IdentityClientServiceClient; + +#[cfg(debug_assertions)] +async fn open_identity_channel( +) -> Result, Status> { + IdentityClientServiceClient::connect("https://localhost:50054") + .await + .map_err(|_| { + Status::unavailable("Could not connect to local identity service") + }) +} + +#[cfg(not(debug_assertions))] +async fn open_identity_channel( +) -> Result, Status> { + IdentityClientServiceClient::connect("https://comm.app:50054") + .await + .map_err(|_| Status::unavailable("Could not connect to identity service")) +} pub async fn register_user( - mut _client: Box, - _username: String, - _password: String, - _key_payload: String, - _key_payload_signature: String, - _identity_prekey: String, - _identity_prekey_signature: String, - _notif_prekey: String, - _notif_prekey_signature: String, - _identity_onetime_keys: Vec, - _notif_onetime_keys: Vec, + username: String, + password: String, + key_payload: String, + key_payload_signature: String, + identity_prekey: String, + identity_prekey_signature: String, + notif_prekey: String, + notif_prekey_signature: String, + identity_onetime_keys: Vec, + notif_onetime_keys: Vec, ) -> Result { - unimplemented!(); + let mut client_register = Registration::new(); + let register_message = + client_register.start(&password).map_err(handle_error)?; + + let register_request = identity::RegistrationStartRequest { + opaque_registration_request: register_message, + username, + device_key_upload: Some(identity::DeviceKeyUpload { + device_key_info: Some(identity::IdentityKeyInfo { + payload: key_payload, + payload_signature: key_payload_signature, + social_proof: None, + }), + identity_upload: Some(identity::PreKey { + pre_key: identity_prekey, + pre_key_signature: identity_prekey_signature, + }), + notif_upload: Some(identity::PreKey { + pre_key: notif_prekey, + pre_key_signature: notif_prekey_signature, + }), + onetime_identity_prekeys: identity_onetime_keys, + onetime_notif_prekeys: notif_onetime_keys, + }), + }; + + let mut identity_client = open_identity_channel().await?; + let register_response = identity_client + .register_password_user_start(register_request) + .await? + .into_inner(); + + let register_finish = client_register + .finish(&password, ®ister_response.opaque_registration_response) + .map_err(handle_error)?; + let finish_request = identity::RegistrationFinishRequest { + session_id: register_response.session_id, + opaque_registration_upload: register_finish, + }; + + let final_response = identity_client + .register_password_user_finish(finish_request) + .await? + .into_inner(); + + Ok(final_response.access_token) } // User could be logging in from new device, need to resend device information diff --git a/native/native_rust_library/src/lib.rs b/native/native_rust_library/src/lib.rs --- a/native/native_rust_library/src/lib.rs +++ b/native/native_rust_library/src/lib.rs @@ -48,7 +48,6 @@ #[cxx_name = "identityRegisterUserBlocking"] fn identity_register_user_blocking( - client: Box, username: String, password: String, key_payload: String, @@ -157,7 +156,6 @@ #[instrument] fn identity_register_user_blocking( - client: Box, username: String, password: String, key_payload: String, @@ -170,7 +168,6 @@ notif_onetime_keys: Vec, ) -> Result { RUNTIME.block_on(identity_client::register_user( - client, username, password, key_payload, diff --git a/services/identity/Cargo.lock b/services/identity/Cargo.lock --- a/services/identity/Cargo.lock +++ b/services/identity/Cargo.lock @@ -1330,6 +1330,7 @@ "tonic-web", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -2568,6 +2569,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" +[[package]] +name = "uuid" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +dependencies = [ + "getrandom 0.2.8", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/services/identity/Cargo.toml b/services/identity/Cargo.toml --- a/services/identity/Cargo.toml +++ b/services/identity/Cargo.toml @@ -30,6 +30,7 @@ tonic-web = "0.5" serde = { version = "1.0.159", features = [ "derive" ] } serde_json = "1.0.95" +uuid = { version = "1.3.0", features = [ "v4" ] } [build-dependencies] tonic-build = "0.8" 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 @@ -1,30 +1,38 @@ +use std::str::FromStr; + pub mod client_proto { tonic::include_proto!("identity.client"); } use crate::{ - client_service::client_proto::{ - DeleteUserRequest, Empty, GenerateNonceResponse, KeyserverKeysRequest, - KeyserverKeysResponse, OpaqueLoginFinishRequest, OpaqueLoginFinishResponse, - OpaqueLoginStartRequest, OpaqueLoginStartResponse, - ReceiverKeysForUserRequest, ReceiverKeysForUserResponse, - RefreshUserPreKeysRequest, RegistrationFinishRequest, - RegistrationFinishResponse, RegistrationStartRequest, - RegistrationStartResponse, SenderKeysForUserRequest, - SenderKeysForUserResponse, UpdateUserPasswordFinishRequest, - UpdateUserPasswordFinishResponse, UpdateUserPasswordStartRequest, - UpdateUserPasswordStartResponse, UploadOneTimeKeysRequest, - WalletLoginRequest, WalletLoginResponse, - }, - database::DatabaseClient, + config::CONFIG, + database::{DatabaseClient, KeyPayload}, nonce::generate_nonce_data, service::handle_db_error, + token::AccessTokenData, +}; +use client_proto::{ + DeleteUserRequest, Empty, GenerateNonceResponse, KeyserverKeysRequest, + KeyserverKeysResponse, OpaqueLoginFinishRequest, OpaqueLoginFinishResponse, + OpaqueLoginStartRequest, OpaqueLoginStartResponse, + ReceiverKeysForUserRequest, ReceiverKeysForUserResponse, + RefreshUserPreKeysRequest, RegistrationFinishRequest, + RegistrationFinishResponse, RegistrationStartRequest, + RegistrationStartResponse, SenderKeysForUserRequest, + SenderKeysForUserResponse, UpdateUserPasswordFinishRequest, + UpdateUserPasswordFinishResponse, UpdateUserPasswordStartRequest, + UpdateUserPasswordStartResponse, UploadOneTimeKeysRequest, + WalletLoginRequest, WalletLoginResponse, }; + +use comm_opaque2; +use comm_opaque2::grpc::protocol_error_to_grpc_status; + pub use client_proto::identity_client_service_server::{ IdentityClientService, IdentityClientServiceServer, }; use rand::rngs::OsRng; -use tonic::Response; +use tonic::{Response, Status}; #[derive(derive_more::Constructor)] pub struct ClientService { @@ -35,16 +43,135 @@ impl IdentityClientService for ClientService { async fn register_password_user_start( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { - unimplemented!(); + let message = request.into_inner(); + let username_available = self + .client + .username_available(message.username.clone()) + .await + .map_err(handle_db_error)?; + + if !username_available { + return Err(tonic::Status::already_exists("Username already taken")); + }; + + match message { + 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, + }), + identity_upload: + Some(client_proto::PreKey { + pre_key: identity_prekey, + pre_key_signature: identity_prekey_signature, + }), + notif_upload: + Some(client_proto::PreKey { + pre_key: notif_prekey, + pre_key_signature: notif_prekey_signature, + }), + onetime_identity_prekeys, + onetime_notif_prekeys, + }), + } => { + let server_register = comm_opaque2::server::Registration::new(); + let server_message = server_register + .start( + &CONFIG.server_setup, + ®ister_message, + &username.as_bytes(), + ) + .map_err(comm_opaque2::grpc::protocol_error_to_grpc_status)?; + + let key_info = KeyPayload::from_str(&payload) + .map_err(|_| Status::invalid_argument("Malformed key payload"))?; + + let user_id = self + .client + .add_user_to_opaque2_users_table( + username, + key_info.primary_identity_public_keys.curve25519, + payload, + payload_signature, + identity_prekey, + identity_prekey_signature, + onetime_identity_prekeys, + notif_prekey, + notif_prekey_signature, + onetime_notif_prekeys, + ) + .await + .map_err(handle_db_error)?; + + let response = RegistrationStartResponse { + session_id: user_id, + opaque_registration_response: server_message, + }; + Ok(Response::new(response)) + } + _ => Err(tonic::Status::invalid_argument("Unexpected message data")), + } } + // Called by client when they respond to opaque password registration challenge. + // Finishes opaque protocol and writes user's password file to DDB + // and returns access token to user. async fn register_password_user_finish( &self, - _request: tonic::Request, + request: tonic::Request, ) -> Result, tonic::Status> { - unimplemented!(); + let message = request.into_inner(); + + let server_register = comm_opaque2::server::Registration::new(); + let password_file = server_register + .finish(&message.opaque_registration_upload) + .map_err(protocol_error_to_grpc_status)?; + + self + .client + .set_password_file_for_user(&message.session_id, password_file) + .await + .map_err(handle_db_error)?; + + // Get user device information, access token is device specific + let device_id = self + .client + .get_user_devices(&message.session_id) + .await + .map_err(handle_db_error)? + .item + .ok_or(Status::invalid_argument("Missing a device list"))? + .keys() + .next() + .ok_or(Status::invalid_argument("Missing a device in device list"))? + .to_string(); + + // 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 { access_token }; + Ok(Response::new(response)) } 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 @@ -27,6 +27,7 @@ // { // deviceType: String, # client or keyserver // keyPayload: String, + // keyPayloadSignature: String, // identityPreKey: String, // identityPreKeySignature: String, // identityOneTimeKeys: Vec, @@ -46,8 +47,12 @@ 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_IDENTITY_PREKEY_ATTRIBUTE_NAME: &str = "identityPreKey"; pub const USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME: 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 @@ -14,10 +14,11 @@ use opaque_ke::{errors::ProtocolError, ServerRegistration}; use serde::{Deserialize, Serialize}; use tracing::{debug, error, info, warn}; +use uuid::Uuid; use crate::config::CONFIG; use crate::constants::{ - ACCESS_TOKEN_SORT_KEY, ACCESS_TOKEN_TABLE, + opaque2, ACCESS_TOKEN_SORT_KEY, ACCESS_TOKEN_TABLE, ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE, ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE, ACCESS_TOKEN_TABLE_PARTITION_KEY, ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE, ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE, NONCE_TABLE, @@ -34,15 +35,15 @@ #[derive(Serialize, Deserialize)] pub struct OlmKeys { - curve25519: String, - ed25519: String, + pub curve25519: String, + pub ed25519: String, } #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct KeyPayload { - notification_identity_public_keys: OlmKeys, - primary_identity_public_keys: OlmKeys, + pub notification_identity_public_keys: OlmKeys, + pub primary_identity_public_keys: OlmKeys, } impl FromStr for KeyPayload { @@ -53,10 +54,20 @@ fn from_str(payload: &str) -> Result { serde_json::from_str(&payload.replace(r#"\""#, r#"""#)) } +} + +pub enum Device { + Client, + Keyserver, +} - // fn to_payload(&self) -> Result { - // Ok(serde_json::to_string(self)?.replace(r#"""#, r#"\""#)) - // } +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)] @@ -270,6 +281,152 @@ .map_err(|e| Error::AwsSdk(e.into())) } + // This is called during the first call of a user register, the registration data + // will be empty, but later replaced with the opaque registration password + // file after opaque protocol finishes + pub async fn add_user_to_opaque2_users_table( + &self, + 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, + ) -> Result { + use opaque2::*; + + 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(key_payload), + ), + ( + USERS_TABLE_DEVICES_MAP_KEY_PAYLOAD_SIGNATURE_ATTRIBUTE_NAME + .to_string(), + AttributeValue::S(key_payload_signature), + ), + ( + USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_ATTRIBUTE_NAME.to_string(), + AttributeValue::S(identity_prekey), + ), + ( + USERS_TABLE_DEVICES_MAP_IDENTITY_PREKEY_SIGNATURE_ATTRIBUTE_NAME + .to_string(), + AttributeValue::S(identity_prekey_signature), + ), + ( + USERS_TABLE_DEVICES_MAP_IDENTITY_ONETIME_KEYS_ATTRIBUTE_NAME + .to_string(), + AttributeValue::L( + identity_onetime_keys + .into_iter() + .map(|s| AttributeValue::S(s)) + .collect(), + ), + ), + ( + USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME.to_string(), + AttributeValue::S(notif_prekey), + ), + ( + USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME + .to_string(), + AttributeValue::S(notif_prekey_signature), + ), + ( + USERS_TABLE_DEVICES_MAP_NOTIF_ONETIME_KEYS_ATTRIBUTE_NAME.to_string(), + AttributeValue::L( + notif_onetime_keys + .into_iter() + .map(|s| AttributeValue::S(s)) + .collect(), + ), + ), + ]); + let devices = + HashMap::from([(device_id_key, AttributeValue::M(device_info))]); + + let user = HashMap::from([ + ( + USERS_TABLE_PARTITION_KEY.to_string(), + AttributeValue::S(user_id.clone()), + ), + ( + USERS_TABLE_USERNAME_ATTRIBUTE.to_string(), + AttributeValue::S(username), + ), + ( + USERS_TABLE_DEVICES_ATTRIBUTE.to_string(), + AttributeValue::M(devices), + ), + ]); + + self + .client + .put_item() + .table_name(opaque2::USERS_TABLE) + .set_item(Some(user)) + .send() + .await + .map_err(|e| Error::AwsSdk(e.into()))?; + + Ok(user_id) + } + + pub async fn set_password_file_for_user( + &self, + user_id: &str, + password_file: Vec, + ) -> Result { + let update_expression = + format!("SET {} = :reg", opaque2::USERS_TABLE_REGISTRATION_ATTRIBUTE); + let update_values = HashMap::from([( + ":reg".to_string(), + AttributeValue::B(Blob::new(password_file)), + )]); + + self + .client + .update_item() + .table_name(opaque2::USERS_TABLE) + .key( + USERS_TABLE_PARTITION_KEY, + AttributeValue::S(user_id.to_string()), + ) + .update_expression(update_expression) + .set_expression_attribute_values(Some(update_values)) + .send() + .await + .map_err(|e| Error::AwsSdk(e.into())) + } + + pub async fn get_user_devices( + &self, + user_id: &str, + ) -> Result { + self + .client + .get_item() + .table_name(opaque2::USERS_TABLE) + .key( + opaque2::USERS_TABLE_PARTITION_KEY, + AttributeValue::S(user_id.to_string()), + ) + .projection_expression(opaque2::USERS_TABLE_DEVICES_ATTRIBUTE) + .send() + .await + .map_err(|e| Error::AwsSdk(e.into())) + } + pub async fn delete_user( &self, user_id: String, @@ -407,6 +564,22 @@ .map_err(|e| Error::AwsSdk(e.into())) } + pub async fn username_available( + &self, + username: String, + ) -> Result { + let result = self + .client + .get_item() + .table_name(USERS_TABLE) + .key(USERS_TABLE_USERNAME_ATTRIBUTE, AttributeValue::S(username)) + .send() + .await + .map_err(|e| Error::AwsSdk(e.into()))?; + + Ok(result.item.is_none()) + } + pub async fn get_user_id_from_user_info( &self, user_info: String, @@ -778,6 +951,14 @@ } } +fn generate_uuid() -> String { + let mut buf = [b'\0'; 36]; + Uuid::new_v4().hyphenated().encode_upper(&mut buf); + std::str::from_utf8(&buf) + .expect("Unable to create UUID") + .to_string() +} + #[cfg(test)] mod tests { use super::*; @@ -830,4 +1011,9 @@ "DYmV8VdkjwG/VtC8C53morogNJhpTPT/4jzW0/cxzQo" ); } + + #[test] + fn test_uuid() { + generate_uuid(); + } }