From 9b9c01fb8f5a777ff614b3140f259ca10fa2a411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 20 Dec 2022 00:48:00 +0100 Subject: [PATCH] rust-sdk: initial version --- Cargo.lock | 71 ++-- .../client-core/src/client/base_client/mod.rs | 35 +- .../client/base_client/non_wasm_helpers.rs | 9 +- clients/client-core/src/client/key_manager.rs | 74 ++++- .../action_controller.rs | 19 +- .../replies/reply_controller/requests.rs | 18 ++ .../reply_storage/backend/browser_backend.rs | 1 + .../replies/reply_storage/backend/mod.rs | 9 +- .../src/config/persistence/key_pathfinder.rs | 41 ++- clients/client-core/src/error.rs | 3 + clients/client-core/src/init/helpers.rs | 15 +- clients/client-core/src/init/mod.rs | 76 ++++- clients/native/src/client/mod.rs | 14 +- clients/socks5/src/client/mod.rs | 11 +- common/task/src/manager.rs | 6 +- sdk/rust/nym-sdk/Cargo.toml | 20 +- sdk/rust/nym-sdk/examples-common/Cargo.toml | 10 + sdk/rust/nym-sdk/examples-common/src/lib.rs | 21 ++ sdk/rust/nym-sdk/examples/complex_config.rs | 40 ++- sdk/rust/nym-sdk/examples/complex_config2.rs | 45 +++ sdk/rust/nym-sdk/examples/simple.rs | 29 +- sdk/rust/nym-sdk/src/error.rs | 10 + sdk/rust/nym-sdk/src/lib.rs | 1 + sdk/rust/nym-sdk/src/mixnet.rs | 15 + sdk/rust/nym-sdk/src/mixnet/client.rs | 306 ++++++++++++++++++ sdk/rust/nym-sdk/src/mixnet/config.rs | 36 +++ .../nym-sdk/src/mixnet/connection_state.rs | 115 +++++++ sdk/rust/nym-sdk/src/mixnet/key_paths.rs | 142 ++++++++ sdk/rust/nym-sdk/src/mixnet/mod.rs | 62 ---- 29 files changed, 1077 insertions(+), 177 deletions(-) create mode 100644 sdk/rust/nym-sdk/examples-common/Cargo.toml create mode 100644 sdk/rust/nym-sdk/examples-common/src/lib.rs create mode 100644 sdk/rust/nym-sdk/examples/complex_config2.rs create mode 100644 sdk/rust/nym-sdk/src/error.rs create mode 100644 sdk/rust/nym-sdk/src/mixnet.rs create mode 100644 sdk/rust/nym-sdk/src/mixnet/client.rs create mode 100644 sdk/rust/nym-sdk/src/mixnet/config.rs create mode 100644 sdk/rust/nym-sdk/src/mixnet/connection_state.rs create mode 100644 sdk/rust/nym-sdk/src/mixnet/key_paths.rs delete mode 100644 sdk/rust/nym-sdk/src/mixnet/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6506f5d6da..053b8f1790 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1778,6 +1778,14 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "examples-common" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", +] + [[package]] name = "execute" version = "0.1.0" @@ -1970,9 +1978,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", @@ -1985,9 +1993,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -1995,15 +2003,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -2023,15 +2031,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-macro" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", @@ -2040,21 +2048,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -3579,9 +3587,24 @@ dependencies = [ name = "nym-sdk" version = "0.1.0" dependencies = [ + "client-connections", "client-core", + "crypto", + "examples-common", + "futures", + "gateway-client", + "gateway-requests", + "log", + "network-defaults", + "nymsphinx", + "pretty_env_logger", "rand 0.7.3", + "tap", + "task", + "thiserror", "tokio", + "toml", + "url", ] [[package]] @@ -5986,18 +6009,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -6217,9 +6240,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" dependencies = [ "serde", ] diff --git a/clients/client-core/src/client/base_client/mod.rs b/clients/client-core/src/client/base_client/mod.rs index 4757c3bf2c..196ae058d8 100644 --- a/clients/client-core/src/client/base_client/mod.rs +++ b/clients/client-core/src/client/base_client/mod.rs @@ -33,12 +33,15 @@ use log::{debug, info}; use nymsphinx::acknowledgements::AckKey; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; +use nymsphinx::receiver::ReconstructedMessage; use std::sync::Arc; use std::time::Duration; use tap::TapFallible; use task::{TaskClient, TaskManager}; use url::Url; +use super::received_buffer::ReceivedBufferMessage; + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub mod non_wasm_helpers; @@ -48,10 +51,30 @@ pub struct ClientInput { } pub struct ClientOutput { - pub shared_lane_queue_lengths: LaneQueueLengths, pub received_buffer_request_sender: ReceivedBufferRequestSender, } +impl ClientOutput { + pub fn register_receiver( + &mut self, + ) -> Result>, ClientCoreError> { + let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); + + self.received_buffer_request_sender + .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( + reconstructed_sender, + )) + .map_err(|_| ClientCoreError::FailedToRegisterReceiver)?; + + Ok(reconstructed_receiver) + } +} + +pub struct ClientState { + pub shared_lane_queue_lengths: LaneQueueLengths, + pub reply_controller_sender: ReplyControllerSender, +} + pub enum ClientInputStatus { AwaitingProducer { client_input: ClientInput }, Connected, @@ -475,11 +498,13 @@ where }, client_output: ClientOutputStatus::AwaitingConsumer { client_output: ClientOutput { - shared_lane_queue_lengths, received_buffer_request_sender, }, }, - reply_controller_sender, + client_state: ClientState { + shared_lane_queue_lengths, + reply_controller_sender, + }, task_manager, }) } @@ -488,9 +513,7 @@ where pub struct BaseClient { pub client_input: ClientInputStatus, pub client_output: ClientOutputStatus, - - // it feels very wrong to put this channel here, but I can't think of any other way of passing it to the native client - pub reply_controller_sender: ReplyControllerSender, + pub client_state: ClientState, pub task_manager: TaskManager, } diff --git a/clients/client-core/src/client/base_client/non_wasm_helpers.rs b/clients/client-core/src/client/base_client/non_wasm_helpers.rs index 996dcc8fc3..6ed6116e63 100644 --- a/clients/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/clients/client-core/src/client/base_client/non_wasm_helpers.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::replies::reply_storage::{ - fs_backend, CombinedReplyStorage, ReplyStorageBackend, + browser_backend, fs_backend, CombinedReplyStorage, ReplyStorageBackend, }; use crate::config::DebugConfig; use crate::error::ClientCoreError; @@ -85,3 +85,10 @@ pub async fn setup_fs_reply_surb_backend>( setup_fresh_backend(db_path, debug_config).await } } + +pub fn setup_empty_reply_surb_backend(debug_config: &DebugConfig) -> browser_backend::Backend { + browser_backend::Backend::new( + debug_config.minimum_reply_surb_storage_threshold, + debug_config.maximum_reply_surb_storage_threshold, + ) +} diff --git a/clients/client-core/src/client/key_manager.rs b/clients/client-core/src/client/key_manager.rs index 6fa264f43f..4b6647ecbc 100644 --- a/clients/client-core/src/client/key_manager.rs +++ b/clients/client-core/src/client/key_manager.rs @@ -17,6 +17,8 @@ use std::sync::Arc; // use the old key after new one was issued. // Remember that Arc has Deref implementation for T +// WIP(JON): let's try not to have the clone +#[derive(Clone)] pub struct KeyManager { /// identity key associated with the client instance. identity_keypair: Arc, @@ -57,6 +59,20 @@ impl KeyManager { } } + pub fn new_from_keys( + id_keypair: identity::KeyPair, + enc_keypair: encryption::KeyPair, + gateway_shared_key: SharedKeys, + ack_key: AckKey, + ) -> Self { + Self { + identity_keypair: Arc::new(id_keypair), + encryption_keypair: Arc::new(enc_keypair), + gateway_shared_key: Some(Arc::new(gateway_shared_key)), + ack_key: Arc::new(ack_key), + } + } + // this is actually **NOT** dead code // I have absolutely no idea why the compiler insists it's unused. The call happens during client::init::execute #[allow(dead_code)] @@ -65,8 +81,8 @@ impl KeyManager { self.gateway_shared_key = Some(gateway_shared_key) } - /// Loads previously stored keys from the disk. - pub fn load_keys(client_pathfinder: &ClientKeyPathfinder) -> io::Result { + /// Loads previously stored client keys from the disk. + fn load_keys_client_only(client_pathfinder: &ClientKeyPathfinder) -> io::Result { let identity_keypair: identity::KeyPair = pemstore::load_keypair(&pemstore::KeyPairPath::new( client_pathfinder.private_identity_key().to_owned(), @@ -78,21 +94,47 @@ impl KeyManager { client_pathfinder.public_encryption_key().to_owned(), ))?; - let gateway_shared_key: SharedKeys = - pemstore::load_key(client_pathfinder.gateway_shared_key())?; - let ack_key: AckKey = pemstore::load_key(client_pathfinder.ack_key())?; - // TODO: ack key is never stored so it is generated now. But perhaps it should be stored - // after all for consistency sake? Ok(KeyManager { identity_keypair: Arc::new(identity_keypair), encryption_keypair: Arc::new(encryption_keypair), - gateway_shared_key: Some(Arc::new(gateway_shared_key)), + gateway_shared_key: None, ack_key: Arc::new(ack_key), }) } + /// Loads previously stored keys from the disk. Fails if not all, including the shared gateway + /// key, is available. + pub fn load_keys(client_pathfinder: &ClientKeyPathfinder) -> io::Result { + let mut key_manager = Self::load_keys_client_only(client_pathfinder)?; + + let gateway_shared_key: SharedKeys = + pemstore::load_key(client_pathfinder.gateway_shared_key())?; + + key_manager.gateway_shared_key = Some(Arc::new(gateway_shared_key)); + + Ok(key_manager) + } + + pub fn load_keys_maybe_gateway(client_pathfinder: &ClientKeyPathfinder) -> io::Result { + let mut key_manager = Self::load_keys_client_only(client_pathfinder)?; + + let gateway_shared_key: Result = + pemstore::load_key(client_pathfinder.gateway_shared_key()); + + // It's ok if the gateway key was not found + let gateway_shared_key = match gateway_shared_key { + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err), + Ok(key) => Ok(Some(key)), + }?; + + key_manager.gateway_shared_key = gateway_shared_key.map(Arc::new); + + Ok(key_manager) + } + // this is actually **NOT** dead code // I have absolutely no idea why the compiler insists it's unused. The call happens during client::init::execute #[allow(dead_code)] @@ -119,7 +161,21 @@ impl KeyManager { pemstore::store_key(self.ack_key.as_ref(), client_pathfinder.ack_key())?; match self.gateway_shared_key.as_ref() { - None => warn!("No gateway shared key available to store!"), + None => debug!("No gateway shared key available to store!"), + Some(gate_key) => { + pemstore::store_key(gate_key.as_ref(), client_pathfinder.gateway_shared_key())? + } + } + + Ok(()) + } + + pub fn store_key_gateway_only( + &self, + client_pathfinder: &ClientKeyPathfinder, + ) -> io::Result<()> { + match self.gateway_shared_key.as_ref() { + None => panic!("WIP(JON)"), Some(gate_key) => { pemstore::store_key(gate_key.as_ref(), client_pathfinder.gateway_shared_key())? } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index c65592b2f6..faa250aeac 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -213,7 +213,11 @@ impl ActionController { } // note: when the entry expires it's automatically removed from pending_acks_timers - fn handle_expired_ack_timer(&mut self, expired_ack: Expired) { + fn handle_expired_ack_timer( + &mut self, + expired_ack: Expired, + task_client: &mut task::TaskClient, + ) { // I'm honestly not sure how to handle it, because getting it means other things in our // system are already misbehaving. If we ever see this panic, then I guess we should worry // about it. Perhaps just reschedule it at later point? @@ -231,9 +235,16 @@ impl ActionController { // downgrading an arc and then upgrading vs cloning is difference of 30ns vs 15ns // so it's literally a NO difference while it might prevent us from unnecessarily // resending data (in maybe 1 in 1 million cases, but it's something) - self.retransmission_sender + if self + .retransmission_sender .unbounded_send(Arc::downgrade(pending_ack_data)) - .unwrap() + .is_err() + { + assert!( + task_client.is_shutdown_poll(), + "Failed to send pending ack for retransmission" + ); + } } else { // this shouldn't cause any issues but shouldn't have happened to begin with! error!("An already removed pending ack has expired") @@ -264,7 +275,7 @@ impl ActionController { } }, expired_ack = self.pending_acks_timers.next() => match expired_ack { - Some(expired_ack) => self.handle_expired_ack_timer(expired_ack), + Some(expired_ack) => self.handle_expired_ack_timer(expired_ack, &mut shutdown), None => { log::trace!("ActionController: Stopping since ack channel closed"); break; diff --git a/clients/client-core/src/client/replies/reply_controller/requests.rs b/clients/client-core/src/client/replies/reply_controller/requests.rs index 1f47f2ca07..6b910ebde2 100644 --- a/clients/client-core/src/client/replies/reply_controller/requests.rs +++ b/clients/client-core/src/client/replies/reply_controller/requests.rs @@ -99,6 +99,24 @@ impl ReplyControllerSender { } } +pub struct ReplyQueueLengths { + reply_controller_sender: ReplyControllerSender, +} + +impl ReplyQueueLengths { + pub fn new(reply_controller_sender: ReplyControllerSender) -> Self { + Self { + reply_controller_sender, + } + } + + pub async fn get_lane_queue_length(&self, connection_id: ConnectionId) -> usize { + self.reply_controller_sender + .get_lane_queue_length(connection_id) + .await + } +} + pub(crate) type ReplyControllerReceiver = mpsc::UnboundedReceiver; #[derive(Debug)] diff --git a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs index 9711ea759f..ff2da4ab72 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs @@ -7,6 +7,7 @@ use async_trait::async_trait; // well, right now we don't have the browser storage : ( // so we keep everything in memory +#[derive(Debug)] pub struct Backend { empty: Empty, } diff --git a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs index d80231ae1e..0920b37657 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs @@ -6,10 +6,12 @@ use async_trait::async_trait; use std::error::Error; use thiserror::Error; -#[cfg(target_arch = "wasm32")] +//#[cfg(target_arch = "wasm32")] +//#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub mod browser_backend; #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +//#[cfg(target_arch = "wasm32")] pub mod fs_backend; // #[cfg(all(test, feature = "std"))] @@ -19,10 +21,11 @@ pub mod fs_backend; #[error("no information provided")] pub struct UndefinedError; +#[derive(Debug)] pub struct Empty { // we need to keep 'basic' metadata here to "load" the CombinedReplyStorage - min_surb_threshold: usize, - max_surb_threshold: usize, + pub min_surb_threshold: usize, + pub max_surb_threshold: usize, } #[async_trait] diff --git a/clients/client-core/src/config/persistence/key_pathfinder.rs b/clients/client-core/src/config/persistence/key_pathfinder.rs index d5dda75a27..90f00eb730 100644 --- a/clients/client-core/src/config/persistence/key_pathfinder.rs +++ b/clients/client-core/src/config/persistence/key_pathfinder.rs @@ -7,12 +7,12 @@ use std::path::{Path, PathBuf}; #[derive(Debug)] pub struct ClientKeyPathfinder { - identity_private_key: PathBuf, - identity_public_key: PathBuf, - encryption_private_key: PathBuf, - encryption_public_key: PathBuf, - gateway_shared_key: PathBuf, - ack_key: PathBuf, + pub identity_private_key: PathBuf, + pub identity_public_key: PathBuf, + pub encryption_private_key: PathBuf, + pub encryption_public_key: PathBuf, + pub gateway_shared_key: PathBuf, + pub ack_key: PathBuf, } impl ClientKeyPathfinder { @@ -22,13 +22,25 @@ impl ClientKeyPathfinder { ClientKeyPathfinder { identity_private_key: config_dir.join("private_identity.pem"), identity_public_key: config_dir.join("public_identity.pem"), - encryption_private_key: config_dir.join("public_encryption.pem"), - encryption_public_key: config_dir.join("private_encryption.pem"), + encryption_private_key: config_dir.join("private_encryption.pem"), + encryption_public_key: config_dir.join("public_encryption.pem"), gateway_shared_key: config_dir.join("gateway_shared.pem"), ack_key: config_dir.join("ack_key.pem"), } } + //pub fn new_from_dir(config_dir: &PathBuf) -> Self { + // dbg!(&config_dir); + // ClientKeyPathfinder { + // identity_private_key: config_dir.join("private_identity.pem"), + // identity_public_key: config_dir.join("public_identity.pem"), + // encryption_private_key: config_dir.join("public_encryption.pem"), + // encryption_public_key: config_dir.join("private_encryption.pem"), + // gateway_shared_key: config_dir.join("gateway_shared.pem"), + // ack_key: config_dir.join("ack_key.pem"), + // } + //} + pub fn new_from_config(config: &Config) -> Self { ClientKeyPathfinder { identity_private_key: config.get_private_identity_key_file(), @@ -40,6 +52,19 @@ impl ClientKeyPathfinder { } } + pub fn any_file_exists(&self) -> bool { + matches!(self.identity_public_key.try_exists(), Ok(true)) + || matches!(self.identity_private_key.try_exists(), Ok(true)) + || matches!(self.encryption_public_key.try_exists(), Ok(true)) + || matches!(self.encryption_private_key.try_exists(), Ok(true)) + || matches!(self.gateway_shared_key.try_exists(), Ok(true)) + || matches!(self.ack_key.try_exists(), Ok(true)) + } + + pub fn gateway_key_file_exists(&self) -> bool { + matches!(self.gateway_shared_key.try_exists(), Ok(true)) + } + pub fn private_identity_key(&self) -> &Path { &self.identity_private_key } diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs index 07bd9442ec..6162a5d9be 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -55,6 +55,9 @@ pub enum ClientCoreError { #[error("The address of the gateway is unknown - did you run init?")] GatwayAddressUnknown, + #[error("failed to register receiver for reconstructed mixnet messages")] + FailedToRegisterReceiver, + #[error("Unexpected exit")] UnexpectedExit, } diff --git a/clients/client-core/src/init/helpers.rs b/clients/client-core/src/init/helpers.rs index b3662149b7..2d078a4751 100644 --- a/clients/client-core/src/init/helpers.rs +++ b/clients/client-core/src/init/helpers.rs @@ -10,7 +10,7 @@ use config::NymConfig; use crypto::asymmetric::identity; use gateway_client::GatewayClient; use gateway_requests::registration::handshake::SharedKeys; -use rand::{rngs::OsRng, seq::SliceRandom, thread_rng}; +use rand::{seq::SliceRandom, thread_rng}; use std::{sync::Arc, time::Duration}; use tap::TapFallible; use topology::{filter::VersionFilterable, gateway}; @@ -51,7 +51,7 @@ pub(super) async fn query_gateway_details( } } -async fn register_with_gateway( +pub(super) async fn register_with_gateway( gateway: &gateway::Node, our_identity: Arc, ) -> Result, ClientCoreError> { @@ -74,20 +74,13 @@ async fn register_with_gateway( Ok(shared_keys) } -pub(super) async fn register_with_gateway_and_store_keys( - gateway_details: gateway::Node, +pub(super) fn store_keys( + key_manager: &KeyManager, config: &Config, ) -> Result<(), ClientCoreError> where T: NymConfig, { - let mut rng = OsRng; - let mut key_manager = KeyManager::new(&mut rng); - - let shared_keys = - register_with_gateway(&gateway_details, key_manager.identity_keypair()).await?; - key_manager.insert_gateway_shared_key(shared_keys); - let pathfinder = ClientKeyPathfinder::new_from_config(config); Ok(key_manager .store_keys(&pathfinder) diff --git a/clients/client-core/src/init/mod.rs b/clients/client-core/src/init/mod.rs index 4d012d366b..f02d228242 100644 --- a/clients/client-core/src/init/mod.rs +++ b/clients/client-core/src/init/mod.rs @@ -6,23 +6,26 @@ use std::fmt::Display; use nymsphinx::addressing::{clients::Recipient, nodes::NodeIdentity}; +use rand::rngs::OsRng; use serde::Serialize; use tap::TapFallible; use config::NymConfig; use crypto::asymmetric::{encryption, identity}; +use url::Url; +use crate::client::key_manager::KeyManager; use crate::{ config::{ persistence::key_pathfinder::ClientKeyPathfinder, ClientCoreConfigTrait, Config, GatewayEndpointConfig, }, error::ClientCoreError, - init::helpers::{query_gateway_details, register_with_gateway_and_store_keys}, }; mod helpers; +/// Struct describing the results of the client initialization procedure. #[derive(Debug, Serialize)] pub struct InitResults { version: String, @@ -60,6 +63,12 @@ impl Display for InitResults { } } +/// Create a new set of client keys. +pub fn new_client_keys() -> KeyManager { + let mut rng = OsRng; + KeyManager::new(&mut rng) +} + /// Convenience function for setting up the gateway for a client. Depending on the arguments given /// it will do the sensible thing. pub async fn setup_gateway( @@ -74,7 +83,7 @@ where { let id = config.get_id(); if register_gateway { - register_with_gateway(user_chosen_gateway_id, config).await + register_with_gateway_and_store(user_chosen_gateway_id, config).await } else if let Some(user_chosen_gateway_id) = user_chosen_gateway_id { config_gateway_with_existing_keys(user_chosen_gateway_id, config).await } else { @@ -82,27 +91,51 @@ where } } +/// Get the gateway details by querying the validator-api. Either pick one at random or use +/// the chosen one if it's among the available ones. +pub async fn register_with_gateway( + key_manager: &mut KeyManager, + nym_api_endpoints: Vec, + chosen_gateway_id: Option, +) -> Result { + // Our identity is derived from our key + let our_identity = key_manager.identity_keypair(); + + // Get the gateway details of the gateway we will use + let gateway = helpers::query_gateway_details(nym_api_endpoints, chosen_gateway_id).await?; + log::debug!("Querying gateway gives: {}", gateway); + + // Establish connection, authenticate and generate keys for talking with the gateway + let shared_keys = helpers::register_with_gateway(&gateway, our_identity).await?; + key_manager.insert_gateway_shared_key(shared_keys); + + Ok(gateway.into()) +} + /// Get the gateway details by querying the validator-api. Either pick one at random or use /// the chosen one if it's among the available ones. /// Saves keys to disk, specified by the paths in `config`. -pub async fn register_with_gateway( - user_chosen_gateway_id: Option, +pub async fn register_with_gateway_and_store( + chosen_gateway_id: Option, config: &Config, ) -> Result where T: NymConfig, { println!("Configuring gateway"); - let gateway = - query_gateway_details(config.get_nym_api_endpoints(), user_chosen_gateway_id).await?; - log::debug!("Querying gateway gives: {}", gateway); + let mut key_manager = new_client_keys(); - // Registering with gateway by setting up and writing shared keys to disk - log::trace!("Registering gateway"); - register_with_gateway_and_store_keys(gateway.clone(), config).await?; + let gateway = register_with_gateway( + &mut key_manager, + config.get_nym_api_endpoints(), + chosen_gateway_id, + ) + .await?; + + helpers::store_keys(&key_manager, config)?; println!("Saved all generated keys"); - Ok(gateway.into()) + Ok(gateway) } /// Set the gateway using the usual procedue of querying the validator-api, but don't register or @@ -117,8 +150,11 @@ where T: NymConfig, { println!("Using gateway provided by user, keeping existing keys"); - let gateway = - query_gateway_details(config.get_nym_api_endpoints(), Some(user_chosen_gateway_id)).await?; + let gateway = helpers::query_gateway_details( + config.get_nym_api_endpoints(), + Some(user_chosen_gateway_id), + ) + .await?; log::debug!("Querying gateway gives: {}", gateway); Ok(gateway.into()) } @@ -143,6 +179,20 @@ where }) } +/// Get the full client address from the client keys and the gateway identity +pub fn get_client_address( + key_manager: &KeyManager, + gateway_config: &GatewayEndpointConfig, +) -> Recipient { + Recipient::new( + *key_manager.identity_keypair().public_key(), + *key_manager.encryption_keypair().public_key(), + // TODO: below only works under assumption that gateway address == gateway id + // (which currently is true) + NodeIdentity::from_base58_string(&gateway_config.gateway_id).unwrap(), + ) +} + /// Get the client address by loading the keys from stored files. pub fn get_client_address_from_stored_keys( config: &Config, diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index ca367018e8..1ba6e2a70d 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -8,12 +8,11 @@ use crate::error::ClientError; use crate::websocket; use client_connections::TransmissionLane; use client_core::client::base_client::{ - non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, ClientState, }; use client_core::client::inbound_messages::InputMessage; use client_core::client::key_manager::KeyManager; use client_core::client::received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; -use client_core::client::replies::reply_controller::requests::ReplyControllerSender; use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder; use futures::channel::mpsc; use gateway_client::bandwidth::BandwidthController; @@ -87,8 +86,8 @@ impl SocketClient { config: &Config, client_input: ClientInput, client_output: ClientOutput, + client_state: ClientState, self_address: &Recipient, - reply_controller_sender: ReplyControllerSender, shutdown: task::TaskClient, ) { info!("Starting websocket listener..."); @@ -99,10 +98,14 @@ impl SocketClient { } = client_input; let ClientOutput { - shared_lane_queue_lengths, received_buffer_request_sender, } = client_output; + let ClientState { + shared_lane_queue_lengths, + reply_controller_sender, + } = client_state; + let websocket_handler = websocket::HandlerBuilder::new( input_sender, connection_command_sender, @@ -151,13 +154,14 @@ impl SocketClient { let mut started_client = base_builder.start_base().await?; let client_input = started_client.client_input.register_producer(); let client_output = started_client.client_output.register_consumer(); + let client_state = started_client.client_state; Self::start_websocket_listener( &self.config, client_input, client_output, + client_state, &self_address, - started_client.reply_controller_sender, started_client.task_manager.subscribe(), ); diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index c7710056f8..1f22fff717 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -9,7 +9,7 @@ use crate::socks::{ server::SphinxSocksServer, }; use client_core::client::base_client::{ - non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, ClientState, }; use client_core::client::key_manager::KeyManager; use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder; @@ -95,6 +95,7 @@ impl NymClient { config: &Config, client_input: ClientInput, client_output: ClientOutput, + client_status: ClientState, self_address: Recipient, shutdown: TaskClient, ) { @@ -108,10 +109,14 @@ impl NymClient { } = client_input; let ClientOutput { - shared_lane_queue_lengths, received_buffer_request_sender, } = client_output; + let ClientState { + shared_lane_queue_lengths, + reply_controller_sender: _, + } = client_status; + let authenticator = Authenticator::new(auth_methods, allowed_users); let mut sphinx_socks = SphinxSocksServer::new( config.get_listening_port(), @@ -218,11 +223,13 @@ impl NymClient { let mut started_client = base_builder.start_base().await?; let client_input = started_client.client_input.register_producer(); let client_output = started_client.client_output.register_consumer(); + let client_state = started_client.client_state; Self::start_socks5_listener( &self.config, client_input, client_output, + client_state, self_address, started_client.task_manager.subscribe(), ); diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index f40a525527..7c8ab6586e 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -208,7 +208,7 @@ pub struct TaskClient { impl TaskClient { #[cfg(not(target_arch = "wasm32"))] - const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); + const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); fn new( notify: watch::Receiver<()>, @@ -300,8 +300,8 @@ impl TaskClient { has_changed } Err(err) => { - log::debug!("Polling shutdown failed: {err}"); - log::debug!("Assuming this means we should shutdown..."); + log::error!("Polling shutdown failed: {err}"); + log::error!("Assuming this means we should shutdown..."); true } } diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index 562b127604..314bf9e711 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -6,8 +6,24 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -client-core = { path = "../../../clients/client-core" } +client-connections = { path = "../../../common/client-connections" } +client-core = { path = "../../../clients/client-core", features = ["fs-surb-storage"]} +crypto = { path = "../../../common/crypto" } +gateway-client = { path = "../../../common/client-libs/gateway-client" } +gateway-requests = { path = "../../../gateway/gateway-requests" } +network-defaults = { path = "../../../common/network-defaults" } +nymsphinx = { path = "../../../common/nymsphinx" } +task = { path = "../../../common/task" } + +futures = "0.3" +log = "0.4" rand = { version = "0.7.3" } +tap = "1.0.1" +thiserror = "1.0.38" +url = "2.2" +toml = "0.5.10" [dev-dependencies] -tokio = { version = "1", features = ["full"] } \ No newline at end of file +pretty_env_logger = "0.4.0" +tokio = { version = "1", features = ["full"] } +examples-common = { path = "examples-common" } diff --git a/sdk/rust/nym-sdk/examples-common/Cargo.toml b/sdk/rust/nym-sdk/examples-common/Cargo.toml new file mode 100644 index 0000000000..338b766a8f --- /dev/null +++ b/sdk/rust/nym-sdk/examples-common/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "examples-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.17" +pretty_env_logger = "0.4.0" diff --git a/sdk/rust/nym-sdk/examples-common/src/lib.rs b/sdk/rust/nym-sdk/examples-common/src/lib.rs new file mode 100644 index 0000000000..7aa0420987 --- /dev/null +++ b/sdk/rust/nym-sdk/examples-common/src/lib.rs @@ -0,0 +1,21 @@ +pub fn setup_logging() { + let mut log_builder = pretty_env_logger::formatted_timed_builder(); + if let Ok(s) = ::std::env::var("RUST_LOG") { + log_builder.parse_filters(&s); + } else { + // default to 'Info' + log_builder.filter(None, log::LevelFilter::Info); + } + + log_builder + .filter_module("hyper", log::LevelFilter::Warn) + .filter_module("tokio_reactor", log::LevelFilter::Warn) + .filter_module("reqwest", log::LevelFilter::Warn) + .filter_module("mio", log::LevelFilter::Warn) + .filter_module("want", log::LevelFilter::Warn) + .filter_module("tungstenite", log::LevelFilter::Warn) + .filter_module("tokio_tungstenite", log::LevelFilter::Warn) + .filter_module("handlebars", log::LevelFilter::Warn) + .filter_module("sled", log::LevelFilter::Warn) + .init(); +} diff --git a/sdk/rust/nym-sdk/examples/complex_config.rs b/sdk/rust/nym-sdk/examples/complex_config.rs index c34038162e..757c95c8a0 100644 --- a/sdk/rust/nym-sdk/examples/complex_config.rs +++ b/sdk/rust/nym-sdk/examples/complex_config.rs @@ -1,24 +1,40 @@ use std::path::PathBuf; +use examples_common::setup_logging; use nym_sdk::mixnet; #[tokio::main] async fn main() { - // specify some config options - let keys = Some(PathBuf::from("~/.nym/clients/superfoomp")); - let config = mixnet::Config { keys }; + setup_logging(); - let client = mixnet::Client::new(Some(config)); // passing a config allows the user to set values + // Specify some config options + let config_dir = PathBuf::from("/tmp/mixnet-client"); - // let show_receive = move || println!("got a message from the mixnet: {}", message); // might need to bury this in a struct as a `FnOnce`, see https://stackoverflow.com/questions/41081240/idiomatic-callbacks-in-rust - // client.on_receive(show_receive); // have some way to pipe any received info to a function for processing + // Setting `KeyMode::Keep` will use existing keys, and existing config, if there is one. + // Regardles of `user_chosen_gateway`. + let keys = mixnet::KeyPaths::new_from_dir(mixnet::KeyMode::Keep, &config_dir); - // connect to the mixnet, now we're listening for incoming - client.connect_to_mixnet(); + // Provide key paths for the client to read/write keys to. + let mut client = mixnet::Client::new(None, Some(keys)).unwrap(); - // be able to get our client address - println!("Our client address is {}", client.nym_address); + // Connect to the mixnet, now we're listening for incoming + client.connect_to_mixnet().await; - // send important info up the pipe to a buddy - client.send_str("foo.bar@blah", "flappappa"); + // Be able to get our client address + let our_address = client.nym_address().unwrap(); + println!("Our client nym address is: {our_address}"); + + // Send a message throught the mixnet to ourselves + client + .send_str(&our_address.to_string(), "hello there") + .await; + + println!("Waiting for message"); + if let Some(received) = client.wait_for_messages().await { + for r in received { + println!("Received: {}", String::from_utf8_lossy(&r.message)); + } + } + + client.disconnect().await; } diff --git a/sdk/rust/nym-sdk/examples/complex_config2.rs b/sdk/rust/nym-sdk/examples/complex_config2.rs new file mode 100644 index 0000000000..413722769a --- /dev/null +++ b/sdk/rust/nym-sdk/examples/complex_config2.rs @@ -0,0 +1,45 @@ +use examples_common::setup_logging; +use nym_sdk::mixnet; + +#[tokio::main] +async fn main() { + setup_logging(); + + // We can set a few options + let user_chosen_gateway_id = None; + let nym_api_endpointgs = vec![]; + let config = mixnet::Config::new(user_chosen_gateway_id, nym_api_endpointgs); + + let mut client = mixnet::Client::new(Some(config), None).unwrap(); + + // In this we want to provide our own gateway config struct, and handle persisting this info to disk + // ourselves (e.g., as part of our own configuration file). + // NOTE: gateway shared key is written to disk according to the path given earlier + // Checks if we have a shared gateway key loaded + let first_run = true; + if first_run { + client.register_with_gateway().await; + write_to_storage(client.get_keys(), client.get_gateway_endpoint().unwrap()); + } else { + let (keys, gateway_config) = read_from_storage(); + client.set_keys(&keys); + client.set_gateway_endpoint(&gateway_config); + } + + // Connect to the mixnet, now we're listening for incoming + client.connect_to_mixnet().await; + + // Be able to get our client address + println!("Our client address is {}", client.nym_address().unwrap()); + + // Send important info up the pipe to a buddy + client.send_str("foo.bar@blah", "flappappa").await; +} + +fn write_to_storage(_keys: &mixnet::Keys, _gateway_config: &mixnet::GatewayEndpointConfig) { + todo!(); +} + +fn read_from_storage() -> (mixnet::Keys, mixnet::GatewayEndpointConfig) { + todo!(); +} diff --git a/sdk/rust/nym-sdk/examples/simple.rs b/sdk/rust/nym-sdk/examples/simple.rs index 2ff8dae9c8..f55bcc9095 100644 --- a/sdk/rust/nym-sdk/examples/simple.rs +++ b/sdk/rust/nym-sdk/examples/simple.rs @@ -1,22 +1,27 @@ +use examples_common::setup_logging; use nym_sdk::mixnet; #[tokio::main] async fn main() { - // here's what I'd actually like to write - let client = mixnet::Client::new(None); // passing no config makes the client fire up an ephemeral session and figure shit out on its own + setup_logging(); - // let show_receive = move || println!("got a message from the mixnet: {}", message); // might need to bury this in a struct as a `FnMut`, see https://stackoverflow.com/questions/41081240/idiomatic-callbacks-in-rust - // connect to the mixnet, now we're listening for incoming - client.connect_to_mixnet(); + // Passing no config makes the client fire up an ephemeral session and figure shit out on its own + let mut client = mixnet::Client::new(None, None).unwrap(); - // be able to get our client address - println!("Our client address is {}", client.nym_address); + // Connect to the mixnet, now we're listening for incoming + client.connect_to_mixnet().await; - // send important string info up the pipe to a buddy - client.send_str("foo.bar@blah", "flappappa"); + // Be able to get our client address + let our_address = client.nym_address().unwrap(); + println!("Our client nym address is: {our_address}"); - // send some bytes to a buddy - client.send_bytes("foo.bar@blah", "flappappa".as_bytes().to_vec()); + // Send a message throught the mixnet to ourselves + client + .send_str(&our_address.to_string(), "hello there") + .await; - + println!("Waiting for message"); + client + .on_messages(|msg| println!("Received: {}", String::from_utf8_lossy(&msg.message))) + .await; } diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs new file mode 100644 index 0000000000..5327aa142c --- /dev/null +++ b/sdk/rust/nym-sdk/src/error.rs @@ -0,0 +1,10 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("i/o error: {0}")] + IoError(#[from] std::io::Error), + + #[error("key file encountered that we don't want to overwrite")] + DontOverwrite, +} + +pub type Result = std::result::Result; diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index ae7f01c0eb..de6ec53412 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -1 +1,2 @@ +pub mod error; pub mod mixnet; diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs new file mode 100644 index 0000000000..fc190053c9 --- /dev/null +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -0,0 +1,15 @@ +mod client; +mod config; +mod connection_state; +mod key_paths; + +pub use client_core::config::GatewayEndpointConfig; +pub use nymsphinx::{ + addressing::clients::{ClientIdentity, Recipient}, + receiver::ReconstructedMessage, +}; + +pub use key_paths::{GatewayKeyMode, KeyMode, KeyPaths, Keys}; + +pub use client::Client; +pub use config::Config; diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs new file mode 100644 index 0000000000..92ad59b262 --- /dev/null +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -0,0 +1,306 @@ +use futures::StreamExt; +use nymsphinx::{ + addressing::clients::{ClientIdentity, Recipient}, + receiver::ReconstructedMessage, +}; +use tap::TapOptional; + +use client_connections::TransmissionLane; +use client_core::{ + client::{ + base_client::{non_wasm_helpers, BaseClientBuilder}, + inbound_messages::InputMessage, + key_manager::KeyManager, + }, + config::{persistence::key_pathfinder::ClientKeyPathfinder, GatewayEndpointConfig}, +}; + +use crate::error::{Error, Result}; + +use super::{connection_state::ConnectionState, Config, GatewayKeyMode, KeyPaths, Keys}; + +pub struct Client { + /// Keys handled by the client + key_manager: KeyManager, + + /// Client configuration + config: Config, + + /// Paths for client keys, including identity, encryption, ack and shared gateway keys. + key_paths: Option, + + /// The client can be in one of multiple states, depending on how it is created and if it's + /// connected to the mixnet. + connection_state: ConnectionState, +} + +impl Client { + /// Create a new mixnet client. If no config options are supplied, creates a new client with + /// ephemeral keys stored in RAM, which will be discarded at application close. + /// + /// Callers have the option of supplying futher parameters to store persistent identities at a + /// location on-disk, if desired. + pub fn new(config_option: Option, key_paths: Option) -> Result { + let config = config_option.unwrap_or_default(); + + // If we are provided paths to keys, use them if they are available. And if they are + // not, write the generated keys back to storage. + let key_manager = if let Some(ref key_paths) = key_paths { + let path_finder = ClientKeyPathfinder::from(key_paths.clone()); + + // Try load keys + match KeyManager::load_keys_maybe_gateway(&path_finder) { + Ok(key_manager) => key_manager, + Err(err) => { + log::debug!("Not loading keys: {err}"); + if path_finder.any_file_exists() && key_paths.operating_mode.is_keep() { + return Err(Error::DontOverwrite); + } + + // Create new keys and write to storage + let key_manager = client_core::init::new_client_keys(); + // WARN: this will overwrite! + key_manager.store_keys(&path_finder)?; + key_manager + } + } + } else { + // Ephemeral keys that we only store in memory + client_core::init::new_client_keys() + }; + + Ok(Client { + key_manager, + config, + key_paths, + connection_state: ConnectionState::New, + }) + } + + /// Get the client identity, which is the public key of the identity key pair. + pub fn identity(&self) -> ClientIdentity { + *self.key_manager.identity_keypair().public_key() + } + + /// Get the nym address for this client, if it is available. The nym address is composed of the + /// client identity, the client encryption key, and the gateway identity. + pub fn nym_address(&self) -> Option<&Recipient> { + self.connection_state.nym_address() + } + + /// 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. + pub fn has_gateway_key(&self) -> bool { + self.key_manager.gateway_key_set() + } + + pub fn set_keys(&mut self, _keys: &Keys) { + todo!(); + } + + pub fn get_keys(&self) -> &Keys { + todo!(); + } + + pub fn set_gateway_endpoint(&mut self, _gateway_endpoint_config: &GatewayEndpointConfig) { + todo!(); + } + + pub fn get_gateway_endpoint(&self) -> Option<&GatewayEndpointConfig> { + self.connection_state.gateway_endpoint_config() + } + + pub async fn register_with_gateway(&mut self) { + assert!( + matches!(self.connection_state, ConnectionState::New), + "can only setup gateway when in `New` connection state" + ); + + let gateway_config = client_core::init::register_with_gateway( + &mut self.key_manager, + self.config.nym_api_endpoints.clone(), + self.config.user_chosen_gateway.clone(), + ) + .await + .expect("WIP"); + + let nym_address = client_core::init::get_client_address(&self.key_manager, &gateway_config); + + self.connection_state = ConnectionState::Registered { + gateway_endpoint_config: gateway_config, + nym_address, + }; + } + + fn write_gateway_key(&self, key_mode: &GatewayKeyMode) { + let key_paths = self.key_paths.as_ref().unwrap().clone(); + + let path_finder = ClientKeyPathfinder::from(key_paths); + if path_finder.gateway_key_file_exists() && key_mode.is_keep() { + todo!("WIP"); + }; + self.key_manager + .store_key_gateway_only(&path_finder) + .expect("WIP"); + } + + fn write_gateway_endpoint_config(&self) { + let key_paths = self.key_paths.as_ref().unwrap().clone(); + let gateway_endpoint_config_path = key_paths.gateway_endpoint_config; + let gateway_endpoint_config = + toml::to_string(self.get_gateway_endpoint().unwrap()).unwrap(); + + // Ensure the whole directory structure exists + if let Some(parent_dir) = gateway_endpoint_config_path.parent() { + std::fs::create_dir_all(parent_dir).expect("WIP"); + } + std::fs::write(gateway_endpoint_config_path, gateway_endpoint_config).expect("WIP"); + } + + fn read_gateway_endpoint_config(&mut self) { + let key_paths = self.key_paths.as_ref().unwrap().clone(); + let gateway_endpoint_config: GatewayEndpointConfig = toml::from_str( + &std::fs::read_to_string(key_paths.gateway_endpoint_config).expect("WIP"), + ) + .expect("WIP"); + + let nym_address = + client_core::init::get_client_address(&self.key_manager, &gateway_endpoint_config); + + self.connection_state = ConnectionState::Registered { + gateway_endpoint_config, + nym_address, + }; + } + + /// Connects to the mixnet via the gateway in the client config + pub async fn connect_to_mixnet(&mut self) { + // For some simple cases we can figure how to setup gateway without it having to have been + // called in advance. + if matches!(self.connection_state, ConnectionState::New) { + if self.key_paths.is_some() { + if self.has_gateway_key() { + // If we have a gateway key from client, then we can just read the corresponding + // config + println!("Has gateway key: loading"); + self.read_gateway_endpoint_config(); + } else { + // If we didn't find any shared gateway key during creation, that means we first + // need to register a gateway + println!("NO gateway key: registering new"); + self.register_with_gateway().await; + self.write_gateway_key(&GatewayKeyMode::Overwrite); + self.write_gateway_endpoint_config(); + } + } else { + // If we don't have any key paths, just use ephemeral keys + self.register_with_gateway().await; + } + } + + // At this point we should be in a registered state, either at function entry or by the + // above convenience logic. + assert!(matches!( + self.connection_state, + ConnectionState::Registered { .. } + )); + + let bandwidth_controller = None; + + let reply_storage_backend = + non_wasm_helpers::setup_empty_reply_surb_backend(&self.config.debug_config); + + let disabled_credentials = true; + + let gateway_config = self.connection_state.gateway_endpoint_config().unwrap(); + + let base_builder = BaseClientBuilder::new( + gateway_config, + &self.config.debug_config, + self.key_manager.clone(), + bandwidth_controller, + reply_storage_backend, + disabled_credentials, + self.config.nym_api_endpoints.clone(), + ); + + let nym_address = base_builder.as_mix_recipient(); + + let mut started_client = base_builder.start_base().await.unwrap(); + let client_input = started_client.client_input.register_producer(); + let mut client_output = started_client.client_output.register_consumer(); + let client_state = started_client.client_state; + + // Register our receiver + let reconstructed_receiver = client_output.register_receiver().unwrap(); + + self.connection_state = ConnectionState::Connected { + nym_address, + client_input, + client_output, + client_state, + reconstructed_receiver, + task_manager: started_client.task_manager, + }; + } + + /// Sends stringy data to the supplied Nym address + pub async fn send_str(&self, address: &str, message: &str) { + log::debug!("send_str"); + let message_bytes = message.to_string().into_bytes(); + self.send_bytes(address, message_bytes).await; + } + + /// Sends bytes to the supplied Nym address + pub async fn send_bytes(&self, address: &str, message: Vec) { + log::debug!("send_bytes"); + let Some(client_input) = self.connection_state.client_input() else { + log::error!("Error: trying to send without being connected"); + return; + }; + + let lane = TransmissionLane::General; + let recipient = Recipient::try_from_base58_string(address).unwrap(); + let input_msg = InputMessage::new_regular(recipient, message, lane); + client_input.input_sender.send(input_msg).await.unwrap(); + } + + /// Wait for messages from the mixnet + pub async fn wait_for_messages(&mut self) -> Option> { + let receiver = self + .connection_state + .reconstructed_receiver() + .tap_none(|| log::error!("Error: trying to wait without being connected"))?; + + receiver.next().await + } + + pub fn wait_for_messages_split(&mut self) -> Option { + todo!(); + } + + pub async fn on_messages(&mut self, fun: F) + where + F: Fn(ReconstructedMessage), + { + while let Some(msgs) = self.wait_for_messages().await { + for msg in msgs { + fun(msg) + } + } + } + + /// Disconnect from the mixnet. Currently it is not supported to reconnect a disconnected + /// client. + pub async fn disconnect(&mut self) { + let Some(task_manager) = self.connection_state.task_manager() else { + log::error!("Trying to disconnect when not connected!"); + return; + }; + + task_manager.signal_shutdown().ok(); + task_manager.wait_for_shutdown().await; + self.connection_state = ConnectionState::Disconnected; + } +} diff --git a/sdk/rust/nym-sdk/src/mixnet/config.rs b/sdk/rust/nym-sdk/src/mixnet/config.rs new file mode 100644 index 0000000000..92abf0560d --- /dev/null +++ b/sdk/rust/nym-sdk/src/mixnet/config.rs @@ -0,0 +1,36 @@ +use client_core::config::DebugConfig; +use network_defaults::mainnet; +use url::Url; + +pub struct Config { + /// If the user has explicitly specified a gateway. + pub user_chosen_gateway: Option, + + /// List of nym-api endpoints + pub nym_api_endpoints: Vec, + + /// Flags controlling all sorts of internal client behaviour. + /// Changing these risk compromising network anonymity! + pub debug_config: DebugConfig, +} + +impl Default for Config { + fn default() -> Self { + let nym_api_endpoints = vec![mainnet::API_VALIDATOR.to_string().parse().unwrap()]; + Self { + user_chosen_gateway: Default::default(), + nym_api_endpoints, + debug_config: Default::default(), + } + } +} + +impl Config { + pub fn new(user_chosen_gateway: Option, nym_api_endpoints: Vec) -> Self { + Self { + user_chosen_gateway, + nym_api_endpoints, + debug_config: DebugConfig::default(), + } + } +} diff --git a/sdk/rust/nym-sdk/src/mixnet/connection_state.rs b/sdk/rust/nym-sdk/src/mixnet/connection_state.rs new file mode 100644 index 0000000000..5336143dc5 --- /dev/null +++ b/sdk/rust/nym-sdk/src/mixnet/connection_state.rs @@ -0,0 +1,115 @@ +use client_core::{ + client::{ + base_client::{ClientInput, ClientOutput, ClientState}, + received_buffer::ReconstructedMessagesReceiver, + }, + config::GatewayEndpointConfig, +}; +use nymsphinx::addressing::clients::Recipient; +use task::TaskManager; + +/// States that the client connection can be in. Currently the states can only progress linearly +/// from New -> Registered -> Connected -> Disconnected. In the future it could be useful to be able +/// to re-connect a disconnected client. +// WIP(JON): consider adding inner types +#[allow(clippy::large_enum_variant)] +pub(super) enum ConnectionState { + New, + Registered { + nym_address: Recipient, + gateway_endpoint_config: GatewayEndpointConfig, + }, + Connected { + nym_address: Recipient, + client_input: ClientInput, + #[allow(dead_code)] + client_output: ClientOutput, + #[allow(dead_code)] + client_state: ClientState, + reconstructed_receiver: ReconstructedMessagesReceiver, + task_manager: TaskManager, + }, + Disconnected, +} + +impl std::fmt::Debug for ConnectionState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::New => write!(f, "New"), + Self::Registered { + nym_address, + gateway_endpoint_config: gateway_config, + } => f + .debug_struct("Registered") + .field("nym_address", nym_address) + .field("gateway_config", gateway_config) + .finish(), + Self::Connected { + nym_address, + client_input: _, + client_output: _, + client_state: _, + reconstructed_receiver, + task_manager, + } => f + .debug_struct("Connected") + .field("nym_address", nym_address) + .field("reconstructed_receiver", reconstructed_receiver) + .field("task_manager", task_manager) + .finish(), + Self::Disconnected => write!(f, "Disconnected"), + } + } +} + +impl ConnectionState { + pub(super) fn client_input(&self) -> Option<&ClientInput> { + match self { + ConnectionState::New + | ConnectionState::Registered { .. } + | ConnectionState::Disconnected => None, + ConnectionState::Connected { client_input, .. } => Some(client_input), + } + } + + pub(super) fn reconstructed_receiver(&mut self) -> Option<&mut ReconstructedMessagesReceiver> { + match self { + ConnectionState::New + | ConnectionState::Registered { .. } + | ConnectionState::Disconnected => None, + ConnectionState::Connected { + reconstructed_receiver, + .. + } => Some(reconstructed_receiver), + } + } + + pub(super) fn gateway_endpoint_config(&self) -> Option<&GatewayEndpointConfig> { + match self { + ConnectionState::New + | ConnectionState::Connected { .. } + | ConnectionState::Disconnected => None, + ConnectionState::Registered { + gateway_endpoint_config: gateway_config, + .. + } => Some(gateway_config), + } + } + + pub(super) fn nym_address(&self) -> Option<&Recipient> { + match self { + ConnectionState::New | ConnectionState::Disconnected => None, + ConnectionState::Registered { nym_address, .. } + | ConnectionState::Connected { nym_address, .. } => Some(nym_address), + } + } + + pub(super) fn task_manager(&mut self) -> Option<&mut TaskManager> { + match self { + ConnectionState::New + | ConnectionState::Registered { .. } + | ConnectionState::Disconnected => None, + ConnectionState::Connected { task_manager, .. } => Some(task_manager), + } + } +} diff --git a/sdk/rust/nym-sdk/src/mixnet/key_paths.rs b/sdk/rust/nym-sdk/src/mixnet/key_paths.rs new file mode 100644 index 0000000000..630e562b45 --- /dev/null +++ b/sdk/rust/nym-sdk/src/mixnet/key_paths.rs @@ -0,0 +1,142 @@ +use client_core::{ + client::key_manager::KeyManager, config::persistence::key_pathfinder::ClientKeyPathfinder, +}; +use crypto::asymmetric::{encryption, identity}; +use gateway_requests::registration::handshake::SharedKeys; +use nymsphinx::acknowledgements::AckKey; + +use std::path::{Path, PathBuf}; + +#[derive(Clone, Debug)] +pub enum KeyMode { + /// Use existing key files if they exists, otherwise create new ones. + Keep, + /// Create new keys, overwriting any potential previously existing keys. + Overwrite, +} + +impl KeyMode { + pub(crate) fn is_keep(&self) -> bool { + matches!(self, KeyMode::Keep) + } +} + +pub enum GatewayKeyMode { + /// Keep shared gateway key if found, otherwise create a new one. + Keep, + /// Create a new shared key and overwrite any potential existing one. + Overwrite, +} + +impl GatewayKeyMode { + pub(crate) fn is_keep(&self) -> bool { + matches!(self, GatewayKeyMode::Keep) + } +} + +#[derive(Clone, Debug)] +pub struct KeyPaths { + // Determines how to handle existing key files found. + pub operating_mode: KeyMode, + + // Client identity keys + pub private_identity: PathBuf, + pub public_identity: PathBuf, + + // Client encryption keys + pub private_encryption: PathBuf, + pub public_encryption: PathBuf, + + // Key for handling acks + pub ack_key: PathBuf, + + // Key setup after authenticating with a gateway + pub gateway_shared_key: PathBuf, + + // The key isn't much use without knowing which entity it refers to. + // This is an `Option` in case the end user might want to read write keys to storage, but + // handle the gateway configuration manually through `set_gateway_endpoint`. + // WIP(JON): make it an option? + pub gateway_endpoint_config: PathBuf, + + pub credential_database_path: PathBuf, + + pub reply_surb_database_path: PathBuf, +} + +pub struct Keys { + pub identity_keypair: identity::KeyPair, + pub encryption_keypair: encryption::KeyPair, + pub ack_key: AckKey, + pub gateway_shared_key: SharedKeys, +} + +//#[derive(Clone, Debug)] +//pub struct GatewaySetup { +// pub gateway_shared_key: PathBuf, +// pub gateway_endpoint_config: GatewayEndpointConfig, +// //pub gateway_endpoint_config: GatewayConfig, +//} + +//#[derive(Clone, Debug)] +//pub enum GatewayConfig { +// File(PathBuf), +// Struct(GatewayEndpointConfig), +//} + +impl KeyPaths { + pub fn new_from_dir(operating_mode: KeyMode, dir: &Path) -> Self { + assert!(!dir.is_file(), "WIP"); + Self { + // These filenames were chosen to match the ones we use in `nym-client`. Consider + // changing the defaults + operating_mode, + private_identity: dir.join("private_identity.pem"), + public_identity: dir.join("public_identity.pem"), + 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"), + gateway_endpoint_config: dir.join("gateway_endpoint_config.toml"), + credential_database_path: dir.join("db.sqlite"), + reply_surb_database_path: dir.join("persistent_reply_store.sqlite"), + } + } +} + +impl From for ClientKeyPathfinder { + fn from(paths: KeyPaths) -> Self { + Self { + identity_private_key: paths.private_identity, + identity_public_key: paths.public_identity, + encryption_private_key: paths.private_encryption, + encryption_public_key: paths.public_encryption, + gateway_shared_key: paths.gateway_shared_key, + ack_key: paths.ack_key, + } + } +} + +impl From for KeyManager { + fn from(keys: Keys) -> Self { + KeyManager::new_from_keys( + keys.identity_keypair, + keys.encryption_keypair, + keys.gateway_shared_key, + keys.ack_key, + ) + } +} + +//pub struct GatewayConfigPath { +// pub path: PathBuf, +//} +// +//impl GatewayConfigPath { +// pub fn new_from_dir(dir: PathBuf) -> Self { +// assert!(!dir.is_file(), "WIP"); +// Self { +// path: dir.join("gateway_endpoint_config.toml"), +// } +// } +//} diff --git a/sdk/rust/nym-sdk/src/mixnet/mod.rs b/sdk/rust/nym-sdk/src/mixnet/mod.rs deleted file mode 100644 index 46ea1aadde..0000000000 --- a/sdk/rust/nym-sdk/src/mixnet/mod.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::path::PathBuf; - -pub struct Client { - pub nym_address: String, - config: Config, -} - -impl Client { - /// Create a new mixnet client. If no config options are supplied, creates a new client with ephemeral keys - /// stored in RAM, which will be discarded at application close. - /// - /// Callers have the option of supplying futher parameters to store persistent identities at a location on-disk, - /// if desired. - pub fn new(config_option: Option) -> Client { - if config_option.is_none() { - let config = Self::some_minimal_config(); - let nym_address = Self::new_nym_address(); - Client { - config, - nym_address, - } - } else { - let self_address = Self::new_nym_address(); - let nym_address = Self::new_nym_address(); - - Client { - config: config_option.unwrap(), - nym_address, - } - } - } - - /// Connects to the mixnet via the gateway in the client config - pub fn connect_to_mixnet(&self) {} - - /// Sets the callback function which is run when the client receives a message - /// from the mixnet - pub fn on_receive(&mut self, message: &str) { - println!("Message received: {}", message); - } - - /// Sends stringy data to the supplied Nym address - pub fn send_str(&self, address: &str, message: &str) {} - - /// Sends bytes to the supplied Nym address - pub fn send_bytes(&self, address: &str, message: Vec) {} - - fn new_nym_address() -> String { - "the.nym@address".to_string() - } - - fn some_minimal_config() -> Config { - let mut keys_path = PathBuf::new(); - Config { - keys: Some(keys_path), - } - } -} - -pub struct Config { - pub keys: Option, -}