diff --git a/.buildkite/eslint_flow_jest.yml b/.buildkite/eslint_flow_jest.yml --- a/.buildkite/eslint_flow_jest.yml +++ b/.buildkite/eslint_flow_jest.yml @@ -4,6 +4,7 @@ - '(pkill flow || true)' - 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y' - '. /root/.cargo/env' + - 'apt update && apt install -y cmake' - 'yarn cleaninstall --frozen-lockfile --skip-optional --network-timeout 180000' - 'yarn eslint --max-warnings=0 && yarn workspace lib flow && yarn workspace web flow && yarn workspace landing flow && yarn workspace native flow && yarn workspace keyserver flow && yarn workspace desktop flow' - 'yarn workspace lib test && yarn workspace keyserver test' diff --git a/.buildkite/jsi_codegen.yml b/.buildkite/jsi_codegen.yml --- a/.buildkite/jsi_codegen.yml +++ b/.buildkite/jsi_codegen.yml @@ -2,6 +2,9 @@ - label: 'JSI Codegen' command: - '(pkill flow || true)' + - 'apt update && apt install -y cmake' + - 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y' + - '. /root/.cargo/env' - 'yarn cleaninstall --frozen-lockfile --skip-optional --network-timeout 180000' - 'cd native && yarn codegen-jsi && git diff --exit-code' plugins: diff --git a/keyserver/Dockerfile b/keyserver/Dockerfile --- a/keyserver/Dockerfile +++ b/keyserver/Dockerfile @@ -49,9 +49,11 @@ # We need rsync in the prod-build yarn script # We need mariadb-client so we can use mysqldump for backups +# We need protobuf-compiler to build rust-node-addon RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ rsync \ mariadb-client \ + protobuf-compiler \ && rm -rf /var/lib/apt/lists/* #------------------------------------------------------------------------------- @@ -100,7 +102,7 @@ # We run yarn cleaninstall before copying most of the files in for build caching #------------------------------------------------------------------------------- -# Copy in package.json and yarn.lock files +# Copy in package.json files, yarn.lock files, and required scripts COPY --chown=comm package.json yarn.lock ./ COPY --chown=comm keyserver/package.json keyserver/.flowconfig keyserver/ COPY --chown=comm lib/package.json lib/.flowconfig lib/ @@ -109,6 +111,7 @@ COPY --chown=comm landing/package.json landing/.flowconfig landing/ COPY --chown=comm desktop/package.json desktop/ COPY --chown=comm keyserver/addons/rust-node-addon/package.json \ + keyserver/addons/rust-node-addon/install_external_deps.sh \ keyserver/addons/rust-node-addon/ COPY --chown=comm native/expo-modules/android-lifecycle/package.json \ native/expo-modules/android-lifecycle/ @@ -118,6 +121,9 @@ COPY --chown=comm keyserver/addons/rust-node-addon/Cargo.toml \ keyserver/addons/rust-node-addon/ +# Copy in comm-opaque library, a dependency of rust-node-addon +COPY --chown=comm shared/comm-opaque shared/comm-opaque/ + # Copy in files needed for patch-package COPY --chown=comm patches patches/ diff --git a/keyserver/addons/rust-node-addon/Cargo.toml b/keyserver/addons/rust-node-addon/Cargo.toml --- a/keyserver/addons/rust-node-addon/Cargo.toml +++ b/keyserver/addons/rust-node-addon/Cargo.toml @@ -9,11 +9,24 @@ [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 = { version = "2.9.1", default-features = false } +opaque-ke = "1.2" +rand = "0.8" +tonic = "0.8" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } +tokio-stream = "0.1" +tracing = "0.1" +prost = "0.11" +comm-opaque = {path = "../../../shared/comm-opaque"} +lazy_static = "1.4" [build-dependencies] napi-build = "2.0.1" +tonic-build = "0.8" [profile.release] lto = true diff --git a/keyserver/addons/rust-node-addon/build.rs b/keyserver/addons/rust-node-addon/build.rs --- a/keyserver/addons/rust-node-addon/build.rs +++ b/keyserver/addons/rust-node-addon/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/rust-node-addon/index.js b/keyserver/addons/rust-node-addon/index.js --- a/keyserver/addons/rust-node-addon/index.js +++ b/keyserver/addons/rust-node-addon/index.js @@ -3,7 +3,13 @@ const { platform, arch } = process; type RustAPI = { - +sum: (a: number, b: number) => number, + +registerUser: ( + userId: string, + deviceId: string, + username: string, + password: string, + userPublicKey: string, + ) => Promise, }; async function getRustAPI(): Promise { @@ -28,8 +34,8 @@ throw new Error('Failed to load native binding'); } - const { sum } = nativeBinding.default; - return { sum }; + const { registerUser } = nativeBinding.default; + return { registerUser }; } export { getRustAPI }; diff --git a/keyserver/addons/rust-node-addon/install_external_deps.sh b/keyserver/addons/rust-node-addon/install_external_deps.sh new file mode 100755 --- /dev/null +++ b/keyserver/addons/rust-node-addon/install_external_deps.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -e + +echo "Buildkite var set: $BUILDKITE" + +# We can skip this script if it's not part of a CI workflow +if ! { [ -n "$BUILDKITE" ] || [ -n "$CI" ]; } +then + echo "Not in a CI workflow, exiting" + exit +fi + +# Install protobuf if it's not already installed +if [[ ! "$(command -v protoc)" ]] +then + echo "Installing protobuf" + . ../../../scripts/install_protobuf.sh +fi diff --git a/keyserver/addons/rust-node-addon/package.json b/keyserver/addons/rust-node-addon/package.json --- a/keyserver/addons/rust-node-addon/package.json +++ b/keyserver/addons/rust-node-addon/package.json @@ -30,10 +30,11 @@ }, "scripts": { "artifacts": "napi artifacts", - "build": "napi build --platform napi --release", + "build": "yarn install-external-deps && napi build --platform napi --release", "build:debug": "napi build --platform napi", "version": "napi version", "postinstall": "yarn build", - "clean": "rm -rf target/ && rm -rf napi/ && rm -rf node_modules/" + "clean": "rm -rf target/ && rm -rf napi/ && rm -rf node_modules/", + "install-external-deps": "./install_external_deps.sh" } } diff --git a/keyserver/addons/rust-node-addon/src/identity_client.rs b/keyserver/addons/rust-node-addon/src/identity_client.rs new file mode 100644 --- /dev/null +++ b/keyserver/addons/rust-node-addon/src/identity_client.rs @@ -0,0 +1,312 @@ +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 tokio::sync::mpsc; +use tokio_stream::wrappers::ReceiverStream; +use tonic::Request; +use tracing::{error, instrument}; +mod identity { + tonic::include_proto!("identity"); +} +use comm_opaque::Cipher; +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, +}; +use std::env::var; + +lazy_static! { + static ref IDENTITY_SERVICE_SOCKET_ADDR: String = + var("COMM_IDENTITY_SERVICE_SOCKET_ADDR") + .unwrap_or("https://[::1]:50051".to_string()); +} + +#[napi] +#[instrument(skip_all)] +pub async fn register_user( + user_id: String, + device_id: String, + username: String, + password: String, + user_public_key: String, +) -> Result { + let mut identity_client = + IdentityServiceClient::connect(IDENTITY_SERVICE_SOCKET_ADDR.as_str()) + .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 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, + 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)) + } +} + +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) +} diff --git a/keyserver/addons/rust-node-addon/src/lib.rs b/keyserver/addons/rust-node-addon/src/lib.rs --- a/keyserver/addons/rust-node-addon/src/lib.rs +++ b/keyserver/addons/rust-node-addon/src/lib.rs @@ -1,9 +1,4 @@ -#![deny(clippy::all)] +pub mod identity_client; #[macro_use] extern crate napi_derive; - -#[napi] -pub fn sum(a: i32, b: i32) -> i32 { - a + b -}