diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index b3da7071d4..97311758f4 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -43,6 +43,8 @@ cfg_native! { /// Default interval to export and record metrics to log. const EXPORT_METRICS_INTERVAL: f64 = 5. * 60.; +/// File extension for files containing a wallet's encrypted mnemonic phrase. +pub const WALLET_FILE_EXTENSION: &str = "json"; /// MarketMaker state, shared between the various MarketMaker threads. /// @@ -319,10 +321,6 @@ impl MmCtx { /// Returns the path to the MM databases root. #[cfg(not(target_arch = "wasm32"))] pub fn db_root(&self) -> PathBuf { path_to_db_root(self.conf["dbdir"].as_str()) } - #[cfg(not(target_arch = "wasm32"))] - pub fn wallet_file_path(&self, wallet_name: &str) -> PathBuf { - self.db_root().join(wallet_name.to_string() + ".dat") - } /// MM database path. /// Defaults to a relative "DB". diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index cc4f128278..b06318e56f 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -1,3 +1,4 @@ +use common::password_policy::{password_policy, PasswordPolicyError}; use common::HttpStatusCode; use crypto::{decrypt_mnemonic, encrypt_mnemonic, generate_mnemonic, CryptoCtx, CryptoInitError, EncryptedData, MnemonicError}; @@ -27,7 +28,7 @@ cfg_native! { type WalletInitResult = Result>; -#[derive(Debug, Deserialize, Display, Serialize)] +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize)] pub enum WalletInitError { #[display(fmt = "Error deserializing '{}' config field: {}", field, error)] ErrorDeserializingConfig { @@ -48,6 +49,9 @@ pub enum WalletInitError { MnemonicError(String), #[display(fmt = "Error initializing crypto context: {}", _0)] CryptoInitError(String), + #[display(fmt = "Password does not meet policy requirements: {}", _0)] + #[from_stringify("PasswordPolicyError")] + PasswordPolicyViolation(String), InternalError(String), } @@ -173,6 +177,15 @@ async fn retrieve_or_create_passphrase( Ok(Some(passphrase_from_file)) }, None => { + if wallet_password.is_empty() { + return MmError::err(WalletInitError::PasswordPolicyViolation( + "`wallet_password` cannot be empty".to_string(), + )); + } + let is_weak_password_accepted = ctx.conf["allow_weak_password"].as_bool().unwrap_or(false); + if !is_weak_password_accepted { + password_policy(wallet_password)?; + } // If no passphrase is found, generate a new one let new_passphrase = generate_mnemonic(ctx)?.to_string(); // Encrypt and save the new passphrase @@ -195,6 +208,15 @@ async fn confirm_or_encrypt_and_store_passphrase( Ok(Some(passphrase_from_file)) }, None => { + if wallet_password.is_empty() { + return MmError::err(WalletInitError::PasswordPolicyViolation( + "`wallet_password` cannot be empty".to_string(), + )); + } + let is_weak_password_accepted = ctx.conf["allow_weak_password"].as_bool().unwrap_or(false); + if !is_weak_password_accepted { + password_policy(wallet_password)?; + } // If no passphrase is found in the file, encrypt and save the provided passphrase encrypt_and_save_passphrase(ctx, wallet_name, passphrase, wallet_password).await?; Ok(Some(passphrase.to_string())) @@ -425,12 +447,17 @@ pub enum MnemonicRpcError { #[display(fmt = "Invalid password error: {}", _0)] #[from_stringify("MnemonicError")] InvalidPassword(String), + #[display(fmt = "Password does not meet policy requirements: {}", _0)] + #[from_stringify("PasswordPolicyError")] + PasswordPolicyViolation(String), } impl HttpStatusCode for MnemonicRpcError { fn status_code(&self) -> StatusCode { match self { - MnemonicRpcError::InvalidRequest(_) | MnemonicRpcError::InvalidPassword(_) => StatusCode::BAD_REQUEST, + MnemonicRpcError::InvalidRequest(_) + | MnemonicRpcError::InvalidPassword(_) + | MnemonicRpcError::PasswordPolicyViolation(_) => StatusCode::BAD_REQUEST, MnemonicRpcError::WalletsStorageError(_) | MnemonicRpcError::Internal(_) => { StatusCode::INTERNAL_SERVER_ERROR }, @@ -539,6 +566,15 @@ pub struct ChangeMnemonicPasswordReq { /// RPC function to handle a request for changing mnemonic password. pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq) -> MmResult<(), MnemonicRpcError> { + if req.new_password.is_empty() { + return MmError::err(MnemonicRpcError::PasswordPolicyViolation( + "`new_password` cannot be empty".to_string(), + )); + } + let is_weak_password_accepted = ctx.conf["allow_weak_password"].as_bool().unwrap_or(false); + if !is_weak_password_accepted { + password_policy(&req.new_password)?; + } let wallet_name = ctx .wallet_name .get() diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs index e779f7b86a..25e77c27d1 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs @@ -1,7 +1,8 @@ use crypto::EncryptedData; -use mm2_core::mm_ctx::MmArc; +use mm2_core::mm_ctx::{MmArc, WALLET_FILE_EXTENSION}; use mm2_err_handle::prelude::*; use mm2_io::fs::{ensure_file_is_writable, list_files_by_extension}; +use std::path::PathBuf; type WalletsStorageResult = Result>; @@ -11,10 +12,33 @@ pub enum WalletsStorageError { FsWriteError(String), #[display(fmt = "Error reading from file: {}", _0)] FsReadError(String), + #[display(fmt = "Invalid wallet name: {}", _0)] + InvalidWalletName(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), } +fn wallet_file_path(ctx: &MmArc, wallet_name: &str) -> Result { + let wallet_name_trimmed = wallet_name.trim(); + if wallet_name_trimmed.is_empty() { + return Err("Wallet name cannot be empty or consist only of whitespace.".to_string()); + } + + if !wallet_name_trimmed + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == ' ') + { + return Err(format!( + "Invalid wallet name: '{}'. Only alphanumeric characters, spaces, dash and underscore are allowed.", + wallet_name_trimmed + )); + } + + Ok(ctx + .db_root() + .join(format!("{}.{}", wallet_name_trimmed, WALLET_FILE_EXTENSION))) +} + /// Saves the passphrase to a file associated with the given wallet name. /// /// # Returns @@ -24,7 +48,7 @@ pub(super) async fn save_encrypted_passphrase( wallet_name: &str, encrypted_passphrase_data: &EncryptedData, ) -> WalletsStorageResult<()> { - let wallet_path = ctx.wallet_file_path(wallet_name); + let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; ensure_file_is_writable(&wallet_path).map_to_mm(WalletsStorageError::FsWriteError)?; mm2_io::fs::write_json(encrypted_passphrase_data, &wallet_path, true) .await @@ -53,7 +77,7 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle ))? .clone() .ok_or_else(|| WalletsStorageError::Internal("`wallet_name` cannot be None!".to_string()))?; - let wallet_path = ctx.wallet_file_path(&wallet_name); + let wallet_path = wallet_file_path(ctx, &wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?; mm2_io::fs::read_json(&wallet_path).await.mm_err(|e| { WalletsStorageError::FsReadError(format!( "Error reading passphrase from file {}: {}", @@ -64,7 +88,7 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle } pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsStorageResult> { - let wallet_names = list_files_by_extension(&ctx.db_root(), "dat", false) + let wallet_names = list_files_by_extension(&ctx.db_root(), WALLET_FILE_EXTENSION, false) .await .mm_err(|e| WalletsStorageError::FsReadError(format!("Error reading wallets directory: {}", e)))?; Ok(wallet_names) diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 621e76a597..9285768bc1 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -1444,6 +1444,7 @@ impl MarketMakerIt { local: Option, db_namespace_id: Option, ) -> Result { + conf["allow_weak_password"] = true.into(); if conf["p2p_in_memory"].is_null() { conf["p2p_in_memory"] = Json::Bool(true); }