diff --git a/Cargo.lock b/Cargo.lock index 28efbd69ef..61a6d6120e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2549,7 +2549,7 @@ dependencies = [ [[package]] name = "extension-storage" -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" dependencies = [ "bip39", "console_error_panic_hook", @@ -4557,7 +4557,7 @@ dependencies = [ [[package]] name = "mix-fetch-wasm" -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" dependencies = [ "async-trait", "futures", @@ -5230,10 +5230,9 @@ version = "1.1.15" dependencies = [ "async-trait", "base64 0.21.4", + "bs58 0.5.0", "cfg-if", "clap 4.4.7", - "dashmap", - "dirs 4.0.0", "futures", "gloo-timers", "http-body-util", @@ -5242,12 +5241,15 @@ dependencies = [ "hyper-util", "log", "nym-bandwidth-controller", + "nym-client-core-gateways-storage", + "nym-client-core-surb-storage", "nym-config", "nym-credential-storage", "nym-crypto", "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", + "nym-id", "nym-metrics", "nym-network-defaults", "nym-nonexhaustive-delayqueue", @@ -5257,12 +5259,10 @@ dependencies = [ "nym-topology", "nym-validator-client", "rand 0.7.3", - "reqwest", "serde", "serde_json", "sha2 0.10.8", "si-scale", - "sqlx", "tap", "tempfile", "thiserror", @@ -5279,9 +5279,43 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-client-core-gateways-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "cosmrs 0.15.0 (git+https://github.com/jstuczyn/cosmos-rust?branch=nym-temp/all-validator-features)", + "log", + "nym-crypto", + "nym-gateway-requests", + "serde", + "sqlx", + "thiserror", + "time", + "tokio", + "url", + "zeroize", +] + +[[package]] +name = "nym-client-core-surb-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "dashmap", + "log", + "nym-crypto", + "nym-sphinx", + "nym-task", + "sqlx", + "thiserror", + "time", + "tokio", +] + [[package]] name = "nym-client-wasm" -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" dependencies = [ "anyhow", "futures", @@ -5738,6 +5772,7 @@ dependencies = [ name = "nym-ip-packet-router" version = "0.1.0" dependencies = [ + "anyhow", "bincode", "bs58 0.5.0", "bytes", @@ -5750,6 +5785,7 @@ dependencies = [ "nym-config", "nym-crypto", "nym-exit-policy", + "nym-id", "nym-ip-packet-requests", "nym-network-defaults", "nym-network-requester", @@ -6085,7 +6121,7 @@ dependencies = [ [[package]] name = "nym-node-tester-wasm" -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" dependencies = [ "futures", "js-sys", @@ -9096,9 +9132,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snafu" @@ -10846,6 +10882,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "thiserror", + "time", "tsify", "url", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 6e322cc32a..f7a7982019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ members = [ "common/bandwidth-controller", "common/bin-common", "common/client-core", + "common/client-core/surb-storage", + "common/client-core/gateways-storage", "common/client-libs/gateway-client", "common/client-libs/mixnet-client", "common/client-libs/validator-client", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index a85b73e5a4..2e49dd8261 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -38,7 +38,7 @@ zeroize = { workspace = true } ## internal nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } -nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "cli"] } +nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] } nym-config = { path = "../../common/config" } nym-credential-storage = { path = "../../common/credential-storage" } nym-credentials = { path = "../../common/credentials" } diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index 7fa28c8ab4..f699d1e027 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -4,7 +4,7 @@ use crate::client::config::persistence::ClientPaths; use crate::client::config::template::CONFIG_TEMPLATE; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::cli_helpers::client_init::ClientConfig; +use nym_client_core::cli_helpers::CliClientConfig; use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_config::defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; use nym_config::{ @@ -19,11 +19,12 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; pub use nym_client_core::config::Config as BaseClientConfig; -pub use nym_client_core::config::{DebugConfig, GatewayEndpointConfig}; +pub use nym_client_core::config::DebugConfig; pub mod old_config_v1_1_13; pub mod old_config_v1_1_20; pub mod old_config_v1_1_20_2; +pub mod old_config_v1_1_33; mod persistence; mod template; @@ -74,7 +75,7 @@ impl NymConfigTemplate for Config { } } -impl ClientConfig for Config { +impl CliClientConfig for Config { fn common_paths(&self) -> &CommonClientPaths { &self.storage_paths.common_paths } diff --git a/clients/native/src/client/config/old_config_v1_1_20.rs b/clients/native/src/client/config/old_config_v1_1_20.rs index 8c2452d3e1..77ca8f5ca1 100644 --- a/clients/native/src/client/config/old_config_v1_1_20.rs +++ b/clients/native/src/client/config/old_config_v1_1_20.rs @@ -5,8 +5,8 @@ use crate::client::config::old_config_v1_1_20_2::{ ClientPathsV1_1_20_2, ConfigV1_1_20_2, SocketTypeV1_1_20_2, SocketV1_1_20_2, }; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::config::disk_persistence::keys_paths::ClientKeysPaths; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; +use nym_client_core::config::disk_persistence::old_v1_1_33::ClientKeysPathsV1_1_33; use nym_client_core::config::old_config_v1_1_20::ConfigV1_1_20 as BaseConfigV1_1_20; use nym_client_core::config::old_config_v1_1_20_2::{ ClientV1_1_20_2, ConfigV1_1_20_2 as BaseConfigV1_1_20_2, @@ -60,7 +60,7 @@ impl From for ConfigV1_1_20_2 { socket: value.socket.into(), storage_paths: ClientPathsV1_1_20_2 { common_paths: CommonClientPathsV1_1_20_2 { - keys: ClientKeysPaths { + keys: ClientKeysPathsV1_1_33 { private_identity_key_file: value.base.client.private_identity_key_file, public_identity_key_file: value.base.client.public_identity_key_file, private_encryption_key_file: value.base.client.private_encryption_key_file, diff --git a/clients/native/src/client/config/old_config_v1_1_20_2.rs b/clients/native/src/client/config/old_config_v1_1_20_2.rs index 8e50fe0047..90e161827a 100644 --- a/clients/native/src/client/config/old_config_v1_1_20_2.rs +++ b/clients/native/src/client/config/old_config_v1_1_20_2.rs @@ -1,18 +1,15 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{ - client::config::{ - default_config_filepath, persistence::ClientPaths, Config, Socket, SocketType, - }, - error::ClientError, +use crate::client::config::old_config_v1_1_33::{ + ClientPathsV1_1_33, ConfigV1_1_33, SocketTypeV1_1_33, SocketV1_1_33, }; - +use crate::{client::config::default_config_filepath, error::ClientError}; use nym_bin_common::logging::LoggingSettings; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; use nym_client_core::config::old_config_v1_1_20_2::ConfigV1_1_20_2 as BaseConfigV1_1_20_2; use nym_client_core::config::old_config_v1_1_30::ConfigV1_1_30 as BaseConfigV1_1_30; -use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; use nym_config::read_config_from_toml_file; use nym_network_defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; use serde::{Deserialize, Serialize}; @@ -49,12 +46,12 @@ impl ConfigV1_1_20_2 { // in this upgrade, gateway endpoint configuration was moved out of the config file, // so its returned to be stored elsewhere. - pub fn upgrade(self) -> Result<(Config, GatewayEndpointConfig), ClientError> { + pub fn upgrade(self) -> Result<(ConfigV1_1_33, OldGatewayEndpointConfigV1_1_33), ClientError> { let gateway_details = self.base.client.gateway_endpoint.clone().into(); - let config = Config { + let config = ConfigV1_1_33 { base: BaseConfigV1_1_30::from(self.base).into(), socket: self.socket.into(), - storage_paths: ClientPaths { + storage_paths: ClientPathsV1_1_33 { common_paths: self.storage_paths.common_paths.upgrade_default()?, }, logging: self.logging, @@ -71,11 +68,11 @@ pub enum SocketTypeV1_1_20_2 { None, } -impl From for SocketType { +impl From for SocketTypeV1_1_33 { fn from(value: SocketTypeV1_1_20_2) -> Self { match value { - SocketTypeV1_1_20_2::WebSocket => SocketType::WebSocket, - SocketTypeV1_1_20_2::None => SocketType::None, + SocketTypeV1_1_20_2::WebSocket => SocketTypeV1_1_33::WebSocket, + SocketTypeV1_1_20_2::None => SocketTypeV1_1_33::None, } } } @@ -88,9 +85,9 @@ pub struct SocketV1_1_20_2 { pub listening_port: u16, } -impl From for Socket { +impl From for SocketV1_1_33 { fn from(value: SocketV1_1_20_2) -> Self { - Socket { + SocketV1_1_33 { socket_type: value.socket_type.into(), host: value.host, listening_port: value.listening_port, diff --git a/clients/native/src/client/config/old_config_v1_1_33.rs b/clients/native/src/client/config/old_config_v1_1_33.rs new file mode 100644 index 0000000000..02ed7218d1 --- /dev/null +++ b/clients/native/src/client/config/old_config_v1_1_33.rs @@ -0,0 +1,99 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::config::persistence::ClientPaths; +use crate::client::config::{default_config_filepath, Config, Socket, SocketType}; +use crate::error::ClientError; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; +use nym_client_core::config::old_config_v1_1_33::ConfigV1_1_33 as BaseConfigV1_1_33; +use nym_config::read_config_from_toml_file; +use nym_network_defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; +use serde::{Deserialize, Serialize}; +use std::io; +use std::net::{IpAddr, Ipv4Addr}; +use std::path::Path; + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone)] +pub struct ClientPathsV1_1_33 { + #[serde(flatten)] + pub common_paths: CommonClientPathsV1_1_33, +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct ConfigV1_1_33 { + #[serde(flatten)] + pub base: BaseConfigV1_1_33, + + pub socket: SocketV1_1_33, + + // \/ CHANGED + pub storage_paths: ClientPathsV1_1_33, + // /\ CHANGED + pub logging: LoggingSettings, +} + +impl ConfigV1_1_33 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + base: self.base.into(), + socket: self.socket.into(), + storage_paths: ClientPaths { + common_paths: self.storage_paths.common_paths.upgrade_default()?, + }, + logging: self.logging, + }) + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone, Copy)] +#[serde(deny_unknown_fields)] +pub enum SocketTypeV1_1_33 { + WebSocket, + None, +} + +impl From for SocketType { + fn from(value: SocketTypeV1_1_33) -> Self { + match value { + SocketTypeV1_1_33::WebSocket => SocketType::WebSocket, + SocketTypeV1_1_33::None => SocketType::None, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct SocketV1_1_33 { + pub socket_type: SocketTypeV1_1_33, + pub host: IpAddr, + pub listening_port: u16, +} + +impl From for Socket { + fn from(value: SocketV1_1_33) -> Self { + Socket { + socket_type: value.socket_type.into(), + host: value.host, + listening_port: value.listening_port, + } + } +} + +impl Default for SocketV1_1_33 { + fn default() -> Self { + SocketV1_1_33 { + socket_type: SocketTypeV1_1_33::WebSocket, + host: IpAddr::V4(Ipv4Addr::LOCALHOST), + listening_port: DEFAULT_WEBSOCKET_LISTENING_PORT, + } + } +} diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index 413b61790d..965fc07991 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -50,10 +50,6 @@ keys.private_encryption_key_file = '{{ storage_paths.keys.private_encryption_key # Path to file containing public encryption key. keys.public_encryption_key_file = '{{ storage_paths.keys.public_encryption_key_file }}' -# A gateway specific, optional, base58 stringified shared key used for -# communication with particular gateway. -keys.gateway_shared_key_file = '{{ storage_paths.keys.gateway_shared_key_file }}' - # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' @@ -64,9 +60,9 @@ credentials_database = '{{ storage_paths.credentials_database }}' # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. reply_surb_database = '{{ storage_paths.reply_surb_database }}' -# Path to the file containing information about gateway used by this client, -# i.e. details such as its public key, owner address or the network information. -gateway_details = '{{ storage_paths.gateway_details }}' +# Path to the file containing information about gateways used by this client, +# i.e. details such as their public keys, owner addresses or the network information. +gateway_registrations = '{{ storage_paths.gateway_registrations }}' ##### socket config options ##### diff --git a/clients/native/src/commands/add_gateway.rs b/clients/native/src/commands/add_gateway.rs new file mode 100644 index 0000000000..254839af04 --- /dev/null +++ b/clients/native/src/commands/add_gateway.rs @@ -0,0 +1,30 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliNativeClient; +use crate::error::ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_add_gateway::{add_gateway, CommonClientAddGatewayArgs}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientAddGatewayArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientAddGatewayArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { + let output = args.output; + let res = add_gateway::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/clients/native/src/commands/import_credential.rs b/clients/native/src/commands/import_credential.rs index 8554f74ee7..c7327797d0 100644 --- a/clients/native/src/commands/import_credential.rs +++ b/clients/native/src/commands/import_credential.rs @@ -1,54 +1,12 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::try_load_current_config; +use crate::commands::CliNativeClient; use crate::error::ClientError; -use clap::ArgGroup; +use nym_client_core::cli_helpers::client_import_credential::{ + import_credential, CommonClientImportCredentialArgs, +}; -use nym_id::import_credential; -use std::fs; -use std::path::PathBuf; - -fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { - bs58::decode(raw).into_vec() -} - -#[derive(clap::Args)] -#[clap(group(ArgGroup::new("cred_data").required(true)))] -pub(crate) struct Args { - /// Id of client that is going to import the credential - #[clap(long)] - pub id: String, - - /// Explicitly provide the encoded credential data (as base58) - #[clap(long, group = "cred_data", value_parser = parse_encoded_credential_data)] - pub(crate) credential_data: Option>, - - /// Specifies the path to file containing binary credential data - #[clap(long, group = "cred_data")] - pub(crate) credential_path: Option, - - // currently hidden as there exists only a single serialization standard - #[clap(long, hide = true)] - pub(crate) version: Option, -} - -pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { - let config = try_load_current_config(&args.id)?; - - let credentials_store = nym_credential_storage::initialise_persistent_storage( - &config.storage_paths.common_paths.credentials_database, - ) - .await; - - let raw_credential = match args.credential_data { - Some(data) => data, - None => { - // SAFETY: one of those arguments must have been set - fs::read(args.credential_path.unwrap())? - } - }; - - import_credential(credentials_store, raw_credential, args.version).await?; - Ok(()) +pub(crate) async fn execute(args: CommonClientImportCredentialArgs) -> Result<(), ClientError> { + import_credential::(args).await } diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index def0072d26..b71b168b03 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -4,7 +4,7 @@ use crate::client::config::{ default_config_directory, default_config_filepath, default_data_directory, }; -use crate::commands::try_upgrade_config; +use crate::commands::CliNativeClient; use crate::{ client::config::Config, commands::{override_config, OverrideConfig}, @@ -21,17 +21,8 @@ use std::fs; use std::net::IpAddr; use std::path::PathBuf; -struct NativeClientInit; - -impl InitialisableClient for NativeClientInit { - const NAME: &'static str = "native"; - type Error = ClientError; +impl InitialisableClient for CliNativeClient { type InitArgs = Init; - type Config = Config; - - fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { - try_upgrade_config(id) - } fn initialise_storage_paths(id: &str) -> Result<(), Self::Error> { fs::create_dir_all(default_data_directory(id))?; @@ -124,7 +115,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), ClientError> { eprintln!("Initialising client..."); let output = args.output; - let res = initialise_client::(args).await?; + let res = initialise_client::(args).await?; let init_results = InitResults::new(res); println!("{}", output.format(&init_results)); diff --git a/clients/native/src/commands/list_gateways.rs b/clients/native/src/commands/list_gateways.rs new file mode 100644 index 0000000000..63f41e7bfb --- /dev/null +++ b/clients/native/src/commands/list_gateways.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliNativeClient; +use crate::error::ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_list_gateways::{ + list_gateways, CommonClientListGatewaysArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientListGatewaysArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientListGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { + let output = args.output; + let res = list_gateways::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index ff1b8087fd..3f8e86e94f 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -4,6 +4,7 @@ use crate::client::config::old_config_v1_1_13::OldConfigV1_1_13; use crate::client::config::old_config_v1_1_20::ConfigV1_1_20; use crate::client::config::old_config_v1_1_20_2::ConfigV1_1_20_2; +use crate::client::config::old_config_v1_1_33::ConfigV1_1_33; use crate::client::config::{BaseClientConfig, Config}; use crate::error::ClientError; use clap::CommandFactory; @@ -11,21 +12,37 @@ use clap::{Parser, Subcommand}; use log::{error, info}; use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_client_core::client::base_client::storage::gateway_details::{ - OnDiskGatewayDetails, PersistedGatewayDetails, -}; -use nym_client_core::client::key_manager::persistence::OnDiskKeys; -use nym_client_core::config::GatewayEndpointConfig; -use nym_client_core::error::ClientCoreError; +use nym_client_core::cli_helpers::client_import_credential::CommonClientImportCredentialArgs; +use nym_client_core::cli_helpers::CliClient; +use nym_client_core::client::base_client::storage::migration_helpers::v1_1_33; use nym_config::OptionalSet; use std::error::Error; use std::net::IpAddr; use std::sync::OnceLock; +mod add_gateway; pub(crate) mod build_info; pub(crate) mod import_credential; pub(crate) mod init; +mod list_gateways; pub(crate) mod run; +mod switch_gateway; + +pub(crate) struct CliNativeClient; + +impl CliClient for CliNativeClient { + const NAME: &'static str = "native"; + type Error = ClientError; + type Config = Config; + + async fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { + try_upgrade_config(id).await + } + + async fn try_load_current_config(id: &str) -> Result { + try_load_current_config(id).await + } +} fn pretty_build_info_static() -> &'static str { static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); @@ -56,7 +73,16 @@ pub(crate) enum Commands { Run(run::Run), /// Import a pre-generated credential - ImportCredential(import_credential::Args), + ImportCredential(CommonClientImportCredentialArgs), + + /// List all registered with gateways + ListGateways(list_gateways::Args), + + /// Add new gateway to this client + AddGateway(add_gateway::Args), + + /// Change the currently active gateway. Note that you must have already registered with the new gateway! + SwitchGateway(switch_gateway::Args), /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -87,6 +113,9 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box init::execute(m).await?, Commands::Run(m) => run::execute(m).await?, Commands::ImportCredential(m) => import_credential::execute(m).await?, + Commands::ListGateways(args) => list_gateways::execute(args).await?, + Commands::AddGateway(args) => add_gateway::execute(args).await?, + Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), @@ -122,29 +151,7 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { ) } -fn persist_gateway_details( - config: &Config, - details: GatewayEndpointConfig, -) -> Result<(), ClientError> { - let details_store = - OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); - let keys_store = OnDiskKeys::new(config.storage_paths.common_paths.keys.clone()); - let shared_keys = keys_store.ephemeral_load_gateway_keys().map_err(|source| { - ClientError::ClientCoreError(ClientCoreError::KeyStoreError { - source: Box::new(source), - }) - })?; - let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; - details_store - .store_to_disk(&persisted_details) - .map_err(|source| { - ClientError::ClientCoreError(ClientCoreError::GatewayDetailsStoreError { - source: Box::new(source), - }) - }) -} - -fn try_upgrade_v1_1_13_config(id: &str) -> Result { +async fn try_upgrade_v1_1_13_config(id: &str) -> Result { use nym_config::legacy_helpers::nym_config::MigrationNymConfig; // explicitly load it as v1.1.13 (which is incompatible with the next step, i.e. 1.1.19) @@ -158,14 +165,22 @@ fn try_upgrade_v1_1_13_config(id: &str) -> Result { let updated_step1: ConfigV1_1_20 = old_config.into(); let updated_step2: ConfigV1_1_20_2 = updated_step1.into(); - let (updated, gateway_config) = updated_step2.upgrade()?; - persist_gateway_details(&updated, gateway_config)?; + let (updated_step3, gateway_config) = updated_step2.upgrade()?; + let old_paths = updated_step3.storage_paths.clone(); + let updated = updated_step3.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_20_config(id: &str) -> Result { +async fn try_upgrade_v1_1_20_config(id: &str) -> Result { use nym_config::legacy_helpers::nym_config::MigrationNymConfig; // explicitly load it as v1.1.20 (which is incompatible with the current one, i.e. +1.1.21) @@ -178,14 +193,21 @@ fn try_upgrade_v1_1_20_config(id: &str) -> Result { info!("It is going to get updated to the current specification."); let updated_step1: ConfigV1_1_20_2 = old_config.into(); - let (updated, gateway_config) = updated_step1.upgrade()?; - persist_gateway_details(&updated, gateway_config)?; + let (updated_step2, gateway_config) = updated_step1.upgrade()?; + let old_paths = updated_step2.storage_paths.clone(); + let updated = updated_step2.try_upgrade()?; + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { +async fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { // explicitly load it as v1.1.20_2 (which is incompatible with the current one, i.e. +1.1.21) let Ok(old_config) = ConfigV1_1_20_2::read_from_default_path(id) else { // if we failed to load it, there might have been nothing to upgrade @@ -195,28 +217,62 @@ fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { info!("It seems the client is using <= v1.1.20_2 config template."); info!("It is going to get updated to the current specification."); - let (updated, gateway_config) = old_config.upgrade()?; - persist_gateway_details(&updated, gateway_config)?; + let (updated_step1, gateway_config) = old_config.upgrade()?; + let old_paths = updated_step1.storage_paths.clone(); + let updated = updated_step1.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; + updated.save_to_default_location()?; + Ok(true) +} + +async fn try_upgrade_v1_1_33_config(id: &str) -> Result { + // explicitly load it as v1.1.33 (which is incompatible with the current one, i.e. +1.1.34) + let Ok(old_config) = ConfigV1_1_33::read_from_default_path(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.33 config template."); + info!("It is going to get updated to the current specification."); + + let old_paths = old_config.storage_paths.clone(); + let updated = old_config.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + None, + ) + .await?; updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_config(id: &str) -> Result<(), ClientError> { - if try_upgrade_v1_1_13_config(id)? { +async fn try_upgrade_config(id: &str) -> Result<(), ClientError> { + if try_upgrade_v1_1_13_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_20_config(id)? { + if try_upgrade_v1_1_20_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_20_2_config(id)? { + if try_upgrade_v1_1_20_2_config(id).await? { + return Ok(()); + } + if try_upgrade_v1_1_33_config(id).await? { return Ok(()); } Ok(()) } -fn try_load_current_config(id: &str) -> Result { +async fn try_load_current_config(id: &str) -> Result { // try to load the config as is if let Ok(cfg) = Config::read_from_default_path(id) { return if !cfg.validate() { @@ -227,7 +283,7 @@ fn try_load_current_config(id: &str) -> Result { } // we couldn't load it - try upgrading it from older revisions - try_upgrade_config(id)?; + try_upgrade_config(id).await?; let config = match Config::read_from_default_path(id) { Ok(cfg) => cfg, diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index b836fe6d20..2ab3fc8489 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -69,7 +69,7 @@ fn version_check(cfg: &Config) -> bool { pub(crate) async fn execute(args: Run) -> Result<(), Box> { eprintln!("Starting client {}...", args.common_args.id); - let mut config = try_load_current_config(&args.common_args.id)?; + let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); if !version_check(&config) { diff --git a/clients/native/src/commands/switch_gateway.rs b/clients/native/src/commands/switch_gateway.rs new file mode 100644 index 0000000000..f7dbcfa65d --- /dev/null +++ b/clients/native/src/commands/switch_gateway.rs @@ -0,0 +1,24 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliNativeClient; +use crate::error::ClientError; +use nym_client_core::cli_helpers::client_switch_gateway::{ + switch_gateway, CommonClientSwitchGatewaysArgs, +}; + +#[derive(clap::Args, Clone, Debug)] +pub struct Args { + #[command(flatten)] + common_args: CommonClientSwitchGatewaysArgs, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientSwitchGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), ClientError> { + switch_gateway::(args).await +} diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 2665f6355c..66fbc64e5a 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -23,7 +23,7 @@ zeroize = { workspace = true } # internal nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } -nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "cli"] } +nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] } nym-config = { path = "../../common/config" } nym-credentials = { path = "../../common/credentials" } nym-crypto = { path = "../../common/crypto" } diff --git a/clients/socks5/src/commands/add_gateway.rs b/clients/socks5/src/commands/add_gateway.rs new file mode 100644 index 0000000000..fb7b645619 --- /dev/null +++ b/clients/socks5/src/commands/add_gateway.rs @@ -0,0 +1,30 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliSocks5Client; +use crate::error::Socks5ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_add_gateway::{add_gateway, CommonClientAddGatewayArgs}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientAddGatewayArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientAddGatewayArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { + let output = args.output; + let res = add_gateway::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/clients/socks5/src/commands/import_credential.rs b/clients/socks5/src/commands/import_credential.rs index 8a13e799d3..91ea0bf552 100644 --- a/clients/socks5/src/commands/import_credential.rs +++ b/clients/socks5/src/commands/import_credential.rs @@ -1,54 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::try_load_current_config; +use crate::commands::CliSocks5Client; use crate::error::Socks5ClientError; -use clap::ArgGroup; +use nym_client_core::cli_helpers::client_import_credential::{ + import_credential, CommonClientImportCredentialArgs, +}; -use nym_id::import_credential; -use std::fs; -use std::path::PathBuf; - -fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { - bs58::decode(raw).into_vec() -} - -#[derive(clap::Args)] -#[clap(group(ArgGroup::new("cred_data").required(true)))] -pub(crate) struct Args { - /// Id of client that is going to import the credential - #[clap(long)] - pub id: String, - - /// Explicitly provide the encoded credential data (as base58) - #[clap(long, group = "cred_data", value_parser = parse_encoded_credential_data)] - pub(crate) credential_data: Option>, - - /// Specifies the path to file containing binary credential data - #[clap(long, group = "cred_data")] - pub(crate) credential_path: Option, - - // currently hidden as there exists only a single serialization standard - #[clap(long, hide = true)] - pub(crate) version: Option, -} - -pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { - let config = try_load_current_config(&args.id)?; - - let credentials_store = nym_credential_storage::initialise_persistent_storage( - &config.storage_paths.common_paths.credentials_database, - ) - .await; - - let raw_credential = match args.credential_data { - Some(data) => data, - None => { - // SAFETY: one of those arguments must have been set - fs::read(args.credential_path.unwrap())? - } - }; - - import_credential(credentials_store, raw_credential, args.version).await?; - Ok(()) +pub(crate) async fn execute( + args: CommonClientImportCredentialArgs, +) -> Result<(), Socks5ClientError> { + import_credential::(args).await } diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index ce13a1b7cc..83c26e7a3c 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::commands::try_upgrade_config; +use crate::commands::CliSocks5Client; use crate::config::{ default_config_directory, default_config_filepath, default_data_directory, Config, }; @@ -21,17 +21,8 @@ use std::fs; use std::net::{IpAddr, SocketAddr}; use std::path::PathBuf; -struct Socks5ClientInit; - -impl InitialisableClient for Socks5ClientInit { - const NAME: &'static str = "socks5"; - type Error = Socks5ClientError; +impl InitialisableClient for CliSocks5Client { type InitArgs = Init; - type Config = Config; - - fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { - try_upgrade_config(id) - } fn initialise_storage_paths(id: &str) -> Result<(), Self::Error> { fs::create_dir_all(default_data_directory(id))?; @@ -118,7 +109,7 @@ impl InitResults { Self { client_address: res.init_results.address.to_string(), client_core: res.init_results, - socks5_listening_address: res.config.core.socks5.bind_adddress, + socks5_listening_address: res.config.core.socks5.bind_address, } } } @@ -139,7 +130,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), Socks5ClientError> { eprintln!("Initialising client..."); let output = args.output; - let res = initialise_client::(args).await?; + let res = initialise_client::(args).await?; let init_results = InitResults::new(res); println!("{}", output.format(&init_results)); diff --git a/clients/socks5/src/commands/list_gateways.rs b/clients/socks5/src/commands/list_gateways.rs new file mode 100644 index 0000000000..e33921c05f --- /dev/null +++ b/clients/socks5/src/commands/list_gateways.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliSocks5Client; +use crate::error::Socks5ClientError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_list_gateways::{ + list_gateways, CommonClientListGatewaysArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientListGatewaysArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientListGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { + let output = args.output; + let res = list_gateways::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index da3166fd7e..961c6551e5 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -5,30 +5,48 @@ use crate::config::old_config_v1_1_13::OldConfigV1_1_13; use crate::config::old_config_v1_1_20::ConfigV1_1_20; use crate::config::old_config_v1_1_20_2::ConfigV1_1_20_2; use crate::config::old_config_v1_1_30::ConfigV1_1_30; -use crate::config::{BaseClientConfig, Config, SocksClientPaths}; +use crate::config::old_config_v1_1_33::ConfigV1_1_33; +use crate::config::{BaseClientConfig, Config}; use crate::error::Socks5ClientError; use clap::CommandFactory; use clap::{Parser, Subcommand}; use log::{error, info}; use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_client_core::client::base_client::storage::gateway_details::{ - OnDiskGatewayDetails, PersistedGatewayDetails, -}; -use nym_client_core::client::key_manager::persistence::OnDiskKeys; +use nym_client_core::cli_helpers::client_import_credential::CommonClientImportCredentialArgs; +use nym_client_core::cli_helpers::CliClient; +use nym_client_core::client::base_client::storage::migration_helpers::v1_1_33; use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup; -use nym_client_core::config::{GatewayEndpointConfig, GroupBy, TopologyStructure}; -use nym_client_core::error::ClientCoreError; +use nym_client_core::config::{GroupBy, TopologyStructure}; use nym_config::OptionalSet; use nym_sphinx::params::{PacketSize, PacketType}; use std::error::Error; use std::net::IpAddr; use std::sync::OnceLock; +mod add_gateway; pub(crate) mod build_info; mod import_credential; pub mod init; +mod list_gateways; pub(crate) mod run; +mod switch_gateway; + +pub(crate) struct CliSocks5Client; + +impl CliClient for CliSocks5Client { + const NAME: &'static str = "socks5"; + type Error = Socks5ClientError; + type Config = Config; + + async fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { + try_upgrade_config(id).await + } + + async fn try_load_current_config(id: &str) -> Result { + try_load_current_config(id).await + } +} fn pretty_build_info_static() -> &'static str { static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); @@ -59,7 +77,16 @@ pub(crate) enum Commands { Run(run::Run), /// Import a pre-generated credential - ImportCredential(import_credential::Args), + ImportCredential(CommonClientImportCredentialArgs), + + /// List all registered with gateways + ListGateways(list_gateways::Args), + + /// Add new gateway to this client + AddGateway(add_gateway::Args), + + /// Change the currently active gateway. Note that you must have already registered with the new gateway! + SwitchGateway(switch_gateway::Args), /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -93,6 +120,9 @@ pub(crate) async fn execute(args: Cli) -> Result<(), Box init::execute(m).await?, Commands::Run(m) => run::execute(m).await?, Commands::ImportCredential(m) => import_credential::execute(m).await?, + Commands::ListGateways(args) => list_gateways::execute(args).await?, + Commands::AddGateway(args) => add_gateway::execute(args).await?, + Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), @@ -168,28 +198,7 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { ) } -fn persist_gateway_details( - storage_paths: &SocksClientPaths, - details: GatewayEndpointConfig, -) -> Result<(), Socks5ClientError> { - let details_store = OnDiskGatewayDetails::new(&storage_paths.common_paths.gateway_details); - let keys_store = OnDiskKeys::new(storage_paths.common_paths.keys.clone()); - let shared_keys = keys_store.ephemeral_load_gateway_keys().map_err(|source| { - Socks5ClientError::ClientCoreError(ClientCoreError::KeyStoreError { - source: Box::new(source), - }) - })?; - let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; - details_store - .store_to_disk(&persisted_details) - .map_err(|source| { - Socks5ClientError::ClientCoreError(ClientCoreError::GatewayDetailsStoreError { - source: Box::new(source), - }) - }) -} - -fn try_upgrade_v1_1_13_config(id: &str) -> Result { +async fn try_upgrade_v1_1_13_config(id: &str) -> Result { use nym_config::legacy_helpers::nym_config::MigrationNymConfig; // explicitly load it as v1.1.13 (which is incompatible with the next step, i.e. 1.1.19) @@ -204,14 +213,23 @@ fn try_upgrade_v1_1_13_config(id: &str) -> Result { let updated_step1: ConfigV1_1_20 = old_config.into(); let updated_step2: ConfigV1_1_20_2 = updated_step1.into(); let (updated_step3, gateway_config) = updated_step2.upgrade()?; - persist_gateway_details(&updated_step3.storage_paths, gateway_config)?; + let old_paths = updated_step3.storage_paths.clone(); + + let updated_step4: ConfigV1_1_33 = updated_step3.into(); + let updated = updated_step4.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; - let updated: Config = updated_step3.into(); updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_20_config(id: &str) -> Result { +async fn try_upgrade_v1_1_20_config(id: &str) -> Result { use nym_config::legacy_helpers::nym_config::MigrationNymConfig; // explicitly load it as v1.1.20 (which is incompatible with the current one, i.e. +1.1.21) @@ -225,14 +243,23 @@ fn try_upgrade_v1_1_20_config(id: &str) -> Result { let updated_step1: ConfigV1_1_20_2 = old_config.into(); let (updated_step2, gateway_config) = updated_step1.upgrade()?; - persist_gateway_details(&updated_step2.storage_paths, gateway_config)?; + let old_paths = updated_step2.storage_paths.clone(); + + let updated_step3: ConfigV1_1_33 = updated_step2.into(); + let updated = updated_step3.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; - let updated: Config = updated_step2.into(); updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { +async fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { // explicitly load it as v1.1.20_2 (which is incompatible with the current one, i.e. +1.1.21) let Ok(old_config) = ConfigV1_1_20_2::read_from_default_path(id) else { // if we failed to load it, there might have been nothing to upgrade @@ -243,14 +270,23 @@ fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { info!("It is going to get updated to the current specification."); let (updated_step1, gateway_config) = old_config.upgrade()?; - persist_gateway_details(&updated_step1.storage_paths, gateway_config)?; + let old_paths = updated_step1.storage_paths.clone(); + + let updated_step2: ConfigV1_1_33 = updated_step1.into(); + let updated = updated_step2.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; - let updated: Config = updated_step1.into(); updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_30_config(id: &str) -> Result { +async fn try_upgrade_v1_1_30_config(id: &str) -> Result { // explicitly load it as v1.1.30 (which is incompatible with the current one, i.e. +1.1.31) let Ok(old_config) = ConfigV1_1_30::read_from_default_path(id) else { // if we failed to load it, there might have been nothing to upgrade @@ -260,29 +296,68 @@ fn try_upgrade_v1_1_30_config(id: &str) -> Result { info!("It seems the client is using <= v1.1.30 config template."); info!("It is going to get updated to the current specification."); - let updated: Config = old_config.into(); + let old_paths = old_config.storage_paths.clone(); + + let updated_step1: ConfigV1_1_33 = old_config.into(); + let updated = updated_step1.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + None, + ) + .await?; + updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_config(id: &str) -> Result<(), Socks5ClientError> { - if try_upgrade_v1_1_13_config(id)? { +async fn try_upgrade_v1_1_33_config(id: &str) -> Result { + // explicitly load it as v1.1.33 (which is incompatible with the current one, i.e. +1.1.34) + let Ok(old_config) = ConfigV1_1_33::read_from_default_path(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.33 config template."); + info!("It is going to get updated to the current specification."); + + let old_paths = old_config.storage_paths.clone(); + + let updated = old_config.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + None, + ) + .await?; + + updated.save_to_default_location()?; + Ok(true) +} + +async fn try_upgrade_config(id: &str) -> Result<(), Socks5ClientError> { + if try_upgrade_v1_1_13_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_20_config(id)? { + if try_upgrade_v1_1_20_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_20_2_config(id)? { + if try_upgrade_v1_1_20_2_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_30_config(id)? { + if try_upgrade_v1_1_30_config(id).await? { + return Ok(()); + } + if try_upgrade_v1_1_33_config(id).await? { return Ok(()); } Ok(()) } -fn try_load_current_config(id: &str) -> Result { +async fn try_load_current_config(id: &str) -> Result { // try to load the config as is if let Ok(cfg) = Config::read_from_default_path(id) { return if !cfg.validate() { @@ -293,7 +368,7 @@ fn try_load_current_config(id: &str) -> Result { } // we couldn't load it - try upgrading it from older revisions - try_upgrade_config(id)?; + try_upgrade_config(id).await?; let config = match Config::read_from_default_path(id) { Ok(cfg) => cfg, diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 5638c80d9b..87234d676c 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -105,7 +105,7 @@ fn version_check(cfg: &Config) -> bool { pub(crate) async fn execute(args: Run) -> Result<(), Box> { eprintln!("Starting client {}...", args.common_args.id); - let mut config = try_load_current_config(&args.common_args.id)?; + let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); if !version_check(&config) { diff --git a/clients/socks5/src/commands/switch_gateway.rs b/clients/socks5/src/commands/switch_gateway.rs new file mode 100644 index 0000000000..6b24fbb6a3 --- /dev/null +++ b/clients/socks5/src/commands/switch_gateway.rs @@ -0,0 +1,24 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::commands::CliSocks5Client; +use crate::error::Socks5ClientError; +use nym_client_core::cli_helpers::client_switch_gateway::{ + switch_gateway, CommonClientSwitchGatewaysArgs, +}; + +#[derive(clap::Args, Clone, Debug)] +pub struct Args { + #[command(flatten)] + common_args: CommonClientSwitchGatewaysArgs, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientSwitchGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), Socks5ClientError> { + switch_gateway::(args).await +} diff --git a/clients/socks5/src/config/mod.rs b/clients/socks5/src/config/mod.rs index 9df903e64b..5f2f07d64d 100644 --- a/clients/socks5/src/config/mod.rs +++ b/clients/socks5/src/config/mod.rs @@ -3,7 +3,7 @@ use crate::config::template::CONFIG_TEMPLATE; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::cli_helpers::client_init::ClientConfig; +use nym_client_core::cli_helpers::CliClientConfig; use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_config::{ must_get_home, read_config_from_toml_file, save_formatted_config_to_file, NymConfigTemplate, @@ -24,6 +24,7 @@ pub mod old_config_v1_1_13; pub mod old_config_v1_1_20; pub mod old_config_v1_1_20_2; pub mod old_config_v1_1_30; +pub mod old_config_v1_1_33; mod persistence; mod template; @@ -71,7 +72,7 @@ impl NymConfigTemplate for Config { } } -impl ClientConfig for Config { +impl CliClientConfig for Config { fn common_paths(&self) -> &CommonClientPaths { &self.storage_paths.common_paths } diff --git a/clients/socks5/src/config/old_config_v1_1_20.rs b/clients/socks5/src/config/old_config_v1_1_20.rs index 4acc4fccbd..fbad434c68 100644 --- a/clients/socks5/src/config/old_config_v1_1_20.rs +++ b/clients/socks5/src/config/old_config_v1_1_20.rs @@ -5,8 +5,8 @@ use crate::config::old_config_v1_1_20_2::{ ConfigV1_1_20_2, CoreConfigV1_1_20_2, SocksClientPathsV1_1_20_2, }; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::config::disk_persistence::keys_paths::ClientKeysPaths; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; +use nym_client_core::config::disk_persistence::old_v1_1_33::ClientKeysPathsV1_1_33; use nym_client_core::config::old_config_v1_1_20::ConfigV1_1_20 as BaseConfigV1_1_20; use nym_client_core::config::old_config_v1_1_20_2::ClientV1_1_20_2; use nym_config::legacy_helpers::nym_config::MigrationNymConfig; @@ -50,7 +50,7 @@ impl From for ConfigV1_1_20_2 { }, storage_paths: SocksClientPathsV1_1_20_2 { common_paths: CommonClientPathsV1_1_20_2 { - keys: ClientKeysPaths { + keys: ClientKeysPathsV1_1_33 { private_identity_key_file: value.base.client.private_identity_key_file, public_identity_key_file: value.base.client.public_identity_key_file, private_encryption_key_file: value.base.client.private_encryption_key_file, diff --git a/clients/socks5/src/config/old_config_v1_1_20_2.rs b/clients/socks5/src/config/old_config_v1_1_20_2.rs index 4668e3ea9d..6b1e5f0219 100644 --- a/clients/socks5/src/config/old_config_v1_1_20_2.rs +++ b/clients/socks5/src/config/old_config_v1_1_20_2.rs @@ -2,13 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::old_config_v1_1_30::ConfigV1_1_30; -use crate::{ - config::{default_config_filepath, persistence::SocksClientPaths}, - error::Socks5ClientError, -}; +use crate::config::old_config_v1_1_33::SocksClientPathsV1_1_33; +use crate::{config::default_config_filepath, error::Socks5ClientError}; use nym_bin_common::logging::LoggingSettings; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; -use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; use nym_config::read_config_from_toml_file; use serde::{Deserialize, Serialize}; use std::io; @@ -43,11 +41,13 @@ impl ConfigV1_1_20_2 { // in this upgrade, gateway endpoint configuration was moved out of the config file, // so its returned to be stored elsewhere. - pub fn upgrade(self) -> Result<(ConfigV1_1_30, GatewayEndpointConfig), Socks5ClientError> { + pub fn upgrade( + self, + ) -> Result<(ConfigV1_1_30, OldGatewayEndpointConfigV1_1_33), Socks5ClientError> { let gateway_details = self.core.base.client.gateway_endpoint.clone().into(); let config = ConfigV1_1_30 { core: self.core.into(), - storage_paths: SocksClientPaths { + storage_paths: SocksClientPathsV1_1_33 { common_paths: self.storage_paths.common_paths.upgrade_default()?, }, logging: self.logging, diff --git a/clients/socks5/src/config/old_config_v1_1_30.rs b/clients/socks5/src/config/old_config_v1_1_30.rs index cd7a5ab9e4..e6f3a12bc0 100644 --- a/clients/socks5/src/config/old_config_v1_1_30.rs +++ b/clients/socks5/src/config/old_config_v1_1_30.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::persistence::SocksClientPaths; -use crate::config::{default_config_filepath, Config}; +use crate::config::default_config_filepath; +use crate::config::old_config_v1_1_33::{ConfigV1_1_33, SocksClientPathsV1_1_33}; use nym_bin_common::logging::LoggingSettings; use nym_config::read_config_from_toml_file; use nym_socks5_client_core::config::old_config_v1_1_30::ConfigV1_1_30 as CoreConfigV1_1_30; @@ -15,17 +15,14 @@ use std::path::Path; pub struct ConfigV1_1_30 { pub core: CoreConfigV1_1_30, - // I'm leaving a landmine here for when the paths actually do change the next time, - // but propagating the change right now (in ALL clients) would be such a hassle..., - // so sorry for the next person looking at it : ) - pub storage_paths: SocksClientPaths, + pub storage_paths: SocksClientPathsV1_1_33, pub logging: LoggingSettings, } -impl From for Config { +impl From for ConfigV1_1_33 { fn from(value: ConfigV1_1_30) -> Self { - Config { + ConfigV1_1_33 { core: value.core.into(), storage_paths: value.storage_paths, logging: LoggingSettings::default(), diff --git a/clients/socks5/src/config/old_config_v1_1_33.rs b/clients/socks5/src/config/old_config_v1_1_33.rs new file mode 100644 index 0000000000..fb14259bf4 --- /dev/null +++ b/clients/socks5/src/config/old_config_v1_1_33.rs @@ -0,0 +1,49 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::{default_config_filepath, Config, SocksClientPaths}; +use crate::error::Socks5ClientError; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; +use nym_config::read_config_from_toml_file; +use nym_socks5_client_core::config::old_config_v1_1_33::ConfigV1_1_33 as CoreConfigV1_1_33; +use serde::{Deserialize, Serialize}; +use std::io; +use std::path::Path; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct SocksClientPathsV1_1_33 { + #[serde(flatten)] + pub common_paths: CommonClientPathsV1_1_33, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_33 { + pub core: CoreConfigV1_1_33, + + // \/ CHANGED + pub storage_paths: SocksClientPathsV1_1_33, + // /\ CHANGED + pub logging: LoggingSettings, +} + +impl ConfigV1_1_33 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + core: self.core.into(), + storage_paths: SocksClientPaths { + common_paths: self.storage_paths.common_paths.upgrade_default()?, + }, + logging: self.logging, + }) + } +} diff --git a/clients/socks5/src/config/template.rs b/clients/socks5/src/config/template.rs index 764a7c6884..07556a5401 100644 --- a/clients/socks5/src/config/template.rs +++ b/clients/socks5/src/config/template.rs @@ -50,10 +50,6 @@ keys.private_encryption_key_file = '{{ storage_paths.keys.private_encryption_key # Path to file containing public encryption key. keys.public_encryption_key_file = '{{ storage_paths.keys.public_encryption_key_file }}' -# A gateway specific, optional, base58 stringified shared key used for -# communication with particular gateway. -keys.gateway_shared_key_file = '{{ storage_paths.keys.gateway_shared_key_file }}' - # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' @@ -64,9 +60,9 @@ credentials_database = '{{ storage_paths.credentials_database }}' # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. reply_surb_database = '{{ storage_paths.reply_surb_database }}' -# Path to the file containing information about gateway used by this client, -# i.e. details such as its public key, owner address or the network information. -gateway_details = '{{ storage_paths.gateway_details }}' +# Path to the file containing information about gateways used by this client, +# i.e. details such as their public keys, owner addresses or the network information. +gateway_registrations = '{{ storage_paths.gateway_registrations }}' ##### socket config options ##### @@ -77,7 +73,7 @@ provider_mix_address = '{{ core.socks5.provider_mix_address }}' # The address on which the client will be listening for incoming requests # (default: 127.0.0.1:1080) -bind_adddress = '{{ core.socks5.bind_adddress }}' +bind_address = '{{ core.socks5.bind_address }}' # Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. # While this is going to hide its actual address information, it will make the actual communication diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 31d499dac9..33f9428e44 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -49,6 +49,7 @@ impl BandwidthController { /// It marks any retrieved intermediate credentials as expired. pub async fn get_next_usable_credential( &self, + gateway_id: &str, ) -> Result where ::StorageError: Send + Sync + 'static, @@ -56,7 +57,7 @@ impl BandwidthController { loop { let Some(maybe_next) = self .storage - .get_next_unspent_credential() + .get_next_unspent_credential(gateway_id) .await .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))? else { @@ -114,12 +115,13 @@ impl BandwidthController { pub async fn prepare_bandwidth_credential( &self, + gateway_id: &str, ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let retrieved_credential = self.get_next_usable_credential().await?; + let retrieved_credential = self.get_next_usable_credential(gateway_id).await?; let epoch_id = retrieved_credential.credential.epoch_id(); let credential_id = retrieved_credential.credential_id; @@ -137,14 +139,16 @@ impl BandwidthController { }) } - pub async fn consume_credential(&self, id: i64) -> Result<(), BandwidthControllerError> + pub async fn consume_credential( + &self, + id: i64, + gateway_id: &str, + ) -> Result<(), BandwidthControllerError> where ::StorageError: Send + Sync + 'static, { - // JS: shouldn't we send some contract/validator/gateway message here to actually, you know, - // consume it? self.storage - .consume_coconut_credential(id) + .consume_coconut_credential(id, gateway_id) .await .map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err))) } diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index ae452c3e9b..b5c8aa5e2e 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -11,27 +11,27 @@ license.workspace = true [dependencies] async-trait = { workspace = true } base64 = "0.21.2" +bs58 = { workspace = true } cfg-if = "1.0.0" clap = { workspace = true, optional = true } -dashmap = { workspace = true } -dirs = "4.0" futures = { workspace = true } humantime-serde = "1.0" log = { workspace = true } rand = { version = "0.7.3", features = ["wasm-bindgen"] } -reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = "0.10.6" +si-scale = "0.2.2" tap = "1.0.1" thiserror = { workspace = true } url = { workspace = true, features = ["serde"] } tungstenite = { workspace = true, default-features = false } tokio = { workspace = true, features = ["macros"] } -time = "0.3.17" +time = { workspace = true } zeroize = { workspace = true } # internal +nym-id = { path = "../nym-id" } nym-bandwidth-controller = { path = "../bandwidth-controller" } nym-config = { path = "../config" } nym-crypto = { path = "../crypto" } @@ -47,7 +47,8 @@ nym-validator-client = { path = "../client-libs/validator-client", default-featu nym-task = { path = "../task" } nym-credential-storage = { path = "../credential-storage" } nym-network-defaults = { path = "../network-defaults" } -si-scale = "0.2.2" +nym-client-core-surb-storage = { path = "./surb-storage" } +nym-client-core-gateways-storage = { path = "./gateways-storage" } ### For serving prometheus metrics [target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper] @@ -74,11 +75,6 @@ features = ["time"] version = "0.20.1" features = ["rustls-tls-native-roots"] -[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] -workspace = true -features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] -optional = true - [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures] workspace = true @@ -104,18 +100,10 @@ features = ["wasm-bindgen"] [dev-dependencies] tempfile = "3.1.0" -[build-dependencies] -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -sqlx = { workspace = true, features = [ - "runtime-tokio-rustls", - "sqlite", - "macros", - "migrate", -] } - [features] default = [] cli = ["clap"] -fs-surb-storage = ["sqlx"] +fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"] +fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"] wasm = ["nym-gateway-client/wasm"] metrics-server = [] diff --git a/common/client-core/gateways-storage/Cargo.toml b/common/client-core/gateways-storage/Cargo.toml new file mode 100644 index 0000000000..4392798c0b --- /dev/null +++ b/common/client-core/gateways-storage/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "nym-client-core-gateways-storage" +version = "0.1.0" +edition = "2021" +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait.workspace = true +cosmrs.workspace = true +log.workspace = true +serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true +time.workspace = true +tokio = { workspace = true, features = ["sync"] } +url.workspace = true +zeroize = { workspace = true, features = ["zeroize_derive"] } + +nym-crypto = { path = "../../crypto", features = ["asymmetric"] } +nym-gateway-requests = { path = "../../../gateway/gateway-requests" } + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] +workspace = true +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] +optional = true + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + +[features] +fs-gateways-storage = ["sqlx"] \ No newline at end of file diff --git a/common/client-core/gateways-storage/build.rs b/common/client-core/gateways-storage/build.rs new file mode 100644 index 0000000000..f8b85cbb4a --- /dev/null +++ b/common/client-core/gateways-storage/build.rs @@ -0,0 +1,31 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[tokio::main] +async fn main() { + #[cfg(feature = "fs-gateways-storage")] + { + use sqlx::{Connection, SqliteConnection}; + use std::env; + + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{out_dir}/gateways-storage-example.sqlite"); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./fs_gateways_migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + #[cfg(target_family = "unix")] + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + } +} diff --git a/common/client-core/gateways-storage/fs_gateways_migrations/20240304120000_create_initial_tables.sql b/common/client-core/gateways-storage/fs_gateways_migrations/20240304120000_create_initial_tables.sql new file mode 100644 index 0000000000..0382f37313 --- /dev/null +++ b/common/client-core/gateways-storage/fs_gateways_migrations/20240304120000_create_initial_tables.sql @@ -0,0 +1,39 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +CREATE TABLE active_gateway +( + id INTEGER PRIMARY KEY CHECK (id = 0), + active_gateway_id_bs58 TEXT REFERENCES registered_gateway (gateway_id_bs58) +); + +CREATE TABLE registered_gateway +( + gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY, + registration_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, + gateway_type TEXT CHECK ( gateway_type IN ('remote', 'custom') ) NOT NULL DEFAULT 'remote' +); + +-- TODO: perhaps keep additional metadata such as bandwidth, credential usage, etc + + +CREATE TABLE remote_gateway_details +( + gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58), + derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT NOT NULL, + gateway_owner_address TEXT NOT NULL, + gateway_listener TEXT NOT NULL, + wg_tun_address TEXT +); + +CREATE TABLE custom_gateway_details +( + gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58), + data BLOB +); + + +INSERT INTO active_gateway(id, active_gateway_id_bs58) +values (0, NULL); \ No newline at end of file diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/error.rs b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs new file mode 100644 index 0000000000..bdcaa0fdb5 --- /dev/null +++ b/common/client-core/gateways-storage/src/backend/fs_backend/error.rs @@ -0,0 +1,45 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::BadGateway; +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum StorageError { + #[error("the provided database path doesn't have a filename defined")] + DatabasePathWithoutFilename { provided_path: PathBuf }, + + #[error("unable to create the directory for the database at {}: {source}", provided_path.display())] + DatabasePathUnableToCreateParentDirectory { + provided_path: PathBuf, + source: io::Error, + }, + + #[error("failed to perform sqlx migration: {source}")] + MigrationError { + #[source] + #[from] + source: sqlx::migrate::MigrateError, + }, + + #[error("failed to connect to the underlying connection pool: {source}")] + DatabaseConnectionError { + #[source] + source: sqlx::error::Error, + }, + + #[error("failed to run the SQL query: {source}")] + QueryError { + #[source] + #[from] + source: sqlx::error::Error, + }, + + #[error(transparent)] + MalformedGateway(#[from] BadGateway), + + #[error("gateway {gateway_id} does not exist in the storage")] + GatewayDoesNotExist { gateway_id: String }, +} diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs new file mode 100644 index 0000000000..4c3e3c48b5 --- /dev/null +++ b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs @@ -0,0 +1,234 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + backend::fs_backend::error::StorageError, + types::{ + RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails, + }, +}; +use log::{debug, error}; +use sqlx::ConnectOptions; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct StorageManager { + pub connection_pool: sqlx::SqlitePool, +} + +// all SQL goes here +impl StorageManager { + pub async fn init>(database_path: P) -> Result { + // ensure the whole directory structure exists + if let Some(parent_dir) = database_path.as_ref().parent() { + std::fs::create_dir_all(parent_dir).map_err(|source| { + StorageError::DatabasePathUnableToCreateParentDirectory { + provided_path: database_path.as_ref().to_path_buf(), + source, + } + })?; + } + + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true); + + opts.disable_statement_logging(); + + let connection_pool = sqlx::SqlitePool::connect_with(opts) + .await + .map_err(|source| { + error!("Failed to connect to SQLx database: {source}"); + StorageError::DatabaseConnectionError { source } + })?; + + sqlx::migrate!("./fs_gateways_migrations") + .run(&connection_pool) + .await + .inspect_err(|err| { + error!("Failed to initialize SQLx database: {err}"); + })?; + + debug!("Database migration finished!"); + Ok(StorageManager { connection_pool }) + } + + pub(crate) async fn get_active_gateway(&self) -> Result { + sqlx::query_as!( + RawActiveGateway, + "SELECT active_gateway_id_bs58 FROM active_gateway" + ) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn set_active_gateway( + &self, + gateway_id: Option<&str>, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE active_gateway SET active_gateway_id_bs58 = ?", + gateway_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn has_registered_gateway( + &self, + gateway_id: &str, + ) -> Result { + sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id) + .fetch_one(&self.connection_pool) + .await + .map(|result| result.exists == 1) + } + + pub(crate) async fn maybe_get_registered_gateway( + &self, + gateway_id: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM registered_gateway WHERE gateway_id_bs58 = ?") + .bind(gateway_id) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn must_get_registered_gateway( + &self, + gateway_id: &str, + ) -> Result { + sqlx::query_as("SELECT * FROM registered_gateway WHERE gateway_id_bs58 = ?") + .bind(gateway_id) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn set_registered_gateway( + &self, + registered_gateway: &RawRegisteredGateway, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type) + VALUES (?, ?, ?) + "#, + registered_gateway.gateway_id_bs58, + registered_gateway.registration_timestamp, + registered_gateway.gateway_type, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_registered_gateway( + &self, + gateway_id: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM registered_gateway WHERE gateway_id_bs58 = ?", + gateway_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_remote_gateway_details( + &self, + gateway_id: &str, + ) -> Result { + sqlx::query_as!( + RawRemoteGatewayDetails, + "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?", + gateway_id + ) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn set_remote_gateway_details( + &self, + remote: &RawRemoteGatewayDetails, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, gateway_owner_address, gateway_listener, wg_tun_address) + VALUES (?, ?, ?, ?, ?) + "#, + remote.gateway_id_bs58, + remote.derived_aes128_ctr_blake3_hmac_keys_bs58, + remote.gateway_owner_address, + remote.gateway_listener, + remote.wg_tun_address, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_remote_gateway_details( + &self, + gateway_id: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM remote_gateway_details WHERE gateway_id_bs58 = ?", + gateway_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_custom_gateway_details( + &self, + gateway_id: &str, + ) -> Result { + sqlx::query_as!( + RawCustomGatewayDetails, + "SELECT * FROM custom_gateway_details WHERE gateway_id_bs58 = ?", + gateway_id + ) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn set_custom_gateway_details( + &self, + custom: &RawCustomGatewayDetails, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO custom_gateway_details(gateway_id_bs58, data) + VALUES (?, ?) + "#, + custom.gateway_id_bs58, + custom.data, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_custom_gateway_details( + &self, + gateway_id: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "DELETE FROM custom_gateway_details WHERE gateway_id_bs58 = ?", + gateway_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn registered_gateways(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT gateway_id_bs58 FROM registered_gateway") + .fetch_all(&self.connection_pool) + .await + .map(|records| records.into_iter().map(|r| r.gateway_id_bs58).collect()) + } +} diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/mod.rs b/common/client-core/gateways-storage/src/backend/fs_backend/mod.rs new file mode 100644 index 0000000000..0b93d284f6 --- /dev/null +++ b/common/client-core/gateways-storage/src/backend/fs_backend/mod.rs @@ -0,0 +1,155 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + ActiveGateway, BadGateway, GatewayDetails, GatewayRegistration, GatewayType, + GatewaysDetailsStore, StorageError, +}; +use async_trait::async_trait; +use manager::StorageManager; +use nym_crypto::asymmetric::identity::PublicKey; +use std::path::Path; + +pub mod error; +mod manager; +mod models; + +pub struct OnDiskGatewaysDetails { + manager: StorageManager, +} + +impl OnDiskGatewaysDetails { + pub async fn init>(database_path: P) -> Result { + Ok(OnDiskGatewaysDetails { + manager: StorageManager::init(database_path).await?, + }) + } +} + +#[async_trait] +impl GatewaysDetailsStore for OnDiskGatewaysDetails { + type StorageError = error::StorageError; + + async fn has_gateway_details(&self, gateway_id: &str) -> Result { + Ok(self + .manager + .maybe_get_registered_gateway(gateway_id) + .await? + .is_some()) + } + + async fn active_gateway(&self) -> Result { + let raw_active = self.manager.get_active_gateway().await?; + let registration = match raw_active.active_gateway_id_bs58 { + None => None, + Some(gateway_id) => Some(self.load_gateway_details(&gateway_id).await?), + }; + + Ok(ActiveGateway { registration }) + } + + async fn set_active_gateway(&self, gateway_id: &str) -> Result<(), Self::StorageError> { + if !self.manager.has_registered_gateway(gateway_id).await? { + return Err(StorageError::GatewayDoesNotExist { + gateway_id: gateway_id.to_string(), + }); + } + Ok(self.manager.set_active_gateway(Some(gateway_id)).await?) + } + + async fn all_gateways(&self) -> Result, Self::StorageError> { + let identities = self.manager.registered_gateways().await?; + let mut registered = Vec::with_capacity(identities.len()); + for gateway_id in identities { + registered.push(self.load_gateway_details(&gateway_id).await?) + } + + Ok(registered) + } + + async fn all_gateways_identities(&self) -> Result, Self::StorageError> { + Ok(self + .manager + .registered_gateways() + .await? + .into_iter() + .map(|gateway_id| { + gateway_id + .as_str() + .parse() + .map_err(|source| BadGateway::MalformedGatewayIdentity { gateway_id, source }) + }) + .collect::>()?) + } + + async fn load_gateway_details( + &self, + gateway_id: &str, + ) -> Result { + let raw_registration = self.manager.must_get_registered_gateway(gateway_id).await?; + let typ: GatewayType = raw_registration.gateway_type.parse()?; + + let details = match typ { + GatewayType::Remote => { + let raw_details = self.manager.get_remote_gateway_details(gateway_id).await?; + GatewayDetails::Remote(raw_details.try_into()?) + } + GatewayType::Custom => { + let raw_details = self.manager.get_custom_gateway_details(gateway_id).await?; + GatewayDetails::Custom(raw_details.try_into()?) + } + }; + + Ok(GatewayRegistration { + details, + registration_timestamp: raw_registration.registration_timestamp, + }) + } + + async fn store_gateway_details( + &self, + details: &GatewayRegistration, + ) -> Result<(), Self::StorageError> { + let raw_registration = details.into(); + self.manager + .set_registered_gateway(&raw_registration) + .await?; + + match &details.details { + GatewayDetails::Remote(remote_details) => { + let raw_details = remote_details.into(); + self.manager + .set_remote_gateway_details(&raw_details) + .await?; + } + GatewayDetails::Custom(custom_details) => { + let raw_details = custom_details.into(); + self.manager + .set_custom_gateway_details(&raw_details) + .await?; + } + } + Ok(()) + } + + // ideally all of those should be run under a storage tx to ensure storage consistency, + // but at that point it's fine + async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { + let active = self.manager.get_active_gateway().await?; + if let Some(currently_active) = &active.active_gateway_id_bs58 { + if currently_active == gateway_id { + self.manager.set_active_gateway(None).await?; + } + } + + // just try remove it from all tables even if it doesn't actually exist + self.manager.remove_registered_gateway(gateway_id).await?; + self.manager + .remove_remote_gateway_details(gateway_id) + .await?; + self.manager + .remove_custom_gateway_details(gateway_id) + .await?; + Ok(()) + } +} diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/models.rs b/common/client-core/gateways-storage/src/backend/fs_backend/models.rs new file mode 100644 index 0000000000..755fb6cc8b --- /dev/null +++ b/common/client-core/gateways-storage/src/backend/fs_backend/models.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/common/client-core/gateways-storage/src/backend/mem_backend.rs b/common/client-core/gateways-storage/src/backend/mem_backend.rs new file mode 100644 index 0000000000..451f6b2187 --- /dev/null +++ b/common/client-core/gateways-storage/src/backend/mem_backend.rs @@ -0,0 +1,108 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::{ActiveGateway, GatewayRegistration}; +use crate::{BadGateway, GatewaysDetailsStore}; +use async_trait::async_trait; +use std::collections::HashMap; +use std::sync::Arc; +use thiserror::Error; +use tokio::sync::RwLock; + +#[derive(Debug, Error)] +pub enum InMemStorageError { + #[error("gateway {gateway_id} does not exist")] + GatewayDoesNotExist { gateway_id: String }, + + #[error(transparent)] + MalformedGateway(#[from] BadGateway), +} + +#[derive(Debug, Default)] +pub struct InMemGatewaysDetails { + inner: Arc>, +} + +#[derive(Debug, Default)] +struct InMemStorageInner { + active_gateway: Option, + gateways: HashMap, +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl GatewaysDetailsStore for InMemGatewaysDetails { + type StorageError = InMemStorageError; + + async fn has_gateway_details(&self, gateway_id: &str) -> Result { + Ok(self.inner.read().await.gateways.contains_key(gateway_id)) + } + + async fn active_gateway(&self) -> Result { + let guard = self.inner.read().await; + + let registration = guard.active_gateway.as_ref().map(|id| { + // SAFETY: if particular gateway is set as active, its details MUST exist + #[allow(clippy::unwrap_used)] + guard.gateways.get(id).unwrap().clone() + }); + + Ok(ActiveGateway { registration }) + } + + async fn set_active_gateway(&self, gateway_id: &str) -> Result<(), Self::StorageError> { + // ensure the gateway with provided id exists + let mut guard = self.inner.write().await; + + if !guard.gateways.contains_key(gateway_id) { + return Err(InMemStorageError::GatewayDoesNotExist { + gateway_id: gateway_id.to_string(), + }); + } + + guard.active_gateway = Some(gateway_id.to_string()); + Ok(()) + } + + async fn all_gateways(&self) -> Result, Self::StorageError> { + Ok(self.inner.read().await.gateways.values().cloned().collect()) + } + + async fn load_gateway_details( + &self, + gateway_id: &str, + ) -> Result { + self.inner + .read() + .await + .gateways + .get(gateway_id) + .cloned() + .ok_or(InMemStorageError::GatewayDoesNotExist { + gateway_id: gateway_id.to_string(), + }) + } + + async fn store_gateway_details( + &self, + details: &GatewayRegistration, + ) -> Result<(), Self::StorageError> { + self.inner.write().await.gateways.insert( + details.details.gateway_id().to_base58_string(), + details.clone(), + ); + Ok(()) + } + + async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { + let mut guard = self.inner.write().await; + if let Some(active) = guard.active_gateway.as_ref() { + if active == gateway_id { + guard.active_gateway = None + } + } + guard.gateways.remove(gateway_id); + + Ok(()) + } +} diff --git a/common/client-core/gateways-storage/src/backend/mod.rs b/common/client-core/gateways-storage/src/backend/mod.rs new file mode 100644 index 0000000000..9414bb4f14 --- /dev/null +++ b/common/client-core/gateways-storage/src/backend/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-gateways-storage"))] +pub mod fs_backend; + +pub mod mem_backend; diff --git a/common/client-core/gateways-storage/src/error.rs b/common/client-core/gateways-storage/src/error.rs new file mode 100644 index 0000000000..f547164022 --- /dev/null +++ b/common/client-core/gateways-storage/src/error.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::identity::Ed25519RecoveryError; +use nym_gateway_requests::registration::handshake::shared_key::SharedKeyConversionError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum BadGateway { + #[error("{typ} is not a valid gateway type")] + InvalidGatewayType { typ: String }, + + #[error("the provided gateway identity {gateway_id} is malformed: {source}")] + MalformedGatewayIdentity { + gateway_id: String, + + #[source] + source: Ed25519RecoveryError, + }, + + #[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {source}")] + MalformedGatewayOwnerAccountAddress { + gateway_id: String, + + raw_owner: String, + + #[source] + source: cosmrs::ErrorReport, + }, + + #[error("the shared keys provided for gateway {gateway_id} are malformed: {source}")] + MalformedSharedKeys { + gateway_id: String, + + #[source] + source: SharedKeyConversionError, + }, + + #[error( + "the listening address of gateway {gateway_id} ({raw_listener}) is malformed: {source}" + )] + MalformedListener { + gateway_id: String, + + raw_listener: String, + + #[source] + source: url::ParseError, + }, +} diff --git a/common/client-core/gateways-storage/src/lib.rs b/common/client-core/gateways-storage/src/lib.rs new file mode 100644 index 0000000000..2d4f18422f --- /dev/null +++ b/common/client-core/gateways-storage/src/lib.rs @@ -0,0 +1,66 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] + +use async_trait::async_trait; +use std::error::Error; + +pub mod backend; +pub mod error; +pub mod types; + +// todo: export port types +pub use crate::types::*; +pub use backend::mem_backend::{InMemGatewaysDetails, InMemStorageError}; +pub use error::BadGateway; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-gateways-storage"))] +pub use backend::fs_backend::{error::StorageError, OnDiskGatewaysDetails}; +use nym_crypto::asymmetric::identity; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait GatewaysDetailsStore { + type StorageError: Error + From; + + /// Returns details of the currently active gateway, if available. + async fn active_gateway(&self) -> Result; + + /// Set the provided gateway as the currently active gateway. + async fn set_active_gateway(&self, gateway_id: &str) -> Result<(), Self::StorageError>; + + /// Returns details of all registered gateways. + async fn all_gateways(&self) -> Result, Self::StorageError>; + + /// Return identity keys of all registered gateways. + async fn all_gateways_identities( + &self, + ) -> Result, Self::StorageError> { + Ok(self + .all_gateways() + .await? + .into_iter() + .map(|gateway| gateway.details.gateway_id()) + .collect()) + } + + /// Check if the gateway with the provided id already exists in the store. + async fn has_gateway_details(&self, gateway_id: &str) -> Result; + + /// Returns details of the particular gateway. + async fn load_gateway_details( + &self, + gateway_id: &str, + ) -> Result; + + /// Store the provided gateway details. + async fn store_gateway_details( + &self, + details: &GatewayRegistration, + ) -> Result<(), Self::StorageError>; + + /// Remove given gateway details from the underlying store. + async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError>; +} diff --git a/common/client-core/gateways-storage/src/types.rs b/common/client-core/gateways-storage/src/types.rs new file mode 100644 index 0000000000..3755cb6127 --- /dev/null +++ b/common/client-core/gateways-storage/src/types.rs @@ -0,0 +1,300 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::BadGateway; +use cosmrs::AccountId; +use nym_crypto::asymmetric::identity; +use nym_gateway_requests::registration::handshake::SharedKeys; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use std::sync::Arc; +use time::OffsetDateTime; +use url::Url; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +pub const REMOTE_GATEWAY_TYPE: &str = "remote"; +pub const CUSTOM_GATEWAY_TYPE: &str = "custom"; + +#[derive(Debug, Clone, Default)] +pub struct ActiveGateway { + pub registration: Option, +} + +#[derive(Debug, Clone)] +pub struct GatewayRegistration { + pub details: GatewayDetails, + pub registration_timestamp: OffsetDateTime, +} + +impl<'a> From<&'a GatewayRegistration> for RawRegisteredGateway { + fn from(value: &'a GatewayRegistration) -> Self { + RawRegisteredGateway { + gateway_id_bs58: value.details.gateway_id().to_base58_string(), + registration_timestamp: value.registration_timestamp, + gateway_type: value.details.typ().to_string(), + } + } +} + +#[derive(Debug, Clone)] +pub enum GatewayDetails { + /// Standard details of a remote gateway + Remote(RemoteGatewayDetails), + + /// Custom gateway setup, such as for a client embedded inside gateway itself + Custom(CustomGatewayDetails), +} + +impl From for GatewayRegistration { + fn from(details: GatewayDetails) -> Self { + GatewayRegistration { + details, + registration_timestamp: OffsetDateTime::now_utc(), + } + } +} + +impl GatewayDetails { + pub fn new_remote( + gateway_id: identity::PublicKey, + derived_aes128_ctr_blake3_hmac_keys: Arc, + gateway_owner_address: AccountId, + gateway_listener: Url, + wg_tun_address: Option, + ) -> Self { + GatewayDetails::Remote(RemoteGatewayDetails { + gateway_id, + derived_aes128_ctr_blake3_hmac_keys, + gateway_owner_address, + gateway_listener, + wg_tun_address, + }) + } + + pub fn new_custom(gateway_id: identity::PublicKey, data: Option>) -> Self { + GatewayDetails::Custom(CustomGatewayDetails { gateway_id, data }) + } + + pub fn gateway_id(&self) -> identity::PublicKey { + match self { + GatewayDetails::Remote(details) => details.gateway_id, + GatewayDetails::Custom(details) => details.gateway_id, + } + } + + pub fn shared_key(&self) -> Option<&SharedKeys> { + match self { + GatewayDetails::Remote(details) => Some(&details.derived_aes128_ctr_blake3_hmac_keys), + GatewayDetails::Custom(_) => None, + } + } + + pub fn is_custom(&self) -> bool { + matches!(self, GatewayDetails::Custom(..)) + } + + pub fn typ(&self) -> GatewayType { + match self { + GatewayDetails::Remote(_) => GatewayType::Remote, + GatewayDetails::Custom(_) => GatewayType::Custom, + } + } +} + +#[derive(Debug, Copy, Clone, Default)] +pub enum GatewayType { + #[default] + Remote, + + Custom, +} + +impl FromStr for GatewayType { + type Err = BadGateway; + + fn from_str(s: &str) -> Result { + match s { + REMOTE_GATEWAY_TYPE => Ok(GatewayType::Remote), + CUSTOM_GATEWAY_TYPE => Ok(GatewayType::Custom), + other => Err(BadGateway::InvalidGatewayType { + typ: other.to_string(), + }), + } + } +} + +impl Display for GatewayType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + GatewayType::Remote => REMOTE_GATEWAY_TYPE.fmt(f), + GatewayType::Custom => CUSTOM_GATEWAY_TYPE.fmt(f), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct RawActiveGateway { + pub active_gateway_id_bs58: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct RawRegisteredGateway { + pub gateway_id_bs58: String, + + // not necessarily needed but is nice for display purposes + pub registration_timestamp: OffsetDateTime, + + pub gateway_type: String, +} + +#[derive(Debug, Clone, Copy)] +pub struct RegisteredGateway { + pub gateway_id: identity::PublicKey, + + pub registration_timestamp: OffsetDateTime, + + pub gateway_type: GatewayType, +} + +#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct RawRemoteGatewayDetails { + pub gateway_id_bs58: String, + pub derived_aes128_ctr_blake3_hmac_keys_bs58: String, + pub gateway_owner_address: String, + pub gateway_listener: String, + pub wg_tun_address: Option, +} + +impl TryFrom for RemoteGatewayDetails { + type Error = BadGateway; + + fn try_from(value: RawRemoteGatewayDetails) -> Result { + let gateway_id = + identity::PublicKey::from_base58_string(&value.gateway_id_bs58).map_err(|source| { + BadGateway::MalformedGatewayIdentity { + gateway_id: value.gateway_id_bs58.clone(), + source, + } + })?; + + let derived_aes128_ctr_blake3_hmac_keys = Arc::new( + SharedKeys::try_from_base58_string(&value.derived_aes128_ctr_blake3_hmac_keys_bs58) + .map_err(|source| BadGateway::MalformedSharedKeys { + gateway_id: value.gateway_id_bs58.clone(), + source, + })?, + ); + + let gateway_owner_address = + AccountId::from_str(&value.gateway_owner_address).map_err(|source| { + BadGateway::MalformedGatewayOwnerAccountAddress { + gateway_id: value.gateway_id_bs58.clone(), + raw_owner: value.gateway_owner_address.clone(), + source, + } + })?; + + let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| { + BadGateway::MalformedListener { + gateway_id: value.gateway_id_bs58.clone(), + raw_listener: value.gateway_listener.clone(), + source, + } + })?; + + let wg_tun_address = value + .wg_tun_address + .as_ref() + .map(|addr| { + Url::parse(addr).map_err(|source| BadGateway::MalformedListener { + gateway_id: value.gateway_id_bs58.clone(), + raw_listener: addr.clone(), + source, + }) + }) + .transpose()?; + + Ok(RemoteGatewayDetails { + gateway_id, + derived_aes128_ctr_blake3_hmac_keys, + gateway_owner_address, + gateway_listener, + wg_tun_address, + }) + } +} + +impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails { + fn from(value: &'a RemoteGatewayDetails) -> Self { + RawRemoteGatewayDetails { + gateway_id_bs58: value.gateway_id.to_base58_string(), + derived_aes128_ctr_blake3_hmac_keys_bs58: value + .derived_aes128_ctr_blake3_hmac_keys + .to_base58_string(), + gateway_owner_address: value.gateway_owner_address.to_string(), + gateway_listener: value.gateway_listener.to_string(), + wg_tun_address: value.wg_tun_address.as_ref().map(|addr| addr.to_string()), + } + } +} + +#[derive(Debug, Clone)] +pub struct RemoteGatewayDetails { + pub gateway_id: identity::PublicKey, + + // note: `SharedKeys` implement ZeroizeOnDrop, meaning when `RemoteGatewayDetails` is dropped, + // the keys will be zeroized + pub derived_aes128_ctr_blake3_hmac_keys: Arc, + + pub gateway_owner_address: AccountId, + + pub gateway_listener: Url, + + pub wg_tun_address: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] +pub struct RawCustomGatewayDetails { + pub gateway_id_bs58: String, + pub data: Option>, +} + +impl TryFrom for CustomGatewayDetails { + type Error = BadGateway; + + fn try_from(value: RawCustomGatewayDetails) -> Result { + let gateway_id = + identity::PublicKey::from_base58_string(&value.gateway_id_bs58).map_err(|source| { + BadGateway::MalformedGatewayIdentity { + gateway_id: value.gateway_id_bs58.clone(), + source, + } + })?; + + Ok(CustomGatewayDetails { + gateway_id, + data: value.data, + }) + } +} + +impl<'a> From<&'a CustomGatewayDetails> for RawCustomGatewayDetails { + fn from(value: &'a CustomGatewayDetails) -> Self { + RawCustomGatewayDetails { + gateway_id_bs58: value.gateway_id.to_base58_string(), + // I don't know what to feel about that clone here given it might contain possibly sensitive data + data: value.data.clone(), + } + } +} + +#[derive(Debug, Clone)] +pub struct CustomGatewayDetails { + pub gateway_id: identity::PublicKey, + pub data: Option>, +} diff --git a/common/client-core/src/cli_helpers/client_add_gateway.rs b/common/client-core/src/cli_helpers/client_add_gateway.rs new file mode 100644 index 0000000000..05c367f49a --- /dev/null +++ b/common/client-core/src/cli_helpers/client_add_gateway.rs @@ -0,0 +1,162 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_helpers::types::GatewayInfo; +use crate::cli_helpers::{CliClient, CliClientConfig}; +use crate::client::base_client::non_wasm_helpers::setup_fs_gateways_storage; +use crate::{ + client::{ + base_client::storage::helpers::{get_all_registered_identities, set_active_gateway}, + key_manager::persistence::OnDiskKeys, + }, + error::ClientCoreError, + init::types::{GatewaySelectionSpecification, GatewaySetup}, +}; +use log::info; +use nym_client_core_gateways_storage::GatewayDetails; +use nym_crypto::asymmetric::identity; +use nym_topology::NymTopology; +use std::path::PathBuf; + +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[derive(Debug, Clone)] +pub struct CommonClientAddGatewayArgs { + /// Id of client we want to add gateway for. + #[cfg_attr(feature = "cli", clap(long))] + pub id: String, + + /// Explicitly specify id of the gateway to register with. + /// If unspecified, a random gateway will be chosen instead. + #[cfg_attr(feature = "cli", clap(long, alias = "gateway"))] + pub gateway_id: Option, + + /// Specifies whether the client will attempt to enforce tls connection to the desired gateway. + #[cfg_attr(feature = "cli", clap(long))] + pub force_tls_gateway: bool, + + /// Specifies whether the new gateway should be determined based by latency as opposed to being chosen + /// uniformly. + #[cfg_attr(feature = "cli", clap(long, conflicts_with = "gateway_id"))] + pub latency_based_selection: bool, + + /// Specify whether this new gateway should be set as the active one + #[cfg_attr(feature = "cli", clap(long, default_value_t = true))] + pub set_active: bool, + + /// Comma separated list of rest endpoints of the API validators + #[cfg_attr( + feature = "cli", + clap( + long, + alias = "api_validators", + value_delimiter = ',', + group = "network" + ) + )] + pub nym_apis: Option>, + + /// Path to .json file containing custom network specification. + #[cfg_attr(feature = "cli", clap(long, group = "network", hide = true))] + pub custom_mixnet: Option, +} + +pub async fn add_gateway(args: A) -> Result +where + A: AsRef, + C: CliClient, +{ + let common_args = args.as_ref(); + let id = &common_args.id; + + let config = C::try_load_current_config(id).await?; + let core = config.core_config(); + let paths = config.common_paths(); + + let key_store = OnDiskKeys::new(paths.keys.clone()); + let details_store = setup_fs_gateways_storage(&paths.gateway_registrations).await?; + + // Attempt to use a user-provided gateway, if possible + let user_chosen_gateway_id = common_args.gateway_id; + log::debug!("User chosen gateway id: {user_chosen_gateway_id:?}"); + + let selection_spec = GatewaySelectionSpecification::new( + user_chosen_gateway_id.map(|id| id.to_base58_string()), + Some(common_args.latency_based_selection), + common_args.force_tls_gateway, + ); + log::debug!("Gateway selection specification: {selection_spec:?}"); + + let registered_gateways = get_all_registered_identities(&details_store).await?; + + // if user provided gateway id (and we can't overwrite data), make sure we're not trying to register + // with a known gateway + if let Some(user_chosen) = user_chosen_gateway_id { + if registered_gateways.contains(&user_chosen) { + return Err(ClientCoreError::AlreadyRegistered { + gateway_id: user_chosen.to_base58_string(), + } + .into()); + } + } + + // Setup gateway by either registering a new one, or creating a new config from the selected + // one but with keys kept, or reusing the gateway configuration. + let available_gateways = if let Some(custom_mixnet) = common_args.custom_mixnet.as_ref() { + let hardcoded_topology = NymTopology::new_from_file(custom_mixnet).map_err(|source| { + ClientCoreError::CustomTopologyLoadFailure { + file_path: custom_mixnet.clone(), + source, + } + })?; + hardcoded_topology.get_gateways() + } else { + let mut rng = rand::thread_rng(); + crate::init::helpers::current_gateways(&mut rng, &core.client.nym_api_urls).await? + }; + + // since we're registering with a brand new gateway, + // make sure the list of available gateways doesn't overlap the list of known gateways + let available_gateways = available_gateways + .into_iter() + .filter(|g| !registered_gateways.contains(g.identity())) + .collect::>(); + + if available_gateways.is_empty() { + return Err(ClientCoreError::NoNewGatewaysAvailable.into()); + } + + let gateway_setup = GatewaySetup::New { + specification: selection_spec, + available_gateways, + wg_tun_address: None, + }; + + let init_details = + crate::init::setup_gateway(gateway_setup, &key_store, &details_store).await?; + + let address = init_details.client_address(); + + let gateway_registration = init_details.gateway_registration; + let GatewayDetails::Remote(ref gateway_details) = gateway_registration.details else { + return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails)?; + }; + + if common_args.set_active { + set_active_gateway( + &details_store, + &gateway_details.gateway_id.to_base58_string(), + ) + .await?; + } else { + info!("registered with new gateway {} (under address {address}), but this will not be our default address", gateway_details.gateway_id); + } + + Ok(GatewayInfo { + registration: gateway_registration.registration_timestamp, + identity: gateway_details.gateway_id, + active: common_args.set_active, + typ: gateway_registration.details.typ().to_string(), + endpoint: Some(gateway_details.gateway_listener.clone()), + wg_tun_address: gateway_details.wg_tun_address.clone(), + }) +} diff --git a/common/client-core/src/cli_helpers/client_import_credential.rs b/common/client-core/src/cli_helpers/client_import_credential.rs new file mode 100644 index 0000000000..48468771da --- /dev/null +++ b/common/client-core/src/cli_helpers/client_import_credential.rs @@ -0,0 +1,59 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_helpers::{CliClient, CliClientConfig}; +use std::fs; +use std::path::PathBuf; + +#[cfg(feature = "cli")] +fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { + bs58::decode(raw).into_vec() +} + +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[cfg_attr(feature = "cli", clap(group(clap::ArgGroup::new("cred_data").required(true))))] +#[derive(Debug, Clone)] +pub struct CommonClientImportCredentialArgs { + /// Id of client that is going to import the credential + #[cfg_attr(feature = "cli", clap(long))] + pub id: String, + + /// Explicitly provide the encoded credential data (as base58) + #[cfg_attr(feature = "cli", clap(long, group = "cred_data", value_parser = parse_encoded_credential_data))] + pub(crate) credential_data: Option>, + + /// Specifies the path to file containing binary credential data + #[cfg_attr(feature = "cli", clap(long, group = "cred_data"))] + pub(crate) credential_path: Option, + + // currently hidden as there exists only a single serialization standard + #[cfg_attr(feature = "cli", clap(long, hide = true))] + pub(crate) version: Option, +} + +pub async fn import_credential(args: A) -> Result<(), C::Error> +where + A: Into, + C: CliClient, + C::Error: From + From, +{ + let common_args = args.into(); + let id = &common_args.id; + + let config = C::try_load_current_config(id).await?; + let paths = config.common_paths(); + + let credentials_store = + nym_credential_storage::initialise_persistent_storage(&paths.credentials_database).await; + + let raw_credential = match common_args.credential_data { + Some(data) => data, + None => { + // SAFETY: one of those arguments must have been set + fs::read(common_args.credential_path.unwrap())? + } + }; + + nym_id::import_credential(credentials_store, raw_credential, common_args.version).await?; + Ok(()) +} diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index 9550cffdb2..019416fb2d 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -1,27 +1,28 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::disk_persistence::CommonClientPaths; +use crate::cli_helpers::traits::{CliClient, CliClientConfig}; use crate::error::ClientCoreError; use crate::{ client::{ - base_client::storage::gateway_details::OnDiskGatewayDetails, + base_client::{ + non_wasm_helpers::setup_fs_gateways_storage, storage::helpers::set_active_gateway, + }, key_manager::persistence::OnDiskKeys, }, - init::types::{GatewayDetails, GatewaySelectionSpecification, GatewaySetup, InitResults}, + init::types::{GatewaySelectionSpecification, GatewaySetup, InitResults}, }; use log::info; +use nym_client_core_gateways_storage::GatewayDetails; use nym_crypto::asymmetric::identity; use nym_topology::NymTopology; -use std::path::{Path, PathBuf}; +use rand::rngs::OsRng; +use std::path::PathBuf; -pub trait InitialisableClient { - const NAME: &'static str; - type Error: From; +// we can suppress this warning (as suggested by linter itself) since we're only using it in our own code +#[allow(async_fn_in_trait)] +pub trait InitialisableClient: CliClient { type InitArgs: AsRef; - type Config: ClientConfig; - - fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error>; fn initialise_storage_paths(id: &str) -> Result<(), Self::Error>; @@ -30,16 +31,6 @@ pub trait InitialisableClient { fn construct_config(init_args: &Self::InitArgs) -> Self::Config; } -pub trait ClientConfig { - fn common_paths(&self) -> &CommonClientPaths; - - fn core_config(&self) -> &crate::config::Config; - - fn default_store_location(&self) -> PathBuf; - - fn save_to>(&self, path: P) -> std::io::Result<()>; -} - #[cfg_attr(feature = "cli", derive(clap::Args))] #[derive(Debug, Clone)] pub struct CommonClientInitArgs { @@ -51,16 +42,15 @@ pub struct CommonClientInitArgs { #[cfg_attr(feature = "cli", clap(long))] pub gateway: Option, + /// Specifies whether the client will attempt to enforce tls connection to the desired gateway. + #[cfg_attr(feature = "cli", clap(long))] + pub force_tls_gateway: bool, + /// Specifies whether the new gateway should be determined based by latency as opposed to being chosen /// uniformly. #[cfg_attr(feature = "cli", clap(long, conflicts_with = "gateway"))] pub latency_based_selection: bool, - /// Force register gateway. WARNING: this will overwrite any existing keys for the given id, - /// potentially causing loss of access. - #[cfg_attr(feature = "cli", clap(long))] - pub force_register_gateway: bool, - /// Comma separated list of rest endpoints of the nyxd validators #[cfg_attr( feature = "cli", @@ -109,7 +99,7 @@ pub async fn initialise_client( ) -> Result, C::Error> where C: InitialisableClient, - ::Config: std::fmt::Debug, + ::Config: std::fmt::Debug, ::InitArgs: std::fmt::Debug, { info!("initialising {} client", C::NAME); @@ -117,28 +107,15 @@ where let common_args = init_args.as_ref(); let id = &common_args.id; - let already_init = if C::default_config_path(id).exists() { - // in case we're using old config, try to upgrade it - // (if we're using the current version, it's a no-op) - C::try_upgrade_outdated_config(id)?; + if C::default_config_path(id).exists() { eprintln!("{} client \"{id}\" was already initialised before", C::NAME); - true - } else { - C::initialise_storage_paths(id)?; - false - }; - - // Usually you only register with the gateway on the first init, however you can force - // re-registering if wanted. - let user_wants_force_register = common_args.force_register_gateway; - if user_wants_force_register { - eprintln!("Instructed to force registering gateway. This might overwrite keys!"); + return Err(ClientCoreError::AlreadyInitialised { + client_id: id.to_string(), + } + .into()); } - // If the client was already initialized, don't generate new keys and don't re-register with - // the gateway (because this would create a new shared key). - // Unless the user really wants to. - let register_gateway = !already_init || user_wants_force_register; + C::initialise_storage_paths(id)?; // Attempt to use a user-provided gateway, if possible let user_chosen_gateway_id = common_args.gateway; @@ -147,7 +124,7 @@ where let selection_spec = GatewaySelectionSpecification::new( user_chosen_gateway_id.map(|id| id.to_base58_string()), Some(common_args.latency_based_selection), - false, + common_args.force_tls_gateway, ); log::debug!("Gateway selection specification: {selection_spec:?}"); @@ -168,11 +145,14 @@ where .join(",") ); + let key_store = OnDiskKeys::new(paths.keys.clone()); + let details_store = setup_fs_gateways_storage(&paths.gateway_registrations).await?; + + let mut rng = OsRng; + crate::init::generate_new_client_keys(&mut rng, &key_store).await?; + // Setup gateway by either registering a new one, or creating a new config from the selected // one but with keys kept, or reusing the gateway configuration. - let key_store = OnDiskKeys::new(paths.keys.clone()); - let details_store = OnDiskGatewayDetails::new(&paths.gateway_details); - let available_gateways = if let Some(custom_mixnet) = common_args.custom_mixnet.as_ref() { let hardcoded_topology = NymTopology::new_from_file(custom_mixnet).map_err(|source| { ClientCoreError::CustomTopologyLoadFailure { @@ -189,14 +169,13 @@ where let gateway_setup = GatewaySetup::New { specification: selection_spec, available_gateways, - overwrite_data: register_gateway, + wg_tun_address: None, }; let init_details = crate::init::setup_gateway(gateway_setup, &key_store, &details_store).await?; // TODO: ask the service provider we specified for its interface version and set it in the config - let config_save_location = config.default_store_location(); if let Err(err) = config.save_to(&config_save_location) { return Err(ClientCoreError::ConfigSaveFailure { @@ -213,12 +192,20 @@ where config_save_location.display() ); - let address = init_details.client_address()?; + let address = init_details.client_address(); - let GatewayDetails::Configured(gateway_details) = init_details.gateway_details else { + let GatewayDetails::Remote(gateway_details) = init_details.gateway_registration.details else { return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails)?; }; - let init_results = InitResults::new(config.core_config(), address, &gateway_details); + + let init_results = InitResults::new( + config.core_config(), + address, + &gateway_details, + init_details.gateway_registration.registration_timestamp, + ); + + set_active_gateway(&details_store, &init_results.gateway_id).await?; Ok(InitResultsWithConfig { config, diff --git a/common/client-core/src/cli_helpers/client_list_gateways.rs b/common/client-core/src/cli_helpers/client_list_gateways.rs new file mode 100644 index 0000000000..1bfcafb027 --- /dev/null +++ b/common/client-core/src/cli_helpers/client_list_gateways.rs @@ -0,0 +1,74 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::types::GatewayInfo; +use crate::cli_helpers::{CliClient, CliClientConfig}; +use crate::client::base_client::non_wasm_helpers::setup_fs_gateways_storage; +use crate::client::base_client::storage::helpers::{ + get_active_gateway_identity, get_gateway_registrations, +}; +use nym_client_core_gateways_storage::{GatewayDetails, GatewayType}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[derive(Debug, Clone)] +pub struct CommonClientListGatewaysArgs { + /// Id of client we want to list gateways for. + #[cfg_attr(feature = "cli", clap(long))] + pub id: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(transparent)] +pub struct RegisteredGateways(Vec); + +impl Display for RegisteredGateways { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for (i, gateway) in self.0.iter().enumerate() { + writeln!(f, "[{i}]: {gateway}")?; + } + Ok(()) + } +} + +pub async fn list_gateways(args: A) -> Result +where + A: AsRef, + C: CliClient, +{ + let common_args = args.as_ref(); + let id = &common_args.id; + + let config = C::try_load_current_config(id).await?; + let paths = config.common_paths(); + + let details_store = setup_fs_gateways_storage(&paths.gateway_registrations).await?; + + let active_gateway = get_active_gateway_identity(&details_store).await?; + + let gateways = get_gateway_registrations(&details_store).await?; + let mut info = Vec::with_capacity(gateways.len()); + for gateway in gateways { + match gateway.details { + GatewayDetails::Remote(remote_details) => info.push(GatewayInfo { + registration: gateway.registration_timestamp, + identity: remote_details.gateway_id, + active: active_gateway == Some(remote_details.gateway_id), + typ: GatewayType::Remote.to_string(), + endpoint: Some(remote_details.gateway_listener), + wg_tun_address: remote_details.wg_tun_address, + }), + GatewayDetails::Custom(_) => info.push(GatewayInfo { + registration: gateway.registration_timestamp, + identity: gateway.details.gateway_id(), + active: active_gateway == Some(gateway.details.gateway_id()), + typ: gateway.details.typ().to_string(), + endpoint: None, + wg_tun_address: None, + }), + }; + } + + Ok(RegisteredGateways(info)) +} diff --git a/common/client-core/src/cli_helpers/client_switch_gateway.rs b/common/client-core/src/cli_helpers/client_switch_gateway.rs new file mode 100644 index 0000000000..0468575e63 --- /dev/null +++ b/common/client-core/src/cli_helpers/client_switch_gateway.rs @@ -0,0 +1,37 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_helpers::{CliClient, CliClientConfig}; +use crate::client::base_client::non_wasm_helpers::setup_fs_gateways_storage; +use crate::client::base_client::storage::helpers::set_active_gateway; +use nym_crypto::asymmetric::identity; + +#[cfg_attr(feature = "cli", derive(clap::Args))] +#[derive(Debug, Clone)] +pub struct CommonClientSwitchGatewaysArgs { + /// Id of client we want to list gateways for. + #[cfg_attr(feature = "cli", clap(long))] + pub id: String, + + /// Id of the gateway we want to switch to. + #[cfg_attr(feature = "cli", clap(long))] + pub gateway_id: identity::PublicKey, +} + +pub async fn switch_gateway(args: A) -> Result<(), C::Error> +where + A: AsRef, + C: CliClient, +{ + let common_args = args.as_ref(); + let id = &common_args.id; + + let config = C::try_load_current_config(id).await?; + let paths = config.common_paths(); + + let details_store = setup_fs_gateways_storage(&paths.gateway_registrations).await?; + + set_active_gateway(&details_store, &common_args.gateway_id.to_base58_string()).await?; + + Ok(()) +} diff --git a/common/client-core/src/cli_helpers/mod.rs b/common/client-core/src/cli_helpers/mod.rs index 874663b75e..5ab51b8b4c 100644 --- a/common/client-core/src/cli_helpers/mod.rs +++ b/common/client-core/src/cli_helpers/mod.rs @@ -1,5 +1,14 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod client_add_gateway; +pub mod client_import_credential; pub mod client_init; +pub mod client_list_gateways; pub mod client_run; +pub mod client_switch_gateway; +pub mod traits; +mod types; + +pub use client_init::InitialisableClient; +pub use traits::{CliClient, CliClientConfig}; diff --git a/common/client-core/src/cli_helpers/traits.rs b/common/client-core/src/cli_helpers/traits.rs new file mode 100644 index 0000000000..ec93cb09dd --- /dev/null +++ b/common/client-core/src/cli_helpers/traits.rs @@ -0,0 +1,28 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::disk_persistence::CommonClientPaths; +use crate::error::ClientCoreError; +use std::path::{Path, PathBuf}; + +// we can suppress this warning (as suggested by linter itself) since we're only using it in our own code +#[allow(async_fn_in_trait)] +pub trait CliClient { + const NAME: &'static str; + type Error: From; + type Config: CliClientConfig; + + async fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error>; + + async fn try_load_current_config(id: &str) -> Result; +} + +pub trait CliClientConfig { + fn common_paths(&self) -> &CommonClientPaths; + + fn core_config(&self) -> &crate::config::Config; + + fn default_store_location(&self) -> PathBuf; + + fn save_to>(&self, path: P) -> std::io::Result<()>; +} diff --git a/common/client-core/src/cli_helpers/types.rs b/common/client-core/src/cli_helpers/types.rs new file mode 100644 index 0000000000..4a351f38ef --- /dev/null +++ b/common/client-core/src/cli_helpers/types.rs @@ -0,0 +1,40 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::identity; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use time::OffsetDateTime; +use url::Url; + +#[derive(Serialize, Deserialize)] +pub struct GatewayInfo { + pub registration: OffsetDateTime, + pub identity: identity::PublicKey, + pub active: bool, + + pub typ: String, + pub endpoint: Option, + pub wg_tun_address: Option, +} + +impl Display for GatewayInfo { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.active { + write!(f, "[ACTIVE] ")?; + } + write!( + f, + "{} gateway '{}' registered at: {}", + self.typ, self.identity, self.registration + )?; + if let Some(endpoint) = &self.endpoint { + write!(f, " endpoint: {endpoint}")?; + } + + if let Some(wg_tun_address) = &self.wg_tun_address { + write!(f, " wg tun address: {wg_tun_address}")?; + } + Ok(()) + } +} diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index c2632f73ad..42adcf2da4 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -4,11 +4,12 @@ use super::packet_statistics_control::PacketStatisticsReporter; use super::received_buffer::ReceivedBufferMessage; use super::topology_control::geo_aware_provider::GeoAwareTopologyProvider; -use crate::client::base_client::storage::gateway_details::GatewayDetailsStore; +use crate::client::base_client::storage::helpers::store_client_keys; use crate::client::base_client::storage::MixnetClientStorage; use crate::client::cover_traffic_stream::LoopCoverTrafficStream; use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; use crate::client::key_manager::persistence::KeyStore; +use crate::client::key_manager::ClientKeys; use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway}; use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; use crate::client::packet_statistics_control::PacketStatisticsControl; @@ -30,17 +31,19 @@ use crate::config::{Config, DebugConfig}; use crate::error::ClientCoreError; use crate::init::{ setup_gateway, - types::{GatewayDetails, GatewaySetup, InitialisationResult}, + types::{GatewaySetup, InitialisationResult}, }; use crate::{config, spawn_future}; use futures::channel::mpsc; -use log::{debug, error, info}; +use log::{debug, error, info, warn}; use nym_bandwidth_controller::BandwidthController; +use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore}; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_crypto::asymmetric::encryption; use nym_gateway_client::{ - AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, + AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter, }; +use nym_network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, WG_TUN_DEVICE_ADDRESS}; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::nodes::NodeIdentity; @@ -51,13 +54,18 @@ use nym_task::{TaskClient, TaskHandle}; use nym_topology::provider_trait::TopologyProvider; use nym_topology::HardcodedTopologyProvider; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; +use rand::rngs::OsRng; use std::fmt::Debug; use std::os::raw::c_int as RawFd; use std::path::Path; use std::sync::Arc; use url::Url; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +#[cfg(all( + not(target_arch = "wasm32"), + feature = "fs-surb-storage", + feature = "fs-gateways-storage" +))] pub mod non_wasm_helpers; pub mod helpers; @@ -172,6 +180,7 @@ pub struct BaseClientBuilder<'a, C, S: MixnetClientStorage> { dkg_query_client: Option, wait_for_gateway: bool, + wireguard_connection: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, shutdown: Option, @@ -194,10 +203,11 @@ where client_store, dkg_query_client, wait_for_gateway: false, + wireguard_connection: false, custom_topology_provider: None, custom_gateway_transceiver: None, shutdown: None, - setup_method: GatewaySetup::MustLoad, + setup_method: GatewaySetup::MustLoad { gateway_id: None }, } } @@ -213,6 +223,12 @@ where self } + #[must_use] + pub fn with_wireguard_connection(mut self, wireguard_connection: bool) -> Self { + self.wireguard_connection = wireguard_connection; + self + } + #[must_use] pub fn with_topology_provider( mut self, @@ -246,13 +262,7 @@ where // note: do **NOT** make this method public as its only valid usage is from within `start_base` // because it relies on the crypto keys being already loaded fn mix_address(details: &InitialisationResult) -> Recipient { - Recipient::new( - *details.managed_keys.identity_public_key(), - *details.managed_keys.encryption_public_key(), - // TODO: below only works under assumption that gateway address == gateway id - // (which currently is true) - NodeIdentity::from_base58_string(details.gateway_details.gateway_id()).unwrap(), - ) + details.client_address() } // future constantly pumping loop cover traffic at some specified average rate @@ -342,6 +352,7 @@ where async fn start_gateway_client( config: &Config, + wireguard_connection: bool, initialisation_result: InitialisationResult, bandwidth_controller: Option>, packet_router: PacketRouter, @@ -351,50 +362,65 @@ where ::StorageError: Send + Sync + 'static, ::StorageError: Send + Sync + 'static, { - let managed_keys = initialisation_result.managed_keys; - let GatewayDetails::Configured(gateway_config) = initialisation_result.gateway_details + let managed_keys = initialisation_result.client_keys; + let GatewayDetails::Remote(details) = initialisation_result.gateway_registration.details else { return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails); }; - let mut gateway_client = - if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client { - existing_client.upgrade(packet_router, bandwidth_controller, shutdown) + let mut gateway_client = if let Some(existing_client) = + initialisation_result.authenticated_ephemeral_client + { + existing_client.upgrade(packet_router, bandwidth_controller, shutdown) + } else { + let gateway_listener = if wireguard_connection { + if let Some(tun_address) = details.wg_tun_address { + tun_address.to_string() + } else { + let default = + format!("ws://{WG_TUN_DEVICE_ADDRESS}:{DEFAULT_CLIENT_LISTENING_PORT}"); + warn!("gateway {} does not have tun device address set. defaulting to '{default}'", details.gateway_id); + default + } } else { - let cfg = gateway_config.try_into()?; - GatewayClient::new( - cfg, - managed_keys.identity_keypair(), - Some(managed_keys.must_get_gateway_shared_key()), - packet_router, - bandwidth_controller, - shutdown, - ) - .with_disabled_credentials_mode(config.client.disabled_credentials_mode) - .with_response_timeout(config.debug.gateway_connection.gateway_response_timeout) + details.gateway_listener.to_string() }; - let gateway_id = gateway_client.gateway_identity(); + let cfg = GatewayConfig::new( + details.gateway_id, + Some(details.gateway_owner_address.to_string()), + gateway_listener, + ); + GatewayClient::new( + cfg, + managed_keys.identity_keypair(), + Some(details.derived_aes128_ctr_blake3_hmac_keys), + packet_router, + bandwidth_controller, + shutdown, + ) + .with_disabled_credentials_mode(config.client.disabled_credentials_mode) + .with_response_timeout(config.debug.gateway_connection.gateway_response_timeout) + }; - let shared_key = gateway_client + gateway_client .authenticate_and_start() .await .map_err(|err| { log::error!("Could not authenticate and start up the gateway connection - {err}"); ClientCoreError::GatewayClientError { - gateway_id: gateway_id.to_base58_string(), + gateway_id: details.gateway_id.to_base58_string(), source: err, } })?; - managed_keys.ensure_gateway_key(Some(shared_key)); - Ok(gateway_client) } async fn setup_gateway_transceiver( custom_gateway_transceiver: Option>, config: &Config, + wireguard_connection: bool, initialisation_result: InitialisationResult, bandwidth_controller: Option>, packet_router: PacketRouter, @@ -406,7 +432,11 @@ where { // if we have setup custom gateway sender and persisted details agree with it, return it if let Some(mut custom_gateway_transceiver) = custom_gateway_transceiver { - return if !initialisation_result.gateway_details.is_custom() { + return if !initialisation_result + .gateway_registration + .details + .is_custom() + { Err(ClientCoreError::CustomGatewaySelectionExpected) } else { // and make sure to invalidate the task client so we wouldn't cause premature shutdown @@ -419,6 +449,7 @@ where // otherwise, setup normal gateway client, etc let gateway_client = Self::start_gateway_client( config, + wireguard_connection, initialisation_result, bandwidth_controller, packet_router, @@ -569,12 +600,20 @@ where async fn initialise_keys_and_gateway( setup_method: GatewaySetup, key_store: &S::KeyStore, - details_store: &S::GatewayDetailsStore, + details_store: &S::GatewaysDetailsStore, ) -> Result where ::StorageError: Sync + Send, - ::StorageError: Sync + Send, + ::StorageError: Sync + Send, { + // if client keys do not exist already, create and persist them + if key_store.load_keys().await.is_err() { + info!("could not find valid client keys - a new set will be generated"); + let mut rng = OsRng; + let keys = ClientKeys::generate_new(&mut rng); + store_client_keys(keys, key_store).await?; + } + setup_gateway(setup_method, key_store, details_store).await } @@ -584,7 +623,7 @@ where ::StorageError: Send + Sync, ::StorageError: Sync + Send, ::StorageError: Send + Sync + 'static, - ::StorageError: Sync + Send, + ::StorageError: Sync + Send, { info!("Starting nym client"); @@ -629,8 +668,8 @@ where reply_controller::requests::new_control_channels(); let self_address = Self::mix_address(&init_res); - let ack_key = init_res.managed_keys.ack_key(); - let encryption_keys = init_res.managed_keys.encryption_keypair(); + let ack_key = init_res.client_keys.ack_key(); + let encryption_keys = init_res.client_keys.encryption_keypair(); // the components are started in very specific order. Unless you know what you are doing, // do not change that. @@ -667,6 +706,7 @@ where let gateway_transceiver = Self::setup_gateway_transceiver( self.custom_gateway_transceiver, self.config, + self.wireguard_connection, init_res, bandwidth_controller, gateway_packet_router, diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index d0d2d5a058..ce3f05b0f2 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -1,4 +1,4 @@ -// Copyright 2022-2023 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::client::replies::reply_storage::{ @@ -7,8 +7,9 @@ use crate::client::replies::reply_storage::{ use crate::config; use crate::config::Config; use crate::error::ClientCoreError; -use log::{error, info}; +use log::{error, info, trace}; use nym_bandwidth_controller::BandwidthController; +use nym_client_core_gateways_storage::OnDiskGatewaysDetails; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_validator_client::nyxd; use nym_validator_client::QueryHttpRpcNyxdClient; @@ -101,6 +102,17 @@ pub async fn setup_fs_reply_surb_backend>( } } +pub async fn setup_fs_gateways_storage>( + db_path: P, +) -> Result { + trace!("setting up gateways details storage"); + OnDiskGatewaysDetails::init(db_path) + .await + .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + }) +} + pub fn create_bandwidth_controller( config: &Config, storage: St, diff --git a/common/client-core/src/client/base_client/storage/gateway_details.rs b/common/client-core/src/client/base_client/storage/gateway_details.rs deleted file mode 100644 index d9eaddfa87..0000000000 --- a/common/client-core/src/client/base_client/storage/gateway_details.rs +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::config::GatewayEndpointConfig; -use crate::error::ClientCoreError; -use crate::init::types::{EmptyCustomDetails, GatewayDetails}; -use async_trait::async_trait; -use log::error; -use nym_gateway_requests::registration::handshake::SharedKeys; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::error::Error; -use std::ops::Deref; -use tokio::sync::Mutex; -use zeroize::Zeroizing; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait GatewayDetailsStore { - type StorageError: Error; - - async fn load_gateway_details(&self) -> Result, Self::StorageError> - where - T: DeserializeOwned + Send + Sync; - - async fn store_gateway_details( - &self, - details: &PersistedGatewayDetails, - ) -> Result<(), Self::StorageError> - where - T: Serialize + Send + Sync; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(untagged)] -pub enum PersistedGatewayDetails { - /// Standard details of a remote gateway - Default(PersistedGatewayConfig), - - /// Custom gateway setup, such as for a client embedded inside gateway itself - Custom(PersistedCustomGatewayDetails), -} - -impl PersistedGatewayDetails { - // TODO: this should probably allow for custom verification over T - pub fn validate(&self, shared_key: Option<&SharedKeys>) -> Result<(), ClientCoreError> { - match self { - PersistedGatewayDetails::Default(details) => { - if !details.verify(shared_key.ok_or(ClientCoreError::UnavailableSharedKey)?) { - Err(ClientCoreError::MismatchedGatewayDetails { - gateway_id: details.details.gateway_id.clone(), - }) - } else { - Ok(()) - } - } - PersistedGatewayDetails::Custom(_) => { - if shared_key.is_some() { - error!("using custom persisted gateway setup with shared key present - are you sure that's what you want?"); - // but technically we could still continue. just ignore the key - } - Ok(()) - } - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct PersistedGatewayConfig { - // TODO: should we also verify correctness of the details themselves? - // i.e. we could include a checksum or tag (via the shared keys) - // counterargument: if we wanted to modify, say, the host information in the stored file on disk, - // in order to actually use it, we'd have to recompute the whole checksum which would be a huge pain. - /// The hash of the shared keys to ensure the correct ones are used with those gateway details. - #[serde(with = "base64")] - key_hash: Vec, - - /// Actual gateway details being persisted. - pub details: GatewayEndpointConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PersistedCustomGatewayDetails { - // whatever custom method is used, gateway's identity must be known - pub gateway_id: String, - - #[serde(flatten)] - pub additional_data: T, -} - -impl PersistedGatewayConfig { - pub fn new(details: GatewayEndpointConfig, shared_key: &SharedKeys) -> Self { - let key_bytes = Zeroizing::new(shared_key.to_bytes()); - - let mut key_hasher = Sha256::new(); - key_hasher.update(&key_bytes); - let key_hash = key_hasher.finalize().to_vec(); - - PersistedGatewayConfig { key_hash, details } - } - - pub fn verify(&self, shared_key: &SharedKeys) -> bool { - let key_bytes = Zeroizing::new(shared_key.to_bytes()); - - let mut key_hasher = Sha256::new(); - key_hasher.update(&key_bytes); - let key_hash = key_hasher.finalize(); - - self.key_hash == key_hash.deref() - } -} - -impl PersistedGatewayDetails { - pub fn new( - details: GatewayDetails, - shared_key: Option<&SharedKeys>, - ) -> Result { - match details { - GatewayDetails::Configured(cfg) => { - let shared_key = shared_key.ok_or(ClientCoreError::UnavailableSharedKey)?; - Ok(PersistedGatewayDetails::Default( - PersistedGatewayConfig::new(cfg, shared_key), - )) - } - GatewayDetails::Custom(custom) => Ok(PersistedGatewayDetails::Custom(custom.into())), - } - } - - pub fn is_custom(&self) -> bool { - matches!(self, PersistedGatewayDetails::Custom(..)) - } - - pub fn matches(&self, other: &GatewayDetails) -> bool - where - T: PartialEq, - { - match self { - PersistedGatewayDetails::Default(default) => { - if let GatewayDetails::Configured(other_configured) = other { - &default.details == other_configured - } else { - false - } - } - PersistedGatewayDetails::Custom(custom) => { - if let GatewayDetails::Custom(other_custom) = other { - custom.gateway_id == other_custom.gateway_id - && custom.additional_data == other_custom.additional_data - } else { - false - } - } - } - } -} - -// helper to make Vec serialization use base64 representation to make it human readable -// so that it would be easier for users to copy contents from the disk if they wanted to use it elsewhere -mod base64 { - use base64::{engine::general_purpose::STANDARD, Engine as _}; - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn serialize(bytes: &[u8], serializer: S) -> Result { - serializer.serialize_str(&STANDARD.encode(bytes)) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { - let s = ::deserialize(deserializer)?; - STANDARD.decode(s).map_err(serde::de::Error::custom) - } -} - -#[cfg(not(target_arch = "wasm32"))] -#[derive(Debug, thiserror::Error)] -pub enum OnDiskGatewayDetailsError { - #[error("JSON failure: {0}")] - SerializationFailure(#[from] serde_json::Error), - - #[error("failed to store gateway details to {path}: {err}")] - StoreFailure { - path: String, - #[source] - err: std::io::Error, - }, - - #[error("failed to load gateway details from {path}: {err}")] - LoadFailure { - path: String, - #[source] - err: std::io::Error, - }, -} - -#[cfg(not(target_arch = "wasm32"))] -pub struct OnDiskGatewayDetails { - file_location: std::path::PathBuf, -} - -#[cfg(not(target_arch = "wasm32"))] -impl OnDiskGatewayDetails { - pub fn new>(path: P) -> Self { - OnDiskGatewayDetails { - file_location: path.as_ref().to_owned(), - } - } - - pub fn load_from_disk(&self) -> Result, OnDiskGatewayDetailsError> - where - T: DeserializeOwned, - { - let file = std::fs::File::open(&self.file_location).map_err(|err| { - OnDiskGatewayDetailsError::LoadFailure { - path: self.file_location.display().to_string(), - err, - } - })?; - - Ok(serde_json::from_reader(file)?) - } - - pub fn store_to_disk( - &self, - details: &PersistedGatewayDetails, - ) -> Result<(), OnDiskGatewayDetailsError> - where - T: Serialize, - { - // ensure the whole directory structure exists - if let Some(parent_dir) = &self.file_location.parent() { - std::fs::create_dir_all(parent_dir).map_err(|err| { - OnDiskGatewayDetailsError::StoreFailure { - path: self.file_location.display().to_string(), - err, - } - })? - } - - let file = std::fs::File::create(&self.file_location).map_err(|err| { - OnDiskGatewayDetailsError::StoreFailure { - path: self.file_location.display().to_string(), - err, - } - })?; - - Ok(serde_json::to_writer_pretty(file, details)?) - } -} - -#[cfg(not(target_arch = "wasm32"))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl GatewayDetailsStore for OnDiskGatewayDetails { - type StorageError = OnDiskGatewayDetailsError; - - async fn load_gateway_details(&self) -> Result { - self.load_from_disk() - } - - async fn store_gateway_details( - &self, - gateway_details: &PersistedGatewayDetails, - ) -> Result<(), Self::StorageError> { - self.store_to_disk(gateway_details) - } -} - -#[derive(Default)] -pub struct InMemGatewayDetails { - details: Mutex>>, -} - -#[derive(Debug, thiserror::Error)] -#[error("old ephemeral gateway details can't be loaded from storage")] -pub struct EphemeralGatewayDetailsError; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl GatewayDetailsStore for InMemGatewayDetails { - type StorageError = EphemeralGatewayDetailsError; - - async fn load_gateway_details(&self) -> Result { - self.details - .lock() - .await - .clone() - .ok_or(EphemeralGatewayDetailsError) - } - - async fn store_gateway_details( - &self, - gateway_details: &PersistedGatewayDetails, - ) -> Result<(), Self::StorageError> { - *self.details.lock().await = Some(gateway_details.clone()); - Ok(()) - } -} diff --git a/common/client-core/src/client/base_client/storage/helpers.rs b/common/client-core/src/client/base_client/storage/helpers.rs new file mode 100644 index 0000000000..b2a200579d --- /dev/null +++ b/common/client-core/src/client/base_client/storage/helpers.rs @@ -0,0 +1,157 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::key_manager::persistence::KeyStore; +use crate::client::key_manager::ClientKeys; +use crate::error::ClientCoreError; +use nym_client_core_gateways_storage::{ActiveGateway, GatewayRegistration, GatewaysDetailsStore}; +use nym_crypto::asymmetric::identity; + +// helpers for error wrapping +pub async fn set_active_gateway( + details_store: &D, + gateway_id: &str, +) -> Result<(), ClientCoreError> +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store + .set_active_gateway(gateway_id) + .await + .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + }) +} + +pub async fn get_active_gateway_identity( + details_store: &D, +) -> Result, ClientCoreError> +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store + .active_gateway() + .await + .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + }) + .map(|a| a.registration.map(|r| r.details.gateway_id())) +} + +pub async fn get_all_registered_identities( + details_store: &D, +) -> Result, ClientCoreError> +where + D: GatewaysDetailsStore + Sync, + D::StorageError: Send + Sync + 'static, +{ + details_store + .all_gateways_identities() + .await + .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + }) +} + +pub async fn get_gateway_registrations( + details_store: &D, +) -> Result, ClientCoreError> +where + D: GatewaysDetailsStore + Sync, + D::StorageError: Send + Sync + 'static, +{ + details_store.all_gateways().await.map_err(|source| { + ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + } + }) +} + +pub async fn store_gateway_details( + details_store: &D, + details: &GatewayRegistration, +) -> Result<(), ClientCoreError> +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store + .store_gateway_details(details) + .await + .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + }) +} + +pub async fn load_active_gateway_details( + details_store: &D, +) -> Result +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store.active_gateway().await.map_err(|source| { + ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + } + }) +} + +pub async fn load_gateway_details( + details_store: &D, + gateway_id: &str, +) -> Result +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store + .load_gateway_details(gateway_id) + .await + .map_err(|source| ClientCoreError::UnavailableGatewayDetails { + gateway_id: gateway_id.to_string(), + source: Box::new(source), + }) +} + +pub async fn has_gateway_details( + details_store: &D, + gateway_id: &str, +) -> Result +where + D: GatewaysDetailsStore, + D::StorageError: Send + Sync + 'static, +{ + details_store + .has_gateway_details(gateway_id) + .await + .map_err(|source| ClientCoreError::GatewaysDetailsStoreError { + source: Box::new(source), + }) +} + +pub async fn load_client_keys(key_store: &K) -> Result +where + K: KeyStore, + K::StorageError: Send + Sync + 'static, +{ + ClientKeys::load_keys(key_store) + .await + .map_err(|source| ClientCoreError::KeyStoreError { + source: Box::new(source), + }) +} + +pub async fn store_client_keys(keys: ClientKeys, key_store: &K) -> Result<(), ClientCoreError> +where + K: KeyStore, + K::StorageError: Send + Sync + 'static, +{ + keys.persist_keys(key_store) + .await + .map_err(|source| ClientCoreError::KeyStoreError { + source: Box::new(source), + }) +} diff --git a/common/client-core/src/client/base_client/storage/migration_helpers.rs b/common/client-core/src/client/base_client/storage/migration_helpers.rs new file mode 100644 index 0000000000..87738faef4 --- /dev/null +++ b/common/client-core/src/client/base_client/storage/migration_helpers.rs @@ -0,0 +1,212 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod v1_1_33 { + use crate::client::base_client::{ + non_wasm_helpers::setup_fs_gateways_storage, + storage::helpers::{set_active_gateway, store_gateway_details}, + }; + use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; + use crate::config::disk_persistence::CommonClientPaths; + use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; + use crate::error::ClientCoreError; + use nym_client_core_gateways_storage::{ + CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails, + }; + use nym_gateway_requests::registration::handshake::SharedKeys; + use serde::{Deserialize, Serialize}; + use sha2::{digest::Digest, Sha256}; + use std::ops::Deref; + use std::path::Path; + use std::sync::Arc; + use zeroize::Zeroizing; + + mod base64 { + use base64::{engine::general_purpose::STANDARD, Engine as _}; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&STANDARD.encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + let s = ::deserialize(deserializer)?; + STANDARD.decode(s).map_err(serde::de::Error::custom) + } + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + #[serde(untagged)] + enum PersistedGatewayDetails { + /// Standard details of a remote gateway + Default(PersistedGatewayConfig), + + /// Custom gateway setup, such as for a client embedded inside gateway itself + Custom(PersistedCustomGatewayDetails), + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + struct PersistedGatewayConfig { + /// The hash of the shared keys to ensure the correct ones are used with those gateway details. + #[serde(with = "base64")] + key_hash: Vec, + + /// Actual gateway details being persisted. + details: OldGatewayEndpointConfigV1_1_33, + } + + impl PersistedGatewayConfig { + fn verify(&self, shared_key: &SharedKeys) -> bool { + let key_bytes = Zeroizing::new(shared_key.to_bytes()); + + let mut key_hasher = Sha256::new(); + key_hasher.update(&key_bytes); + let key_hash = key_hasher.finalize(); + + self.key_hash == key_hash.deref() + } + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + struct PersistedCustomGatewayDetails { + gateway_id: String, + } + + fn load_shared_key>(path: P) -> Result { + // the shared key was a simple pem file + Ok(nym_pemstore::load_key(path)?) + } + + fn gateway_details_from_raw( + gateway_id: String, + gateway_owner: String, + gateway_listener: String, + gateway_shared_key: SharedKeys, + ) -> Result { + Ok(GatewayDetails::Remote(RemoteGatewayDetails { + gateway_id: gateway_id + .parse() + .map_err(|err| ClientCoreError::UpgradeFailure { + message: format!("the stored gateway id was malformed: {err}"), + })?, + derived_aes128_ctr_blake3_hmac_keys: Arc::new(gateway_shared_key), + gateway_owner_address: gateway_owner.parse().map_err(|err| { + ClientCoreError::UpgradeFailure { + message: format!("the stored gateway owner address was malformed: {err}"), + } + })?, + gateway_listener: gateway_listener.parse().map_err(|err| { + ClientCoreError::UpgradeFailure { + message: format!("the stored gateway listener address was malformed: {err}"), + } + })?, + wg_tun_address: None, + })) + } + + // helper to extract shared key and gateway details into the new GatewayRegistration + fn extract_gateway_registration( + storage_paths: &CommonClientPathsV1_1_33, + ) -> Result { + let details_file = std::fs::File::open(&storage_paths.gateway_details).map_err(|err| { + ClientCoreError::UpgradeFailure { + message: format!( + "failed to open gateway details file at {}: {err}", + storage_paths.gateway_details.display() + ), + } + })?; + + // in v1.1.33 of the clients, the gateway details struct was saved as json + let details: PersistedGatewayDetails = + serde_json::from_reader(details_file).map_err(|err| { + ClientCoreError::UpgradeFailure { + message: format!( + "failed to deserialize gateway details from {}: {err}", + storage_paths.gateway_details.display() + ), + } + })?; + + let details = match details { + PersistedGatewayDetails::Default(config) => { + let gateway_shared_key = + load_shared_key(&storage_paths.keys.gateway_shared_key_file)?; + if !config.verify(&gateway_shared_key) { + return Err(ClientCoreError::UpgradeFailure { + message: "failed to verify consistency of the existing gateway details" + .to_string(), + }); + } + gateway_details_from_raw( + config.details.gateway_id, + config.details.gateway_owner, + config.details.gateway_listener, + gateway_shared_key, + )? + } + PersistedGatewayDetails::Custom(custom) => { + GatewayDetails::Custom(CustomGatewayDetails { + gateway_id: custom.gateway_id.parse().map_err(|err| { + ClientCoreError::UpgradeFailure { + message: format!("the stored gateway id was malformed: {err}"), + } + })?, + data: None, + }) + } + }; + + Ok(details.into()) + } + + // it's responsibility of the caller to ensure this is called **after** new registration has already been saved + fn remove_old_gateway_details(storage_paths: &CommonClientPathsV1_1_33) -> std::io::Result<()> { + std::fs::remove_file(&storage_paths.gateway_details)?; + + if storage_paths.keys.gateway_shared_key_file.exists() { + std::fs::remove_file(&storage_paths.keys.gateway_shared_key_file)?; + } + Ok(()) + } + + pub async fn migrate_gateway_details( + old_storage_paths: &CommonClientPathsV1_1_33, + new_storage_paths: &CommonClientPaths, + preloaded_config: Option, + ) -> Result<(), ClientCoreError> { + let gateway_registration = match preloaded_config { + Some(config) => { + let gateway_shared_key = + load_shared_key(&old_storage_paths.keys.gateway_shared_key_file)?; + gateway_details_from_raw( + config.gateway_id, + config.gateway_owner, + config.gateway_listener, + gateway_shared_key, + )? + .into() + } + None => extract_gateway_registration(old_storage_paths)?, + }; + + // since we're migrating to a brand new store, the store should be empty + // and thus set the 'new' gateway as the active one + let details_store = + setup_fs_gateways_storage(&new_storage_paths.gateway_registrations).await?; + store_gateway_details(&details_store, &gateway_registration).await?; + set_active_gateway( + &details_store, + &gateway_registration.details.gateway_id().to_base58_string(), + ) + .await?; + + remove_old_gateway_details(old_storage_paths).map_err(|err| { + ClientCoreError::UpgradeFailure { + message: format!("failed to remove old data: {err}"), + } + }) + } +} diff --git a/common/client-core/src/client/base_client/storage/mod.rs b/common/client-core/src/client/base_client/storage/mod.rs index 60e3ff9a73..e7c15315a6 100644 --- a/common/client-core/src/client/base_client/storage/mod.rs +++ b/common/client-core/src/client/base_client/storage/mod.rs @@ -4,46 +4,57 @@ // TODO: combine those more closely. Perhaps into a single underlying store. // Like for persistent, on-disk, storage, what's the point of having 3 different databases? -use crate::client::base_client::storage::gateway_details::{ - GatewayDetailsStore, InMemGatewayDetails, -}; use crate::client::key_manager::persistence::{InMemEphemeralKeys, KeyStore}; use crate::client::replies::reply_storage; use crate::client::replies::reply_storage::ReplyStorageBackend; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; use nym_credential_storage::storage::Storage as CredentialStorage; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] -use crate::client::base_client::non_wasm_helpers; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] -use crate::client::base_client::storage::gateway_details::OnDiskGatewayDetails; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] -use crate::client::key_manager::persistence::OnDiskKeys; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] -use crate::client::replies::reply_storage::fs_backend; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] -use crate::config::{self, disk_persistence::CommonClientPaths}; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] -use crate::error::ClientCoreError; +#[cfg(all( + not(target_arch = "wasm32"), + feature = "fs-surb-storage", + feature = "fs-gateways-storage" +))] +use crate::{ + client::{ + base_client::non_wasm_helpers, key_manager::persistence::OnDiskKeys, + replies::reply_storage::fs_backend, + }, + config::{self, disk_persistence::CommonClientPaths}, + error::ClientCoreError, +}; #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage; -pub mod gateway_details; +pub use nym_client_core_gateways_storage as gateways_storage; +pub use nym_client_core_gateways_storage::{GatewaysDetailsStore, InMemGatewaysDetails}; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-gateways-storage"))] +pub use nym_client_core_gateways_storage::{OnDiskGatewaysDetails, StorageError}; + +pub mod helpers; + +#[cfg(all( + not(target_arch = "wasm32"), + feature = "fs-surb-storage", + feature = "fs-gateways-storage" +))] +pub mod migration_helpers; // TODO: ideally this should be changed into -// `MixnetClientStorage: KeyStore + ReplyStorageBackend + CredentialStorage + GatewayDetailsStore` +// `MixnetClientStorage: KeyStore + ReplyStorageBackend + CredentialStorage + GatewaysDetailsStore` pub trait MixnetClientStorage { type KeyStore: KeyStore; type ReplyStore: ReplyStorageBackend; type CredentialStore: CredentialStorage; - type GatewayDetailsStore: GatewayDetailsStore; + type GatewaysDetailsStore: GatewaysDetailsStore; fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore); fn key_store(&self) -> &Self::KeyStore; fn reply_store(&self) -> &Self::ReplyStore; fn credential_store(&self) -> &Self::CredentialStore; - fn gateway_details_store(&self) -> &Self::GatewayDetailsStore; + fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore; } #[derive(Default)] @@ -51,7 +62,7 @@ pub struct Ephemeral { key_store: InMemEphemeralKeys, reply_store: reply_storage::Empty, credential_store: EphemeralCredentialStorage, - gateway_details_store: InMemGatewayDetails, + gateway_details_store: InMemGatewaysDetails, } impl Ephemeral { @@ -64,7 +75,7 @@ impl MixnetClientStorage for Ephemeral { type KeyStore = InMemEphemeralKeys; type ReplyStore = reply_storage::Empty; type CredentialStore = EphemeralCredentialStorage; - type GatewayDetailsStore = InMemGatewayDetails; + type GatewaysDetailsStore = InMemGatewaysDetails; fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { (self.reply_store, self.credential_store) @@ -82,26 +93,34 @@ impl MixnetClientStorage for Ephemeral { &self.credential_store } - fn gateway_details_store(&self) -> &Self::GatewayDetailsStore { + fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore { &self.gateway_details_store } } -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +#[cfg(all( + not(target_arch = "wasm32"), + feature = "fs-surb-storage", + feature = "fs-gateways-storage" +))] pub struct OnDiskPersistent { pub(crate) key_store: OnDiskKeys, pub(crate) reply_store: fs_backend::Backend, pub(crate) credential_store: PersistentCredentialStorage, - pub(crate) gateway_details_store: OnDiskGatewayDetails, + pub(crate) gateway_details_store: OnDiskGatewaysDetails, } -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +#[cfg(all( + not(target_arch = "wasm32"), + feature = "fs-surb-storage", + feature = "fs-gateways-storage" +))] impl OnDiskPersistent { pub fn new( key_store: OnDiskKeys, reply_store: fs_backend::Backend, credential_store: PersistentCredentialStorage, - gateway_details_store: OnDiskGatewayDetails, + gateway_details_store: OnDiskGatewaysDetails, ) -> Self { Self { key_store, @@ -126,7 +145,8 @@ impl OnDiskPersistent { let credential_store = nym_credential_storage::initialise_persistent_storage(paths.credentials_database).await; - let gateway_details_store = OnDiskGatewayDetails::new(paths.gateway_details); + let gateway_details_store = + non_wasm_helpers::setup_fs_gateways_storage(paths.gateway_registrations).await?; Ok(OnDiskPersistent { key_store, @@ -137,12 +157,16 @@ impl OnDiskPersistent { } } -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +#[cfg(all( + not(target_arch = "wasm32"), + feature = "fs-surb-storage", + feature = "fs-gateways-storage" +))] impl MixnetClientStorage for OnDiskPersistent { type KeyStore = OnDiskKeys; type ReplyStore = fs_backend::Backend; type CredentialStore = PersistentCredentialStorage; - type GatewayDetailsStore = OnDiskGatewayDetails; + type GatewaysDetailsStore = OnDiskGatewaysDetails; fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { (self.reply_store, self.credential_store) @@ -160,7 +184,7 @@ impl MixnetClientStorage for OnDiskPersistent { &self.credential_store } - fn gateway_details_store(&self) -> &Self::GatewayDetailsStore { + fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore { &self.gateway_details_store } } diff --git a/common/client-core/src/client/key_manager/mod.rs b/common/client-core/src/client/key_manager/mod.rs index f01f8e2fbb..095980e753 100644 --- a/common/client-core/src/client/key_manager/mod.rs +++ b/common/client-core/src/client/key_manager/mod.rs @@ -6,225 +6,11 @@ use nym_crypto::asymmetric::{encryption, identity}; use nym_gateway_requests::registration::handshake::SharedKeys; use nym_sphinx::acknowledgements::AckKey; use rand::{CryptoRng, RngCore}; -use std::fmt::{Debug, Formatter}; use std::sync::Arc; use zeroize::ZeroizeOnDrop; pub mod persistence; -pub enum ManagedKeys { - Initial(KeyManagerBuilder), - FullyDerived(KeyManager), - - // I really hate the existence of this variant, but I couldn't come up with a better way to handle - // `Self::deal_with_gateway_key` otherwise. - Invalidated, -} - -impl Debug for ManagedKeys { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ManagedKeys::Initial(_) => write!(f, "initial"), - ManagedKeys::FullyDerived(_) => write!(f, "fully derived"), - ManagedKeys::Invalidated => write!(f, "invalidated"), - } - } -} - -impl From for ManagedKeys { - fn from(value: KeyManagerBuilder) -> Self { - ManagedKeys::Initial(value) - } -} - -impl From for ManagedKeys { - fn from(value: KeyManager) -> Self { - ManagedKeys::FullyDerived(value) - } -} - -impl ManagedKeys { - pub fn is_valid(&self) -> bool { - !matches!(self, ManagedKeys::Invalidated) - } - - pub async fn try_load(key_store: &S) -> Result { - Ok(ManagedKeys::FullyDerived( - KeyManager::load_keys(key_store).await?, - )) - } - - pub fn generate_new(rng: &mut R) -> Self - where - R: RngCore + CryptoRng, - { - ManagedKeys::Initial(KeyManagerBuilder::new(rng)) - } - - pub async fn load_or_generate(rng: &mut R, key_store: &S) -> Self - where - R: RngCore + CryptoRng, - S: KeyStore, - { - Self::try_load(key_store) - .await - .unwrap_or_else(|_| Self::generate_new(rng)) - } - - pub fn identity_keypair(&self) -> Arc { - match self { - ManagedKeys::Initial(keys) => keys.identity_keypair(), - ManagedKeys::FullyDerived(keys) => keys.identity_keypair(), - ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), - } - } - - pub fn encryption_keypair(&self) -> Arc { - match self { - ManagedKeys::Initial(keys) => keys.encryption_keypair(), - ManagedKeys::FullyDerived(keys) => keys.encryption_keypair(), - ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), - } - } - - pub fn ack_key(&self) -> Arc { - match self { - ManagedKeys::Initial(keys) => keys.ack_key(), - ManagedKeys::FullyDerived(keys) => keys.ack_key(), - ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), - } - } - - pub fn must_get_gateway_shared_key(&self) -> Arc { - self.gateway_shared_key() - .expect("failed to extract gateway shared key") - } - - pub fn gateway_shared_key(&self) -> Option> { - match self { - ManagedKeys::Initial(_) => None, - ManagedKeys::FullyDerived(keys) => keys.gateway_shared_key(), - ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), - } - } - - pub fn identity_public_key(&self) -> &identity::PublicKey { - match self { - ManagedKeys::Initial(keys) => keys.identity_keypair.public_key(), - ManagedKeys::FullyDerived(keys) => keys.identity_keypair.public_key(), - ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), - } - } - - pub fn encryption_public_key(&self) -> &encryption::PublicKey { - match self { - ManagedKeys::Initial(keys) => keys.encryption_keypair.public_key(), - ManagedKeys::FullyDerived(keys) => keys.encryption_keypair.public_key(), - ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), - } - } - - pub fn ensure_gateway_key(&self, gateway_shared_key: Option>) { - if let ManagedKeys::FullyDerived(key_manager) = &self { - if self.gateway_shared_key().is_none() && gateway_shared_key.is_none() { - // the key doesn't exist in either state - return; - } - - if gateway_shared_key.is_some() && self.gateway_shared_key().is_none() - || gateway_shared_key.is_none() && self.gateway_shared_key().is_some() - { - // if one is provided whilst the other is not... - // TODO: should this actually panic or return an error? would this branch be possible - // under normal operation? - panic!("inconsistent re-derived gateway key") - } - - // here we know both keys MUST exist - let provided = gateway_shared_key.unwrap(); - if !Arc::ptr_eq(key_manager.must_get_gateway_shared_key(), &provided) - || *key_manager.must_get_gateway_shared_key() != provided - { - // this should NEVER happen thus panic here - panic!("derived fresh gateway shared key whilst already holding one!") - } - } - } - - pub async fn deal_with_gateway_key( - &mut self, - gateway_shared_key: Option>, - key_store: &S, - ) -> Result<(), S::StorageError> { - let key_manager = match std::mem::replace(self, ManagedKeys::Invalidated) { - ManagedKeys::Initial(keys) => { - let key_manager = keys.insert_maybe_gateway_shared_key(gateway_shared_key); - key_manager.persist_keys(key_store).await?; - key_manager - } - ManagedKeys::FullyDerived(key_manager) => { - self.ensure_gateway_key(gateway_shared_key); - key_manager - } - ManagedKeys::Invalidated => unreachable!("the managed keys got invalidated"), - }; - - *self = ManagedKeys::FullyDerived(key_manager); - Ok(()) - } -} - -// all of the keys really shouldn't be wrapped in `Arc`, but due to how the gateway client is currently -// constructed, changing that would require more work than what it's worth -pub struct KeyManagerBuilder { - /// identity key associated with the client instance. - identity_keypair: Arc, - - /// encryption key associated with the client instance. - encryption_keypair: Arc, - - /// key used for producing and processing acknowledgement packets. - ack_key: Arc, -} - -impl KeyManagerBuilder { - /// Creates new instance of a [`KeyManager`] - pub fn new(rng: &mut R) -> Self - where - R: RngCore + CryptoRng, - { - KeyManagerBuilder { - identity_keypair: Arc::new(identity::KeyPair::new(rng)), - encryption_keypair: Arc::new(encryption::KeyPair::new(rng)), - ack_key: Arc::new(AckKey::new(rng)), - } - } - - pub fn insert_maybe_gateway_shared_key( - self, - gateway_shared_key: Option>, - ) -> KeyManager { - KeyManager { - identity_keypair: self.identity_keypair, - encryption_keypair: self.encryption_keypair, - gateway_shared_key, - ack_key: self.ack_key, - } - } - - pub fn identity_keypair(&self) -> Arc { - Arc::clone(&self.identity_keypair) - } - - pub fn encryption_keypair(&self) -> Arc { - Arc::clone(&self.encryption_keypair) - } - - pub fn ack_key(&self) -> Arc { - Arc::clone(&self.ack_key) - } -} - // Note: to support key rotation in the future, all keys will require adding an extra smart pointer, // most likely an AtomicCell, or if it doesn't work as I think it does, a Mutex. Although I think // AtomicCell includes a Mutex implicitly if the underlying type does not work atomically. @@ -233,35 +19,38 @@ impl KeyManagerBuilder { // Remember that Arc has Deref implementation for T #[derive(Clone)] -pub struct KeyManager { +pub struct ClientKeys { /// identity key associated with the client instance. identity_keypair: Arc, /// encryption key associated with the client instance. encryption_keypair: Arc, - /// shared key derived with the gateway during "registration handshake" - // I'm not a fan of how we broke the nice transition of `KeyManagerBuilder` -> `KeyManager` - // by making this field optional. - // However, it has to be optional for when we use embedded NR inside a gateway, - // since it won't have a shared key (because why would it?) - gateway_shared_key: Option>, - /// key used for producing and processing acknowledgement packets. ack_key: Arc, } -impl KeyManager { +impl ClientKeys { + /// Creates new instance of a [`ClientKeys`] + pub fn generate_new(rng: &mut R) -> Self + where + R: RngCore + CryptoRng, + { + ClientKeys { + identity_keypair: Arc::new(identity::KeyPair::new(rng)), + encryption_keypair: Arc::new(encryption::KeyPair::new(rng)), + ack_key: Arc::new(AckKey::new(rng)), + } + } + pub fn from_keys( id_keypair: identity::KeyPair, enc_keypair: encryption::KeyPair, - gateway_shared_key: Option, ack_key: AckKey, ) -> Self { Self { identity_keypair: Arc::new(id_keypair), encryption_keypair: Arc::new(enc_keypair), - gateway_shared_key: gateway_shared_key.map(Arc::new), ack_key: Arc::new(ack_key), } } @@ -287,32 +76,6 @@ impl KeyManager { pub fn ack_key(&self) -> Arc { Arc::clone(&self.ack_key) } - - fn must_get_gateway_shared_key(&self) -> &Arc { - self.gateway_shared_key - .as_ref() - .expect("gateway shared key is unavailable") - } - - pub fn uses_custom_gateway(&self) -> bool { - self.gateway_shared_key.is_none() - } - - /// Gets an atomically reference counted pointer to [`SharedKey`]. - pub fn gateway_shared_key(&self) -> Option> { - self.gateway_shared_key.clone() - } - - pub fn remove_gateway_key(self) -> KeyManagerBuilder { - if Arc::strong_count(self.must_get_gateway_shared_key()) > 1 { - panic!("attempted to remove gateway key whilst still holding multiple references!") - } - KeyManagerBuilder { - identity_keypair: self.identity_keypair, - encryption_keypair: self.encryption_keypair, - ack_key: self.ack_key, - } - } } fn _assert_keys_zeroize_on_drop() { diff --git a/common/client-core/src/client/key_manager/persistence.rs b/common/client-core/src/client/key_manager/persistence.rs index 984019eb93..a4711e3d39 100644 --- a/common/client-core/src/client/key_manager/persistence.rs +++ b/common/client-core/src/client/key_manager/persistence.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::key_manager::KeyManager; +use crate::client::key_manager::ClientKeys; use async_trait::async_trait; use std::error::Error; use tokio::sync::Mutex; @@ -11,8 +11,6 @@ use crate::config::disk_persistence::keys_paths::ClientKeysPaths; #[cfg(not(target_arch = "wasm32"))] use nym_crypto::asymmetric::{encryption, identity}; #[cfg(not(target_arch = "wasm32"))] -use nym_gateway_requests::registration::handshake::SharedKeys; -#[cfg(not(target_arch = "wasm32"))] use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; #[cfg(not(target_arch = "wasm32"))] use nym_pemstore::KeyPairPath; @@ -25,9 +23,9 @@ use nym_sphinx::acknowledgements::AckKey; pub trait KeyStore { type StorageError: Error; - async fn load_keys(&self) -> Result; + async fn load_keys(&self) -> Result; - async fn store_keys(&self, keys: &KeyManager) -> Result<(), Self::StorageError>; + async fn store_keys(&self, keys: &ClientKeys) -> Result<(), Self::StorageError>; } #[cfg(not(target_arch = "wasm32"))] @@ -84,14 +82,6 @@ impl OnDiskKeys { OnDiskKeys { paths } } - #[doc(hidden)] - pub fn ephemeral_load_gateway_keys( - &self, - ) -> Result, OnDiskKeysError> { - self.load_key(self.paths.gateway_shared_key(), "gateway shared") - .map(zeroize::Zeroizing::new) - } - #[doc(hidden)] pub fn load_encryption_keypair(&self) -> Result { let encryption_paths = self.paths.encryption_key_pair_path(); @@ -156,26 +146,19 @@ impl OnDiskKeys { }) } - fn load_keys(&self) -> Result { + fn load_keys(&self) -> Result { let identity_keypair = self.load_identity_keypair()?; let encryption_keypair = self.load_encryption_keypair()?; - let ack_key: AckKey = self.load_key(self.paths.ack_key(), "ack key")?; - let gateway_shared_key: Option = self - .load_key(self.paths.gateway_shared_key(), "gateway shared keys") - .ok(); - Ok(KeyManager::from_keys( + Ok(ClientKeys::from_keys( identity_keypair, encryption_keypair, - gateway_shared_key, ack_key, )) } - fn store_keys(&self, keys: &KeyManager) -> Result<(), OnDiskKeysError> { - use std::ops::Deref; - + fn store_keys(&self, keys: &ClientKeys) -> Result<(), OnDiskKeysError> { let identity_paths = self.paths.identity_key_pair_path(); let encryption_paths = self.paths.encryption_key_pair_path(); @@ -192,14 +175,6 @@ impl OnDiskKeys { self.store_key(keys.ack_key.as_ref(), self.paths.ack_key(), "ack key")?; - if let Some(shared_keys) = &keys.gateway_shared_key { - self.store_key( - shared_keys.deref(), - self.paths.gateway_shared_key(), - "gateway shared keys", - )?; - } - Ok(()) } } @@ -209,18 +184,18 @@ impl OnDiskKeys { impl KeyStore for OnDiskKeys { type StorageError = OnDiskKeysError; - async fn load_keys(&self) -> Result { + async fn load_keys(&self) -> Result { self.load_keys() } - async fn store_keys(&self, keys: &KeyManager) -> Result<(), Self::StorageError> { + async fn store_keys(&self, keys: &ClientKeys) -> Result<(), Self::StorageError> { self.store_keys(keys) } } #[derive(Default)] pub struct InMemEphemeralKeys { - keys: Mutex>, + keys: Mutex>, } #[derive(Debug, thiserror::Error)] @@ -232,11 +207,11 @@ pub struct EphemeralKeysError; impl KeyStore for InMemEphemeralKeys { type StorageError = EphemeralKeysError; - async fn load_keys(&self) -> Result { + async fn load_keys(&self) -> Result { self.keys.lock().await.clone().ok_or(EphemeralKeysError) } - async fn store_keys(&self, keys: &KeyManager) -> Result<(), Self::StorageError> { + async fn store_keys(&self, keys: &ClientKeys) -> Result<(), Self::StorageError> { *self.keys.lock().await = Some(keys.clone()); Ok(()) } diff --git a/common/client-core/src/client/replies/mod.rs b/common/client-core/src/client/replies/mod.rs index 61b5f19abf..7f425dbd38 100644 --- a/common/client-core/src/client/replies/mod.rs +++ b/common/client-core/src/client/replies/mod.rs @@ -2,4 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 pub mod reply_controller; -pub mod reply_storage; + +// re-export it under the old name to preserve import paths +pub use nym_client_core_surb_storage as reply_storage; diff --git a/common/client-core/src/config/disk_persistence/keys_paths.rs b/common/client-core/src/config/disk_persistence/keys_paths.rs index aeab0359ea..cdffebe1e2 100644 --- a/common/client-core/src/config/disk_persistence/keys_paths.rs +++ b/common/client-core/src/config/disk_persistence/keys_paths.rs @@ -8,7 +8,6 @@ pub const DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME: &str = "private_identity.pem"; pub const DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME: &str = "public_identity.pem"; pub const DEFAULT_PRIVATE_ENCRYPTION_KEY_FILENAME: &str = "private_encryption.pem"; pub const DEFAULT_PUBLIC_ENCRYPTION_KEY_FILENAME: &str = "public_encryption.pem"; -pub const DEFAULT_GATEWAY_SHARED_KEY_FILENAME: &str = "gateway_shared.pem"; pub const DEFAULT_ACK_KEY_FILENAME: &str = "ack_key.pem"; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] @@ -25,10 +24,6 @@ pub struct ClientKeysPaths { /// Path to file containing public encryption key. pub public_encryption_key_file: PathBuf, - /// Path to file containing shared key derived with the specified gateway that is used - /// for all communication with it. - pub gateway_shared_key_file: PathBuf, - /// Path to file containing key used for encrypting and decrypting the content of an /// acknowledgement so that nobody besides the client knows which packet it refers to. pub ack_key_file: PathBuf, @@ -43,7 +38,6 @@ impl ClientKeysPaths { public_identity_key_file: base_dir.join(DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME), private_encryption_key_file: base_dir.join(DEFAULT_PRIVATE_ENCRYPTION_KEY_FILENAME), public_encryption_key_file: base_dir.join(DEFAULT_PUBLIC_ENCRYPTION_KEY_FILENAME), - gateway_shared_key_file: base_dir.join(DEFAULT_GATEWAY_SHARED_KEY_FILENAME), ack_key_file: base_dir.join(DEFAULT_ACK_KEY_FILENAME), } } @@ -67,7 +61,6 @@ impl ClientKeysPaths { || matches!(self.private_identity_key_file.try_exists(), Ok(true)) || matches!(self.public_encryption_key_file.try_exists(), Ok(true)) || matches!(self.private_encryption_key_file.try_exists(), Ok(true)) - || matches!(self.gateway_shared_key_file.try_exists(), Ok(true)) || matches!(self.ack_key_file.try_exists(), Ok(true)) } @@ -76,14 +69,9 @@ impl ClientKeysPaths { .or_else(|| file_exists(&self.private_identity_key_file)) .or_else(|| file_exists(&self.public_encryption_key_file)) .or_else(|| file_exists(&self.private_encryption_key_file)) - .or_else(|| file_exists(&self.gateway_shared_key_file)) .or_else(|| file_exists(&self.ack_key_file)) } - pub fn gateway_key_file_exists(&self) -> bool { - matches!(self.gateway_shared_key_file.try_exists(), Ok(true)) - } - pub fn private_identity_key(&self) -> &Path { &self.private_identity_key_file } @@ -100,10 +88,6 @@ impl ClientKeysPaths { &self.public_encryption_key_file } - pub fn gateway_shared_key(&self) -> &Path { - &self.gateway_shared_key_file - } - pub fn ack_key(&self) -> &Path { &self.ack_key_file } diff --git a/common/client-core/src/config/disk_persistence/mod.rs b/common/client-core/src/config/disk_persistence/mod.rs index b323d059dc..c086696a4b 100644 --- a/common/client-core/src/config/disk_persistence/mod.rs +++ b/common/client-core/src/config/disk_persistence/mod.rs @@ -7,19 +7,20 @@ use std::path::{Path, PathBuf}; pub mod keys_paths; pub mod old_v1_1_20_2; +pub mod old_v1_1_33; -pub const DEFAULT_GATEWAY_DETAILS_FILENAME: &str = "gateway_details.json"; pub const DEFAULT_REPLY_SURB_DB_FILENAME: &str = "persistent_reply_store.sqlite"; pub const DEFAULT_CREDENTIALS_DB_FILENAME: &str = "credentials_database.db"; +pub const DEFAULT_GATEWAYS_DETAILS_DB_FILENAME: &str = "gateways_registrations.sqlite"; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct CommonClientPaths { pub keys: ClientKeysPaths, - /// Path to the file containing information about gateway used by this client, - /// i.e. details such as its public key, owner address or the network information. - pub gateway_details: PathBuf, + /// Path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + pub gateway_registrations: PathBuf, /// Path to the database containing bandwidth credentials of this client. pub credentials_database: PathBuf, @@ -35,7 +36,7 @@ impl CommonClientPaths { CommonClientPaths { credentials_database: base_dir.join(DEFAULT_CREDENTIALS_DB_FILENAME), reply_surb_database: base_dir.join(DEFAULT_REPLY_SURB_DB_FILENAME), - gateway_details: base_dir.join(DEFAULT_GATEWAY_DETAILS_FILENAME), + gateway_registrations: base_dir.join(DEFAULT_GATEWAYS_DETAILS_DB_FILENAME), keys: ClientKeysPaths::new_base(base_data_directory), } } diff --git a/common/client-core/src/config/disk_persistence/old_v1_1_20_2.rs b/common/client-core/src/config/disk_persistence/old_v1_1_20_2.rs index d26cf1461c..e1863bb567 100644 --- a/common/client-core/src/config/disk_persistence/old_v1_1_20_2.rs +++ b/common/client-core/src/config/disk_persistence/old_v1_1_20_2.rs @@ -1,8 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::disk_persistence::keys_paths::ClientKeysPaths; -use crate::config::disk_persistence::{CommonClientPaths, DEFAULT_GATEWAY_DETAILS_FILENAME}; +use crate::config::disk_persistence::old_v1_1_33::{ + ClientKeysPathsV1_1_33, CommonClientPathsV1_1_33, DEFAULT_GATEWAY_DETAILS_FILENAME, +}; use crate::error::ClientCoreError; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -10,19 +11,19 @@ use std::path::PathBuf; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct CommonClientPathsV1_1_20_2 { - pub keys: ClientKeysPaths, + pub keys: ClientKeysPathsV1_1_33, pub credentials_database: PathBuf, pub reply_surb_database: PathBuf, } impl CommonClientPathsV1_1_20_2 { - pub fn upgrade_default(self) -> Result { + pub fn upgrade_default(self) -> Result { let data_dir = self.reply_surb_database.parent().ok_or_else(|| { - ClientCoreError::UnableToUpgradeConfigFile { - new_version: "1.1.20-2".to_string(), + ClientCoreError::ConfigFileUpgradeFailure { + current_version: "1.1.20-2".to_string(), } })?; - Ok(CommonClientPaths { + Ok(CommonClientPathsV1_1_33 { keys: self.keys, gateway_details: data_dir.join(DEFAULT_GATEWAY_DETAILS_FILENAME), credentials_database: self.credentials_database, diff --git a/common/client-core/src/config/disk_persistence/old_v1_1_33.rs b/common/client-core/src/config/disk_persistence/old_v1_1_33.rs new file mode 100644 index 0000000000..f453b5e01a --- /dev/null +++ b/common/client-core/src/config/disk_persistence/old_v1_1_33.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::disk_persistence::keys_paths::ClientKeysPaths; +use crate::config::disk_persistence::{CommonClientPaths, DEFAULT_GATEWAYS_DETAILS_DB_FILENAME}; +use crate::error::ClientCoreError; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +pub const DEFAULT_GATEWAY_DETAILS_FILENAME: &str = "gateway_details.json"; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +pub struct ClientKeysPathsV1_1_33 { + /// Path to file containing private identity key. + pub private_identity_key_file: PathBuf, + + /// Path to file containing public identity key. + pub public_identity_key_file: PathBuf, + + /// Path to file containing private encryption key. + pub private_encryption_key_file: PathBuf, + + /// Path to file containing public encryption key. + pub public_encryption_key_file: PathBuf, + + /// Path to file containing shared key derived with the specified gateway that is used + /// for all communication with it. + pub gateway_shared_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, +} + +impl ClientKeysPathsV1_1_33 { + pub fn upgrade(self) -> ClientKeysPaths { + ClientKeysPaths { + private_identity_key_file: self.private_identity_key_file, + public_identity_key_file: self.public_identity_key_file, + private_encryption_key_file: self.private_encryption_key_file, + public_encryption_key_file: self.public_encryption_key_file, + ack_key_file: self.ack_key_file, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct CommonClientPathsV1_1_33 { + pub keys: ClientKeysPathsV1_1_33, + + /// Path to the file containing information about gateway used by this client, + /// i.e. details such as its public key, owner address or the network information. + pub gateway_details: PathBuf, + + /// Path to the database containing bandwidth credentials of this client. + pub credentials_database: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, +} + +impl CommonClientPathsV1_1_33 { + // note that during the upgrade process, the caller will need to extract the key and gateway details + // manually and resave them in the new database + pub fn upgrade_default(self) -> Result { + let data_dir = self.gateway_details.parent().ok_or_else(|| { + ClientCoreError::ConfigFileUpgradeFailure { + current_version: "1.1.33".to_string(), + } + })?; + + Ok(CommonClientPaths { + keys: self.keys.upgrade(), + gateway_registrations: data_dir.join(DEFAULT_GATEWAYS_DETAILS_DB_FILENAME), + credentials_database: self.credentials_database, + reply_surb_database: self.reply_surb_database, + }) + } +} diff --git a/common/client-core/src/config/mod.rs b/common/client-core/src/config/mod.rs index 3f4bbb4efe..2333c58df4 100644 --- a/common/client-core/src/config/mod.rs +++ b/common/client-core/src/config/mod.rs @@ -1,10 +1,8 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{client::topology_control::geo_aware_provider::CountryGroup, error::ClientCoreError}; +use crate::client::topology_control::geo_aware_provider::CountryGroup; use nym_config::defaults::NymNetworkDetails; -use nym_crypto::asymmetric::identity; -use nym_gateway_client::client::GatewayConfig; use nym_sphinx::{ addressing::clients::Recipient, params::{PacketSize, PacketType}, @@ -13,14 +11,12 @@ use serde::{Deserialize, Serialize}; use std::time::Duration; use url::Url; -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; - pub mod disk_persistence; pub mod old_config_v1_1_13; pub mod old_config_v1_1_20; pub mod old_config_v1_1_20_2; pub mod old_config_v1_1_30; +pub mod old_config_v1_1_33; // 'DEBUG' const DEFAULT_ACK_WAIT_MULTIPLIER: f64 = 1.5; @@ -231,77 +227,6 @@ impl Config { } } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] -#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] -pub struct GatewayEndpointConfig { - /// gateway_id specifies ID of the gateway to which the client should send messages. - /// If initially omitted, a random gateway will be chosen from the available topology. - pub gateway_id: String, - - /// Address of the gateway owner to which the client should send messages. - pub gateway_owner: String, - - /// Address of the gateway listener to which all client requests should be sent. - pub gateway_listener: String, -} - -impl TryFrom for GatewayConfig { - type Error = ClientCoreError; - - fn try_from(value: GatewayEndpointConfig) -> Result { - Ok(GatewayConfig { - gateway_identity: identity::PublicKey::from_base58_string(value.gateway_id) - .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?, - gateway_owner: Some(value.gateway_owner), - gateway_listener: value.gateway_listener, - }) - } -} - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -impl GatewayEndpointConfig { - #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] - pub fn new( - gateway_id: String, - gateway_owner: String, - gateway_listener: String, - ) -> GatewayEndpointConfig { - GatewayEndpointConfig { - gateway_id, - gateway_owner, - gateway_listener, - } - } -} - -// separate block so it wouldn't be exported via wasm bindgen -impl GatewayEndpointConfig { - pub fn try_get_gateway_identity_key(&self) -> Result { - identity::PublicKey::from_base58_string(&self.gateway_id) - .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId) - } - - pub fn from_node( - node: nym_topology::gateway::Node, - must_use_tls: bool, - ) -> Result { - let gateway_listener = if must_use_tls { - node.clients_address_tls() - .ok_or(ClientCoreError::UnsupportedWssProtocol { - gateway: node.identity_key.to_base58_string(), - })? - } else { - node.clients_address() - }; - - Ok(GatewayEndpointConfig { - gateway_id: node.identity_key.to_base58_string(), - gateway_listener, - gateway_owner: node.owner, - }) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] // note: the deny_unknown_fields is VITAL here to allow upgrades from v1.1.20_2 #[serde(deny_unknown_fields)] diff --git a/common/client-core/src/config/old_config_v1_1_20_2.rs b/common/client-core/src/config/old_config_v1_1_20_2.rs index a7abd41ddc..5fe039fc82 100644 --- a/common/client-core/src/config/old_config_v1_1_20_2.rs +++ b/common/client-core/src/config/old_config_v1_1_20_2.rs @@ -5,7 +5,7 @@ use crate::config::old_config_v1_1_30::{ AcknowledgementsV1_1_30, ClientV1_1_30, ConfigV1_1_30, CoverTrafficV1_1_30, DebugConfigV1_1_30, GatewayConnectionV1_1_30, ReplySurbsV1_1_30, TopologyV1_1_30, TrafficV1_1_30, }; -use crate::config::GatewayEndpointConfig; +use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; use nym_sphinx::params::{PacketSize, PacketType}; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -81,9 +81,9 @@ pub struct GatewayEndpointConfigV1_1_20_2 { pub gateway_listener: String, } -impl From for GatewayEndpointConfig { +impl From for OldGatewayEndpointConfigV1_1_33 { fn from(value: GatewayEndpointConfigV1_1_20_2) -> Self { - GatewayEndpointConfig { + OldGatewayEndpointConfigV1_1_33 { gateway_id: value.gateway_id, gateway_owner: value.gateway_owner, gateway_listener: value.gateway_listener, diff --git a/common/client-core/src/config/old_config_v1_1_30.rs b/common/client-core/src/config/old_config_v1_1_30.rs index 87dae0eb3f..17aeba6bfd 100644 --- a/common/client-core/src/config/old_config_v1_1_30.rs +++ b/common/client-core/src/config/old_config_v1_1_30.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::topology_control::geo_aware_provider::CountryGroup; -use crate::config::{ - Acknowledgements, Client, Config, CoverTraffic, DebugConfig, GatewayConnection, GroupBy, - ReplySurbs, Topology, TopologyStructure, Traffic, +use crate::config::old_config_v1_1_33::{ + AcknowledgementsV1_1_33, ClientV1_1_33, ConfigV1_1_33, CoverTrafficV1_1_33, DebugConfigV1_1_33, + GatewayConnectionV1_1_33, GroupByV1_1_33, ReplySurbsV1_1_33, TopologyStructureV1_1_33, + TopologyV1_1_33, TrafficV1_1_33, }; use nym_sphinx::{ addressing::clients::Recipient, @@ -64,18 +65,18 @@ pub struct ConfigV1_1_30 { pub debug: DebugConfigV1_1_30, } -impl From for Config { +impl From for ConfigV1_1_33 { fn from(value: ConfigV1_1_30) -> Self { - Config { - client: Client { + ConfigV1_1_33 { + client: ClientV1_1_33 { version: value.client.version, id: value.client.id, disabled_credentials_mode: value.client.disabled_credentials_mode, nyxd_urls: value.client.nyxd_urls, nym_api_urls: value.client.nym_api_urls, }, - debug: DebugConfig { - traffic: Traffic { + debug: DebugConfigV1_1_33 { + traffic: TrafficV1_1_33 { average_packet_delay: value.debug.traffic.average_packet_delay, message_sending_average_delay: value .debug @@ -89,7 +90,7 @@ impl From for Config { secondary_packet_size: value.debug.traffic.secondary_packet_size, packet_type: value.debug.traffic.packet_type, }, - cover_traffic: CoverTraffic { + cover_traffic: CoverTrafficV1_1_33 { loop_cover_traffic_average_delay: value .debug .cover_traffic @@ -103,18 +104,18 @@ impl From for Config { .cover_traffic .disable_loop_cover_traffic_stream, }, - gateway_connection: GatewayConnection { + gateway_connection: GatewayConnectionV1_1_33 { gateway_response_timeout: value .debug .gateway_connection .gateway_response_timeout, }, - acknowledgements: Acknowledgements { + acknowledgements: AcknowledgementsV1_1_33 { average_ack_delay: value.debug.acknowledgements.average_ack_delay, ack_wait_multiplier: value.debug.acknowledgements.ack_wait_multiplier, ack_wait_addition: value.debug.acknowledgements.ack_wait_addition, }, - topology: Topology { + topology: TopologyV1_1_33 { topology_refresh_rate: value.debug.topology.topology_refresh_rate, topology_resolution_timeout: value.debug.topology.topology_resolution_timeout, disable_refreshing: value.debug.topology.disable_refreshing, @@ -124,7 +125,7 @@ impl From for Config { .max_startup_gateway_waiting_period, topology_structure: value.debug.topology.topology_structure.into(), }, - reply_surbs: ReplySurbs { + reply_surbs: ReplySurbsV1_1_33 { minimum_reply_surb_storage_threshold: value .debug .reply_surbs @@ -155,7 +156,10 @@ impl From for Config { .maximum_reply_surb_drop_waiting_period, maximum_reply_surb_age: value.debug.reply_surbs.maximum_reply_surb_age, maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age, + + // \/ ADDED surb_mix_hops: None, + // /\ ADDED }, }, } @@ -345,12 +349,12 @@ pub enum TopologyStructureV1_1_30 { GeoAware(GroupByV1_1_30), } -impl From for TopologyStructure { +impl From for TopologyStructureV1_1_33 { fn from(value: TopologyStructureV1_1_30) -> Self { match value { - TopologyStructureV1_1_30::NymApi => TopologyStructure::NymApi, + TopologyStructureV1_1_30::NymApi => TopologyStructureV1_1_33::NymApi, TopologyStructureV1_1_30::GeoAware(group_by) => { - TopologyStructure::GeoAware(group_by.into()) + TopologyStructureV1_1_33::GeoAware(group_by.into()) } } } @@ -363,11 +367,11 @@ pub enum GroupByV1_1_30 { NymAddress(Recipient), } -impl From for GroupBy { +impl From for GroupByV1_1_33 { fn from(value: GroupByV1_1_30) -> Self { match value { - GroupByV1_1_30::CountryGroup(country) => GroupBy::CountryGroup(country), - GroupByV1_1_30::NymAddress(addr) => GroupBy::NymAddress(addr), + GroupByV1_1_30::CountryGroup(country) => GroupByV1_1_33::CountryGroup(country), + GroupByV1_1_30::NymAddress(addr) => GroupByV1_1_33::NymAddress(addr), } } } diff --git a/common/client-core/src/config/old_config_v1_1_33.rs b/common/client-core/src/config/old_config_v1_1_33.rs new file mode 100644 index 0000000000..028b35fb52 --- /dev/null +++ b/common/client-core/src/config/old_config_v1_1_33.rs @@ -0,0 +1,491 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::topology_control::geo_aware_provider::CountryGroup; +use crate::config::{ + Acknowledgements, Client, Config, CoverTraffic, DebugConfig, GatewayConnection, GroupBy, + ReplySurbs, Topology, TopologyStructure, Traffic, +}; +use nym_sphinx::{ + addressing::clients::Recipient, + params::{PacketSize, PacketType}, +}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use url::Url; + +// 'DEBUG' +const DEFAULT_ACK_WAIT_MULTIPLIER: f64 = 1.5; + +const DEFAULT_ACK_WAIT_ADDITION: Duration = Duration::from_millis(1_500); +const DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(200); +const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: Duration = Duration::from_millis(20); +const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(50); +const DEFAULT_TOPOLOGY_REFRESH_RATE: Duration = Duration::from_secs(5 * 60); // every 5min +const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_000); +const DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead + +// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause +// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the +// bandwidth bridging protocol, we can come back to a smaller timeout value +const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); + +const DEFAULT_COVER_TRAFFIC_PRIMARY_SIZE_RATIO: f64 = 0.70; + +// reply-surbs related: + +// define when to request +// clients/client-core/src/client/replies/reply_storage/surb_storage.rs +const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200; + +// define how much to request at once +// clients/client-core/src/client/replies/reply_controller.rs +const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100; + +const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; + +const DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD: Duration = Duration::from_secs(10); +const DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD: Duration = Duration::from_secs(5 * 60); + +// 12 hours +const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60); + +// 24 hours +const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60); + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +pub struct OldGatewayEndpointConfigV1_1_33 { + /// gateway_id specifies ID of the gateway to which the client should send messages. + /// If initially omitted, a random gateway will be chosen from the available topology. + pub gateway_id: String, + + /// Address of the gateway owner to which the client should send messages. + pub gateway_owner: String, + + /// Address of the gateway listener to which all client requests should be sent. + pub gateway_listener: String, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_33 { + pub client: ClientV1_1_33, + + #[serde(default)] + pub debug: DebugConfigV1_1_33, +} + +impl From for Config { + fn from(value: ConfigV1_1_33) -> Self { + Config { + client: Client { + version: value.client.version, + id: value.client.id, + disabled_credentials_mode: value.client.disabled_credentials_mode, + nyxd_urls: value.client.nyxd_urls, + nym_api_urls: value.client.nym_api_urls, + }, + debug: DebugConfig { + traffic: Traffic { + average_packet_delay: value.debug.traffic.average_packet_delay, + message_sending_average_delay: value + .debug + .traffic + .message_sending_average_delay, + disable_main_poisson_packet_distribution: value + .debug + .traffic + .disable_main_poisson_packet_distribution, + primary_packet_size: value.debug.traffic.primary_packet_size, + secondary_packet_size: value.debug.traffic.secondary_packet_size, + packet_type: value.debug.traffic.packet_type, + }, + cover_traffic: CoverTraffic { + loop_cover_traffic_average_delay: value + .debug + .cover_traffic + .loop_cover_traffic_average_delay, + cover_traffic_primary_size_ratio: value + .debug + .cover_traffic + .cover_traffic_primary_size_ratio, + disable_loop_cover_traffic_stream: value + .debug + .cover_traffic + .disable_loop_cover_traffic_stream, + }, + gateway_connection: GatewayConnection { + gateway_response_timeout: value + .debug + .gateway_connection + .gateway_response_timeout, + }, + acknowledgements: Acknowledgements { + average_ack_delay: value.debug.acknowledgements.average_ack_delay, + ack_wait_multiplier: value.debug.acknowledgements.ack_wait_multiplier, + ack_wait_addition: value.debug.acknowledgements.ack_wait_addition, + }, + topology: Topology { + topology_refresh_rate: value.debug.topology.topology_refresh_rate, + topology_resolution_timeout: value.debug.topology.topology_resolution_timeout, + disable_refreshing: value.debug.topology.disable_refreshing, + max_startup_gateway_waiting_period: value + .debug + .topology + .max_startup_gateway_waiting_period, + topology_structure: value.debug.topology.topology_structure.into(), + }, + reply_surbs: ReplySurbs { + minimum_reply_surb_storage_threshold: value + .debug + .reply_surbs + .minimum_reply_surb_storage_threshold, + maximum_reply_surb_storage_threshold: value + .debug + .reply_surbs + .maximum_reply_surb_storage_threshold, + minimum_reply_surb_request_size: value + .debug + .reply_surbs + .minimum_reply_surb_request_size, + maximum_reply_surb_request_size: value + .debug + .reply_surbs + .maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: value + .debug + .reply_surbs + .maximum_allowed_reply_surb_request_size, + maximum_reply_surb_rerequest_waiting_period: value + .debug + .reply_surbs + .maximum_reply_surb_rerequest_waiting_period, + maximum_reply_surb_drop_waiting_period: value + .debug + .reply_surbs + .maximum_reply_surb_drop_waiting_period, + maximum_reply_surb_age: value.debug.reply_surbs.maximum_reply_surb_age, + maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age, + surb_mix_hops: value.debug.reply_surbs.surb_mix_hops, + }, + }, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +// note: the deny_unknown_fields is VITAL here to allow upgrades from v1.1.20_2 +#[serde(deny_unknown_fields)] +pub struct ClientV1_1_33 { + /// Version of the client for which this configuration was created. + pub version: String, + + /// ID specifies the human readable ID of this particular client. + pub id: String, + + /// Indicates whether this client is running in a disabled credentials mode, thus attempting + /// to claim bandwidth without presenting bandwidth credentials. + // TODO: this should be moved to `debug.gateway_connection` + #[serde(default)] + pub disabled_credentials_mode: bool, + + /// Addresses to nyxd validators via which the client can communicate with the chain. + #[serde(alias = "validator_urls")] + pub nyxd_urls: Vec, + + /// Addresses to APIs running on validator from which the client gets the view of the network. + #[serde(alias = "validator_api_urls")] + pub nym_api_urls: Vec, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct TrafficV1_1_33 { + /// The parameter of Poisson distribution determining how long, on average, + /// sent packet is going to be delayed at any given mix node. + /// So for a packet going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + #[serde(with = "humantime_serde")] + pub average_packet_delay: Duration, + + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take another 'real traffic stream' message to be sent. + /// If no real packets are available and cover traffic is enabled, + /// a loop cover message is sent instead in order to preserve the rate. + #[serde(with = "humantime_serde")] + pub message_sending_average_delay: Duration, + + /// Controls whether the main packet stream constantly produces packets according to the predefined + /// poisson distribution. + pub disable_main_poisson_packet_distribution: bool, + + /// Specifies the packet size used for sent messages. + /// Do not override it unless you understand the consequences of that change. + pub primary_packet_size: PacketSize, + + /// Specifies the optional auxiliary packet size for optimizing message streams. + /// Note that its use decreases overall anonymity. + /// Do not set it it unless you understand the consequences of that change. + pub secondary_packet_size: Option, + + pub packet_type: PacketType, +} + +impl Default for TrafficV1_1_33 { + fn default() -> Self { + TrafficV1_1_33 { + average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, + message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, + disable_main_poisson_packet_distribution: false, + primary_packet_size: PacketSize::RegularPacket, + secondary_packet_size: None, + packet_type: PacketType::Mix, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct CoverTrafficV1_1_33 { + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take for another loop cover traffic message to be sent. + #[serde(with = "humantime_serde")] + pub loop_cover_traffic_average_delay: Duration, + + /// Specifies the ratio of `primary_packet_size` to `secondary_packet_size` used in cover traffic. + /// Only applicable if `secondary_packet_size` is enabled. + pub cover_traffic_primary_size_ratio: f64, + + /// Controls whether the dedicated loop cover traffic stream should be enabled. + /// (and sending packets, on average, every [Self::loop_cover_traffic_average_delay]) + pub disable_loop_cover_traffic_stream: bool, +} + +impl Default for CoverTrafficV1_1_33 { + fn default() -> Self { + CoverTrafficV1_1_33 { + loop_cover_traffic_average_delay: DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY, + cover_traffic_primary_size_ratio: DEFAULT_COVER_TRAFFIC_PRIMARY_SIZE_RATIO, + disable_loop_cover_traffic_stream: false, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct GatewayConnectionV1_1_33 { + /// How long we're willing to wait for a response to a message sent to the gateway, + /// before giving up on it. + #[serde(with = "humantime_serde")] + pub gateway_response_timeout: Duration, +} + +impl Default for GatewayConnectionV1_1_33 { + fn default() -> Self { + GatewayConnectionV1_1_33 { + gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct AcknowledgementsV1_1_33 { + /// The parameter of Poisson distribution determining how long, on average, + /// sent acknowledgement is going to be delayed at any given mix node. + /// So for an ack going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + #[serde(with = "humantime_serde")] + pub average_ack_delay: Duration, + + /// Value multiplied with the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 1. + pub ack_wait_multiplier: f64, + + /// Value added to the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 0. + #[serde(with = "humantime_serde")] + pub ack_wait_addition: Duration, +} + +impl Default for AcknowledgementsV1_1_33 { + fn default() -> Self { + AcknowledgementsV1_1_33 { + average_ack_delay: DEFAULT_AVERAGE_PACKET_DELAY, + ack_wait_multiplier: DEFAULT_ACK_WAIT_MULTIPLIER, + ack_wait_addition: DEFAULT_ACK_WAIT_ADDITION, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct TopologyV1_1_33 { + /// The uniform delay every which clients are querying the directory server + /// to try to obtain a compatible network topology to send sphinx packets through. + #[serde(with = "humantime_serde")] + pub topology_refresh_rate: Duration, + + /// During topology refresh, test packets are sent through every single possible network + /// path. This timeout determines waiting period until it is decided that the packet + /// did not reach its destination. + #[serde(with = "humantime_serde")] + pub topology_resolution_timeout: Duration, + + /// Specifies whether the client should not refresh the network topology after obtaining + /// the first valid instance. + /// Supersedes `topology_refresh_rate_ms`. + pub disable_refreshing: bool, + + /// Defines how long the client is going to wait on startup for its gateway to come online, + /// before abandoning the procedure. + #[serde(with = "humantime_serde")] + pub max_startup_gateway_waiting_period: Duration, + + /// Specifies the mixnode topology to be used for sending packets. + pub topology_structure: TopologyStructureV1_1_33, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Default, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum TopologyStructureV1_1_33 { + #[default] + NymApi, + GeoAware(GroupByV1_1_33), +} + +impl From for TopologyStructure { + fn from(value: TopologyStructureV1_1_33) -> Self { + match value { + TopologyStructureV1_1_33::NymApi => TopologyStructure::NymApi, + TopologyStructureV1_1_33::GeoAware(group_by) => { + TopologyStructure::GeoAware(group_by.into()) + } + } + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum GroupByV1_1_33 { + CountryGroup(CountryGroup), + NymAddress(Recipient), +} + +impl From for GroupBy { + fn from(value: GroupByV1_1_33) -> Self { + match value { + GroupByV1_1_33::CountryGroup(country) => GroupBy::CountryGroup(country), + GroupByV1_1_33::NymAddress(addr) => GroupBy::NymAddress(addr), + } + } +} + +impl std::fmt::Display for GroupByV1_1_33 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GroupByV1_1_33::CountryGroup(group) => write!(f, "group: {}", group), + GroupByV1_1_33::NymAddress(address) => write!(f, "address: {}", address), + } + } +} + +impl Default for TopologyV1_1_33 { + fn default() -> Self { + TopologyV1_1_33 { + topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, + topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, + disable_refreshing: false, + max_startup_gateway_waiting_period: DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD, + topology_structure: TopologyStructureV1_1_33::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct ReplySurbsV1_1_33 { + /// Defines the minimum number of reply surbs the client wants to keep in its storage at all times. + /// It can only allow to go below that value if its to request additional reply surbs. + pub minimum_reply_surb_storage_threshold: usize, + + /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. + pub maximum_reply_surb_storage_threshold: usize, + + /// Defines the minimum number of reply surbs the client would request. + pub minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + pub maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + pub maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_rerequest_waiting_period: Duration, + + /// Defines maximum amount of time the client is going to wait for reply surbs before + /// deciding it's never going to get them and would drop all pending messages + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_drop_waiting_period: Duration, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_age: Duration, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_key_age: Duration, + + /// Specifies the number of mixnet hops the packet should go through. If not specified, then + /// the default value is used. + pub surb_mix_hops: Option, +} + +impl Default for ReplySurbsV1_1_33 { + fn default() -> Self { + ReplySurbsV1_1_33 { + minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD, + maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD, + minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE, + maximum_reply_surb_rerequest_waiting_period: + DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD, + maximum_reply_surb_drop_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD, + maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE, + maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE, + surb_mix_hops: None, + } + } +} + +#[derive(Debug, Default, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct DebugConfigV1_1_33 { + /// Defines all configuration options related to traffic streams. + pub traffic: TrafficV1_1_33, + + /// Defines all configuration options related to cover traffic stream(s). + pub cover_traffic: CoverTrafficV1_1_33, + + /// Defines all configuration options related to the gateway connection. + pub gateway_connection: GatewayConnectionV1_1_33, + + /// Defines all configuration options related to acknowledgements, such as delays or wait timeouts. + pub acknowledgements: AcknowledgementsV1_1_33, + + /// Defines all configuration options related topology, such as refresh rates or timeouts. + pub topology: TopologyV1_1_33, + + /// Defines all configuration options related to reply SURBs. + pub reply_surbs: ReplySurbsV1_1_33, +} diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index e4496e461a..bdfecfda5e 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -39,6 +39,9 @@ pub enum ClientCoreError { #[error("no gateways on network")] NoGatewaysOnNetwork, + #[error("there are no more new gateways on the network - it seems this client has already registered with all nodes it could have")] + NoNewGatewaysAvailable, + #[error("list of nym apis is empty")] ListOfNymApisIsEmpty, @@ -55,8 +58,8 @@ pub enum ClientCoreError { source: Box, }, - #[error("experienced a failure with our gateway details storage: {source}")] - GatewayDetailsStoreError { + #[error("experienced a failure with our gateways details storage: {source}")] + GatewaysDetailsStoreError { source: Box, }, @@ -94,13 +97,21 @@ pub enum ClientCoreError { #[error("unexpected exit")] UnexpectedExit, + #[error("this operation would have resulted in the gateway {gateway_id:?} key being overwritten without permission")] + ForbiddenGatewayKeyOverwrite { gateway_id: String }, + #[error( "this operation would have resulted in clients keys being overwritten without permission" )] ForbiddenKeyOverwrite, - #[error("gateway details are unavailable")] + #[error("the client doesn't have any gateway set as active")] + NoActiveGatewaySet, + + #[error("gateway details for gateway {gateway_id:?} are unavailable")] UnavailableGatewayDetails { + gateway_id: String, + #[source] source: Box, }, @@ -113,9 +124,15 @@ pub enum ClientCoreError { #[error("the provided gateway details (for gateway {gateway_id}) do not correspond to the shared keys")] MismatchedGatewayDetails { gateway_id: String }, + #[error("unable to upgrade config file from `{current_version}`")] + ConfigFileUpgradeFailure { current_version: String }, + #[error("unable to upgrade config file to `{new_version}`")] UnableToUpgradeConfigFile { new_version: String }, + #[error("failed to upgrade config file: {message}")] + UpgradeFailure { message: String }, + #[error("the provided gateway details don't much the stored data")] MismatchedStoredGatewayDetails, @@ -153,6 +170,42 @@ pub enum ClientCoreError { #[source] source: std::io::Error, }, + + #[error("the provided gateway identity {gateway_id} is malformed: {source}")] + MalformedGatewayIdentity { + gateway_id: String, + + #[source] + source: Ed25519RecoveryError, + }, + + #[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {err}")] + MalformedGatewayOwnerAccountAddress { + gateway_id: String, + + raw_owner: String, + + // just use the string formatting as opposed to underlying type to avoid having to import cosmrs + err: String, + }, + + #[error( + "the listening address of gateway {gateway_id} ({raw_listener}) is malformed: {source}" + )] + MalformedListener { + gateway_id: String, + + raw_listener: String, + + #[source] + source: url::ParseError, + }, + + #[error("this client (id: '{client_id}') has already been initialised before. If you want to add additional gateway, use `add-gateway` command")] + AlreadyInitialised { client_id: String }, + + #[error("this client has already registered with gateway {gateway_id}")] + AlreadyRegistered { gateway_id: String }, } /// Set of messages that the client can send to listeners via the task manager diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 06c5c8ef59..4d15327634 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -1,7 +1,6 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::GatewayEndpointConfig; use crate::error::ClientCoreError; use crate::init::types::RegistrationResult; use futures::{SinkExt, StreamExt}; @@ -9,6 +8,7 @@ use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; use nym_gateway_client::GatewayClient; use nym_topology::{filter::VersionFilterable, gateway, mix}; +use nym_validator_client::client::IdentityKeyRef; use rand::{seq::SliceRandom, Rng}; use std::{sync::Arc, time::Duration}; use tungstenite::Message; @@ -17,16 +17,13 @@ use url::Url; #[cfg(not(target_arch = "wasm32"))] use tokio::net::TcpStream; #[cfg(not(target_arch = "wasm32"))] +use tokio::time::sleep; +#[cfg(not(target_arch = "wasm32"))] use tokio::time::Instant; #[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::connect_async; #[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; -#[cfg(not(target_arch = "wasm32"))] -type WsConn = WebSocketStream>; -use nym_validator_client::client::IdentityKeyRef; -#[cfg(not(target_arch = "wasm32"))] -use tokio::time::sleep; #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; @@ -35,6 +32,9 @@ use wasmtimer::std::Instant; #[cfg(target_arch = "wasm32")] use wasmtimer::tokio::sleep; +#[cfg(not(target_arch = "wasm32"))] +type WsConn = WebSocketStream>; + #[cfg(target_arch = "wasm32")] type WsConn = JSWebsocket; @@ -278,16 +278,17 @@ pub(super) fn get_specified_gateway( } pub(super) async fn register_with_gateway( - gateway: &GatewayEndpointConfig, + gateway_id: identity::PublicKey, + gateway_listener: Url, our_identity: Arc, ) -> Result { let mut gateway_client = - GatewayClient::new_init(gateway.to_owned().try_into()?, our_identity.clone()); + GatewayClient::new_init(gateway_listener, gateway_id, our_identity.clone()); gateway_client.establish_connection().await.map_err(|err| { log::warn!("Failed to establish connection with gateway!"); ClientCoreError::GatewayClientError { - gateway_id: gateway.gateway_id.clone(), + gateway_id: gateway_id.to_base58_string(), source: err, } })?; @@ -295,12 +296,9 @@ pub(super) async fn register_with_gateway( .perform_initial_authentication() .await .map_err(|err| { - log::warn!( - "Failed to register with the gateway {}!", - gateway.gateway_id - ); + log::warn!("Failed to register with the gateway {gateway_id}: {err}"); ClientCoreError::GatewayClientError { - gateway_id: gateway.gateway_id.clone(), + gateway_id: gateway_id.to_base58_string(), source: err, } })?; diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 13129bf5f5..a45c87da6a 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -1,243 +1,236 @@ -// Copyright 2022-2023 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 //! Collection of initialization steps used by client implementations -use crate::client::base_client::storage::gateway_details::{ - GatewayDetailsStore, PersistedGatewayDetails, +use crate::client::base_client::storage::helpers::{ + has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details, + store_gateway_details, }; use crate::client::key_manager::persistence::KeyStore; -use crate::client::key_manager::ManagedKeys; -use crate::config::GatewayEndpointConfig; +use crate::client::key_manager::ClientKeys; use crate::error::ClientCoreError; use crate::init::helpers::{ choose_gateway_by_latency, get_specified_gateway, uniformly_random_gateway, }; use crate::init::types::{ - CustomGatewayDetails, GatewayDetails, GatewaySelectionSpecification, GatewaySetup, - InitialisationResult, + GatewaySelectionSpecification, GatewaySetup, InitialisationResult, SelectedGateway, }; +use nym_client_core_gateways_storage::GatewaysDetailsStore; +use nym_client_core_gateways_storage::{GatewayDetails, GatewayRegistration}; use nym_gateway_client::client::InitGatewayClient; use nym_topology::gateway; use rand::rngs::OsRng; -use serde::de::DeserializeOwned; +use rand::{CryptoRng, RngCore}; use serde::Serialize; -use std::sync::Arc; +use std::net::IpAddr; pub mod helpers; pub mod types; // helpers for error wrapping -async fn _store_gateway_details( - details_store: &D, - details: &PersistedGatewayDetails, + +pub async fn generate_new_client_keys( + rng: &mut R, + key_store: &K, ) -> Result<(), ClientCoreError> where - D: GatewayDetailsStore, - D::StorageError: Send + Sync + 'static, - T: Serialize + Send + Sync, -{ - details_store - .store_gateway_details(details) - .await - .map_err(|source| ClientCoreError::GatewayDetailsStoreError { - source: Box::new(source), - }) -} - -async fn _load_gateway_details( - details_store: &D, -) -> Result, ClientCoreError> -where - D: GatewayDetailsStore, - D::StorageError: Send + Sync + 'static, - T: DeserializeOwned + Send + Sync, -{ - details_store - .load_gateway_details() - .await - .map_err(|source| ClientCoreError::UnavailableGatewayDetails { - source: Box::new(source), - }) -} - -async fn _load_managed_keys(key_store: &K) -> Result -where + R: RngCore + CryptoRng, K: KeyStore, K::StorageError: Send + Sync + 'static, { - ManagedKeys::try_load(key_store) + ClientKeys::generate_new(rng) + .persist_keys(key_store) .await .map_err(|source| ClientCoreError::KeyStoreError { source: Box::new(source), }) } -fn ensure_valid_details( - details: &PersistedGatewayDetails, - loaded_keys: &ManagedKeys, -) -> Result<(), ClientCoreError> { - details.validate(loaded_keys.gateway_shared_key().as_deref()) -} - -async fn setup_new_gateway( +async fn setup_new_gateway( key_store: &K, details_store: &D, - overwrite_data: bool, - selection_specification: GatewaySelectionSpecification, + selection_specification: GatewaySelectionSpecification, available_gateways: Vec, -) -> Result, ClientCoreError> + wg_tun_ip_address: Option, +) -> Result where K: KeyStore, - D: GatewayDetailsStore, + D: GatewaysDetailsStore, K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, - T: DeserializeOwned + Serialize + Send + Sync, { log::trace!("Setting up new gateway"); - // if we're setting up new gateway, failing to load existing information is fine. - // as a matter of fact, it's only potentially a problem if we DO succeed - if _load_gateway_details(details_store).await.is_ok() && !overwrite_data { - return Err(ClientCoreError::ForbiddenKeyOverwrite); - } - if _load_managed_keys(key_store).await.is_ok() && !overwrite_data { - return Err(ClientCoreError::ForbiddenKeyOverwrite); - } + // if we're setting up new gateway, we must have had generated long-term client keys before + let client_keys = load_client_keys(key_store).await?; let mut rng = OsRng; - let mut new_keys = ManagedKeys::generate_new(&mut rng); - let gateway_details = match selection_specification { + let selected_gateway = match selection_specification { GatewaySelectionSpecification::UniformRemote { must_use_tls } => { let gateway = uniformly_random_gateway(&mut rng, &available_gateways, must_use_tls)?; - GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)?) + SelectedGateway::from_topology_node(gateway, wg_tun_ip_address, must_use_tls)? } GatewaySelectionSpecification::RemoteByLatency { must_use_tls } => { let gateway = choose_gateway_by_latency(&mut rng, &available_gateways, must_use_tls).await?; - GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)?) + SelectedGateway::from_topology_node(gateway, wg_tun_ip_address, must_use_tls)? } GatewaySelectionSpecification::Specified { must_use_tls, identity, } => { let gateway = get_specified_gateway(&identity, &available_gateways, must_use_tls)?; - GatewayDetails::Configured(GatewayEndpointConfig::from_node(gateway, must_use_tls)?) + SelectedGateway::from_topology_node(gateway, wg_tun_ip_address, must_use_tls)? } GatewaySelectionSpecification::Custom { gateway_identity, additional_data, - } => GatewayDetails::Custom(CustomGatewayDetails::new(gateway_identity, additional_data)), + } => SelectedGateway::custom(gateway_identity, additional_data)?, }; - let registration_result = if let GatewayDetails::Configured(gateway_cfg) = &gateway_details { - // if we're using a 'normal' gateway setup, do register - let our_identity = new_keys.identity_keypair(); - Some(helpers::register_with_gateway(gateway_cfg, our_identity).await?) - } else { - None + // check if we already have details associated with this particular gateway + // and if so, see if we can overwrite it + let selected_id = selected_gateway.gateway_id().to_base58_string(); + if has_gateway_details(details_store, &selected_id).await? { + return Err(ClientCoreError::AlreadyRegistered { + gateway_id: selected_id, + }); + } + + let (gateway_details, authenticated_ephemeral_client) = match selected_gateway { + SelectedGateway::Remote { + gateway_id, + gateway_owner_address, + gateway_listener, + wg_tun_address, + } => { + // if we're using a 'normal' gateway setup, do register + let our_identity = client_keys.identity_keypair(); + + // if wg address is set, use that one + let url = wg_tun_address.clone().unwrap_or(gateway_listener.clone()); + + let registration = + helpers::register_with_gateway(gateway_id, url, our_identity).await?; + ( + GatewayDetails::new_remote( + gateway_id, + registration.shared_keys, + gateway_owner_address, + gateway_listener, + wg_tun_address, + ), + Some(registration.authenticated_ephemeral_client), + ) + } + SelectedGateway::Custom { + gateway_id, + additional_data, + } => ( + GatewayDetails::new_custom(gateway_id, additional_data), + None, + ), }; - let maybe_shared_keys = registration_result - .as_ref() - .map(|r| Arc::clone(&r.shared_keys)); + let gateway_registration = gateway_details.into(); - let persisted_details = - PersistedGatewayDetails::new(gateway_details, maybe_shared_keys.as_deref())?; - - // persist the keys - new_keys - .deal_with_gateway_key(maybe_shared_keys, key_store) - .await - .map_err(|source| ClientCoreError::KeyStoreError { - source: Box::new(source), - })?; - - // persist gateway configs - _store_gateway_details(details_store, &persisted_details).await?; + // persist gateway details + store_gateway_details(details_store, &gateway_registration).await?; Ok(InitialisationResult { - gateway_details: persisted_details.into(), - managed_keys: new_keys, - authenticated_ephemeral_client: registration_result - .map(|r| r.authenticated_ephemeral_client), + gateway_registration, + client_keys, + authenticated_ephemeral_client, }) } -async fn use_loaded_gateway_details( +async fn use_loaded_gateway_details( key_store: &K, details_store: &D, -) -> Result, ClientCoreError> + gateway_id: Option, +) -> Result where K: KeyStore, - D: GatewayDetailsStore, + D: GatewaysDetailsStore, K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, - T: DeserializeOwned + Send + Sync, { - let loaded_details = _load_gateway_details(details_store).await?; - let loaded_keys = _load_managed_keys(key_store).await?; + let loaded_details = if let Some(gateway_id) = gateway_id { + load_gateway_details(details_store, &gateway_id).await? + } else { + load_active_gateway_details(details_store) + .await? + .registration + .ok_or(ClientCoreError::NoActiveGatewaySet)? + }; - ensure_valid_details(&loaded_details, &loaded_keys)?; + let loaded_keys = load_client_keys(key_store).await?; // no need to persist anything as we got everything from the storage Ok(InitialisationResult::new_loaded( - loaded_details.into(), + loaded_details, loaded_keys, )) } -fn reuse_gateway_connection( +fn reuse_gateway_connection( authenticated_ephemeral_client: InitGatewayClient, - gateway_details: GatewayDetails, - managed_keys: ManagedKeys, -) -> InitialisationResult { + gateway_registration: GatewayRegistration, + client_keys: ClientKeys, +) -> InitialisationResult { InitialisationResult { - gateway_details, - managed_keys, + gateway_registration, + client_keys, authenticated_ephemeral_client: Some(authenticated_ephemeral_client), } } -pub async fn setup_gateway( - setup: GatewaySetup, +pub async fn setup_gateway( + setup: GatewaySetup, key_store: &K, details_store: &D, -) -> Result, ClientCoreError> +) -> Result where K: KeyStore, - D: GatewayDetailsStore, + D: GatewaysDetailsStore, K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, - T: DeserializeOwned + Serialize + Send + Sync, { log::debug!("Setting up gateway"); match setup { - GatewaySetup::MustLoad => use_loaded_gateway_details(key_store, details_store).await, + GatewaySetup::MustLoad { gateway_id } => { + log::trace!("GatewaySetup::MustLoad with id: {gateway_id:?}"); + use_loaded_gateway_details(key_store, details_store, gateway_id).await + } GatewaySetup::New { specification, available_gateways, - overwrite_data, + wg_tun_address, } => { + log::trace!("GatewaySetup::New with spec: {specification:?}"); setup_new_gateway( key_store, details_store, - overwrite_data, specification, available_gateways, + wg_tun_address, ) .await } GatewaySetup::ReuseConnection { authenticated_ephemeral_client, gateway_details, - managed_keys, - } => Ok(reuse_gateway_connection( - authenticated_ephemeral_client, - gateway_details, - managed_keys, - )), + client_keys: managed_keys, + } => { + log::trace!("GatewaySetup::ReuseConnection"); + Ok(reuse_gateway_connection( + authenticated_ephemeral_client, + *gateway_details, + managed_keys, + )) + } } } diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 141bf8dd2d..74a6a72ab8 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -1,24 +1,132 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::base_client::storage::gateway_details::{ - GatewayDetailsStore, PersistedCustomGatewayDetails, PersistedGatewayDetails, -}; use crate::client::key_manager::persistence::KeyStore; -use crate::client::key_manager::ManagedKeys; -use crate::config::{Config, GatewayEndpointConfig}; +use crate::client::key_manager::ClientKeys; +use crate::config::Config; use crate::error::ClientCoreError; -use crate::init::{_load_gateway_details, _load_managed_keys, setup_gateway}; +use crate::init::{setup_gateway, use_loaded_gateway_details}; +use log::info; +use nym_client_core_gateways_storage::{ + GatewayRegistration, GatewaysDetailsStore, RemoteGatewayDetails, +}; +use nym_crypto::asymmetric::identity; use nym_gateway_client::client::InitGatewayClient; use nym_gateway_requests::registration::handshake::SharedKeys; use nym_sphinx::addressing::clients::Recipient; -use nym_sphinx::addressing::nodes::NodeIdentity; use nym_topology::gateway; use nym_validator_client::client::IdentityKey; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +use nym_validator_client::nyxd::AccountId; +use serde::Serialize; use std::fmt::Display; +use std::net::IpAddr; +use std::str::FromStr; use std::sync::Arc; +use time::OffsetDateTime; +use url::Url; + +pub enum SelectedGateway { + Remote { + gateway_id: identity::PublicKey, + + gateway_owner_address: AccountId, + + gateway_listener: Url, + + wg_tun_address: Option, + }, + Custom { + gateway_id: identity::PublicKey, + additional_data: Option>, + }, +} + +fn wg_tun_address( + tun_ip: Option, + gateway: &gateway::Node, +) -> Result, ClientCoreError> { + let Some(tun_ip) = tun_ip else { + return Ok(None); + }; + + // log this so we'd remember about it if we ever decided to actually use that port + if gateway.clients_wss_port.is_some() { + info!( + "gateway {} exposes wss but for wireguard we're going to use ws", + gateway.identity_key + ); + } + + let raw_url = format!("ws://{tun_ip}:{}", gateway.clients_ws_port); + Ok(Some(raw_url.as_str().parse().map_err(|source| { + ClientCoreError::MalformedListener { + gateway_id: gateway.identity_key.to_base58_string(), + raw_listener: raw_url, + source, + } + })?)) +} + +impl SelectedGateway { + pub fn from_topology_node( + node: gateway::Node, + wg_tun_ip_address: Option, + must_use_tls: bool, + ) -> Result { + let gateway_listener = if must_use_tls { + node.clients_address_tls() + .ok_or(ClientCoreError::UnsupportedWssProtocol { + gateway: node.identity_key.to_base58_string(), + })? + } else { + node.clients_address() + }; + + let wg_tun_address = wg_tun_address(wg_tun_ip_address, &node)?; + + let gateway_owner_address = AccountId::from_str(&node.owner).map_err(|source| { + ClientCoreError::MalformedGatewayOwnerAccountAddress { + gateway_id: node.identity_key.to_base58_string(), + raw_owner: node.owner, + err: source.to_string(), + } + })?; + + let gateway_listener = + Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { + gateway_id: node.identity_key.to_base58_string(), + raw_listener: gateway_listener, + source, + })?; + + Ok(SelectedGateway::Remote { + gateway_id: node.identity_key, + gateway_owner_address, + gateway_listener, + wg_tun_address, + }) + } + + pub fn custom( + gateway_id: String, + additional_data: Option>, + ) -> Result { + let gateway_id = identity::PublicKey::from_base58_string(&gateway_id) + .map_err(|source| ClientCoreError::MalformedGatewayIdentity { gateway_id, source })?; + + Ok(SelectedGateway::Custom { + gateway_id, + additional_data, + }) + } + + pub fn gateway_id(&self) -> &identity::PublicKey { + match self { + SelectedGateway::Remote { gateway_id, .. } => gateway_id, + SelectedGateway::Custom { gateway_id, .. } => gateway_id, + } + } +} /// Result of registering with a gateway: /// - shared keys derived between ourselves and the node @@ -33,17 +141,17 @@ pub struct RegistrationResult { /// - all loaded (or derived) keys /// - an optional authenticated handle of an ephemeral gateway handle created for the purposes of registration, /// if this was the first time this client registered -pub struct InitialisationResult { - pub gateway_details: GatewayDetails, - pub managed_keys: ManagedKeys, +pub struct InitialisationResult { + pub gateway_registration: GatewayRegistration, + pub client_keys: ClientKeys, pub authenticated_ephemeral_client: Option, } -impl InitialisationResult { - pub fn new_loaded(gateway_details: GatewayDetails, managed_keys: ManagedKeys) -> Self { +impl InitialisationResult { + pub fn new_loaded(gateway_registration: GatewayRegistration, client_keys: ClientKeys) -> Self { InitialisationResult { - gateway_details, - managed_keys, + gateway_registration, + client_keys, authenticated_ephemeral_client: None, } } @@ -51,135 +159,30 @@ impl InitialisationResult { pub async fn try_load(key_store: &K, details_store: &D) -> Result where K: KeyStore, - D: GatewayDetailsStore, + D: GatewaysDetailsStore, K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, - T: DeserializeOwned + Send + Sync, { - let loaded_details = _load_gateway_details(details_store).await?; - let loaded_keys = _load_managed_keys(key_store).await?; - - match &loaded_details { - PersistedGatewayDetails::Default(loaded_default) => { - if !loaded_default.verify(&loaded_keys.must_get_gateway_shared_key()) { - return Err(ClientCoreError::MismatchedGatewayDetails { - gateway_id: loaded_default.details.gateway_id.clone(), - }); - } - } - PersistedGatewayDetails::Custom(_) => {} - } - - Ok(InitialisationResult { - gateway_details: loaded_details.into(), - managed_keys: loaded_keys, - authenticated_ephemeral_client: None, - }) + use_loaded_gateway_details(key_store, details_store, None).await } - pub fn client_address(&self) -> Result { - let client_recipient = Recipient::new( - *self.managed_keys.identity_public_key(), - *self.managed_keys.encryption_public_key(), + pub fn client_address(&self) -> Recipient { + Recipient::new( + *self.client_keys.identity_keypair().public_key(), + *self.client_keys.encryption_keypair().public_key(), // TODO: below only works under assumption that gateway address == gateway id // (which currently is true) - NodeIdentity::from_base58_string(self.gateway_details.gateway_id())?, - ); - - Ok(client_recipient) - } -} - -/// Details of particular gateway client got registered with -#[derive(Debug, Clone)] -pub enum GatewayDetails { - /// Standard details of a remote gateway - Configured(GatewayEndpointConfig), - - /// Custom gateway setup, such as for a client embedded inside gateway itself - Custom(CustomGatewayDetails), -} - -#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "snake_case")] -pub struct EmptyCustomDetails {} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CustomGatewayDetails { - // whatever custom method is used, gateway's identity must be known - pub gateway_id: String, - - #[serde(flatten)] - pub additional_data: T, -} - -impl CustomGatewayDetails { - pub fn new(gateway_id: String, additional_data: T) -> Self { - Self { - gateway_id, - additional_data, - } - } -} - -impl GatewayDetails { - pub fn try_get_configured_endpoint(&self) -> Option<&GatewayEndpointConfig> { - if let GatewayDetails::Configured(endpoint) = &self { - Some(endpoint) - } else { - None - } + self.gateway_id(), + ) } - pub fn is_custom(&self) -> bool { - matches!(self, GatewayDetails::Custom(_)) - } - - pub fn gateway_id(&self) -> &str { - match self { - GatewayDetails::Configured(cfg) => &cfg.gateway_id, - GatewayDetails::Custom(custom) => &custom.gateway_id, - } - } -} - -impl From for GatewayDetails { - fn from(value: GatewayEndpointConfig) -> Self { - GatewayDetails::Configured(value) - } -} - -impl From> for CustomGatewayDetails { - fn from(value: PersistedCustomGatewayDetails) -> Self { - CustomGatewayDetails { - gateway_id: value.gateway_id, - additional_data: value.additional_data, - } - } -} - -impl From> for PersistedCustomGatewayDetails { - fn from(value: CustomGatewayDetails) -> Self { - PersistedCustomGatewayDetails { - gateway_id: value.gateway_id, - additional_data: value.additional_data, - } - } -} - -impl From> for GatewayDetails { - fn from(value: PersistedGatewayDetails) -> Self { - match value { - PersistedGatewayDetails::Default(default) => { - GatewayDetails::Configured(default.details) - } - PersistedGatewayDetails::Custom(custom) => GatewayDetails::Custom(custom.into()), - } + pub fn gateway_id(&self) -> identity::PublicKey { + self.gateway_registration.details.gateway_id() } } #[derive(Clone, Debug)] -pub enum GatewaySelectionSpecification { +pub enum GatewaySelectionSpecification { /// Uniformly choose a random remote gateway. UniformRemote { must_use_tls: bool }, @@ -197,11 +200,11 @@ pub enum GatewaySelectionSpecification { /// This client has handled the selection by itself Custom { gateway_identity: String, - additional_data: T, + additional_data: Option>, }, } -impl Default for GatewaySelectionSpecification { +impl Default for GatewaySelectionSpecification { fn default() -> Self { GatewaySelectionSpecification::UniformRemote { must_use_tls: false, @@ -209,7 +212,7 @@ impl Default for GatewaySelectionSpecification { } } -impl GatewaySelectionSpecification { +impl GatewaySelectionSpecification { pub fn new( gateway_identity: Option, latency_based_selection: Option, @@ -228,19 +231,25 @@ impl GatewaySelectionSpecification { } } -pub enum GatewaySetup { +pub enum GatewaySetup { /// The gateway specification (details + keys) MUST BE loaded from the underlying storage. - MustLoad, + MustLoad { + /// Optionally specify concrete gateway id. If none is selected, the current active gateway will be used. + gateway_id: Option, + }, /// Specifies usage of a new gateway New { - specification: GatewaySelectionSpecification, + specification: GatewaySelectionSpecification, // TODO: seems to be a bit inefficient to pass them by value available_gateways: Vec, - /// Specifies whether old data should be overwritten whilst setting up new gateway client. - overwrite_data: bool, + /// Implicitly specify whether the chosen gateway must use wireguard mode by setting the tun address. + /// + /// Currently this is imperfect solution as I'd imagine this address could vary from gateway to gateway + /// so perhaps it should be part of gateway::Node struct + wg_tun_address: Option, }, ReuseConnection { @@ -248,44 +257,53 @@ pub enum GatewaySetup { authenticated_ephemeral_client: InitGatewayClient, // Details of this pre-initialised client (i.e. gateway and keys) - gateway_details: GatewayDetails, + gateway_details: Box, - managed_keys: ManagedKeys, + client_keys: ClientKeys, }, } -impl GatewaySetup { - pub fn try_reuse_connection( - init_res: InitialisationResult, - ) -> Result { +impl GatewaySetup { + pub fn try_reuse_connection(init_res: InitialisationResult) -> Result { if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client { Ok(GatewaySetup::ReuseConnection { authenticated_ephemeral_client, - gateway_details: init_res.gateway_details, - managed_keys: init_res.managed_keys, + gateway_details: Box::new(init_res.gateway_registration), + client_keys: init_res.client_keys, }) } else { Err(ClientCoreError::NoInitClientPresent) } } + /// new gateway setup performed by each client that's inbuilt in a gateway (like NR or IPR) + pub fn new_inbuilt(identity: identity::PublicKey) -> Self { + GatewaySetup::New { + specification: GatewaySelectionSpecification::Custom { + gateway_identity: identity.to_base58_string(), + additional_data: None, + }, + available_gateways: vec![], + wg_tun_address: None, + } + } + pub async fn try_setup( self, key_store: &K, details_store: &D, - ) -> Result, ClientCoreError> + ) -> Result where K: KeyStore, - D: GatewayDetailsStore, + D: GatewaysDetailsStore, K::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static, - T: DeserializeOwned + Serialize + Send + Sync, { setup_gateway(self, key_store, details_store).await } pub fn is_must_load(&self) -> bool { - matches!(self, GatewaySetup::MustLoad) + matches!(self, GatewaySetup::MustLoad { .. }) } pub fn has_full_details(&self) -> bool { @@ -302,18 +320,25 @@ pub struct InitResults { pub encryption_key: String, pub gateway_id: String, pub gateway_listener: String, + pub gateway_registration: OffsetDateTime, pub address: Recipient, } impl InitResults { - pub fn new(config: &Config, address: Recipient, gateway: &GatewayEndpointConfig) -> Self { + pub fn new( + config: &Config, + address: Recipient, + gateway: &RemoteGatewayDetails, + registration: OffsetDateTime, + ) -> Self { Self { version: config.client.version.clone(), id: config.client.id.clone(), identity_key: address.identity().to_base58_string(), encryption_key: address.encryption_key().to_base58_string(), - gateway_id: gateway.gateway_id.clone(), - gateway_listener: gateway.gateway_listener.clone(), + gateway_id: gateway.gateway_id.to_base58_string(), + gateway_listener: gateway.gateway_listener.to_string(), + gateway_registration: registration, address, } } @@ -326,6 +351,7 @@ impl Display for InitResults { writeln!(f, "Identity key: {}", self.identity_key)?; writeln!(f, "Encryption: {}", self.encryption_key)?; writeln!(f, "Gateway ID: {}", self.gateway_id)?; - write!(f, "Gateway: {}", self.gateway_listener) + writeln!(f, "Gateway: {}", self.gateway_listener)?; + write!(f, "Registered at: {}", self.gateway_registration) } } diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index 0b401f4e02..1eb90ddcf7 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -1,6 +1,10 @@ use std::future::Future; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all( + not(target_arch = "wasm32"), + feature = "fs-surb-storage", + feature = "fs-gateways-storage" +))] pub mod cli_helpers; pub mod client; pub mod config; diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml new file mode 100644 index 0000000000..7a902bef94 --- /dev/null +++ b/common/client-core/surb-storage/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "nym-client-core-surb-storage" +version = "0.1.0" +edition = "2021" +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait.workspace = true +dashmap.workspace = true +log.workspace = true +thiserror.workspace = true +time.workspace = true + +nym-crypto = { path = "../../crypto", optional = true, default-features = false } +nym-sphinx = { path = "../../nymsphinx" } +nym-task = { path = "../../task" } + + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] +workspace = true +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] +optional = true + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + +[features] +fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"] diff --git a/common/client-core/build.rs b/common/client-core/surb-storage/build.rs similarity index 100% rename from common/client-core/build.rs rename to common/client-core/surb-storage/build.rs diff --git a/common/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql b/common/client-core/surb-storage/fs_surbs_migrations/20221130120000_create_initial_tables.sql similarity index 100% rename from common/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql rename to common/client-core/surb-storage/fs_surbs_migrations/20221130120000_create_initial_tables.sql diff --git a/common/client-core/src/client/replies/reply_storage/backend/browser_backend.rs b/common/client-core/surb-storage/src/backend/browser_backend.rs similarity index 88% rename from common/client-core/src/client/replies/reply_storage/backend/browser_backend.rs rename to common/client-core/surb-storage/src/backend/browser_backend.rs index ff2da4ab72..de39fd49b6 100644 --- a/common/client-core/src/client/replies/reply_storage/backend/browser_backend.rs +++ b/common/client-core/surb-storage/src/backend/browser_backend.rs @@ -1,8 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::backend::Empty; -use crate::client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend}; +use crate::backend::Empty; +use crate::{CombinedReplyStorage, ReplyStorageBackend}; use async_trait::async_trait; // well, right now we don't have the browser storage : ( diff --git a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs b/common/client-core/surb-storage/src/backend/fs_backend/error.rs similarity index 94% rename from common/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs rename to common/client-core/surb-storage/src/backend/fs_backend/error.rs index 84cac2f7ec..02742758ae 100644 --- a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/error.rs @@ -10,7 +10,7 @@ pub enum StorageError { #[error("the provided database path doesn't have a filename defined")] DatabasePathWithoutFilename { provided_path: PathBuf }, - #[error("unable to create the directory for the database")] + #[error("unable to create the directory for the database at {}: {source}", provided_path.display())] DatabasePathUnableToCreateParentDirectory { provided_path: PathBuf, source: io::Error, diff --git a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs similarity index 77% rename from common/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs rename to common/client-core/surb-storage/src/backend/fs_backend/manager.rs index 40c4da18fa..8b663b5c9a 100644 --- a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs @@ -1,8 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError; -use crate::client::replies::reply_storage::backend::fs_backend::models::{ +use crate::backend::fs_backend::error::StorageError; +use crate::backend::fs_backend::models::{ ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, }; use log::{error, info}; @@ -10,16 +10,13 @@ use sqlx::ConnectOptions; use std::path::Path; #[derive(Debug, Clone)] -pub(crate) struct StorageManager { - pub(crate) connection_pool: sqlx::SqlitePool, +pub struct StorageManager { + pub connection_pool: sqlx::SqlitePool, } // all SQL goes here impl StorageManager { - pub(crate) async fn init>( - database_path: P, - fresh: bool, - ) -> Result { + pub async fn init>(database_path: P, fresh: bool) -> Result { // ensure the whole directory structure exists if let Some(parent_dir) = database_path.as_ref().parent() { std::fs::create_dir_all(parent_dir).map_err(|source| { @@ -57,45 +54,42 @@ impl StorageManager { } #[allow(dead_code)] - pub(crate) async fn status_table_exists(&self) -> Result { + pub async fn status_table_exists(&self) -> Result { sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") .fetch_optional(&self.connection_pool) .await .map(|r| r.is_some()) } - pub(crate) async fn create_status_table(&self) -> Result<(), sqlx::Error> { + pub async fn create_status_table(&self) -> Result<(), sqlx::Error> { sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)") .execute(&self.connection_pool) .await?; Ok(()) } - pub(crate) async fn get_flush_status(&self) -> Result { + pub async fn get_flush_status(&self) -> Result { sqlx::query!("SELECT flush_in_progress FROM status;") .fetch_one(&self.connection_pool) .await .map(|r| r.flush_in_progress > 0) } - pub(crate) async fn set_previous_flush_timestamp( - &self, - timestamp: i64, - ) -> Result<(), sqlx::Error> { + pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> { sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp) .execute(&self.connection_pool) .await?; Ok(()) } - pub(crate) async fn get_previous_flush_timestamp(&self) -> Result { + pub async fn get_previous_flush_timestamp(&self) -> Result { sqlx::query!("SELECT previous_flush_timestamp FROM status;") .fetch_one(&self.connection_pool) .await .map(|r| r.previous_flush_timestamp) } - pub(crate) async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { + pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { let in_progress_int = i64::from(in_progress); sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int) .execute(&self.connection_pool) @@ -103,14 +97,14 @@ impl StorageManager { Ok(()) } - pub(crate) async fn get_client_in_use_status(&self) -> Result { + pub async fn get_client_in_use_status(&self) -> Result { sqlx::query!("SELECT client_in_use FROM status;") .fetch_one(&self.connection_pool) .await .map(|r| r.client_in_use > 0) } - pub(crate) async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { + pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { let in_use_int = i64::from(in_use); sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int) .execute(&self.connection_pool) @@ -118,20 +112,20 @@ impl StorageManager { Ok(()) } - pub(crate) async fn delete_all_tags(&self) -> Result<(), sqlx::Error> { + pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM sender_tag;") .execute(&self.connection_pool) .await?; Ok(()) } - pub(crate) async fn get_tags(&self) -> Result, sqlx::Error> { + pub async fn get_tags(&self) -> Result, sqlx::Error> { sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",) .fetch_all(&self.connection_pool) .await } - pub(crate) async fn insert_tag(&self, stored_tag: StoredSenderTag) -> Result<(), sqlx::Error> { + pub async fn insert_tag(&self, stored_tag: StoredSenderTag) -> Result<(), sqlx::Error> { sqlx::query!( r#" INSERT INTO sender_tag(recipient, tag) VALUES (?, ?); @@ -144,20 +138,20 @@ impl StorageManager { Ok(()) } - pub(crate) async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { + pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_key;") .execute(&self.connection_pool) .await?; Ok(()) } - pub(crate) async fn get_reply_keys(&self) -> Result, sqlx::Error> { + pub async fn get_reply_keys(&self) -> Result, sqlx::Error> { sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",) .fetch_all(&self.connection_pool) .await } - pub(crate) async fn insert_reply_key( + pub async fn insert_reply_key( &self, stored_reply_key: StoredReplyKey, ) -> Result<(), sqlx::Error> { @@ -174,13 +168,13 @@ impl StorageManager { Ok(()) } - pub(crate) async fn get_surb_senders(&self) -> Result, sqlx::Error> { + pub async fn get_surb_senders(&self) -> Result, sqlx::Error> { sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",) .fetch_all(&self.connection_pool) .await } - pub(crate) async fn insert_surb_sender( + pub async fn insert_surb_sender( &self, stored_surb_sender: StoredSurbSender, ) -> Result { @@ -197,7 +191,7 @@ impl StorageManager { Ok(id) } - pub(crate) async fn get_reply_surbs( + pub async fn get_reply_surbs( &self, sender_id: i64, ) -> Result, sqlx::Error> { @@ -210,7 +204,7 @@ impl StorageManager { .await } - pub(crate) async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { + pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { sqlx::query!("DELETE FROM reply_surb;") .execute(&self.connection_pool) .await?; @@ -222,7 +216,7 @@ impl StorageManager { Ok(()) } - pub(crate) async fn insert_reply_surb( + pub async fn insert_reply_surb( &self, stored_reply_surb: StoredReplySurb, ) -> Result<(), sqlx::Error> { @@ -238,7 +232,7 @@ impl StorageManager { Ok(()) } - pub(crate) async fn get_reply_surb_storage_metadata( + pub async fn get_reply_surb_storage_metadata( &self, ) -> Result { sqlx::query_as!( @@ -251,7 +245,7 @@ impl StorageManager { .await } - pub(crate) async fn insert_reply_surb_storage_metadata( + pub async fn insert_reply_surb_storage_metadata( &self, metadata: ReplySurbStorageMetadata, ) -> Result<(), sqlx::Error> { diff --git a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs similarity index 97% rename from common/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs rename to common/client-core/surb-storage/src/backend/fs_backend/mod.rs index 4800634563..6f68c59201 100644 --- a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs @@ -1,12 +1,12 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::backend::fs_backend::manager::StorageManager; -use crate::client::replies::reply_storage::backend::fs_backend::models::{ +use crate::backend::fs_backend::manager::StorageManager; +use crate::backend::fs_backend::models::{ ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, }; -use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbs; -use crate::client::replies::reply_storage::{ +use crate::surb_storage::ReceivedReplySurbs; +use crate::{ CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags, }; use async_trait::async_trait; diff --git a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs b/common/client-core/surb-storage/src/backend/fs_backend/models.rs similarity index 79% rename from common/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs rename to common/client-core/surb-storage/src/backend/fs_backend/models.rs index c7b5196654..8acf83f676 100644 --- a/common/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/models.rs @@ -1,8 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError; -use crate::client::replies::reply_storage::key_storage::UsedReplyKey; +use crate::backend::fs_backend::error::StorageError; +use crate::key_storage::UsedReplyKey; use nym_crypto::generic_array::typenum::Unsigned; use nym_crypto::Digest; use nym_sphinx::addressing::clients::{Recipient, RecipientBytes}; @@ -12,13 +12,13 @@ use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryption use nym_sphinx::params::ReplySurbKeyDigestAlgorithm; #[derive(Debug, Clone)] -pub(crate) struct StoredSenderTag { - pub(crate) recipient: Vec, - pub(crate) tag: Vec, +pub struct StoredSenderTag { + pub recipient: Vec, + pub tag: Vec, } impl StoredSenderTag { - pub(crate) fn new(recipient: RecipientBytes, tag: AnonymousSenderTag) -> StoredSenderTag { + pub fn new(recipient: RecipientBytes, tag: AnonymousSenderTag) -> StoredSenderTag { StoredSenderTag { recipient: recipient.to_vec(), tag: tag.to_bytes().to_vec(), @@ -57,14 +57,14 @@ impl TryFrom for (RecipientBytes, AnonymousSenderTag) { } #[derive(Debug, Clone)] -pub(crate) struct StoredReplyKey { - pub(crate) key_digest: Vec, - pub(crate) reply_key: Vec, - pub(crate) sent_at_timestamp: i64, +pub struct StoredReplyKey { + pub key_digest: Vec, + pub reply_key: Vec, + pub sent_at_timestamp: i64, } impl StoredReplyKey { - pub(crate) fn new(key_digest: EncryptionKeyDigest, reply_key: UsedReplyKey) -> StoredReplyKey { + pub fn new(key_digest: EncryptionKeyDigest, reply_key: UsedReplyKey) -> StoredReplyKey { StoredReplyKey { key_digest: key_digest.to_vec(), reply_key: (*reply_key).to_bytes(), @@ -105,14 +105,14 @@ impl TryFrom for (EncryptionKeyDigest, UsedReplyKey) { } } -pub(crate) struct StoredSurbSender { - pub(crate) id: i64, - pub(crate) tag: Vec, - pub(crate) last_sent_timestamp: i64, +pub struct StoredSurbSender { + pub id: i64, + pub tag: Vec, + pub last_sent_timestamp: i64, } impl StoredSurbSender { - pub(crate) fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self { + pub fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self { StoredSurbSender { // for the purposes of STORING data, // we ignore that field anyway @@ -143,13 +143,13 @@ impl TryFrom for (AnonymousSenderTag, i64) { } } -pub(crate) struct StoredReplySurb { - pub(crate) reply_surb_sender_id: i64, - pub(crate) reply_surb: Vec, +pub struct StoredReplySurb { + pub reply_surb_sender_id: i64, + pub reply_surb: Vec, } impl StoredReplySurb { - pub(crate) fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self { + pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self { StoredReplySurb { reply_surb_sender_id, reply_surb: reply_surb.to_bytes(), @@ -168,13 +168,13 @@ impl TryFrom for ReplySurb { } #[derive(Copy, Clone)] -pub(crate) struct ReplySurbStorageMetadata { - pub(crate) min_reply_surb_threshold: u32, - pub(crate) max_reply_surb_threshold: u32, +pub struct ReplySurbStorageMetadata { + pub min_reply_surb_threshold: u32, + pub max_reply_surb_threshold: u32, } impl ReplySurbStorageMetadata { - pub(crate) fn new(min_reply_surb_threshold: usize, max_reply_surb_threshold: usize) -> Self { + pub fn new(min_reply_surb_threshold: usize, max_reply_surb_threshold: usize) -> Self { Self { min_reply_surb_threshold: min_reply_surb_threshold as u32, max_reply_surb_threshold: max_reply_surb_threshold as u32, diff --git a/common/client-core/src/client/replies/reply_storage/backend/mod.rs b/common/client-core/surb-storage/src/backend/mod.rs similarity index 97% rename from common/client-core/src/client/replies/reply_storage/backend/mod.rs rename to common/client-core/surb-storage/src/backend/mod.rs index 557346a2da..e195051e4e 100644 --- a/common/client-core/src/client/replies/reply_storage/backend/mod.rs +++ b/common/client-core/surb-storage/src/backend/mod.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::CombinedReplyStorage; +use crate::CombinedReplyStorage; use async_trait::async_trait; use std::error::Error; use thiserror::Error; diff --git a/common/client-core/src/client/replies/reply_storage/combined.rs b/common/client-core/surb-storage/src/combined.rs similarity index 90% rename from common/client-core/src/client/replies/reply_storage/combined.rs rename to common/client-core/surb-storage/src/combined.rs index c9fb9e4a5c..9d44e53923 100644 --- a/common/client-core/src/client/replies/reply_storage/combined.rs +++ b/common/client-core/surb-storage/src/combined.rs @@ -1,7 +1,7 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; +use crate::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; #[derive(Debug, Clone)] pub struct CombinedReplyStorage { diff --git a/common/client-core/src/client/replies/reply_storage/key_storage.rs b/common/client-core/surb-storage/src/key_storage.rs similarity index 72% rename from common/client-core/src/client/replies/reply_storage/key_storage.rs rename to common/client-core/surb-storage/src/key_storage.rs index 310a44e0eb..61a474ad5c 100644 --- a/common/client-core/src/client/replies/reply_storage/key_storage.rs +++ b/common/client-core/surb-storage/src/key_storage.rs @@ -1,4 +1,4 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use dashmap::iter::Iter; @@ -14,13 +14,19 @@ pub struct SentReplyKeys { inner: Arc, } +impl Default for SentReplyKeys { + fn default() -> Self { + SentReplyKeys::new() + } +} + #[derive(Debug)] struct SentReplyKeysInner { data: DashMap, } impl SentReplyKeys { - pub(crate) fn new() -> SentReplyKeys { + pub fn new() -> SentReplyKeys { SentReplyKeys { inner: Arc::new(SentReplyKeysInner { data: DashMap::new(), @@ -29,7 +35,7 @@ impl SentReplyKeys { } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] - pub(crate) fn from_raw(raw: Vec<(EncryptionKeyDigest, UsedReplyKey)>) -> SentReplyKeys { + pub fn from_raw(raw: Vec<(EncryptionKeyDigest, UsedReplyKey)>) -> SentReplyKeys { SentReplyKeys { inner: Arc::new(SentReplyKeysInner { data: raw.into_iter().collect(), @@ -37,35 +43,35 @@ impl SentReplyKeys { } } - pub(crate) fn as_raw_iter(&self) -> Iter<'_, EncryptionKeyDigest, UsedReplyKey> { + pub fn as_raw_iter(&self) -> Iter<'_, EncryptionKeyDigest, UsedReplyKey> { self.inner.data.iter() } - pub(crate) fn insert_multiple(&self, keys: Vec) { + pub fn insert_multiple(&self, keys: Vec) { let now = OffsetDateTime::now_utc().unix_timestamp(); for key in keys { self.insert(UsedReplyKey::new(key, now)) } } - pub(crate) fn insert(&self, key: UsedReplyKey) { + pub fn insert(&self, key: UsedReplyKey) { self.inner.data.insert(key.compute_digest(), key); } - pub(crate) fn try_pop(&self, digest: EncryptionKeyDigest) -> Option { + pub fn try_pop(&self, digest: EncryptionKeyDigest) -> Option { self.inner.data.remove(&digest).map(|(_k, v)| v) } - pub(crate) fn remove(&self, digest: EncryptionKeyDigest) { + pub fn remove(&self, digest: EncryptionKeyDigest) { self.inner.data.remove(&digest); } } #[derive(Debug, Copy, Clone)] -pub(crate) struct UsedReplyKey { +pub struct UsedReplyKey { key: SurbEncryptionKey, // the purpose of this field is to perform invalidation at relatively very long intervals - pub(crate) sent_at_timestamp: i64, + pub sent_at_timestamp: i64, } impl UsedReplyKey { diff --git a/common/client-core/src/client/replies/reply_storage/mod.rs b/common/client-core/surb-storage/src/lib.rs similarity index 85% rename from common/client-core/src/client/replies/reply_storage/mod.rs rename to common/client-core/surb-storage/src/lib.rs index 16a0a36cd4..9e0ba14b34 100644 --- a/common/client-core/src/client/replies/reply_storage/mod.rs +++ b/common/client-core/surb-storage/src/lib.rs @@ -1,11 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub use crate::client::replies::reply_storage::combined::CombinedReplyStorage; -pub use crate::client::replies::reply_storage::key_storage::SentReplyKeys; -pub use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbsMap; -pub use crate::client::replies::reply_storage::tag_storage::UsedSenderTags; pub use backend::*; +pub use combined::CombinedReplyStorage; +pub use key_storage::SentReplyKeys; +pub use surb_storage::ReceivedReplySurbsMap; +pub use tag_storage::UsedSenderTags; mod backend; mod combined; diff --git a/common/client-core/src/client/replies/reply_storage/surb_storage.rs b/common/client-core/surb-storage/src/surb_storage.rs similarity index 77% rename from common/client-core/src/client/replies/reply_storage/surb_storage.rs rename to common/client-core/surb-storage/src/surb_storage.rs index cb88e30a90..92050c0104 100644 --- a/common/client-core/src/client/replies/reply_storage/surb_storage.rs +++ b/common/client-core/surb-storage/src/surb_storage.rs @@ -1,4 +1,4 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use dashmap::iter::Iter; @@ -28,10 +28,7 @@ struct ReceivedReplySurbsMapInner { } impl ReceivedReplySurbsMap { - pub(crate) fn new( - min_surb_threshold: usize, - max_surb_threshold: usize, - ) -> ReceivedReplySurbsMap { + pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> ReceivedReplySurbsMap { ReceivedReplySurbsMap { inner: Arc::new(ReceivedReplySurbsMapInner { data: DashMap::new(), @@ -42,7 +39,7 @@ impl ReceivedReplySurbsMap { } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] - pub(crate) fn from_raw( + pub fn from_raw( min_surb_threshold: usize, max_surb_threshold: usize, raw: Vec<(AnonymousSenderTag, ReceivedReplySurbs)>, @@ -56,28 +53,28 @@ impl ReceivedReplySurbsMap { } } - pub(crate) fn as_raw_iter(&self) -> Iter<'_, AnonymousSenderTag, ReceivedReplySurbs> { + pub fn as_raw_iter(&self) -> Iter<'_, AnonymousSenderTag, ReceivedReplySurbs> { self.inner.data.iter() } - pub(crate) fn remove(&self, target: &AnonymousSenderTag) { + pub fn remove(&self, target: &AnonymousSenderTag) { self.inner.data.remove(target); } - pub(crate) fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) { + pub fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) { if let Some(mut entry) = self.inner.data.get_mut(target) { entry.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp(); } } - pub(crate) fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option { + pub fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option { self.inner .data .get(target) .map(|e| e.surbs_last_received_at()) } - pub(crate) fn pending_reception(&self, target: &AnonymousSenderTag) -> u32 { + pub fn pending_reception(&self, target: &AnonymousSenderTag) -> u32 { self.inner .data .get(target) @@ -85,7 +82,7 @@ impl ReceivedReplySurbsMap { .unwrap_or_default() } - pub(crate) fn increment_pending_reception( + pub fn increment_pending_reception( &self, target: &AnonymousSenderTag, amount: u32, @@ -96,7 +93,7 @@ impl ReceivedReplySurbsMap { .map(|mut e| e.increment_pending_reception(amount)) } - pub(crate) fn decrement_pending_reception( + pub fn decrement_pending_reception( &self, target: &AnonymousSenderTag, amount: u32, @@ -107,21 +104,21 @@ impl ReceivedReplySurbsMap { .map(|mut e| e.decrement_pending_reception(amount)) } - pub(crate) fn reset_pending_reception(&self, target: &AnonymousSenderTag) { + pub fn reset_pending_reception(&self, target: &AnonymousSenderTag) { if let Some(mut e) = self.inner.data.get_mut(target) { e.reset_pending_reception() } } - pub(crate) fn min_surb_threshold(&self) -> usize { + pub fn min_surb_threshold(&self) -> usize { self.inner.min_surb_threshold.load(Ordering::Relaxed) } - pub(crate) fn max_surb_threshold(&self) -> usize { + pub fn max_surb_threshold(&self) -> usize { self.inner.max_surb_threshold.load(Ordering::Relaxed) } - pub(crate) fn available_surbs(&self, target: &AnonymousSenderTag) -> usize { + pub fn available_surbs(&self, target: &AnonymousSenderTag) -> usize { self.inner .data .get(target) @@ -129,11 +126,11 @@ impl ReceivedReplySurbsMap { .unwrap_or_default() } - pub(crate) fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool { + pub fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool { self.inner.data.contains_key(target) } - pub(crate) fn get_reply_surbs( + pub fn get_reply_surbs( &self, target: &AnonymousSenderTag, amount: usize, @@ -150,7 +147,7 @@ impl ReceivedReplySurbsMap { } } - pub(crate) fn get_reply_surb_ignoring_threshold( + pub fn get_reply_surb_ignoring_threshold( &self, target: &AnonymousSenderTag, ) -> Option<(Option, usize)> { @@ -160,7 +157,7 @@ impl ReceivedReplySurbsMap { .map(|mut s| s.get_reply_surb()) } - pub(crate) fn get_reply_surb( + pub fn get_reply_surb( &self, target: &AnonymousSenderTag, ) -> Option<(Option, usize)> { @@ -174,7 +171,7 @@ impl ReceivedReplySurbsMap { }) } - pub(crate) fn insert_surbs>( + pub fn insert_surbs>( &self, target: &AnonymousSenderTag, surbs: I, @@ -189,7 +186,7 @@ impl ReceivedReplySurbsMap { } #[derive(Debug)] -pub(crate) struct ReceivedReplySurbs { +pub struct ReceivedReplySurbs { // in the future we'd probably want to put extra data here to indicate when the SURBs got received // so we could invalidate entries from the previous key rotations data: VecDeque, @@ -208,7 +205,7 @@ impl ReceivedReplySurbs { } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] - pub(crate) fn new_retrieved( + pub fn new_retrieved( surbs: Vec, surbs_last_received_at_timestamp: i64, ) -> ReceivedReplySurbs { @@ -220,33 +217,33 @@ impl ReceivedReplySurbs { } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] - pub(crate) fn surbs_ref(&self) -> &VecDeque { + pub fn surbs_ref(&self) -> &VecDeque { &self.data } - pub(crate) fn surbs_last_received_at(&self) -> i64 { + pub fn surbs_last_received_at(&self) -> i64 { self.surbs_last_received_at_timestamp } - pub(crate) fn pending_reception(&self) -> u32 { + pub fn pending_reception(&self) -> u32 { self.pending_reception } - pub(crate) fn increment_pending_reception(&mut self, amount: u32) -> u32 { + pub fn increment_pending_reception(&mut self, amount: u32) -> u32 { self.pending_reception += amount; self.pending_reception } - pub(crate) fn decrement_pending_reception(&mut self, amount: u32) -> u32 { + pub fn decrement_pending_reception(&mut self, amount: u32) -> u32 { self.pending_reception = self.pending_reception.saturating_sub(amount); self.pending_reception } - pub(crate) fn reset_pending_reception(&mut self) { + pub fn reset_pending_reception(&mut self) { self.pending_reception = 0; } - pub(crate) fn get_reply_surbs(&mut self, amount: usize) -> (Option>, usize) { + pub fn get_reply_surbs(&mut self, amount: usize) -> (Option>, usize) { if self.items_left() < amount { (None, self.items_left()) } else { @@ -255,7 +252,7 @@ impl ReceivedReplySurbs { } } - pub(crate) fn get_reply_surb(&mut self) -> (Option, usize) { + pub fn get_reply_surb(&mut self) -> (Option, usize) { (self.pop_surb(), self.items_left()) } @@ -268,7 +265,7 @@ impl ReceivedReplySurbs { } // realistically we're always going to be getting multiple surbs at once - pub(crate) fn insert_reply_surbs>(&mut self, surbs: I) { + pub fn insert_reply_surbs>(&mut self, surbs: I) { let mut v = surbs.into_iter().collect::>(); trace!("storing {} surbs in the storage", v.len()); self.data.append(&mut v); diff --git a/common/client-core/src/client/replies/reply_storage/tag_storage.rs b/common/client-core/surb-storage/src/tag_storage.rs similarity index 68% rename from common/client-core/src/client/replies/reply_storage/tag_storage.rs rename to common/client-core/surb-storage/src/tag_storage.rs index 892018b6fc..8c72daffea 100644 --- a/common/client-core/src/client/replies/reply_storage/tag_storage.rs +++ b/common/client-core/surb-storage/src/tag_storage.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use dashmap::DashMap; @@ -14,13 +14,19 @@ pub struct UsedSenderTags { inner: Arc, } +impl Default for UsedSenderTags { + fn default() -> Self { + UsedSenderTags::new() + } +} + #[derive(Debug)] struct UsedSenderTagsInner { data: DashMap, } impl UsedSenderTags { - pub(crate) fn new() -> UsedSenderTags { + pub fn new() -> UsedSenderTags { UsedSenderTags { inner: Arc::new(UsedSenderTagsInner { data: DashMap::new(), @@ -29,7 +35,7 @@ impl UsedSenderTags { } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] - pub(crate) fn from_raw(raw: Vec<(RecipientBytes, AnonymousSenderTag)>) -> UsedSenderTags { + pub fn from_raw(raw: Vec<(RecipientBytes, AnonymousSenderTag)>) -> UsedSenderTags { UsedSenderTags { inner: Arc::new(UsedSenderTagsInner { data: raw.into_iter().collect(), @@ -38,22 +44,22 @@ impl UsedSenderTags { } #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] - pub(crate) fn as_raw_iter(&self) -> Iter<'_, RecipientBytes, AnonymousSenderTag> { + pub fn as_raw_iter(&self) -> Iter<'_, RecipientBytes, AnonymousSenderTag> { self.inner.data.iter() } - pub(crate) fn insert_new(&self, recipient: &Recipient, tag: AnonymousSenderTag) { + pub fn insert_new(&self, recipient: &Recipient, tag: AnonymousSenderTag) { self.inner.data.insert(recipient.to_bytes(), tag); } - pub(crate) fn try_get_existing(&self, recipient: &Recipient) -> Option { + pub fn try_get_existing(&self, recipient: &Recipient) -> Option { self.inner .data .get(&recipient.to_bytes()) .map(|r| *r.value()) } - pub(crate) fn exists(&self, recipient: &Recipient) -> bool { + pub fn exists(&self, recipient: &Recipient) -> bool { self.inner.data.contains_key(&recipient.to_bytes()) } } diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 77d28ca2a7..5fb062065f 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -32,6 +32,7 @@ use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; use tungstenite::protocol::Message; +use url::Url; #[cfg(unix)] use std::os::fd::RawFd; @@ -63,6 +64,20 @@ pub struct GatewayConfig { pub gateway_listener: String, } +impl GatewayConfig { + pub fn new( + gateway_identity: identity::PublicKey, + gateway_owner: Option, + gateway_listener: String, + ) -> Self { + GatewayConfig { + gateway_identity, + gateway_owner, + gateway_listener, + } + } +} + // TODO: this should be refactored into a state machine that keeps track of its authentication state pub struct GatewayClient { authenticated: bool, @@ -611,11 +626,13 @@ impl GatewayClient { }); } + let gateway_id = self.gateway_identity().to_base58_string(); + let prepared_credential = self .bandwidth_controller .as_ref() .unwrap() - .prepare_bandwidth_credential() + .prepare_bandwidth_credential(&gateway_id) .await?; self.claim_coconut_bandwidth(prepared_credential.data) @@ -623,7 +640,7 @@ impl GatewayClient { self.bandwidth_controller .as_ref() .unwrap() - .consume_credential(prepared_credential.credential_id) + .consume_credential(prepared_credential.credential_id, &gateway_id) .await?; Ok(()) @@ -836,7 +853,11 @@ pub struct InitOnly; impl GatewayClient { // for initialisation we do not need credential storage. Though it's still a bit weird we have to set the generic... - pub fn new_init(config: GatewayConfig, local_identity: Arc) -> Self { + pub fn new_init( + gateway_listener: Url, + gateway_identity: identity::PublicKey, + local_identity: Arc, + ) -> Self { log::trace!("Initialising gateway client"); use futures::channel::mpsc; @@ -851,8 +872,8 @@ impl GatewayClient { authenticated: false, disabled_credentials_mode: true, bandwidth_remaining: 0, - gateway_address: config.gateway_listener, - gateway_identity: config.gateway_identity, + gateway_address: gateway_listener.to_string(), + gateway_identity, local_identity, shared_key: None, connection: SocketState::NotConnected, diff --git a/common/credential-storage/migrations/20240311120000_credential_used_gateways.sql b/common/credential-storage/migrations/20240311120000_credential_used_gateways.sql new file mode 100644 index 0000000000..96398f6251 --- /dev/null +++ b/common/credential-storage/migrations/20240311120000_credential_used_gateways.sql @@ -0,0 +1,31 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +DROP TABLE coconut_credentials; +CREATE TABLE coconut_credentials +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + +-- introduce a way for us to introduce breaking changes in serialization + serialization_revision INTEGER NOT NULL, + +-- the best we can do without enums + credential_type TEXT CHECK ( credential_type IN ('BandwidthVoucher', 'FreeBandwidthPass') ) NOT NULL, + credential_data BLOB NOT NULL, + epoch_id INTEGER NOT NULL, + +-- this field is only really applicable to free passes + expired BOOLEAN NOT NULL +); + +-- for bandwidth vouchers there's going to be only a single entry; for freepasses there can be as many as there are gateways +CREATE TABLE credential_usage +( + credential_id INTEGER NOT NULL REFERENCES coconut_credentials (id), + gateway_id_bs58 TEXT NOT NULL, + +-- no matter credential type, we can't spend the same credential with the same gateway multiple times + UNIQUE (credential_id, gateway_id_bs58) +) \ No newline at end of file diff --git a/common/credential-storage/migrations/20240311120001_drop_erc20.sql b/common/credential-storage/migrations/20240311120001_drop_erc20.sql new file mode 100644 index 0000000000..f74034ffc6 --- /dev/null +++ b/common/credential-storage/migrations/20240311120001_drop_erc20.sql @@ -0,0 +1,7 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +-- we never actually dropped that table... +DROP TABLE erc20_credentials; \ No newline at end of file diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index ddf5b7017e..78d18f3b88 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -1,7 +1,7 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::StoredIssuedCredential; +use crate::models::{CredentialUsage, StoredIssuedCredential}; use std::sync::Arc; use tokio::sync::RwLock; @@ -12,7 +12,8 @@ pub struct CoconutCredentialManager { #[derive(Default)] struct CoconutCredentialManagerInner { - data: Vec, + credentials: Vec, + credential_usage: Vec, _next_id: i64, } @@ -41,25 +42,67 @@ impl CoconutCredentialManager { ) { let mut inner = self.inner.write().await; let id = inner.next_id(); - inner.data.push(StoredIssuedCredential { + inner.credentials.push(StoredIssuedCredential { id, serialization_revision, credential_data: credential_data.to_vec(), credential_type, epoch_id, - consumed: false, expired: false, }) } - /// Tries to retrieve one of the stored, unused credentials. - pub async fn get_next_unspent_credential(&self) -> Option { - let creds = self.inner.read().await; - creds - .data + async fn bandwidth_voucher_spent(&self, id: i64) -> bool { + self.inner + .read() + .await + .credential_usage .iter() - .find(|c| !c.consumed && !c.expired) - .cloned() + .any(|c| c.credential_id == id) + } + + async fn freepass_spent(&self, id: i64, gateway_id: &str) -> bool { + self.inner + .read() + .await + .credential_usage + .iter() + .any(|c| c.credential_id == id && c.gateway_id_bs58 == gateway_id) + } + + /// Tries to retrieve one of the stored, unused credentials. + pub async fn get_next_unspect_bandwidth_voucher(&self) -> Option { + let guard = self.inner.read().await; + for credential in guard + .credentials + .iter() + .filter(|c| c.credential_type == "BandwidthVoucher") + { + if !self.bandwidth_voucher_spent(credential.id).await { + return Some(credential.clone()); + } + } + None + } + + pub async fn get_next_unspect_freepass( + &self, + gateway_id: &str, + ) -> Option { + let guard = self.inner.read().await; + for credential in guard + .credentials + .iter() + .filter(|c| c.credential_type == "FreeBandwidthPass") + { + if credential.expired { + continue; + } + if !self.freepass_spent(credential.id, gateway_id).await { + return Some(credential.clone()); + } + } + None } /// Consumes in the database the specified credential. @@ -67,11 +110,12 @@ impl CoconutCredentialManager { /// # Arguments /// /// * `id`: Database id. - pub async fn consume_coconut_credential(&self, id: i64) { - let mut creds = self.inner.write().await; - if let Some(cred) = creds.data.get_mut(id as usize) { - cred.consumed = true; - } + pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) { + let mut guard = self.inner.write().await; + guard.credential_usage.push(CredentialUsage { + credential_id: id, + gateway_id_bs58: gateway_id.to_string(), + }); } /// Marks the specified credential as expired @@ -81,7 +125,7 @@ impl CoconutCredentialManager { /// * `id`: Id of the credential to mark as expired. pub async fn mark_expired(&self, id: i64) { let mut creds = self.inner.write().await; - if let Some(cred) = creds.data.get_mut(id as usize) { + if let Some(cred) = creds.credentials.get_mut(id as usize) { cred.expired = true; } } diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 50465f434d..c356e09820 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -27,19 +27,52 @@ impl CoconutCredentialManager { ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, consumed, expired) - VALUES (?, ?, ?, ?, false, false) + INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired) + VALUES (?, ?, ?, ?, false) "#, serialization_revision, credential_type, credential_data, epoch_id ).execute(&self.connection_pool).await?; Ok(()) } - pub async fn get_next_unspent_credential( + pub async fn get_next_unspect_freepass( + &self, + gateway_id: &str, + ) -> Result, sqlx::Error> { + // get a credential of freepass type that doesn't appear in `credential_usage` for the provided gateway_id + sqlx::query_as( + r#" + SELECT * + FROM coconut_credentials + WHERE coconut_credentials.credential_type == "FreeBandwidthPass" + AND NOT EXISTS (SELECT 1 + FROM credential_usage + WHERE credential_usage.credential_id = coconut_credentials.id + AND credential_usage.gateway_id_bs58 == ?) + ORDER BY coconut_credentials.id + LIMIT 1 + "#, + ) + .bind(gateway_id) + .fetch_optional(&self.connection_pool) + .await + } + + pub async fn get_next_unspect_bandwidth_voucher( &self, ) -> Result, sqlx::Error> { + // get a credential of bandwidth voucher type that doesn't appear in `credential_usage` for any gateway_id sqlx::query_as( - "SELECT * FROM coconut_credentials WHERE NOT consumed AND NOT expired LIMIT 1", + r#" + SELECT * + FROM coconut_credentials + WHERE coconut_credentials.credential_type == "BandwidthVoucher" + AND NOT EXISTS (SELECT 1 + FROM credential_usage + WHERE credential_usage.credential_id = coconut_credentials.id) + ORDER BY coconut_credentials.id + LIMIT 1 + "#, ) .fetch_optional(&self.connection_pool) .await @@ -50,10 +83,16 @@ impl CoconutCredentialManager { /// # Arguments /// /// * `id`: Database id. - pub async fn consume_coconut_credential(&self, id: i64) -> Result<(), sqlx::Error> { + /// * `gateway_id`: id of the gateway that received the credential + pub async fn consume_coconut_credential( + &self, + id: i64, + gateway_id: &str, + ) -> Result<(), sqlx::Error> { sqlx::query!( - "UPDATE coconut_credentials SET consumed = TRUE WHERE id = ?", - id + "INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)", + id, + gateway_id ) .execute(&self.connection_pool) .await?; diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index 5b1b5b23e9..97d456ea46 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -44,16 +44,30 @@ impl Storage for EphemeralStorage { async fn get_next_unspent_credential( &self, + gateway_id: &str, ) -> Result, Self::StorageError> { + // first try to get a free pass if available, otherwise fallback to bandwidth voucher + let maybe_freepass = self + .coconut_credential_manager + .get_next_unspect_freepass(gateway_id) + .await; + if maybe_freepass.is_some() { + return Ok(maybe_freepass); + } + Ok(self .coconut_credential_manager - .get_next_unspent_credential() + .get_next_unspect_bandwidth_voucher() .await) } - async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError> { + async fn consume_coconut_credential( + &self, + id: i64, + gateway_id: &str, + ) -> Result<(), StorageError> { self.coconut_credential_manager - .consume_coconut_credential(id) + .consume_coconut_credential(id, gateway_id) .await; Ok(()) diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 5af5a3c089..49c004ece0 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -26,7 +26,6 @@ pub struct StoredIssuedCredential { pub credential_type: String, pub epoch_id: u32, - pub consumed: bool, pub expired: bool, } @@ -37,3 +36,9 @@ pub struct StorableIssuedCredential<'a> { pub epoch_id: u32, } + +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct CredentialUsage { + pub credential_id: i64, + pub gateway_id_bs58: String, +} diff --git a/common/credential-storage/src/persistent_storage.rs b/common/credential-storage/src/persistent_storage.rs index 8d0961c38d..f03d1abf68 100644 --- a/common/credential-storage/src/persistent_storage.rs +++ b/common/credential-storage/src/persistent_storage.rs @@ -76,16 +76,30 @@ impl Storage for PersistentStorage { async fn get_next_unspent_credential( &self, + gateway_id: &str, ) -> Result, Self::StorageError> { + // first try to get a free pass if available, otherwise fallback to bandwidth voucher + let maybe_freepass = self + .coconut_credential_manager + .get_next_unspect_freepass(gateway_id) + .await?; + if maybe_freepass.is_some() { + return Ok(maybe_freepass); + } + Ok(self .coconut_credential_manager - .get_next_unspent_credential() + .get_next_unspect_bandwidth_voucher() .await?) } - async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError> { + async fn consume_coconut_credential( + &self, + id: i64, + gateway_id: &str, + ) -> Result<(), StorageError> { self.coconut_credential_manager - .consume_coconut_credential(id) + .consume_coconut_credential(id, gateway_id) .await?; Ok(()) diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index 8f8cb7798e..7880a3722e 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -18,6 +18,7 @@ pub trait Storage: Send + Sync { /// that is also not marked as expired async fn get_next_unspent_credential( &self, + gateway_id: &str, ) -> Result, Self::StorageError>; /// Marks as consumed in the database the specified credential. @@ -25,7 +26,12 @@ pub trait Storage: Send + Sync { /// # Arguments /// /// * `id`: Id of the credential to be consumed. - async fn consume_coconut_credential(&self, id: i64) -> Result<(), Self::StorageError>; + /// * `gateway_id`: id of the gateway that received the credential. + async fn consume_coconut_credential( + &self, + id: i64, + gateway_id: &str, + ) -> Result<(), Self::StorageError>; /// Marks the specified credential as expired /// diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index bc97f67df4..5e095beeab 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -381,3 +381,18 @@ mod sphinx_key_conversion { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn private_key_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index ffbcac88ac..7d1db5a8c1 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -359,3 +359,18 @@ impl<'d> Deserialize<'d> for Signature { Signature::from_bytes(bytes.as_ref()).map_err(SerdeError::custom) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn private_key_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 17968821bb..55037c50ee 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -4,6 +4,7 @@ use crate::var_names::{DEPRECATED_API_VALIDATOR, DEPRECATED_NYMD_VALIDATOR, NYM_API, NYXD}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::net::{IpAddr, Ipv4Addr}; use std::path::Path; use std::{ env::{var, VarError}, @@ -521,4 +522,5 @@ pub const WG_PORT: u16 = 51822; // The interface used to route traffic pub const WG_TUN_BASE_NAME: &str = "nymwg"; pub const WG_TUN_DEVICE_ADDRESS: &str = "10.1.0.1"; +pub const WG_TUN_DEVICE_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(10, 1, 0, 1)); pub const WG_TUN_DEVICE_NETMASK: &str = "255.255.255.0"; diff --git a/common/nymsphinx/acknowledgements/src/key.rs b/common/nymsphinx/acknowledgements/src/key.rs index d3615c3718..869f713657 100644 --- a/common/nymsphinx/acknowledgements/src/key.rs +++ b/common/nymsphinx/acknowledgements/src/key.rs @@ -77,3 +77,18 @@ impl PemStorableKey for AckKey { Self::try_from_bytes(bytes) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_zeroize_on_drop() {} + + fn assert_zeroize() {} + + #[test] + fn ack_key_is_zeroized() { + assert_zeroize::(); + assert_zeroize_on_drop::(); + } +} diff --git a/common/socks5-client-core/Cargo.toml b/common/socks5-client-core/Cargo.toml index 80aefcf934..52fc4d6d0b 100644 --- a/common/socks5-client-core/Cargo.toml +++ b/common/socks5-client-core/Cargo.toml @@ -22,7 +22,7 @@ tokio = { version = "1.24.1", features = ["rt-multi-thread", "net", "signal"] } url = { workspace = true } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-client-core = { path = "../client-core", features = ["fs-surb-storage"] } +nym-client-core = { path = "../client-core", features = ["fs-surb-storage", "fs-gateways-storage"] } nym-config = { path = "../config" } nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-credential-storage = { path = "../credential-storage" } diff --git a/common/socks5-client-core/src/config/mod.rs b/common/socks5-client-core/src/config/mod.rs index 8c5c722093..e4b3a8b623 100644 --- a/common/socks5-client-core/src/config/mod.rs +++ b/common/socks5-client-core/src/config/mod.rs @@ -12,6 +12,7 @@ use std::str::FromStr; pub mod old_config_v1_1_20_2; pub mod old_config_v1_1_30; +pub mod old_config_v1_1_33; pub use nym_service_providers_common::interface::ProviderInterfaceVersion; pub use nym_socks5_requests::Socks5ProtocolVersion; @@ -47,13 +48,13 @@ impl Config { #[must_use] pub fn with_port(mut self, port: u16) -> Self { - self.socks5.bind_adddress = SocketAddr::new(self.socks5.bind_adddress.ip(), port); + self.socks5.bind_address = SocketAddr::new(self.socks5.bind_address.ip(), port); self } #[must_use] pub fn with_ip(mut self, ip: IpAddr) -> Self { - self.socks5.bind_adddress = SocketAddr::new(ip, self.socks5.bind_adddress.port()); + self.socks5.bind_address = SocketAddr::new(ip, self.socks5.bind_address.port()); self } @@ -112,7 +113,7 @@ impl Config { pub struct Socks5 { /// The address on which the client will be listening for incoming requests /// (default: 127.0.0.1:1080) - pub bind_adddress: SocketAddr, + pub bind_address: SocketAddr, /// The mix address of the provider to which all requests are going to be sent. pub provider_mix_address: String, @@ -141,7 +142,7 @@ pub struct Socks5 { impl Socks5 { pub fn new>(provider_mix_address: S) -> Self { Socks5 { - bind_adddress: SocketAddr::new( + bind_address: SocketAddr::new( IpAddr::V4(Ipv4Addr::LOCALHOST), DEFAULT_SOCKS5_LISTENING_PORT, ), diff --git a/common/socks5-client-core/src/config/old_config_v1_1_30.rs b/common/socks5-client-core/src/config/old_config_v1_1_30.rs index 3f3f877797..13591f7d9a 100644 --- a/common/socks5-client-core/src/config/old_config_v1_1_30.rs +++ b/common/socks5-client-core/src/config/old_config_v1_1_30.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::{Config, Socks5, Socks5Debug}; +use super::old_config_v1_1_33::{ConfigV1_1_33, Socks5DebugV1_1_33, Socks5V1_1_33}; pub use nym_client_core::config::old_config_v1_1_30::ConfigV1_1_30 as BaseClientConfigV1_1_30; use serde::{Deserialize, Serialize}; use std::fmt::Debug; @@ -23,9 +23,9 @@ pub struct ConfigV1_1_30 { pub socks5: Socks5V1_1_30, } -impl From for Config { +impl From for ConfigV1_1_33 { fn from(value: ConfigV1_1_30) -> Self { - Config { + ConfigV1_1_33 { base: value.base.into(), socks5: value.socks5.into(), } @@ -62,11 +62,11 @@ pub struct Socks5V1_1_30 { pub socks5_debug: Socks5DebugV1_1_30, } -impl From for Socks5 { +impl From for Socks5V1_1_33 { fn from(value: Socks5V1_1_30) -> Self { - Socks5 { + Socks5V1_1_33 { // in <= 1.1.30 the address was hardcoded to 127.0.0.1 - bind_adddress: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), value.listening_port), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), value.listening_port), provider_mix_address: value.provider_mix_address, provider_interface_version: value.provider_interface_version, socks5_protocol_version: value.socks5_protocol_version, @@ -86,9 +86,9 @@ pub struct Socks5DebugV1_1_30 { pub per_request_surbs: u32, } -impl From for Socks5Debug { +impl From for Socks5DebugV1_1_33 { fn from(value: Socks5DebugV1_1_30) -> Self { - Socks5Debug { + Socks5DebugV1_1_33 { connection_start_surbs: value.connection_start_surbs, per_request_surbs: value.per_request_surbs, } diff --git a/common/socks5-client-core/src/config/old_config_v1_1_33.rs b/common/socks5-client-core/src/config/old_config_v1_1_33.rs new file mode 100644 index 0000000000..715b9d108a --- /dev/null +++ b/common/socks5-client-core/src/config/old_config_v1_1_33.rs @@ -0,0 +1,107 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{Config, Socks5, Socks5Debug}; +pub use nym_client_core::config::old_config_v1_1_33::ConfigV1_1_33 as BaseClientConfigV1_1_33; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::net::SocketAddr; + +// TODO: those should really be redefined here in case we change them... +use nym_service_providers_common::interface::ProviderInterfaceVersion; +use nym_socks5_requests::Socks5ProtocolVersion; + +const DEFAULT_CONNECTION_START_SURBS: u32 = 20; +const DEFAULT_PER_REQUEST_SURBS: u32 = 3; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_33 { + #[serde(flatten)] + pub base: BaseClientConfigV1_1_33, + + pub socks5: Socks5V1_1_33, +} + +impl From for Config { + fn from(value: ConfigV1_1_33) -> Self { + Config { + base: value.base.into(), + socks5: value.socks5.into(), + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Socks5V1_1_33 { + /// The address on which the client will be listening for incoming requests + /// (default: 127.0.0.1:1080) + // there was a typo in here, so accept the wrong name for the purposes of backwards compatibility + #[serde(alias = "bind_address")] + pub bind_address: SocketAddr, + + /// The mix address of the provider to which all requests are going to be sent. + pub provider_mix_address: String, + + /// The version of the 'service provider' this client is going to use in its communication with the + /// specified socks5 provider. + // if in doubt, use the legacy version as initially nobody will be using the updated binaries + #[serde(default)] + pub provider_interface_version: ProviderInterfaceVersion, + + #[serde(default)] + pub socks5_protocol_version: Socks5ProtocolVersion, + + /// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. + /// While this is going to hide its actual address information, it will make the actual communication + /// slower and consume nearly double the bandwidth as it will require sending reply SURBs. + /// + /// Note that some service providers might not support this. + #[serde(default)] + pub send_anonymously: bool, + + #[serde(default)] + pub socks5_debug: Socks5DebugV1_1_33, +} + +impl From for Socks5 { + fn from(value: Socks5V1_1_33) -> Self { + Socks5 { + bind_address: value.bind_address, + provider_mix_address: value.provider_mix_address, + provider_interface_version: value.provider_interface_version, + socks5_protocol_version: value.socks5_protocol_version, + send_anonymously: value.send_anonymously, + socks5_debug: value.socks5_debug.into(), + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct Socks5DebugV1_1_33 { + /// Number of reply SURBs attached to each `Request::Connect` message. + pub connection_start_surbs: u32, + + /// Number of reply SURBs attached to each `Request::Send` message. + pub per_request_surbs: u32, +} + +impl From for Socks5Debug { + fn from(value: Socks5DebugV1_1_33) -> Self { + Socks5Debug { + connection_start_surbs: value.connection_start_surbs, + per_request_surbs: value.per_request_surbs, + } + } +} + +impl Default for Socks5DebugV1_1_33 { + fn default() -> Self { + Socks5DebugV1_1_33 { + connection_start_surbs: DEFAULT_CONNECTION_START_SURBS, + per_request_surbs: DEFAULT_PER_REQUEST_SURBS, + } + } +} diff --git a/common/socks5-client-core/src/lib.rs b/common/socks5-client-core/src/lib.rs index ecef31354f..41a7105dad 100644 --- a/common/socks5-client-core/src/lib.rs +++ b/common/socks5-client-core/src/lib.rs @@ -11,7 +11,7 @@ use futures::channel::mpsc; use futures::StreamExt; use log::*; use nym_client_core::client::base_client::non_wasm_helpers::default_query_dkg_client_from_config; -use nym_client_core::client::base_client::storage::gateway_details::GatewayDetailsStore; +use nym_client_core::client::base_client::storage::GatewaysDetailsStore; use nym_client_core::client::base_client::storage::MixnetClientStorage; use nym_client_core::client::base_client::{ BaseClientBuilder, ClientInput, ClientOutput, ClientState, @@ -71,14 +71,14 @@ where S::ReplyStore: Send + Sync, ::StorageError: Sync + Send, ::StorageError: Send + Sync, - ::StorageError: Sync + Send, + ::StorageError: Sync + Send, ::StorageError: Send + Sync, { pub fn new(config: Config, storage: S, custom_mixnet: Option) -> Self { NymClient { config, storage, - setup_method: GatewaySetup::MustLoad, + setup_method: GatewaySetup::MustLoad { gateway_id: None }, custom_mixnet, } } @@ -124,7 +124,7 @@ where let authenticator = Authenticator::new(auth_methods, allowed_users); let mut sphinx_socks = NymSocksServer::new( - socks5_config.bind_adddress, + socks5_config.bind_address, authenticator, socks5_config.get_provider_mix_address(), self_address, diff --git a/common/socks5-client-core/src/socks/server.rs b/common/socks5-client-core/src/socks/server.rs index a67ed64e19..1a3fc22f3f 100644 --- a/common/socks5-client-core/src/socks/server.rs +++ b/common/socks5-client-core/src/socks/server.rs @@ -33,7 +33,7 @@ impl NymSocksServer { /// Create a new SphinxSocks instance #[allow(clippy::too_many_arguments)] pub(crate) fn new( - bind_adddress: SocketAddr, + bind_address: SocketAddr, authenticator: Authenticator, service_provider: Recipient, self_address: Recipient, @@ -42,10 +42,10 @@ impl NymSocksServer { shutdown: TaskClient, packet_type: PacketType, ) -> Self { - info!("Listening on {bind_adddress}"); + info!("Listening on {bind_address}"); NymSocksServer { authenticator, - listening_address: bind_adddress, + listening_address: bind_address, service_provider, self_address, client_config, diff --git a/common/wasm/client-core/Cargo.toml b/common/wasm/client-core/Cargo.toml index 3e8ec57163..5e6a701883 100644 --- a/common/wasm/client-core/Cargo.toml +++ b/common/wasm/client-core/Cargo.toml @@ -15,6 +15,7 @@ rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { workspace = true, features = ["derive"] } serde-wasm-bindgen = { workspace = true } thiserror = { workspace = true } +time = { workspace = true, features = ["wasm-bindgen"] } tsify = { workspace = true, features = ["js"] } url = { workspace = true } wasm-bindgen = { workspace = true } diff --git a/common/wasm/client-core/src/error.rs b/common/wasm/client-core/src/error.rs index 01cbbcd757..f80327265a 100644 --- a/common/wasm/client-core/src/error.rs +++ b/common/wasm/client-core/src/error.rs @@ -4,7 +4,7 @@ use crate::storage::wasm_client_traits::WasmClientStorageError; use crate::topology::WasmTopologyError; use js_sys::Promise; -use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::client::base_client::storage::gateways_storage::BadGateway; use nym_client_core::error::ClientCoreError; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; use nym_gateway_client::error::GatewayClientError; @@ -89,10 +89,14 @@ pub enum WasmCoreError { source: WasmClientStorageError, }, - #[error("this client has already registered with a gateway: {gateway_config:?}")] - AlreadyRegistered { - gateway_config: GatewayEndpointConfig, + #[error(transparent)] + MalformedGateway { + #[from] + source: BadGateway, }, + + #[error("this client has already registered with a gateway: {gateway_id:?}")] + AlreadyRegistered { gateway_id: String }, } wasm_error!(WasmCoreError); diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 6b59d30d80..80c9e90135 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -90,8 +90,15 @@ pub async fn setup_gateway_wasm( chosen_gateway: Option, gateways: &[gateway::Node], ) -> Result { - let setup = if client_store.has_full_gateway_info().await? { - GatewaySetup::MustLoad + // TODO: so much optimization and extra features could be added here, but that's for the future + + let setup = if client_store + .get_active_gateway_id() + .await? + .active_gateway_id_bs58 + .is_some() + { + GatewaySetup::MustLoad { gateway_id: None } } else { let selection_spec = GatewaySelectionSpecification::new(chosen_gateway.clone(), None, force_tls); @@ -99,7 +106,7 @@ pub async fn setup_gateway_wasm( GatewaySetup::New { specification: selection_spec, available_gateways: gateways.to_vec(), - overwrite_data: false, + wg_tun_address: None, } }; diff --git a/common/wasm/client-core/src/lib.rs b/common/wasm/client-core/src/lib.rs index 568b2b459e..5204b75925 100644 --- a/common/wasm/client-core/src/lib.rs +++ b/common/wasm/client-core/src/lib.rs @@ -16,7 +16,7 @@ pub mod topology; pub use nym_bandwidth_controller::BandwidthController; pub use nym_client_core::*; pub use nym_client_core::{ - client::key_manager::ManagedKeys, error::ClientCoreError, init::types::InitialisationResult, + client::key_manager::ClientKeys, error::ClientCoreError, init::types::InitialisationResult, }; pub use nym_gateway_client::{error::GatewayClientError, GatewayClient, GatewayConfig}; pub use nym_sphinx::{ diff --git a/common/wasm/client-core/src/storage/core_client_traits.rs b/common/wasm/client-core/src/storage/core_client_traits.rs index b017bb3914..20105ac339 100644 --- a/common/wasm/client-core/src/storage/core_client_traits.rs +++ b/common/wasm/client-core/src/storage/core_client_traits.rs @@ -7,12 +7,12 @@ use crate::helpers::setup_reply_surb_storage_backend; use crate::storage::wasm_client_traits::WasmClientStorage; use crate::storage::ClientStorage; use async_trait::async_trait; -use nym_client_core::client::base_client::storage::gateway_details::{ - GatewayDetailsStore, PersistedGatewayDetails, +use nym_client_core::client::base_client::storage::{ + gateways_storage::{ActiveGateway, GatewayRegistration, GatewaysDetailsStore}, + MixnetClientStorage, }; -use nym_client_core::client::base_client::storage::MixnetClientStorage; use nym_client_core::client::key_manager::persistence::KeyStore; -use nym_client_core::client::key_manager::KeyManager; +use nym_client_core::client::key_manager::ClientKeys; use nym_client_core::client::replies::reply_storage::browser_backend; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; use wasm_utils::console_log; @@ -41,7 +41,7 @@ impl MixnetClientStorage for FullWasmClientStorage { type ReplyStore = browser_backend::Backend; type CredentialStore = EphemeralCredentialStorage; - type GatewayDetailsStore = ClientStorage; + type GatewaysDetailsStore = ClientStorage; fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { (self.reply_storage, self.credential_storage) @@ -59,7 +59,7 @@ impl MixnetClientStorage for FullWasmClientStorage { &self.credential_storage } - fn gateway_details_store(&self) -> &Self::GatewayDetailsStore { + fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore { &self.keys_and_gateway_store } } @@ -68,24 +68,22 @@ impl MixnetClientStorage for FullWasmClientStorage { impl KeyStore for ClientStorage { type StorageError = WasmCoreError; - async fn load_keys(&self) -> Result { + async fn load_keys(&self) -> Result { console_log!("attempting to load cryptographic keys..."); // all keys implement `ZeroizeOnDrop`, so if we return an Error, whatever was already loaded will be cleared let identity_keypair = self.must_read_identity_keypair().await?; let encryption_keypair = self.must_read_encryption_keypair().await?; let ack_keypair = self.must_read_ack_key().await?; - let gateway_shared_key = self.must_read_gateway_shared_key().await?; - Ok(KeyManager::from_keys( + Ok(ClientKeys::from_keys( identity_keypair, encryption_keypair, - Some(gateway_shared_key), ack_keypair, )) } - async fn store_keys(&self, keys: &KeyManager) -> Result<(), Self::StorageError> { + async fn store_keys(&self, keys: &ClientKeys) -> Result<(), Self::StorageError> { console_log!("attempting to store cryptographic keys..."); self.store_identity_keypair(&keys.identity_keypair()) @@ -94,25 +92,57 @@ impl KeyStore for ClientStorage { .await?; self.store_ack_key(&keys.ack_key()).await?; - if let Some(shared_keys) = keys.gateway_shared_key() { - self.store_gateway_shared_key(&shared_keys).await?; - } Ok(()) } } #[async_trait(?Send)] -impl GatewayDetailsStore for ClientStorage { +impl GatewaysDetailsStore for ClientStorage { type StorageError = WasmCoreError; - async fn load_gateway_details(&self) -> Result { - self.must_read_gateway_details().await + async fn active_gateway(&self) -> Result { + let raw_active = self.get_active_gateway_id().await?; + let registration = match raw_active.active_gateway_id_bs58 { + None => None, + Some(gateway_id) => Some(self.load_gateway_details(&gateway_id).await?), + }; + + Ok(ActiveGateway { registration }) + } + + async fn set_active_gateway(&self, gateway_id: &str) -> Result<(), Self::StorageError> { + ::set_active_gateway(self, Some(gateway_id)).await?; + Ok(()) + } + + async fn all_gateways(&self) -> Result, Self::StorageError> { + todo!() + // let identities = self.all + } + + async fn has_gateway_details(&self, gateway_id: &str) -> Result { + self.has_registered_gateway(gateway_id).await + } + + async fn load_gateway_details( + &self, + gateway_id: &str, + ) -> Result { + Ok(self + .must_get_registered_gateway(gateway_id) + .await? + .try_into()?) } async fn store_gateway_details( &self, - details: &PersistedGatewayDetails, + details: &GatewayRegistration, ) -> Result<(), Self::StorageError> { - ::store_gateway_details(self, details).await + let raw_registration = details.into(); + self.store_registered_gateway(&raw_registration).await + } + + async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> { + self.remove_registered_gateway(gateway_id).await } } diff --git a/common/wasm/client-core/src/storage/mod.rs b/common/wasm/client-core/src/storage/mod.rs index c94e8fc599..c210dc1dcc 100644 --- a/common/wasm/client-core/src/storage/mod.rs +++ b/common/wasm/client-core/src/storage/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::WasmCoreError; -use crate::storage::wasm_client_traits::{v1, WasmClientStorage}; +use crate::storage::wasm_client_traits::{v1, v2, WasmClientStorage}; use async_trait::async_trait; use js_sys::{Array, Promise}; use serde::de::DeserializeOwned; @@ -11,14 +11,15 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; use wasm_storage::traits::BaseWasmStorage; use wasm_storage::{IdbVersionChangeEvent, WasmStorage}; -use wasm_utils::error::PromisableResult; +use wasm_utils::error::{simple_js_error, PromisableResult}; use zeroize::Zeroizing; pub mod core_client_traits; +mod types; pub mod wasm_client_traits; const STORAGE_NAME_PREFIX: &str = "wasm-client-storage"; -const STORAGE_VERSION: u32 = 1; +const STORAGE_VERSION: u32 = 2; #[wasm_bindgen] pub struct ClientStorage { @@ -48,13 +49,21 @@ impl ClientStorage { // works with an unsigned integer. // See let old_version = evt.old_version() as u32; + let db = evt.db(); if old_version < 1 { - // migrating to version 1 - let db = evt.db(); + // migrating to version 2 db.create_object_store(v1::KEYS_STORE)?; db.create_object_store(v1::CORE_STORE)?; + + db.create_object_store(v2::GATEWAY_REGISTRATIONS_ACTIVE_GATEWAY_STORE)?; + db.create_object_store(v2::GATEWAY_REGISTRATIONS_REGISTERED_GATEWAYS_STORE)?; + } + + // version 1 -> unimplemented migration + if old_version < 2 { + return Err(simple_js_error("this client is incompatible with existing storage. please initialise it again.")); } Ok(()) diff --git a/common/wasm/client-core/src/storage/types.rs b/common/wasm/client-core/src/storage/types.rs new file mode 100644 index 0000000000..e0cc790739 --- /dev/null +++ b/common/wasm/client-core/src/storage/types.rs @@ -0,0 +1,62 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_client_core::client::base_client::storage::gateways_storage::{ + BadGateway, GatewayDetails, GatewayRegistration, RawRemoteGatewayDetails, RemoteGatewayDetails, +}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +// a more nested struct since we only have a single gateway type in wasm (no 'custom') +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WasmRawRegisteredGateway { + pub gateway_id_bs58: String, + + pub registration_timestamp: OffsetDateTime, + + pub derived_aes128_ctr_blake3_hmac_keys_bs58: String, + + pub gateway_owner_address: String, + + pub gateway_listener: String, +} + +impl TryFrom for GatewayRegistration { + type Error = BadGateway; + + fn try_from(value: WasmRawRegisteredGateway) -> Result { + // offload some parsing to an existing impl + let raw_remote = RawRemoteGatewayDetails { + gateway_id_bs58: value.gateway_id_bs58, + derived_aes128_ctr_blake3_hmac_keys_bs58: value + .derived_aes128_ctr_blake3_hmac_keys_bs58, + gateway_owner_address: value.gateway_owner_address, + gateway_listener: value.gateway_listener, + wg_tun_address: None, + }; + let remote: RemoteGatewayDetails = raw_remote.try_into()?; + + Ok(GatewayRegistration { + details: GatewayDetails::Remote(remote), + registration_timestamp: value.registration_timestamp, + }) + } +} + +impl<'a> From<&'a GatewayRegistration> for WasmRawRegisteredGateway { + fn from(value: &'a GatewayRegistration) -> Self { + let GatewayDetails::Remote(remote_details) = &value.details else { + panic!("somehow obtained custom gateway registration in wasm!") + }; + + WasmRawRegisteredGateway { + gateway_id_bs58: remote_details.gateway_id.to_string(), + registration_timestamp: value.registration_timestamp, + derived_aes128_ctr_blake3_hmac_keys_bs58: remote_details + .derived_aes128_ctr_blake3_hmac_keys + .to_base58_string(), + gateway_owner_address: remote_details.gateway_owner_address.to_string(), + gateway_listener: remote_details.gateway_listener.to_string(), + } + } +} diff --git a/common/wasm/client-core/src/storage/wasm_client_traits.rs b/common/wasm/client-core/src/storage/wasm_client_traits.rs index 83e0ca1340..7c1b8e3e02 100644 --- a/common/wasm/client-core/src/storage/wasm_client_traits.rs +++ b/common/wasm/client-core/src/storage/wasm_client_traits.rs @@ -1,10 +1,10 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::storage::types::WasmRawRegisteredGateway; use async_trait::async_trait; -use nym_client_core::client::base_client::storage::gateway_details::PersistedGatewayDetails; +use nym_client_core::client::base_client::storage::gateways_storage::RawActiveGateway; use nym_crypto::asymmetric::{encryption, identity}; -use nym_gateway_client::SharedKeys; use nym_sphinx_acknowledgements::AckKey; use std::error::Error; use thiserror::Error; @@ -19,14 +19,22 @@ pub(crate) mod v1 { // keys // pub const CONFIG: &str = "config"; - pub const GATEWAY_DETAILS: &str = "gateway_details"; + // pub const GATEWAY_DETAILS: &str = "gateway_details"; pub const ED25519_IDENTITY_KEYPAIR: &str = "ed25519_identity_keypair"; pub const X25519_ENCRYPTION_KEYPAIR: &str = "x25519_encryption_keypair"; // TODO: for those we could actually use the subtle crypto storage pub const AES128CTR_ACK_KEY: &str = "aes128ctr_ack_key"; - pub const AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS: &str = "aes128ctr_blake3_hmac_gateway_keys"; + // pub const AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS: &str = "aes128ctr_blake3_hmac_gateway_keys"; +} + +pub(crate) mod v2 { + pub const GATEWAY_REGISTRATIONS_ACTIVE_GATEWAY_STORE: &str = "active_gateway"; + pub const ACTIVE_GATEWAY_KEY: &str = "active_gateway"; + + // there's no concept of 'custom' gateways in wasm so the store is simpler + pub const GATEWAY_REGISTRATIONS_REGISTERED_GATEWAYS_STORE: &str = "gateway_registrations"; } #[derive(Debug, Error)] @@ -34,32 +42,20 @@ pub enum WasmClientStorageError { #[error("{typ} cryptographic key is not available in storage")] CryptoKeyNotInStorage { typ: String }, - #[error("the prior gateway details are not available in the storage")] - GatewayDetailsNotInStorage, + #[error( + "the prior gateway details for gateway {gateway_id:?} are not available in the storage" + )] + GatewayDetailsNotInStorage { gateway_id: String }, } #[async_trait(?Send)] +#[async_trait] pub trait WasmClientStorage: BaseWasmStorage { type StorageError: Error + From<::StorageError> + From; - async fn may_read_gateway_details( - &self, - ) -> Result, ::StorageError> { - self.read_value(v1::CORE_STORE, JsValue::from_str(v1::GATEWAY_DETAILS)) - .await - .map_err(Into::into) - } - - async fn must_read_gateway_details( - &self, - ) -> Result::StorageError> { - self.may_read_gateway_details() - .await? - .ok_or(WasmClientStorageError::GatewayDetailsNotInStorage) - .map_err(Into::into) - } + // keys: async fn may_read_identity_keypair( &self, @@ -91,17 +87,6 @@ pub trait WasmClientStorage: BaseWasmStorage { .map_err(Into::into) } - async fn may_read_gateway_shared_key( - &self, - ) -> Result, ::StorageError> { - self.read_value( - v1::KEYS_STORE, - JsValue::from_str(v1::AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS), - ) - .await - .map_err(Into::into) - } - async fn must_read_identity_keypair( &self, ) -> Result::StorageError> { @@ -133,17 +118,6 @@ pub trait WasmClientStorage: BaseWasmStorage { .map_err(Into::into) } - async fn must_read_gateway_shared_key( - &self, - ) -> Result::StorageError> { - self.may_read_gateway_shared_key() - .await? - .ok_or(WasmClientStorageError::CryptoKeyNotInStorage { - typ: v1::AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS.to_string(), - }) - .map_err(Into::into) - } - async fn store_identity_keypair( &self, keypair: &identity::KeyPair, @@ -183,38 +157,112 @@ pub trait WasmClientStorage: BaseWasmStorage { .map_err(Into::into) } - async fn store_gateway_shared_key( + // gateways: + + async fn get_active_gateway_id( &self, - key: &SharedKeys, + ) -> Result::StorageError> { + let maybe_active: Option = self + .read_value( + v2::GATEWAY_REGISTRATIONS_ACTIVE_GATEWAY_STORE, + JsValue::from_str(v2::ACTIVE_GATEWAY_KEY), + ) + .await?; + + // a 'temporary' hack + // (proper solution: make sure to insert empty value during db creation) + Ok(RawActiveGateway { + active_gateway_id_bs58: maybe_active.and_then(|a| a.active_gateway_id_bs58), + }) + } + + async fn set_active_gateway( + &self, + gateway_id: Option<&str>, ) -> Result<(), ::StorageError> { self.store_value( - v1::KEYS_STORE, - JsValue::from_str(v1::AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS), - key, + v2::GATEWAY_REGISTRATIONS_ACTIVE_GATEWAY_STORE, + JsValue::from_str(v2::ACTIVE_GATEWAY_KEY), + &RawActiveGateway { + active_gateway_id_bs58: gateway_id.map(|id| id.to_string()), + }, ) .await .map_err(Into::into) } - async fn store_gateway_details( + async fn maybe_get_registered_gateway( &self, - gateway_endpoint: &PersistedGatewayDetails, - ) -> Result<(), ::StorageError> { - self.store_value( - v1::CORE_STORE, - JsValue::from_str(v1::GATEWAY_DETAILS), - gateway_endpoint, + gateway_id: &str, + ) -> Result, ::StorageError> { + self.read_value( + v2::GATEWAY_REGISTRATIONS_REGISTERED_GATEWAYS_STORE, + JsValue::from_str(gateway_id), ) .await .map_err(Into::into) } - async fn has_full_gateway_info( + async fn must_get_registered_gateway( &self, + gateway_id: &str, + ) -> Result::StorageError> { + self.maybe_get_registered_gateway(gateway_id) + .await? + .ok_or(WasmClientStorageError::GatewayDetailsNotInStorage { + gateway_id: gateway_id.to_string(), + }) + .map_err(Into::into) + } + + async fn store_registered_gateway( + &self, + registered_gateway: &WasmRawRegisteredGateway, + ) -> Result<(), ::StorageError> { + self.store_value( + v2::GATEWAY_REGISTRATIONS_REGISTERED_GATEWAYS_STORE, + JsValue::from_str(®istered_gateway.gateway_id_bs58), + registered_gateway, + ) + .await + .map_err(Into::into) + } + + async fn remove_registered_gateway( + &self, + gateway_id: &str, + ) -> Result<(), ::StorageError> { + self.remove_value( + v2::GATEWAY_REGISTRATIONS_REGISTERED_GATEWAYS_STORE, + JsValue::from_str(gateway_id), + ) + .await + .map_err(Into::into) + } + + async fn has_registered_gateway( + &self, + gateway_id: &str, ) -> Result::StorageError> { - let has_keys = self.may_read_gateway_shared_key().await?.is_some(); - let has_details = self.may_read_gateway_details().await?.is_some(); + self.has_value( + v2::GATEWAY_REGISTRATIONS_REGISTERED_GATEWAYS_STORE, + JsValue::from_str(gateway_id), + ) + .await + .map_err(Into::into) + } - Ok(has_keys && has_details) + async fn registered_gateways( + &self, + ) -> Result, ::StorageError> { + self.get_all_keys(v2::GATEWAY_REGISTRATIONS_REGISTERED_GATEWAYS_STORE) + .await + .map_err(Into::into) + .map(|arr| { + arr.to_vec() + .into_iter() + .filter_map(|key| key.as_string()) + .collect() + }) } } diff --git a/common/wasm/client-core/src/topology.rs b/common/wasm/client-core/src/topology.rs index 8c84dee4a1..25300c41a4 100644 --- a/common/wasm/client-core/src/topology.rs +++ b/common/wasm/client-core/src/topology.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_client_core::config::GatewayEndpointConfig; use nym_topology::SerializableTopologyError; use nym_validator_client::client::IdentityKeyRef; use wasm_utils::console_log; @@ -17,10 +16,6 @@ pub type WasmTopologyError = SerializableTopologyError; pub trait SerializableTopologyExt { fn print(&self); - fn ensure_contains(&self, gateway_config: &GatewayEndpointConfig) -> bool { - self.ensure_contains_gateway_id(&gateway_config.gateway_id) - } - fn ensure_contains_gateway_id(&self, gateway_id: IdentityKeyRef) -> bool; } diff --git a/gateway/gateway-requests/src/registration/handshake/shared_key.rs b/gateway/gateway-requests/src/registration/handshake/shared_key.rs index d3101017a4..4d4dce6e7f 100644 --- a/gateway/gateway-requests/src/registration/handshake/shared_key.rs +++ b/gateway/gateway-requests/src/registration/handshake/shared_key.rs @@ -30,7 +30,7 @@ pub struct SharedKeys { mac_key: MacKey, } -#[derive(Debug, Error)] +#[derive(Debug, Clone, Copy, Error)] pub enum SharedKeyConversionError { #[error("the string representation of the shared keys was malformed - {0}")] DecodeError(#[from] bs58::decode::Error), diff --git a/gateway/src/commands/helpers.rs b/gateway/src/commands/helpers.rs index 043288db42..968b9b14c6 100644 --- a/gateway/src/commands/helpers.rs +++ b/gateway/src/commands/helpers.rs @@ -17,10 +17,12 @@ use nym_network_defaults::var_names::NYXD; use nym_network_defaults::var_names::{BECH32_PREFIX, NYM_API, STATISTICS_SERVICE_DOMAIN_ADDRESS}; use nym_network_requester::config::BaseClientConfig; use nym_network_requester::{ - setup_gateway, GatewaySelectionSpecification, GatewaySetup, OnDiskGatewayDetails, OnDiskKeys, + generate_new_client_keys, set_active_gateway, setup_fs_gateways_storage, setup_gateway, + GatewaySetup, OnDiskKeys, }; use nym_types::gateway::{GatewayIpPacketRouterDetails, GatewayNetworkRequesterDetails}; use nym_validator_client::nyxd::AccountId; +use rand::rngs::OsRng; use std::net::IpAddr; use std::path::PathBuf; @@ -250,7 +252,7 @@ pub(crate) fn override_ip_packet_router_config( // disable poisson rate in the BASE client if the IPR option is enabled if cfg.ip_packet_router.disable_poisson_rate { - log::info!("Disabling poisson rate for ip packet router"); + info!("Disabling poisson rate for ip packet router"); cfg.set_no_poisson_process(); } @@ -275,24 +277,24 @@ pub(crate) async fn initialise_local_network_requester( let key_store = OnDiskKeys::new(nr_cfg.storage_paths.common_paths.keys.clone()); let details_store = - OnDiskGatewayDetails::new(&nr_cfg.storage_paths.common_paths.gateway_details); + setup_fs_gateways_storage(&nr_cfg.storage_paths.common_paths.gateway_registrations).await?; + + // if this is a first time client with this particular id is initialised, generated long-term keys + if !nr_cfg_path.exists() { + let mut rng = OsRng; + generate_new_client_keys(&mut rng, &key_store).await?; + } // gateway setup here is way simpler as we're 'connecting' to ourselves let init_res = setup_gateway( - GatewaySetup::New { - specification: GatewaySelectionSpecification::Custom { - gateway_identity: identity.to_base58_string(), - additional_data: Default::default(), - }, - available_gateways: vec![], - overwrite_data: false, - }, + GatewaySetup::new_inbuilt(identity), &key_store, &details_store, ) .await?; + set_active_gateway(&details_store, &init_res.gateway_id().to_base58_string()).await?; - let address = init_res.client_address()?; + let address = init_res.client_address(); if let Err(err) = save_formatted_config_to_file(&nr_cfg, nr_cfg_path) { log::error!("Failed to save the network requester config file: {err}"); @@ -348,24 +350,24 @@ pub(crate) async fn initialise_local_ip_packet_router( let key_store = OnDiskKeys::new(ip_cfg.storage_paths.common_paths.keys.clone()); let details_store = - OnDiskGatewayDetails::new(&ip_cfg.storage_paths.common_paths.gateway_details); + setup_fs_gateways_storage(&ip_cfg.storage_paths.common_paths.gateway_registrations).await?; + + // if this is a first time client with this particular id is initialised, generated long-term keys + if !ip_cfg_path.exists() { + let mut rng = OsRng; + generate_new_client_keys(&mut rng, &key_store).await?; + } // gateway setup here is way simpler as we're 'connecting' to ourselves let init_res = setup_gateway( - GatewaySetup::New { - specification: GatewaySelectionSpecification::Custom { - gateway_identity: identity.to_base58_string(), - additional_data: Default::default(), - }, - available_gateways: vec![], - overwrite_data: false, - }, + GatewaySetup::new_inbuilt(identity), &key_store, &details_store, ) .await?; + set_active_gateway(&details_store, &init_res.gateway_id().to_base58_string()).await?; - let address = init_res.client_address()?; + let address = init_res.client_address(); if let Err(err) = save_formatted_config_to_file(&ip_cfg, ip_cfg_path) { log::error!("Failed to save the ip packet router config file: {err}"); diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index b65cc062d2..010d270222 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -265,7 +265,7 @@ pub async fn execute(args: Init) -> anyhow::Result<()> { ); eprintln!("Gateway configuration completed.\n\n\n"); - output.to_stdout(&node_details(&config)?); + output.to_stdout(&node_details(&config).await?); Ok(()) } diff --git a/gateway/src/commands/node_details.rs b/gateway/src/commands/node_details.rs index 02c0ca9875..72840a8a7a 100644 --- a/gateway/src/commands/node_details.rs +++ b/gateway/src/commands/node_details.rs @@ -18,7 +18,7 @@ pub struct NodeDetails { pub async fn execute(args: NodeDetails) -> anyhow::Result<()> { let config = try_load_current_config(&args.id)?; - args.output.to_stdout(&node_details(&config)?); + args.output.to_stdout(&node_details(&config).await?); Ok(()) } diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index 3418c216ac..34c1b6be8d 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -248,7 +248,7 @@ pub async fn execute(args: Run) -> anyhow::Result<()> { show_binding_warning(config.gateway.listening_address); } - let node_details = node_details(&config)?; + let node_details = node_details(&config).await?; let gateway = crate::node::create_gateway(config, Some(nr_opts), Some(ip_opts), custom_mixnet).await?; eprintln!( diff --git a/gateway/src/node/helpers.rs b/gateway/src/node/helpers.rs index b6765cc7bb..3e2d54417a 100644 --- a/gateway/src/node/helpers.rs +++ b/gateway/src/node/helpers.rs @@ -23,7 +23,9 @@ fn display_path>(path: P) -> String { path.as_ref().display().to_string() } -pub(crate) fn node_details(config: &Config) -> Result { +pub(crate) async fn node_details( + config: &Config, +) -> Result { let gateway_identity_public_key: identity::PublicKey = load_public_key( &config.storage_paths.keys.public_identity_key_file, "gateway identity", @@ -36,7 +38,7 @@ pub(crate) fn node_details(config: &Config) -> Result Result Result>( +pub(crate) async fn load_network_requester_config>( + id: &str, + path: P, +) -> Result { + let path = path.as_ref(); + if let Ok(cfg) = read_network_requester_config(id, path) { + return Ok(cfg); + } + + nym_network_requester::config::helpers::try_upgrade_config(path).await?; + read_network_requester_config(id, path) +} + +pub(crate) async fn load_ip_packet_router_config>( + id: &str, + path: P, +) -> Result { + let path = path.as_ref(); + if let Ok(cfg) = read_ip_packet_router_config(id, path) { + return Ok(cfg); + } + + nym_ip_packet_router::config::helpers::try_upgrade_config(path).await?; + read_ip_packet_router_config(id, path) +} + +fn read_network_requester_config>( id: &str, path: P, ) -> Result { @@ -134,7 +162,7 @@ pub(crate) fn load_network_requester_config>( }) } -pub(crate) fn load_ip_packet_router_config>( +fn read_ip_packet_router_config>( id: &str, path: P, ) -> Result { diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 30e1302d1a..26ad47b689 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -62,7 +62,7 @@ pub(crate) async fn create_gateway( // don't attempt to read config if NR is disabled let network_requester_config = if config.network_requester.enabled { if let Some(path) = &config.storage_paths.network_requester_config { - let cfg = load_network_requester_config(&config.gateway.id, path)?; + let cfg = load_network_requester_config(&config.gateway.id, path).await?; Some(override_network_requester_config(cfg, nr_config_override)) } else { // if NR is enabled, the config path must be specified @@ -75,7 +75,7 @@ pub(crate) async fn create_gateway( // don't attempt to read config if NR is disabled let ip_packet_router_config = if config.ip_packet_router.enabled { if let Some(path) = &config.storage_paths.ip_packet_router_config { - let cfg = load_ip_packet_router_config(&config.gateway.id, path)?; + let cfg = load_ip_packet_router_config(&config.gateway.id, path).await?; Some(override_ip_packet_router_config(cfg, ip_config_override)) } else { // if IPR is enabled, the config path must be specified diff --git a/nym-browser-extension/storage/Cargo.toml b/nym-browser-extension/storage/Cargo.toml index 037386774c..68ed96e26b 100644 --- a/nym-browser-extension/storage/Cargo.toml +++ b/nym-browser-extension/storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "extension-storage" -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" edition = "2021" license = "Apache-2.0" repository = "https://github.com/nymtech/nym" diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 8a87804f61..11e7b040c7 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -3804,9 +3804,8 @@ version = "1.1.15" dependencies = [ "async-trait", "base64 0.21.4", + "bs58 0.5.0", "cfg-if", - "dashmap", - "dirs 4.0.0", "futures", "gloo-timers", "http-body-util", @@ -3815,12 +3814,15 @@ dependencies = [ "hyper-util", "log", "nym-bandwidth-controller", + "nym-client-core-gateways-storage", + "nym-client-core-surb-storage", "nym-config", "nym-credential-storage", "nym-crypto", "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", + "nym-id", "nym-metrics", "nym-network-defaults", "nym-nonexhaustive-delayqueue", @@ -3830,12 +3832,10 @@ dependencies = [ "nym-topology", "nym-validator-client", "rand 0.7.3", - "reqwest", "serde", "serde_json", "sha2 0.10.8", "si-scale", - "sqlx", "tap", "thiserror", "time", @@ -3851,6 +3851,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-client-core-gateways-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "cosmrs", + "log", + "nym-crypto", + "nym-gateway-requests", + "serde", + "sqlx", + "thiserror", + "time", + "tokio", + "url", + "zeroize", +] + +[[package]] +name = "nym-client-core-surb-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "dashmap", + "log", + "nym-crypto", + "nym-sphinx", + "nym-task", + "sqlx", + "thiserror", + "time", + "tokio", +] + [[package]] name = "nym-coconut" version = "0.5.0" @@ -4177,6 +4211,18 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-id" +version = "0.1.0" +dependencies = [ + "nym-credential-storage", + "nym-credentials", + "thiserror", + "time", + "tracing", + "zeroize", +] + [[package]] name = "nym-metrics" version = "0.1.0" @@ -6586,6 +6632,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", + "time", "tokio-stream", "url", "webpki-roots", diff --git a/nym-connect/desktop/src-tauri/src/config/mod.rs b/nym-connect/desktop/src-tauri/src/config/mod.rs index 3426cf36b2..79b70f814b 100644 --- a/nym-connect/desktop/src-tauri/src/config/mod.rs +++ b/nym-connect/desktop/src-tauri/src/config/mod.rs @@ -5,10 +5,13 @@ use crate::config::persistence::NymConnectPaths; use crate::config::template::CONFIG_TEMPLATE; use crate::config::upgrade::try_upgrade_config; use crate::error::{BackendError, Result}; -use nym_client_core::client::base_client::storage::gateway_details::OnDiskGatewayDetails; +use nym_client_core::client::base_client::non_wasm_helpers::setup_fs_gateways_storage; +use nym_client_core::client::base_client::storage::gateways_storage::{ + GatewayDetails, RemoteGatewayDetails, +}; use nym_client_core::client::key_manager::persistence::OnDiskKeys; -use nym_client_core::config::GatewayEndpointConfig; use nym_client_core::error::ClientCoreError; +use nym_client_core::init::generate_new_client_keys; use nym_client_core::init::helpers::current_gateways; use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; use nym_config::{ @@ -17,6 +20,7 @@ use nym_config::{ }; use nym_crypto::asymmetric::identity; use nym_socks5_client_core::config::Config as Socks5CoreConfig; +use rand_07::rngs::OsRng; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::{fs, io}; @@ -26,6 +30,7 @@ mod old_config_v1_1_13; mod old_config_v1_1_20; mod old_config_v1_1_20_2; mod old_config_v1_1_30; +mod old_config_v1_1_33; mod persistence; mod template; mod upgrade; @@ -163,24 +168,21 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str let already_init = if config_path.exists() { // in case we're using old config, try to upgrade it // (if we're using the current version, it's a no-op) - if let Err(err) = try_upgrade_config(&id) { + if let Err(err) = try_upgrade_config(&id).await { log::error!( - "Failed to upgrade config file {}: {:?}", + "Failed to upgrade config file {}: {err}", config_path.display(), - err ); - if let Some(failed_at_version) = try_extract_version_for_upgrade_failure(err) { - return Err( + return if let Some(failed_at_version) = try_extract_version_for_upgrade_failure(err) { + Err( BackendError::CouldNotUpgradeExistingConfigurationFileAtVersion { file: config_path, failed_at_version, }, - ); + ) } else { - return Err(BackendError::CouldNotUpgradeExistingConfigurationFile { - file: config_path, - }); - } + Err(BackendError::CouldNotUpgradeExistingConfigurationFile { file: config_path }) + }; }; eprintln!("SOCKS5 client \"{id}\" was already initialised before"); true @@ -189,13 +191,8 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str false }; - // Future proofing. This flag exists for the other clients - let user_wants_force_register = false; - - // If the client was already initialized, don't generate new keys and don't re-register with - // the gateway (because this would create a new shared key). - // Unless the user really wants to. - let register_gateway = !already_init || user_wants_force_register; + // // Future proofing. This flag exists for the other clients + // let user_wants_force_register = false; log::trace!("Creating config for id: {id}"); let mut config = Config::new(&id, &provider_address); @@ -204,7 +201,17 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str config.core.base.client.nym_api_urls = nym_config::parse_urls(&raw_validators); } - let gateway_setup = if register_gateway { + let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys.clone()); + let details_store = + setup_fs_gateways_storage(&config.storage_paths.common_paths.gateway_registrations).await?; + + // if this is a first time client with this particular id is initialised, generated long-term keys + if !already_init { + let mut rng = OsRng; + generate_new_client_keys(&mut rng, &key_store).await?; + } + + let gateway_setup = if !already_init { let selection_spec = GatewaySelectionSpecification::new(Some(chosen_gateway_id), None, false); let mut rng = rand_07::thread_rng(); @@ -213,41 +220,39 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str GatewaySetup::New { specification: selection_spec, available_gateways, - overwrite_data: register_gateway, + wg_tun_address: None, } } else { - GatewaySetup::MustLoad + GatewaySetup::MustLoad { + gateway_id: Some(chosen_gateway_id), + } }; - // Setup gateway by either registering a new one, or reusing exiting keys - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys.clone()); - let details_store = - OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); let init_details = nym_client_core::init::setup_gateway(gateway_setup, &key_store, &details_store).await?; - let gateway_endpoint = init_details - .gateway_details - .try_get_configured_endpoint() - .unwrap(); + + let GatewayDetails::Remote(gateway_details) = &init_details.gateway_registration.details else { + return Err(ClientCoreError::UnexpectedPersistedCustomGatewayDetails)?; + }; config.save_to_default_location().tap_err(|_| { log::error!("Failed to save the config file"); })?; - print_saved_config(&config, gateway_endpoint); + print_saved_config(&config, gateway_details); - let address = init_details.client_address()?; + let address = init_details.client_address(); log::info!("The address of this client is: {address}"); Ok(()) } -fn print_saved_config(config: &Config, gateway_details: &GatewayEndpointConfig) { +fn print_saved_config(config: &Config, gateway_details: &RemoteGatewayDetails) { log::info!( "Saved configuration file to {}", config.default_location().display() ); log::info!("Gateway id: {}", gateway_details.gateway_id); - log::info!("Gateway owner: {}", gateway_details.gateway_owner); + log::info!("Gateway owner: {}", gateway_details.gateway_owner_address); log::info!("Gateway listener: {}", gateway_details.gateway_listener); log::info!( "Service provider address: {}", @@ -255,7 +260,7 @@ fn print_saved_config(config: &Config, gateway_details: &GatewayEndpointConfig) ); log::info!( "Service provider port: {}", - config.core.socks5.bind_adddress.port() + config.core.socks5.bind_address.port() ); log::info!("Client configuration completed."); } diff --git a/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20.rs b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20.rs index 4acc4fccbd..fbad434c68 100644 --- a/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20.rs +++ b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20.rs @@ -5,8 +5,8 @@ use crate::config::old_config_v1_1_20_2::{ ConfigV1_1_20_2, CoreConfigV1_1_20_2, SocksClientPathsV1_1_20_2, }; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::config::disk_persistence::keys_paths::ClientKeysPaths; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; +use nym_client_core::config::disk_persistence::old_v1_1_33::ClientKeysPathsV1_1_33; use nym_client_core::config::old_config_v1_1_20::ConfigV1_1_20 as BaseConfigV1_1_20; use nym_client_core::config::old_config_v1_1_20_2::ClientV1_1_20_2; use nym_config::legacy_helpers::nym_config::MigrationNymConfig; @@ -50,7 +50,7 @@ impl From for ConfigV1_1_20_2 { }, storage_paths: SocksClientPathsV1_1_20_2 { common_paths: CommonClientPathsV1_1_20_2 { - keys: ClientKeysPaths { + keys: ClientKeysPathsV1_1_33 { private_identity_key_file: value.base.client.private_identity_key_file, public_identity_key_file: value.base.client.public_identity_key_file, private_encryption_key_file: value.base.client.private_encryption_key_file, diff --git a/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20_2.rs b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20_2.rs index d87f41ae0f..6af24d249a 100644 --- a/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20_2.rs +++ b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_20_2.rs @@ -3,11 +3,11 @@ use crate::config::default_config_filepath; use crate::config::old_config_v1_1_30::ConfigV1_1_30; -use crate::config::persistence::NymConnectPaths; +use crate::config::old_config_v1_1_33::NymConnectPathsV1_1_33; use crate::error::Result; use nym_bin_common::logging::LoggingSettings; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; -use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; use nym_config::read_config_from_toml_file; use serde::{Deserialize, Serialize}; use std::io; @@ -42,11 +42,11 @@ impl ConfigV1_1_20_2 { // in this upgrade, gateway endpoint configuration was moved out of the config file, // so its returned to be stored elsewhere. - pub fn upgrade(self) -> Result<(ConfigV1_1_30, GatewayEndpointConfig)> { + pub fn upgrade(self) -> Result<(ConfigV1_1_30, OldGatewayEndpointConfigV1_1_33)> { let gateway_details = self.core.base.client.gateway_endpoint.clone().into(); let config = ConfigV1_1_30 { core: self.core.into(), - storage_paths: NymConnectPaths { + storage_paths: NymConnectPathsV1_1_33 { common_paths: self.storage_paths.common_paths.upgrade_default()?, }, // logging: self.logging, diff --git a/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_30.rs b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_30.rs index a18ae5831c..3d64011c8c 100644 --- a/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_30.rs +++ b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_30.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::config::persistence::NymConnectPaths; -use crate::config::{default_config_filepath, Config}; +use crate::config::default_config_filepath; +use crate::config::old_config_v1_1_33::{ConfigV1_1_33, NymConnectPathsV1_1_33}; use nym_config::read_config_from_toml_file; use nym_socks5_client_core::config::old_config_v1_1_30::ConfigV1_1_30 as CoreConfigV1_1_30; use serde::{Deserialize, Serialize}; @@ -14,15 +14,12 @@ use std::path::Path; pub struct ConfigV1_1_30 { pub core: CoreConfigV1_1_30, - // I'm leaving a landmine here for when the paths actually do change the next time, - // but propagating the change right now (in ALL clients) would be such a hassle..., - // so sorry for the next person looking at it : ) - pub storage_paths: NymConnectPaths, + pub storage_paths: NymConnectPathsV1_1_33, } -impl From for Config { +impl From for ConfigV1_1_33 { fn from(value: ConfigV1_1_30) -> Self { - Config { + ConfigV1_1_33 { core: value.core.into(), storage_paths: value.storage_paths, } diff --git a/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_33.rs b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_33.rs new file mode 100644 index 0000000000..15de9d1750 --- /dev/null +++ b/nym-connect/desktop/src-tauri/src/config/old_config_v1_1_33.rs @@ -0,0 +1,47 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::persistence::NymConnectPaths; +use crate::config::{default_config_filepath, Config}; +use crate::error::BackendError; +use nym_client_core::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; +use nym_config::read_config_from_toml_file; +use nym_socks5_client_core::config::old_config_v1_1_33::ConfigV1_1_33 as CoreConfigV1_1_33; +use serde::{Deserialize, Serialize}; +use std::io; +use std::path::Path; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct NymConnectPathsV1_1_33 { + #[serde(flatten)] + pub common_paths: CommonClientPathsV1_1_33, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_33 { + pub core: CoreConfigV1_1_33, + + // \/ CHANGED + pub storage_paths: NymConnectPathsV1_1_33, + // /\ CHANGED +} + +impl ConfigV1_1_33 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + core: self.core.into(), + storage_paths: NymConnectPaths { + common_paths: self.storage_paths.common_paths.upgrade_default()?, + }, + }) + } +} diff --git a/nym-connect/desktop/src-tauri/src/config/template.rs b/nym-connect/desktop/src-tauri/src/config/template.rs index 27e2ed435a..3de6a1be5e 100644 --- a/nym-connect/desktop/src-tauri/src/config/template.rs +++ b/nym-connect/desktop/src-tauri/src/config/template.rs @@ -78,7 +78,7 @@ provider_mix_address = '{{ core.socks5.provider_mix_address }}' # The address on which the client will be listening for incoming requests # (default: 127.0.0.1:1080) -bind_adddress = '{{ core.socks5.bind_adddress }}' +bind_address = '{{ core.socks5.bind_address }}' # Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. # While this is going to hide its actual address information, it will make the actual communication diff --git a/nym-connect/desktop/src-tauri/src/config/upgrade.rs b/nym-connect/desktop/src-tauri/src/config/upgrade.rs index 5ec509e733..8df77ab7b6 100644 --- a/nym-connect/desktop/src-tauri/src/config/upgrade.rs +++ b/nym-connect/desktop/src-tauri/src/config/upgrade.rs @@ -2,47 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::old_config_v1_1_30::ConfigV1_1_30; -use crate::config::persistence::NymConnectPaths; +use crate::config::old_config_v1_1_33::ConfigV1_1_33; use crate::{ config::{ old_config_v1_1_13::OldConfigV1_1_13, old_config_v1_1_20::ConfigV1_1_20, - old_config_v1_1_20_2::ConfigV1_1_20_2, Config, + old_config_v1_1_20_2::ConfigV1_1_20_2, }, - error::{BackendError, Result}, + error::Result, }; use log::{debug, info}; -use nym_client_core::{ - client::{ - base_client::storage::gateway_details::{OnDiskGatewayDetails, PersistedGatewayDetails}, - key_manager::persistence::OnDiskKeys, - }, - config::GatewayEndpointConfig, - error::ClientCoreError, -}; +use nym_client_core::client::base_client::storage::migration_helpers::v1_1_33; -fn persist_gateway_details( - storage_paths: &NymConnectPaths, - details: GatewayEndpointConfig, -) -> Result<()> { - let details_store = OnDiskGatewayDetails::new(&storage_paths.common_paths.gateway_details); - let keys_store = OnDiskKeys::new(storage_paths.common_paths.keys.clone()); - let shared_keys = keys_store.ephemeral_load_gateway_keys().map_err(|source| { - BackendError::ClientCoreError { - source: ClientCoreError::KeyStoreError { - source: Box::new(source), - }, - } - })?; - let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; - details_store - .store_to_disk(&persisted_details) - .map_err(|source| BackendError::ClientCoreError { - source: ClientCoreError::GatewayDetailsStoreError { - source: Box::new(source), - }, - }) -} -fn try_upgrade_v1_1_13_config(id: &str) -> Result { +async fn try_upgrade_v1_1_13_config(id: &str) -> Result { use nym_config::legacy_helpers::nym_config::MigrationNymConfig; // explicitly load it as v1.1.13 (which is incompatible with the next step, i.e. 1.1.19) @@ -57,14 +28,23 @@ fn try_upgrade_v1_1_13_config(id: &str) -> Result { let updated_step1: ConfigV1_1_20 = old_config.into(); let updated_step2: ConfigV1_1_20_2 = updated_step1.into(); let (updated_step3, gateway_config) = updated_step2.upgrade()?; - persist_gateway_details(&updated_step3.storage_paths, gateway_config)?; + let old_paths = updated_step3.storage_paths.clone(); + + let updated_step4: ConfigV1_1_33 = updated_step3.into(); + let updated = updated_step4.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; - let updated: Config = updated_step3.into(); updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_20_config(id: &str) -> Result { +async fn try_upgrade_v1_1_20_config(id: &str) -> Result { use nym_config::legacy_helpers::nym_config::MigrationNymConfig; // explicitly load it as v1.1.20 (which is incompatible with the current one, i.e. +1.1.21) @@ -78,14 +58,23 @@ fn try_upgrade_v1_1_20_config(id: &str) -> Result { let updated_step1: ConfigV1_1_20_2 = old_config.into(); let (updated_step2, gateway_config) = updated_step1.upgrade()?; - persist_gateway_details(&updated_step2.storage_paths, gateway_config)?; + let old_paths = updated_step2.storage_paths.clone(); + + let updated_step3: ConfigV1_1_33 = updated_step2.into(); + let updated = updated_step3.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; - let updated: Config = updated_step2.into(); updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { +async fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { // explicitly load it as v1.1.20_2 (which is incompatible with the current one, i.e. +1.1.21) let Ok(old_config) = ConfigV1_1_20_2::read_from_default_path(id) else { // if we failed to load it, there might have been nothing to upgrade @@ -96,14 +85,23 @@ fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { info!("It is going to get updated to the current specification."); let (updated_step1, gateway_config) = old_config.upgrade()?; - persist_gateway_details(&updated_step1.storage_paths, gateway_config)?; + let old_paths = updated_step1.storage_paths.clone(); + + let updated_step2: ConfigV1_1_33 = updated_step1.into(); + let updated = updated_step2.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; - let updated: Config = updated_step1.into(); updated.save_to_default_location()?; Ok(true) } -fn try_upgrade_v1_1_30_config(id: &str) -> Result { +async fn try_upgrade_v1_1_30_config(id: &str) -> Result { // explicitly load it as v1.1.20_2 (which is incompatible with the current one, i.e. +1.1.21) let Ok(old_config) = ConfigV1_1_30::read_from_default_path(id) else { // if we failed to load it, there might have been nothing to upgrade @@ -113,23 +111,62 @@ fn try_upgrade_v1_1_30_config(id: &str) -> Result { info!("It seems the client is using <= v1.1.30 config template."); info!("It is going to get updated to the current specification."); - let updated: Config = old_config.into(); + let old_paths = old_config.storage_paths.clone(); + + let updated_step1: ConfigV1_1_33 = old_config.into(); + let updated = updated_step1.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + None, + ) + .await?; + updated.save_to_default_location()?; Ok(true) } -pub fn try_upgrade_config(id: &str) -> Result<()> { +async fn try_upgrade_v1_1_33_config(id: &str) -> Result { + // explicitly load it as v1.1.33 (which is incompatible with the current one, i.e. +1.1.34) + let Ok(old_config) = ConfigV1_1_33::read_from_default_path(id) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.33 config template."); + info!("It is going to get updated to the current specification."); + + let old_paths = old_config.storage_paths.clone(); + + let updated = old_config.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + None, + ) + .await?; + + updated.save_to_default_location()?; + Ok(true) +} + +pub async fn try_upgrade_config(id: &str) -> Result<()> { debug!("Attempting to upgrade config file for \"{id}\""); - if try_upgrade_v1_1_13_config(id)? { + if try_upgrade_v1_1_13_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_20_config(id)? { + if try_upgrade_v1_1_20_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_20_2_config(id)? { + if try_upgrade_v1_1_20_2_config(id).await? { return Ok(()); } - if try_upgrade_v1_1_30_config(id)? { + if try_upgrade_v1_1_30_config(id).await? { + return Ok(()); + } + if try_upgrade_v1_1_33_config(id).await? { return Ok(()); } diff --git a/nym-connect/desktop/src-tauri/src/operations/export.rs b/nym-connect/desktop/src-tauri/src/operations/export.rs index 3b11891e9c..bf047ee84e 100644 --- a/nym-connect/desktop/src-tauri/src/operations/export.rs +++ b/nym-connect/desktop/src-tauri/src/operations/export.rs @@ -7,7 +7,7 @@ use crate::{ state::State, }; use nym_client_core::client::key_manager::persistence::OnDiskKeys; -use nym_client_core::client::key_manager::KeyManager; +use nym_client_core::client::key_manager::ClientKeys; use nym_crypto::asymmetric::identity; pub async fn get_identity_key( @@ -23,7 +23,7 @@ pub async fn get_identity_key( // wtf, why are we loading EVERYTHING to just get identity key?? let key_store = OnDiskKeys::from(paths); let key_manager = - KeyManager::load_keys(&key_store) + ClientKeys::load_keys(&key_store) .await .map_err(|err| BackendError::UnableToLoadKeys { source: Box::new(err), @@ -54,7 +54,6 @@ pub async fn export_keys(state: tauri::State<'_, Arc>>) -> Result< // Get key paths let ack_key_file = key_paths.ack_key(); - let gateway_shared_key_file = key_paths.gateway_shared_key(); let pub_id_key_file = key_paths.public_identity_key(); let priv_id_key_file = key_paths.private_identity_key(); @@ -64,7 +63,6 @@ pub async fn export_keys(state: tauri::State<'_, Arc>>) -> Result< // Read file contents let ack_key = fs::read_to_string(ack_key_file)?; - let gateway_shared_key = fs::read_to_string(gateway_shared_key_file)?; let pub_id_key = fs::read_to_string(pub_id_key_file)?; let priv_id_key = fs::read_to_string(priv_id_key_file)?; @@ -73,7 +71,6 @@ pub async fn export_keys(state: tauri::State<'_, Arc>>) -> Result< let priv_enc_key = fs::read_to_string(priv_enc_key_file)?; let ack_key_file = key_filename(&key_paths.ack_key_file)?; - let gateway_shared_key_file = key_filename(&key_paths.gateway_shared_key_file)?; let pub_id_key_file = key_filename(&key_paths.public_identity_key_file)?; let priv_id_key_file = key_filename(&key_paths.private_identity_key_file)?; let pub_enc_key_file = key_filename(&key_paths.public_encryption_key_file)?; @@ -82,7 +79,6 @@ pub async fn export_keys(state: tauri::State<'_, Arc>>) -> Result< // Format and return as json let json = serde_json::json!({ ack_key_file: ack_key, - gateway_shared_key_file: gateway_shared_key, pub_id_key_file: pub_id_key, priv_id_key_file: priv_id_key, pub_enc_key_file: pub_enc_key, diff --git a/nym-connect/desktop/src-tauri/src/state.rs b/nym-connect/desktop/src-tauri/src/state.rs index 20471e6171..83350b98fe 100644 --- a/nym-connect/desktop/src-tauri/src/state.rs +++ b/nym-connect/desktop/src-tauri/src/state.rs @@ -257,13 +257,7 @@ impl State { let (control_tx, msg_rx, exit_status_rx, used_gateway) = tasks::start_nym_socks5_client(&id, &privacy_level).await?; self.socks5_client_sender = Some(control_tx); - self.gateway = Some( - used_gateway - .try_get_configured_endpoint() - .unwrap() - .gateway_id - .clone(), - ); + self.gateway = Some(used_gateway.gateway_id().to_base58_string()); Ok((msg_rx, exit_status_rx)) } diff --git a/nym-connect/desktop/src-tauri/src/tasks.rs b/nym-connect/desktop/src-tauri/src/tasks.rs index d4264f2274..d2f2e1c3a7 100644 --- a/nym-connect/desktop/src-tauri/src/tasks.rs +++ b/nym-connect/desktop/src-tauri/src/tasks.rs @@ -1,9 +1,9 @@ use futures::{channel::mpsc, StreamExt}; -use nym_client_core::init::types::GatewayDetails; +use nym_client_core::client::base_client::storage::gateways_storage::{ + GatewayDetails, GatewaysDetailsStore, +}; use nym_client_core::{ - client::base_client::storage::{ - gateway_details::GatewayDetailsStore, MixnetClientStorage, OnDiskPersistent, - }, + client::base_client::storage::{MixnetClientStorage, OnDiskPersistent}, config::{GroupBy, TopologyStructure}, error::ClientCoreStatusMessage, }; @@ -90,10 +90,12 @@ pub async fn start_nym_socks5_client( let used_gateway = storage .gateway_details_store() - .load_gateway_details() + .active_gateway() .await - .expect("failed to load gateway details") - .into(); + .expect("failed to load active gateway details") + .registration + .expect("no active gateway set") + .details; log::info!("Starting socks5 client"); diff --git a/sdk/lib/socks5-listener/src/config/template.rs b/sdk/lib/socks5-listener/src/config/template.rs index e310ef2e32..e2ad4c414d 100644 --- a/sdk/lib/socks5-listener/src/config/template.rs +++ b/sdk/lib/socks5-listener/src/config/template.rs @@ -50,10 +50,6 @@ keys.private_encryption_key_file = '{{ storage_paths.keys.private_encryption_key # Path to file containing public encryption key. keys.public_encryption_key_file = '{{ storage_paths.keys.public_encryption_key_file }}' -# A gateway specific, optional, base58 stringified shared key used for -# communication with particular gateway. -keys.gateway_shared_key_file = '{{ storage_paths.keys.gateway_shared_key_file }}' - # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' @@ -64,17 +60,9 @@ credentials_database = '{{ storage_paths.credentials_database }}' # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. reply_surb_database = '{{ storage_paths.reply_surb_database }}' -# DEPRECATED -[core.client.gateway_endpoint] -# ID of the gateway from which the client should be fetching messages. -gateway_id = '{{ core.client.gateway_endpoint.gateway_id }}' - -# Address of the gateway owner to which the client should send messages. -gateway_owner = '{{ core.client.gateway_endpoint.gateway_owner }}' - -# Address of the gateway listener to which all client requests should be sent. -gateway_listener = '{{ core.client.gateway_endpoint.gateway_listener }}' - +# Path to the file containing information about gateways used by this client, +# i.e. details such as their public keys, owner addresses or the network information. +gateway_registrations = '{{ storage_paths.gateway_registrations }}' ##### socket config options ##### @@ -85,7 +73,7 @@ provider_mix_address = '{{ core.socks5.provider_mix_address }}' # The address on which the client will be listening for incoming requests # (default: 127.0.0.1:1080) -bind_adddress = '{{ core.socks5.bind_adddress }}' +bind_address = '{{ core.socks5.bind_address }}' # Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. # While this is going to hide its actual address information, it will make the actual communication diff --git a/sdk/lib/socks5-listener/src/lib.rs b/sdk/lib/socks5-listener/src/lib.rs index 0c9e2f7771..a0127d713c 100644 --- a/sdk/lib/socks5-listener/src/lib.rs +++ b/sdk/lib/socks5-listener/src/lib.rs @@ -276,7 +276,7 @@ where must_use_tls: false, }, available_gateways: current_gateways(&mut rng, &nym_apis).await?, - overwrite_data: false, + wg_tun_address: None, }); eprintln!("starting the socks5 client"); diff --git a/sdk/lib/socks5-listener/src/persistence.rs b/sdk/lib/socks5-listener/src/persistence.rs index 68455fad27..9ee4990e6c 100644 --- a/sdk/lib/socks5-listener/src/persistence.rs +++ b/sdk/lib/socks5-listener/src/persistence.rs @@ -2,8 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::Config; -use nym_client_core::client::base_client::storage::gateway_details::InMemGatewayDetails; -use nym_client_core::client::base_client::storage::MixnetClientStorage; +use nym_client_core::client::base_client::storage::{InMemGatewaysDetails, MixnetClientStorage}; use nym_client_core::client::key_manager::persistence::InMemEphemeralKeys; use nym_client_core::client::replies::reply_storage; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; @@ -11,7 +10,7 @@ use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCred pub struct MobileClientStorage { // the key storage is now useless without gateway details store. so use ephemeral for everything. key_store: InMemEphemeralKeys, - gateway_details_store: InMemGatewayDetails, + gateway_details_store: InMemGatewaysDetails, reply_store: reply_storage::Empty, credential_store: EphemeralCredentialStorage, @@ -21,7 +20,7 @@ impl MixnetClientStorage for MobileClientStorage { type KeyStore = InMemEphemeralKeys; type ReplyStore = reply_storage::Empty; type CredentialStore = EphemeralCredentialStorage; - type GatewayDetailsStore = InMemGatewayDetails; + type GatewaysDetailsStore = InMemGatewaysDetails; fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { (self.reply_store, self.credential_store) @@ -39,7 +38,7 @@ impl MixnetClientStorage for MobileClientStorage { &self.credential_store } - fn gateway_details_store(&self) -> &Self::GatewayDetailsStore { + fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore { &self.gateway_details_store } } diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index c0a0960f79..4948f230f5 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true [dependencies] async-trait = { workspace = true } bip39 = { workspace = true } -nym-client-core = { path = "../../../common/client-core", features = ["fs-surb-storage"]} +nym-client-core = { path = "../../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage"]} nym-crypto = { path = "../../../common/crypto" } nym-gateway-requests = { path = "../../../gateway/gateway-requests" } nym-bandwidth-controller = { path = "../../../common/bandwidth-controller" } diff --git a/sdk/rust/nym-sdk/examples/change_gateway.rs b/sdk/rust/nym-sdk/examples/change_gateway.rs new file mode 100644 index 0000000000..7a6771a7b9 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/change_gateway.rs @@ -0,0 +1,9 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[tokio::main] +async fn main() { + nym_bin_common::logging::setup_logging(); + + todo!() +} diff --git a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs index dd650b1e2a..298502458c 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_storage.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_storage.rs @@ -1,9 +1,6 @@ -use nym_client_core::client::base_client::storage::gateway_details::{ - GatewayDetailsStore, PersistedGatewayDetails, -}; use nym_sdk::mixnet::{ - self, EmptyReplyStorage, EphemeralCredentialStorage, KeyManager, KeyStore, MixnetClientStorage, - MixnetMessageSender, + self, ActiveGateway, BadGateway, ClientKeys, EmptyReplyStorage, EphemeralCredentialStorage, + GatewayRegistration, GatewaysDetailsStore, KeyStore, MixnetClientStorage, MixnetMessageSender, }; use nym_topology::provider_trait::async_trait; @@ -64,7 +61,7 @@ impl MixnetClientStorage for MockClientStorage { type KeyStore = MockKeyStore; type ReplyStore = EmptyReplyStorage; type CredentialStore = EphemeralCredentialStorage; - type GatewayDetailsStore = MockGatewayDetailsStore; + type GatewaysDetailsStore = MockGatewayDetailsStore; fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) { (self.reply_store, self.credential_store) @@ -82,7 +79,7 @@ impl MixnetClientStorage for MockClientStorage { &self.credential_store } - fn gateway_details_store(&self) -> &Self::GatewayDetailsStore { + fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore { &self.gateway_details_store } } @@ -93,13 +90,13 @@ struct MockKeyStore; impl KeyStore for MockKeyStore { type StorageError = MyError; - async fn load_keys(&self) -> Result { + async fn load_keys(&self) -> Result { println!("loading stored keys"); Err(MyError) } - async fn store_keys(&self, _keys: &KeyManager) -> Result<(), Self::StorageError> { + async fn store_keys(&self, _keys: &ClientKeys) -> Result<(), Self::StorageError> { println!("storing keys"); Ok(()) @@ -109,23 +106,56 @@ impl KeyStore for MockKeyStore { struct MockGatewayDetailsStore; #[async_trait] -impl GatewayDetailsStore for MockGatewayDetailsStore { +impl GatewaysDetailsStore for MockGatewayDetailsStore { type StorageError = MyError; - async fn load_gateway_details(&self) -> Result { - println!("loading stored gateway details"); + async fn active_gateway(&self) -> Result { + println!("getting active gateway"); + + Err(MyError) + } + + async fn set_active_gateway(&self, _gateway_id: &str) -> Result<(), Self::StorageError> { + println!("setting active gateway"); + + Ok(()) + } + + async fn all_gateways(&self) -> Result, Self::StorageError> { + println!("getting all registered gateways"); + + Err(MyError) + } + + async fn has_gateway_details(&self, _gateway_id: &str) -> Result { + println!("checking for gateway details"); + + Err(MyError) + } + + async fn load_gateway_details( + &self, + _gateway_id: &str, + ) -> Result { + println!("loading gateway details"); Err(MyError) } async fn store_gateway_details( &self, - _details: &PersistedGatewayDetails, + _details: &GatewayRegistration, ) -> Result<(), Self::StorageError> { println!("storing gateway details"); Ok(()) } + + async fn remove_gateway_details(&self, _gateway_id: &str) -> Result<(), Self::StorageError> { + println!("removing gateway details"); + + Ok(()) + } } // @@ -181,3 +211,9 @@ impl GatewayDetailsStore for MockGatewayDetailsStore { #[derive(thiserror::Error, Debug)] #[error("foobar")] struct MyError; + +impl From for MyError { + fn from(_: BadGateway) -> Self { + MyError + } +} diff --git a/sdk/rust/nym-sdk/examples/simple.rs b/sdk/rust/nym-sdk/examples/simple.rs index 14720ba24c..72b6568b6d 100644 --- a/sdk/rust/nym-sdk/examples/simple.rs +++ b/sdk/rust/nym-sdk/examples/simple.rs @@ -12,7 +12,7 @@ async fn main() { let our_address = client.nym_address(); println!("Our client nym address is: {our_address}"); - // Send a message throught the mixnet to ourselves + // Send a message through the mixnet to ourselves client .send_plain_message(*our_address, "hello there") .await diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index b54ef462ee..c89229fb28 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -44,11 +44,16 @@ pub use native_client::MixnetClient; pub use native_client::MixnetClientSender; pub use nym_client_core::{ client::{ - base_client::storage::{Ephemeral, MixnetClientStorage, OnDiskPersistent}, + base_client::storage::{ + gateways_storage::{ + ActiveGateway, BadGateway, GatewayRegistration, GatewaysDetailsStore, + }, + Ephemeral, MixnetClientStorage, OnDiskPersistent, + }, inbound_messages::InputMessage, key_manager::{ persistence::{InMemEphemeralKeys, KeyStore, OnDiskKeys}, - KeyManager, + ClientKeys, }, replies::reply_storage::{ fs_backend::Backend as ReplyStorage, CombinedReplyStorage, Empty as EmptyReplyStorage, @@ -56,7 +61,7 @@ pub use nym_client_core::{ }, topology_control::geo_aware_provider::{CountryGroup, GeoAwareTopologyProvider}, }, - config::{GatewayEndpointConfig, GroupBy}, + config::GroupBy, }; pub use nym_credential_storage::{ ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage, diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 69995214b8..ec438e40ef 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -11,28 +11,27 @@ use crate::{Error, Result}; use futures::channel::mpsc; use futures::StreamExt; use log::warn; -use nym_client_core::client::base_client::storage::gateway_details::{ - GatewayDetailsStore, PersistedGatewayDetails, -}; +use nym_client_core::client::base_client::storage::helpers::get_all_registered_identities; use nym_client_core::client::base_client::storage::{ - Ephemeral, MixnetClientStorage, OnDiskPersistent, + Ephemeral, GatewaysDetailsStore, MixnetClientStorage, OnDiskPersistent, }; use nym_client_core::client::base_client::BaseClient; use nym_client_core::client::key_manager::persistence::KeyStore; +use nym_client_core::client::{ + base_client::BaseClientBuilder, replies::reply_storage::ReplyStorageBackend, +}; use nym_client_core::config::DebugConfig; +use nym_client_core::error::ClientCoreError; use nym_client_core::init::helpers::current_gateways; use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; -use nym_client_core::{ - client::{base_client::BaseClientBuilder, replies::reply_storage::ReplyStorageBackend}, - config::GatewayEndpointConfig, -}; -use nym_network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, WG_TUN_DEVICE_ADDRESS}; +use nym_network_defaults::WG_TUN_DEVICE_IP_ADDRESS; use nym_socks5_client_core::config::Socks5; use nym_task::manager::TaskStatus; use nym_task::{TaskClient, TaskHandle}; use nym_topology::provider_trait::TopologyProvider; use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; use rand::rngs::OsRng; +use std::net::IpAddr; use std::path::Path; use std::path::PathBuf; use url::Url; @@ -44,7 +43,6 @@ const DEFAULT_NUMBER_OF_SURBS: u32 = 10; pub struct MixnetClientBuilder { config: Config, storage_paths: Option, - gateway_config: Option, socks5_config: Option, wireguard_mode: bool, @@ -81,7 +79,6 @@ impl MixnetClientBuilder { Ok(MixnetClientBuilder { config: Default::default(), storage_paths: None, - gateway_config: None, socks5_config: None, wireguard_mode: false, wait_for_gateway: false, @@ -101,10 +98,11 @@ impl MixnetClientBuilder where S: MixnetClientStorage + 'static, S::ReplyStore: Send + Sync, + S::GatewaysDetailsStore: Sync, ::StorageError: Sync + Send, ::StorageError: Send + Sync, ::StorageError: Send + Sync, - ::StorageError: Send + Sync, + ::StorageError: Send + Sync, { /// Creates a client builder with the provided client storage implementation. #[must_use] @@ -112,7 +110,6 @@ where MixnetClientBuilder { config: Default::default(), storage_paths: None, - gateway_config: None, socks5_config: None, wireguard_mode: false, wait_for_gateway: false, @@ -131,7 +128,6 @@ where MixnetClientBuilder { config: self.config, storage_paths: self.storage_paths, - gateway_config: self.gateway_config, socks5_config: self.socks5_config, wireguard_mode: self.wireguard_mode, wait_for_gateway: self.wait_for_gateway, @@ -306,10 +302,11 @@ impl DisconnectedMixnetClient where S: MixnetClientStorage + 'static, S::ReplyStore: Send + Sync, + S::GatewaysDetailsStore: Sync, ::StorageError: Sync + Send, ::StorageError: Send + Sync, ::StorageError: Send + Sync, - ::StorageError: Send + Sync, + ::StorageError: Send + Sync, { /// Create a new mixnet client in a disconnected state. The default configuration, /// creates a new mainnet client with ephemeral keys stored in RAM, which will be discarded at @@ -413,38 +410,65 @@ where .collect() } - /// Client keys are generated at client creation if none were found. The gateway shared - /// key, however, is created during the gateway registration handshake so it might not - /// necessarily be available. - /// Furthermore, it has to be coupled with particular gateway's config. - async fn has_valid_gateway_info(&self) -> bool { - let keys = match self.storage.key_store().load_keys().await { - Ok(keys) => keys, + fn wireguard_tun_address(&self) -> Option { + // currently use a hardcoded value here, but perhaps we should change that later + if self.wireguard_mode { + Some(WG_TUN_DEVICE_IP_ADDRESS) + } else { + None + } + } + + async fn new_gateway_setup(&self) -> Result { + let nym_api_endpoints = self.get_api_endpoints(); + + let selection_spec = GatewaySelectionSpecification::new( + self.config.user_chosen_gateway.clone(), + None, + self.force_tls, + ); + + let mut rng = OsRng; + let available_gateways = current_gateways(&mut rng, &nym_api_endpoints).await?; + + Ok(GatewaySetup::New { + specification: selection_spec, + available_gateways, + wg_tun_address: self.wireguard_tun_address(), + }) + } + + /// Check if the client already has an active gateway enabled. + async fn has_active_gateway(&self) -> bool { + let storage = self.storage.gateway_details_store(); + + match storage.active_gateway().await { Err(err) => { - warn!("failed to load stored keys: {err}"); + warn!("failed to query for the current active gateway: {err}"); return false; } - }; - - let gateway_details = match self - .storage - .gateway_details_store() - .load_gateway_details() - .await - { - Ok(details) => details, - Err(err) => { - warn!("failed to load stored gateway details: {err}"); - return false; + Ok(active) => { + if active.registration.is_some() { + return true; + } } - }; - - if let Err(err) = gateway_details.validate(keys.gateway_shared_key().as_deref()) { - warn!("stored key verification failure: {err}"); - return false; } - true + match get_all_registered_identities(storage).await { + Err(err) => { + warn!("failed to query for all registered gateways: {err}") + } + Ok(all_ids) => { + if !all_ids.is_empty() { + warn!("this client doesn't have an active gateway set, however, it's already registered with the following gateways (consider making one of them active):"); + for id in all_ids { + warn!("{id}") + } + } + } + } + + false } /// Register with a gateway. If a gateway is provided in the config then that will try to be @@ -461,24 +485,10 @@ where log::debug!("Registering with gateway"); - let api_endpoints = self.get_api_endpoints(); - - let gateway_setup = if self.has_valid_gateway_info().await { - GatewaySetup::MustLoad + let gateway_setup = if self.has_active_gateway().await { + GatewaySetup::MustLoad { gateway_id: None } } else { - let selection_spec = GatewaySelectionSpecification::new( - self.config.user_chosen_gateway.clone(), - None, - self.force_tls, - ); - - let mut rng = OsRng; - - GatewaySetup::New { - specification: selection_spec, - available_gateways: current_gateways(&mut rng, &api_endpoints).await?, - overwrite_data: !self.config.key_mode.is_keep(), - } + self.new_gateway_setup().await? }; // this will perform necessary key and details load and optional store @@ -526,59 +536,70 @@ where .config .as_base_client_config(nyxd_endpoints, nym_api_endpoints.clone()); - let known_gateway = self.has_valid_gateway_info().await; + let known_gateway = self.has_active_gateway().await; - let mut base_builder: BaseClientBuilder<_, _> = if !known_gateway { - let selection_spec = GatewaySelectionSpecification::new( - self.config.user_chosen_gateway, - None, - self.force_tls, - ); - - let mut rng = OsRng; - let mut available_gateways = current_gateways(&mut rng, &nym_api_endpoints).await?; - if self.wireguard_mode { - available_gateways - .iter_mut() - .for_each(|node| node.host = WG_TUN_DEVICE_ADDRESS.parse().unwrap()); - } - let setup = GatewaySetup::New { - specification: selection_spec, - available_gateways, - overwrite_data: !self.config.key_mode.is_keep(), - }; - - BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client) - .with_wait_for_gateway(self.wait_for_gateway) - .with_gateway_setup(setup) - } else if self.wireguard_mode { - if let Ok(PersistedGatewayDetails::Default(mut config)) = self - .storage - .gateway_details_store() - .load_gateway_details() - .await - { - config.details.gateway_listener = format!( - "ws://{}:{}", - WG_TUN_DEVICE_ADDRESS, DEFAULT_CLIENT_LISTENING_PORT - ); - if let Err(e) = self - .storage - .gateway_details_store() - .store_gateway_details(&PersistedGatewayDetails::Default(config)) - .await - { - warn!("Could not switch to using wireguard mode - {:?}", e); - } - } else { - warn!("Storage type not supported with wireguard mode"); - } - BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client) - .with_wait_for_gateway(self.wait_for_gateway) + // if we have a known gateway, don't bother doing all of those queries + let gateway_setup = if known_gateway { + None } else { + Some(self.new_gateway_setup().await?) + }; + + let mut base_builder: BaseClientBuilder<_, _> = BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client) .with_wait_for_gateway(self.wait_for_gateway) - }; + .with_wireguard_connection(self.wireguard_mode); + + if !known_gateway { + // safety: `gateway_setup` is always set whenever `known_gateway` is false + base_builder = base_builder.with_gateway_setup(gateway_setup.unwrap()); + } + + // let mut base_builder: BaseClientBuilder<_, _> = if !known_gateway { + // // we need to setup a new gateway + // let setup = self.new_gateway_setup().await; + // + // BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client) + // .with_wait_for_gateway(self.wait_for_gateway) + // .with_gateway_setup(setup) + // // } else if self.wireguard_mode { + // // // load current active gateway in wireguard mode + // // details_store.set_wireguard_mode(true).await?; + // // + // // if let Ok(PersistedGatewayDetails::Default(mut config)) = self + // // .storage + // // .gateway_details_store() + // // .load_gateway_details() + // // .await + // // { + // // config.details.gateway_listener = format!( + // // "ws://{}:{}", + // // WG_TUN_DEVICE_ADDRESS, DEFAULT_CLIENT_LISTENING_PORT + // // ); + // // if let Err(e) = self + // // .storage + // // .gateway_details_store() + // // .store_gateway_details(&PersistedGatewayDetails::Default(config)) + // // .await + // // { + // // warn!("Could not switch to using wireguard mode - {:?}", e); + // // } + // // } else { + // // warn!("Storage type not supported with wireguard mode"); + // // } + // // BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client) + // // .with_wait_for_gateway(self.wait_for_gateway) + // } else { + // // load current active gateway in non-wireguard mode + // + // // make sure our current storage mode matches the desired wg mode + // details_store + // .set_wireguard_mode(self.wireguard_mode) + // .await?; + // + // BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client) + // .with_wait_for_gateway(self.wait_for_gateway) + // }; if let Some(topology_provider) = self.custom_topology_provider { base_builder = base_builder.with_topology_provider(topology_provider); diff --git a/sdk/rust/nym-sdk/src/mixnet/paths.rs b/sdk/rust/nym-sdk/src/mixnet/paths.rs index 288218f5c5..8051b49350 100644 --- a/sdk/rust/nym-sdk/src/mixnet/paths.rs +++ b/sdk/rust/nym-sdk/src/mixnet/paths.rs @@ -1,8 +1,8 @@ -// Copyright 2022-2023 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::{Error, Result}; -use nym_client_core::client::base_client::storage::gateway_details::OnDiskGatewayDetails; +use nym_client_core::client::base_client::storage::OnDiskGatewaysDetails; use nym_client_core::client::base_client::{non_wasm_helpers, storage}; use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::client::replies::reply_storage::fs_backend; @@ -31,17 +31,14 @@ pub struct StoragePaths { /// Key for handling acks pub ack_key: PathBuf, - /// Key setup after authenticating with a gateway - pub gateway_shared_key: PathBuf, - /// The database containing credentials pub credential_database_path: PathBuf, /// The database storing reply surbs in-between sessions pub reply_surb_database_path: PathBuf, - /// Details of the used gateway - pub gateway_details_path: PathBuf, + /// Details of the used gateways + pub gateway_registrations: PathBuf, } impl StoragePaths { @@ -63,10 +60,9 @@ impl StoragePaths { private_encryption: dir.join("private_encryption.pem"), public_encryption: dir.join("public_encryption.pem"), ack_key: dir.join("ack_key.pem"), - gateway_shared_key: dir.join("gateway_shared.pem"), credential_database_path: dir.join("db.sqlite"), reply_surb_database_path: dir.join("persistent_reply_store.sqlite"), - gateway_details_path: dir.join("gateway_details.json"), + gateway_registrations: dir.join("gateways_registrations.sqlite"), }) } @@ -78,7 +74,7 @@ impl StoragePaths { self.on_disk_key_storage_spec(), self.default_persistent_fs_reply_backend().await?, self.persistent_credential_storage().await?, - self.on_disk_gateway_details_storage(), + self.on_disk_gateway_details_storage().await?, )) } @@ -92,7 +88,7 @@ impl StoragePaths { self.persistent_fs_reply_backend(&config.reply_surbs) .await?, self.persistent_credential_storage().await?, - self.on_disk_gateway_details_storage(), + self.on_disk_gateway_details_storage().await?, )) } @@ -129,8 +125,8 @@ impl StoragePaths { OnDiskKeys::new(self.client_keys_paths()) } - pub fn on_disk_gateway_details_storage(&self) -> OnDiskGatewayDetails { - OnDiskGatewayDetails::new(&self.gateway_details_path) + pub async fn on_disk_gateway_details_storage(&self) -> Result { + Ok(non_wasm_helpers::setup_fs_gateways_storage(&self.gateway_registrations).await?) } fn client_keys_paths(&self) -> ClientKeysPaths { @@ -139,7 +135,6 @@ impl StoragePaths { public_identity_key_file: self.public_identity.clone(), private_encryption_key_file: self.private_encryption.clone(), public_encryption_key_file: self.public_encryption.clone(), - gateway_shared_key_file: self.gateway_shared_key.clone(), ack_key_file: self.ack_key.clone(), } } @@ -153,10 +148,9 @@ impl From for CommonClientPaths { public_identity_key_file: value.public_identity, private_encryption_key_file: value.private_encryption, public_encryption_key_file: value.public_encryption, - gateway_shared_key_file: value.gateway_shared_key, ack_key_file: value.ack_key, }, - gateway_details: value.gateway_details_path, + gateway_registrations: value.gateway_registrations, credentials_database: value.credential_database_path, reply_surb_database: value.reply_surb_database_path, } @@ -171,10 +165,9 @@ impl From for StoragePaths { private_encryption: value.keys.private_encryption_key_file, public_encryption: value.keys.public_encryption_key_file, ack_key: value.keys.ack_key_file, - gateway_shared_key: value.keys.gateway_shared_key_file, credential_database_path: value.credentials_database, reply_surb_database_path: value.reply_surb_database, - gateway_details_path: value.gateway_details, + gateway_registrations: value.gateway_registrations, } } } diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index ab340447f9..6bc2a46a06 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -56,7 +56,7 @@ impl Socks5MixnetClient { /// Get the SOCKS5 proxy URL that a HTTP(S) client can connect to. pub fn socks5_url(&self) -> String { - format!("socks5h://{}", self.socks5_config.bind_adddress) + format!("socks5h://{}", self.socks5_config.bind_address) } /// Get a shallow clone of [`LaneQueueLengths`]. This is useful to manually implement some form diff --git a/sdk/typescript/codegen/contract-clients/package.json b/sdk/typescript/codegen/contract-clients/package.json index b4f91a948b..f429659cbb 100644 --- a/sdk/typescript/codegen/contract-clients/package.json +++ b/sdk/typescript/codegen/contract-clients/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/contract-clients", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "description": "A client for all Nym smart contracts", "license": "Apache-2.0", "author": "Nym Technologies SA", diff --git a/sdk/typescript/docs/package.json b/sdk/typescript/docs/package.json index 4d0bb71c80..abdf67048a 100644 --- a/sdk/typescript/docs/package.json +++ b/sdk/typescript/docs/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/ts-sdk-docs", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "description": "Nym Typescript SDK Docs", "license": "Apache-2.0", "author": "Nym Technologies SA", diff --git a/sdk/typescript/packages/mix-fetch-node/package.json b/sdk/typescript/packages/mix-fetch-node/package.json index 3367b87244..e1618cb318 100644 --- a/sdk/typescript/packages/mix-fetch-node/package.json +++ b/sdk/typescript/packages/mix-fetch-node/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/mix-fetch-node", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "description": "This package is a drop-in replacement for `fetch` in NodeJS to send HTTP requests over the Nym Mixnet.", "license": "Apache-2.0", "author": "Nym Technologies SA", diff --git a/sdk/typescript/packages/mix-fetch/package.json b/sdk/typescript/packages/mix-fetch/package.json index 381ea96372..9af4e116f2 100644 --- a/sdk/typescript/packages/mix-fetch/package.json +++ b/sdk/typescript/packages/mix-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/mix-fetch", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "description": "This package is a drop-in replacement for `fetch` to send HTTP requests over the Nym Mixnet.", "license": "Apache-2.0", "author": "Nym Technologies SA", diff --git a/sdk/typescript/packages/node-tester/package.json b/sdk/typescript/packages/node-tester/package.json index eb377423bd..de487b0729 100644 --- a/sdk/typescript/packages/node-tester/package.json +++ b/sdk/typescript/packages/node-tester/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/node-tester", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "description": "This package provides a tester that can send test packets to mixnode that is part of the Nym Mixnet.", "license": "Apache-2.0", "author": "Nym Technologies SA", diff --git a/sdk/typescript/packages/nodejs-client/package.json b/sdk/typescript/packages/nodejs-client/package.json index e8df393fc4..7cd003087c 100644 --- a/sdk/typescript/packages/nodejs-client/package.json +++ b/sdk/typescript/packages/nodejs-client/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nodejs-client", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "license": "Apache-2.0", "author": "Nym Technologies SA", "files": [ diff --git a/sdk/typescript/packages/sdk-react/package.json b/sdk/typescript/packages/sdk-react/package.json index 6a163d7269..e67263157f 100644 --- a/sdk/typescript/packages/sdk-react/package.json +++ b/sdk/typescript/packages/sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/sdk-react", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "license": "Apache-2.0", "author": "Nym Technologies SA", "files": [ diff --git a/sdk/typescript/packages/sdk/package.json b/sdk/typescript/packages/sdk/package.json index fabc99574a..0a416329f4 100644 --- a/sdk/typescript/packages/sdk/package.json +++ b/sdk/typescript/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/sdk", - "version": "1.2.4-rc.2", + "version": "1.3.0-rc.0", "license": "Apache-2.0", "author": "Nym Technologies SA", "files": [ diff --git a/service-providers/ip-packet-router/Cargo.toml b/service-providers/ip-packet-router/Cargo.toml index e700505a96..f36c7b7ed9 100644 --- a/service-providers/ip-packet-router/Cargo.toml +++ b/service-providers/ip-packet-router/Cargo.toml @@ -9,6 +9,7 @@ edition.workspace = true license.workspace = true [dependencies] +anyhow.workspace = true bincode = "1.3.3" bs58 = { workspace = true } bytes = "1.5.0" @@ -32,6 +33,7 @@ nym-tun = { path = "../../common/tun" } nym-types = { path = "../../common/types" } nym-wireguard = { path = "../../common/wireguard" } nym-wireguard-types = { path = "../../common/wireguard-types" } +nym-id = { path = "../../common/nym-id" } rand = "0.8.5" reqwest.workspace = true serde = { workspace = true, features = ["derive"] } diff --git a/service-providers/ip-packet-router/src/cli/add_gateway.rs b/service-providers/ip-packet-router/src/cli/add_gateway.rs new file mode 100644 index 0000000000..ebfffcf149 --- /dev/null +++ b/service-providers/ip-packet-router/src/cli/add_gateway.rs @@ -0,0 +1,30 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliIpPacketRouterClient; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_add_gateway::{add_gateway, CommonClientAddGatewayArgs}; +use nym_ip_packet_router::error::IpPacketRouterError; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientAddGatewayArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientAddGatewayArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), IpPacketRouterError> { + let output = args.output; + let res = add_gateway::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/service-providers/ip-packet-router/src/cli/import_credential.rs b/service-providers/ip-packet-router/src/cli/import_credential.rs new file mode 100644 index 0000000000..2ef4119803 --- /dev/null +++ b/service-providers/ip-packet-router/src/cli/import_credential.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliIpPacketRouterClient; +use nym_client_core::cli_helpers::client_import_credential::{ + import_credential, CommonClientImportCredentialArgs, +}; +use nym_ip_packet_router::error::IpPacketRouterError; + +pub(crate) async fn execute( + args: CommonClientImportCredentialArgs, +) -> Result<(), IpPacketRouterError> { + import_credential::(args).await +} diff --git a/service-providers/ip-packet-router/src/cli/init.rs b/service-providers/ip-packet-router/src/cli/init.rs index 207a6a748c..6b41b287d8 100644 --- a/service-providers/ip-packet-router/src/cli/init.rs +++ b/service-providers/ip-packet-router/src/cli/init.rs @@ -1,29 +1,18 @@ -use std::{fmt::Display, fs, path::PathBuf}; - +use crate::cli::{override_config, CliIpPacketRouterClient, OverrideConfig}; use clap::Args; use nym_bin_common::output_format::OutputFormat; use nym_client_core::cli_helpers::client_init::{ initialise_client, CommonClientInitArgs, InitResultsWithConfig, InitialisableClient, }; -use serde::Serialize; - -use crate::{ - cli::{override_config, try_upgrade_config, OverrideConfig}, +use nym_ip_packet_router::{ config::{default_config_directory, default_config_filepath, default_data_directory, Config}, error::IpPacketRouterError, }; +use serde::Serialize; +use std::{fmt::Display, fs, path::PathBuf}; -struct IpPacketRouterInit; - -impl InitialisableClient for IpPacketRouterInit { - const NAME: &'static str = "ip packet router"; - type Error = IpPacketRouterError; +impl InitialisableClient for CliIpPacketRouterClient { type InitArgs = Init; - type Config = Config; - - fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { - try_upgrade_config(id) - } fn initialise_storage_paths(id: &str) -> Result<(), Self::Error> { fs::create_dir_all(default_data_directory(id))?; @@ -99,7 +88,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), IpPacketRouterError> { eprintln!("Initialising client..."); let output = args.output; - let res = initialise_client::(args).await?; + let res = initialise_client::(args).await?; let init_results = InitResults::new(res); println!("{}", output.format(&init_results)); diff --git a/service-providers/ip-packet-router/src/cli/list_gateways.rs b/service-providers/ip-packet-router/src/cli/list_gateways.rs new file mode 100644 index 0000000000..3050233c87 --- /dev/null +++ b/service-providers/ip-packet-router/src/cli/list_gateways.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliIpPacketRouterClient; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_list_gateways::{ + list_gateways, CommonClientListGatewaysArgs, +}; +use nym_ip_packet_router::error::IpPacketRouterError; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientListGatewaysArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientListGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), IpPacketRouterError> { + let output = args.output; + let res = list_gateways::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/service-providers/ip-packet-router/src/cli/mod.rs b/service-providers/ip-packet-router/src/cli/mod.rs index eda51a5554..3de91de6be 100644 --- a/service-providers/ip-packet-router/src/cli/mod.rs +++ b/service-providers/ip-packet-router/src/cli/mod.rs @@ -1,22 +1,38 @@ use clap::{CommandFactory, Parser, Subcommand}; -use log::{error, trace}; +use log::error; use nym_bin_common::completions::{fig_generate, ArgShell}; use nym_bin_common::{bin_info, version_checker}; -use nym_client_core::client::base_client::storage::gateway_details::{ - OnDiskGatewayDetails, PersistedGatewayDetails, -}; -use nym_client_core::client::key_manager::persistence::OnDiskKeys; -use nym_client_core::config::GatewayEndpointConfig; -use nym_client_core::error::ClientCoreError; +use nym_client_core::cli_helpers::client_import_credential::CommonClientImportCredentialArgs; +use nym_client_core::cli_helpers::CliClient; +use nym_ip_packet_router::config::helpers::try_upgrade_config; +use nym_ip_packet_router::config::{BaseClientConfig, Config}; +use nym_ip_packet_router::error::IpPacketRouterError; use std::sync::OnceLock; -use crate::config::{BaseClientConfig, Config}; -use crate::error::IpPacketRouterError; - +mod add_gateway; mod build_info; +mod import_credential; mod init; +mod list_gateways; mod run; mod sign; +mod switch_gateway; + +pub(crate) struct CliIpPacketRouterClient; + +impl CliClient for CliIpPacketRouterClient { + const NAME: &'static str = "ip packet router"; + type Error = IpPacketRouterError; + type Config = Config; + + async fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { + try_upgrade_config(id).await + } + + async fn try_load_current_config(id: &str) -> Result { + try_load_current_config(id).await + } +} fn pretty_build_info_static() -> &'static str { static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); @@ -48,6 +64,18 @@ pub(crate) enum Commands { /// parameters. Run(run::Run), + /// Import a pre-generated credential + ImportCredential(CommonClientImportCredentialArgs), + + /// List all registered with gateways + ListGateways(list_gateways::Args), + + /// Add new gateway to this client + AddGateway(add_gateway::Args), + + /// Change the currently active gateway. Note that you must have already registered with the new gateway! + SwitchGateway(switch_gateway::Args), + /// Sign to prove ownership of this network requester Sign(sign::Sign), @@ -100,6 +128,10 @@ pub(crate) async fn execute(args: Cli) -> Result<(), IpPacketRouterError> { match args.command { Commands::Init(m) => init::execute(m).await?, Commands::Run(m) => run::execute(&m).await?, + Commands::ImportCredential(m) => import_credential::execute(m).await?, + Commands::ListGateways(args) => list_gateways::execute(args).await?, + Commands::AddGateway(args) => add_gateway::execute(args).await?, + Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, Commands::Sign(m) => sign::execute(&m).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), @@ -108,36 +140,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), IpPacketRouterError> { Ok(()) } -// Unused until we need to start implementing config upgrades -#[allow(unused)] -fn persist_gateway_details( - config: &Config, - details: GatewayEndpointConfig, -) -> Result<(), IpPacketRouterError> { - let details_store = - OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); - let keys_store = OnDiskKeys::new(config.storage_paths.common_paths.keys.clone()); - let shared_keys = keys_store.ephemeral_load_gateway_keys().map_err(|source| { - IpPacketRouterError::ClientCoreError(ClientCoreError::KeyStoreError { - source: Box::new(source), - }) - })?; - let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; - details_store - .store_to_disk(&persisted_details) - .map_err(|source| { - IpPacketRouterError::ClientCoreError(ClientCoreError::GatewayDetailsStoreError { - source: Box::new(source), - }) - }) -} - -fn try_upgrade_config(_id: &str) -> Result<(), IpPacketRouterError> { - trace!("Attempting to upgrade config"); - Ok(()) -} - -fn try_load_current_config(id: &str) -> Result { +async fn try_load_current_config(id: &str) -> Result { // try to load the config as is if let Ok(cfg) = Config::read_from_default_path(id) { return if !cfg.validate() { @@ -148,7 +151,7 @@ fn try_load_current_config(id: &str) -> Result { } // we couldn't load it - try upgrading it from older revisions - try_upgrade_config(id)?; + try_upgrade_config(id).await?; let config = match Config::read_from_default_path(id) { Ok(cfg) => cfg, diff --git a/service-providers/ip-packet-router/src/cli/run.rs b/service-providers/ip-packet-router/src/cli/run.rs index cbfb3f8453..32d97efddc 100644 --- a/service-providers/ip-packet-router/src/cli/run.rs +++ b/service-providers/ip-packet-router/src/cli/run.rs @@ -1,11 +1,9 @@ +use crate::cli::{override_config, OverrideConfig}; use crate::cli::{try_load_current_config, version_check}; -use crate::{ - cli::{override_config, OverrideConfig}, - error::IpPacketRouterError, -}; use clap::Args; use log::error; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; +use nym_ip_packet_router::error::IpPacketRouterError; #[allow(clippy::struct_excessive_bools)] #[derive(Args, Clone)] @@ -25,7 +23,7 @@ impl From for OverrideConfig { } pub(crate) async fn execute(args: &Run) -> Result<(), IpPacketRouterError> { - let mut config = try_load_current_config(&args.common_args.id)?; + let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); log::debug!("Using config: {:#?}", config); @@ -35,7 +33,7 @@ pub(crate) async fn execute(args: &Run) -> Result<(), IpPacketRouterError> { } log::info!("Starting ip packet router service provider"); - let mut server = crate::ip_packet_router::IpPacketRouter::new(config); + let mut server = nym_ip_packet_router::IpPacketRouter::new(config); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { server = server.with_stored_topology(custom_mixnet)? } diff --git a/service-providers/ip-packet-router/src/cli/sign.rs b/service-providers/ip-packet-router/src/cli/sign.rs index c282483de3..968ce83536 100644 --- a/service-providers/ip-packet-router/src/cli/sign.rs +++ b/service-providers/ip-packet-router/src/cli/sign.rs @@ -1,10 +1,10 @@ use crate::cli::{try_load_current_config, version_check}; -use crate::error::IpPacketRouterError; use clap::Args; use nym_bin_common::output_format::OutputFormat; use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::error::ClientCoreError; use nym_crypto::asymmetric::identity; +use nym_ip_packet_router::error::IpPacketRouterError; use nym_types::helpers::ConsoleSigningOutput; #[derive(Args, Clone)] @@ -52,7 +52,7 @@ fn print_signed_contract_msg( } pub(crate) async fn execute(args: &Sign) -> Result<(), IpPacketRouterError> { - let config = try_load_current_config(&args.id)?; + let config = try_load_current_config(&args.id).await?; if !version_check(&config) { log::error!("Failed the local version check"); diff --git a/service-providers/ip-packet-router/src/cli/switch_gateway.rs b/service-providers/ip-packet-router/src/cli/switch_gateway.rs new file mode 100644 index 0000000000..4a365a4bdb --- /dev/null +++ b/service-providers/ip-packet-router/src/cli/switch_gateway.rs @@ -0,0 +1,24 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliIpPacketRouterClient; +use nym_client_core::cli_helpers::client_switch_gateway::{ + switch_gateway, CommonClientSwitchGatewaysArgs, +}; +use nym_ip_packet_router::error::IpPacketRouterError; + +#[derive(clap::Args, Clone, Debug)] +pub struct Args { + #[command(flatten)] + common_args: CommonClientSwitchGatewaysArgs, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientSwitchGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), IpPacketRouterError> { + switch_gateway::(args).await +} diff --git a/service-providers/ip-packet-router/src/config/helpers.rs b/service-providers/ip-packet-router/src/config/helpers.rs new file mode 100644 index 0000000000..e6abda1d98 --- /dev/null +++ b/service-providers/ip-packet-router/src/config/helpers.rs @@ -0,0 +1,49 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::default_config_filepath; +use crate::config::old_config_v1::ConfigV1; +use crate::error::IpPacketRouterError; +use log::{info, trace}; +use nym_client_core::cli_helpers::CliClientConfig; +use nym_client_core::client::base_client::storage::migration_helpers::v1_1_33; +use std::path::Path; + +async fn try_upgrade_v1_config>( + config_path: P, +) -> Result { + // explicitly load it as v1 (which is incompatible with the current one) + let Ok(old_config) = ConfigV1::read_from_toml_file(config_path.as_ref()) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using v1 config template."); + info!("It is going to get updated to the current specification."); + + let old_paths = old_config.storage_paths.clone(); + let updated = old_config.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + None, + ) + .await?; + + updated.save_to(config_path)?; + Ok(true) +} + +pub async fn try_upgrade_config>(config_path: P) -> Result<(), IpPacketRouterError> { + trace!("Attempting to upgrade config"); + if try_upgrade_v1_config(config_path).await? { + return Ok(()); + } + + Ok(()) +} + +pub async fn try_upgrade_config_by_id(id: &str) -> Result<(), IpPacketRouterError> { + try_upgrade_config(default_config_filepath(id)).await +} diff --git a/service-providers/ip-packet-router/src/config/mod.rs b/service-providers/ip-packet-router/src/config/mod.rs index 1ea0ee755b..048fcd67f8 100644 --- a/service-providers/ip-packet-router/src/config/mod.rs +++ b/service-providers/ip-packet-router/src/config/mod.rs @@ -1,9 +1,7 @@ pub use nym_client_core::config::Config as BaseClientConfig; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::{ - cli_helpers::client_init::ClientConfig, config::disk_persistence::CommonClientPaths, -}; +use nym_client_core::{cli_helpers::CliClientConfig, config::disk_persistence::CommonClientPaths}; use nym_config::{ defaults::mainnet, must_get_home, save_formatted_config_to_file, serde_helpers::de_maybe_stringified, NymConfigTemplate, OptionalSet, DEFAULT_CONFIG_DIR, @@ -22,6 +20,8 @@ use crate::config::persistence::IpPacketRouterPaths; use self::template::CONFIG_TEMPLATE; +pub mod helpers; +pub mod old_config_v1; mod persistence; mod template; @@ -75,7 +75,7 @@ impl NymConfigTemplate for Config { } } -impl ClientConfig for Config { +impl CliClientConfig for Config { fn common_paths(&self) -> &CommonClientPaths { &self.storage_paths.common_paths } diff --git a/service-providers/ip-packet-router/src/config/old_config_v1.rs b/service-providers/ip-packet-router/src/config/old_config_v1.rs new file mode 100644 index 0000000000..34399af4cf --- /dev/null +++ b/service-providers/ip-packet-router/src/config/old_config_v1.rs @@ -0,0 +1,95 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::persistence::IpPacketRouterPaths; +use crate::config::Config; +use crate::config::{default_config_filepath, IpPacketRouter}; +use crate::error::IpPacketRouterError; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; +use nym_client_core::config::old_config_v1_1_33::ConfigV1_1_33 as BaseConfigV1_1_33; +use nym_config::read_config_from_toml_file; +use nym_config::serde_helpers::de_maybe_stringified; +use nym_network_defaults::mainnet; +use serde::{Deserialize, Serialize}; +use std::io; +use std::path::{Path, PathBuf}; +use url::Url; + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone)] +pub struct IpPacketRouterPathsV1 { + #[serde(flatten)] + pub common_paths: CommonClientPathsV1_1_33, + + /// Location of the file containing our description + pub ip_packet_router_description: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1 { + #[serde(flatten)] + pub base: BaseConfigV1_1_33, + + #[serde(default)] + pub ip_packet_router: IpPacketRouterV1, + + pub storage_paths: IpPacketRouterPathsV1, + + pub logging: LoggingSettings, +} + +impl ConfigV1 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + base: self.base.into(), + ip_packet_router: self.ip_packet_router.into(), + storage_paths: IpPacketRouterPaths { + common_paths: self.storage_paths.common_paths.upgrade_default()?, + ip_packet_router_description: self.storage_paths.ip_packet_router_description, + }, + logging: self.logging, + }) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct IpPacketRouterV1 { + /// Disable Poisson sending rate. + pub disable_poisson_rate: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub upstream_exit_policy_url: Option, +} + +impl Default for IpPacketRouterV1 { + fn default() -> Self { + IpPacketRouterV1 { + disable_poisson_rate: true, + upstream_exit_policy_url: Some( + mainnet::EXIT_POLICY_URL + .parse() + .expect("invalid default exit policy URL"), + ), + } + } +} + +impl From for IpPacketRouter { + fn from(value: IpPacketRouterV1) -> Self { + IpPacketRouter { + disable_poisson_rate: value.disable_poisson_rate, + upstream_exit_policy_url: value.upstream_exit_policy_url, + } + } +} diff --git a/service-providers/ip-packet-router/src/config/template.rs b/service-providers/ip-packet-router/src/config/template.rs index bbd9e18d5d..ab5db7bbe8 100644 --- a/service-providers/ip-packet-router/src/config/template.rs +++ b/service-providers/ip-packet-router/src/config/template.rs @@ -51,10 +51,6 @@ keys.private_encryption_key_file = '{{ storage_paths.keys.private_encryption_key # Path to file containing public encryption key. keys.public_encryption_key_file = '{{ storage_paths.keys.public_encryption_key_file }}' -# A gateway specific, optional, base58 stringified shared key used for -# communication with particular gateway. -keys.gateway_shared_key_file = '{{ storage_paths.keys.gateway_shared_key_file }}' - # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' @@ -62,13 +58,13 @@ keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' # Path to the database containing bandwidth credentials credentials_database = '{{ storage_paths.credentials_database }}' -# Path to the file containing information about gateway used by this client, -# i.e. details such as its public key, owner address or the network information. -gateway_details = '{{ storage_paths.gateway_details }}' - # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. reply_surb_database = '{{ storage_paths.reply_surb_database }}' +# Path to the file containing information about gateways used by this client, +# i.e. details such as their public keys, owner addresses or the network information. +gateway_registrations = '{{ storage_paths.gateway_registrations }}' + # Location of the file containing our allow.list allowed_list_location = '{{ storage_paths.allowed_list_location }}' diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index 4732177191..2cff116db4 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -2,6 +2,7 @@ use std::net::SocketAddr; pub use nym_client_core::error::ClientCoreError; use nym_exit_policy::PolicyError; +use nym_id::NymIdError; #[derive(thiserror::Error, Debug)] pub enum IpPacketRouterError { @@ -81,6 +82,9 @@ pub enum IpPacketRouterError { #[error("failed to update client activity")] FailedToUpdateClientActivity, + + #[error(transparent)] + NymIdError(#[from] NymIdError), } pub type Result = std::result::Result; diff --git a/service-providers/ip-packet-router/src/main.rs b/service-providers/ip-packet-router/src/main.rs index 143833aecb..c8f9fd0e1b 100644 --- a/service-providers/ip-packet-router/src/main.rs +++ b/service-providers/ip-packet-router/src/main.rs @@ -1,29 +1,9 @@ #[cfg(target_os = "linux")] mod cli; -#[cfg(target_os = "linux")] -mod config; -#[cfg(target_os = "linux")] -mod connected_client_handler; -#[cfg(target_os = "linux")] -mod constants; -#[cfg(target_os = "linux")] -mod error; -#[cfg(target_os = "linux")] -mod ip_packet_router; -#[cfg(target_os = "linux")] -mod mixnet_client; -#[cfg(target_os = "linux")] -mod mixnet_listener; -#[cfg(target_os = "linux")] -mod request_filter; -#[cfg(target_os = "linux")] -mod tun_listener; -#[cfg(target_os = "linux")] -mod util; #[cfg(target_os = "linux")] #[tokio::main] -async fn main() -> Result<(), error::IpPacketRouterError> { +async fn main() -> anyhow::Result<()> { use clap::Parser; let args = cli::Cli::parse(); @@ -34,7 +14,8 @@ async fn main() -> Result<(), error::IpPacketRouterError> { nym_bin_common::logging::maybe_print_banner(clap::crate_name!(), clap::crate_version!()); } - cli::execute(args).await + cli::execute(args).await?; + Ok(()) } #[cfg(not(target_os = "linux"))] diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 03923f46ad..63e82dbe58 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -16,6 +16,7 @@ name = "nym_network_requester" path = "src/lib.rs" [dependencies] +anyhow = { workspace = true } addr = "0.15.6" async-trait = { workspace = true } bs58 = { workspace = true } @@ -44,7 +45,7 @@ zeroize = "1.6.0" # internal nym-async-file-watcher = { path = "../../common/async-file-watcher" } nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } -nym-client-core = { path = "../../common/client-core", features = ["cli"] } +nym-client-core = { path = "../../common/client-core", features = ["cli", "fs-gateways-storage", "fs-surb-storage"] } nym-client-websocket-requests = { path = "../../clients/native/websocket-requests" } nym-config = { path = "../../common/config" } nym-credentials = { path = "../../common/credentials" } @@ -65,4 +66,3 @@ nym-id = { path = "../../common/nym-id" } [dev-dependencies] tempfile = "3.5.0" -anyhow = { workspace = true } diff --git a/service-providers/network-requester/src/cli/add_gateway.rs b/service-providers/network-requester/src/cli/add_gateway.rs new file mode 100644 index 0000000000..e07f16f46d --- /dev/null +++ b/service-providers/network-requester/src/cli/add_gateway.rs @@ -0,0 +1,30 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliNetworkRequesterClient; +use crate::error::NetworkRequesterError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_add_gateway::{add_gateway, CommonClientAddGatewayArgs}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientAddGatewayArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientAddGatewayArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { + let output = args.output; + let res = add_gateway::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/service-providers/network-requester/src/cli/import_credential.rs b/service-providers/network-requester/src/cli/import_credential.rs index d46b0442bf..aeb42d79b7 100644 --- a/service-providers/network-requester/src/cli/import_credential.rs +++ b/service-providers/network-requester/src/cli/import_credential.rs @@ -1,54 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cli::try_load_current_config; +use crate::cli::CliNetworkRequesterClient; use crate::error::NetworkRequesterError; -use clap::ArgGroup; +use nym_client_core::cli_helpers::client_import_credential::{ + import_credential, CommonClientImportCredentialArgs, +}; -use nym_id::import_credential; -use std::fs; -use std::path::PathBuf; - -fn parse_encoded_credential_data(raw: &str) -> bs58::decode::Result> { - bs58::decode(raw).into_vec() -} - -#[derive(clap::Args)] -#[clap(group(ArgGroup::new("cred_data").required(true)))] -pub(crate) struct Args { - /// Id of client that is going to import the credential - #[clap(long)] - pub id: String, - - /// Explicitly provide the encoded credential data (as base58) - #[clap(long, group = "cred_data", value_parser = parse_encoded_credential_data)] - pub(crate) credential_data: Option>, - - /// Specifies the path to file containing binary credential data - #[clap(long, group = "cred_data")] - pub(crate) credential_path: Option, - - // currently hidden as there exists only a single serialization standard - #[clap(long, hide = true)] - pub(crate) version: Option, -} - -pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { - let config = try_load_current_config(&args.id)?; - - let credentials_store = nym_credential_storage::initialise_persistent_storage( - &config.storage_paths.common_paths.credentials_database, - ) - .await; - - let raw_credential = match args.credential_data { - Some(data) => data, - None => { - // SAFETY: one of those arguments must have been set - fs::read(args.credential_path.unwrap())? - } - }; - - import_credential(credentials_store, raw_credential, args.version).await?; - Ok(()) +pub(crate) async fn execute( + args: CommonClientImportCredentialArgs, +) -> Result<(), NetworkRequesterError> { + import_credential::(args).await } diff --git a/service-providers/network-requester/src/cli/init.rs b/service-providers/network-requester/src/cli/init.rs index 630c4546f4..b9cef1f16a 100644 --- a/service-providers/network-requester/src/cli/init.rs +++ b/service-providers/network-requester/src/cli/init.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::cli::try_upgrade_config; +use crate::cli::CliNetworkRequesterClient; use crate::config::{default_config_directory, default_config_filepath, default_data_directory}; use crate::{ cli::{override_config, OverrideConfig}, @@ -18,17 +18,8 @@ use std::fmt::Display; use std::fs; use std::path::PathBuf; -struct NetworkRequesterInit; - -impl InitialisableClient for NetworkRequesterInit { - const NAME: &'static str = "network requester"; - type Error = NetworkRequesterError; +impl InitialisableClient for CliNetworkRequesterClient { type InitArgs = Init; - type Config = Config; - - fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { - try_upgrade_config(id) - } fn initialise_storage_paths(id: &str) -> Result<(), Self::Error> { fs::create_dir_all(default_data_directory(id))?; @@ -130,7 +121,7 @@ pub(crate) async fn execute(args: Init) -> Result<(), NetworkRequesterError> { eprintln!("Initialising client..."); let output = args.output; - let res = initialise_client::(args).await?; + let res = initialise_client::(args).await?; let init_results = InitResults::new(res); println!("{}", output.format(&init_results)); diff --git a/service-providers/network-requester/src/cli/list_gateways.rs b/service-providers/network-requester/src/cli/list_gateways.rs new file mode 100644 index 0000000000..ec4703045b --- /dev/null +++ b/service-providers/network-requester/src/cli/list_gateways.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliNetworkRequesterClient; +use crate::error::NetworkRequesterError; +use nym_bin_common::output_format::OutputFormat; +use nym_client_core::cli_helpers::client_list_gateways::{ + list_gateways, CommonClientListGatewaysArgs, +}; + +#[derive(clap::Args)] +pub(crate) struct Args { + #[command(flatten)] + common_args: CommonClientListGatewaysArgs, + + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientListGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { + let output = args.output; + let res = list_gateways::(args).await?; + + println!("{}", output.format(&res)); + Ok(()) +} diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index cd5e84071e..290c4b8980 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -1,32 +1,45 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::old_config_v1_1_13::OldConfigV1_1_13; -use crate::config::old_config_v1_1_20::ConfigV1_1_20; -use crate::config::old_config_v1_1_20_2::ConfigV1_1_20_2; +use crate::config::helpers::try_upgrade_config_by_id; use crate::{ config::{BaseClientConfig, Config}, error::NetworkRequesterError, }; use clap::{CommandFactory, Parser, Subcommand}; -use log::{error, info, trace}; +use log::error; use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; use nym_bin_common::version_checker; -use nym_client_core::client::base_client::storage::gateway_details::{ - OnDiskGatewayDetails, PersistedGatewayDetails, -}; -use nym_client_core::client::key_manager::persistence::OnDiskKeys; -use nym_client_core::config::GatewayEndpointConfig; -use nym_client_core::error::ClientCoreError; +use nym_client_core::cli_helpers::client_import_credential::CommonClientImportCredentialArgs; +use nym_client_core::cli_helpers::CliClient; use nym_config::OptionalSet; use std::sync::OnceLock; +mod add_gateway; mod build_info; mod import_credential; mod init; +mod list_gateways; mod run; mod sign; +mod switch_gateway; + +pub(crate) struct CliNetworkRequesterClient; + +impl CliClient for CliNetworkRequesterClient { + const NAME: &'static str = "network requester"; + type Error = NetworkRequesterError; + type Config = Config; + + async fn try_upgrade_outdated_config(id: &str) -> Result<(), Self::Error> { + try_upgrade_config_by_id(id).await + } + + async fn try_load_current_config(id: &str) -> Result { + try_load_current_config(id).await + } +} fn pretty_build_info_static() -> &'static str { static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); @@ -62,7 +75,16 @@ pub(crate) enum Commands { Sign(sign::Sign), /// Import a pre-generated credential - ImportCredential(import_credential::Args), + ImportCredential(CommonClientImportCredentialArgs), + + /// List all registered with gateways + ListGateways(list_gateways::Args), + + /// Add new gateway to this client + AddGateway(add_gateway::Args), + + /// Change the currently active gateway. Note that you must have already registered with the new gateway! + SwitchGateway(switch_gateway::Args), /// Show build information of this binary BuildInfo(build_info::BuildInfo), @@ -159,6 +181,9 @@ pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> { Commands::Run(m) => run::execute(&m).await?, Commands::Sign(m) => sign::execute(&m).await?, Commands::ImportCredential(m) => import_credential::execute(m).await?, + Commands::ListGateways(args) => list_gateways::execute(args).await?, + Commands::AddGateway(args) => add_gateway::execute(args).await?, + Commands::SwitchGateway(args) => switch_gateway::execute(args).await?, Commands::BuildInfo(m) => build_info::execute(m), Commands::Completions(s) => s.generate(&mut Cli::command(), bin_name), Commands::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), @@ -166,107 +191,7 @@ pub(crate) async fn execute(args: Cli) -> Result<(), NetworkRequesterError> { Ok(()) } -fn persist_gateway_details( - config: &Config, - details: GatewayEndpointConfig, -) -> Result<(), NetworkRequesterError> { - let details_store = - OnDiskGatewayDetails::new(&config.storage_paths.common_paths.gateway_details); - let keys_store = OnDiskKeys::new(config.storage_paths.common_paths.keys.clone()); - let shared_keys = keys_store.ephemeral_load_gateway_keys().map_err(|source| { - NetworkRequesterError::ClientCoreError(ClientCoreError::KeyStoreError { - source: Box::new(source), - }) - })?; - let persisted_details = PersistedGatewayDetails::new(details.into(), Some(&shared_keys))?; - details_store - .store_to_disk(&persisted_details) - .map_err(|source| { - NetworkRequesterError::ClientCoreError(ClientCoreError::GatewayDetailsStoreError { - source: Box::new(source), - }) - }) -} - -fn try_upgrade_v1_1_13_config(id: &str) -> Result { - trace!("Trying to load as v1.1.13 config"); - use nym_config::legacy_helpers::nym_config::MigrationNymConfig; - - // explicitly load it as v1.1.13 (which is incompatible with the next step, i.e. 1.1.19) - let Ok(old_config) = OldConfigV1_1_13::load_from_file(id) else { - // if we failed to load it, there might have been nothing to upgrade - // or maybe it was an even older file. in either way. just ignore it and carry on with our day - return Ok(false); - }; - info!("It seems the client is using <= v1.1.13 config template."); - info!("It is going to get updated to the current specification."); - - let updated_step1: ConfigV1_1_20 = old_config.into(); - let updated_step2: ConfigV1_1_20_2 = updated_step1.into(); - let (updated, gateway_config) = updated_step2.upgrade()?; - persist_gateway_details(&updated, gateway_config)?; - - updated.save_to_default_location()?; - Ok(true) -} - -fn try_upgrade_v1_1_20_config(id: &str) -> Result { - trace!("Trying to load as v1.1.20 config"); - use nym_config::legacy_helpers::nym_config::MigrationNymConfig; - - // explicitly load it as v1.1.20 (which is incompatible with the current one, i.e. +1.1.21) - let Ok(old_config) = ConfigV1_1_20::load_from_file(id) else { - // if we failed to load it, there might have been nothing to upgrade - // or maybe it was an even older file. in either way. just ignore it and carry on with our day - return Ok(false); - }; - - info!("It seems the client is using <= v1.1.20 config template."); - info!("It is going to get updated to the current specification."); - - let updated_step1: ConfigV1_1_20_2 = old_config.into(); - let (updated, gateway_config) = updated_step1.upgrade()?; - persist_gateway_details(&updated, gateway_config)?; - - updated.save_to_default_location()?; - Ok(true) -} - -fn try_upgrade_v1_1_20_2_config(id: &str) -> Result { - trace!("Trying to load as v1.1.20_2 config"); - - // explicitly load it as v1.1.20_2 (which is incompatible with the current one, i.e. +1.1.21) - let Ok(old_config) = ConfigV1_1_20_2::read_from_default_path(id) else { - // if we failed to load it, there might have been nothing to upgrade - // or maybe it was an even older file. in either way. just ignore it and carry on with our day - return Ok(false); - }; - info!("It seems the client is using <= v1.1.20_2 config template."); - info!("It is going to get updated to the current specification."); - - let (updated, gateway_config) = old_config.upgrade()?; - persist_gateway_details(&updated, gateway_config)?; - - updated.save_to_default_location()?; - Ok(true) -} - -fn try_upgrade_config(id: &str) -> Result<(), NetworkRequesterError> { - trace!("Attempting to upgrade config"); - if try_upgrade_v1_1_13_config(id)? { - return Ok(()); - } - if try_upgrade_v1_1_20_config(id)? { - return Ok(()); - } - if try_upgrade_v1_1_20_2_config(id)? { - return Ok(()); - } - - Ok(()) -} - -fn try_load_current_config(id: &str) -> Result { +async fn try_load_current_config(id: &str) -> Result { // try to load the config as is if let Ok(cfg) = Config::read_from_default_path(id) { return if !cfg.validate() { @@ -277,7 +202,7 @@ fn try_load_current_config(id: &str) -> Result { } // we couldn't load it - try upgrading it from older revisions - try_upgrade_config(id)?; + try_upgrade_config_by_id(id).await?; let config = match Config::read_from_default_path(id) { Ok(cfg) => cfg, diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 1295734bda..cd3a5ad653 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -66,7 +66,7 @@ impl From for OverrideConfig { } pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { - let mut config = try_load_current_config(&args.common_args.id)?; + let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); log::debug!("Using config: {:#?}", config); diff --git a/service-providers/network-requester/src/cli/sign.rs b/service-providers/network-requester/src/cli/sign.rs index 7a0dc29d7b..9e43de6009 100644 --- a/service-providers/network-requester/src/cli/sign.rs +++ b/service-providers/network-requester/src/cli/sign.rs @@ -55,7 +55,7 @@ fn print_signed_contract_msg( } pub(crate) async fn execute(args: &Sign) -> Result<(), NetworkRequesterError> { - let config = try_load_current_config(&args.id)?; + let config = try_load_current_config(&args.id).await?; if !version_check(&config) { log::error!("Failed the local version check"); diff --git a/service-providers/network-requester/src/cli/switch_gateway.rs b/service-providers/network-requester/src/cli/switch_gateway.rs new file mode 100644 index 0000000000..7496fc0018 --- /dev/null +++ b/service-providers/network-requester/src/cli/switch_gateway.rs @@ -0,0 +1,24 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli::CliNetworkRequesterClient; +use crate::error::NetworkRequesterError; +use nym_client_core::cli_helpers::client_switch_gateway::{ + switch_gateway, CommonClientSwitchGatewaysArgs, +}; + +#[derive(clap::Args, Clone, Debug)] +pub struct Args { + #[command(flatten)] + common_args: CommonClientSwitchGatewaysArgs, +} + +impl AsRef for Args { + fn as_ref(&self) -> &CommonClientSwitchGatewaysArgs { + &self.common_args + } +} + +pub(crate) async fn execute(args: Args) -> Result<(), NetworkRequesterError> { + switch_gateway::(args).await +} diff --git a/service-providers/network-requester/src/config/helpers.rs b/service-providers/network-requester/src/config/helpers.rs new file mode 100644 index 0000000000..e03281fa01 --- /dev/null +++ b/service-providers/network-requester/src/config/helpers.rs @@ -0,0 +1,158 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::default_config_filepath; +use crate::config::old_config_v1_1_13::OldConfigV1_1_13; +use crate::config::old_config_v1_1_20::ConfigV1_1_20; +use crate::config::old_config_v1_1_20_2::ConfigV1_1_20_2; +use crate::config::old_config_v1_1_33::ConfigV1_1_33; +use crate::error::NetworkRequesterError; +use log::{info, trace}; +use nym_client_core::cli_helpers::CliClientConfig; +use nym_client_core::client::base_client::storage::migration_helpers::v1_1_33; +use std::path::Path; + +async fn try_upgrade_v1_1_13_config>( + config_path: P, +) -> Result { + trace!("Trying to load as v1.1.13 config"); + use nym_config::legacy_helpers::nym_config::MigrationNymConfig; + + // explicitly load it as v1.1.13 (which is incompatible with the next step, i.e. 1.1.19) + let Ok(old_config) = OldConfigV1_1_13::load_from_filepath(config_path.as_ref()) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.13 config template."); + info!("It is going to get updated to the current specification."); + + let updated_step1: ConfigV1_1_20 = old_config.into(); + let updated_step2: ConfigV1_1_20_2 = updated_step1.into(); + let (updated_step3, gateway_config) = updated_step2.upgrade()?; + let old_paths = updated_step3.storage_paths.clone(); + let updated = updated_step3.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; + + updated.save_to(config_path)?; + Ok(true) +} + +async fn try_upgrade_v1_1_20_config>( + config_path: P, +) -> Result { + trace!("Trying to load as v1.1.20 config"); + use nym_config::legacy_helpers::nym_config::MigrationNymConfig; + + // explicitly load it as v1.1.20 (which is incompatible with the current one, i.e. +1.1.21) + let Ok(old_config) = ConfigV1_1_20::load_from_filepath(config_path.as_ref()) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + + info!("It seems the client is using <= v1.1.20 config template."); + info!("It is going to get updated to the current specification."); + + let updated_step1: ConfigV1_1_20_2 = old_config.into(); + let (updated_step2, gateway_config) = updated_step1.upgrade()?; + let old_paths = updated_step2.storage_paths.clone(); + let updated = updated_step2.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; + + updated.save_to(config_path)?; + Ok(true) +} + +async fn try_upgrade_v1_1_20_2_config>( + config_path: P, +) -> Result { + trace!("Trying to load as v1.1.20_2 config"); + + // explicitly load it as v1.1.20_2 (which is incompatible with the current one, i.e. +1.1.21) + let Ok(old_config) = ConfigV1_1_20_2::read_from_toml_file(config_path.as_ref()) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.20_2 config template."); + info!("It is going to get updated to the current specification."); + + let (updated_step1, gateway_config) = old_config.upgrade()?; + let old_paths = updated_step1.storage_paths.clone(); + let updated = updated_step1.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + Some(gateway_config), + ) + .await?; + + updated.save_to(config_path)?; + Ok(true) +} + +async fn try_upgrade_v1_1_33_config>( + config_path: P, +) -> Result { + trace!("Trying to load as v1.1.33 config"); + + // explicitly load it as v1.1.33 (which is incompatible with the current one, i.e. +1.1.34) + let Ok(old_config) = ConfigV1_1_33::read_from_toml_file(config_path.as_ref()) else { + // if we failed to load it, there might have been nothing to upgrade + // or maybe it was an even older file. in either way. just ignore it and carry on with our day + return Ok(false); + }; + info!("It seems the client is using <= v1.1.33 config template."); + info!("It is going to get updated to the current specification."); + + let old_paths = old_config.storage_paths.clone(); + let updated = old_config.try_upgrade()?; + + v1_1_33::migrate_gateway_details( + &old_paths.common_paths, + &updated.storage_paths.common_paths, + None, + ) + .await?; + + updated.save_to(config_path)?; + Ok(true) +} + +pub async fn try_upgrade_config>( + config_path: P, +) -> Result<(), NetworkRequesterError> { + trace!("Attempting to upgrade config"); + if try_upgrade_v1_1_13_config(config_path.as_ref()).await? { + return Ok(()); + } + if try_upgrade_v1_1_20_config(config_path.as_ref()).await? { + return Ok(()); + } + if try_upgrade_v1_1_20_2_config(config_path.as_ref()).await? { + return Ok(()); + } + if try_upgrade_v1_1_33_config(config_path).await? { + return Ok(()); + } + + Ok(()) +} + +pub async fn try_upgrade_config_by_id(id: &str) -> Result<(), NetworkRequesterError> { + try_upgrade_config(default_config_filepath(id)).await +} diff --git a/service-providers/network-requester/src/config/mod.rs b/service-providers/network-requester/src/config/mod.rs index bce2d5effa..53cdc143f4 100644 --- a/service-providers/network-requester/src/config/mod.rs +++ b/service-providers/network-requester/src/config/mod.rs @@ -4,7 +4,7 @@ use crate::config::persistence::NetworkRequesterPaths; use crate::config::template::CONFIG_TEMPLATE; use nym_bin_common::logging::LoggingSettings; -use nym_client_core::cli_helpers::client_init::ClientConfig; +use nym_client_core::cli_helpers::CliClientConfig; use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_config::{ must_get_home, read_config_from_toml_file, save_formatted_config_to_file, @@ -23,9 +23,11 @@ use url::Url; pub use nym_client_core::config::Config as BaseClientConfig; +pub mod helpers; pub mod old_config_v1_1_13; pub mod old_config_v1_1_20; pub mod old_config_v1_1_20_2; +pub mod old_config_v1_1_33; mod persistence; mod template; @@ -84,7 +86,7 @@ impl NymConfigTemplate for Config { } } -impl ClientConfig for Config { +impl CliClientConfig for Config { fn common_paths(&self) -> &CommonClientPaths { &self.storage_paths.common_paths } @@ -133,6 +135,7 @@ impl Config { default_config_filepath(&self.base.client.id) } + #[allow(dead_code)] pub fn save_to_default_location(&self) -> io::Result<()> { let config_save_location: PathBuf = self.default_location(); save_formatted_config_to_file(self, config_save_location) diff --git a/service-providers/network-requester/src/config/old_config_v1_1_20.rs b/service-providers/network-requester/src/config/old_config_v1_1_20.rs index a3ab8c6610..603b0f6d92 100644 --- a/service-providers/network-requester/src/config/old_config_v1_1_20.rs +++ b/service-providers/network-requester/src/config/old_config_v1_1_20.rs @@ -4,8 +4,8 @@ use crate::config::old_config_v1_1_20_2::{ ConfigV1_1_20_2, DebugV1_1_20_2, NetworkRequesterPathsV1_1_20_2, }; -use nym_client_core::config::disk_persistence::keys_paths::ClientKeysPaths; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; +use nym_client_core::config::disk_persistence::old_v1_1_33::ClientKeysPathsV1_1_33; use nym_client_core::config::old_config_v1_1_20::ConfigV1_1_20 as BaseConfigV1_1_20; use nym_client_core::config::old_config_v1_1_20_2::{ ClientV1_1_20_2, ConfigV1_1_20_2 as BaseClientConfigV1_1_20_2, @@ -47,7 +47,7 @@ impl From for ConfigV1_1_20_2 { network_requester: Default::default(), storage_paths: NetworkRequesterPathsV1_1_20_2 { common_paths: CommonClientPathsV1_1_20_2 { - keys: ClientKeysPaths { + keys: ClientKeysPathsV1_1_33 { private_identity_key_file: value.base.client.private_identity_key_file, public_identity_key_file: value.base.client.public_identity_key_file, private_encryption_key_file: value.base.client.private_encryption_key_file, diff --git a/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs b/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs index 2263bfd42a..8fe4c90232 100644 --- a/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs +++ b/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs @@ -1,20 +1,16 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - config::{ - default_config_filepath, persistence::NetworkRequesterPaths, Config, Debug, - NetworkRequester, - }, - error::NetworkRequesterError, +use super::old_config_v1_1_33::{ + ConfigV1_1_33, DebugV1_1_33, NetworkRequesterPathsV1_1_33, NetworkRequesterV1_1_33, }; - +use crate::{config::default_config_filepath, error::NetworkRequesterError}; use log::trace; use nym_bin_common::logging::LoggingSettings; use nym_client_core::config::disk_persistence::old_v1_1_20_2::CommonClientPathsV1_1_20_2; use nym_client_core::config::old_config_v1_1_20_2::ConfigV1_1_20_2 as BaseClientConfigV1_1_20_2; use nym_client_core::config::old_config_v1_1_30::ConfigV1_1_30 as BaseConfigV1_1_30; -use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33; use nym_config::read_config_from_toml_file; use serde::{Deserialize, Serialize}; use std::io; @@ -59,13 +55,16 @@ impl ConfigV1_1_20_2 { read_config_from_toml_file(path) } + #[allow(dead_code)] pub fn read_from_default_path>(id: P) -> io::Result { Self::read_from_toml_file(default_config_filepath(id)) } // in this upgrade, gateway endpoint configuration was moved out of the config file, // so its returned to be stored elsewhere. - pub fn upgrade(self) -> Result<(Config, GatewayEndpointConfig), NetworkRequesterError> { + pub fn upgrade( + self, + ) -> Result<(ConfigV1_1_33, OldGatewayEndpointConfigV1_1_33), NetworkRequesterError> { trace!("Upgrading from v1.1.20_2"); let gateway_details = self.base.client.gateway_endpoint.clone().into(); let nr_description = self @@ -76,9 +75,9 @@ impl ConfigV1_1_20_2 { .parent() .expect("config paths upgrade failure") .join(DEFAULT_DESCRIPTION_FILENAME); - let config = Config { + let config = ConfigV1_1_33 { base: BaseConfigV1_1_30::from(self.base).into(), - storage_paths: NetworkRequesterPaths { + storage_paths: NetworkRequesterPathsV1_1_33 { common_paths: self.storage_paths.common_paths.upgrade_default()?, allowed_list_location: self.storage_paths.allowed_list_location, unknown_list_location: self.storage_paths.unknown_list_location, @@ -97,9 +96,9 @@ impl ConfigV1_1_20_2 { #[serde(default, deny_unknown_fields)] pub struct NetworkRequesterV1_1_20_2 {} -impl From for NetworkRequester { +impl From for NetworkRequesterV1_1_33 { fn from(_value: NetworkRequesterV1_1_20_2) -> Self { - NetworkRequester::default() + NetworkRequesterV1_1_33::default() } } @@ -111,9 +110,9 @@ pub struct DebugV1_1_20_2 { pub standard_list_update_interval: Duration, } -impl From for Debug { +impl From for DebugV1_1_33 { fn from(value: DebugV1_1_20_2) -> Self { - Debug { + DebugV1_1_33 { standard_list_update_interval: value.standard_list_update_interval, } } diff --git a/service-providers/network-requester/src/config/old_config_v1_1_33.rs b/service-providers/network-requester/src/config/old_config_v1_1_33.rs new file mode 100644 index 0000000000..b3089b036f --- /dev/null +++ b/service-providers/network-requester/src/config/old_config_v1_1_33.rs @@ -0,0 +1,141 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::config::persistence::NetworkRequesterPaths; +use crate::config::Config; +use crate::config::{default_config_filepath, Debug, NetworkRequester}; +use crate::error::NetworkRequesterError; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33; +use nym_client_core::config::old_config_v1_1_33::ConfigV1_1_33 as BaseConfigV1_1_33; +use nym_config::read_config_from_toml_file; +use nym_config::serde_helpers::de_maybe_stringified; +use serde::{Deserialize, Serialize}; +use std::io; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use url::Url; + +pub const DEFAULT_STANDARD_LIST_UPDATE_INTERVAL: Duration = Duration::from_secs(30 * 60); + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Clone)] +pub struct NetworkRequesterPathsV1_1_33 { + #[serde(flatten)] + pub common_paths: CommonClientPathsV1_1_33, + + /// Location of the file containing our allow.list + pub allowed_list_location: PathBuf, + + /// Location of the file containing our unknown.list + pub unknown_list_location: PathBuf, + + #[serde(default)] + pub nr_description: PathBuf, +} + +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV1_1_33 { + #[serde(flatten)] + pub base: BaseConfigV1_1_33, + + #[serde(default)] + pub network_requester: NetworkRequesterV1_1_33, + + pub storage_paths: NetworkRequesterPathsV1_1_33, + + #[serde(default)] + pub network_requester_debug: DebugV1_1_33, + + pub logging: LoggingSettings, +} + +impl ConfigV1_1_33 { + pub fn read_from_toml_file>(path: P) -> io::Result { + read_config_from_toml_file(path) + } + + #[allow(dead_code)] + pub fn read_from_default_path>(id: P) -> io::Result { + Self::read_from_toml_file(default_config_filepath(id)) + } + + pub fn try_upgrade(self) -> Result { + Ok(Config { + base: self.base.into(), + network_requester: self.network_requester.into(), + storage_paths: NetworkRequesterPaths { + common_paths: self.storage_paths.common_paths.upgrade_default()?, + allowed_list_location: self.storage_paths.allowed_list_location, + unknown_list_location: self.storage_paths.unknown_list_location, + nr_description: self.storage_paths.nr_description, + }, + network_requester_debug: self.network_requester_debug.into(), + logging: self.logging, + }) + } +} + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct NetworkRequesterV1_1_33 { + /// specifies whether this network requester should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// specifies whether this network requester would send anonymized statistics to a statistics aggregator server + pub enabled_statistics: bool, + + /// in case of enabled statistics, specifies mixnet client address where a statistics aggregator is running + pub statistics_recipient: Option, + + /// Disable Poisson sending rate. + /// This is equivalent to setting debug.traffic.disable_main_poisson_packet_distribution = true, + pub disable_poisson_rate: bool, + + /// Specifies whether this network requester should be using the deprecated allow-list, + /// as opposed to the new ExitPolicy. + /// Note: this field will be removed in a near future. + pub use_deprecated_allow_list: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub upstream_exit_policy_url: Option, +} + +impl From for NetworkRequester { + fn from(value: NetworkRequesterV1_1_33) -> Self { + NetworkRequester { + open_proxy: value.open_proxy, + enabled_statistics: value.enabled_statistics, + statistics_recipient: value.statistics_recipient, + disable_poisson_rate: value.disable_poisson_rate, + use_deprecated_allow_list: value.use_deprecated_allow_list, + upstream_exit_policy_url: value.upstream_exit_policy_url, + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default, deny_unknown_fields)] +pub struct DebugV1_1_33 { + /// Defines how often the standard allow list should get updated + #[serde(with = "humantime_serde")] + pub standard_list_update_interval: Duration, +} + +impl From for Debug { + fn from(value: DebugV1_1_33) -> Self { + Debug { + standard_list_update_interval: value.standard_list_update_interval, + } + } +} + +impl Default for DebugV1_1_33 { + fn default() -> Self { + DebugV1_1_33 { + standard_list_update_interval: DEFAULT_STANDARD_LIST_UPDATE_INTERVAL, + } + } +} diff --git a/service-providers/network-requester/src/config/persistence.rs b/service-providers/network-requester/src/config/persistence.rs index 242562c1c7..cc3d4378ba 100644 --- a/service-providers/network-requester/src/config/persistence.rs +++ b/service-providers/network-requester/src/config/persistence.rs @@ -23,8 +23,6 @@ pub struct NetworkRequesterPaths { pub unknown_list_location: PathBuf, /// Location of the file containing our description - // For upgrade use default if missing. On next config upgrade iteration, remove the serde(default) - #[serde(default)] pub nr_description: PathBuf, } diff --git a/service-providers/network-requester/src/config/template.rs b/service-providers/network-requester/src/config/template.rs index ee6e3ffc51..84cb00e80c 100644 --- a/service-providers/network-requester/src/config/template.rs +++ b/service-providers/network-requester/src/config/template.rs @@ -51,10 +51,6 @@ keys.private_encryption_key_file = '{{ storage_paths.keys.private_encryption_key # Path to file containing public encryption key. keys.public_encryption_key_file = '{{ storage_paths.keys.public_encryption_key_file }}' -# A gateway specific, optional, base58 stringified shared key used for -# communication with particular gateway. -keys.gateway_shared_key_file = '{{ storage_paths.keys.gateway_shared_key_file }}' - # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' @@ -62,13 +58,13 @@ keys.ack_key_file = '{{ storage_paths.keys.ack_key_file }}' # Path to the database containing bandwidth credentials credentials_database = '{{ storage_paths.credentials_database }}' -# Path to the file containing information about gateway used by this client, -# i.e. details such as its public key, owner address or the network information. -gateway_details = '{{ storage_paths.gateway_details }}' - # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. reply_surb_database = '{{ storage_paths.reply_surb_database }}' +# Path to the file containing information about gateways used by this client, +# i.e. details such as their public keys, owner addresses or the network information. +gateway_registrations = '{{ storage_paths.gateway_registrations }}' + # Location of the file containing our allow.list allowed_list_location = '{{ storage_paths.allowed_list_location }}' diff --git a/service-providers/network-requester/src/lib.rs b/service-providers/network-requester/src/lib.rs index a8e54ad19f..457afc99cd 100644 --- a/service-providers/network-requester/src/lib.rs +++ b/service-providers/network-requester/src/lib.rs @@ -13,16 +13,19 @@ pub use crate::core::{NRServiceProvider, NRServiceProviderBuilder}; pub use config::Config; pub use nym_client_core::{ client::{ - base_client::storage::{gateway_details::OnDiskGatewayDetails, OnDiskPersistent}, + base_client::{ + non_wasm_helpers::{setup_fs_gateways_storage, setup_fs_reply_surb_backend}, + storage::{ + helpers::set_active_gateway, GatewaysDetailsStore, OnDiskGatewaysDetails, + OnDiskPersistent, + }, + }, key_manager::persistence::OnDiskKeys, mix_traffic::transceiver::*, }, init::{ - setup_gateway, - types::{ - CustomGatewayDetails, GatewayDetails, GatewaySelectionSpecification, GatewaySetup, - InitResults, InitialisationResult, - }, + generate_new_client_keys, setup_gateway, + types::{GatewaySelectionSpecification, GatewaySetup, InitResults, InitialisationResult}, }, }; pub use request_filter::RequestFilter; diff --git a/service-providers/network-requester/src/main.rs b/service-providers/network-requester/src/main.rs index 391fe38242..edf687e167 100644 --- a/service-providers/network-requester/src/main.rs +++ b/service-providers/network-requester/src/main.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use clap::{crate_name, crate_version, Parser}; -use error::NetworkRequesterError; use nym_bin_common::logging::{maybe_print_banner, setup_logging}; use nym_network_defaults::setup_env; @@ -16,7 +15,7 @@ mod socks5; mod statistics; #[tokio::main] -async fn main() -> Result<(), NetworkRequesterError> { +async fn main() -> anyhow::Result<()> { let args = cli::Cli::parse(); setup_env(args.config_env_file.as_ref()); @@ -25,5 +24,7 @@ async fn main() -> Result<(), NetworkRequesterError> { } setup_logging(); - cli::execute(args).await + cli::execute(args).await?; + + Ok(()) } diff --git a/wasm/client/Cargo.toml b/wasm/client/Cargo.toml index 6961325d61..2232c88877 100644 --- a/wasm/client/Cargo.toml +++ b/wasm/client/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nym-client-wasm" authors = ["Dave Hrycyszyn ", "Jedrzej Stuczynski "] -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" edition = "2021" keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"] license = "Apache-2.0" diff --git a/wasm/mix-fetch/Cargo.toml b/wasm/mix-fetch/Cargo.toml index 336cee63e3..373b6d423f 100644 --- a/wasm/mix-fetch/Cargo.toml +++ b/wasm/mix-fetch/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "mix-fetch-wasm" authors = ["Jedrzej Stuczynski "] -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" edition = "2021" keywords = ["nym", "fetch", "wasm", "webassembly", "privacy"] license = "Apache-2.0" diff --git a/wasm/node-tester/Cargo.toml b/wasm/node-tester/Cargo.toml index 6a3d8126e7..fd295cd0e3 100644 --- a/wasm/node-tester/Cargo.toml +++ b/wasm/node-tester/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nym-node-tester-wasm" authors = ["Jedrzej Stuczynski "] -version = "1.2.4-rc.2" +version = "1.3.0-rc.0" edition = "2021" keywords = ["nym", "sphinx", "webassembly", "privacy", "tester"] license = "Apache-2.0" diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index 1da2ee7a4f..790c80257d 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -20,16 +20,16 @@ use tokio::sync::Mutex as AsyncMutex; use tsify::Tsify; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; +use wasm_client_core::client::base_client::storage::gateways_storage::GatewayDetails; use wasm_client_core::client::mix_traffic::transceiver::PacketRouter; use wasm_client_core::helpers::{ current_network_topology_async, setup_from_topology, EphemeralCredentialStorage, }; -use wasm_client_core::init::types::GatewayDetails; use wasm_client_core::storage::ClientStorage; use wasm_client_core::topology::SerializableNymTopology; use wasm_client_core::{ - nym_task, BandwidthController, GatewayClient, GatewayConfig, IdentityKey, InitialisationResult, - ManagedKeys, NodeIdentity, NymTopology, QueryReqwestRpcNyxdClient, Recipient, + nym_task, BandwidthController, ClientKeys, GatewayClient, GatewayConfig, IdentityKey, + InitialisationResult, NodeIdentity, NymTopology, QueryReqwestRpcNyxdClient, Recipient, }; use wasm_utils::check_promise_result; use wasm_utils::error::PromisableResult; @@ -77,10 +77,10 @@ pub struct NymNodeTesterBuilder { Option>, } -fn address(keys: &ManagedKeys, gateway_identity: NodeIdentity) -> Recipient { +fn address(keys: &ClientKeys, gateway_identity: NodeIdentity) -> Recipient { Recipient::new( - *keys.identity_public_key(), - *keys.encryption_public_key(), + *keys.identity_keypair().public_key(), + *keys.encryption_keypair().public_key(), gateway_identity, ) } @@ -161,13 +161,14 @@ impl NymNodeTesterBuilder { let client_store = ClientStorage::new_async(&storage_id, None).await?; let initialisation_result = self.gateway_info(&client_store).await?; - let GatewayDetails::Configured(gateway_endpoint) = initialisation_result.gateway_details + let GatewayDetails::Remote(gateway_info) = + initialisation_result.gateway_registration.details else { // don't bother supporting it panic!("unsupported custom gateway configuration in wasm node tester") }; - let managed_keys = initialisation_result.managed_keys; + let managed_keys = initialisation_result.client_keys; let (mixnet_message_sender, mixnet_message_receiver) = mpsc::unbounded(); let (ack_sender, ack_receiver) = mpsc::unbounded(); @@ -179,8 +180,7 @@ impl NymNodeTesterBuilder { gateway_task.fork("packet_router"), ); - let gateway_config: GatewayConfig = gateway_endpoint.try_into()?; - let gateway_identity = gateway_config.gateway_identity; + let gateway_identity = gateway_info.gateway_id; let mut gateway_client = if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client { @@ -190,10 +190,15 @@ impl NymNodeTesterBuilder { gateway_task, ) } else { + let cfg = GatewayConfig::new( + gateway_info.gateway_id, + Some(gateway_info.gateway_owner_address.to_string()), + gateway_info.gateway_listener.to_string(), + ); GatewayClient::new( - gateway_config, + cfg, managed_keys.identity_keypair(), - Some(managed_keys.must_get_gateway_shared_key()), + Some(gateway_info.derived_aes128_ctr_blake3_hmac_keys), packet_router, self.bandwidth_controller.take(), gateway_task,