diff --git a/applications/minotari_app_grpc/src/authentication/basic_auth.rs b/applications/minotari_app_grpc/src/authentication/basic_auth.rs index 1891d92c1a..20d23eb7fa 100644 --- a/applications/minotari_app_grpc/src/authentication/basic_auth.rs +++ b/applications/minotari_app_grpc/src/authentication/basic_auth.rs @@ -34,12 +34,9 @@ use tari_utilities::{ByteArray, SafePassword}; use tonic::metadata::{errors::InvalidMetadataValue, Ascii, MetadataValue}; use zeroize::{Zeroize, Zeroizing}; -use crate::authentication::salted_password::create_salted_hashed_password; - const MAX_USERNAME_LEN: usize = 256; -/// 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. +/// Implements [RFC 2617](https://www.ietf.org/rfc/rfc2617.txt) by allowing authentication of provided credentials #[derive(Debug)] pub struct BasicAuthCredentials { /// The username bytes length @@ -82,42 +79,29 @@ impl BasicAuthCredentials { }) } - /// 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 hashed_password = create_salted_hashed_password(password.as_bytes())?; - let credentials = Self::new(user_name.into(), hashed_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 + /// Parses the contents of an HTTP Authorization (Basic) header into a username and password + /// These can be used later to validate against `BasicAuthCredentials` + /// The input must be of the form `Basic base64(username:password)` + pub fn parse_header(auth_header: &str) -> Result<(String, SafePassword), BasicAuthError> { + // Check that the authentication type is `Basic` 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) + // Decode the credentials using base64 + let decoded = base64::decode(encoded_credentials)?; + let as_utf8 = Zeroizing::new(String::from_utf8(decoded)?); + + // Parse the username and password, which must be separated by a colon + if let Some((user_name, password)) = as_utf8.split_once(':') { + return Ok((user_name.into(), password.into())); + } + + Err(BasicAuthError::InvalidAuthorizationHeader) } // This function provides a constant time comparison of the given username with the registered username. @@ -150,7 +134,7 @@ impl BasicAuthCredentials { /// do the same amount of work irrespective if the username or password is correct or not. This is to prevent timing /// attacks. Also, no distinction is made between a non-existent username or an incorrect password in the error /// that is returned. - pub fn constant_time_validate(&self, username: &str, password: &[u8]) -> Result<(), BasicAuthError> { + pub fn constant_time_validate(&self, username: &str, password: &SafePassword) -> Result<(), BasicAuthError> { let valid_username = self.constant_time_verify_username(username); // These bytes can leak if the password is not utf-8, but since argon encoding is utf-8 the given @@ -159,7 +143,9 @@ impl BasicAuthCredentials { let str_password = Zeroizing::new(String::from_utf8(bytes)?); let header_password = PasswordHash::parse(&str_password, Encoding::B64)?; let valid_password = Choice::from(u8::from( - Argon2::default().verify_password(password, &header_password).is_ok(), + Argon2::default() + .verify_password(password.reveal(), &header_password) + .is_ok(), )); // The use of `Choice` logic here is by design to hide the boolean logic from compiler optimizations. @@ -218,26 +204,20 @@ mod tests { #[test] fn it_decodes_from_well_formed_header() { - let credentials = BasicAuthCredentials::from_header("Basic YWRtaW46c2VjcmV0").unwrap(); - assert_eq!(credentials.user_name_bytes_length, "admin".as_bytes().len()); - let bytes = "admin".as_bytes(); - let mut user_name_bytes = [0u8; MAX_USERNAME_LEN]; - user_name_bytes[0..bytes.len()].clone_from_slice(bytes); - user_name_bytes[bytes.len()..MAX_USERNAME_LEN] - .clone_from_slice(&credentials.random_bytes[bytes.len()..MAX_USERNAME_LEN]); - assert_eq!(credentials.user_name_bytes, user_name_bytes); - assert!(credentials.constant_time_validate("admin", b"secret").is_ok()); + let (username, password) = BasicAuthCredentials::parse_header("Basic YWRtaW46c2VjcmV0").unwrap(); + assert_eq!(username, "admin".to_string()); + assert_eq!(password.reveal(), b"secret"); } #[test] fn it_rejects_header_without_basic_scheme() { - let err = BasicAuthCredentials::from_header(" YWRtaW46c2VjcmV0").unwrap_err(); + let err = BasicAuthCredentials::parse_header(" YWRtaW46c2VjcmV0").unwrap_err(); if let BasicAuthError::InvalidScheme(s) = err { assert_eq!(s, ""); } else { panic!("Unexpected error: {:?}", err); }; - let err = BasicAuthCredentials::from_header("Cookie YWRtaW46c2VjcmV0").unwrap_err(); + let err = BasicAuthCredentials::parse_header("Cookie YWRtaW46c2VjcmV0").unwrap_err(); if let BasicAuthError::InvalidScheme(s) = err { assert_eq!(s, "Cookie"); } else { @@ -264,10 +244,14 @@ mod tests { // Typical username let credentials = BasicAuthCredentials::new("admin".to_string(), hashed_password.to_string().into()).unwrap(); - credentials.constant_time_validate("admin", b"secret").unwrap(); + credentials + .constant_time_validate("admin", &SafePassword::from("secret".to_string())) + .unwrap(); // Empty username is also fine let credentials = BasicAuthCredentials::new("".to_string(), hashed_password.to_string().into()).unwrap(); - credentials.constant_time_validate("", b"secret").unwrap(); + credentials + .constant_time_validate("", &SafePassword::from("secret".to_string())) + .unwrap(); } #[test] @@ -286,16 +270,20 @@ mod tests { BasicAuthCredentials::new("admin".to_string(), hashed_password.to_string().into()).unwrap(); // Wrong password - let err = credentials.constant_time_validate("admin", b"bruteforce").unwrap_err(); + let err = credentials + .constant_time_validate("admin", &SafePassword::from("bruteforce".to_string())) + .unwrap_err(); assert!(matches!(err, BasicAuthError::InvalidUsernameOrPassword)); // Wrong username - let err = credentials.constant_time_validate("wrong_user", b"secret").unwrap_err(); + let err = credentials + .constant_time_validate("wrong_user", &SafePassword::from("secret".to_string())) + .unwrap_err(); assert!(matches!(err, BasicAuthError::InvalidUsernameOrPassword)); // Wrong username and password let err = credentials - .constant_time_validate("wrong_user", b"bruteforce") + .constant_time_validate("wrong_user", &SafePassword::from("bruteforce".to_string())) .unwrap_err(); assert!(matches!(err, BasicAuthError::InvalidUsernameOrPassword)); } @@ -561,21 +549,22 @@ mod tests { let start = Instant::now(); for short in &short_usernames { - let res = credentials.constant_time_validate(short, b"bruteforce"); + let res = credentials.constant_time_validate(short, &SafePassword::from("bruteforce".to_string())); assert!(res.is_err()); } let time_taken_1 = start.elapsed().as_millis(); let start = Instant::now(); for long in &long_usernames { - let res = credentials.constant_time_validate(long, b"bruteforce"); + let res = credentials.constant_time_validate(long, &SafePassword::from("bruteforce".to_string())); assert!(res.is_err()); } let time_taken_2 = start.elapsed().as_millis(); let start = Instant::now(); for _ in 0..COUNTS { - let res = credentials.constant_time_validate(username_actual, b"secret"); + let res = + credentials.constant_time_validate(username_actual, &SafePassword::from("secret".to_string())); assert!(res.is_ok()); } let time_taken_3 = start.elapsed().as_millis(); @@ -624,15 +613,9 @@ mod tests { #[test] fn it_generates_a_valid_header() { let header = BasicAuthCredentials::generate_header("admin", b"secret").unwrap(); - let cred = BasicAuthCredentials::from_header(header.to_str().unwrap()).unwrap(); - assert_eq!(cred.user_name_bytes_length, "admin".as_bytes().len()); - let bytes = "admin".as_bytes(); - let mut user_name_bytes = [0u8; MAX_USERNAME_LEN]; - user_name_bytes[0..bytes.len()].clone_from_slice(bytes); - user_name_bytes[bytes.len()..MAX_USERNAME_LEN] - .clone_from_slice(&cred.random_bytes[bytes.len()..MAX_USERNAME_LEN]); - assert_eq!(cred.user_name_bytes, user_name_bytes); - assert!(cred.constant_time_validate("admin", b"secret").is_ok()); + let (username, password) = BasicAuthCredentials::parse_header(header.to_str().unwrap()).unwrap(); + assert_eq!(username, "admin".to_string()); + assert_eq!(password.reveal(), b"secret"); } } } diff --git a/applications/minotari_app_grpc/src/authentication/server_interceptor.rs b/applications/minotari_app_grpc/src/authentication/server_interceptor.rs index 44466ee8de..ca7c0653da 100644 --- a/applications/minotari_app_grpc/src/authentication/server_interceptor.rs +++ b/applications/minotari_app_grpc/src/authentication/server_interceptor.rs @@ -22,35 +22,53 @@ use log::*; use tari_common_types::grpc_authentication::GrpcAuthentication; +use tari_utilities::SafePassword; use tonic::{codegen::http::header::AUTHORIZATION, service::Interceptor, Request, Status}; -use crate::authentication::BasicAuthCredentials; +use crate::authentication::{salted_password::create_salted_hashed_password, BasicAuthCredentials}; const LOG_TARGET: &str = "applications::minotari_app_grpc::authentication"; #[derive(Debug)] pub struct ServerAuthenticationInterceptor { - auth: GrpcAuthentication, + auth: GrpcAuthentication, // this contains a hashed PHC password in the case of basic authentication } impl ServerAuthenticationInterceptor { - pub fn new(auth: GrpcAuthentication) -> Self { - Self { auth } + pub fn new(auth: GrpcAuthentication) -> Option { + // The server password needs to be hashed for later client verification + let processed_auth = match auth { + GrpcAuthentication::None => auth, + GrpcAuthentication::Basic { username, password } => GrpcAuthentication::Basic { + username, + password: create_salted_hashed_password(password.reveal()).ok()?.as_str().into(), + }, + }; + + Some(Self { auth: processed_auth }) } fn handle_basic_auth( &self, req: Request<()>, valid_username: &str, - valid_password: &[u8], + valid_phc_password: &SafePassword, ) -> 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 - .constant_time_validate(valid_username, valid_password) + // Parse the provided header + let header = t.to_str().map_err(unauthenticated)?; + let (header_username, header_password) = + BasicAuthCredentials::parse_header(header).map_err(unauthenticated)?; + + // Validate the header credentials against the valid credentials + let valid_credentials = + BasicAuthCredentials::new(valid_username.to_owned(), valid_phc_password.clone()) + .map_err(unauthenticated)?; + valid_credentials + .constant_time_validate(&header_username, &header_password) .map_err(unauthenticated)?; + Ok(req) }, _ => Err(unauthenticated("Missing authorization header")), @@ -64,8 +82,8 @@ impl Interceptor for ServerAuthenticationInterceptor { GrpcAuthentication::None => Ok(request), GrpcAuthentication::Basic { username, - password: hashed_password, - } => self.handle_basic_auth(request, username, hashed_password.reveal()), + password: phc_password, + } => self.handle_basic_auth(request, username, phc_password), } } } diff --git a/applications/minotari_console_wallet/src/automation/commands.rs b/applications/minotari_console_wallet/src/automation/commands.rs index 3647acdc8d..46be63d208 100644 --- a/applications/minotari_console_wallet/src/automation/commands.rs +++ b/applications/minotari_console_wallet/src/automation/commands.rs @@ -34,7 +34,6 @@ use chrono::{DateTime, Utc}; use digest::Digest; use futures::FutureExt; use log::*; -use minotari_app_grpc::authentication::salted_password::create_salted_hashed_password; use minotari_wallet::{ connectivity_service::WalletConnectivityInterface, output_manager_service::{handle::OutputManagerHandle, UtxoSelectionCriteria}, @@ -962,36 +961,6 @@ pub async fn command_runner( eprintln!("RevalidateWalletDb error! {}", e); } }, - HashGrpcPassword(args) => { - match config - .grpc_authentication - .username_password() - .ok_or_else(|| CommandError::General("GRPC basic auth is not configured".to_string())) - { - Ok((username, password)) => { - match create_salted_hashed_password(password.reveal()) - .map_err(|e| CommandError::General(e.to_string())) - { - Ok(hashed_password) => { - 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 - ); - } - }, - Err(e) => eprintln!("HashGrpcPassword error! {}", e), - } - }, - Err(e) => eprintln!("HashGrpcPassword error! {}", e), - } - }, RegisterValidatorNode(args) => { let tx_id = register_validator_node( args.amount, diff --git a/applications/minotari_console_wallet/src/cli.rs b/applications/minotari_console_wallet/src/cli.rs index a92346081f..562e872ad4 100644 --- a/applications/minotari_console_wallet/src/cli.rs +++ b/applications/minotari_console_wallet/src/cli.rs @@ -132,7 +132,6 @@ pub enum CliCommands { FinaliseShaAtomicSwap(FinaliseShaAtomicSwapArgs), ClaimShaAtomicSwapRefund(ClaimShaAtomicSwapRefundArgs), RevalidateWalletDb, - HashGrpcPassword(HashPasswordArgs), RegisterValidatorNode(RegisterValidatorNodeArgs), } @@ -281,12 +280,6 @@ pub struct ClaimShaAtomicSwapRefundArgs { 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, -} - #[derive(Debug, Args, Clone)] pub struct RegisterValidatorNodeArgs { pub amount: MicroMinotari, diff --git a/applications/minotari_console_wallet/src/wallet_modes.rs b/applications/minotari_console_wallet/src/wallet_modes.rs index 4383401e66..b5a7b4cdc4 100644 --- a/applications/minotari_console_wallet/src/wallet_modes.rs +++ b/applications/minotari_console_wallet/src/wallet_modes.rs @@ -404,7 +404,8 @@ async fn run_grpc( 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 auth = ServerAuthenticationInterceptor::new(auth_config) + .ok_or("Unable to prepare server gRPC authentication".to_string())?; let service = minotari_app_grpc::tari_rpc::wallet_server::WalletServer::with_interceptor(grpc, auth); Server::builder() @@ -481,7 +482,6 @@ mod test { CliCommands::FinaliseShaAtomicSwap(_) => {}, CliCommands::ClaimShaAtomicSwapRefund(_) => {}, CliCommands::RevalidateWalletDb => {}, - CliCommands::HashGrpcPassword(_) => {}, CliCommands::RegisterValidatorNode(_) => {}, } } diff --git a/applications/minotari_node/src/commands/command/hash_grpc_password.rs b/applications/minotari_node/src/commands/command/hash_grpc_password.rs deleted file mode 100644 index 5571f7c0d0..0000000000 --- a/applications/minotari_node/src/commands/command/hash_grpc_password.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2023, 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 anyhow::{anyhow, Error}; -use async_trait::async_trait; -use clap::Parser; -use minotari_app_grpc::authentication::salted_password::create_salted_hashed_password; - -use super::{CommandContext, HandleCommand}; - -/// Hashes the GRPC authentication password from the config and returns an argon2 hash -#[derive(Debug, Parser)] -pub struct Args {} - -#[async_trait] -impl HandleCommand for CommandContext { - async fn handle_command(&mut self, _: Args) -> Result<(), Error> { - self.hash_grpc_password().await - } -} - -impl CommandContext { - pub async fn hash_grpc_password(&mut self) -> Result<(), Error> { - match self - .config - .base_node - .grpc_authentication - .username_password() - .ok_or_else(|| anyhow!("GRPC basic auth is not configured")) - { - Ok((username, password)) => { - match create_salted_hashed_password(password.reveal()).map_err(|e| anyhow!(e.to_string())) { - Ok(hashed_password) => { - 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 - ); - }, - Err(e) => eprintln!("HashGrpcPassword error! {}", e), - } - }, - Err(e) => eprintln!("HashGrpcPassword error! {}", e), - } - - Ok(()) - } -} diff --git a/applications/minotari_node/src/commands/command/mod.rs b/applications/minotari_node/src/commands/command/mod.rs index e4efb921aa..4a02c670d0 100644 --- a/applications/minotari_node/src/commands/command/mod.rs +++ b/applications/minotari_node/src/commands/command/mod.rs @@ -35,7 +35,6 @@ mod get_mempool_stats; mod get_network_stats; mod get_peer; mod get_state_info; -mod hash_grpc_password; mod header_stats; mod list_banned_peers; mod list_connections; @@ -137,7 +136,6 @@ pub enum Command { Quit(quit::Args), Exit(quit::Args), Watch(watch_command::Args), - HashGrpcPassword(hash_grpc_password::Args), } impl Command { @@ -230,7 +228,6 @@ impl CommandContext { Command::Status(_) | Command::Watch(_) | Command::ListValidatorNodes(_) | - Command::HashGrpcPassword(_) | Command::Quit(_) | Command::Exit(_) => 30, // These commands involve intense blockchain db operations and needs a lot of time to complete @@ -296,7 +293,6 @@ impl HandleCommand for CommandContext { Command::Quit(args) | Command::Exit(args) => self.handle_command(args).await, Command::Watch(args) => self.handle_command(args).await, Command::ListValidatorNodes(args) => self.handle_command(args).await, - Command::HashGrpcPassword(args) => self.handle_command(args).await, } } } diff --git a/applications/minotari_node/src/lib.rs b/applications/minotari_node/src/lib.rs index bc4ebf542f..82702c4dca 100644 --- a/applications/minotari_node/src/lib.rs +++ b/applications/minotari_node/src/lib.rs @@ -181,7 +181,8 @@ async fn run_grpc( info!(target: LOG_TARGET, "Starting GRPC on {}", grpc_address); let grpc_address = multiaddr_to_socketaddr(&grpc_address)?; - let auth = ServerAuthenticationInterceptor::new(auth_config); + let auth = ServerAuthenticationInterceptor::new(auth_config) + .ok_or(anyhow::anyhow!("Unable to prepare server gRPC authentication"))?; let service = minotari_app_grpc::tari_rpc::base_node_server::BaseNodeServer::with_interceptor(grpc, auth); Server::builder() diff --git a/common/config/presets/f_merge_mining_proxy.toml b/common/config/presets/f_merge_mining_proxy.toml index 8744f28b90..ea9d59cec0 100644 --- a/common/config/presets/f_merge_mining_proxy.toml +++ b/common/config/presets/f_merge_mining_proxy.toml @@ -40,13 +40,13 @@ monerod_url = [# stagenet #base_node_grpc_address = "/ip4/127.0.0.1/tcp/18142" # GRPC authentication for the base node (default = "none") -#base_node_grpc_authentication = { username = "miner", password = "$argon..." } +#base_node_grpc_authentication = { username = "miner", password = "xxxx" } # The Minotari wallet's GRPC address. (default = "/ip4/127.0.0.1/tcp/18143") #console_wallet_grpc_address = "/ip4/127.0.0.1/tcp/18143" # GRPC authentication for the Minotari wallet (default = "none") -#wallet_grpc_authentication = { username = "miner", password = "$argon..." } +#wallet_grpc_authentication = { username = "miner", password = "xxxx" } # Address of the minotari_merge_mining_proxy application. (default = "/ip4/127.0.0.1/tcp/18081") #listener_address = "/ip4/127.0.0.1/tcp/18081" diff --git a/common/config/presets/g_miner.toml b/common/config/presets/g_miner.toml index b821e95521..f0d3d63b16 100644 --- a/common/config/presets/g_miner.toml +++ b/common/config/presets/g_miner.toml @@ -10,12 +10,12 @@ # GRPC address of base node (default = "/ip4/127.0.0.1/tcp/18142") #base_node_grpc_address = "/ip4/127.0.0.1/tcp/18142" # GRPC authentication for the base node (default = "none") -#base_node_grpc_authentication = { username = "miner", password = "$argon..." } +#base_node_grpc_authentication = { username = "miner", password = "xxxx" } # 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..." } +#wallet_grpc_authentication = { username = "miner", password = "xxxx" } # Number of mining threads (default: number of logical CPU cores) #num_mining_threads = 8