diff --git a/keyserver/addons/rust-node-addon/Cargo.lock b/keyserver/addons/rust-node-addon/Cargo.lock --- a/keyserver/addons/rust-node-addon/Cargo.lock +++ b/keyserver/addons/rust-node-addon/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "anyhow" version = "1.0.69" @@ -1013,6 +1022,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ + "aho-corasick", + "memchr", "regex-syntax", ] @@ -1065,6 +1076,7 @@ "napi", "napi-build", "napi-derive", + "regex", "serde", "serde_json", "tokio", 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 @@ -27,6 +27,7 @@ [build-dependencies] napi-build = "2.0.1" +regex = "1" [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 @@ -1,5 +1,44 @@ extern crate napi_build; +use regex::Regex; + +use std::env; +use std::fs; +use std::path::Path; + fn main() { napi_build::setup(); + + let js_path = Path::new("../../../lib/facts/version.js"); + + let content = + fs::read_to_string(&js_path).expect("Failed to read version.js"); + + let version_line = content + .lines() + .find(|line| line.contains("webAndKeyserverCodeVersion")) + .expect("Failed to find webAndKeyserverCodeVersion line"); + + // Find a sequence in the input string that starts with + // 'webAndKeyserverCodeVersion', followed by any number of whitespace + // characters, an equals sign, any number of additional whitespace characters, + // a series of one or more digits (and capture these digits), and finally a + // semicolon. + let re = Regex::new(r"webAndKeyserverCodeVersion\s*=\s*(\d+);").unwrap(); + let version: u64 = re + .captures(&version_line) + .and_then(|cap| cap.get(1)) + .map_or_else( + || panic!("Failed to capture version number"), + |m| m.as_str().parse::().unwrap(), + ); + + let out_dir = env::var("OUT_DIR").unwrap(); + let rust_path = Path::new(&out_dir).join("version.rs"); + + fs::write( + &rust_path, + format!("pub const CODE_VERSION: u64 = {};", version), + ) + .expect("Failed to write version.rs"); } diff --git a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs --- a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs @@ -5,24 +5,28 @@ pub mod remove_reserved_usernames; pub mod upload_one_time_keys; -use grpc_clients::identity::authenticated::AuthLayer; -use grpc_clients::identity::protos::unauthenticated as client_proto; -use grpc_clients::identity::protos::authenticated::identity_client_service_client::IdentityClientServiceClient as AuthClient; use client_proto::identity_client_service_client::IdentityClientServiceClient; use client_proto::{ - AddReservedUsernamesRequest, DeviceKeyUpload, IdentityKeyInfo, PreKey, - RegistrationFinishRequest, RegistrationStartRequest, DeviceType, - RemoveReservedUsernameRequest, InboundKeyInfo, UploadOneTimeKeysRequest + AddReservedUsernamesRequest, DeviceKeyUpload, DeviceType, IdentityKeyInfo, + InboundKeyInfo, PreKey, RegistrationFinishRequest, RegistrationStartRequest, + RemoveReservedUsernameRequest, UploadOneTimeKeysRequest, }; +use grpc_clients::identity::authenticated::ChainedInterceptedAuthClient; +use grpc_clients::identity::protos::unauthenticated as client_proto; +use grpc_clients::identity::shared::CodeVersionLayer; use lazy_static::lazy_static; use napi::bindgen_prelude::*; use serde::{Deserialize, Serialize}; -use tonic::codegen::InterceptedService; use std::env::var; +use tonic::codegen::InterceptedService; use tonic::{transport::Channel, Request}; use tracing::{self, info, instrument, warn, Level}; use tracing_subscriber::EnvFilter; +// We get the CODE_VERSION from this generated file +include!(concat!(env!("OUT_DIR"), "/version.rs")); +pub const DEVICE_TYPE: &str = "keyserver"; + lazy_static! { static ref IDENTITY_SERVICE_CONFIG: IdentityServiceConfig = { let filter = EnvFilter::builder() @@ -58,12 +62,15 @@ } } -async fn get_identity_client_service_channel( -) -> Result> { +async fn get_identity_client_service_channel() -> Result< + IdentityClientServiceClient>, +> { info!("Connecting to identity service"); grpc_clients::identity::get_unauthenticated_client( &IDENTITY_SERVICE_CONFIG.identity_socket_addr, + CODE_VERSION, + DEVICE_TYPE.to_string(), ) .await .map_err(|_| { @@ -78,7 +85,7 @@ user_id: String, device_id: String, access_token: String, -) -> Result>> { +) -> Result { info!("Connecting to identity service"); grpc_clients::identity::get_auth_client( @@ -86,6 +93,8 @@ user_id, device_id, access_token, + CODE_VERSION, + DEVICE_TYPE.to_string(), ) .await .map_err(|_| { @@ -167,3 +176,13 @@ warn!("Received error: {}", error.message()); Error::new(Status::GenericFailure, error.message()) } + +#[cfg(test)] +mod tests { + use super::CODE_VERSION; + + #[test] + fn test_code_version_exists() { + assert!(CODE_VERSION > 0); + } +} diff --git a/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs b/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs --- a/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs @@ -23,7 +23,10 @@ }; debug!("Sending one time keys to Identity service"); - let result = identity_client.upload_one_time_keys(upload_request).await; + identity_client + .upload_one_time_keys(upload_request) + .await + .map_err(handle_grpc_error)?; Ok(true) } diff --git a/lib/facts/version.js b/lib/facts/version.js --- a/lib/facts/version.js +++ b/lib/facts/version.js @@ -2,4 +2,7 @@ // This file defines the version of both web and keyserver. Today they are // deployed together, so their version are sourced from the same place. + +// This version number should not be changed without also changing CODE_VERSION +// in keyserver/addons/rust-node-addon/src/identity_client/mod.rs export const webAndKeyserverCodeVersion = 33; diff --git a/services/comm-services-lib/src/auth/service.rs b/services/comm-services-lib/src/auth/service.rs --- a/services/comm-services-lib/src/auth/service.rs +++ b/services/comm-services-lib/src/auth/service.rs @@ -13,6 +13,12 @@ const AWSCURRENT: &str = "AWSCURRENT"; const AWSPREVIOUS: &str = "AWSPREVIOUS"; +// Identity service gRPC clients require a code version and device type. +// We can supply some placeholder values for services for the time being, since +// this metadata is only relevant for devices. +const PLACEHOLDER_CODE_VERSION: u64 = 0; +const DEVICE_TYPE: &str = "service"; + #[derive( Debug, derive_more::Display, derive_more::Error, derive_more::From, )] @@ -81,6 +87,8 @@ user_id, device_id, access_token, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), ) .await .map_err(AuthServiceError::from) diff --git a/services/commtest/tests/grpc_client_test.rs b/services/commtest/tests/grpc_client_test.rs --- a/services/commtest/tests/grpc_client_test.rs +++ b/services/commtest/tests/grpc_client_test.rs @@ -4,12 +4,16 @@ async fn verify_access_token() { use grpc_clients::identity::unauthenticated::client::verify_user_access_token; let device_info = create_device(None).await; + let code_version = 100; + let device_type = "android"; let token_valid = verify_user_access_token( "http://127.0.0.1:50054", &device_info.user_id, &device_info.device_id, &device_info.access_token, + code_version, + device_type.to_string(), ) .await .expect("Failed to call identity's verify_user_access_token endpoint"); @@ -22,6 +26,8 @@ &device_info.user_id, &device_info.device_id, "garbage", + code_version, + device_type.to_string(), ) .await .expect("Failed to call identity's verify_user_access_token endpoint"); diff --git a/services/tunnelbroker/src/identity/mod.rs b/services/tunnelbroker/src/identity/mod.rs --- a/services/tunnelbroker/src/identity/mod.rs +++ b/services/tunnelbroker/src/identity/mod.rs @@ -7,14 +7,24 @@ use crate::config::CONFIG; use crate::error::Error; +// Identity service gRPC clients require a code version and device type. +// We can supply some placeholder values for services for the time being, since +// this metadata is only relevant for devices. +const PLACEHOLDER_CODE_VERSION: u64 = 0; +const DEVICE_TYPE: &str = "service"; + /// Returns true if access token is valid pub async fn verify_user_access_token( user_id: &str, device_id: &str, access_token: &str, ) -> Result { - let mut grpc_client = - get_unauthenticated_client(&CONFIG.identity_endpoint).await?; + let mut grpc_client = get_unauthenticated_client( + &CONFIG.identity_endpoint, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), + ) + .await?; let message = VerifyUserAccessTokenRequest { user_id: user_id.to_string(), signing_public_key: device_id.to_string(), diff --git a/shared/grpc_clients/src/identity/authenticated.rs b/shared/grpc_clients/src/identity/authenticated.rs --- a/shared/grpc_clients/src/identity/authenticated.rs +++ b/shared/grpc_clients/src/identity/authenticated.rs @@ -7,7 +7,8 @@ Request, Status, }; -use crate::error::Error; +use crate::identity::shared::{ChainedInterceptor, ToMetadataValueAscii}; +use crate::{error::Error, identity::shared::CodeVersionLayer}; pub struct AuthLayer { user_id: String, @@ -15,10 +16,6 @@ access_token: String, } -trait ToMetadataValueAscii { - fn parse_to_ascii(&self) -> Result, Status>; -} - impl ToMetadataValueAscii for str { fn parse_to_ascii(&self) -> Result, Status> { self.parse().map_err(|e: InvalidMetadataValue| { @@ -40,21 +37,38 @@ Ok(request) } } + +pub type ChainedInterceptedAuthClient = AuthClient< + InterceptedService>, +>; + pub async fn get_auth_client( url: &str, user_id: String, device_id: String, access_token: String, -) -> Result>, Error> { + code_version: u64, + device_type: String, +) -> Result { use crate::get_grpc_service_channel; let channel = get_grpc_service_channel(url).await?; - let interceptor = AuthLayer { + let auth_interceptor = AuthLayer { user_id, device_id, access_token, }; - Ok(AuthClient::with_interceptor(channel, interceptor)) + let version_interceptor = CodeVersionLayer { + version: code_version, + device_type, + }; + + let chained = ChainedInterceptor { + first: auth_interceptor, + second: version_interceptor, + }; + + Ok(AuthClient::with_interceptor(channel, chained)) } diff --git a/shared/grpc_clients/src/identity/mod.rs b/shared/grpc_clients/src/identity/mod.rs --- a/shared/grpc_clients/src/identity/mod.rs +++ b/shared/grpc_clients/src/identity/mod.rs @@ -1,4 +1,5 @@ pub mod authenticated; +pub mod shared; pub mod unauthenticated; pub mod protos { diff --git a/shared/grpc_clients/src/identity/shared.rs b/shared/grpc_clients/src/identity/shared.rs new file mode 100644 --- /dev/null +++ b/shared/grpc_clients/src/identity/shared.rs @@ -0,0 +1,57 @@ +use tonic::{ + metadata::{errors::InvalidMetadataValue, Ascii, MetadataValue}, + service::Interceptor, + Request, Status, +}; + +pub struct CodeVersionLayer { + pub(crate) version: u64, + pub(crate) device_type: String, +} + +impl Interceptor for CodeVersionLayer { + fn call(&mut self, mut request: Request<()>) -> Result, Status> { + let metadata = request.metadata_mut(); + metadata.insert("code_version", self.version.parse_to_ascii()?); + metadata.insert("device_type", self.device_type.parse_to_ascii()?); + + Ok(request) + } +} + +pub trait ToMetadataValueAscii { + fn parse_to_ascii(&self) -> Result, Status>; +} + +impl ToMetadataValueAscii for u64 { + fn parse_to_ascii(&self) -> Result, Status> { + let ascii_string = self.to_string(); + + ascii_string.parse().map_err(|e: InvalidMetadataValue| { + Status::invalid_argument(format!( + "Non-Ascii character present in metadata value: {}", + e + )) + }) + } +} + +pub struct ChainedInterceptor +where + A: Interceptor + Send + Sync + 'static, + B: Interceptor + Send + Sync + 'static, +{ + pub(crate) first: A, + pub(crate) second: B, +} + +impl Interceptor for ChainedInterceptor +where + A: Interceptor + Send + Sync + 'static, + B: Interceptor + Send + Sync + 'static, +{ + fn call(&mut self, request: Request<()>) -> Result, Status> { + let request = self.first.call(request)?; + self.second.call(request) + } +} diff --git a/shared/grpc_clients/src/identity/unauthenticated/client.rs b/shared/grpc_clients/src/identity/unauthenticated/client.rs --- a/shared/grpc_clients/src/identity/unauthenticated/client.rs +++ b/shared/grpc_clients/src/identity/unauthenticated/client.rs @@ -12,8 +12,11 @@ user_id: &str, device_id: &str, access_token: &str, + code_version: u64, + device_type: String, ) -> Result { - let mut grpc_client = get_unauthenticated_client(identity_url).await?; + let mut grpc_client = + get_unauthenticated_client(identity_url, code_version, device_type).await?; let message = VerifyUserAccessTokenRequest { user_id: user_id.to_string(), @@ -23,5 +26,5 @@ let request = Request::new(message); let response = grpc_client.verify_user_access_token(request).await?; - return Ok(response.into_inner().token_valid); + Ok(response.into_inner().token_valid) } diff --git a/shared/grpc_clients/src/identity/unauthenticated/mod.rs b/shared/grpc_clients/src/identity/unauthenticated/mod.rs --- a/shared/grpc_clients/src/identity/unauthenticated/mod.rs +++ b/shared/grpc_clients/src/identity/unauthenticated/mod.rs @@ -1,13 +1,29 @@ pub mod client; +use tonic::codegen::InterceptedService; use tonic::transport::Channel; -use super::protos::client::identity_client_service_client::IdentityClientServiceClient; +use super::{ + protos::client::identity_client_service_client::IdentityClientServiceClient, + shared::CodeVersionLayer, +}; use crate::error::Error; pub async fn get_unauthenticated_client( url: &str, -) -> Result, Error> { + code_version: u64, + device_type: String, +) -> Result< + IdentityClientServiceClient>, + Error, +> { let channel = crate::get_grpc_service_channel(url).await?; - Ok(IdentityClientServiceClient::new(channel)) + let version_interceptor = CodeVersionLayer { + version: code_version, + device_type, + }; + Ok(IdentityClientServiceClient::with_interceptor( + channel, + version_interceptor, + )) } diff --git a/shared/grpc_clients/src/lib.rs b/shared/grpc_clients/src/lib.rs --- a/shared/grpc_clients/src/lib.rs +++ b/shared/grpc_clients/src/lib.rs @@ -10,7 +10,7 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig}; use tracing::info; -const CERT_PATHS: &'static [&'static str] = &[ +const CERT_PATHS: &[&str] = &[ // MacOS and newer Ubuntu "/etc/ssl/cert.pem", // Common CA cert paths