diff --git a/services/identity/src/config.rs b/services/identity/src/config.rs index f1e629397..1bc446c15 100644 --- a/services/identity/src/config.rs +++ b/services/identity/src/config.rs @@ -1,84 +1,110 @@ +use base64::{engine::general_purpose, DecodeError, Engine as _}; use once_cell::sync::Lazy; use std::{collections::HashSet, env, fmt, fs, io, path}; +use tracing::{error, info}; use crate::constants::{ - KEYSERVER_PUBLIC_KEY, LOCALSTACK_ENDPOINT, SECRETS_DIRECTORY, - SECRETS_SETUP_FILE, + KEYSERVER_PUBLIC_KEY, LOCALSTACK_ENDPOINT, OPAQUE_SERVER_SETUP, + SECRETS_DIRECTORY, SECRETS_SETUP_FILE, }; pub static CONFIG: Lazy = Lazy::new(|| Config::load().expect("failed to load config")); pub(super) fn load_config() { Lazy::force(&CONFIG); } #[derive(Clone)] pub struct Config { pub localstack_endpoint: Option, // Opaque 2.0 server secrets pub server_setup: comm_opaque2::ServerSetup, // Reserved usernames pub reserved_usernames: HashSet, pub keyserver_public_key: Option, } impl Config { fn load() -> Result { let localstack_endpoint = env::var(LOCALSTACK_ENDPOINT).ok(); - let mut path = path::PathBuf::new(); - path.push(SECRETS_DIRECTORY); - path.push(SECRETS_SETUP_FILE); - let server_setup = get_server_setup_from_file(&path)?; + let mut path_buf = path::PathBuf::new(); + path_buf.push(SECRETS_DIRECTORY); + path_buf.push(SECRETS_SETUP_FILE); + + let server_setup = get_server_setup(path_buf.as_path())?; let reserved_usernames = get_reserved_usernames_set()?; let keyserver_public_key = env::var(KEYSERVER_PUBLIC_KEY).ok(); Ok(Self { localstack_endpoint, server_setup, reserved_usernames, keyserver_public_key, }) } } impl fmt::Debug for Config { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Config") .field("server_keypair", &"** redacted **") .field("keyserver_auth_token", &"** redacted **") .field("localstack_endpoint", &self.localstack_endpoint) .finish() } } #[derive(Debug, derive_more::Display, derive_more::From)] pub enum Error { #[display(...)] Opaque(comm_opaque2::ProtocolError), #[display(...)] Io(io::Error), #[display(...)] Env(env::VarError), #[display(...)] Json(serde_json::Error), + #[display(...)] + Decode(DecodeError), } -fn get_server_setup_from_file>( - path: &P, +fn get_server_setup( + path: &path::Path, ) -> Result, Error> { - let bytes = fs::read(path)?; - comm_opaque2::ServerSetup::deserialize(&bytes).map_err(Error::Opaque) + let encoded_server_setup = if let Ok(env_setup) = + env::var(OPAQUE_SERVER_SETUP) + { + info!( + "Using OPAQUE server setup from env var: {}", + OPAQUE_SERVER_SETUP + ); + env_setup + } else if let Ok(file_setup) = fs::read_to_string(path) { + info!("Using OPAQUE server setup from file: {}", path.display()); + file_setup + } else { + error!("Unable to locate OPAQUE server setup. Please run `keygen` command and run Identity service again."); + return Err(Error::Io(io::Error::new( + io::ErrorKind::NotFound, + "Missing server credentials", + ))); + }; + + let decoded_server_setup = + general_purpose::STANDARD_NO_PAD.decode(encoded_server_setup)?; + comm_opaque2::ServerSetup::deserialize(&decoded_server_setup) + .map_err(Error::Opaque) } fn get_reserved_usernames_set() -> Result, Error> { // All entries in `reserved_usernames.json` must be lowercase and must also be // included in `lib/utils/reserved-users.js`!! let contents = include_str!("../reserved_usernames.json"); let reserved_usernames: Vec = serde_json::from_str(contents)?; Ok(reserved_usernames.into_iter().collect()) } diff --git a/services/identity/src/constants.rs b/services/identity/src/constants.rs index f911806d6..1bc06819e 100644 --- a/services/identity/src/constants.rs +++ b/services/identity/src/constants.rs @@ -1,108 +1,112 @@ // Secrets pub const SECRETS_DIRECTORY: &str = "secrets"; pub const SECRETS_SETUP_FILE: &str = "server_setup.txt"; // DynamoDB // User table information, supporting opaque_ke 2.0 and X3DH information // Users can sign in either through username+password or Eth wallet. // // This structure should be aligned with the messages defined in // shared/protos/identity_client.proto // // Structure for a user should be: // { // userID: String, // opaqueRegistrationData: Option, // username: Option, // walletAddress: Option, // devices: HashMap // } // // A device is defined as: // { // deviceType: String, # client or keyserver // keyPayload: String, // keyPayloadSignature: String, // identityPreKey: String, // identityPreKeySignature: String, // identityOneTimeKeys: Vec, // notifPreKey: String, // notifPreKeySignature: String, // notifOneTimeKeys: Vec, // socialProof: Option // } // } // // Additional context: // "devices" uses the signing public identity key of the device as a key for the devices map // "keyPayload" is a JSON encoded string containing identity and notif keys (both signature and verification) // if "deviceType" == "keyserver", then the device will not have any notif key information pub const USERS_TABLE: &str = "identity-users"; pub const USERS_TABLE_PARTITION_KEY: &str = "userID"; 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_CONTENT_PREKEY_ATTRIBUTE_NAME: &str = "identityPreKey"; pub const USERS_TABLE_DEVICES_MAP_CONTENT_PREKEY_SIGNATURE_ATTRIBUTE_NAME: &str = "identityPreKeySignature"; pub const USERS_TABLE_DEVICES_MAP_CONTENT_ONETIME_KEYS_ATTRIBUTE_NAME: &str = "identityOneTimeKeys"; pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_ATTRIBUTE_NAME: &str = "preKey"; pub const USERS_TABLE_DEVICES_MAP_NOTIF_PREKEY_SIGNATURE_ATTRIBUTE_NAME: &str = "preKeySignature"; pub const USERS_TABLE_DEVICES_MAP_NOTIF_ONETIME_KEYS_ATTRIBUTE_NAME: &str = "notifOneTimeKeys"; pub const USERS_TABLE_WALLET_ADDRESS_ATTRIBUTE: &str = "walletAddress"; pub const USERS_TABLE_DEVICES_MAP_SOCIAL_PROOF_ATTRIBUTE_NAME: &str = "socialProof"; pub const USERS_TABLE_USERNAME_INDEX: &str = "username-index"; pub const USERS_TABLE_WALLET_ADDRESS_INDEX: &str = "walletAddress-index"; pub const ACCESS_TOKEN_TABLE: &str = "identity-tokens"; pub const ACCESS_TOKEN_TABLE_PARTITION_KEY: &str = "userID"; pub const ACCESS_TOKEN_SORT_KEY: &str = "signingPublicKey"; pub const ACCESS_TOKEN_TABLE_CREATED_ATTRIBUTE: &str = "created"; pub const ACCESS_TOKEN_TABLE_AUTH_TYPE_ATTRIBUTE: &str = "authType"; pub const ACCESS_TOKEN_TABLE_VALID_ATTRIBUTE: &str = "valid"; pub const ACCESS_TOKEN_TABLE_TOKEN_ATTRIBUTE: &str = "token"; pub const NONCE_TABLE: &str = "identity-nonces"; pub const NONCE_TABLE_PARTITION_KEY: &str = "nonce"; pub const NONCE_TABLE_CREATED_ATTRIBUTE: &str = "created"; // Usernames reserved because they exist in Ashoat's keyserver already pub const RESERVED_USERNAMES_TABLE: &str = "identity-reserved-usernames"; pub const RESERVED_USERNAMES_TABLE_PARTITION_KEY: &str = "username"; // Tokio pub const MPSC_CHANNEL_BUFFER_CAPACITY: usize = 1; pub const IDENTITY_SERVICE_SOCKET_ADDR: &str = "[::]:50054"; // Token pub const ACCESS_TOKEN_LENGTH: usize = 512; // Temporary config pub const AUTH_TOKEN: &str = "COMM_IDENTITY_SERVICE_AUTH_TOKEN"; pub const KEYSERVER_PUBLIC_KEY: &str = "KEYSERVER_PUBLIC_KEY"; // Nonce pub const NONCE_LENGTH: usize = 17; // LocalStack pub const LOCALSTACK_ENDPOINT: &str = "LOCALSTACK_ENDPOINT"; + +// OPAQUE Server Setup + +pub const OPAQUE_SERVER_SETUP: &str = "OPAQUE_SERVER_SETUP"; diff --git a/services/identity/src/keygen.rs b/services/identity/src/keygen.rs index f02a1822a..4ff27d65f 100644 --- a/services/identity/src/keygen.rs +++ b/services/identity/src/keygen.rs @@ -1,24 +1,27 @@ use crate::constants::SECRETS_SETUP_FILE; +use base64::{engine::general_purpose, Engine as _}; use std::{fs, io, path}; pub fn generate_and_persist_keypair(dir: &str) -> Result<(), io::Error> { let mut secrets_dir = path::PathBuf::new(); secrets_dir.push(dir); if !secrets_dir.exists() { println!("Creating secrets directory {:?}", secrets_dir); fs::create_dir(&secrets_dir)?; } // Opaque 2.0 setup let server_setup = comm_opaque2::server::generate_server_setup(); let mut path = secrets_dir.clone(); path.push(SECRETS_SETUP_FILE); if path.exists() { eprintln!("{:?} already exists, skipping", path); } else { println!("Writing setup file to {:?}", path); - fs::write(&path, server_setup.serialize())?; + let encoded_server_setup = + general_purpose::STANDARD_NO_PAD.encode(server_setup.serialize()); + fs::write(&path, encoded_server_setup)?; } Ok(()) } diff --git a/services/identity/src/main.rs b/services/identity/src/main.rs index 3803ebc98..2e7cdf3f5 100644 --- a/services/identity/src/main.rs +++ b/services/identity/src/main.rs @@ -1,87 +1,86 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use database::DatabaseClient; use moka::future::Cache; use tonic::transport::Server; -use tracing_subscriber::FmtSubscriber; mod client_service; mod config; pub mod constants; mod database; mod id; mod keygen; mod nonce; mod reserved_users; mod siwe; mod token; use config::load_config; use constants::{IDENTITY_SERVICE_SOCKET_ADDR, SECRETS_DIRECTORY}; use keygen::generate_and_persist_keypair; use tracing::{self, info, Level}; use tracing_subscriber::EnvFilter; use client_service::{ClientService, IdentityClientServiceServer}; #[derive(Parser)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] struct Cli { #[clap(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Runs the server Server, /// Generates and persists a keypair to use for PAKE registration and login Keygen { #[clap(short, long)] #[clap(default_value_t = String::from(SECRETS_DIRECTORY))] dir: String, }, /// Populates the `identity-users` table in DynamoDB from MySQL PopulateDB, } #[tokio::main] async fn main() -> Result<(), Box> { let filter = EnvFilter::builder() .with_default_directive(Level::INFO.into()) .with_env_var(EnvFilter::DEFAULT_ENV) .from_env_lossy(); let subscriber = tracing_subscriber::fmt().with_env_filter(filter).finish(); tracing::subscriber::set_global_default(subscriber)?; let cli = Cli::parse(); match &cli.command { Commands::Keygen { dir } => { generate_and_persist_keypair(dir)?; } Commands::Server => { load_config(); let addr = IDENTITY_SERVICE_SOCKET_ADDR.parse()?; let aws_config = aws_config::from_env().region("us-east-2").load().await; let database_client = DatabaseClient::new(&aws_config); let workflow_cache = Cache::builder() .time_to_live(Duration::from_secs(10)) .build(); let client_service = IdentityClientServiceServer::new( ClientService::new(database_client, workflow_cache), ); info!("Listening to gRPC traffic on {}", addr); Server::builder() .accept_http1(true) .add_service(tonic_web::enable(client_service)) .serve(addr) .await?; } Commands::PopulateDB => unimplemented!(), } Ok(()) }