diff --git a/keyserver/addons/opaque-ke-napi/Cargo.toml b/keyserver/addons/opaque-ke-napi/Cargo.toml --- a/keyserver/addons/opaque-ke-napi/Cargo.toml +++ b/keyserver/addons/opaque-ke-napi/Cargo.toml @@ -9,11 +9,26 @@ [dependencies] # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "2.10.1", default-features = false, features = ["napi4"] } +napi = { version = "2.10.1", default-features = false, features = [ + "napi4", + "tokio_rt", +] } napi-derive = "2.9.1" +argon2 = "0.3" +opaque-ke = "1.2" +curve25519-dalek = "3.2" +rand = "0.8" +sha2 = "0.9" +digest = "0.9" +tonic = "0.8" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +tokio-stream = "0.1" +tracing = "0.1" +prost = "0.11" [build-dependencies] napi-build = "2.0.1" +tonic-build = "0.8" [profile.release] lto = true diff --git a/keyserver/addons/opaque-ke-napi/build.rs b/keyserver/addons/opaque-ke-napi/build.rs --- a/keyserver/addons/opaque-ke-napi/build.rs +++ b/keyserver/addons/opaque-ke-napi/build.rs @@ -2,4 +2,6 @@ fn main() { napi_build::setup(); + tonic_build::compile_protos("../../../shared/protos/identity.proto") + .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); } diff --git a/keyserver/addons/opaque-ke-napi/index.d.ts b/keyserver/addons/opaque-ke-napi/index.d.ts --- a/keyserver/addons/opaque-ke-napi/index.d.ts +++ b/keyserver/addons/opaque-ke-napi/index.d.ts @@ -3,4 +3,5 @@ /* auto-generated by NAPI-RS */ +export function registerUser(userId: string, deviceId: string, username: string, password: string, userPublicKey: string): Promise export function sum(a: number, b: number): number diff --git a/keyserver/addons/opaque-ke-napi/index.js b/keyserver/addons/opaque-ke-napi/index.js --- a/keyserver/addons/opaque-ke-napi/index.js +++ b/keyserver/addons/opaque-ke-napi/index.js @@ -1,5 +1,4 @@ // @flow - const { existsSync, readFileSync } = require('fs'); const { join } = require('path'); @@ -246,6 +245,7 @@ throw new Error(`Failed to load native binding`); } -const { sum } = nativeBinding; +const { registerUser, sum } = nativeBinding; +module.exports.registerUser = registerUser; module.exports.sum = sum; diff --git a/keyserver/addons/opaque-ke-napi/src/lib.rs b/keyserver/addons/opaque-ke-napi/src/lib.rs --- a/keyserver/addons/opaque-ke-napi/src/lib.rs +++ b/keyserver/addons/opaque-ke-napi/src/lib.rs @@ -1,8 +1,339 @@ -#![deny(clippy::all)] - +use argon2::Argon2; +use digest::{generic_array::GenericArray, Digest}; +use napi::bindgen_prelude::*; +use opaque_ke::{ + ciphersuite::CipherSuite, errors::InternalPakeError, hash::Hash, + slow_hash::SlowHash, ClientLogin, ClientLoginFinishParameters, + ClientLoginStartParameters, ClientLoginStartResult, ClientRegistration, + ClientRegistrationFinishParameters, CredentialFinalization, + CredentialResponse, RegistrationResponse, RegistrationUpload, +}; +use rand::{rngs::OsRng, CryptoRng, Rng}; +use tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tonic::Request; +use tracing::{error, instrument}; +mod identity { + tonic::include_proto!("identity"); +} +use identity::identity_service_client::IdentityServiceClient; +use identity::{ + 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, + PakeLoginResponse as PakeLoginResponseStruct, + PakeRegistrationRequestAndUserId as PakeRegistrationRequestAndUserIdStruct, + PakeRegistrationUploadAndCredentialRequest as PakeRegistrationUploadAndCredentialRequestStruct, + RegistrationRequest, RegistrationResponse as RegistrationResponseMessage, +}; #[macro_use] extern crate napi_derive; +const IDENTITY_SERVICE_SOCKET_ADDR: &str = "https://[::1]:50051"; + +pub struct Cipher; + +impl CipherSuite for Cipher { + type Group = curve25519_dalek::ristretto::RistrettoPoint; + type KeyExchange = opaque_ke::key_exchange::tripledh::TripleDH; + type Hash = sha2::Sha512; + type SlowHash = ArgonWrapper; +} + +pub struct ArgonWrapper(Argon2<'static>); + +impl SlowHash for ArgonWrapper { + fn hash( + input: GenericArray::OutputSize>, + ) -> std::result::Result, InternalPakeError> { + let params = Argon2::default(); + let mut output = vec![0u8; ::output_size()]; + params + .hash_password_into(&input, &[0; argon2::MIN_SALT_LEN], &mut output) + .map_err(|_| InternalPakeError::SlowHashError)?; + Ok(output) + } +} + +#[napi] +#[instrument] +pub async fn register_user( + user_id: String, + device_id: String, + username: String, + password: String, + user_public_key: String, +) -> Result { + // Create a gRPC client + let mut identity_client = + IdentityServiceClient::connect(IDENTITY_SERVICE_SOCKET_ADDR) + .await + .map_err(|_| Error::from_status(Status::GenericFailure))?; + + // 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 = Request::new(stream); + + // `response` is the Stream for inbound messages + let mut response = identity_client + .register_user(request) + .await + .map_err(|_| Error::from_status(Status::GenericFailure))? + .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, + device_id, + username, + user_public_key, + )?; + if let Err(e) = tx.send(registration_request).await { + error!("Response was dropped: {}", e); + return Err(Error::from_status(Status::GenericFailure)); + } + + // 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(|_| Error::from_status(Status::GenericFailure))?; + let client_login = handle_registration_response( + message, + &mut client_rng, + client_registration, + &password, + tx.clone(), + ) + .await?; + + // Finish PAKE login; send final login request to Identity service + let message = response + .message() + .await + .map_err(|_| Error::from_status(Status::GenericFailure))?; + handle_registration_credential_response(message, client_login, tx) + .await + .map_err(|_| Error::from_status(Status::GenericFailure))?; + + // Return access token + let message = response + .message() + .await + .map_err(|_| Error::from_status(Status::GenericFailure))?; + handle_registration_token_response(message) +} + +fn pake_registration_start( + rng: &mut (impl Rng + CryptoRng), + user_id: String, + password: &str, + device_id: String, + username: String, + user_public_key: String, +) -> Result<(RegistrationRequest, 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(( + RegistrationRequest { + data: Some(PakeRegistrationRequestAndUserId( + PakeRegistrationRequestAndUserIdStruct { + user_id, + device_id, + pake_registration_request, + username, + user_public_key, + }, + )), + }, + client_registration_start_result.state, + )) +} + +async fn handle_registration_response( + message: Option, + client_rng: &mut (impl Rng + CryptoRng), + client_registration: ClientRegistration, + password: &str, + tx: mpsc::Sender, +) -> Result> { + if let Some(RegistrationResponseMessage { + data: Some(PakeRegistrationResponse(registration_response_bytes)), + .. + }) = message + { + let pake_registration_upload = pake_registration_finish( + client_rng, + ®istration_response_bytes, + client_registration, + )? + .serialize(); + let client_login_start_result = pake_login_start(client_rng, password)?; + + // `registration_request` is a gRPC message containing serialized bytes to + // complete PAKE registration and begin PAKE login + let registration_request = RegistrationRequest { + data: Some(PakeRegistrationUploadAndCredentialRequest( + PakeRegistrationUploadAndCredentialRequestStruct { + pake_registration_upload, + pake_credential_request: client_login_start_result + .message + .serialize() + .map_err(|e| { + error!("Could not serialize credential request: {}", e); + Error::from_status(Status::GenericFailure) + })?, + }, + )), + }; + if let Err(e) = tx.send(registration_request).await { + error!("Response was dropped: {}", e); + return Err(Error::from_status(Status::GenericFailure)); + } + Ok(client_login_start_result.state) + } else { + Err(handle_unexpected_response(message)) + } +} + +async fn handle_registration_credential_response( + message: Option, + client_login: ClientLogin, + tx: mpsc::Sender, +) -> Result<()> { + if let Some(RegistrationResponseMessage { + data: + Some(RegistrationPakeLoginResponse(PakeLoginResponseStruct { + data: Some(PakeCredentialResponse(credential_response_bytes)), + })), + }) = message + { + let registration_request = RegistrationRequest { + data: Some(RegistrationPakeCredentialFinalization( + pake_login_finish(&credential_response_bytes, client_login)? + .serialize() + .map_err(|e| { + error!("Could not serialize credential request: {}", e); + Error::from_status(Status::GenericFailure) + })?, + )), + }; + send_to_mpsc(tx, registration_request).await + } else { + Err(handle_unexpected_response(message)) + } +} + +fn handle_registration_token_response( + message: Option, +) -> Result { + if let Some(RegistrationResponseMessage { + data: + Some(RegistrationPakeLoginResponse(PakeLoginResponseStruct { + data: Some(AccessToken(access_token)), + })), + }) = message + { + Ok(access_token) + } else { + Err(handle_unexpected_response(message)) + } +} + +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 handle_unexpected_response(message: Option) -> Error { + error!("Received an unexpected message: {:?}", message); + Error::from_status(Status::GenericFailure) +} + +fn pake_registration_finish( + rng: &mut (impl Rng + CryptoRng), + registration_response_bytes: &[u8], + client_registration: ClientRegistration, +) -> Result> { + client_registration + .finish( + rng, + RegistrationResponse::deserialize(registration_response_bytes).map_err( + |e| { + error!("Could not deserialize registration response bytes: {}", e); + Error::from_status(Status::GenericFailure) + }, + )?, + ClientRegistrationFinishParameters::default(), + ) + .map_err(|e| { + error!("Failed to finish PAKE registration: {}", e); + Error::from_status(Status::GenericFailure) + }) + .map(|res| res.message) +} + +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) +} + #[napi] pub fn sum(a: i32, b: i32) -> i32 { a + b