Skip to content
6 changes: 2 additions & 4 deletions mm2src/mm2_core/src/mm_ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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".
Expand Down
40 changes: 38 additions & 2 deletions mm2src/mm2_main/src/lp_wallet.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -27,7 +28,7 @@ cfg_native! {

type WalletInitResult<T> = Result<T, MmError<WalletInitError>>;

#[derive(Debug, Deserialize, Display, Serialize)]
#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize)]
pub enum WalletInitError {
#[display(fmt = "Error deserializing '{}' config field: {}", field, error)]
ErrorDeserializingConfig {
Expand All @@ -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),
}

Expand Down Expand Up @@ -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
Expand All @@ -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(),
));
}
Comment on lines +211 to +215
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can allow empty pass and have such check in the password_policy checker (i.e. when weak passes are disabled).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, following same standards as rpc password ref. https://github.com/KomodoPlatform/komodo-defi-framework/blob/60302fc6da783d65da02485a40d7fa50a79bdab5/mm2src/mm2_main/src/mm2.rs#L143-L145
I will leave the empty check as if it were removed it would allow no password not a weak password.

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()))
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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()
Expand Down
32 changes: 28 additions & 4 deletions mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs
Original file line number Diff line number Diff line change
@@ -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<T> = Result<T, MmError<WalletsStorageError>>;

Expand All @@ -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<PathBuf, String> {
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
Expand All @@ -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
Expand Down Expand Up @@ -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 {}: {}",
Expand All @@ -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<impl Iterator<Item = String>> {
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)
Expand Down
1 change: 1 addition & 0 deletions mm2src/mm2_test_helpers/src/for_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,7 @@ impl MarketMakerIt {
local: Option<LocalStart>,
db_namespace_id: Option<u64>,
) -> Result<MarketMakerIt, String> {
conf["allow_weak_password"] = true.into();
if conf["p2p_in_memory"].is_null() {
conf["p2p_in_memory"] = Json::Bool(true);
}
Expand Down
Loading