diff --git a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs index e1abab831..815cdb8bf 100644 --- a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs @@ -1,117 +1,118 @@ pub mod delete_user; pub mod login_user; pub mod register_user; pub mod identity { tonic::include_proto!("identity"); } +pub mod update_user; use comm_opaque::Cipher; use identity::identity_service_client::IdentityServiceClient; use identity::{ login_request::Data::PakeLoginRequest, login_request::Data::WalletLoginRequest, login_response::Data::PakeLoginResponse as LoginPakeLoginResponse, login_response::Data::WalletLoginResponse, pake_login_request::Data::PakeCredentialFinalization as LoginPakeCredentialFinalization, pake_login_request::Data::PakeCredentialRequestAndUserId, pake_login_response::Data::AccessToken, pake_login_response::Data::PakeCredentialResponse, registration_request::Data::PakeCredentialFinalization as RegistrationPakeCredentialFinalization, registration_request::Data::PakeRegistrationRequestAndUserId, registration_request::Data::PakeRegistrationUploadAndCredentialRequest, registration_response::Data::PakeLoginResponse as RegistrationPakeLoginResponse, registration_response::Data::PakeRegistrationResponse, DeleteUserRequest, LoginRequest, LoginResponse, PakeCredentialRequestAndUserId as PakeCredentialRequestAndUserIdStruct, PakeLoginRequest as PakeLoginRequestStruct, PakeLoginResponse as PakeLoginResponseStruct, PakeRegistrationRequestAndUserId as PakeRegistrationRequestAndUserIdStruct, PakeRegistrationUploadAndCredentialRequest as PakeRegistrationUploadAndCredentialRequestStruct, RegistrationRequest, RegistrationResponse as RegistrationResponseMessage, SessionInitializationInfo, WalletLoginRequest as WalletLoginRequestStruct, WalletLoginResponse as WalletLoginResponseStruct, }; use lazy_static::lazy_static; use napi::bindgen_prelude::*; use opaque_ke::{ ClientLogin, ClientLoginFinishParameters, ClientLoginStartParameters, ClientLoginStartResult, ClientRegistration, ClientRegistrationFinishParameters, CredentialFinalization, CredentialResponse, RegistrationResponse, RegistrationUpload, }; use rand::{rngs::OsRng, CryptoRng, Rng}; use std::collections::HashMap; use std::env::var; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; use tonic::{metadata::MetadataValue, transport::Channel, Request}; use tracing::{error, instrument}; lazy_static! { pub static ref IDENTITY_SERVICE_SOCKET_ADDR: String = var("COMM_IDENTITY_SERVICE_SOCKET_ADDR") .unwrap_or_else(|_| "https://[::1]:50051".to_string()); pub static ref AUTH_TOKEN: String = var("COMM_IDENTITY_SERVICE_AUTH_TOKEN") .unwrap_or_else(|_| "test".to_string()); } fn handle_unexpected_response(message: Option) -> Error { error!("Received an unexpected message: {:?}", message); Error::from_status(Status::GenericFailure) } async fn send_to_mpsc(tx: mpsc::Sender, request: T) -> Result<()> { if let Err(e) = tx.send(request).await { error!("Response was dropped: {}", e); return Err(Error::from_status(Status::GenericFailure)); } Ok(()) } fn pake_login_start( rng: &mut (impl Rng + CryptoRng), password: &str, ) -> Result> { ClientLogin::::start( rng, password.as_bytes(), ClientLoginStartParameters::default(), ) .map_err(|e| { error!("Failed to start PAKE login: {}", e); Error::from_status(Status::GenericFailure) }) } fn pake_login_finish( credential_response_bytes: &[u8], client_login: ClientLogin, ) -> Result> { client_login .finish( CredentialResponse::deserialize(credential_response_bytes).map_err( |e| { error!("Could not deserialize credential response bytes: {}", e); Error::from_status(Status::GenericFailure) }, )?, ClientLoginFinishParameters::default(), ) .map_err(|e| { error!("Failed to finish PAKE login: {}", e); Error::from_status(Status::GenericFailure) }) .map(|res| res.message) } async fn get_identity_service_channel() -> Result { Channel::from_static(&IDENTITY_SERVICE_SOCKET_ADDR) .connect() .await .map_err(|_| { Error::new( Status::GenericFailure, "Unable to connect to identity service".to_string(), ) }) } diff --git a/keyserver/addons/rust-node-addon/src/identity_client/update_user.rs b/keyserver/addons/rust-node-addon/src/identity_client/update_user.rs new file mode 100644 index 000000000..256bfb755 --- /dev/null +++ b/keyserver/addons/rust-node-addon/src/identity_client/update_user.rs @@ -0,0 +1,312 @@ +use crate::identity_client::identity as proto; +use crate::identity_client::identity::identity_service_client::IdentityServiceClient; +use crate::identity_client::identity::pake_login_response::Data::AccessToken; +use crate::identity_client::identity::{ + update_user_request, update_user_response, UpdateUserRequest, + UpdateUserResponse, +}; +use crate::identity_client::{AUTH_TOKEN, IDENTITY_SERVICE_SOCKET_ADDR}; +use comm_opaque::Cipher; +use napi::bindgen_prelude::*; +use opaque_ke::{ + ClientLogin, ClientLoginFinishParameters, ClientLoginStartParameters, + ClientLoginStartResult, ClientRegistration, + ClientRegistrationFinishParameters, CredentialFinalization, + CredentialResponse, RegistrationUpload, +}; +use rand::{rngs::OsRng, CryptoRng, Rng}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tonic; +use tonic::{metadata::MetadataValue, transport::Channel}; +use tracing::{error, instrument}; + +#[napi] +#[instrument(skip_all)] +pub async fn update_user(user_id: String, password: String) -> Result { + let channel = Channel::from_static(&IDENTITY_SERVICE_SOCKET_ADDR) + .connect() + .await + .map_err(|_| Error::from_status(Status::GenericFailure))?; + let token: MetadataValue<_> = AUTH_TOKEN + .parse() + .map_err(|_| Error::from_status(Status::GenericFailure))?; + let mut identity_client = IdentityServiceClient::with_interceptor( + channel, + |mut req: tonic::Request<()>| { + req.metadata_mut().insert("authorization", token.clone()); + Ok(req) + }, + ); + + // Create a RegistrationRequest channel and use ReceiverStream to turn the + // MPSC receiver into a Stream for outbound messages + let (tx, rx) = mpsc::channel(1); + let stream = ReceiverStream::new(rx); + let request = tonic::Request::new(stream); + + // `response` is the Stream for inbound messages + let mut response = identity_client + .update_user(request) + .await + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))? + .into_inner(); + + // Start PAKE registration on client and send initial registration request + // to Identity service + let mut client_rng = OsRng; + let (registration_request, client_registration) = + pake_registration_start(&mut client_rng, user_id, &password)?; + send_to_mpsc(&tx, registration_request).await?; + + // Handle responses from Identity service sequentially, making sure we get + // messages in the correct order + + // Finish PAKE registration and begin PAKE login; send the final + // registration request and initial login request together to reduce the + // number of trips + let message = response + .message() + .await + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + let registration_response = get_registration_response(message)?; + let client_login = handle_registration_response( + ®istration_response, + &mut client_rng, + client_registration, + &password, + &tx, + ) + .await?; + + // Finish PAKE login; send final login request to Identity service + let message = response + .message() + .await + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + let credential_response = get_login_credential_response(message)?; + handle_login_credential_response(&credential_response, client_login, &tx) + .await + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + + // Return access token + let message = response + .message() + .await + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + get_login_token_response(message) +} + +fn handle_unexpected_response(message: Option) -> Error { + error!("Received an unexpected message: {:?}", message); + Error::from_status(Status::GenericFailure) +} + +async fn send_to_mpsc(tx: &mpsc::Sender, request: T) -> Result<()> { + if let Err(e) = tx.send(request).await { + error!("Response was dropped: {}", e); + return Err(Error::from_status(Status::GenericFailure)); + } + Ok(()) +} + +fn pake_login_start( + rng: &mut (impl Rng + CryptoRng), + password: &str, +) -> Result> { + ClientLogin::::start( + rng, + password.as_bytes(), + ClientLoginStartParameters::default(), + ) + .map_err(|e| { + error!("Failed to start PAKE login: {}", e); + Error::from_status(Status::GenericFailure) + }) +} + +fn pake_login_finish( + credential_response_bytes: &[u8], + client_login: ClientLogin, +) -> Result> { + client_login + .finish( + CredentialResponse::deserialize(credential_response_bytes).map_err( + |e| { + error!("Could not deserialize credential response bytes: {}", e); + Error::from_status(Status::GenericFailure) + }, + )?, + ClientLoginFinishParameters::default(), + ) + .map_err(|e| { + error!("Failed to finish PAKE login: {}", e); + Error::from_status(Status::GenericFailure) + }) + .map(|res| res.message) +} + +fn pake_registration_start( + rng: &mut (impl Rng + CryptoRng), + user_id: String, + password: &str, +) -> Result<(UpdateUserRequest, ClientRegistration)> { + let client_registration_start_result = + ClientRegistration::::start(rng, password.as_bytes()).map_err( + |e| { + error!("Failed to start PAKE registration: {}", e); + Error::from_status(Status::GenericFailure) + }, + )?; + let pake_registration_request = + client_registration_start_result.message.serialize(); + Ok(( + UpdateUserRequest { + data: Some(update_user_request::Data::Request( + crate::identity_client::identity::PakeRegistrationRequestAndUserId { + user_id, + pake_registration_request, + username: "placeholder-username".to_string(), + signing_public_key: "placeholder-signing-public-key".to_string(), + session_initialization_info: None, + }, + )), + }, + client_registration_start_result.state, + )) +} + +async fn handle_registration_response( + registration_reponse_payload: &[u8], + client_rng: &mut (impl Rng + CryptoRng), + client_registration: ClientRegistration, + password: &str, + tx: &mpsc::Sender, +) -> Result> { + let pake_registration_upload = pake_registration_finish( + client_rng, + ®istration_reponse_payload, + client_registration, + )? + .serialize(); + let client_login_start_result = pake_login_start(client_rng, password)?; + let pake_login_request = + client_login_start_result.message.serialize().map_err(|e| { + error!("Could not serialize credential request: {}", e); + Error::from_status(Status::GenericFailure) + })?; + + // `registration_request` is a gRPC message containing serialized bytes to + // complete PAKE registration and begin PAKE login + let inner_message: update_user_request::Data = + update_user_request::Data::PakeRegistrationUploadAndCredentialRequest( + crate::identity_client::identity::PakeRegistrationUploadAndCredentialRequest { + pake_registration_upload, + pake_credential_request: pake_login_request, + }, + ); + let registration_request = UpdateUserRequest { + data: Some(inner_message), + }; + send_to_mpsc(tx, registration_request).await?; + Ok(client_login_start_result.state) +} + +fn get_registration_response( + message: Option, +) -> Result> { + match message { + Some(UpdateUserResponse { + data: + Some(update_user_response::Data::PakeRegistrationResponse( + registration_response_bytes, + )), + .. + }) => Ok(registration_response_bytes), + _ => { + error!("Received an unexpected message: {:?}", message); + Err(Error::from_status(Status::GenericFailure)) + } + } +} + +async fn handle_login_credential_response( + registration_response_payload: &[u8], + client_login: ClientLogin, + tx: &mpsc::Sender, +) -> Result<()> { + let pake_login_finish_result = + pake_login_finish(®istration_response_payload, client_login)?; + let login_finish_message = + pake_login_finish_result.serialize().map_err(|e| { + error!("Could not serialize credential request: {}", e); + Error::from_status(Status::GenericFailure) + })?; + let registration_request = UpdateUserRequest { + data: Some( + proto::update_user_request::Data::PakeLoginFinalizationMessage( + login_finish_message, + ), + ), + }; + send_to_mpsc(tx, registration_request).await +} + +fn get_login_credential_response( + message: Option, +) -> Result> { + match message { + Some(UpdateUserResponse { + data: + Some(update_user_response::Data::PakeLoginResponse( + proto::PakeLoginResponse { + data: + Some(proto::pake_login_response::Data::PakeCredentialResponse( + bytes, + )), + }, + )), + }) => Ok(bytes), + _ => Err(handle_unexpected_response(message)), + } +} + +fn get_login_token_response( + message: Option, +) -> Result { + match message { + Some(UpdateUserResponse { + data: + Some(update_user_response::Data::PakeLoginResponse( + proto::PakeLoginResponse { + data: Some(AccessToken(access_token)), + }, + )), + }) => Ok(access_token), + _ => Err(handle_unexpected_response(message)), + } +} + +fn pake_registration_finish( + rng: &mut (impl Rng + CryptoRng), + registration_response_bytes: &[u8], + client_registration: ClientRegistration, +) -> Result> { + let register_payload = + opaque_ke::RegistrationResponse::deserialize(registration_response_bytes) + .map_err(|e| { + error!("Could not deserialize registration response bytes: {}", e); + Error::from_status(Status::GenericFailure) + })?; + client_registration + .finish( + rng, + register_payload, + ClientRegistrationFinishParameters::default(), + ) + .map_err(|e| { + error!("Failed to finish PAKE registration: {}", e); + Error::from_status(Status::GenericFailure) + }) + .map(|res| res.message) +} diff --git a/keyserver/src/updaters/account-updaters.js b/keyserver/src/updaters/account-updaters.js index aed36bb54..4eaef7827 100644 --- a/keyserver/src/updaters/account-updaters.js +++ b/keyserver/src/updaters/account-updaters.js @@ -1,111 +1,119 @@ // @flow +import { getRustAPI } from 'rust-node-addon'; import bcrypt from 'twin-bcrypt'; import type { ResetPasswordRequest, UpdatePasswordRequest, UpdateUserSettingsRequest, LogInResponse, } from 'lib/types/account-types.js'; import { updateTypes } from 'lib/types/update-types.js'; import type { PasswordUpdate } from 'lib/types/user-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { createUpdates } from '../creators/update-creator.js'; import { dbQuery, SQL } from '../database/database.js'; +import { handleAsyncPromise } from '../responders/handlers.js'; import type { Viewer } from '../session/viewer.js'; async function accountUpdater( viewer: Viewer, update: PasswordUpdate, ): Promise { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const newPassword = update.updatedFields.password; if (!newPassword) { // If it's an old client it may have given us an email, // but we don't store those anymore return; } const verifyQuery = SQL` SELECT username, hash FROM users WHERE id = ${viewer.userID} `; const [verifyResult] = await dbQuery(verifyQuery); if (verifyResult.length === 0) { throw new ServerError('internal_error'); } const verifyRow = verifyResult[0]; if (!bcrypt.compareSync(update.currentPassword, verifyRow.hash)) { throw new ServerError('invalid_credentials'); } const changedFields = { hash: bcrypt.hashSync(newPassword) }; const saveQuery = SQL` UPDATE users SET ${changedFields} WHERE id = ${viewer.userID} `; await dbQuery(saveQuery); + handleAsyncPromise( + (async () => { + const rustApi = await getRustAPI(); + await rustApi.updateUser(viewer.userID, newPassword); + })(), + ); const updateDatas = [ { type: updateTypes.UPDATE_CURRENT_USER, userID: viewer.userID, time: Date.now(), }, ]; await createUpdates(updateDatas, { viewer, updatesForCurrentSession: 'broadcast', }); } // eslint-disable-next-line no-unused-vars async function checkAndSendVerificationEmail(viewer: Viewer): Promise { // We don't want to crash old clients that call this, // but we have nothing we can do because we no longer store email addresses } async function checkAndSendPasswordResetEmail( // eslint-disable-next-line no-unused-vars request: ResetPasswordRequest, ): Promise { // We don't want to crash old clients that call this, // but we have nothing we can do because we no longer store email addresses } /* eslint-disable no-unused-vars */ async function updatePassword( viewer: Viewer, request: UpdatePasswordRequest, ): Promise { /* eslint-enable no-unused-vars */ // We have no way to handle this request anymore throw new ServerError('deprecated'); } async function updateUserSettings( viewer: Viewer, request: UpdateUserSettingsRequest, ) { if (!viewer.loggedIn) { throw new ServerError('not_logged_in'); } const createOrUpdateSettingsQuery = SQL` INSERT INTO settings (user, name, data) VALUES ${[[viewer.id, request.name, request.data]]} ON DUPLICATE KEY UPDATE data = VALUE(data) `; await dbQuery(createOrUpdateSettingsQuery); } export { accountUpdater, checkAndSendVerificationEmail, checkAndSendPasswordResetEmail, updateUserSettings, updatePassword, }; diff --git a/lib/types/rust-binding-types.js b/lib/types/rust-binding-types.js index e50b54454..ad09fa458 100644 --- a/lib/types/rust-binding-types.js +++ b/lib/types/rust-binding-types.js @@ -1,44 +1,45 @@ // @flow import type { SignedIdentityKeysBlob } from './crypto-types.js'; type tunnelbrokerOnReceiveCallback = ( err: Error | null, payload: string, ) => mixed; declare class TunnelbrokerClientClass { constructor( deviceId: string, onReceiveCallback: tunnelbrokerOnReceiveCallback, ): TunnelbrokerClientClass; publish(toDeviceId: string, payload: string): Promise; } type RustNativeBindingAPI = { +registerUser: ( userId: string, signingPublicKey: string, username: string, password: string, sessionInitializationInfo: SignedIdentityKeysBlob, ) => Promise, +loginUserPake: ( userId: string, signingPublicKey: string, password: string, sessionInitializationInfo: SignedIdentityKeysBlob, ) => Promise, +loginUserWallet: ( userId: string, signingPublicKey: string, siweMessage: string, siweSignature: string, sessionInitializationInfo: SignedIdentityKeysBlob, socialProof: string, ) => Promise, +deleteUser: (userId: string) => Promise, + +updateUser: (userId: string, password: string) => Promise, +TunnelbrokerClient: Class, }; export type { RustNativeBindingAPI };