From 2615c1baa58f897f048d6a6f22149726983e22fd Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 25 Aug 2022 15:55:48 +0400 Subject: [PATCH] feat(console-wallet): adds basic grpc authentication (#4532) Description --- - adds basic password auth interceptor to wallet grpc. - disables grpc by default - adds `hash-grpc-password` command that produces a salted hashed password that clients can use for authentication - adds cli args to explicitly enable grpc - adds `wallet.grpc_authentication` configuration - allow grpc client to use basic auth if configured - add `--enable-grpc` cli flag to wallet Motivation and Context --- Fixes #4478 Secure GRPC calls with basic auth. User should explicitly enable grpc when it is needed. Usage: - Set `grpc_authentication = { username: "foo", password: "bar" }` - Call `tari_console_wallet hash-grpc-password` and copy the hash - Use the username and hashed password in e.g. postman or any other grpc client How Has This Been Tested? --- Basic unit tests Manually (POSTman and running a miner with GRPC auth enabled) --- Cargo.lock | 39 +++- applications/tari_app_grpc/Cargo.toml | 15 +- .../src/authentication/basic_auth.rs | 180 ++++++++++++++++++ .../src/authentication/client_interceptor.rs | 65 +++++++ .../tari_app_grpc/src/authentication/mod.rs | 32 ++++ .../src/authentication/salted_password.rs | 36 ++++ .../src/authentication/server_interceptor.rs | 85 +++++++++ applications/tari_app_grpc/src/lib.rs | 2 + applications/tari_console_wallet/Cargo.toml | 17 +- .../src/automation/commands.rs | 20 ++ applications/tari_console_wallet/src/cli.rs | 20 ++ .../tari_console_wallet/src/wallet_modes.rs | 33 +++- applications/tari_miner/Cargo.toml | 13 +- applications/tari_miner/src/config.rs | 4 + applications/tari_miner/src/errors.rs | 6 + applications/tari_miner/src/main.rs | 33 +++- base_layer/common_types/Cargo.toml | 2 + .../common_types/src/grpc_authentication.rs | 51 +++++ base_layer/common_types/src/lib.rs | 1 + base_layer/core/src/lib.rs | 73 +++---- base_layer/wallet/src/config.rs | 13 +- clients/wallet_grpc_client/index.js | 22 ++- common/config/presets/d_console_wallet.toml | 4 + common/config/presets/g_miner.toml | 2 + integration_tests/helpers/walletProcess.js | 1 + 25 files changed, 693 insertions(+), 76 deletions(-) create mode 100644 applications/tari_app_grpc/src/authentication/basic_auth.rs create mode 100644 applications/tari_app_grpc/src/authentication/client_interceptor.rs create mode 100644 applications/tari_app_grpc/src/authentication/mod.rs create mode 100644 applications/tari_app_grpc/src/authentication/salted_password.rs create mode 100644 applications/tari_app_grpc/src/authentication/server_interceptor.rs create mode 100644 base_layer/common_types/src/grpc_authentication.rs diff --git a/Cargo.lock b/Cargo.lock index d72a2ccff5..38e66f09d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca5162d1b961cb589a8ca08a2aa7cabc6341e05e0bf18d66a07697900b5d2ad0" dependencies = [ "blake2 0.9.2", - "password-hash", + "password-hash 0.2.3", +] + +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2 0.10.4", + "password-hash 0.4.2", ] [[package]] @@ -3201,6 +3212,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.3", + "subtle", +] + [[package]] name = "path-clean" version = "0.1.0" @@ -4618,18 +4640,25 @@ dependencies = [ name = "tari_app_grpc" version = "0.36.0" dependencies = [ + "argon2 0.4.1", + "base64 0.13.0", "chrono", + "digest 0.9.0", + "log", "num-traits", "prost", "prost-types", + "rand 0.8.5", "tari_common_types", "tari_comms", "tari_core", "tari_crypto", "tari_script", "tari_utilities", + "thiserror", "tonic", "tonic-build", + "zeroize", ] [[package]] @@ -4789,6 +4818,7 @@ dependencies = [ name = "tari_common_types" version = "0.36.0" dependencies = [ + "base64 0.13.0", "digest 0.9.0", "lazy_static", "rand 0.8.5", @@ -4797,6 +4827,7 @@ dependencies = [ "tari_utilities", "thiserror", "tokio", + "zeroize", ] [[package]] @@ -4915,6 +4946,7 @@ dependencies = [ name = "tari_console_wallet" version = "0.36.0" dependencies = [ + "base64 0.13.0", "bitflags 1.3.2", "chrono", "clap 3.2.15", @@ -5052,7 +5084,7 @@ dependencies = [ name = "tari_key_manager" version = "0.36.0" dependencies = [ - "argon2", + "argon2 0.2.4", "arrayvec 0.7.2", "blake2 0.9.2", "chacha20 0.7.1", @@ -5173,6 +5205,7 @@ dependencies = [ "tari_app_grpc", "tari_app_utilities", "tari_common", + "tari_common_types", "tari_comms", "tari_core", "tari_crypto", @@ -5348,7 +5381,7 @@ name = "tari_wallet" version = "0.36.0" dependencies = [ "aes-gcm 0.10.1", - "argon2", + "argon2 0.2.4", "async-trait", "bincode", "blake2 0.9.2", diff --git a/applications/tari_app_grpc/Cargo.toml b/applications/tari_app_grpc/Cargo.toml index eeaed43743..3922f49470 100644 --- a/applications/tari_app_grpc/Cargo.toml +++ b/applications/tari_app_grpc/Cargo.toml @@ -8,18 +8,25 @@ version = "0.36.0" edition = "2018" [dependencies] -tari_common_types = { version = "^0.36", path = "../../base_layer/common_types"} -tari_comms = { path = "../../comms/core"} -tari_core = { path = "../../base_layer/core"} +tari_common_types = { version = "^0.36", path = "../../base_layer/common_types" } +tari_comms = { path = "../../comms/core" } +tari_core = { path = "../../base_layer/core" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", tag = "v0.15.4" } tari_script = { path = "../../infrastructure/tari_script" } tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", tag = "v0.4.5" } +argon2 = { version = "0.4.1", features = ["std"] } +base64 = "0.13.0" chrono = { version = "0.4.19", default-features = false } +digest = "0.9" +log = "0.4" +num-traits = "0.2.15" prost = "0.9" prost-types = "0.9" -num-traits = "0.2.15" +rand = "0.8" +thiserror = "1" tonic = "0.6.2" +zeroize = "1.5" [build-dependencies] tonic-build = "0.6.2" diff --git a/applications/tari_app_grpc/src/authentication/basic_auth.rs b/applications/tari_app_grpc/src/authentication/basic_auth.rs new file mode 100644 index 0000000000..36e6acfe30 --- /dev/null +++ b/applications/tari_app_grpc/src/authentication/basic_auth.rs @@ -0,0 +1,180 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::{borrow::Cow, string::FromUtf8Error}; + +use argon2::{password_hash::Encoding, Argon2, PasswordHash, PasswordVerifier}; +use tari_utilities::SafePassword; +use zeroize::{Zeroize, Zeroizing}; + +/// Implements [RFC 2617](https://www.ietf.org/rfc/rfc2617.txt#:~:text=The%20%22basic%22%20authentication%20scheme%20is,other%20realms%20on%20that%20server.) +/// Represents the username and password contained within a Authenticate header. +#[derive(Debug)] +pub struct BasicAuthCredentials { + pub user_name: String, + pub password: SafePassword, +} + +impl BasicAuthCredentials { + pub fn new(user_name: String, password: SafePassword) -> Self { + Self { user_name, password } + } + + /// Creates a `Credentials` instance from a base64 `String` + /// which must encode user credentials as `username:password` + pub fn decode(auth_header_value: &str) -> Result { + let decoded = base64::decode(auth_header_value)?; + let as_utf8 = Zeroizing::new(String::from_utf8(decoded)?); + + if let Some((user_name, password)) = as_utf8.split_once(':') { + let credentials = Self::new(user_name.into(), password.to_string().into()); + return Ok(credentials); + } + + Err(BasicAuthError::InvalidAuthorizationHeader) + } + + /// Creates a `Credentials` instance from an HTTP Authorization header + /// which schema is a valid `Basic` HTTP Authorization Schema. + pub fn from_header(auth_header: &str) -> Result { + // check if its a valid basic auth header + let (auth_type, encoded_credentials) = auth_header + .split_once(' ') + .ok_or(BasicAuthError::InvalidAuthorizationHeader)?; + + if encoded_credentials.contains(' ') { + // Invalid authorization token received + return Err(BasicAuthError::InvalidAuthorizationHeader); + } + + // Check the provided authorization header + // to be a "Basic" authorization header + if auth_type.to_lowercase() != "basic" { + return Err(BasicAuthError::InvalidScheme(auth_type.to_string())); + } + + let credentials = BasicAuthCredentials::decode(encoded_credentials)?; + Ok(credentials) + } + + pub fn validate(&self, username: &str, password: &[u8]) -> Result<(), BasicAuthError> { + if self.user_name.as_bytes() != username.as_bytes() { + return Err(BasicAuthError::InvalidUsername); + } + // These bytes can leak if the password is not utf-8, but since argon encoding is utf-8 the given + // password must be incorrect if conversion to utf-8 fails. + let bytes = self.password.reveal().to_vec(); + let str_password = Zeroizing::new(String::from_utf8(bytes)?); + let header_password = PasswordHash::parse(&str_password, Encoding::B64)?; + Argon2::default().verify_password(password, &header_password)?; + Ok(()) + } + + pub fn generate_header(username: &str, password: &[u8]) -> Result { + let password_str = String::from_utf8_lossy(password); + let token_str = Zeroizing::new(format!("{}:{}", username, password_str)); + let mut token = base64::encode(token_str); + let header = format!("Basic {}", token); + token.zeroize(); + match password_str { + Cow::Borrowed(_) => {}, + Cow::Owned(mut owned) => owned.zeroize(), + } + Ok(header) + } +} + +/// Authorization Header Error +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum BasicAuthError { + #[error("Invalid username")] + InvalidUsername, + #[error("The HTTP Authorization header value is invalid")] + InvalidAuthorizationHeader, + #[error("The HTTP Authorization header contains an invalid scheme {0} but only `Basic` is supported")] + InvalidScheme(String), + #[error("The value expected as a base64 encoded `String` is not encoded correctly: {0}")] + InvalidBase64Value(#[from] base64::DecodeError), + #[error("The provided binary is not a valid UTF-8 character: {0}")] + InvalidUtf8Value(#[from] FromUtf8Error), + #[error("Invalid password: {0}")] + InvalidPassword(#[from] argon2::password_hash::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + mod from_header { + use super::*; + + #[test] + fn it_decodes_from_well_formed_header() { + let credentials = BasicAuthCredentials::from_header("Basic YWRtaW46c2VjcmV0").unwrap(); + assert_eq!(credentials.user_name, "admin"); + assert_eq!(credentials.password.reveal(), b"secret"); + } + + #[test] + fn it_rejects_header_without_basic_scheme() { + let err = BasicAuthCredentials::from_header(" YWRtaW46c2VjcmV0").unwrap_err(); + assert_eq!(err, BasicAuthError::InvalidScheme("".to_string())); + let err = BasicAuthCredentials::from_header("Cookie YWRtaW46c2VjcmV0").unwrap_err(); + assert_eq!(err, BasicAuthError::InvalidScheme("Cookie".to_string())); + } + } + + mod validate { + use super::*; + use crate::authentication::salted_password::create_salted_hashed_password; + + #[test] + fn it_validates_for_matching_credentials() { + let hashed = create_salted_hashed_password(b"secret").unwrap(); + let credentials = BasicAuthCredentials::new("admin".to_string(), hashed.to_string().into()); + credentials.validate("admin", b"secret").unwrap(); + } + + #[test] + fn it_rejects_for_mismatching_credentials() { + let credentials = BasicAuthCredentials::new("admin".to_string(), "bruteforce".to_string().into()); + let err = credentials.validate("admin", b"secret").unwrap_err(); + assert!(matches!(err, BasicAuthError::InvalidPassword(_))); + + let credentials = BasicAuthCredentials::new("bruteforce".to_string(), "secret".to_string().into()); + let err = credentials.validate("admin", b"secret").unwrap_err(); + assert_eq!(err, BasicAuthError::InvalidUsername); + } + } + + mod generate_header { + use super::*; + + #[test] + fn it_generates_a_valid_header() { + let header = BasicAuthCredentials::generate_header("admin", b"secret").unwrap(); + let cred = BasicAuthCredentials::from_header(&header).unwrap(); + assert_eq!(cred.user_name, "admin"); + assert_eq!(cred.password.reveal(), &b"secret"[..]); + } + } +} diff --git a/applications/tari_app_grpc/src/authentication/client_interceptor.rs b/applications/tari_app_grpc/src/authentication/client_interceptor.rs new file mode 100644 index 0000000000..47c75a9156 --- /dev/null +++ b/applications/tari_app_grpc/src/authentication/client_interceptor.rs @@ -0,0 +1,65 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use tari_common_types::grpc_authentication::GrpcAuthentication; +use tonic::{ + codegen::http::header::AUTHORIZATION, + metadata::{Ascii, MetadataValue}, + service::Interceptor, + Request, + Status, +}; + +use crate::authentication::{BasicAuthCredentials, BasicAuthError}; + +#[derive(Debug, Clone)] +pub struct ClientAuthenticationInterceptor { + authorization_header: Option>, +} + +impl ClientAuthenticationInterceptor { + pub fn create(auth: &GrpcAuthentication) -> Result { + let authorization_header = match auth { + GrpcAuthentication::None => None, + GrpcAuthentication::Basic { username, password } => Some( + BasicAuthCredentials::generate_header(username, password.reveal())? + .parse() + .unwrap(), + ), + }; + Ok(Self { authorization_header }) + } +} + +impl Interceptor for ClientAuthenticationInterceptor { + fn call(&mut self, mut request: Request<()>) -> Result, Status> { + match self.authorization_header.clone() { + Some(authorization_header) => { + request + .metadata_mut() + .insert(AUTHORIZATION.as_str(), authorization_header); + Ok(request) + }, + None => Ok(request), + } + } +} diff --git a/applications/tari_app_grpc/src/authentication/mod.rs b/applications/tari_app_grpc/src/authentication/mod.rs new file mode 100644 index 0000000000..cc5715afd0 --- /dev/null +++ b/applications/tari_app_grpc/src/authentication/mod.rs @@ -0,0 +1,32 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod client_interceptor; +pub use client_interceptor::ClientAuthenticationInterceptor; + +mod basic_auth; +pub use basic_auth::{BasicAuthCredentials, BasicAuthError}; + +pub mod salted_password; + +mod server_interceptor; +pub use server_interceptor::ServerAuthenticationInterceptor; diff --git a/applications/tari_app_grpc/src/authentication/salted_password.rs b/applications/tari_app_grpc/src/authentication/salted_password.rs new file mode 100644 index 0000000000..1d496902e2 --- /dev/null +++ b/applications/tari_app_grpc/src/authentication/salted_password.rs @@ -0,0 +1,36 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that +// the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the +// following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +// DAMAGE. +use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use rand::rngs::OsRng; +use zeroize::Zeroizing; + +pub fn create_salted_hashed_password(password: &[u8]) -> argon2::password_hash::Result> { + let argon2 = Argon2::default(); + // Generate a random salt + let passphrase_salt = SaltString::generate(&mut OsRng); + + // Hash the password, this is placed in the configuration file + let hashed_password = argon2.hash_password(password, &passphrase_salt)?; + + Ok(Zeroizing::new(hashed_password.to_string())) +} diff --git a/applications/tari_app_grpc/src/authentication/server_interceptor.rs b/applications/tari_app_grpc/src/authentication/server_interceptor.rs new file mode 100644 index 0000000000..f38a6d3138 --- /dev/null +++ b/applications/tari_app_grpc/src/authentication/server_interceptor.rs @@ -0,0 +1,85 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use log::*; +use tari_common_types::grpc_authentication::GrpcAuthentication; +use tonic::{codegen::http::header::AUTHORIZATION, service::Interceptor, Request, Status}; + +use crate::authentication::BasicAuthCredentials; + +const LOG_TARGET: &str = "applications::tari_app_grpc::authentication"; + +#[derive(Debug)] +pub struct ServerAuthenticationInterceptor { + auth: GrpcAuthentication, +} + +impl ServerAuthenticationInterceptor { + pub fn new(auth: GrpcAuthentication) -> Self { + Self { auth } + } + + fn handle_basic_auth( + &self, + req: Request<()>, + valid_username: &str, + valid_password: &[u8], + ) -> Result, Status> { + match req.metadata().get(AUTHORIZATION.as_str()) { + Some(t) => { + let val = t.to_str().map_err(unauthenticated)?; + let credentials = BasicAuthCredentials::from_header(val).map_err(unauthenticated)?; + credentials + .validate(valid_username, valid_password) + .map_err(unauthenticated)?; + Ok(req) + }, + _ => Err(unauthenticated("Missing authorization header")), + } + } +} + +impl Interceptor for ServerAuthenticationInterceptor { + fn call(&mut self, request: Request<()>) -> Result, Status> { + match &self.auth { + GrpcAuthentication::None => Ok(request), + GrpcAuthentication::Basic { + username, + password: hashed_password, + } => self.handle_basic_auth(request, username, hashed_password.reveal()), + } + } +} + +impl Clone for ServerAuthenticationInterceptor { + fn clone(&self) -> Self { + Self { + auth: self.auth.clone(), + } + } +} + +/// Standard unauthenticated response +fn unauthenticated(err: E) -> Status { + warn!(target: LOG_TARGET, "GRPC authentication failed: {}", err.to_string()); + Status::unauthenticated("Auth failed") +} diff --git a/applications/tari_app_grpc/src/lib.rs b/applications/tari_app_grpc/src/lib.rs index be52104505..6cf3715887 100644 --- a/applications/tari_app_grpc/src/lib.rs +++ b/applications/tari_app_grpc/src/lib.rs @@ -19,6 +19,8 @@ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +pub mod authentication; + pub mod conversions; pub mod tari_rpc { diff --git a/applications/tari_console_wallet/Cargo.toml b/applications/tari_console_wallet/Cargo.toml index 114d1ee175..91ec81aada 100644 --- a/applications/tari_console_wallet/Cargo.toml +++ b/applications/tari_console_wallet/Cargo.toml @@ -26,24 +26,23 @@ tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", t # Uncomment for normal use (non tokio-console tracing) tokio = { version = "1.14", default-features = false, features = ["signal", "sync"] } -sha2 = "0.9.5" -digest = "0.9.0" +base64 = "0.13.0" +bitflags = "1.2.1" +chrono = { version = "0.4.19", default-features = false } clap = { version = "3.1.1", features = ["derive", "env"] } config = "0.13.0" -chrono = { version = "0.4.19", default-features = false } -bitflags = "1.2.1" -futures = { version = "^0.3.16", default-features = false, features = ["alloc"] } crossterm = { version = "0.17" } -rand = "0.8" -unicode-width = "0.1" -unicode-segmentation = "1.6.0" +digest = "0.9.0" +futures = { version = "^0.3.16", default-features = false, features = ["alloc"] } log = { version = "0.4.8", features = ["std"] } qrcode = { version = "0.12" } +rand = "0.8" regex = "1.5.4" rpassword = "5.0" rustyline = "9.0" serde = "1.0.136" serde_json = "1.0.79" +sha2 = "0.9.5" strum = "0.22" strum_macros = "0.22" thiserror = "1.0.26" @@ -51,6 +50,8 @@ tonic = "0.6.2" tracing = "0.1.26" tracing-opentelemetry = "0.15.0" tracing-subscriber = "0.2.20" +unicode-segmentation = "1.6.0" +unicode-width = "0.1" # network tracing, rt-tokio for async batch export opentelemetry = { version = "0.16", default-features = false, features = ["trace", "rt-tokio"] } diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 0ba07e6de1..4e686bb468 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -37,6 +37,7 @@ use log::*; use serde::{de::DeserializeOwned, Serialize}; use sha2::Sha256; use strum_macros::{Display, EnumIter, EnumString}; +use tari_app_grpc::authentication::salted_password::create_salted_hashed_password; use tari_common_types::{ emoji::EmojiId, transaction::TxId, @@ -775,6 +776,25 @@ pub async fn command_runner( .await .map_err(CommandError::TransactionServiceError)?; }, + HashGrpcPassword(args) => { + let (username, password) = config + .grpc_authentication + .username_password() + .ok_or_else(|| CommandError::General("GRPC basic auth is not configured".to_string()))?; + let hashed_password = create_salted_hashed_password(password.reveal()) + .map_err(|e| CommandError::General(e.to_string()))?; + if args.short { + println!("{}", *hashed_password); + } else { + println!("Your hashed password is:"); + println!("{}", *hashed_password); + println!(); + println!( + "Use HTTP basic auth with username '{}' and the hashed password to make GRPC requests", + username + ); + } + }, } } diff --git a/applications/tari_console_wallet/src/cli.rs b/applications/tari_console_wallet/src/cli.rs index 145e166e33..3bb56faa94 100644 --- a/applications/tari_console_wallet/src/cli.rs +++ b/applications/tari_console_wallet/src/cli.rs @@ -82,6 +82,10 @@ pub(crate) struct Cli { /// Supply a network (overrides existing configuration) #[clap(long, env = "TARI_NETWORK")] pub network: Option, + #[clap(long, env = "TARI_WALLET_ENABLE_GRPC", alias = "enable-grpc")] + pub grpc_enabled: bool, + #[clap(long, env = "TARI_WALLET_GRPC_ADDRESS")] + pub grpc_address: Option, #[clap(subcommand)] pub command2: Option, } @@ -93,6 +97,15 @@ impl ConfigOverrideProvider for Cli { overrides.push(("wallet.network".to_string(), network.clone())); overrides.push(("wallet.override_from".to_string(), network.clone())); overrides.push(("p2p.seeds.override_from".to_string(), network)); + // Either of these configs enable grpc + if let Some(ref addr) = self.grpc_address { + overrides.push(("wallet.grpc_enabled".to_string(), "true".to_string())); + overrides.push(("wallet.grpc_address".to_string(), addr.clone())); + } else if self.grpc_enabled { + overrides.push(("wallet.grpc_enabled".to_string(), "true".to_string())); + } else { + // GRPC is disabled + } overrides } } @@ -118,6 +131,7 @@ pub enum CliCommands { FinaliseShaAtomicSwap(FinaliseShaAtomicSwapArgs), ClaimShaAtomicSwapRefund(ClaimShaAtomicSwapRefundArgs), RevalidateWalletDb, + HashGrpcPassword(HashPasswordArgs), } #[derive(Debug, Args, Clone)] @@ -240,3 +254,9 @@ pub struct ClaimShaAtomicSwapRefundArgs { #[clap(short, long, default_value = "Claimed HTLC atomic swap refund")] pub message: String, } + +#[derive(Debug, Args, Clone)] +pub struct HashPasswordArgs { + /// If true, only output the hashed password and the salted password. Otherwise a usage explanation is output. + pub short: bool, +} diff --git a/applications/tari_console_wallet/src/wallet_modes.rs b/applications/tari_console_wallet/src/wallet_modes.rs index 197ff164b2..6d287138b8 100644 --- a/applications/tari_console_wallet/src/wallet_modes.rs +++ b/applications/tari_console_wallet/src/wallet_modes.rs @@ -25,7 +25,9 @@ use std::{fs, io::Stdout, path::PathBuf}; use clap::Parser; use log::*; use rand::{rngs::OsRng, seq::SliceRandom}; +use tari_app_grpc::authentication::ServerAuthenticationInterceptor; use tari_common::exit_codes::{ExitCode, ExitError}; +use tari_common_types::grpc_authentication::GrpcAuthentication; use tari_comms::{multiaddr::Multiaddr, peer_manager::Peer, utils::multiaddr::multiaddr_to_socketaddr}; use tari_wallet::{WalletConfig, WalletSqlite}; use tokio::{runtime::Handle, sync::broadcast}; @@ -261,9 +263,13 @@ pub fn tui_mode( mut wallet: WalletSqlite, ) -> Result<(), ExitError> { let (events_broadcaster, _events_listener) = broadcast::channel(100); - if let Some(ref grpc_address) = config.grpc_address { + if config.grpc_enabled { let grpc = WalletGrpcServer::new(wallet.clone()); - handle.spawn(run_grpc(grpc, grpc_address.clone())); + handle.spawn(run_grpc( + grpc, + config.grpc_address.clone(), + config.grpc_authentication.clone(), + )); } let notifier = Notifier::new( @@ -360,27 +366,35 @@ pub fn recovery_mode( pub fn grpc_mode(handle: Handle, config: &WalletConfig, wallet: WalletSqlite) -> Result<(), ExitError> { info!(target: LOG_TARGET, "Starting grpc server"); - if let Some(grpc_address) = &config.grpc_address { + if config.grpc_enabled { let grpc = WalletGrpcServer::new(wallet); + let auth = config.grpc_authentication.clone(); handle - .block_on(run_grpc(grpc, grpc_address.clone())) + .block_on(run_grpc(grpc, config.grpc_address.clone(), auth)) .map_err(|e| ExitError::new(ExitCode::GrpcError, e))?; } else { - println!("No grpc address specified"); + println!("GRPC server is disabled"); } info!(target: LOG_TARGET, "Shutting down"); Ok(()) } -async fn run_grpc(grpc: WalletGrpcServer, grpc_console_wallet_address: Multiaddr) -> Result<(), String> { +async fn run_grpc( + grpc: WalletGrpcServer, + grpc_listener_addr: Multiaddr, + auth_config: GrpcAuthentication, +) -> Result<(), String> { // Do not remove this println! const CUCUMBER_TEST_MARKER_A: &str = "Tari Console Wallet running... (gRPC mode started)"; println!("{}", CUCUMBER_TEST_MARKER_A); - info!(target: LOG_TARGET, "Starting GRPC on {}", grpc_console_wallet_address); - let address = multiaddr_to_socketaddr(&grpc_console_wallet_address).map_err(|e| e.to_string())?; + info!(target: LOG_TARGET, "Starting GRPC on {}", grpc_listener_addr); + let address = multiaddr_to_socketaddr(&grpc_listener_addr).map_err(|e| e.to_string())?; + let auth = ServerAuthenticationInterceptor::new(auth_config); + let service = tari_app_grpc::tari_rpc::wallet_server::WalletServer::with_interceptor(grpc, auth); + Server::builder() - .add_service(tari_app_grpc::tari_rpc::wallet_server::WalletServer::new(grpc)) + .add_service(service) .serve(address) .await .map_err(|e| format!("GRPC server returned error:{}", e))?; @@ -448,6 +462,7 @@ mod test { CliCommands::FinaliseShaAtomicSwap(_) => {}, CliCommands::ClaimShaAtomicSwapRefund(_) => {}, CliCommands::RevalidateWalletDb => {}, + CliCommands::HashGrpcPassword(_) => {}, } } assert!(get_balance && send_tari && make_it_rain && coin_split && discover_peer && whois); diff --git a/applications/tari_miner/Cargo.toml b/applications/tari_miner/Cargo.toml index 1db7135b6b..1958d4080c 100644 --- a/applications/tari_miner/Cargo.toml +++ b/applications/tari_miner/Cargo.toml @@ -8,11 +8,12 @@ version = "0.36.0" edition = "2018" [dependencies] -tari_core = { path = "../../base_layer/core", default-features = false } -tari_common = { path = "../../common" } -tari_comms = { path = "../../comms/core" } -tari_app_utilities = { path = "../tari_app_utilities"} -tari_app_grpc = { path = "../tari_app_grpc" } +tari_core = { path = "../../base_layer/core", default-features = false } +tari_common = { path = "../../common" } +tari_common_types = { path = "../../base_layer/common_types" } +tari_comms = { path = "../../comms/core" } +tari_app_utilities = { path = "../tari_app_utilities" } +tari_app_grpc = { path = "../tari_app_grpc" } tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", tag = "v0.15.4" } tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", tag = "v0.4.5" } @@ -29,7 +30,7 @@ serde = { version = "1.0", default_features = false, features = ["derive"] } tonic = { version = "0.6.2", features = ["transport"] } tokio = { version = "1.11", default_features = false, features = ["rt-multi-thread"] } thiserror = "1.0" -reqwest = { version = "0.11", features = [ "json"] } +reqwest = { version = "0.11", features = ["json"] } serde_json = "1.0.57" native-tls = "0.2" bufstream = "0.1" diff --git a/applications/tari_miner/src/config.rs b/applications/tari_miner/src/config.rs index b28d9f7c3f..a6f70099bf 100644 --- a/applications/tari_miner/src/config.rs +++ b/applications/tari_miner/src/config.rs @@ -41,6 +41,7 @@ use std::{str::FromStr, time::Duration}; use serde::{Deserialize, Serialize}; use tari_app_grpc::tari_rpc::{pow_algo::PowAlgos, NewBlockTemplateRequest, PowAlgo}; use tari_common::SubConfigPath; +use tari_common_types::grpc_authentication::GrpcAuthentication; use tari_comms::multiaddr::Multiaddr; #[derive(Serialize, Deserialize, Debug)] @@ -50,6 +51,8 @@ pub struct MinerConfig { pub base_node_grpc_address: Multiaddr, /// GRPC address of console wallet pub wallet_grpc_address: Multiaddr, + /// GRPC authentication for console wallet + pub wallet_grpc_authentication: GrpcAuthentication, /// Number of mining threads pub num_mining_threads: usize, /// Start mining only when base node is bootstrapped and current block height is on the tip of network @@ -85,6 +88,7 @@ impl Default for MinerConfig { Self { base_node_grpc_address: Multiaddr::from_str("/ip4/127.0.0.1/tcp/18142").unwrap(), wallet_grpc_address: Multiaddr::from_str("/ip4/127.0.0.1/tcp/18143").unwrap(), + wallet_grpc_authentication: GrpcAuthentication::default(), num_mining_threads: num_cpus::get(), mine_on_tip_only: true, proof_of_work_algo: ProofOfWork::Sha3, diff --git a/applications/tari_miner/src/errors.rs b/applications/tari_miner/src/errors.rs index 9dc4c304da..522d648310 100644 --- a/applications/tari_miner/src/errors.rs +++ b/applications/tari_miner/src/errors.rs @@ -20,7 +20,9 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // +use tari_app_grpc::authentication::BasicAuthError; use thiserror::Error; +use tonic::codegen::http::uri::InvalidUri; #[derive(Debug, Error)] pub enum MinerError { @@ -42,6 +44,10 @@ pub enum MinerError { BlockHeader(String), #[error("Conversion error: {0}")] Conversion(String), + #[error("Invalid grpc credentials: {0}")] + BasicAuthError(#[from] BasicAuthError), + #[error("Invalid grpc url: {0}")] + InvalidUri(#[from] InvalidUri), } pub fn err_empty(name: &str) -> MinerError { diff --git a/applications/tari_miner/src/main.rs b/applications/tari_miner/src/main.rs index 2515c1701b..06a48ac577 100644 --- a/applications/tari_miner/src/main.rs +++ b/applications/tari_miner/src/main.rs @@ -23,6 +23,7 @@ use std::{ convert::TryFrom, io::{stdout, Write}, + str::FromStr, thread, time::Instant, }; @@ -33,7 +34,10 @@ use errors::{err_empty, MinerError}; use futures::stream::StreamExt; use log::*; use miner::Miner; -use tari_app_grpc::tari_rpc::{base_node_client::BaseNodeClient, wallet_client::WalletClient}; +use tari_app_grpc::{ + authentication::ClientAuthenticationInterceptor, + tari_rpc::{base_node_client::BaseNodeClient, wallet_client::WalletClient}, +}; use tari_app_utilities::consts; use tari_common::{ exit_codes::{ExitCode, ExitError}, @@ -46,7 +50,10 @@ use tari_core::blocks::BlockHeader; use tari_crypto::ristretto::RistrettoPublicKey; use tari_utilities::hex::Hex; use tokio::{runtime::Runtime, time::sleep}; -use tonic::transport::Channel; +use tonic::{ + codegen::InterceptedService, + transport::{Channel, Endpoint}, +}; use utils::{coinbase_request, extract_outputs_and_kernels}; use crate::{cli::Cli, config::MinerConfig, miner::MiningReport, stratum::stratum_controller::controller::Controller}; @@ -62,6 +69,8 @@ mod miner; mod stratum; mod utils; +type WalletGrpcClient = WalletClient>; + /// Application entry point fn main() { let rt = Runtime::new().expect("Failed to start tokio runtime"); @@ -194,20 +203,30 @@ async fn main_inner() -> Result<(), ExitError> { } } -async fn connect(config: &MinerConfig) -> Result<(BaseNodeClient, WalletClient), MinerError> { +async fn connect(config: &MinerConfig) -> Result<(BaseNodeClient, WalletGrpcClient), MinerError> { let base_node_addr = multiaddr_to_socketaddr(&config.base_node_grpc_address)?; info!(target: LOG_TARGET, "🔗 Connecting to base node at {}", base_node_addr); let node_conn = BaseNodeClient::connect(format!("http://{}", base_node_addr)).await?; - let wallet_addr = multiaddr_to_socketaddr(&config.wallet_grpc_address)?; - info!(target: LOG_TARGET, "👛 Connecting to wallet at {}", wallet_addr); - let wallet_conn = WalletClient::connect(format!("http://{}", wallet_addr)).await?; + let wallet_conn = connect_wallet(config).await?; Ok((node_conn, wallet_conn)) } +async fn connect_wallet(config: &MinerConfig) -> Result { + let wallet_addr = format!("http://{}", multiaddr_to_socketaddr(&config.wallet_grpc_address)?); + info!(target: LOG_TARGET, "👛 Connecting to wallet at {}", wallet_addr); + let channel = Endpoint::from_str(&wallet_addr)?.connect().await?; + let wallet_conn = WalletClient::with_interceptor( + channel, + ClientAuthenticationInterceptor::create(&config.wallet_grpc_authentication)?, + ); + + Ok(wallet_conn) +} + async fn mining_cycle( node_conn: &mut BaseNodeClient, - wallet_conn: &mut WalletClient, + wallet_conn: &mut WalletGrpcClient, config: &MinerConfig, cli: &Cli, ) -> Result { diff --git a/base_layer/common_types/Cargo.toml b/base_layer/common_types/Cargo.toml index 86dd07c7ae..581a7270bb 100644 --- a/base_layer/common_types/Cargo.toml +++ b/base_layer/common_types/Cargo.toml @@ -10,9 +10,11 @@ edition = "2018" tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", tag = "v0.15.4" } tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", tag = "v0.4.5" } +base64 = "0.13.0" digest = "0.9.0" lazy_static = "1.4.0" rand = "0.8" serde = { version = "1.0.106", features = ["derive"] } thiserror = "1.0.29" tokio = { version = "1.11", features = ["time", "sync"] } +zeroize = "1" diff --git a/base_layer/common_types/src/grpc_authentication.rs b/base_layer/common_types/src/grpc_authentication.rs new file mode 100644 index 0000000000..073c8d6224 --- /dev/null +++ b/base_layer/common_types/src/grpc_authentication.rs @@ -0,0 +1,51 @@ +// Copyright 2022. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use serde::{Deserialize, Serialize}; +use tari_utilities::SafePassword; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GrpcAuthentication { + #[default] + None, + Basic { + username: String, + #[serde(deserialize_with = "deserialize_safe_password")] + password: SafePassword, + }, +} + +impl GrpcAuthentication { + pub fn username_password(&self) -> Option<(&str, &SafePassword)> { + match self { + GrpcAuthentication::Basic { username, password } => Some((username, password)), + _ => None, + } + } +} + +fn deserialize_safe_password<'de, D>(deserializer: D) -> Result +where D: serde::Deserializer<'de> { + let password: String = Deserialize::deserialize(deserializer)?; + Ok(SafePassword::from(password)) +} diff --git a/base_layer/common_types/src/lib.rs b/base_layer/common_types/src/lib.rs index 728c50ede6..e0a0ee4310 100644 --- a/base_layer/common_types/src/lib.rs +++ b/base_layer/common_types/src/lib.rs @@ -22,6 +22,7 @@ pub mod chain_metadata; pub mod emoji; +pub mod grpc_authentication; pub mod luhn; pub mod transaction; mod tx_id; diff --git a/base_layer/core/src/lib.rs b/base_layer/core/src/lib.rs index 77b6439910..da8a129755 100644 --- a/base_layer/core/src/lib.rs +++ b/base_layer/core/src/lib.rs @@ -68,41 +68,46 @@ pub mod large_ints { } pub use large_ints::{U256, U512}; -use tari_crypto::{hash::blake2::Blake256, hash_domain, hashing::DomainSeparatedHasher}; -use tari_mmr::{pruned_hashset::PrunedHashSet, Hash, MerkleMountainRange, MutableMmr}; +#[cfg(feature = "base_node")] +mod domain_hashing { + use tari_crypto::{hash::blake2::Blake256, hash_domain, hashing::DomainSeparatedHasher}; + use tari_mmr::{pruned_hashset::PrunedHashSet, Hash, MerkleMountainRange, MutableMmr}; -hash_domain!( - KernelMmrHashDomain, - "com.tari.tari_project.base_layer.core.kernel_mmr", - 1 -); -pub type KernelMmrHasherBlake256 = DomainSeparatedHasher; -pub type KernelMmr = MerkleMountainRange>; -pub type PrunedKernelMmr = MerkleMountainRange; + hash_domain!( + KernelMmrHashDomain, + "com.tari.tari_project.base_layer.core.kernel_mmr", + 1 + ); + pub type KernelMmrHasherBlake256 = DomainSeparatedHasher; + pub type KernelMmr = MerkleMountainRange>; + pub type PrunedKernelMmr = MerkleMountainRange; -hash_domain!( - WitnessMmrHashDomain, - "com.tari.tari_project.base_layer.core.witness_mmr", - 1 -); -pub type WitnessMmrHasherBlake256 = DomainSeparatedHasher; -pub type WitnessMmr = MerkleMountainRange>; -pub type PrunedWitnessMmr = MerkleMountainRange; + hash_domain!( + WitnessMmrHashDomain, + "com.tari.tari_project.base_layer.core.witness_mmr", + 1 + ); + pub type WitnessMmrHasherBlake256 = DomainSeparatedHasher; + pub type WitnessMmr = MerkleMountainRange>; + pub type PrunedWitnessMmr = MerkleMountainRange; -hash_domain!( - OutputMmrHashDomain, - "com.tari.tari_project.base_layer.core.output_mmr", - 1 -); -pub type OutputMmrHasherBlake256 = DomainSeparatedHasher; -pub type MutableOutputMmr = MutableMmr>; -pub type PrunedOutputMmr = MerkleMountainRange; -pub type MutablePrunedOutputMmr = MutableMmr; + hash_domain!( + OutputMmrHashDomain, + "com.tari.tari_project.base_layer.core.output_mmr", + 1 + ); + pub type OutputMmrHasherBlake256 = DomainSeparatedHasher; + pub type MutableOutputMmr = MutableMmr>; + pub type PrunedOutputMmr = MerkleMountainRange; + pub type MutablePrunedOutputMmr = MutableMmr; -hash_domain!( - InputMmrHashDomain, - "com.tari.tari_project.base_layer.core.output_mmr", - 1 -); -pub type InputMmrHasherBlake256 = DomainSeparatedHasher; -pub type PrunedInputMmr = MerkleMountainRange; + hash_domain!( + InputMmrHashDomain, + "com.tari.tari_project.base_layer.core.output_mmr", + 1 + ); + pub type InputMmrHasherBlake256 = DomainSeparatedHasher; + pub type PrunedInputMmr = MerkleMountainRange; +} +#[cfg(feature = "base_node")] +pub use domain_hashing::*; diff --git a/base_layer/wallet/src/config.rs b/base_layer/wallet/src/config.rs index b480833cfb..8d13f1d328 100644 --- a/base_layer/wallet/src/config.rs +++ b/base_layer/wallet/src/config.rs @@ -32,6 +32,7 @@ use tari_common::{ configuration::{serializers, Network, StringList}, SubConfigPath, }; +use tari_common_types::grpc_authentication::GrpcAuthentication; use tari_comms::multiaddr::Multiaddr; use tari_p2p::P2pConfig; use tari_utilities::SafePassword; @@ -88,8 +89,12 @@ pub struct WalletConfig { /// transaction events are received by the console wallet . /// (see example at 'applications/tari_console_wallet/src/notifier/notify_example.sh') pub notify_file: Option, - /// GRPC address of base node - pub grpc_address: Option, + /// If true, a GRPC server will bind to the configured address and listen for incoming GRPC requests. + pub grpc_enabled: bool, + /// GRPC bind address of the wallet + pub grpc_address: Multiaddr, + /// GRPC authentication mode + pub grpc_authentication: GrpcAuthentication, /// A custom base node peer that will be used to obtain metadata from pub custom_base_node: Option, /// A list of base node peers that the wallet should use for service requests and tracking chain state @@ -131,7 +136,9 @@ impl Default for WalletConfig { command_send_wait_stage: TransactionStage::Broadcast, command_send_wait_timeout: Duration::from_secs(300), notify_file: None, - grpc_address: Some("/ip4/127.0.0.1/tcp/18143".parse().unwrap()), + grpc_enabled: false, + grpc_address: "/ip4/127.0.0.1/tcp/18143".parse().unwrap(), + grpc_authentication: GrpcAuthentication::default(), custom_base_node: None, base_node_service_peers: StringList::default(), recovery_retry_limit: 3, diff --git a/clients/wallet_grpc_client/index.js b/clients/wallet_grpc_client/index.js index 10c96b8866..f05c4cb24f 100644 --- a/clients/wallet_grpc_client/index.js +++ b/clients/wallet_grpc_client/index.js @@ -18,10 +18,10 @@ const packageDefinition = protoLoader.loadSync( const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); const tariGrpc = protoDescriptor.tari.rpc; -function connect(address) { +function connect(address, options = {}) { const client = new tariGrpc.Wallet( address, - grpc.credentials.createInsecure() + createAuth(options.authentication || {}), ); promisifyAll(client, { metadata: new grpc.Metadata() }); return client; @@ -66,6 +66,24 @@ function Client(address) { Client.connect = (address) => new Client(address); +function createAuth(auth = {}) { + if (auth.type === "basic") { + const { + username, + password + } = auth; + return grpc.credentials.createFromMetadataGenerator((params, callback) => { + const md = new grpc.Metadata(); + let token = new Buffer(`${username}:${password}`).toString("base64"); + md.set('authorization', 'Basic ' + token); + return callback(null, md); + }); + } else{ + return grpc.credentials.createInsecure(); + } + +} + module.exports = { Client, types: tariGrpc, diff --git a/common/config/presets/d_console_wallet.toml b/common/config/presets/d_console_wallet.toml index fd15573700..61c18c2c05 100644 --- a/common/config/presets/d_console_wallet.toml +++ b/common/config/presets/d_console_wallet.toml @@ -61,8 +61,12 @@ #command_send_wait_timeout = 300 #command_send_wait_stage = "Broadcast" +# Set to true to enable grpc. (default = false) +#grpc_enabled = false # The socket to expose for the gRPC base node server (default = "/ip4/127.0.0.1/tcp/18143") #grpc_address = "/ip4/127.0.0.1/tcp/18143" +# gRPC authentication method (default = "none") +#grpc_authentication = { username = "admin", password = "xxxx" } # A custom base node peer that will be used to obtain metadata from, example # "0eefb45a4de9484eca74846a4f47d2c8d38e76be1fec63b0112bd00d297c0928::/ip4/13.40.98.39/tcp/18189" diff --git a/common/config/presets/g_miner.toml b/common/config/presets/g_miner.toml index 258207d076..31e649f54e 100644 --- a/common/config/presets/g_miner.toml +++ b/common/config/presets/g_miner.toml @@ -12,6 +12,8 @@ # GRPC address of console wallet (default = "/ip4/127.0.0.1/tcp/18143") #wallet_grpc_address = "/ip4/127.0.0.1/tcp/18143" +# GRPC authentication for the console wallet (default = "none") +#wallet_grpc_authentication = { username: "miner", password: "$argon..." } # Number of mining threads (default: number of logical CPU cores) #num_mining_threads = 8 diff --git a/integration_tests/helpers/walletProcess.js b/integration_tests/helpers/walletProcess.js index 6806266187..75b95d297f 100644 --- a/integration_tests/helpers/walletProcess.js +++ b/integration_tests/helpers/walletProcess.js @@ -228,6 +228,7 @@ class WalletProcess { "--seed-words-file-name", this.seedWordsFile, "--non-interactive", + "--enable-grpc", "--network", opts.network || (this.options || {}).network || "localnet", ];