From e849e45b126b23d214a93ac0b6dac0a665bcd543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 30 Jun 2020 12:01:45 +0100 Subject: [PATCH] Feature/gateway shared key generation (#272) * Changed identity keypair to use ed25519 * Encryption key is now x25519 based + compatibiltiy with sphinx * Pathing and import fixes * Moved all asymmetric keys to sub-module in crypto * Extracted aes to separate module * kdf module in crypto * Ability to perform diffie hellman on encryption keys * ecdsa on identity keys * Extremely rough and incomplete registration handshake * Authentication primitives * Creating new random authenticationIV * Wrapper type for the derived shared key * Removed AuthToken in favour of using SharedKey for authentication * Gateway identity keys * Registration handshake without error mapping * Gateway address in client config * Added extra key for gateway presence * Updated pemstore to work on borrows instead * Gateway client trying to perform the handshake * Gateway changes to allow for handshake and shared key * Debug trait on sharedkey * native client using updated gateway client * Slightly updated gateway API * Minor cleanup * Fixed pemstore to correctly save multiple keypairs * Gateway actually deriving shared key during handshake * Gateway sending correct mid-handshake message * Missing quotation mark in client config template * Fixed template for correct shared key serialization * Fixed gateway authentication * Fixed tests * Using correct gateway key when converting to sphinx node * "get_all_clients" takes them from gateways as opposed to providers now * cargo fmt * Renamed pemstore methods * Unused import * Encryption of forward requests between client and gateway * Updated sphinx dependency to use public revision * Sending 'error' on handshake processing error * Removed some dead code --- Cargo.lock | 144 +++++++-- clients/native/Cargo.toml | 1 - clients/native/src/client/mod.rs | 87 +++--- clients/native/src/client/received_buffer.rs | 1 - clients/native/src/client/topology_control.rs | 28 +- clients/native/src/commands/init.rs | 99 ++++-- clients/native/src/commands/run.rs | 8 +- clients/native/src/config/mod.rs | 27 +- clients/native/src/config/template.rs | 7 +- clients/webassembly/src/lib.rs | 34 +-- clients/webassembly/src/models/keys.rs | 4 +- .../models/src/presence/gateways.rs | 9 +- .../src/requests/presence_gateways_post.rs | 3 +- .../src/requests/presence_topology_get.rs | 9 +- common/client-libs/gateway-client/Cargo.toml | 1 + .../client-libs/gateway-client/src/error.rs | 20 +- common/client-libs/gateway-client/src/lib.rs | 208 +++++++------ common/crypto/Cargo.toml | 17 +- .../crypto/src/asymmetric/encryption/mod.rs | 273 +++++++++++++++++ common/crypto/src/asymmetric/identity/mod.rs | 186 ++++++++++++ common/crypto/src/asymmetric/mod.rs | 16 + common/crypto/src/encryption/mod.rs | 153 ---------- common/crypto/src/identity/mod.rs | 160 ---------- common/crypto/src/kdf/blake3_hkdf.rs | 39 +++ common/crypto/src/kdf/mod.rs | 15 + common/crypto/src/lib.rs | 10 +- common/crypto/src/symmetric/aes_ctr.rs | 138 +++++++++ common/crypto/src/symmetric/mod.rs | 15 + common/nymsphinx/acknowledgements/Cargo.toml | 7 +- .../acknowledgements/src/identifier.rs | 56 ++-- common/nymsphinx/types/Cargo.toml | 2 +- common/nymsphinx/types/src/lib.rs | 2 +- common/pemstore/src/pemstore.rs | 112 ++++--- common/topology/src/gateway.rs | 12 +- common/topology/src/lib.rs | 4 +- common/topology/src/mix.rs | 2 +- common/topology/src/provider.rs | 2 +- gateway/Cargo.toml | 2 - gateway/gateway-requests/Cargo.toml | 8 +- gateway/gateway-requests/src/auth_token.rs | 125 -------- .../src/authentication/encrypted_address.rs | 91 ++++++ .../gateway-requests/src/authentication/iv.rs | 86 ++++++ .../src/authentication/mod.rs | 16 + gateway/gateway-requests/src/lib.rs | 6 +- .../src/registration/handshake/client.rs | 130 ++++++++ .../src/registration/handshake/error.rs | 52 ++++ .../src/registration/handshake/gateway.rs | 123 ++++++++ .../src/registration/handshake/mod.rs | 81 +++++ .../src/registration/handshake/shared_key.rs | 86 ++++++ .../src/registration/handshake/state.rs | 256 ++++++++++++++++ .../gateway-requests/src/registration/mod.rs | 20 ++ gateway/gateway-requests/src/types.rs | 136 +++++++-- gateway/src/commands/init.rs | 11 +- gateway/src/commands/run.rs | 28 +- gateway/src/config/mod.rs | 37 +++ gateway/src/config/persistence/pathfinder.rs | 10 +- gateway/src/config/template.rs | 6 + .../node/client_handling/clients_handler.rs | 107 ++++--- .../websocket/connection_handler.rs | 282 +++++++++++++----- .../client_handling/websocket/listener.rs | 13 +- .../receiver/packet_processing.rs | 12 +- gateway/src/node/mod.rs | 43 ++- gateway/src/node/presence/mod.rs | 18 +- gateway/src/node/storage/ledger.rs | 69 +++-- mixnode/src/commands/init.rs | 4 +- mixnode/src/commands/run.rs | 4 +- mixnode/src/node/mod.rs | 2 +- mixnode/src/node/packet_processing.rs | 6 +- 68 files changed, 2736 insertions(+), 1045 deletions(-) create mode 100644 common/crypto/src/asymmetric/encryption/mod.rs create mode 100644 common/crypto/src/asymmetric/identity/mod.rs create mode 100644 common/crypto/src/asymmetric/mod.rs delete mode 100644 common/crypto/src/encryption/mod.rs delete mode 100644 common/crypto/src/identity/mod.rs create mode 100644 common/crypto/src/kdf/blake3_hkdf.rs create mode 100644 common/crypto/src/kdf/mod.rs create mode 100644 common/crypto/src/symmetric/aes_ctr.rs create mode 100644 common/crypto/src/symmetric/mod.rs delete mode 100644 gateway/gateway-requests/src/auth_token.rs create mode 100644 gateway/gateway-requests/src/authentication/encrypted_address.rs create mode 100644 gateway/gateway-requests/src/authentication/iv.rs create mode 100644 gateway/gateway-requests/src/authentication/mod.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/client.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/error.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/gateway.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/mod.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/shared_key.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/state.rs create mode 100644 gateway/gateway-requests/src/registration/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e02f9e8d59..9a9cb2f1c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,8 +221,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" dependencies = [ "byte-tools", - "crypto-mac", - "digest", + "crypto-mac 0.7.0", + "digest 0.8.1", "opaque-debug", ] @@ -237,6 +237,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "0.3.4" +source = "git+https://github.com/BLAKE3-team/BLAKE3?rev=4c41a893a00a3ebe7b24529531ccf96d8593a57c#4c41a893a00a3ebe7b24529531ccf96d8593a57c" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "crypto-mac 0.8.0", + "digest 0.9.0", +] + [[package]] name = "block-buffer" version = "0.7.3" @@ -393,6 +407,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "clear_on_drop" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" +dependencies = [ + "cc", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -542,13 +565,16 @@ dependencies = [ name = "crypto" version = "0.1.0" dependencies = [ + "aes-ctr 0.4.0", + "blake3", "bs58", - "curve25519-dalek", + "ed25519-dalek", + "hkdf 0.9.0", "log 0.4.8", - "nymsphinx", + "nymsphinx-types", "pretty_env_logger", "rand 0.7.3", - "rand_core 0.5.1", + "x25519-dalek", ] [[package]] @@ -561,6 +587,16 @@ dependencies = [ "subtle 1.0.0", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.1", + "subtle 2.2.2", +] + [[package]] name = "ctr" version = "0.3.2" @@ -587,7 +623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" dependencies = [ "byteorder", - "digest", + "digest 0.8.1", "rand_core 0.5.1", "subtle 2.2.2", "zeroize", @@ -630,6 +666,15 @@ dependencies = [ "generic-array 0.12.3", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.1", +] + [[package]] name = "directory-client" version = "0.1.0" @@ -686,6 +731,18 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" +[[package]] +name = "ed25519-dalek" +version = "1.0.0-pre.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978710b352437433c97b2bff193f2fb1dfd58a093f863dd95e225a19baa599a2" +dependencies = [ + "clear_on_drop", + "curve25519-dalek", + "rand 0.7.3", + "sha2", +] + [[package]] name = "either" version = "1.5.3" @@ -961,6 +1018,7 @@ dependencies = [ name = "gateway-client" version = "0.1.0" dependencies = [ + "crypto", "futures 0.3.4", "gateway-requests", "log 0.4.8", @@ -974,7 +1032,11 @@ name = "gateway-requests" version = "0.1.0" dependencies = [ "bs58", + "crypto", + "futures 0.3.4", + "log 0.4.8", "nymsphinx", + "rand 0.7.3", "serde", "serde_json", "tokio-tungstenite", @@ -1089,8 +1151,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fa08a006102488bd9cd5b8013aabe84955cf5ae22e304c2caf655b633aefae3" dependencies = [ - "digest", - "hmac", + "digest 0.8.1", + "hmac 0.7.1", +] + +[[package]] +name = "hkdf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1149865383e4526a43aee8495f9a325f0b806c63ce6427d06336a590abbbc9" +dependencies = [ + "digest 0.9.0", + "hmac 0.8.0", ] [[package]] @@ -1099,8 +1171,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" dependencies = [ - "crypto-mac", - "digest", + "crypto-mac 0.7.0", + "digest 0.8.1", +] + +[[package]] +name = "hmac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87b580bd66811cc2324a27f3587de707cacf7525b96dca8122f7493e6cce0da" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", ] [[package]] @@ -1723,7 +1805,6 @@ dependencies = [ "clap", "config", "crypto", - "curve25519-dalek", "directory-client", "dirs", "dotenv", @@ -1779,7 +1860,6 @@ dependencies = [ "dotenv", "futures 0.3.4", "gateway-requests", - "hmac", "log 0.4.8", "mixnet-client", "nymsphinx", @@ -1787,7 +1867,6 @@ dependencies = [ "pretty_env_logger", "rand 0.7.3", "serde", - "sha2", "sled", "tempfile", "tokio 0.2.16", @@ -1868,7 +1947,7 @@ dependencies = [ name = "nymsphinx-acknowledgements" version = "0.1.0" dependencies = [ - "aes-ctr 0.4.0", + "crypto", "nymsphinx-addressing", "nymsphinx-params", "nymsphinx-types", @@ -2786,7 +2865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ "block-buffer", - "digest", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -2798,7 +2877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" dependencies = [ "block-buffer", - "digest", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -2877,7 +2956,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=cd9f09b85de33c60030ba6d7fae613c42cbe1faf#cd9f09b85de33c60030ba6d7fae613c42cbe1faf" +source = "git+https://github.com/nymtech/sphinx?rev=f31efdbf667952440bd9af5eb0c140135fd1bc4c#f31efdbf667952440bd9af5eb0c140135fd1bc4c" dependencies = [ "aes-ctr 0.3.0", "arrayref", @@ -2886,12 +2965,11 @@ dependencies = [ "byteorder", "chacha", "curve25519-dalek", - "hkdf", - "hmac", + "hkdf 0.8.0", + "hmac 0.7.1", "lioness", "log 0.4.8", "rand 0.7.3", - "rand_core 0.5.1", "rand_distr", "sha2", ] @@ -3698,8 +3776,34 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "x25519-dalek" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "zeroize" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn", + "synstructure", +] diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index ed7662676a..d1df1f7071 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -14,7 +14,6 @@ path = "src/lib.rs" [dependencies] bs58 = "0.3.0" clap = "2.33.0" -curve25519-dalek = "2.0.0" dirs = "2.0.2" dotenv = "0.15.0" futures = "0.3.1" diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 27344223fa..6e73884bbd 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -24,13 +24,13 @@ use crate::client::topology_control::{ }; use crate::config::{Config, SocketType}; use crate::websocket; -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use futures::channel::mpsc; use gateway_client::{ AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, MixnetMessageSender, }; -use gateway_requests::auth_token::AuthToken; +use gateway_requests::registration::handshake::SharedKey; use log::*; use nymsphinx::acknowledgements::identifier::AckAes128Key; use nymsphinx::addressing::clients::Recipient; @@ -50,7 +50,7 @@ pub(crate) mod topology_control; pub struct NymClient { config: Config, runtime: Runtime, - identity_keypair: MixIdentityKeyPair, + identity_keypair: Arc, // to be used by "send" function or socket, etc input_tx: Option, @@ -60,11 +60,11 @@ pub struct NymClient { } impl NymClient { - pub fn new(config: Config, identity_keypair: MixIdentityKeyPair) -> Self { + pub fn new(config: Config, identity_keypair: identity::KeyPair) -> Self { NymClient { runtime: Runtime::new().unwrap(), config, - identity_keypair, + identity_keypair: Arc::new(identity_keypair), input_tx: None, receive_tx: None, } @@ -72,7 +72,7 @@ impl NymClient { pub fn as_mix_recipient(&self) -> Recipient { Recipient::new( - self.identity_keypair.public_key.derive_address(), + self.identity_keypair.public_key().derive_address(), // TODO: below only works under assumption that gateway address == gateway id // (which currently is true) NodeAddressBytes::try_from_base58_string(self.config.get_gateway_id()).unwrap(), @@ -159,65 +159,55 @@ impl NymClient { &mut self, mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, - gateway_address: url::Url, ) -> GatewayClient<'static, url::Url> { - let auth_token = self - .config - .get_gateway_auth_token() - .map(|str_token| AuthToken::try_from_base58_string(str_token).ok()) - .unwrap_or(None); + let gateway_id = self.config.get_gateway_id(); + if gateway_id.is_empty() { + panic!("The identity of the gateway is unknown - did you run `nym-client` init?") + } + let gateway_address_str = self.config.get_gateway_listener(); + if gateway_address_str.is_empty() { + panic!("The address of the gateway is unknown - did you run `nym-client` init?") + } + + let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) + .expect("provided gateway id is invalid!"); + + // TODO: since we presumably now get something valid-ish from the `init`, can we just + // ditch url::Url and operate on `String`? + let gateway_address = + url::Url::parse(&gateway_address_str).expect("provided gateway address is invalid!"); + + let shared_key = self.config.get_gateway_shared_key().map(|str_shared_key| { + SharedKey::try_from_base58_string(str_shared_key) + .expect("The stored shared key is invalid!") + }); let mut gateway_client = GatewayClient::new( gateway_address, - self.as_mix_recipient().destination(), - auth_token, + Arc::clone(&self.identity_keypair), + gateway_identity, + shared_key, mixnet_message_sender, ack_sender, self.config.get_gateway_response_timeout(), ); - let auth_token = self.runtime.block_on(async { + let shared_key = self.runtime.block_on(async { gateway_client - .establish_connection() + .authenticate_and_start() .await - .expect("could not establish initial connection with the gateway"); - gateway_client - .perform_initial_authentication() - .await - .expect("could not perform initial authentication with the gateway") + .expect("could not authenticate and start up the gateway connection") }); - // TODO: if we didn't have an auth_token initially, save it to config or something? + // TODO: if we didn't have a shared_key initially, save it to config or something? info!( "Performed initial authentication. Auth token is {:?}", - auth_token.to_base58_string() + shared_key.to_base58_string() ); gateway_client } - // TODO: this information should be just put in the config on init... - async fn get_gateway_address( - gateway_id: String, - topology_accessor: TopologyAccessor, - ) -> url::Url { - // we already have our gateway written in the config - let gateway_address = topology_accessor - .get_gateway_socket_url(&gateway_id) - .await - .unwrap_or_else(|| { - panic!( - "Could not find gateway with id {:?}.\ - It does not seem to be present in the current network topology.\ - Are you sure it is still online?\ - Perhaps try to run `nym-client init` again to obtain a new gateway", - gateway_id - ) - }); - - url::Url::parse(&gateway_address).expect("provided gateway address is invalid!") - } - // future responsible for periodically polling directory server and updating // the current global view of topology fn start_topology_refresher( @@ -363,12 +353,7 @@ impl NymClient { mixnet_messages_receiver, ); - let gateway_url = self.runtime.block_on(Self::get_gateway_address( - self.config.get_gateway_id(), - shared_topology_accessor.clone(), - )); - let gateway_client = - self.start_gateway_client(mixnet_messages_sender, ack_sender, gateway_url); + let gateway_client = self.start_gateway_client(mixnet_messages_sender, ack_sender); self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); diff --git a/clients/native/src/client/received_buffer.rs b/clients/native/src/client/received_buffer.rs index 6db09190aa..3ffad96e80 100644 --- a/clients/native/src/client/received_buffer.rs +++ b/clients/native/src/client/received_buffer.rs @@ -18,7 +18,6 @@ use futures::StreamExt; use gateway_client::MixnetMessageReceiver; use log::*; use nymsphinx::chunking::reconstruction::MessageReconstructor; -use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; use std::collections::HashSet; use std::sync::Arc; use tokio::runtime::Handle; diff --git a/clients/native/src/client/topology_control.rs b/clients/native/src/client/topology_control.rs index 126a2bdea0..0a5b6fb8eb 100644 --- a/clients/native/src/client/topology_control.rs +++ b/clients/native/src/client/topology_control.rs @@ -23,7 +23,7 @@ use std::time::Duration; use tokio::runtime::Handle; use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::task::JoinHandle; -use topology::{provider, NymTopology}; +use topology::{gateway, NymTopology}; // I'm extremely curious why compiler NEVER complained about lack of Debug here before #[derive(Debug)] @@ -117,16 +117,16 @@ impl TopologyAccessor { self.inner.write().await.update(new_topology); } - pub(crate) async fn get_gateway_socket_url(&self, id: &str) -> Option { - match &self.inner.read().await.0 { - None => None, - Some(ref topology) => topology - .gateways() - .iter() - .find(|gateway| gateway.pub_key == id) - .map(|gateway| gateway.client_listener.clone()), - } - } + // pub(crate) async fn get_gateway_socket_url(&self, id: &str) -> Option { + // match &self.inner.read().await.0 { + // None => None, + // Some(ref topology) => topology + // .gateways() + // .iter() + // .find(|gateway| gateway.identity_key == id) + // .map(|gateway| gateway.client_listener.clone()), + // } + // } // only used by the client at startup to get a slightly more reasonable error message // (currently displays as unused because healthchecker is disabled due to required changes) @@ -137,15 +137,15 @@ impl TopologyAccessor { } } - pub(crate) async fn get_all_clients(&self) -> Option> { + pub(crate) async fn get_all_clients(&self) -> Option> { // TODO: this will need to be modified to instead return pairs (provider, client) match &self.inner.read().await.0 { None => None, Some(ref topology) => Some( topology - .providers() + .gateways() .iter() - .flat_map(|provider| provider.registered_clients.iter()) + .flat_map(|gateway| gateway.registered_clients.iter()) .cloned() .collect::>(), ), diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 006cefb397..81c6982c49 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -17,12 +17,12 @@ use crate::commands::override_config; use crate::config::persistence::pathfinder::ClientPathfinder; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use directory_client::DirectoryClient; use gateway_client::GatewayClient; -use gateway_requests::AuthToken; -use nymsphinx::DestinationAddressBytes; +use gateway_requests::registration::handshake::SharedKey; use pemstore::pemstore::PemStore; +use std::sync::Arc; use std::time::Duration; use topology::gateway::Node; use topology::NymTopology; @@ -65,21 +65,36 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { async fn try_gateway_registration( gateways: Vec, - our_address: DestinationAddressBytes, -) -> Option<(String, AuthToken)> { + our_identity: Arc, +) -> Option<(String, String, SharedKey)> { let timeout = Duration::from_millis(1500); for gateway in gateways { + let gateway_identity = + match identity::PublicKey::from_base58_string(gateway.identity_key.clone()) { + Ok(id) => id, + Err(_) => { + eprintln!( + "gateway {} announces invalid identity!", + gateway.identity_key + ); + continue; + } + }; + let mut gateway_client = GatewayClient::new_init( url::Url::parse(&gateway.client_listener).unwrap(), - our_address.clone(), + gateway_identity, + our_identity.clone(), timeout, ); - if let Ok(token) = gateway_client.register_without_listening().await { - if let Err(err) = gateway_client.close_connection().await { - eprintln!("Error while closing connection to the gateway! - {:?}", err); - continue; - } else { - return Some((gateway.pub_key, token)); + if let Ok(_) = gateway_client.establish_connection().await { + if let Ok(shared_key) = gateway_client.register().await { + if let Err(err) = gateway_client.close_connection().await { + eprintln!("Error while closing connection to the gateway! - {:?}", err); + continue; + } else { + return Some((gateway.identity_key, gateway.client_listener, shared_key)); + } } } } @@ -88,8 +103,8 @@ async fn try_gateway_registration( async fn choose_gateway( directory_server: String, - our_address: DestinationAddressBytes, -) -> (String, AuthToken) { + our_identity: Arc, +) -> (String, String, SharedKey) { let directory_client_config = directory_client::Config::new(directory_server.clone()); let directory_client = directory_client::Client::new(directory_client_config); let topology = directory_client.get_topology().await.unwrap(); @@ -101,7 +116,7 @@ async fn choose_gateway( // try to perform registration so that we wouldn't need to do it at startup // + at the same time we'll know if we can actually talk with that gateway - let registration_result = try_gateway_registration(gateways, our_address).await; + let registration_result = try_gateway_registration(gateways, our_identity).await; match registration_result { None => { // while technically there's no issue client-side, it will be impossible to execute @@ -113,10 +128,26 @@ async fn choose_gateway( directory_server ) } - Some((gateway_id, auth_token)) => (gateway_id, auth_token), + Some((gateway_id, gateway_listener, shared_key)) => { + (gateway_id, gateway_listener, shared_key) + } } } +async fn get_gateway_listener(directory_server: String, gateway_identity: &str) -> Option { + let directory_client_config = directory_client::Config::new(directory_server); + let directory_client = directory_client::Client::new(directory_client_config); + let topology = directory_client.get_topology().await.unwrap(); + let gateways = topology.gateways(); + + for gateway in gateways { + if gateway.identity_key == gateway_identity { + return Some(gateway.client_listener); + } + } + None +} + pub fn execute(matches: &ArgMatches) { println!("Initialising client..."); @@ -128,26 +159,41 @@ pub fn execute(matches: &ArgMatches) { config = config.set_high_default_traffic_volume(); } - let mix_identity_keys = MixIdentityKeyPair::new(); + let mix_identity_keys = Arc::new(identity::KeyPair::new()); // if there is no gateway chosen, get a random-ish one from the topology if config.get_gateway_id().is_empty() { - let our_address = mix_identity_keys.public_key().derive_address(); // TODO: is there perhaps a way to make it work without having to spawn entire runtime? let mut rt = tokio::runtime::Runtime::new().unwrap(); - let (gateway_id, auth_token) = - rt.block_on(choose_gateway(config.get_directory_server(), our_address)); + let (gateway_id, gateway_listener, shared_key) = rt.block_on(choose_gateway( + config.get_directory_server(), + mix_identity_keys.clone(), + )); - // TODO: this isn't really a gateway, but gateway, yet another change to make config = config .with_gateway_id(gateway_id) - .with_gateway_auth_token(auth_token); + .with_gateway_listener(gateway_listener) + .with_gateway_shared_key(shared_key.to_base58_string()); + } + + // we specified our gateway but don't know its physical address + if config.get_gateway_listener().is_empty() { + // TODO: is there perhaps a way to make it work without having to spawn entire runtime? + let mut rt = tokio::runtime::Runtime::new().unwrap(); + let gateway_listener = rt + .block_on(get_gateway_listener( + config.get_directory_server(), + &config.get_gateway_id(), + )) + .expect("No gateway with provided id exists!"); + + config = config.with_gateway_listener(gateway_listener); } let pathfinder = ClientPathfinder::new_from_config(&config); let pem_store = PemStore::new(pathfinder); pem_store - .write_identity(mix_identity_keys) + .write_identity_keypair(mix_identity_keys.as_ref()) .expect("Failed to save identity keys"); println!("Saved mixnet identity keypair"); @@ -161,11 +207,8 @@ pub fn execute(matches: &ArgMatches) { "Unless overridden in all `nym-client run` we will be talking to the following gateway: {}...", config.get_gateway_id(), ); - if config.get_gateway_auth_token().is_some() { - println!( - "using optional AuthToken: {:?}", - config.get_gateway_auth_token().unwrap() - ) + if let Some(shared_key) = config.get_gateway_shared_key() { + println!("using optional SharedKey: {:?}", shared_key) } println!("Client configuration completed.\n\n\n") } diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index 964cc3116b..776312a844 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -17,7 +17,7 @@ use crate::commands::override_config; use crate::config::{persistence::pathfinder::ClientPathfinder, Config}; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -57,13 +57,13 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { ) } -fn load_identity_keys(config_file: &Config) -> MixIdentityKeyPair { +fn load_identity_keys(config_file: &Config) -> identity::KeyPair { let identity_keypair = PemStore::new(ClientPathfinder::new_from_config(&config_file)) - .read_identity() + .read_identity_keypair() .expect("Failed to read stored identity key files"); println!( "Public identity key: {}\n", - identity_keypair.public_key.to_base58_string() + identity_keypair.public_key().to_base58_string() ); identity_keypair } diff --git a/clients/native/src/config/mod.rs b/clients/native/src/config/mod.rs index 7711129b9c..e5d2b75ac8 100644 --- a/clients/native/src/config/mod.rs +++ b/clients/native/src/config/mod.rs @@ -126,8 +126,13 @@ impl Config { self } - pub fn with_gateway_auth_token>(mut self, token: S) -> Self { - self.client.gateway_authtoken = Some(token.into()); + pub fn with_gateway_listener>(mut self, gateway_listener: S) -> Self { + self.client.gateway_listener = gateway_listener.into(); + self + } + + pub fn with_gateway_shared_key>(mut self, shared_key: S) -> Self { + self.client.gateway_shared_key = Some(shared_key.into()); self } @@ -174,8 +179,12 @@ impl Config { self.client.gateway_id.clone() } - pub fn get_gateway_auth_token(&self) -> Option { - self.client.gateway_authtoken.clone() + pub fn get_gateway_listener(&self) -> String { + self.client.gateway_listener.clone() + } + + pub fn get_gateway_shared_key(&self) -> Option { + self.client.gateway_shared_key.clone() } pub fn get_socket_type(&self) -> SocketType { @@ -255,10 +264,13 @@ pub struct Client { /// If initially omitted, a random gateway will be chosen from the available topology. gateway_id: String, - /// A gateway specific, optional, base58 stringified authentication token used for + /// Address of the gateway listener to which all client requests should be sent. + gateway_listener: String, + + /// A gateway specific, optional, base58 stringified shared key used for /// communication with particular gateway. #[serde(deserialize_with = "de_option_string")] - gateway_authtoken: Option, + gateway_shared_key: Option, /// nym_home_directory specifies absolute path to the home nym Clients directory. /// It is expected to use default value and hence .toml file should not redefine this field. @@ -274,7 +286,8 @@ impl Default for Client { private_identity_key_file: Default::default(), public_identity_key_file: Default::default(), gateway_id: "".to_string(), - gateway_authtoken: None, + gateway_listener: "".to_string(), + gateway_shared_key: None, nym_root_directory: Config::default_root_directory(), } } diff --git a/clients/native/src/config/template.rs b/clients/native/src/config/template.rs index 590e567584..543ff65394 100644 --- a/clients/native/src/config/template.rs +++ b/clients/native/src/config/template.rs @@ -41,9 +41,12 @@ public_identity_key_file = '{{ client.public_identity_key_file }}' # ID of the gateway from which the client should be fetching messages. gateway_id = '{{ client.gateway_id }}' -# A gateway specific, optional, base58 stringified authentication token used for +# Address of the gateway listener to which all client requests should be sent. +gateway_listener = '{{ client.gateway_listener }}' + +# A gateway specific, optional, base58 stringified shared key used for # communication with particular gateway. -gateway_authtoken = '{{ client.gateway_authtoken }}' +gateway_shared_key = '{{ client.gateway_shared_key }}' ##### advanced configuration options ##### diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index 9466912cf9..06967d94c6 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use crypto::identity::MixIdentityPublicKey; use models::topology::Topology; use nymsphinx::addressing::nodes::NymNodeRoutingAddress; use nymsphinx::Node as SphinxNode; @@ -26,6 +25,7 @@ use wasm_bindgen::prelude::*; mod models; mod utils; +use crypto::asymmetric::encryption; pub use models::keys::keygen; use nymsphinx::addressing::clients::Recipient; use topology::NymTopology; @@ -114,19 +114,15 @@ fn sphinx_route_to(topology_json: &str, gateway_address: &NodeAddressBytes) -> V } impl TryFrom for SphinxNode { + // We really should start actually using errors rather than unwrapping on everything type Error = (); fn try_from(node_data: NodeData) -> Result { let addr: SocketAddr = node_data.address.parse().unwrap(); let address: NodeAddressBytes = NymNodeRoutingAddress::from(addr).try_into().unwrap(); - - // this has to be temporarily moved out of separate function as we can't return private types - let pub_key = { - let src = MixIdentityPublicKey::from_base58_string(node_data.public_key).to_bytes(); - let mut dest: [u8; 32] = [0; 32]; - dest.copy_from_slice(&src); - nymsphinx::public_key_from_bytes(dest) - }; + let pub_key = encryption::PublicKey::from_base58_string(node_data.public_key) + .unwrap() + .into(); Ok(SphinxNode { address, pub_key }) } @@ -156,7 +152,7 @@ mod test_constructing_a_sphinx_packet { let mut payload = create_sphinx_packet( topology_fixture(), "foomp", - "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq@7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq@CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ); // you don't really need 32 bytes here, but giving too much won't make it fail let mut address_buffer = [0; 32]; @@ -178,7 +174,7 @@ mod building_a_topology_from_json { sphinx_route_to( "", &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -190,7 +186,7 @@ mod building_a_topology_from_json { sphinx_route_to( "bad bad bad not json", &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -205,7 +201,7 @@ mod building_a_topology_from_json { sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -221,7 +217,7 @@ mod building_a_topology_from_json { sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -234,7 +230,7 @@ mod building_a_topology_from_json { let route = sphinx_route_to( topology_fixture(), &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -248,7 +244,7 @@ mod building_a_topology_from_json { let route = sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -316,7 +312,8 @@ fn topology_fixture() -> &'static str { { "clientListener": "139.162.246.48:9000", "mixnetListener": "139.162.246.48:1789", - "pubKey": "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "identityKey": "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "sphinxKey": "BnLYqQjb8K6TmW5oFdNZrUTocGxa3rgzBvapQrf8XUbF", "version": "0.6.0", "location": "London, UK", "registeredClients": [ @@ -329,7 +326,8 @@ fn topology_fixture() -> &'static str { { "clientListener": "127.0.0.1:9000", "mixnetListener": "127.0.0.1:1789", - "pubKey": "2XK8RDcUTRcJLUWoDfoXc2uP4YViscMLEM5NSzhSi87M", + "identityKey": "B9xz9V6jpp1fEbDkeyR5f8miorw9bzXGKoMbKnaxkD41", + "sphinxKey": "3KCpz1HCD8DqnQjemT1uuBZipmHFXM4V5btxLXwvM1gG", "version": "0.6.0", "location": "unknown", "registeredClients": [], diff --git a/clients/webassembly/src/models/keys.rs b/clients/webassembly/src/models/keys.rs index e559b7a10f..a75b759b78 100644 --- a/clients/webassembly/src/models/keys.rs +++ b/clients/webassembly/src/models/keys.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use wasm_bindgen::prelude::*; @@ -42,7 +42,7 @@ impl TryInto for GatewayIdentity { #[wasm_bindgen] pub fn keygen() -> String { - let keypair = MixIdentityKeyPair::new(); + let keypair = identity::KeyPair::new(); let address = keypair.public_key().derive_address(); GatewayIdentity { diff --git a/common/client-libs/directory-client/models/src/presence/gateways.rs b/common/client-libs/directory-client/models/src/presence/gateways.rs index 78ffa78f44..5822dc6a6a 100644 --- a/common/client-libs/directory-client/models/src/presence/gateways.rs +++ b/common/client-libs/directory-client/models/src/presence/gateways.rs @@ -21,7 +21,8 @@ pub struct GatewayPresence { pub location: String, pub client_listener: String, pub mixnet_listener: String, - pub pub_key: String, + pub identity_key: String, + pub sphinx_key: String, pub registered_clients: Vec, pub last_seen: u64, pub version: String, @@ -33,7 +34,8 @@ impl Into for GatewayPresence { location: self.location, client_listener: self.client_listener.parse().unwrap(), mixnet_listener: self.mixnet_listener.parse().unwrap(), - pub_key: self.pub_key, + identity_key: self.identity_key, + sphinx_key: self.sphinx_key, registered_clients: self .registered_clients .into_iter() @@ -51,7 +53,8 @@ impl From for GatewayPresence { location: mpn.location, client_listener: mpn.client_listener.to_string(), mixnet_listener: mpn.mixnet_listener.to_string(), - pub_key: mpn.pub_key, + identity_key: mpn.identity_key, + sphinx_key: mpn.sphinx_key, registered_clients: mpn .registered_clients .into_iter() diff --git a/common/client-libs/directory-client/src/requests/presence_gateways_post.rs b/common/client-libs/directory-client/src/requests/presence_gateways_post.rs index 1ed7adc5e9..80ef05c4c1 100644 --- a/common/client-libs/directory-client/src/requests/presence_gateways_post.rs +++ b/common/client-libs/directory-client/src/requests/presence_gateways_post.rs @@ -90,7 +90,8 @@ mod presence_gateways_post_request { location: "foomp".to_string(), client_listener: "foo.com".to_string(), mixnet_listener: "foo.com".to_string(), - pub_key: "abc".to_string(), + identity_key: "def".to_string(), + sphinx_key: "abc".to_string(), registered_clients: vec![], last_seen: 0, version: "0.1.0".to_string(), diff --git a/common/client-libs/directory-client/src/requests/presence_topology_get.rs b/common/client-libs/directory-client/src/requests/presence_topology_get.rs index d13a0d1f8e..ff44fb9bfb 100644 --- a/common/client-libs/directory-client/src/requests/presence_topology_get.rs +++ b/common/client-libs/directory-client/src/requests/presence_topology_get.rs @@ -48,6 +48,7 @@ mod topology_requests { #[cfg(test)] mod on_a_400_status { use super::*; + #[tokio::test] async fn it_returns_an_error() { let _m = mock("GET", PATH) @@ -60,9 +61,11 @@ mod topology_requests { _m.assert(); } } + #[cfg(test)] mod on_a_200 { use super::*; + #[tokio::test] async fn it_returns_a_response_with_200_status_and_a_correct_topology() { let json = fixtures::topology_response_json(); @@ -208,7 +211,8 @@ mod topology_requests { "location": "unknown", "clientListener": "3.8.176.11:8888", "mixnetListener": "3.8.176.11:9999", - "pubKey": "54U6krAr-j9nQXFlsHk3io04_p0tctuqH71t7w_usgI=", + "identityKey": "B9xz9V6jpp1fEbDkeyR5f8miorw9bzXGKoMbKnaxkD41", + "sphinxKey": "3KCpz1HCD8DqnQjemT1uuBZipmHFXM4V5btxLXwvM1gG", "registeredClients": [ { "pubKey": "zOqdJFH49HcgGSCRnmbXGzovnwRLEPN0YGN1SCafTyo=" @@ -224,7 +228,8 @@ mod topology_requests { "location": "unknown", "clientListener": "3.8.176.12:8888", "mixnetListener": "3.8.176.12:9999", - "pubKey": "sA-sxi038pEbGy4lgZWG-RdHHDkA6kZzu44G0LUxFSc=", + "identityKey": "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "sphinxKey": "BnLYqQjb8K6TmW5oFdNZrUTocGxa3rgzBvapQrf8XUbF", "registeredClients": [ { "pubKey": "UE-7r6-bpw0b4T3GxOBVxlg02psx23DF2p5Tuf-OBSE=" diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index cca78d7e14..88f7dc68e4 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -15,6 +15,7 @@ tokio = { version = "0.2", features = ["macros", "rt-core", "stream", "sync", "t tokio-tungstenite = "0.10" # internal +crypto = { path = "../../crypto" } gateway-requests = { path = "../../../gateway/gateway-requests" } nymsphinx = { path = "../../nymsphinx" } diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 82f4c1d8c1..6112f1bfa3 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use gateway_requests::auth_token::AuthTokenConversionError; +use gateway_requests::registration::handshake::error::HandshakeError; use std::fmt::{self, Error, Formatter}; use tokio_tungstenite::tungstenite::Error as WsError; @@ -21,11 +21,12 @@ pub enum GatewayClientError { ConnectionNotEstablished, GatewayError(String), NetworkError(WsError), - NoAuthTokenAvailable, + NoSharedKeyAvailable, ConnectionAbruptlyClosed, MalformedResponse, NotAuthenticated, ConnectionInInvalidState, + RegistrationFailure(HandshakeError), AuthenticationFailure, Timeout, } @@ -36,12 +37,6 @@ impl From for GatewayClientError { } } -impl From for GatewayClientError { - fn from(_err: AuthTokenConversionError) -> Self { - GatewayClientError::MalformedResponse - } -} - // better human readable representation of the error, mostly so that GatewayClientError // would implement std::error::Error impl fmt::Display for GatewayClientError { @@ -50,8 +45,8 @@ impl fmt::Display for GatewayClientError { GatewayClientError::ConnectionNotEstablished => { write!(f, "connection to the gateway is not established") } - GatewayClientError::NoAuthTokenAvailable => { - write!(f, "no AuthToken was provided or obtained") + GatewayClientError::NoSharedKeyAvailable => { + write!(f, "no shared key was provided or obtained") } GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), GatewayClientError::NetworkError(err) => { @@ -66,6 +61,11 @@ impl fmt::Display for GatewayClientError { f, "connection is in an invalid state - please send a bug report" ), + GatewayClientError::RegistrationFailure(handshake_err) => write!( + f, + "failed to finish registration handshake - {}", + handshake_err + ), GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), GatewayClientError::GatewayError(err) => { write!(f, "gateway returned an error response - {}", err) diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 2d806d24b4..eb7d1f2821 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -17,12 +17,15 @@ use crate::packet_router::PacketRouter; pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; +use crypto::asymmetric::identity; use futures::stream::{SplitSink, SplitStream}; -use futures::{future::BoxFuture, FutureExt, SinkExt, StreamExt}; -use gateway_requests::auth_token::AuthToken; +use futures::{future::BoxFuture, FutureExt, SinkExt, Stream, StreamExt}; +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::registration::handshake::{client_handshake, SharedKey, DEFAULT_RNG}; use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; use log::*; -use nymsphinx::{DestinationAddressBytes, SphinxPacket}; +use nymsphinx::SphinxPacket; use std::convert::TryFrom; use std::net::SocketAddr; use std::sync::Arc; @@ -31,31 +34,26 @@ use tokio::net::TcpStream; use tokio::sync::Notify; use tokio_tungstenite::{ connect_async, - tungstenite::{client::IntoClientRequest, protocol::Message}, + tungstenite::{client::IntoClientRequest, protocol::Message, Error as WsError}, WebSocketStream, }; pub mod error; pub mod packet_router; -// TODO: combine the duplicate reading procedure, i.e. -/* -msg read from conn.next().await: - if msg.is_none() { - res = Some(Err(GatewayClientError::ConnectionAbruptlyClosed)); - break; +/// A helper method to read an underlying message from the stream or return an error. +async fn read_ws_stream_message(conn: &mut S) -> Result +where + S: Stream> + Unpin, +{ + match conn.next().await { + Some(msg) => match msg { + Ok(msg) => Ok(msg), + Err(err) => Err(GatewayClientError::NetworkError(err)), + }, + None => Err(GatewayClientError::ConnectionAbruptlyClosed), } - let msg = match msg.unwrap() { - Ok(msg) => msg, - Err(err) => { - res = Some(Err(GatewayClientError::NetworkError(err))); - break; - } - }; - match msg { - // specific handling - } -*/ +} // TODO: some batching mechanism to allow reading and sending more than a single packet through @@ -94,24 +92,15 @@ impl<'a> PartiallyDelegated<'a> { _ = notify_clone.notified() => { should_return = true; } - msg = stream.next() => { - if msg.is_none() { - return Err(GatewayClientError::ConnectionAbruptlyClosed); - } - let msg = match msg.unwrap() { - Ok(msg) => msg, - Err(err) => { - return Err(GatewayClientError::NetworkError(err)); - } - }; - match msg { + msg = read_ws_stream_message(&mut stream) => { + match msg? { Message::Binary(bin_msg) => { // TODO: some batching mechanism to allow reading and sending more than // one packet at the time, because the receiver can easily handle it packet_router.route_received(vec![bin_msg]) }, // I think that in the future we should perhaps have some sequence number system, i.e. - // so each request/reponse pair can be easily identified, so that if messages are + // so each request/response pair can be easily identified, so that if messages are // not ordered (for some peculiar reason) we wouldn't lose anything. // This would also require NOT discarding any text responses here. Message::Text(_) => debug!("received a text message - probably a response to some previous query!"), @@ -184,32 +173,25 @@ impl<'a> SocketState<'a> { } } -pub struct GatewayClient<'a, R: IntoClientRequest + Unpin + Clone> { +pub struct GatewayClient<'a, R> { authenticated: bool, // can be String, string slices, `url::Url`, `http::Uri`, etc. gateway_address: R, - our_address: DestinationAddressBytes, - auth_token: Option, + gateway_identity: identity::PublicKey, + local_identity: Arc, + shared_key: Option, connection: SocketState<'a>, packet_router: PacketRouter, response_timeout_duration: Duration, } -impl<'a, R: IntoClientRequest + Unpin + Clone> Drop for GatewayClient<'a, R> { - fn drop(&mut self) { - // TODO to fix forcibly closing connection (although now that I think about it, - // I'm not sure this would do it, as to fix the said issue we'd need graceful shutdowns) - } -} - -impl<'a, R> GatewayClient<'static, R> -where - R: IntoClientRequest + Unpin + Clone, -{ +impl<'a, R> GatewayClient<'static, R> { + // TODO: put it all in a Config struct pub fn new( gateway_address: R, - our_address: DestinationAddressBytes, - auth_token: Option, + local_identity: Arc, + gateway_identity: identity::PublicKey, + shared_key: Option, mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, response_timeout_duration: Duration, @@ -217,8 +199,9 @@ where GatewayClient { authenticated: false, gateway_address, - our_address, - auth_token, + gateway_identity, + local_identity, + shared_key, connection: SocketState::NotConnected, packet_router: PacketRouter::new(ack_sender, mixnet_message_sender), response_timeout_duration, @@ -227,7 +210,8 @@ where pub fn new_init( gateway_address: R, - our_address: DestinationAddressBytes, + gateway_identity: identity::PublicKey, + local_identity: Arc, response_timeout_duration: Duration, ) -> Self { use futures::channel::mpsc; @@ -241,30 +225,15 @@ where GatewayClient { authenticated: false, gateway_address, - our_address, - auth_token: None, + gateway_identity, + local_identity, + shared_key: None, connection: SocketState::NotConnected, packet_router, response_timeout_duration, } } - pub async fn register_without_listening(&mut self) -> Result { - if !self.connection.is_established() { - self.establish_connection().await?; - } - let msg = ClientControlRequest::new_register(self.our_address.clone()).into(); - let token = match self.send_websocket_message(msg).await? { - ServerResponse::Register { token } => { - self.authenticated = true; - Ok(AuthToken::try_from_base58_string(token)?) - } - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => unreachable!(), - }?; - Ok(token) - } - pub async fn close_connection(&mut self) -> Result<(), GatewayClientError> { if self.connection.is_partially_delegated() { self.recover_socket_connection().await?; @@ -279,7 +248,10 @@ where } } - pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { + pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> + where + R: IntoClientRequest + Unpin + Clone, + { let ws_stream = match connect_async(self.gateway_address.clone()).await { Ok((ws_stream, _)) => ws_stream, Err(e) => return Err(GatewayClientError::NetworkError(e)), @@ -308,19 +280,12 @@ where } // just keep getting through socket buffer until we get to what we want... // (or we time out) - msg = conn.next() => { - if msg.is_none() { - res = Some(Err(GatewayClientError::ConnectionAbruptlyClosed)); + msg = read_ws_stream_message(conn) => { + if let Err(err) = msg { + res = Some(Err(err)); break; } - let msg = match msg.unwrap() { - Ok(msg) => msg, - Err(err) => { - res = Some(Err(GatewayClientError::NetworkError(err))); - break; - } - }; - match msg { + match msg.unwrap() { Message::Binary(bin_msg) => { self.packet_router.route_received(vec![bin_msg]); } @@ -376,38 +341,47 @@ where } } - pub async fn register(&mut self) -> Result { + pub async fn register(&mut self) -> Result { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } - let msg = ClientControlRequest::new_register(self.our_address.clone()).into(); - let token = match self.send_websocket_message(msg).await? { - ServerResponse::Register { token } => { - self.authenticated = true; - Ok(AuthToken::try_from_base58_string(token)?) - } - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + + debug_assert!(self.connection.is_available()); + + match &mut self.connection { + SocketState::Available(ws_stream) => client_handshake( + &mut DEFAULT_RNG, + ws_stream, + self.local_identity.as_ref(), + self.gateway_identity.clone(), + ) + .await + .map_err(|handshake_err| GatewayClientError::RegistrationFailure(handshake_err)), _ => unreachable!(), - }?; - self.start_listening_for_mixnet_messages()?; - Ok(token) + } } pub async fn authenticate( &mut self, - auth_token: Option, + shared_key: Option, ) -> Result { - if auth_token.is_none() && self.auth_token.is_none() { - return Err(GatewayClientError::NoAuthTokenAvailable); + if shared_key.is_none() && self.shared_key.is_none() { + return Err(GatewayClientError::NoSharedKeyAvailable); } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } // because of the previous check one of the unwraps MUST succeed - let auth_token = auth_token.unwrap_or_else(|| self.auth_token.unwrap()); + let shared_key = shared_key + .as_ref() + .unwrap_or_else(|| self.shared_key.as_ref().unwrap()); + let iv = AuthenticationIV::new_random(&mut DEFAULT_RNG); + let self_address = self.local_identity.as_ref().public_key().derive_address(); + let encrypted_address = EncryptedAddressBytes::new(&self_address, shared_key, &iv); let msg = - ClientControlRequest::new_authenticate(self.our_address.clone(), auth_token).into(); + ClientControlRequest::new_authenticate(self_address, encrypted_address, iv).into(); + let authenticated = match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { status } => { self.authenticated = status; @@ -416,22 +390,21 @@ where ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => unreachable!(), }?; - self.start_listening_for_mixnet_messages()?; Ok(authenticated) } - // just a helper method to either call register or authenticate based on self.auth_token value + /// Helper method to either call register or authenticate based on self.shared_key value pub async fn perform_initial_authentication( &mut self, - ) -> Result { - if self.auth_token.is_some() { + ) -> Result { + if self.shared_key.is_some() { self.authenticate(None).await?; } else { self.register().await?; } if self.authenticated { - // if we are authenticated it means we MUST have an associated auth_token - Ok(self.auth_token.clone().unwrap()) + // if we are authenticated it means we MUST have an associated shared_key + Ok(self.shared_key.clone().unwrap()) } else { Err(GatewayClientError::AuthenticationFailure) } @@ -449,7 +422,11 @@ where if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } - let msg = BinaryRequest::new_forward_request(address, packet).into(); + let msg = BinaryRequest::new_forward_request(address, packet).into_ws_message( + self.shared_key + .as_ref() + .expect("no shared key present even though we're authenticated!"), + ); self.send_websocket_message_without_response(msg).await } @@ -470,7 +447,11 @@ where Ok(()) } - fn start_listening_for_mixnet_messages(&mut self) -> Result<(), GatewayClientError> { + // Note: this requires prior authentication + pub fn start_listening_for_mixnet_messages(&mut self) -> Result<(), GatewayClientError> { + if !self.authenticated { + return Err(GatewayClientError::NotAuthenticated); + } if self.connection.is_partially_delegated() { return Ok(()); } @@ -492,4 +473,19 @@ where self.connection = SocketState::PartiallyDelegated(partially_delegated); Ok(()) } + + pub async fn authenticate_and_start(&mut self) -> Result + where + R: IntoClientRequest + Unpin + Clone, + { + if !self.connection.is_established() { + self.establish_connection().await?; + } + let shared_key = self.perform_initial_authentication().await?; + + // that call is NON-blocking + self.start_listening_for_mixnet_messages()?; + + Ok(shared_key) + } } diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 6b6be07de0..cb4e3df78b 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -7,12 +7,21 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +aes-ctr = "0.4.0" bs58 = "0.3.0" -curve25519-dalek = "2.0.0" +# can't use proper release just yet (unless we use outdated hkdf) +# as hkdf depends on digest 0.9.0 and most recent release of blake3 still uses 0.8.1 +# they've pushed an update to their repo on 12.06, so I presume another release is imminent. +blake3 = { git = "https://github.com/BLAKE3-team/BLAKE3", rev="4c41a893a00a3ebe7b24529531ccf96d8593a57c" } +#blake3 = "0.3.4" +hkdf = "0.9" + +x25519-dalek = "0.6" +# TODO: do we need serde feature? +ed25519-dalek = "1.0.0-pre.3" log = "0.4" pretty_env_logger = "0.3" -rand = "0.7.2" -rand_core = "0.5" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} # internal -nymsphinx = { path = "../nymsphinx" } +nymsphinx-types = { path = "../nymsphinx/types" } diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs new file mode 100644 index 0000000000..39b1fe61b5 --- /dev/null +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -0,0 +1,273 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{PemStorableKey, PemStorableKeyPair}; +use rand::{rngs::OsRng, CryptoRng, RngCore}; +use std::fmt::{self, Display, Formatter}; + +/// Size of a X25519 private key +pub const PRIVATE_KEY_SIZE: usize = 32; + +/// Size of a X25519 public key +pub const PUBLIC_KEY_SIZE: usize = 32; + +/// Size of a X25519 shared secret +pub const SHARED_SECRET_SIZE: usize = 32; + +#[derive(Debug)] +pub enum EncryptionKeyError { + InvalidPublicKey, + InvalidPrivateKey, +} + +// required for std::error::Error +impl Display for EncryptionKeyError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + EncryptionKeyError::InvalidPrivateKey => write!(f, "Invalid private key"), + EncryptionKeyError::InvalidPublicKey => write!(f, "Invalid public key"), + } + } +} + +impl std::error::Error for EncryptionKeyError {} + +pub struct KeyPair { + pub(crate) private_key: PrivateKey, + pub(crate) public_key: PublicKey, +} + +impl KeyPair { + pub fn new() -> Self { + let mut rng = OsRng; + Self::new_with_rng(&mut rng) + } + + pub fn new_with_rng(rng: &mut R) -> Self { + let private_key = x25519_dalek::StaticSecret::new(rng); + let public_key = (&private_key).into(); + + KeyPair { + private_key: PrivateKey(private_key), + public_key: PublicKey(public_key), + } + } + + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Ok(KeyPair { + private_key: PrivateKey::from_bytes(priv_bytes)?, + public_key: PublicKey::from_bytes(pub_bytes)?, + }) + } +} + +impl Default for KeyPair { + fn default() -> Self { + KeyPair::new() + } +} + +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = PrivateKey; + type PublicPemKey = PublicKey; + type Error = EncryptionKeyError; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Self::from_bytes(priv_bytes, pub_bytes) + } +} + +#[derive(Debug, Clone)] +pub struct PublicKey(x25519_dalek::PublicKey); + +impl PublicKey { + pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] { + *self.0.as_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + if b.len() != PUBLIC_KEY_SIZE { + return Err(EncryptionKeyError::InvalidPublicKey); + } + let mut bytes = [0; PUBLIC_KEY_SIZE]; + bytes.copy_from_slice(&b[..PUBLIC_KEY_SIZE]); + Ok(Self(x25519_dalek::PublicKey::from(bytes))) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } +} + +impl PemStorableKey for PublicKey { + fn pem_type(&self) -> String { + String::from("X25519 PUBLIC KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +#[derive(Clone)] +pub struct PrivateKey(x25519_dalek::StaticSecret); + +impl<'a> From<&'a PrivateKey> for PublicKey { + fn from(pk: &'a PrivateKey) -> Self { + PublicKey((&pk.0).into()) + } +} + +impl PrivateKey { + pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_SIZE] { + self.0.to_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + if b.len() != PRIVATE_KEY_SIZE { + return Err(EncryptionKeyError::InvalidPrivateKey); + } + let mut bytes = [0; 32]; + bytes.copy_from_slice(&b[..PRIVATE_KEY_SIZE]); + Ok(Self(x25519_dalek::StaticSecret::from(bytes))) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } + + /// Perform a key exchange with another public key + pub fn diffie_hellman(&self, remote_public: &PublicKey) -> [u8; SHARED_SECRET_SIZE] { + *self.0.diffie_hellman(&remote_public.0).as_bytes() + } +} + +impl PemStorableKey for PrivateKey { + fn pem_type(&self) -> String { + String::from("X25519 PRIVATE KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +// compatibility with sphinx keys: + +impl Into for PublicKey { + fn into(self) -> nymsphinx_types::PublicKey { + nymsphinx_types::PublicKey::from(self.to_bytes()) + } +} + +impl From for PublicKey { + fn from(pub_key: nymsphinx_types::PublicKey) -> Self { + Self(x25519_dalek::PublicKey::from(*pub_key.as_bytes())) + } +} + +impl Into for PrivateKey { + fn into(self) -> nymsphinx_types::PrivateKey { + nymsphinx_types::PrivateKey::from(self.to_bytes()) + } +} + +impl<'a> Into for &'a PrivateKey { + fn into(self) -> nymsphinx_types::PrivateKey { + nymsphinx_types::PrivateKey::from(self.to_bytes()) + } +} + +impl From for PrivateKey { + fn from(private_key: nymsphinx_types::PrivateKey) -> Self { + let private_key_bytes = private_key.to_bytes(); + assert_eq!(private_key_bytes.len(), PRIVATE_KEY_SIZE); + Self::from_bytes(&private_key_bytes).unwrap() + } +} + +#[cfg(test)] +mod sphinx_key_conversion { + use super::*; + + const NUM_ITERATIONS: usize = 100; + + #[test] + fn works_for_forward_conversion() { + for _ in 0..NUM_ITERATIONS { + let keys = KeyPair::new(); + let private = keys.private_key; + let public = keys.public_key; + + let private_bytes = private.to_bytes(); + let public_bytes = public.to_bytes(); + + let sphinx_private: nymsphinx_types::PrivateKey = private.into(); + let recovered_private = PrivateKey::from(sphinx_private); + + let sphinx_public: nymsphinx_types::PublicKey = public.into(); + let recovered_public = PublicKey::from(sphinx_public); + assert_eq!(private_bytes, recovered_private.to_bytes()); + assert_eq!(public_bytes, recovered_public.to_bytes()); + } + } + + #[test] + fn works_for_backward_conversion() { + for _ in 0..NUM_ITERATIONS { + let (sphinx_private, sphinx_public) = nymsphinx_types::crypto::keygen(); + + let private_bytes = sphinx_private.to_bytes(); + let public_bytes = sphinx_public.as_bytes(); + + let private: PrivateKey = sphinx_private.into(); + let recovered_sphinx_private: nymsphinx_types::PrivateKey = private.into(); + + let public: PublicKey = sphinx_public.into(); + let recovered_sphinx_public: nymsphinx_types::PublicKey = public.into(); + assert_eq!(private_bytes, recovered_sphinx_private.to_bytes()); + assert_eq!(public_bytes, recovered_sphinx_public.as_bytes()); + } + } +} diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs new file mode 100644 index 0000000000..a92c2fc825 --- /dev/null +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -0,0 +1,186 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::{PemStorableKey, PemStorableKeyPair}; +use bs58; +use ed25519_dalek::SignatureError; +pub use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; +use nymsphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; +use rand::{rngs::OsRng, CryptoRng, RngCore}; + +/// Keypair for usage in ed25519 EdDSA. +pub struct KeyPair { + private_key: PrivateKey, + public_key: PublicKey, +} + +impl KeyPair { + pub fn new() -> Self { + let mut rng = OsRng; + Self::new_with_rng(&mut rng) + } + + pub fn new_with_rng(rng: &mut R) -> Self { + let ed25519_keypair = ed25519_dalek::Keypair::generate(rng); + + KeyPair { + private_key: PrivateKey(ed25519_keypair.secret), + public_key: PublicKey(ed25519_keypair.public), + } + } + + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Ok(KeyPair { + private_key: PrivateKey::from_bytes(priv_bytes)?, + public_key: PublicKey::from_bytes(pub_bytes)?, + }) + } +} + +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = PrivateKey; + type PublicPemKey = PublicKey; + type Error = SignatureError; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Self::from_bytes(priv_bytes, pub_bytes) + } +} + +/// ed25519 EdDSA Public Key +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct PublicKey(ed25519_dalek::PublicKey); + +impl PublicKey { + pub fn derive_address(&self) -> DestinationAddressBytes { + let mut temporary_address = [0u8; DESTINATION_ADDRESS_LENGTH]; + let public_key_bytes = self.to_bytes(); + + assert_eq!(DESTINATION_ADDRESS_LENGTH, PUBLIC_KEY_LENGTH); + + temporary_address.copy_from_slice(&public_key_bytes[..]); + DestinationAddressBytes::from_bytes(temporary_address) + } + + /// Convert this public key to a byte array. + pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_LENGTH] { + self.0.to_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + Ok(PublicKey(ed25519_dalek::PublicKey::from_bytes(b)?)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } + + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), SignatureError> { + self.0.verify(message, &signature.0) + } +} + +impl PemStorableKey for PublicKey { + fn pem_type(&self) -> String { + String::from("ED25519 PUBLIC KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +/// ed25519 EdDSA Private Key +#[derive(Debug)] +pub struct PrivateKey(ed25519_dalek::SecretKey); + +impl<'a> From<&'a PrivateKey> for PublicKey { + fn from(pk: &'a PrivateKey) -> Self { + PublicKey((&pk.0).into()) + } +} + +impl PrivateKey { + pub fn to_bytes(&self) -> [u8; SECRET_KEY_LENGTH] { + self.0.to_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + Ok(PrivateKey(ed25519_dalek::SecretKey::from_bytes(b)?)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } + + pub fn sign(&self, message: &[u8]) -> Signature { + let expanded_secret_key = ed25519_dalek::ExpandedSecretKey::from(&self.0); + let public_key: PublicKey = self.into(); + let sig = expanded_secret_key.sign(message, &public_key.0); + Signature(sig) + } +} + +impl PemStorableKey for PrivateKey { + fn pem_type(&self) -> String { + String::from("ED25519 PRIVATE KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +#[derive(Debug)] +pub struct Signature(ed25519_dalek::Signature); + +impl Signature { + pub fn to_bytes(&self) -> [u8; SIGNATURE_LENGTH] { + self.0.to_bytes() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Signature(ed25519_dalek::Signature::from_bytes(bytes)?)) + } +} diff --git a/common/crypto/src/asymmetric/mod.rs b/common/crypto/src/asymmetric/mod.rs new file mode 100644 index 0000000000..1623a8212b --- /dev/null +++ b/common/crypto/src/asymmetric/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod encryption; +pub mod identity; diff --git a/common/crypto/src/encryption/mod.rs b/common/crypto/src/encryption/mod.rs deleted file mode 100644 index da7e548aa8..0000000000 --- a/common/crypto/src/encryption/mod.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{PemStorableKey, PemStorableKeyPair}; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; -use rand_core::OsRng; - -// TODO: ensure this is a proper name for this considering we are not implementing entire DH here - -const CURVE_GENERATOR: MontgomeryPoint = curve25519_dalek::constants::X25519_BASEPOINT; - -pub struct KeyPair { - pub(crate) private_key: PrivateKey, - pub(crate) public_key: PublicKey, -} - -impl KeyPair { - pub fn new() -> Self { - let mut rng = OsRng; - let private_key_value = Scalar::random(&mut rng); - let public_key_value = CURVE_GENERATOR * private_key_value; - - KeyPair { - private_key: PrivateKey(private_key_value), - public_key: PublicKey(public_key_value), - } - } - - pub fn private_key(&self) -> &PrivateKey { - &self.private_key - } - - pub fn public_key(&self) -> &PublicKey { - &self.public_key - } - - pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - KeyPair { - private_key: PrivateKey::from_bytes(priv_bytes), - public_key: PublicKey::from_bytes(pub_bytes), - } - } -} - -impl Default for KeyPair { - fn default() -> Self { - KeyPair::new() - } -} - -impl PemStorableKeyPair for KeyPair { - type PrivatePemKey = PrivateKey; - type PublicPemKey = PublicKey; - - fn private_key(&self) -> &Self::PrivatePemKey { - self.private_key() - } - - fn public_key(&self) -> &Self::PublicPemKey { - self.public_key() - } - - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - Self::from_bytes(priv_bytes, pub_bytes) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct PrivateKey(pub Scalar); - -impl PrivateKey { - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes().to_vec() - } - - pub fn from_bytes(b: &[u8]) -> Self { - let mut bytes = [0; 32]; - bytes.copy_from_slice(&b[..]); - // due to trait restriction we have no choice but to panic if this fails - let key = Scalar::from_canonical_bytes(bytes).unwrap(); - Self(key) - } - - pub fn inner(&self) -> Scalar { - self.0 - } -} - -impl PemStorableKey for PrivateKey { - fn pem_type(&self) -> String { - String::from("X25519 PRIVATE KEY") - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct PublicKey(pub MontgomeryPoint); - -impl<'a> From<&'a PrivateKey> for PublicKey { - fn from(pk: &'a PrivateKey) -> Self { - PublicKey(CURVE_GENERATOR * pk.0) - } -} - -impl PublicKey { - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes().to_vec() - } - - pub fn from_bytes(b: &[u8]) -> Self { - let mut bytes = [0; 32]; - bytes.copy_from_slice(&b[..]); - let key = MontgomeryPoint(bytes); - Self(key) - } - - pub fn inner(&self) -> MontgomeryPoint { - self.0 - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string(val: String) -> Self { - Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) - } -} - -impl PemStorableKey for PublicKey { - fn pem_type(&self) -> String { - String::from("X25519 PUBLIC KEY") - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} diff --git a/common/crypto/src/identity/mod.rs b/common/crypto/src/identity/mod.rs deleted file mode 100644 index 6a0b9d3ffe..0000000000 --- a/common/crypto/src/identity/mod.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::{encryption, PemStorableKey, PemStorableKeyPair}; -use bs58; -use curve25519_dalek::scalar::Scalar; -use nymsphinx::DestinationAddressBytes; - -// for time being define a dummy identity using x25519 encryption keys (as we've done so far) -// and replace it with proper keys, like ed25519 later on -#[derive(Clone)] -pub struct MixIdentityKeyPair { - pub private_key: MixIdentityPrivateKey, - pub public_key: MixIdentityPublicKey, -} - -impl MixIdentityKeyPair { - pub fn new() -> Self { - let keypair = encryption::KeyPair::new(); - MixIdentityKeyPair { - private_key: MixIdentityPrivateKey(keypair.private_key), - public_key: MixIdentityPublicKey(keypair.public_key), - } - } - - pub fn private_key(&self) -> &MixIdentityPrivateKey { - &self.private_key - } - - pub fn public_key(&self) -> &MixIdentityPublicKey { - &self.public_key - } - - pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - MixIdentityKeyPair { - private_key: MixIdentityPrivateKey::from_bytes(priv_bytes), - public_key: MixIdentityPublicKey::from_bytes(pub_bytes), - } - } -} - -impl Default for MixIdentityKeyPair { - fn default() -> Self { - MixIdentityKeyPair::new() - } -} - -impl PemStorableKeyPair for MixIdentityKeyPair { - type PrivatePemKey = MixIdentityPrivateKey; - type PublicPemKey = MixIdentityPublicKey; - - fn private_key(&self) -> &Self::PrivatePemKey { - self.private_key() - } - - fn public_key(&self) -> &Self::PublicPemKey { - self.public_key() - } - - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - Self::from_bytes(priv_bytes, pub_bytes) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MixIdentityPublicKey(encryption::PublicKey); - -impl MixIdentityPublicKey { - pub fn derive_address(&self) -> DestinationAddressBytes { - let mut temporary_address = [0u8; 32]; - let public_key_bytes = self.to_bytes(); - temporary_address.copy_from_slice(&public_key_bytes[..]); - - DestinationAddressBytes::from_bytes(temporary_address) - } - - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - pub fn from_bytes(b: &[u8]) -> Self { - Self(encryption::PublicKey::from_bytes(b)) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string(val: String) -> Self { - Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) - } -} - -impl PemStorableKey for MixIdentityPublicKey { - fn pem_type(&self) -> String { - format!("DUMMY KEY BASED ON {}", self.0.pem_type()) - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MixIdentityPrivateKey(pub encryption::PrivateKey); - -impl<'a> From<&'a MixIdentityPrivateKey> for MixIdentityPublicKey { - fn from(pk: &'a MixIdentityPrivateKey) -> Self { - let private_ref = &pk.0; - let public: encryption::PublicKey = private_ref.into(); - MixIdentityPublicKey(public) - } -} - -impl MixIdentityPrivateKey { - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - pub fn from_bytes(b: &[u8]) -> Self { - Self(encryption::PrivateKey::from_bytes(b)) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string(val: String) -> Self { - Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) - } -} - -// TODO: this will be implemented differently by using the proper trait -impl MixIdentityPrivateKey { - pub fn as_scalar(&self) -> Scalar { - let encryption_key = &self.0; - encryption_key.0 - } -} - -impl PemStorableKey for MixIdentityPrivateKey { - fn pem_type(&self) -> String { - format!("DUMMY KEY BASED ON {}", self.0.pem_type()) - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} diff --git a/common/crypto/src/kdf/blake3_hkdf.rs b/common/crypto/src/kdf/blake3_hkdf.rs new file mode 100644 index 0000000000..56aa58fdc6 --- /dev/null +++ b/common/crypto/src/kdf/blake3_hkdf.rs @@ -0,0 +1,39 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use hkdf::Hkdf; + +#[derive(Debug)] +pub enum HkdfError { + InvalidOkmLength, +} + +/// Perform HKDF `extract` then `expand` as a single step. +pub fn extract_then_expand( + salt: Option<&[u8]>, + ikm: &[u8], + info: Option<&[u8]>, + okm_length: usize, +) -> Result, HkdfError> { + // TODO: this would need to change if we ever needed the generated pseudorandom key, but + // realistically I don't see any reasons why we might need it + + let hkdf = Hkdf::::new(salt, ikm); + let mut okm = vec![0u8; okm_length]; + if let Err(_) = hkdf.expand(info.unwrap_or_else(|| &[]), &mut okm) { + return Err(HkdfError::InvalidOkmLength); + } + + Ok(okm) +} diff --git a/common/crypto/src/kdf/mod.rs b/common/crypto/src/kdf/mod.rs new file mode 100644 index 0000000000..e456e03c97 --- /dev/null +++ b/common/crypto/src/kdf/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod blake3_hkdf; diff --git a/common/crypto/src/lib.rs b/common/crypto/src/lib.rs index 52c8fa7b21..3eaa677b8c 100644 --- a/common/crypto/src/lib.rs +++ b/common/crypto/src/lib.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod encryption; -pub mod identity; +pub mod asymmetric; +pub mod kdf; +pub mod symmetric; // TODO: ideally those trait should be moved to 'pemstore' crate, however, that would cause // circular dependency. The best solution would be to remove dependency on 'crypto' from @@ -25,12 +26,13 @@ pub trait PemStorableKey { fn to_bytes(&self) -> Vec; } -pub trait PemStorableKeyPair { +pub trait PemStorableKeyPair: Sized { type PrivatePemKey: PemStorableKey; type PublicPemKey: PemStorableKey; + type Error: std::error::Error; fn private_key(&self) -> &Self::PrivatePemKey; fn public_key(&self) -> &Self::PublicPemKey; - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self; + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result; } diff --git a/common/crypto/src/symmetric/aes_ctr.rs b/common/crypto/src/symmetric/aes_ctr.rs new file mode 100644 index 0000000000..6be403b63e --- /dev/null +++ b/common/crypto/src/symmetric/aes_ctr.rs @@ -0,0 +1,138 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub use aes_ctr::stream_cipher::generic_array; +use aes_ctr::{ + stream_cipher::{ + generic_array::{ + typenum::{Unsigned, U16}, + GenericArray, + }, + NewStreamCipher, SyncStreamCipher, + }, + Aes128Ctr, +}; +use rand::{CryptoRng, RngCore}; + +// the 'U16' type is taken directly from the `Ctr128` for consistency sake +pub type Aes128KeySize = U16; +pub type Aes128NonceSize = U16; + +pub type Aes128Key = GenericArray; +pub type Aes128IV = GenericArray; + +pub fn generate_key(rng: &mut R) -> Aes128Key { + let mut ack_key = GenericArray::default(); + rng.fill_bytes(&mut ack_key); + ack_key +} + +pub fn random_iv(rng: &mut R) -> Aes128IV { + let mut iv = GenericArray::default(); + rng.fill_bytes(&mut iv); + iv +} + +pub fn zero_iv() -> Aes128IV { + GenericArray::default() +} + +pub fn iv_from_slice(b: &[u8]) -> &Aes128IV { + if b.len() != Aes128NonceSize::to_usize() { + // `from_slice` would have caused a panic about this issue anyway. + // Now we at least have slightly more information + panic!( + "Tried to convert {} bytes to IV. Expected {}", + b.len(), + Aes128NonceSize::to_usize() + ) + } + GenericArray::from_slice(b) +} + +fn apply_aes_ctr(key: &Aes128Key, iv: &Aes128IV, mut data: &mut [u8]) { + let mut cipher = Aes128Ctr::new(key, iv); + cipher.apply_keystream(&mut data) +} + +pub fn encrypt(key: &Aes128Key, iv: &Aes128IV, data: &[u8]) -> Vec { + let mut ciphertext = data.to_vec(); + apply_aes_ctr(key, iv, &mut ciphertext); + ciphertext +} + +pub fn encrypt_in_place(key: &Aes128Key, iv: &Aes128IV, data: &mut [u8]) { + apply_aes_ctr(key, iv, data) +} + +pub fn decrypt(key: &Aes128Key, iv: &Aes128IV, ciphertext: &[u8]) -> Vec { + let mut data = ciphertext.to_vec(); + apply_aes_ctr(key, iv, &mut data); + data +} + +pub fn decrypt_in_place(key: &Aes128Key, iv: &Aes128IV, ciphertext: &mut [u8]) { + apply_aes_ctr(key, iv, ciphertext) +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::rngs::OsRng; + + #[test] + fn zero_iv_is_actually_zero() { + let iv = zero_iv(); + for b in iv { + assert_eq!(b, 0); + } + } + + #[test] + fn decryption_is_reciprocal_to_encryption() { + let mut rng = OsRng; + + let arr_input = [42; 200]; + let vec_input = vec![123, 200]; + + let key1 = generate_key(&mut rng); + let key2 = generate_key(&mut rng); + + let iv1 = random_iv(&mut rng); + let iv2 = random_iv(&mut rng); + + let ciphertext1 = encrypt(&key1, &iv1, &arr_input); + let ciphertext2 = encrypt(&key2, &iv2, &vec_input); + + assert_eq!(arr_input.to_vec(), decrypt(&key1, &iv1, &ciphertext1)); + assert_eq!(vec_input, decrypt(&key2, &iv2, &ciphertext2)); + } + + #[test] + fn in_place_variants_work_same_way() { + let mut rng = OsRng; + + let mut data = [42; 200]; + let original_data = data.clone(); + + let key = generate_key(&mut rng); + let iv = random_iv(&mut rng); + + encrypt_in_place(&key, &iv, &mut data); + assert_eq!(data.to_vec(), encrypt(&key, &iv, &original_data)); + + decrypt_in_place(&key, &iv, &mut data); + assert_eq!(data.to_vec(), original_data.to_vec()); + } +} diff --git a/common/crypto/src/symmetric/mod.rs b/common/crypto/src/symmetric/mod.rs new file mode 100644 index 0000000000..a45cecf0d9 --- /dev/null +++ b/common/crypto/src/symmetric/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod aes_ctr; diff --git a/common/nymsphinx/acknowledgements/Cargo.toml b/common/nymsphinx/acknowledgements/Cargo.toml index c697fccd87..f98993d0a5 100644 --- a/common/nymsphinx/acknowledgements/Cargo.toml +++ b/common/nymsphinx/acknowledgements/Cargo.toml @@ -7,13 +7,12 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -aes-ctr = "0.4.0" rand = {version = "0.7.3", features = ["wasm-bindgen"]} - - +crypto = { path = "../../crypto" } nymsphinx-addressing = { path = "../addressing" } nymsphinx-params = { path = "../params" } nymsphinx-types = { path = "../types" } +topology = { path = "../../topology" } + -topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/acknowledgements/src/identifier.rs b/common/nymsphinx/acknowledgements/src/identifier.rs index 03ed6deedb..6d3cc940c6 100644 --- a/common/nymsphinx/acknowledgements/src/identifier.rs +++ b/common/nymsphinx/acknowledgements/src/identifier.rs @@ -12,36 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use aes_ctr::{ - stream_cipher::{ - generic_array::{ - typenum::{marker_traits::Unsigned, U16}, - GenericArray, - }, - NewStreamCipher, SyncStreamCipher, - }, - Aes128Ctr, -}; +use crypto::symmetric::aes_ctr::generic_array::typenum::Unsigned; +use crypto::symmetric::aes_ctr::{iv_from_slice, Aes128IV, Aes128Key, Aes128NonceSize}; use nymsphinx_params::packet_sizes::PacketSize; use rand::{CryptoRng, RngCore}; -// the 'U16' type is taken directly from the `Ctr128` for consistency sake -pub type Aes128KeySize = U16; -pub type Aes128NonceSize = U16; - -pub type AckAes128Key = GenericArray; -type AckAes128IV = GenericArray; +pub type AckAes128Key = Aes128Key; +type AckAes128IV = Aes128IV; pub fn generate_key(rng: &mut R) -> AckAes128Key { - let mut ack_key = GenericArray::default(); - rng.fill_bytes(&mut ack_key); - ack_key + crypto::symmetric::aes_ctr::generate_key(rng) } fn random_iv(rng: &mut R) -> AckAes128IV { - let mut iv = GenericArray::default(); - rng.fill_bytes(&mut iv); - iv + crypto::symmetric::aes_ctr::random_iv(rng) } pub fn prepare_identifier( @@ -50,30 +34,30 @@ pub fn prepare_identifier( marshaled_id: [u8; 5], ) -> Vec { let iv = random_iv(rng); - let mut cipher = Aes128Ctr::new(key, &iv); - let mut output = marshaled_id.to_vec(); + let id_ciphertext = crypto::symmetric::aes_ctr::encrypt(key, &iv, &marshaled_id); - cipher.apply_keystream(&mut output); - - iv.into_iter().chain(output.into_iter()).collect() + // IV || ID_CIPHERTEXT + iv.into_iter().chain(id_ciphertext.into_iter()).collect() } -pub fn recover_identifier(key: &AckAes128Key, iv_ciphertext: &[u8]) -> Option<[u8; 5]> { +pub fn recover_identifier(key: &AckAes128Key, iv_id_ciphertext: &[u8]) -> Option<[u8; 5]> { // first few bytes are expected to be the concatenated IV. It must be followed by at least 1 more // byte that we wish to recover, but it can be no longer from what we can physically store inside // an ack - if iv_ciphertext.len() != PacketSize::ACKPacket.plaintext_size() { + if iv_id_ciphertext.len() != PacketSize::ACKPacket.plaintext_size() { return None; } - let iv = GenericArray::from_slice(&iv_ciphertext[..Aes128NonceSize::to_usize()]); - let mut cipher = Aes128Ctr::new(key, &iv); - let mut output = iv_ciphertext[Aes128NonceSize::to_usize()..].to_vec(); - cipher.apply_keystream(&mut output); + let iv = iv_from_slice(&iv_id_ciphertext[..Aes128NonceSize::to_usize()]); + let id = crypto::symmetric::aes_ctr::decrypt( + key, + iv, + &iv_id_ciphertext[Aes128NonceSize::to_usize()..], + ); - let mut output_arr = [0u8; 5]; - output_arr.copy_from_slice(&output); - Some(output_arr) + let mut id_arr = [0u8; 5]; + id_arr.copy_from_slice(&id); + Some(id_arr) } #[cfg(test)] diff --git a/common/nymsphinx/types/Cargo.toml b/common/nymsphinx/types/Cargo.toml index 4ac06239d3..cd7c78cadb 100644 --- a/common/nymsphinx/types/Cargo.toml +++ b/common/nymsphinx/types/Cargo.toml @@ -7,5 +7,5 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sphinx = { git = "https://github.com/nymtech/sphinx", rev="cd9f09b85de33c60030ba6d7fae613c42cbe1faf" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="f31efdbf667952440bd9af5eb0c140135fd1bc4c" } #sphinx = { path = "../../../../sphinx"} \ No newline at end of file diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs index 8d16ba194c..e9877d6229 100644 --- a/common/nymsphinx/types/src/lib.rs +++ b/common/nymsphinx/types/src/lib.rs @@ -17,7 +17,7 @@ pub use sphinx::{ constants::{ self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH, }, - crypto::{public_key_from_bytes, PublicKey, SecretKey}, + crypto::{self, EphemeralSecret, PrivateKey, PublicKey, SharedSecret}, header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE}, packet::builder::{self, DEFAULT_PAYLOAD_SIZE}, payload::{Payload, PAYLOAD_OVERHEAD_SIZE}, diff --git a/common/pemstore/src/pemstore.rs b/common/pemstore/src/pemstore.rs index 1e91ad7a00..2bc33c7097 100644 --- a/common/pemstore/src/pemstore.rs +++ b/common/pemstore/src/pemstore.rs @@ -13,9 +13,8 @@ // limitations under the License. use crate::pathfinder::PathFinder; -use crypto::identity::MixIdentityKeyPair; -use crypto::PemStorableKey; -use crypto::{encryption, PemStorableKeyPair}; +use crypto::asymmetric::{encryption, identity}; +use crypto::{PemStorableKey, PemStorableKeyPair}; use log::info; use pem::{encode, parse, Pem}; use std::fs::File; @@ -23,27 +22,54 @@ use std::io; use std::io::prelude::*; use std::path::PathBuf; -pub struct PemStore { - #[allow(dead_code)] - config_dir: PathBuf, - private_mix_key_file: PathBuf, - public_mix_key_file: PathBuf, +pub struct PemStore

{ + pathfinder: P, } -impl PemStore { - pub fn new(pathfinder: P) -> PemStore { - PemStore { - config_dir: pathfinder.config_dir(), - private_mix_key_file: pathfinder.private_identity_key(), - public_mix_key_file: pathfinder.public_identity_key(), +enum KeyType { + Identity, + Encryption, +} + +impl

PemStore

{ + pub fn new(pathfinder: P) -> Self { + PemStore { pathfinder } + } + + fn get_keypair_paths(&self, key_type: KeyType) -> (PathBuf, PathBuf) + where + P: PathFinder, + { + match key_type { + KeyType::Identity => ( + self.pathfinder.private_identity_key(), + self.pathfinder.public_identity_key(), + ), + KeyType::Encryption => ( + self.pathfinder + .private_encryption_key() + .expect("tried to write encryption keypair while no path was specified"), + self.pathfinder + .public_encryption_key() + .expect("tried to write encryption keypair while no path was specified"), + ), } } - pub fn read_keypair(&self) -> io::Result { - let private_pem = self.read_pem_file(self.private_mix_key_file.clone())?; - let public_pem = self.read_pem_file(self.public_mix_key_file.clone())?; + fn read_keypair(&self, key_type: KeyType) -> io::Result + where + P: PathFinder, + T: PemStorableKeyPair, + { + let (private_key_path, public_key_path) = self.get_keypair_paths(key_type); - let key_pair = T::from_bytes(&private_pem.contents, &public_pem.contents); + let private_pem = self.read_pem_file(private_key_path)?; + let public_pem = self.read_pem_file(public_key_path)?; + + let key_pair = match T::from_bytes(&private_pem.contents, &public_pem.contents) { + Ok(key_pair) => key_pair, + Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err.to_string())), + }; if key_pair.private_key().pem_type() != private_pem.tag { return Err(io::Error::new( @@ -62,12 +88,18 @@ impl PemStore { Ok(key_pair) } - pub fn read_encryption(&self) -> io::Result { - self.read_keypair() + pub fn read_encryption_keypair(&self) -> io::Result + where + P: PathFinder, + { + self.read_keypair(KeyType::Encryption) } - pub fn read_identity(&self) -> io::Result { - self.read_keypair() + pub fn read_identity_keypair(&self) -> io::Result + where + P: PathFinder, + { + self.read_keypair(KeyType::Identity) } fn read_pem_file(&self, filepath: PathBuf) -> io::Result { @@ -77,40 +109,46 @@ impl PemStore { parse(&buf).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } - fn write_keypair(&self, key_pair: impl PemStorableKeyPair) -> io::Result<()> { + fn write_keypair(&self, key_pair: &T, key_type: KeyType) -> io::Result<()> + where + P: PathFinder, + T: PemStorableKeyPair, + { let private_key = key_pair.private_key(); let public_key = key_pair.public_key(); + let (private_key_path, public_key_path) = self.get_keypair_paths(key_type); + self.write_pem_file( - self.private_mix_key_file.clone(), + private_key_path.clone(), private_key.to_bytes(), private_key.pem_type(), )?; - info!( - "Written private key to {:?}", - self.private_mix_key_file.clone() - ); + info!("Written private key to {:?}", private_key_path); self.write_pem_file( - self.public_mix_key_file.clone(), + public_key_path.clone(), public_key.to_bytes(), public_key.pem_type(), )?; - info!( - "Written public key to {:?}", - self.public_mix_key_file.clone() - ); + info!("Written public key to {:?}", public_key_path); Ok(()) } // This should be refactored and made more generic for when we have other kinds of // KeyPairs that we want to persist (e.g. validator keypairs, or keys for // signing vs encryption). However, for the moment, it does the job. - pub fn write_identity(&self, key_pair: MixIdentityKeyPair) -> io::Result<()> { - self.write_keypair(key_pair) + pub fn write_identity_keypair(&self, key_pair: &identity::KeyPair) -> io::Result<()> + where + P: PathFinder, + { + self.write_keypair(key_pair, KeyType::Identity) } - pub fn write_encryption_keys(&self, key_pair: encryption::KeyPair) -> io::Result<()> { - self.write_keypair(key_pair) + pub fn write_encryption_keypair(&self, key_pair: &encryption::KeyPair) -> io::Result<()> + where + P: PathFinder, + { + self.write_keypair(key_pair, KeyType::Encryption) } fn write_pem_file(&self, filepath: PathBuf, data: Vec, tag: String) -> io::Result<()> { diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index d514b7ae62..26d4def744 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -28,16 +28,18 @@ pub struct Node { pub location: String, pub client_listener: String, pub mixnet_listener: SocketAddr, - pub pub_key: String, + // TODO: should we just import common/crypto and use 'proper' types for those directly? + pub identity_key: String, + pub sphinx_key: String, pub registered_clients: Vec, pub last_seen: u64, pub version: String, } impl Node { - pub fn get_pub_key_bytes(&self) -> [u8; 32] { + fn get_sphinx_key_bytes(&self) -> [u8; 32] { let mut key_bytes = [0; 32]; - bs58::decode(&self.pub_key).into(&mut key_bytes).unwrap(); + bs58::decode(&self.sphinx_key).into(&mut key_bytes).unwrap(); key_bytes } @@ -60,8 +62,8 @@ impl Into for Node { let node_address_bytes = NymNodeRoutingAddress::from(self.mixnet_listener) .try_into() .unwrap(); - let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::public_key_from_bytes(key_bytes); + let key_bytes = self.get_sphinx_key_bytes(); + let key = nymsphinx_types::PublicKey::from(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 712c0eef21..56b2f24c82 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -89,7 +89,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { let b58_address = gateway_address.to_base58_string(); self.gateways() .iter() - .find(|&gateway| gateway.pub_key == b58_address) + .find(|&gateway| gateway.identity_key == b58_address) .is_some() } @@ -102,7 +102,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { let gateway = self .gateways() .iter() - .find(|&gateway| gateway.pub_key == b58_address) + .find(|&gateway| gateway.identity_key == b58_address) .ok_or_else(|| NymTopologyError::NonExistentGatewayError)? .clone(); diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 588938c69b..3bb5e0caf6 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -46,7 +46,7 @@ impl Into for Node { fn into(self) -> SphinxNode { let node_address_bytes = NymNodeRoutingAddress::from(self.host).try_into().unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::public_key_from_bytes(key_bytes); + let key = nymsphinx_types::PublicKey::from(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/provider.rs b/common/topology/src/provider.rs index de1a722a97..93630b0058 100644 --- a/common/topology/src/provider.rs +++ b/common/topology/src/provider.rs @@ -54,7 +54,7 @@ impl Into for Node { .try_into() .unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::public_key_from_bytes(key_bytes); + let key = nymsphinx_types::PublicKey::from(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 8454722fd5..9c14c11f8b 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -12,12 +12,10 @@ clap = "2.33.0" dirs = "2.0.2" dotenv = "0.15.0" futures = "0.3" -hmac = "0.7.1" log = "0.4" pretty_env_logger = "0.3" rand = "0.7.2" serde = { version = "1.0.104", features = ["derive"] } -sha2 = "0.8.1" sled = "0.31" tokio = { version = "0.2", features = ["full"] } tokio-util = { version = "0.3.1", features = ["codec"] } diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml index 9948964040..79dcaf3ffb 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/gateway/gateway-requests/Cargo.toml @@ -8,7 +8,13 @@ edition = "2018" [dependencies] bs58 = "0.3" -nymsphinx = { path = "../../common/nymsphinx" } serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" tokio-tungstenite = "0.10.1" + +nymsphinx = { path = "../../common/nymsphinx" } + +futures = "0.3" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} +crypto = { path = "../../common/crypto" } +log = "0.4" diff --git a/gateway/gateway-requests/src/auth_token.rs b/gateway/gateway-requests/src/auth_token.rs deleted file mode 100644 index 92768c1dbe..0000000000 --- a/gateway/gateway-requests/src/auth_token.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub const AUTH_TOKEN_SIZE: usize = 32; - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub struct AuthToken([u8; AUTH_TOKEN_SIZE]); - -#[derive(Debug)] -pub enum AuthTokenConversionError { - InvalidStringError, - StringOfInvalidLengthError, -} - -impl AuthToken { - pub fn from_bytes(bytes: [u8; AUTH_TOKEN_SIZE]) -> Self { - AuthToken(bytes) - } - - pub fn to_bytes(&self) -> [u8; AUTH_TOKEN_SIZE] { - self.0 - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn try_from_base58_string>( - val: S, - ) -> Result { - let decoded = match bs58::decode(val.into()).into_vec() { - Ok(decoded) => decoded, - Err(_) => return Err(AuthTokenConversionError::InvalidStringError), - }; - - if decoded.len() != AUTH_TOKEN_SIZE { - return Err(AuthTokenConversionError::StringOfInvalidLengthError); - } - - let mut token = [0u8; AUTH_TOKEN_SIZE]; - token.copy_from_slice(&decoded[..]); - Ok(AuthToken(token)) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(self.0).into_string() - } -} - -impl Into for AuthToken { - fn into(self) -> String { - self.to_base58_string() - } -} - -#[cfg(test)] -mod auth_token_conversion { - use super::*; - - #[test] - fn it_is_possible_to_recover_it_from_valid_b58_string() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - assert_eq!( - auth_token, - AuthToken::try_from_base58_string(auth_token_string).unwrap() - ) - } - - #[test] - fn it_is_possible_to_recover_it_from_valid_b58_str_ref() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - let auth_token_str_ref: &str = &auth_token_string; - assert_eq!( - auth_token, - AuthToken::try_from_base58_string(auth_token_str_ref).unwrap() - ) - } - - #[test] - fn it_returns_error_on_b58_with_invalid_characters() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - - let mut chars = auth_token_string.chars(); - let _consumed_first_char = chars.next().unwrap(); - - let invalid_chars_token = "=".to_string() + chars.as_str(); - assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) - } - - #[test] - fn it_returns_error_on_too_long_b58_string() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let mut auth_token_string = auth_token.to_base58_string(); - auth_token_string.push('f'); - - assert!(AuthToken::try_from_base58_string(auth_token_string).is_err()) - } - - #[test] - fn it_returns_error_on_too_short_b58_string() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - - let mut chars = auth_token_string.chars(); - let _consumed_first_char = chars.next().unwrap(); - let _consumed_second_char = chars.next().unwrap(); - let invalid_chars_token = chars.as_str(); - - assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) - } -} diff --git a/gateway/gateway-requests/src/authentication/encrypted_address.rs b/gateway/gateway-requests/src/authentication/encrypted_address.rs new file mode 100644 index 0000000000..358f056701 --- /dev/null +++ b/gateway/gateway-requests/src/authentication/encrypted_address.rs @@ -0,0 +1,91 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::authentication::iv::AuthenticationIV; +use crate::registration::handshake::shared_key::SharedKey; +use crypto::symmetric::aes_ctr; +use nymsphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; + +pub const ENCRYPTED_ADDRESS_SIZE: usize = DESTINATION_ADDRESS_LENGTH; + +/// Replacement for what used to be an 'AuthToken'. We used to be generating an 'AuthToken' based on +/// local secret and remote address in order to allow for authentication. Due to changes in registration +/// and the fact we are deriving a shared key, we are encrypting remote's address with the previously +/// derived shared key. If the value is as expected, then authentication is successful. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct EncryptedAddressBytes([u8; ENCRYPTED_ADDRESS_SIZE]); + +#[derive(Debug)] +pub enum EncryptedAddressConversionError { + DecodeError(bs58::decode::Error), + StringOfInvalidLengthError, +} + +impl EncryptedAddressBytes { + pub fn new(address: &DestinationAddressBytes, key: &SharedKey, iv: &AuthenticationIV) -> Self { + let ciphertext = aes_ctr::encrypt(key, iv, address.as_bytes()); + + let mut enc_address = [0u8; ENCRYPTED_ADDRESS_SIZE]; + enc_address.copy_from_slice(&ciphertext[..]); + EncryptedAddressBytes(enc_address) + } + + pub fn verify( + &self, + address: &DestinationAddressBytes, + key: &SharedKey, + iv: &AuthenticationIV, + ) -> bool { + self == &Self::new(address, key, iv) + } + + pub fn from_bytes(bytes: [u8; ENCRYPTED_ADDRESS_SIZE]) -> Self { + EncryptedAddressBytes(bytes) + } + + pub fn to_bytes(&self) -> [u8; ENCRYPTED_ADDRESS_SIZE] { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(err) => return Err(EncryptedAddressConversionError::DecodeError(err)), + }; + + if decoded.len() != ENCRYPTED_ADDRESS_SIZE { + return Err(EncryptedAddressConversionError::StringOfInvalidLengthError); + } + + let mut enc_address = [0u8; ENCRYPTED_ADDRESS_SIZE]; + enc_address.copy_from_slice(&decoded[..]); + Ok(EncryptedAddressBytes(enc_address)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.0).into_string() + } +} + +impl Into for EncryptedAddressBytes { + fn into(self) -> String { + self.to_base58_string() + } +} diff --git a/gateway/gateway-requests/src/authentication/iv.rs b/gateway/gateway-requests/src/authentication/iv.rs new file mode 100644 index 0000000000..2ae4b85ac3 --- /dev/null +++ b/gateway/gateway-requests/src/authentication/iv.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crypto::symmetric::aes_ctr::{ + generic_array::{typenum::Unsigned, GenericArray}, + random_iv, Aes128IV, Aes128NonceSize, +}; +use rand::{CryptoRng, RngCore}; +use std::ops::Deref; + +pub struct AuthenticationIV(Aes128IV); + +#[derive(Debug)] +pub enum IVConversionError { + DecodeError(bs58::decode::Error), + BytesOfInvalidLengthError, + StringOfInvalidLengthError, +} + +impl AuthenticationIV { + pub fn new_random(rng: &mut R) -> Self { + AuthenticationIV(random_iv(rng)) + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Aes128NonceSize::to_usize() { + return Err(IVConversionError::BytesOfInvalidLengthError); + } + + Ok(AuthenticationIV(GenericArray::clone_from_slice(bytes))) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_vec() + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_ref() + } + + pub fn try_from_base58_string>(val: S) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(err) => return Err(IVConversionError::DecodeError(err)), + }; + + if decoded.len() != Aes128NonceSize::to_usize() { + return Err(IVConversionError::StringOfInvalidLengthError); + } + + Ok(AuthenticationIV( + GenericArray::from_exact_iter(decoded).expect("Invalid vector length!"), + )) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } +} + +impl Into for AuthenticationIV { + fn into(self) -> String { + self.to_base58_string() + } +} + +impl Deref for AuthenticationIV { + type Target = Aes128IV; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// I don't see any cases in which DerefMut would be useful. So did not implement it. diff --git a/gateway/gateway-requests/src/authentication/mod.rs b/gateway/gateway-requests/src/authentication/mod.rs new file mode 100644 index 0000000000..e1e06430d4 --- /dev/null +++ b/gateway/gateway-requests/src/authentication/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod encrypted_address; +pub mod iv; diff --git a/gateway/gateway-requests/src/lib.rs b/gateway/gateway-requests/src/lib.rs index f437b47ddf..42cc0062bd 100644 --- a/gateway/gateway-requests/src/lib.rs +++ b/gateway/gateway-requests/src/lib.rs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod auth_token; +pub mod authentication; +pub mod registration; pub mod types; pub const DUMMY_MESSAGE_CONTENT: &[u8] = b"[DUMMY MESSAGE] Wanting something does not give you the right to have it."; -pub use auth_token::AuthToken; +pub use crypto::symmetric::aes_ctr::generic_array; + pub use types::*; diff --git a/gateway/gateway-requests/src/registration/handshake/client.rs b/gateway/gateway-requests/src/registration/handshake/client.rs new file mode 100644 index 0000000000..7c11b0b85c --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/client.rs @@ -0,0 +1,130 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::registration::handshake::shared_key::SharedKey; +use crate::registration::handshake::state::State; +use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crypto::asymmetric::encryption::PUBLIC_KEY_SIZE; +use crypto::asymmetric::identity::SIGNATURE_LENGTH; +use crypto::asymmetric::{encryption, identity}; +use futures::future::BoxFuture; +use futures::task::{Context, Poll}; +use futures::{Future, Sink, Stream}; +use rand::{CryptoRng, RngCore}; +use std::pin::Pin; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +pub(crate) struct ClientHandshake<'a> { + handshake_future: BoxFuture<'a, Result>, +} + +impl<'a> ClientHandshake<'a> { + pub(crate) fn new( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a crypto::asymmetric::identity::KeyPair, + gateway_pubkey: identity::PublicKey, + ) -> Self + where + S: Stream + Sink + Unpin + Send + 'a, + { + let mut state = State::new(rng, ws_stream, identity, Some(gateway_pubkey)); + + ClientHandshake { + handshake_future: Box::pin(async move { + // If any step along the way failed (that are non-network related), + // try to send 'error' message to the remote + // party to indicate handshake should be terminated + pub(crate) async fn check_processing_error( + result: Result, + state: &mut State<'_, S>, + ) -> Result + where + S: Sink + Unpin, + { + match result { + Ok(ok) => Ok(ok), + Err(err) => { + state.send_handshake_error(err.to_string()).await?; + Err(err) + } + } + } + + let init_message = state.init_message(); + state.send_handshake_data(init_message).await?; + + // <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) + let mid_res = state.receive_handshake_message().await?; + let (remote_ephemeral_key, remote_key_material) = + check_processing_error(Self::parse_mid_response(mid_res), &mut state).await?; + + // hkdf::::(g^xy) + state.derive_shared_key(&remote_ephemeral_key); + let verification_res = + state.verify_remote_key_material(&remote_key_material, &remote_ephemeral_key); + check_processing_error(verification_res, &mut state).await?; + + // AES(k, sig(client_priv, (g^y || g^x)) + let material = state.prepare_key_material_sig(&remote_ephemeral_key); + + // -> AES(k, sig(client_priv, g^x || g^y)) + state.send_handshake_data(material).await?; + + // <- Ok + let finalization = state.receive_handshake_message().await?; + check_processing_error(Self::parse_finalization_response(finalization), &mut state) + .await?; + Ok(state.finalize_handshake()) + }), + } + } + + // client should have received + // G^y || AES(k, SIG(PRIV_GATE, G^y || G^x)) + fn parse_mid_response( + mut resp: Vec, + ) -> Result<(encryption::PublicKey, Vec), HandshakeError> { + if resp.len() != PUBLIC_KEY_SIZE + SIGNATURE_LENGTH { + return Err(HandshakeError::MalformedResponse); + } + + let remote_key_material = resp.split_off(PUBLIC_KEY_SIZE); + // this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE + // which is impossible + let remote_ephemeral_key = encryption::PublicKey::from_bytes(&resp).unwrap(); + Ok((remote_ephemeral_key, remote_key_material)) + } + + fn parse_finalization_response(resp: Vec) -> Result<(), HandshakeError> { + if resp.len() != 1 { + return Err(HandshakeError::MalformedResponse); + } + if resp[0] == 1 { + Ok(()) + } else if resp[0] == 0 { + Err(HandshakeError::HandshakeFailure) + } else { + Err(HandshakeError::MalformedResponse) + } + } +} + +impl<'a> Future for ClientHandshake<'a> { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.handshake_future).poll(cx) + } +} diff --git a/gateway/gateway-requests/src/registration/handshake/error.rs b/gateway/gateway-requests/src/registration/handshake/error.rs new file mode 100644 index 0000000000..d4be0f3be8 --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/error.rs @@ -0,0 +1,52 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crypto::asymmetric::identity; +use std::fmt::{self, Display, Formatter}; + +#[derive(Debug)] +pub enum HandshakeError { + KeyMaterialOfInvalidSize(usize), + InvalidSignature, + NetworkError, + ClosedStream, + RemoteError(String), + MalformedResponse, + MalformedRequest, + HandshakeFailure, +} + +impl Display for HandshakeError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + HandshakeError::KeyMaterialOfInvalidSize(received) => write!( + f, + "received key material of invalid length - {}. Expected: {}", + received, + identity::SIGNATURE_LENGTH + ), + HandshakeError::InvalidSignature => write!(f, "received invalid signature"), + HandshakeError::NetworkError => write!(f, "encountered network error"), + HandshakeError::ClosedStream => { + write!(f, "the stream was closed before completing handshake") + } + HandshakeError::RemoteError(err) => write!(f, "error on the remote: {}", err), + HandshakeError::MalformedResponse => write!(f, "received response was malformed:"), + HandshakeError::MalformedRequest => write!(f, "sent request was malformed"), + HandshakeError::HandshakeFailure => write!(f, "unknown handshake failure"), + } + } +} + +impl std::error::Error for HandshakeError {} diff --git a/gateway/gateway-requests/src/registration/handshake/gateway.rs b/gateway/gateway-requests/src/registration/handshake/gateway.rs new file mode 100644 index 0000000000..5ebec6a4fd --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/gateway.rs @@ -0,0 +1,123 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::registration::handshake::shared_key::SharedKey; +use crate::registration::handshake::state::State; +use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crypto::asymmetric::encryption; +use futures::future::BoxFuture; +use futures::task::{Context, Poll}; +use futures::{Future, Sink, Stream}; +use rand::{CryptoRng, RngCore}; +use std::pin::Pin; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +pub(crate) struct GatewayHandshake<'a> { + handshake_future: BoxFuture<'a, Result>, +} + +impl<'a> GatewayHandshake<'a> { + pub(crate) fn new( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a crypto::asymmetric::identity::KeyPair, + received_init_payload: Vec, + ) -> Self + where + S: Stream + Sink + Unpin + Send + 'a, + { + let mut state = State::new(rng, ws_stream, identity, None); + GatewayHandshake { + handshake_future: Box::pin(async move { + // If any step along the way failed (that are non-network related), + // try to send 'error' message to the remote + // party to indicate handshake should be terminated + pub(crate) async fn check_processing_error( + result: Result, + state: &mut State<'_, S>, + ) -> Result + where + S: Sink + Unpin, + { + match result { + Ok(ok) => Ok(ok), + Err(err) => { + state.send_handshake_error(err.to_string()).await?; + Err(err) + } + } + } + + // init: <- pub_key || g^x + let (remote_identity, remote_ephemeral_key) = check_processing_error( + State::::parse_init_message(received_init_payload), + &mut state, + ) + .await?; + state.update_remote_identity(remote_identity); + + // hkdf::::(g^xy) + state.derive_shared_key(&remote_ephemeral_key); + + // AES(k, sig(gate_priv, (g^y || g^x)) + let material = state.prepare_key_material_sig(&remote_ephemeral_key); + + // g^y || AES(k, sig(gate_priv, (g^y || g^x)) + let handshake_payload = Self::combine_material_with_ephemeral_key( + state.local_ephemeral_key(), + material, + ); + + // -> g^y || AES(k, sig(gate_priv, (g^y || g^x)) + state.send_handshake_data(handshake_payload).await?; + + // <- AES(k, sig(client_priv, g^x || g^y)) + let remote_key_material = state.receive_handshake_message().await?; + let verification_res = + state.verify_remote_key_material(&remote_key_material, &remote_ephemeral_key); + check_processing_error(verification_res, &mut state).await?; + let finalizer = Self::prepare_finalization_response(); + + // -> Ok + state.send_handshake_data(finalizer).await?; + Ok(state.finalize_handshake()) + }), + } + } + + // create g^y || AES(k, sig(gate_priv, (g^y || g^x)) + fn combine_material_with_ephemeral_key( + ephemeral_key: &encryption::PublicKey, + material: Vec, + ) -> Vec { + ephemeral_key + .to_bytes() + .iter() + .cloned() + .chain(material.into_iter()) + .collect() + } + + fn prepare_finalization_response() -> Vec { + vec![1] + } +} + +impl<'a> Future for GatewayHandshake<'a> { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.handshake_future).poll(cx) + } +} diff --git a/gateway/gateway-requests/src/registration/handshake/mod.rs b/gateway/gateway-requests/src/registration/handshake/mod.rs new file mode 100644 index 0000000000..a317dd87bb --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/mod.rs @@ -0,0 +1,81 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use self::client::ClientHandshake; +use self::error::HandshakeError; +use self::gateway::GatewayHandshake; +pub use self::shared_key::{SharedKey, SharedKeySize}; +use crypto::asymmetric::identity; +use futures::{Sink, Stream}; +use rand::rngs::OsRng; +use rand::{CryptoRng, RngCore}; +use tokio_tungstenite::tungstenite::{Error as WsError, Message as WsMessage}; + +// for ease of use +pub const DEFAULT_RNG: OsRng = OsRng; + +pub(crate) type WsItem = Result; + +mod client; +pub mod error; +mod gateway; +pub mod shared_key; +mod state; + +// Note: the handshake is built on top of WebSocket, but in principle it shouldn't be too difficult +// to remove that restriction, by just changing Sink and Stream into +// AsyncWrite and AsyncRead and slightly adjusting the implementation. But right now +// we do not need to worry about that. + +pub async fn client_handshake<'a, S>( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a identity::KeyPair, + gateway_pubkey: identity::PublicKey, +) -> Result +where + S: Stream + Sink + Unpin + Send + 'a, +{ + ClientHandshake::new(rng, ws_stream, identity, gateway_pubkey).await +} + +pub async fn gateway_handshake<'a, S>( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a identity::KeyPair, + received_init_payload: Vec, +) -> Result +where + S: Stream + Sink + Unpin + Send + 'a, +{ + GatewayHandshake::new(rng, ws_stream, identity, received_init_payload).await +} + +/* + +Messages exchanged: + +CLIENT -> GATEWAY: +CLIENT_ID_KEY || G^x + +GATEWAY -> CLIENT +G^y || AES(k, SIG(PRIV_G, G^y || G^x)) + +CLIENT -> GATEWAY +AES(k, SIG(PRIV_C, G^x || G^y)) + +GATEWAY -> CLIENT +DONE(status) + +*/ diff --git a/gateway/gateway-requests/src/registration/handshake/shared_key.rs b/gateway/gateway-requests/src/registration/handshake/shared_key.rs new file mode 100644 index 0000000000..808f01a7fa --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/shared_key.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crypto::symmetric::aes_ctr::{ + generic_array::{typenum::Unsigned, GenericArray}, + Aes128Key, Aes128KeySize, +}; +use std::ops::Deref; + +pub type SharedKeySize = Aes128KeySize; + +#[derive(Clone, Debug)] +pub struct SharedKey(Aes128Key); + +#[derive(Debug)] +pub enum SharedKeyConversionError { + DecodeError(bs58::decode::Error), + BytesOfInvalidLengthError, + StringOfInvalidLengthError, +} + +impl SharedKey { + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != SharedKeySize::to_usize() { + return Err(SharedKeyConversionError::BytesOfInvalidLengthError); + } + + Ok(SharedKey(GenericArray::clone_from_slice(bytes))) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_vec() + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_ref() + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(err) => return Err(SharedKeyConversionError::DecodeError(err)), + }; + + if decoded.len() != SharedKeySize::to_usize() { + return Err(SharedKeyConversionError::StringOfInvalidLengthError); + } + + Ok(SharedKey( + GenericArray::from_exact_iter(decoded).expect("Invalid vector length!"), + )) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } +} + +impl Into for SharedKey { + fn into(self) -> String { + self.to_base58_string() + } +} + +impl Deref for SharedKey { + type Target = Aes128Key; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// I don't see any cases in which DerefMut would be useful. So did not implement it. diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs new file mode 100644 index 0000000000..fde4f9a9f5 --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -0,0 +1,256 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::registration::handshake::error::HandshakeError; +use crate::registration::handshake::shared_key::{SharedKey, SharedKeySize}; +use crate::registration::handshake::WsItem; +use crate::types; +use crypto::{ + asymmetric::{encryption, identity}, + kdf::blake3_hkdf, + symmetric::aes_ctr::{self, generic_array::typenum::Unsigned}, +}; +use futures::{Sink, SinkExt, Stream, StreamExt}; +use log::*; +use rand::{CryptoRng, RngCore}; +use std::convert::{TryFrom, TryInto}; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +/// Handshake state. +pub(crate) struct State<'a, S> { + /// The underlying WebSocket stream. + ws_stream: &'a mut S, + /// Identity of the local "node" (client or gateway) which is used + /// during the handshake. + identity: &'a identity::KeyPair, + /// Local ephemeral Diffie-Hellman keypair generated as a part of the handshake. + ephemeral_keypair: encryption::KeyPair, + /// The derived shared key using the ephemeral keys of both parties. + derived_shared_key: Option, + /// The known or received public identity key of the remote. + /// Ideally it would always be known before the handshake was initiated. + remote_pubkey: Option, +} + +impl<'a, S> State<'a, S> { + pub(crate) fn new( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a identity::KeyPair, + remote_pubkey: Option, + ) -> Self { + let ephemeral_keypair = encryption::KeyPair::new_with_rng(rng); + State { + ws_stream, + ephemeral_keypair, + identity, + remote_pubkey, + derived_shared_key: None, + } + } + + pub(crate) fn local_ephemeral_key(&self) -> &encryption::PublicKey { + self.ephemeral_keypair.public_key() + } + + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY + // Eventually the ID_PUBKEY prefix will get removed and recipient will know + // initializer's identity from another source. + pub(crate) fn init_message(&self) -> Vec { + self.identity + .public_key() + .to_bytes() + .iter() + .cloned() + .chain( + self.ephemeral_keypair + .public_key() + .to_bytes() + .iter() + .cloned(), + ) + .collect() + } + + // this will need to be adjusted when REMOTE_ID_PUBKEY is removed + pub(crate) fn parse_init_message( + mut init_message: Vec, + ) -> Result<(identity::PublicKey, encryption::PublicKey), HandshakeError> { + if init_message.len() != identity::PUBLIC_KEY_LENGTH + encryption::PUBLIC_KEY_SIZE { + return Err(HandshakeError::MalformedRequest); + } + + let remote_ephemeral_key_bytes = init_message.split_off(identity::PUBLIC_KEY_LENGTH); + // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE + // which is impossible + let remote_ephemeral_key = + encryption::PublicKey::from_bytes(&remote_ephemeral_key_bytes).unwrap(); + + // this could actually fail if the curve point fails to get decompressed + let remote_identity = identity::PublicKey::from_bytes(&init_message) + .map_err(|_| HandshakeError::MalformedRequest)?; + + Ok((remote_identity, remote_ephemeral_key)) + } + + pub(crate) fn derive_shared_key(&mut self, remote_ephemeral_key: &encryption::PublicKey) { + let dh_result = self + .ephemeral_keypair + .private_key() + .diffie_hellman(remote_ephemeral_key); + + // there is no reason for this to fail as our okm is expected to be only 16 bytes + let okm = + blake3_hkdf::extract_then_expand(None, &dh_result, None, SharedKeySize::to_usize()) + .expect("somehow too long okm was provided"); + + let derived_shared_key = + SharedKey::try_from_bytes(&okm).expect("okm was expanded to incorrect length!"); + + self.derived_shared_key = Some(derived_shared_key) + } + + // produces AES(k, SIG(ID_PRIV, G^x || G^y), + // assuming x is local and y is remote + pub(crate) fn prepare_key_material_sig( + &self, + remote_ephemeral_key: &encryption::PublicKey, + ) -> Vec { + let message: Vec<_> = self + .ephemeral_keypair + .public_key() + .to_bytes() + .iter() + .cloned() + .chain(remote_ephemeral_key.to_bytes().iter().cloned()) + .collect(); + + let signature = self.identity.private_key().sign(&message); + aes_ctr::encrypt( + self.derived_shared_key.as_ref().unwrap(), + &aes_ctr::zero_iv(), + &signature.to_bytes(), + ) + } + + // must be called after shared key was derived locally and remote's identity is known + pub(crate) fn verify_remote_key_material( + &self, + remote_material: &[u8], + remote_ephemeral_key: &encryption::PublicKey, + ) -> Result<(), HandshakeError> { + if remote_material.len() != identity::SIGNATURE_LENGTH { + return Err(HandshakeError::KeyMaterialOfInvalidSize( + remote_material.len(), + )); + } + let derived_shared_key = self + .derived_shared_key + .as_ref() + .expect("shared key was not derived!"); + + // first decrypt received data + let decrypted_signature = + aes_ctr::decrypt(derived_shared_key, &aes_ctr::zero_iv(), remote_material); + + // now verify signature itself + let signature = identity::Signature::from_bytes(&decrypted_signature) + .map_err(|_| HandshakeError::InvalidSignature)?; + + // g^y || g^x, if y is remote and x is local + let signed_payload: Vec<_> = remote_ephemeral_key + .to_bytes() + .iter() + .cloned() + .chain( + self.ephemeral_keypair + .public_key() + .to_bytes() + .iter() + .cloned(), + ) + .collect(); + + self.remote_pubkey + .as_ref() + .unwrap() + .verify(&signed_payload, &signature) + .map_err(|_| HandshakeError::InvalidSignature) + } + + pub(crate) fn update_remote_identity(&mut self, remote_pubkey: identity::PublicKey) { + self.remote_pubkey = Some(remote_pubkey) + } + + pub(crate) async fn receive_handshake_message(&mut self) -> Result, HandshakeError> + where + S: Stream + Unpin, + { + loop { + if let Some(msg) = self.ws_stream.next().await { + if let Ok(msg) = msg { + match msg { + WsMessage::Text(ws_msg) => match types::RegistrationHandshake::try_from(ws_msg) { + Ok(reg_handshake_msg) => return match reg_handshake_msg { + types::RegistrationHandshake::HandshakePayload { data } => Ok(data), + types::RegistrationHandshake::HandshakeError { message } => Err(HandshakeError::RemoteError(message)), + }, + Err(_) => error!("Received a non-handshake message during the registration handshake! It's getting dropped."), + }, + _ => error!("Received non-text message during registration handshake"), + } + } else { + return Err(HandshakeError::NetworkError); + } + } else { + return Err(HandshakeError::ClosedStream); + } + } + } + + // upon receiving this, the receiver should terminate the handshake + pub(crate) async fn send_handshake_error>( + &mut self, + message: M, + ) -> Result<(), HandshakeError> + where + S: Sink + Unpin, + { + let handshake_message = types::RegistrationHandshake::new_error(message); + self.ws_stream + .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .await + .map_err(|_| HandshakeError::ClosedStream) + } + + pub(crate) async fn send_handshake_data( + &mut self, + payload: Vec, + ) -> Result<(), HandshakeError> + where + S: Sink + Unpin, + { + let handshake_message = types::RegistrationHandshake::new_payload(payload); + self.ws_stream + .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .await + .map_err(|_| HandshakeError::ClosedStream) + } + + /// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed + /// values. + pub(crate) fn finalize_handshake(self) -> SharedKey { + self.derived_shared_key.unwrap() + } +} diff --git a/gateway/gateway-requests/src/registration/mod.rs b/gateway/gateway-requests/src/registration/mod.rs new file mode 100644 index 0000000000..a4a154f8b9 --- /dev/null +++ b/gateway/gateway-requests/src/registration/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2020 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod handshake; + +// TODO: is it perhaps possible to replace the 'custom' handshake with an existing +// implementation like with one of the variants on the Noise framework? + +// Right now it's based on the STS (Station-to-Station) Protocol. diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 1a5a221743..bce58010c9 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::auth_token::AuthToken; +use crate::authentication::encrypted_address::EncryptedAddressBytes; +use crate::authentication::iv::AuthenticationIV; +use crate::registration::handshake::SharedKey; +use crypto::symmetric::aes_ctr; use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; use nymsphinx::params::packet_sizes::PacketSize; use nymsphinx::{DestinationAddressBytes, SphinxPacket}; @@ -24,11 +27,47 @@ use std::{ }; use tokio_tungstenite::tungstenite::protocol::Message; +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum RegistrationHandshake { + HandshakePayload { data: Vec }, + HandshakeError { message: String }, +} + +impl RegistrationHandshake { + pub fn new_payload(data: Vec) -> Self { + RegistrationHandshake::HandshakePayload { data } + } + + pub fn new_error>(message: S) -> Self { + RegistrationHandshake::HandshakeError { + message: message.into(), + } + } +} + +impl TryFrom for RegistrationHandshake { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + serde_json::from_str(&msg) + } +} + +impl TryInto for RegistrationHandshake { + type Error = serde_json::Error; + + fn try_into(self) -> Result { + serde_json::to_string(&self) + } +} + #[derive(Debug)] pub enum GatewayRequestsError { IncorrectlyEncodedAddress, RequestOfInvalidSize(usize), MalformedSphinxPacket, + MalformedEncryption, } // to use it as `std::error::Error`, and we don't want to just derive is because we want @@ -44,6 +83,7 @@ impl fmt::Display for GatewayRequestsError { actual, PacketSize::ACKPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), + MalformedEncryption => write!(f, "the received encrypted data was malformed"), } } } @@ -57,21 +97,25 @@ impl From for GatewayRequestsError { #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] pub enum ClientControlRequest { - Authenticate { address: String, token: String }, - Register { address: String }, + Authenticate { + address: String, + enc_address: String, + iv: String, + }, + #[serde(alias = "handshakePayload")] + RegisterHandshakeInitRequest { data: Vec }, } impl ClientControlRequest { - pub fn new_authenticate(address: DestinationAddressBytes, token: AuthToken) -> Self { + pub fn new_authenticate( + address: DestinationAddressBytes, + enc_address: EncryptedAddressBytes, + iv: AuthenticationIV, + ) -> Self { ClientControlRequest::Authenticate { address: address.to_base58_string(), - token: token.to_base58_string(), - } - } - - pub fn new_register(address: DestinationAddressBytes) -> Self { - ClientControlRequest::Register { - address: address.to_base58_string(), + enc_address: enc_address.to_base58_string(), + iv: iv.to_base58_string(), } } } @@ -105,7 +149,7 @@ impl TryInto for ClientControlRequest { #[serde(tag = "type", rename_all = "camelCase")] pub enum ServerResponse { Authenticate { status: bool }, - Register { token: String }, + Register { status: bool }, Send { status: bool }, Error { message: String }, } @@ -127,7 +171,7 @@ impl ServerResponse { pub fn implies_successful_authentication(&self) -> bool { match self { ServerResponse::Authenticate { status, .. } => *status, - ServerResponse::Register { .. } => true, + ServerResponse::Register { status, .. } => *status, _ => false, } } @@ -157,20 +201,38 @@ pub enum BinaryRequest { }, } +// TODO: ask @AP if that's sufficient +const PADDING_LEN: usize = 16; + +// Right now the only valid `BinaryRequest` is a request to forward a sphinx packet. +// It is encrypted using the derived shared key between client and the gateway. Thanks to +// randomness inside the sphinx packet themselves (even via the same route), the 0s IV can be used here. +// HOWEVER, NOTE: If we introduced another 'BinaryRequest', we must carefully examine if a 0s IV +// would work there. impl BinaryRequest { - pub fn try_from_bytes(raw_req: &[u8]) -> Result { + pub fn try_from_encrypted_bytes( + mut raw_req: Vec, + shared_key: &SharedKey, + ) -> Result { + aes_ctr::decrypt_in_place(shared_key, &aes_ctr::zero_iv(), &mut raw_req); + // see if the padding is retained + if !raw_req.iter().rev().take(PADDING_LEN).all(|&x| x == 0) { + return Err(GatewayRequestsError::MalformedEncryption); + } + // right now there's only a single option possible which significantly simplifies the logic // if we decided to allow for more 'binary' messages, the API wouldn't need to change let address = NymNodeRoutingAddress::try_from_bytes(&raw_req)?; let addr_offset = address.bytes_min_len(); - let packet_size = raw_req[addr_offset..].len(); + let sphinx_packet_data = &raw_req[addr_offset..raw_req.len() - PADDING_LEN]; + let packet_size = sphinx_packet_data.len(); if let Err(_) = PacketSize::get_type(packet_size) { // TODO: should this allow AckPacket sizes? Err(GatewayRequestsError::RequestOfInvalidSize(packet_size)) } else { - let sphinx_packet = match SphinxPacket::from_bytes(&raw_req[addr_offset..]) { + let sphinx_packet = match SphinxPacket::from_bytes(sphinx_packet_data) { Ok(packet) => packet, Err(_) => return Err(GatewayRequestsError::MalformedSphinxPacket), }; @@ -182,21 +244,27 @@ impl BinaryRequest { } } - pub fn into_bytes(self) -> Vec { + pub fn into_encrypted_bytes(self, shared_key: &SharedKey) -> Vec { match self { BinaryRequest::ForwardSphinx { address, sphinx_packet, } => { // TODO: using intermediate `NymNodeRoutingAddress` here is just temporary, because - // it happens to do exactly what we needed, but we don't really want to be + // it happens to do exactly what we needed, but we really don't want to be // dependant on what it does let wrapped_address = NymNodeRoutingAddress::from(address); - wrapped_address + + // add 16 bytes of padding so that the gateway could catch incorrect encryption + let mut gateway_data: Vec<_> = wrapped_address .as_bytes() .into_iter() .chain(sphinx_packet.to_bytes().into_iter()) - .collect() + .chain(std::iter::repeat(0).take(PADDING_LEN)) + .collect(); + + aes_ctr::encrypt_in_place(shared_key, &aes_ctr::zero_iv(), &mut gateway_data); + gateway_data } } } @@ -208,12 +276,30 @@ impl BinaryRequest { sphinx_packet, } } -} -impl Into for BinaryRequest { - fn into(self) -> Message { - Message::Binary(self.into_bytes()) + pub fn into_ws_message(self, shared_key: &SharedKey) -> Message { + Message::Binary(self.into_encrypted_bytes(shared_key)) } } -// TODO: tests... +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handshake_payload_can_be_deserialized_into_register_handshake_init_request() { + let handshake_data = vec![1, 2, 3, 4, 5, 6]; + let handshake_payload = RegistrationHandshake::HandshakePayload { + data: handshake_data.clone(), + }; + let serialized = serde_json::to_string(&handshake_payload).unwrap(); + let deserialized = ClientControlRequest::try_from(serialized).unwrap(); + + match deserialized { + ClientControlRequest::RegisterHandshakeInitRequest { data } => { + assert_eq!(data, handshake_data) + } + _ => unreachable!("this branch shouldn't have been reached!"), + } + } +} diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index 54752d5f63..0a55ecaf6c 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -16,7 +16,7 @@ use crate::commands::override_config; use crate::config::persistence::pathfinder::GatewayPathfinder; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::{encryption, identity}; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -113,13 +113,18 @@ pub fn execute(matches: &ArgMatches) { config = override_config(config, matches); + let identity_keys = identity::KeyPair::new(); let sphinx_keys = encryption::KeyPair::new(); let pathfinder = GatewayPathfinder::new_from_config(&config); let pem_store = PemStore::new(pathfinder); pem_store - .write_encryption_keys(sphinx_keys) + .write_encryption_keypair(&sphinx_keys) .expect("Failed to save sphinx keys"); - println!("Saved mixnet sphinx keypair"); + pem_store + .write_identity_keypair(&identity_keys) + .expect("Failed to save identity keys"); + + println!("Saved identity and mixnet sphinx keypairs"); let config_save_location = config.get_config_file_save_location(); config diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index 0a0d780c4f..ad87ac87ef 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -18,7 +18,8 @@ use crate::config::Config; use crate::node::Gateway; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::{encryption, identity}; +use pemstore::pathfinder::PathFinder; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -127,17 +128,28 @@ fn special_addresses() -> Vec<&'static str> { vec!["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"] } -fn load_sphinx_keys(config_file: &Config) -> encryption::KeyPair { - let sphinx_keypair = PemStore::new(GatewayPathfinder::new_from_config(&config_file)) - .read_encryption() +fn load_sphinx_keys(pemstore: &PemStore

) -> encryption::KeyPair { + let sphinx_keypair = pemstore + .read_encryption_keypair() .expect("Failed to read stored sphinx key files"); println!( - "Public key: {}\n", + "Public sphinx key: {}\n", sphinx_keypair.public_key().to_base58_string() ); sphinx_keypair } +fn load_identity_keys(pemstore: &PemStore

) -> identity::KeyPair { + let identity_keypair = pemstore + .read_identity_keypair() + .expect("Failed to read stored identity key files"); + println!( + "Public identity key: {}\n", + identity_keypair.public_key().to_base58_string() + ); + identity_keypair +} + pub fn execute(matches: &ArgMatches) { let id = matches.value_of("id").unwrap(); @@ -149,7 +161,9 @@ pub fn execute(matches: &ArgMatches) { config = override_config(config, matches); - let sphinx_keypair = load_sphinx_keys(&config); + let pemstore = PemStore::new(GatewayPathfinder::new_from_config(&config)); + let sphinx_keypair = load_sphinx_keys(&pemstore); + let identity = load_identity_keys(&pemstore); let mix_listening_ip_string = config.get_mix_listening_address().ip().to_string(); if special_addresses().contains(&mix_listening_ip_string.as_ref()) { @@ -194,5 +208,5 @@ pub fn execute(matches: &ArgMatches) { config.get_clients_ledger_path() ); - Gateway::new(config, sphinx_keypair).run(); + Gateway::new(config, sphinx_keypair, identity).run(); } diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index a985499fc3..bb53898960 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -105,6 +105,19 @@ impl Config { self.gateway.public_sphinx_key_file = self::Gateway::default_public_sphinx_key_file(&id); } + if self + .gateway + .private_identity_key_file + .as_os_str() + .is_empty() + { + self.gateway.private_identity_key_file = + self::Gateway::default_private_identity_key_file(&id); + } + if self.gateway.public_identity_key_file.as_os_str().is_empty() { + self.gateway.public_identity_key_file = + self::Gateway::default_public_identity_key_file(&id); + } if self .clients_endpoint .inboxes_directory @@ -317,6 +330,14 @@ impl Config { self.gateway.location.clone() } + pub fn get_private_identity_key_file(&self) -> PathBuf { + self.gateway.private_identity_key_file.clone() + } + + pub fn get_public_identity_key_file(&self) -> PathBuf { + self.gateway.public_identity_key_file.clone() + } + pub fn get_private_sphinx_key_file(&self) -> PathBuf { self.gateway.private_sphinx_key_file.clone() } @@ -390,6 +411,12 @@ pub struct Gateway { /// this field with as much accuracy as you wish to share. location: String, + /// Path to file containing private identity key. + private_identity_key_file: PathBuf, + + /// Path to file containing public identity key. + public_identity_key_file: PathBuf, + /// Path to file containing private sphinx key. private_sphinx_key_file: PathBuf, @@ -413,6 +440,14 @@ impl Gateway { Config::default_data_directory(Some(id)).join("public_sphinx.pem") } + fn default_private_identity_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("private_identity.pem") + } + + fn default_public_identity_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("public_identity.pem") + } + fn default_location() -> String { "unknown".into() } @@ -423,6 +458,8 @@ impl Default for Gateway { Gateway { id: "".to_string(), location: Self::default_location(), + private_identity_key_file: Default::default(), + public_identity_key_file: Default::default(), private_sphinx_key_file: Default::default(), public_sphinx_key_file: Default::default(), presence_directory_server: DEFAULT_DIRECTORY_SERVER.to_string(), diff --git a/gateway/src/config/persistence/pathfinder.rs b/gateway/src/config/persistence/pathfinder.rs index 81d3d2d69b..1a004ace27 100644 --- a/gateway/src/config/persistence/pathfinder.rs +++ b/gateway/src/config/persistence/pathfinder.rs @@ -21,6 +21,8 @@ pub struct GatewayPathfinder { pub config_dir: PathBuf, pub private_sphinx_key: PathBuf, pub public_sphinx_key: PathBuf, + pub private_identity_key: PathBuf, + pub public_identity_key: PathBuf, } impl GatewayPathfinder { @@ -29,6 +31,8 @@ impl GatewayPathfinder { config_dir: config.get_config_file_save_location(), private_sphinx_key: config.get_private_sphinx_key_file(), public_sphinx_key: config.get_public_sphinx_key_file(), + private_identity_key: config.get_private_identity_key_file(), + public_identity_key: config.get_public_identity_key_file(), } } } @@ -39,13 +43,11 @@ impl PathFinder for GatewayPathfinder { } fn private_identity_key(&self) -> PathBuf { - // TEMPORARILY USE SAME KEYS AS ENCRYPTION - self.private_sphinx_key.clone() + self.private_identity_key.clone() } fn public_identity_key(&self) -> PathBuf { - // TEMPORARILY USE SAME KEYS AS ENCRYPTION - self.public_sphinx_key.clone() + self.public_identity_key.clone() } fn private_encryption_key(&self) -> Option { diff --git a/gateway/src/config/template.rs b/gateway/src/config/template.rs index 70eac5b249..55e2c230a0 100644 --- a/gateway/src/config/template.rs +++ b/gateway/src/config/template.rs @@ -33,6 +33,12 @@ id = '{{ gateway.id }}' # this field with as much accuracy as you wish to share. location = '{{ gateway.location }}' +# Path to file containing private identity key. +private_identity_key_file = '{{ gateway.private_identity_key_file }}' + +# Path to file containing public identity key. +public_identity_key_file = '{{ gateway.public_identity_key_file }}' + # Path to file containing private sphinx key. private_sphinx_key_file = '{{ gateway.private_sphinx_key_file }}' diff --git a/gateway/src/node/client_handling/clients_handler.rs b/gateway/src/node/client_handling/clients_handler.rs index 273d6b70b2..163e56b890 100644 --- a/gateway/src/node/client_handling/clients_handler.rs +++ b/gateway/src/node/client_handling/clients_handler.rs @@ -16,18 +16,16 @@ use crate::node::{ client_handling::websocket::message_receiver::MixMessageSender, storage::{inboxes::ClientStorage, ClientLedger}, }; -use crypto::encryption; use futures::{ channel::{mpsc, oneshot}, StreamExt, }; -use gateway_requests::auth_token::AuthToken; -use hmac::{Hmac, Mac}; +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::registration::handshake::SharedKey; use log::*; use nymsphinx::DestinationAddressBytes; -use sha2::Sha256; use std::collections::HashMap; -use std::sync::Arc; use tokio::task::JoinHandle; pub(crate) type ClientsHandlerRequestSender = mpsc::UnboundedSender; @@ -35,17 +33,19 @@ pub(crate) type ClientsHandlerRequestReceiver = mpsc::UnboundedReceiver; -#[derive(Debug)] +// #[derive(Debug)] pub(crate) enum ClientsHandlerRequest { // client Register( DestinationAddressBytes, + SharedKey, MixMessageSender, ClientsHandlerResponseSender, ), Authenticate( DestinationAddressBytes, - AuthToken, + EncryptedAddressBytes, + AuthenticationIV, MixMessageSender, ClientsHandlerResponseSender, ), @@ -57,27 +57,21 @@ pub(crate) enum ClientsHandlerRequest { #[derive(Debug)] pub(crate) enum ClientsHandlerResponse { - Register(AuthToken), - Authenticate(bool), + Register(bool), + Authenticate(Option), IsOnline(Option), Error(Box), } pub(crate) struct ClientsHandler { - secret_key: Arc, open_connections: HashMap, clients_ledger: ClientLedger, clients_inbox_storage: ClientStorage, } impl ClientsHandler { - pub(crate) fn new( - secret_key: Arc, - clients_ledger: ClientLedger, - clients_inbox_storage: ClientStorage, - ) -> Self { + pub(crate) fn new(clients_ledger: ClientLedger, clients_inbox_storage: ClientStorage) -> Self { ClientsHandler { - secret_key, open_connections: HashMap::new(), clients_ledger, clients_inbox_storage, @@ -101,18 +95,6 @@ impl ClientsHandler { } } - fn generate_new_auth_token(&self, client_address: DestinationAddressBytes) -> AuthToken { - type HmacSha256 = Hmac; - - // note that `new_varkey` doesn't even have an execution branch returning an error - // (true as of hmac 0.7.1) - let mut auth_token_raw = HmacSha256::new_varkey(&self.secret_key.to_bytes()).unwrap(); - auth_token_raw.input(client_address.as_bytes()); - let mut auth_token = [0u8; 32]; - auth_token.copy_from_slice(auth_token_raw.result().code().as_slice()); - AuthToken::from_bytes(auth_token) - } - async fn push_stored_messages_to_client_and_save_channel( &mut self, client_address: DestinationAddressBytes, @@ -172,6 +154,7 @@ impl ClientsHandler { async fn handle_register_request( &mut self, address: DestinationAddressBytes, + derived_shared_key: SharedKey, comm_channel: MixMessageSender, res_channel: ClientsHandlerResponseSender, ) { @@ -188,12 +171,9 @@ impl ClientsHandler { return; } - // I presume some additional checks will go here: - // ... - let auth_token = self.generate_new_auth_token(address.clone()); if self .clients_ledger - .insert_token(auth_token.clone(), address.clone()) + .insert_shared_key(derived_shared_key, address.clone()) .unwrap() .is_some() { @@ -206,17 +186,17 @@ impl ClientsHandler { .create_storage_dir(address.clone()) .await { - error!("We failed to create inbox directory for the client -{:?}\nReverting issued token...", e); + error!("We failed to create inbox directory for the client -{:?}\nReverting stored shared key...", e); // we must revert our changes if this operation failed - self.clients_ledger.remove_token(&address).unwrap(); - self.send_error_response("failed to issue an auth token", res_channel); + self.clients_ledger.remove_shared_key(&address).unwrap(); + self.send_error_response("failed to complete issuing shared key", res_channel); return; } self.push_stored_messages_to_client_and_save_channel(address, comm_channel) .await; - if let Err(_) = res_channel.send(ClientsHandlerResponse::Register(auth_token)) { + if let Err(_) = res_channel.send(ClientsHandlerResponse::Register(true)) { error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); } } @@ -224,7 +204,8 @@ impl ClientsHandler { async fn handle_authenticate_request( &mut self, address: DestinationAddressBytes, - token: AuthToken, + encrypted_address: EncryptedAddressBytes, + iv: AuthenticationIV, comm_channel: MixMessageSender, res_channel: ClientsHandlerResponseSender, ) { @@ -239,14 +220,26 @@ impl ClientsHandler { return; } - if self.clients_ledger.verify_token(&token, &address).unwrap() { + if self + .clients_ledger + .verify_shared_key(&address, &encrypted_address, &iv) + .unwrap() + { + // The first unwrap is due to possible db read errors, but I'm not entirely sure when could + // the second one happen. + let shared_key = self + .clients_ledger + .get_shared_key(&address) + .unwrap() + .unwrap(); self.push_stored_messages_to_client_and_save_channel(address, comm_channel) .await; - if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(true)) { + if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(Some(shared_key))) + { error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); } } else { - if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(false)) { + if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(None)) { error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); } } @@ -286,13 +279,35 @@ impl ClientsHandler { ) { while let Some(request) = request_receiver_channel.next().await { match request { - ClientsHandlerRequest::Register(address, comm_channel, res_channel) => { - self.handle_register_request(address, comm_channel, res_channel) - .await + ClientsHandlerRequest::Register( + address, + derived_shared_key, + comm_channel, + res_channel, + ) => { + self.handle_register_request( + address, + derived_shared_key, + comm_channel, + res_channel, + ) + .await } - ClientsHandlerRequest::Authenticate(address, token, comm_channel, res_channel) => { - self.handle_authenticate_request(address, token, comm_channel, res_channel) - .await + ClientsHandlerRequest::Authenticate( + address, + encrypted_address, + iv, + comm_channel, + res_channel, + ) => { + self.handle_authenticate_request( + address, + encrypted_address, + iv, + comm_channel, + res_channel, + ) + .await } ClientsHandlerRequest::Disconnect(address) => self.handle_disconnect(address), ClientsHandlerRequest::IsOnline(address, res_channel) => { diff --git a/gateway/src/node/client_handling/websocket/connection_handler.rs b/gateway/src/node/client_handling/websocket/connection_handler.rs index 92780d2f20..ddf0cef7c5 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler.rs @@ -12,22 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use futures::{ - channel::{mpsc, oneshot}, - SinkExt, -}; -use log::*; -use std::convert::TryFrom; -use tokio::{prelude::*, stream::StreamExt}; -use tokio_tungstenite::{ - tungstenite::{protocol::Message, Error as WsError}, - WebSocketStream, -}; - -use gateway_requests::auth_token::AuthToken; -use gateway_requests::types::{BinaryRequest, ClientControlRequest, ServerResponse}; -use nymsphinx::DestinationAddressBytes; - use crate::node::client_handling::clients_handler::{ ClientsHandlerRequest, ClientsHandlerRequestSender, ClientsHandlerResponse, }; @@ -35,6 +19,25 @@ use crate::node::client_handling::websocket::message_receiver::{ MixMessageReceiver, MixMessageSender, }; use crate::node::mixnet_handling::sender::OutboundMixMessageSender; +use crypto::asymmetric::identity; +use futures::{ + channel::{mpsc, oneshot}, + SinkExt, +}; +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::registration::handshake::error::HandshakeError; +use gateway_requests::registration::handshake::{gateway_handshake, SharedKey, DEFAULT_RNG}; +use gateway_requests::types::{BinaryRequest, ClientControlRequest, ServerResponse}; +use log::*; +use nymsphinx::DestinationAddressBytes; +use std::convert::TryFrom; +use std::sync::Arc; +use tokio::{prelude::*, stream::StreamExt}; +use tokio_tungstenite::{ + tungstenite::{protocol::Message, Error as WsError}, + WebSocketStream, +}; //// TODO: note for my future self to consider the following idea: //// split the socket connection into sink and stream @@ -42,39 +45,54 @@ use crate::node::mixnet_handling::sender::OutboundMixMessageSender; //// and sink for pumping responses AND mix traffic //// but as byproduct this might (or might not) break the clean "SocketStream" enum here -enum SocketStream { +enum SocketStream { RawTCP(S), UpgradedWebSocket(WebSocketStream), Invalid, } -pub(crate) struct Handle { - address: Option, +impl SocketStream { + fn is_websocket(&self) -> bool { + match self { + SocketStream::UpgradedWebSocket(_) => true, + _ => false, + } + } +} + +pub(crate) struct Handle { + remote_address: Option, + shared_key: Option, clients_handler_sender: ClientsHandlerRequestSender, outbound_mix_sender: OutboundMixMessageSender, socket_connection: SocketStream, + + local_identity: Arc, } -impl Handle -where - S: AsyncRead + AsyncWrite + Unpin, -{ +impl Handle { // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult pub(crate) fn new( conn: S, clients_handler_sender: ClientsHandlerRequestSender, outbound_mix_sender: OutboundMixMessageSender, + local_identity: Arc, ) -> Self { Handle { - address: None, + remote_address: None, + shared_key: None, clients_handler_sender, outbound_mix_sender, socket_connection: SocketStream::RawTCP(conn), + local_identity, } } - async fn perform_websocket_handshake(&mut self) -> Result<(), WsError> { + async fn perform_websocket_handshake(&mut self) -> Result<(), WsError> + where + S: AsyncRead + AsyncWrite + Unpin, + { self.socket_connection = match std::mem::replace(&mut self.socket_connection, SocketStream::Invalid) { SocketStream::RawTCP(conn) => { @@ -94,14 +112,42 @@ where Ok(()) } - async fn next_websocket_request(&mut self) -> Option> { + async fn perform_registration_handshake( + &mut self, + init_msg: Vec, + ) -> Result + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { + debug_assert!(self.socket_connection.is_websocket()); + match &mut self.socket_connection { + SocketStream::UpgradedWebSocket(ws_stream) => { + gateway_handshake( + &mut DEFAULT_RNG, + ws_stream, + self.local_identity.as_ref(), + init_msg, + ) + .await + } + _ => unreachable!(), + } + } + + async fn next_websocket_request(&mut self) -> Option> + where + S: AsyncRead + AsyncWrite + Unpin, + { match self.socket_connection { SocketStream::UpgradedWebSocket(ref mut ws_stream) => ws_stream.next().await, _ => panic!("impossible state - websocket handshake was somehow reverted"), } } - async fn send_websocket_response(&mut self, msg: Message) -> Result<(), WsError> { + async fn send_websocket_response(&mut self, msg: Message) -> Result<(), WsError> + where + S: AsyncRead + AsyncWrite + Unpin, + { match self.socket_connection { // TODO: more closely investigate difference between `Sink::send` and `Sink::send_all` // it got something to do with batching and flushing - it might be important if it @@ -111,10 +157,10 @@ where } } - async fn send_websocket_sphinx_packets( - &mut self, - packets: Vec>, - ) -> Result<(), WsError> { + async fn send_websocket_sphinx_packets(&mut self, packets: Vec>) -> Result<(), WsError> + where + S: AsyncRead + AsyncWrite + Unpin, + { let messages: Vec> = packets .into_iter() .map(|packet| Ok(Message::Binary(packet))) @@ -131,7 +177,7 @@ where fn disconnect(&self) { // if we never established what is the address of the client, its connection was never // announced hence we do not need to send 'disconnect' message - self.address.as_ref().map(|addr| { + self.remote_address.as_ref().map(|addr| { self.clients_handler_sender .unbounded_send(ClientsHandlerRequest::Disconnect(addr.clone())) .unwrap(); @@ -141,7 +187,12 @@ where async fn handle_binary(&self, bin_msg: Vec) -> Message { trace!("Handling binary message (presumably sphinx packet)"); - match BinaryRequest::try_from_bytes(&bin_msg) { + match BinaryRequest::try_from_encrypted_bytes( + bin_msg, + self.shared_key + .as_ref() + .expect("no shared key present even though we authenticated the client!"), + ) { Err(e) => ServerResponse::new_error(e.to_string()), Ok(request) => match request { // currently only a single type exists @@ -163,7 +214,8 @@ where async fn handle_authenticate( &mut self, address: String, - token: String, + enc_address: String, + iv: String, mix_sender: MixMessageSender, ) -> ServerResponse { let address = match DestinationAddressBytes::try_from_base58_string(address) { @@ -173,28 +225,43 @@ where return ServerResponse::new_error("malformed destination address").into(); } }; - let token = match AuthToken::try_from_base58_string(token) { - Ok(token) => token, + + let encrypted_address = match EncryptedAddressBytes::try_from_base58_string(enc_address) { + Ok(address) => address, Err(e) => { - trace!("failed to parse received AuthToken: {:?}", e); - return ServerResponse::new_error("malformed authentication token").into(); + trace!("failed to parse received encrypted address: {:?}", e); + return ServerResponse::new_error("malformed encrypted address").into(); + } + }; + + let iv = match AuthenticationIV::try_from_base58_string(iv) { + Ok(iv) => iv, + Err(e) => { + trace!("failed to parse received IV {:?}", e); + return ServerResponse::new_error("malformed iv").into(); } }; let (res_sender, res_receiver) = oneshot::channel(); - let clients_handler_request = - ClientsHandlerRequest::Authenticate(address.clone(), token, mix_sender, res_sender); + let clients_handler_request = ClientsHandlerRequest::Authenticate( + address.clone(), + encrypted_address, + iv, + mix_sender, + res_sender, + ); self.clients_handler_sender .unbounded_send(clients_handler_request) .unwrap(); // the receiver MUST BE alive match res_receiver.await.unwrap() { - ClientsHandlerResponse::Authenticate(authenticated) => { - if authenticated { - self.address = Some(address); - } - ServerResponse::Authenticate { - status: authenticated, + ClientsHandlerResponse::Authenticate(shared_key) => { + if shared_key.is_some() { + self.remote_address = Some(address); + self.shared_key = shared_key; + ServerResponse::Authenticate { status: true } + } else { + ServerResponse::Authenticate { status: false } } } ClientsHandlerResponse::Error(e) => { @@ -207,37 +274,61 @@ where } } + fn extract_remote_identity_from_register_init(init_data: &[u8]) -> Option { + if init_data.len() < identity::PUBLIC_KEY_LENGTH { + None + } else { + identity::PublicKey::from_bytes(&init_data[..identity::PUBLIC_KEY_LENGTH]).ok() + } + } + async fn handle_register( &mut self, - address: String, + init_data: Vec, mix_sender: MixMessageSender, - ) -> ServerResponse { - let address = match DestinationAddressBytes::try_from_base58_string(address) { - Ok(address) => address, - Err(e) => { - trace!("failed to parse received DestinationAddress: {:?}", e); - return ServerResponse::new_error("malformed destination address").into(); + ) -> ServerResponse + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { + // not entirely sure how to it more "nicely"... + // hopefully, eventually this will go away once client's identity is known beforehand + let remote_identity = match Self::extract_remote_identity_from_register_init(&init_data) { + Some(address) => address, + None => return ServerResponse::new_error("malformed request"), + }; + let remote_address = remote_identity.derive_address(); + + let derived_shared_key = match self.perform_registration_handshake(init_data).await { + Ok(shared_key) => shared_key, + Err(err) => { + return ServerResponse::new_error(format!( + "failed to perform the handshake - {}", + err + )) } }; let (res_sender, res_receiver) = oneshot::channel(); - let clients_handler_request = - ClientsHandlerRequest::Register(address.clone(), mix_sender, res_sender); + let clients_handler_request = ClientsHandlerRequest::Register( + remote_address.clone(), + derived_shared_key, + mix_sender, + res_sender, + ); + self.clients_handler_sender .unbounded_send(clients_handler_request) .unwrap(); // the receiver MUST BE alive match res_receiver.await.unwrap() { // currently register can't fail (as in if all machines are working correctly and you - // send valid address, you will receive a valid token) - ClientsHandlerResponse::Register(token) => { - self.address = Some(address); - ServerResponse::Register { - token: token.to_base58_string(), - } + // managed to complete registration handshake) + ClientsHandlerResponse::Register(status) => { + self.remote_address = Some(remote_address); + ServerResponse::Register { status } } ClientsHandlerResponse::Error(e) => { - error!("Authentication unexpectedly failed - {}", e); + error!("Post-handshake registration unexpectedly failed - {}", e); ServerResponse::Error { message: "unexpected failure".into(), } @@ -265,7 +356,44 @@ where } } - async fn wait_for_initial_authentication(&mut self) -> Option { + /// Handles data that resembles request to either start registration handshake or perform + /// authentication. + async fn handle_initial_authentication_request( + &mut self, + mix_sender: MixMessageSender, + raw_request: String, + ) -> ServerResponse + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { + if let Ok(request) = ClientControlRequest::try_from(raw_request) { + match request { + ClientControlRequest::Authenticate { + address, + enc_address, + iv, + } => { + self.handle_authenticate(address, enc_address, iv, mix_sender) + .await + } + ClientControlRequest::RegisterHandshakeInitRequest { data } => { + self.handle_register(data, mix_sender).await + } + } + } else { + // TODO: is this a malformed request or rather a network error and + // connection should be terminated? + ServerResponse::new_error("malformed request") + } + } + + /// Listens for only a subset of possible client requests, i.e. for those that can either + /// result in client getting registered or authenticated. All other requests, such as forwarding + /// sphinx packets are ignored. + async fn wait_for_initial_authentication(&mut self) -> Option + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { trace!("Started waiting for authenticate/register request..."); while let Some(msg) = self.next_websocket_request().await { @@ -287,21 +415,12 @@ where let response = match msg { Message::Close(_) => break, Message::Text(text_msg) => { - if let Ok(request) = ClientControlRequest::try_from(text_msg) { - match request { - ClientControlRequest::Authenticate { address, token } => { - self.handle_authenticate(address, token, mix_sender).await - } - ClientControlRequest::Register { address } => { - self.handle_register(address, mix_sender).await - } - } - } else { - ServerResponse::new_error("malformed request") - } + self.handle_initial_authentication_request(mix_sender, text_msg) + .await } Message::Binary(_) => { // perhaps logging level should be reduced here, let's leave it for now and see what happens + // if client is working correctly, this should have never happened warn!("possibly received a sphinx packet without prior authentication. Request is going to be ignored"); ServerResponse::new_error("binary request without prior authentication") } @@ -328,7 +447,13 @@ where None } - async fn listen_for_requests(&mut self, mut mix_receiver: MixMessageReceiver) { + /// Simultaneously listens for incoming client requests, which realistically should only be + /// binary requests to forward sphinx packets, and for sphinx packets received from the mix + /// network that should be sent back to the client. + async fn listen_for_requests(&mut self, mut mix_receiver: MixMessageReceiver) + where + S: AsyncRead + AsyncWrite + Unpin, + { trace!("Started listening for ALL incoming requests..."); loop { @@ -373,7 +498,10 @@ where trace!("The stream was closed!"); } - pub(crate) async fn start_handling(&mut self) { + pub(crate) async fn start_handling(&mut self) + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { if let Err(e) = self.perform_websocket_handshake().await { warn!( "Failed to complete WebSocket handshake - {:?}. Stopping the handler", diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index 970333ca87..e3acdbd9b4 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -15,17 +15,23 @@ use crate::node::client_handling::clients_handler::ClientsHandlerRequestSender; use crate::node::client_handling::websocket::connection_handler::Handle; use crate::node::mixnet_handling::sender::OutboundMixMessageSender; +use crypto::asymmetric::identity; use log::*; use std::net::SocketAddr; +use std::sync::Arc; use tokio::task::JoinHandle; pub(crate) struct Listener { address: SocketAddr, + local_identity: Arc, } impl Listener { - pub(crate) fn new(address: SocketAddr) -> Self { - Listener { address } + pub(crate) fn new(address: SocketAddr, local_identity: Arc) -> Self { + Listener { + address, + local_identity, + } } pub(crate) async fn run( @@ -42,12 +48,13 @@ impl Listener { match tcp_listener.accept().await { Ok((socket, remote_addr)) => { trace!("received a socket connection from {}", remote_addr); - // TODO: I think we need a mechanism for having a maximum number of connected + // TODO: I think we *REALLY* need a mechanism for having a maximum number of connected // clients or spawned tokio tasks -> perhaps a worker system? let mut handle = Handle::new( socket, clients_handler_sender.clone(), outbound_mix_sender.clone(), + Arc::clone(&self.local_identity), ); tokio::spawn(async move { handle.start_handling().await }); } diff --git a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs index 642c4bc975..59ca2c5563 100644 --- a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -18,18 +18,16 @@ use crate::node::client_handling::clients_handler::{ use crate::node::client_handling::websocket::message_receiver::MixMessageSender; use crate::node::mixnet_handling::sender::OutboundMixMessageSender; use crate::node::storage::inboxes::{ClientStorage, StoreData}; -use crypto::encryption; +use crypto::asymmetric::encryption; use futures::channel::oneshot; use futures::lock::Mutex; use log::*; use nymsphinx::acknowledgements::surb_ack::{SURBAck, SURBAckRecoveryError}; -use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; use nymsphinx::params::packet_sizes::PacketSize; use nymsphinx::{DestinationAddressBytes, Error as SphinxError, ProcessedPacket, SphinxPacket}; use std::collections::HashMap; use std::io; -use std::ops::Deref; use std::sync::Arc; #[derive(Debug)] @@ -70,7 +68,7 @@ impl From for MixProcessingError { // PacketProcessor contains all data required to correctly unwrap and store sphinx packets #[derive(Clone)] pub struct PacketProcessor { - secret_key: Arc, + encryption_keys: Arc, // TODO: later investigate some concurrent hashmap solutions or perhaps RWLocks. // Right now Mutex is the simplest and fastest to implement approach available_socket_senders_cache: Arc>>, @@ -81,7 +79,7 @@ pub struct PacketProcessor { impl PacketProcessor { pub(crate) fn new( - secret_key: Arc, + encryption_keys: Arc, clients_handler_sender: ClientsHandlerRequestSender, client_store: ClientStorage, ack_sender: OutboundMixMessageSender, @@ -90,7 +88,7 @@ impl PacketProcessor { available_socket_senders_cache: Arc::new(Mutex::new(HashMap::new())), clients_handler_sender, client_store, - secret_key, + encryption_keys, ack_sender, } } @@ -184,7 +182,7 @@ impl PacketProcessor { &self, packet: SphinxPacket, ) -> Result<(DestinationAddressBytes, Vec), MixProcessingError> { - match packet.process(self.secret_key.deref().inner()) { + match packet.process(&self.encryption_keys.as_ref().private_key().into()) { Ok(ProcessedPacket::ProcessedPacketForwardHop(_, _, _)) => { warn!("Received a forward hop message - those are not implemented for gateways"); Err(MixProcessingError::ReceivedForwardHopError) diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index fc1bfe8c85..69b0269245 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -17,7 +17,7 @@ use crate::node::client_handling::clients_handler::{ClientsHandler, ClientsHandl use crate::node::client_handling::websocket; use crate::node::mixnet_handling::sender::{OutboundMixMessageSender, PacketForwarder}; use crate::node::storage::{inboxes, ClientLedger}; -use crypto::encryption; +use crypto::asymmetric::{encryption, identity}; use directory_client::DirectoryClient; use log::*; use std::sync::Arc; @@ -28,27 +28,22 @@ pub(crate) mod mixnet_handling; mod presence; pub(crate) mod storage; -// current issues in this file: -// - two calls to `Arc::new(self.sphinx_keypair.private_key().clone()),` - basically 2 separate -// Arcs to the same underlying data (well, after a clone), so what it ends up resulting in is -// private key being in 3 different places in memory rather than in a single location. -// Does it affect performance? No, not really. Is it *SUPER* insecure? Also, not as much, because -// if somebody could read memory of the machine, they probably got better attack vectors. -// Should it get fixed? Probably. But it's very low priority for time being. - pub struct Gateway { config: Config, - sphinx_keypair: encryption::KeyPair, + /// ed25519 keypair used to assert one's identity. + identity: Arc, + /// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation. + encryption_keys: Arc, registered_clients_ledger: ClientLedger, client_inbox_storage: inboxes::ClientStorage, } impl Gateway { - // the constructor differs from mixnodes and providers in that it takes keys directly - // as opposed to having `Self::load_sphinx_keys(cfg: &Config)` method. Let's see - // how it works out, because I'm not sure which one would be "better", but when I think about it, - // I kinda prefer to delegate having to load the keys to outside the gateway - pub fn new(config: Config, sphinx_keypair: encryption::KeyPair) -> Self { + pub fn new( + config: Config, + encryption_keys: encryption::KeyPair, + identity: identity::KeyPair, + ) -> Self { let registered_clients_ledger = match ClientLedger::load(config.get_clients_ledger_path()) { Err(e) => panic!(format!("Failed to load the ledger - {:?}", e)), Ok(ledger) => ledger, @@ -60,7 +55,8 @@ impl Gateway { ); Gateway { config, - sphinx_keypair, + identity: Arc::new(identity), + encryption_keys: Arc::new(encryption_keys), client_inbox_storage, registered_clients_ledger, } @@ -74,7 +70,7 @@ impl Gateway { info!("Starting mix socket listener..."); let packet_processor = mixnet_handling::PacketProcessor::new( - Arc::new(self.sphinx_keypair.private_key().clone()), + Arc::clone(&self.encryption_keys), clients_handler_sender, self.client_inbox_storage.clone(), ack_sender, @@ -91,8 +87,11 @@ impl Gateway { ) { info!("Starting client [web]socket listener..."); - websocket::Listener::new(self.config.get_clients_listening_address()) - .start(clients_handler_sender, forwarding_channel); + websocket::Listener::new( + self.config.get_clients_listening_address(), + Arc::clone(&self.identity), + ) + .start(clients_handler_sender, forwarding_channel); } fn start_packet_forwarder(&self) -> OutboundMixMessageSender { @@ -110,7 +109,6 @@ impl Gateway { fn start_clients_handler(&self) -> ClientsHandlerRequestSender { info!("Starting clients handler"); let (_, clients_handler_sender) = ClientsHandler::new( - Arc::new(self.sphinx_keypair.private_key().clone()), self.registered_clients_ledger.clone(), self.client_inbox_storage.clone(), ) @@ -125,7 +123,8 @@ impl Gateway { self.config.get_presence_directory_server(), self.config.get_mix_announce_address(), self.config.get_clients_announce_address(), - self.sphinx_keypair.public_key().to_base58_string(), + self.identity.public_key().to_base58_string(), + self.encryption_keys.public_key().to_base58_string(), self.config.get_presence_sending_delay(), ); presence::Notifier::new(notifier_config, self.registered_clients_ledger.clone()).start(); @@ -160,7 +159,7 @@ impl Gateway { node.mixnet_listener == announced_mix_host || node.client_listener == announced_clients_host }) - .map(|node| node.pub_key.clone()) + .map(|node| node.identity_key.clone()) } // Rather than starting all futures with explicit `&Handle` argument, let's see how it works diff --git a/gateway/src/node/presence/mod.rs b/gateway/src/node/presence/mod.rs index ca0eeeb125..1173f393c7 100644 --- a/gateway/src/node/presence/mod.rs +++ b/gateway/src/node/presence/mod.rs @@ -25,7 +25,8 @@ pub(crate) struct NotifierConfig { directory_server: String, mix_announce_host: String, clients_announce_host: String, - pub_key_string: String, + identity_string: String, + sphinx_key_string: String, sending_delay: Duration, } @@ -35,7 +36,8 @@ impl NotifierConfig { directory_server: String, mix_announce_host: String, clients_announce_host: String, - pub_key_string: String, + identity_string: String, + sphinx_key_string: String, sending_delay: Duration, ) -> Self { NotifierConfig { @@ -43,7 +45,8 @@ impl NotifierConfig { directory_server, mix_announce_host, clients_announce_host, - pub_key_string, + identity_string, + sphinx_key_string, sending_delay, } } @@ -56,7 +59,8 @@ pub(crate) struct Notifier { sending_delay: Duration, client_listener: String, mixnet_listener: String, - pub_key_string: String, + identity: String, + sphinx_key: String, } impl Notifier { @@ -72,7 +76,8 @@ impl Notifier { location: config.location, client_listener: config.clients_announce_host, mixnet_listener: config.mix_announce_host, - pub_key_string: config.pub_key_string, + identity: config.identity_string, + sphinx_key: config.sphinx_key_string, sending_delay: config.sending_delay, } } @@ -90,7 +95,8 @@ impl Notifier { location: self.location.clone(), client_listener: self.client_listener.clone(), mixnet_listener: self.mixnet_listener.clone(), - pub_key: self.pub_key_string.clone(), + identity_key: self.identity.clone(), + sphinx_key: self.sphinx_key.clone(), registered_clients, last_seen: 0, version: built_info::PKG_VERSION.to_string(), diff --git a/gateway/src/node/storage/ledger.rs b/gateway/src/node/storage/ledger.rs index 1aa9e4ab30..9da2d5beff 100644 --- a/gateway/src/node/storage/ledger.rs +++ b/gateway/src/node/storage/ledger.rs @@ -18,12 +18,13 @@ //use sfw_provider_requests::auth_token::{AuthToken, AUTH_TOKEN_SIZE}; //use std::path::PathBuf; -use std::path::PathBuf; - +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::generic_array::typenum::Unsigned; +use gateway_requests::registration::handshake::{SharedKey, SharedKeySize}; use log::*; - -use gateway_requests::auth_token::{AuthToken, AUTH_TOKEN_SIZE}; use nymsphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; +use std::path::PathBuf; #[derive(Debug)] pub(crate) enum ClientLedgerError { @@ -60,18 +61,18 @@ impl ClientLedger { Ok(ledger) } - fn read_auth_token(&self, raw_token: sled::IVec) -> AuthToken { - let token_bytes_ref = raw_token.as_ref(); + fn read_shared_key(&self, raw_key: sled::IVec) -> SharedKey { + let key_bytes_ref = raw_key.as_ref(); // if this fails it means we have some database corruption and we // absolutely can't continue - if token_bytes_ref.len() != AUTH_TOKEN_SIZE { - error!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); - panic!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); + + if key_bytes_ref.len() != SharedKeySize::to_usize() { + error!("CLIENT LEDGER DATA CORRUPTION - SHARED KEY HAS INVALID LENGTH"); + panic!("CLIENT LEDGER DATA CORRUPTION - SHARED KEY HAS INVALID LENGTH"); } - let mut token_bytes = [0u8; AUTH_TOKEN_SIZE]; - token_bytes.copy_from_slice(token_bytes_ref); - AuthToken::from_bytes(token_bytes) + // this can only fail if the bytes have invalid length but we already asserted it + SharedKey::try_from_bytes(key_bytes_ref).unwrap() } fn read_destination_address_bytes( @@ -91,32 +92,46 @@ impl ClientLedger { DestinationAddressBytes::from_bytes(destination_bytes) } - pub(crate) fn verify_token( + pub(crate) fn verify_shared_key( &self, - auth_token: &AuthToken, client_address: &DestinationAddressBytes, + encrypted_address: &EncryptedAddressBytes, + iv: &AuthenticationIV, ) -> Result { match self.db.get(&client_address.to_bytes()) { Err(e) => Err(ClientLedgerError::DbReadError(e)), - Ok(token) => match token { - Some(token_ivec) => Ok(&self.read_auth_token(token_ivec) == auth_token), + Ok(existing_key) => match existing_key { + Some(existing_key_ivec) => { + let shared_key = &self.read_shared_key(existing_key_ivec); + Ok(encrypted_address.verify(client_address, shared_key, iv)) + } None => Ok(false), }, } } - pub(crate) fn insert_token( + pub(crate) fn get_shared_key( + &self, + client_address: &DestinationAddressBytes, + ) -> Result, ClientLedgerError> { + match self.db.get(&client_address.to_bytes()) { + Err(e) => Err(ClientLedgerError::DbReadError(e)), + Ok(existing_key) => Ok(existing_key.map(|key_ivec| self.read_shared_key(key_ivec))), + } + } + + pub(crate) fn insert_shared_key( &mut self, - auth_token: AuthToken, + shared_key: SharedKey, client_address: DestinationAddressBytes, - ) -> Result, ClientLedgerError> { + ) -> Result, ClientLedgerError> { let insertion_result = match self .db - .insert(&client_address.to_bytes(), &auth_token.to_bytes()) + .insert(&client_address.to_bytes(), shared_key.to_bytes()) { Err(e) => Err(ClientLedgerError::DbWriteError(e)), - Ok(existing_token) => { - Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) + Ok(existing_key) => { + Ok(existing_key.map(|existing_key| self.read_shared_key(existing_key))) } }; @@ -125,18 +140,18 @@ impl ClientLedger { insertion_result } - pub(crate) fn remove_token( + pub(crate) fn remove_shared_key( &mut self, client_address: &DestinationAddressBytes, - ) -> Result, ClientLedgerError> { + ) -> Result, ClientLedgerError> { let removal_result = match self.db.remove(&client_address.to_bytes()) { Err(e) => Err(ClientLedgerError::DbWriteError(e)), - Ok(existing_token) => { - Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) + Ok(existing_key) => { + Ok(existing_key.map(|existing_key| self.read_shared_key(existing_key))) } }; - // removing of tokens happens extremely rarely, so flush is also fine here + // removal of keys happens extremely rarely, so flush is also fine here self.db.flush().unwrap(); removal_result } diff --git a/mixnode/src/commands/init.rs b/mixnode/src/commands/init.rs index 02531e5472..9a760fb4fd 100644 --- a/mixnode/src/commands/init.rs +++ b/mixnode/src/commands/init.rs @@ -16,7 +16,7 @@ use crate::commands::override_config; use crate::config::persistence::pathfinder::MixNodePathfinder; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::encryption; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -88,7 +88,7 @@ pub fn execute(matches: &ArgMatches) { let pathfinder = MixNodePathfinder::new_from_config(&config); let pem_store = PemStore::new(pathfinder); pem_store - .write_encryption_keys(sphinx_keys) + .write_encryption_keypair(&sphinx_keys) .expect("Failed to save sphinx keys"); println!("Saved mixnet sphinx keypair"); diff --git a/mixnode/src/commands/run.rs b/mixnode/src/commands/run.rs index 14f2e58532..029556f15a 100644 --- a/mixnode/src/commands/run.rs +++ b/mixnode/src/commands/run.rs @@ -17,7 +17,7 @@ use crate::config::{persistence::pathfinder::MixNodePathfinder, Config}; use crate::node::MixNode; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::encryption; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> App<'a, 'b> { @@ -98,7 +98,7 @@ fn special_addresses() -> Vec<&'static str> { fn load_sphinx_keys(config_file: &Config) -> encryption::KeyPair { let sphinx_keypair = PemStore::new(MixNodePathfinder::new_from_config(&config_file)) - .read_encryption() + .read_encryption_keypair() .expect("Failed to read stored sphinx key files"); println!( "Public key: {}\n", diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 0338602719..a9010460d5 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -14,7 +14,7 @@ use crate::config::Config; use crate::node::packet_processing::PacketProcessor; -use crypto::encryption; +use crypto::asymmetric::encryption; use directory_client::DirectoryClient; use futures::channel::mpsc; use log::*; diff --git a/mixnode/src/node/packet_processing.rs b/mixnode/src/node/packet_processing.rs index e859f4001f..610781c77c 100644 --- a/mixnode/src/node/packet_processing.rs +++ b/mixnode/src/node/packet_processing.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::node::metrics; -use crypto::encryption; +use crypto::asymmetric::encryption; use log::*; use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; use nymsphinx::{ @@ -21,7 +21,6 @@ use nymsphinx::{ }; use std::convert::TryFrom; use std::net::SocketAddr; -use std::ops::Deref; use std::sync::Arc; #[derive(Debug)] @@ -96,8 +95,7 @@ impl PacketProcessor { ) -> Result { // we received something resembling a sphinx packet, report it! self.metrics_reporter.report_received(); - - match packet.process(self.secret_key.deref().inner()) { + match packet.process(&self.secret_key.as_ref().into()) { Ok(ProcessedPacket::ProcessedPacketForwardHop(packet, address, delay)) => { self.process_forward_hop(packet, address, delay).await }