diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index abe5663024..cc4f128278 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -1,6 +1,7 @@ use common::HttpStatusCode; use crypto::{decrypt_mnemonic, encrypt_mnemonic, generate_mnemonic, CryptoCtx, CryptoInitError, EncryptedData, MnemonicError}; +use enum_derives::EnumFromStringify; use http::StatusCode; use itertools::Itertools; use mm2_core::mm_ctx::MmArc; @@ -21,7 +22,6 @@ cfg_wasm32! { cfg_native! { use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError}; } - #[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage; #[cfg(target_arch = "wasm32")] mod mnemonics_wasm_db; @@ -253,7 +253,7 @@ async fn process_wallet_with_name( async fn process_passphrase_logic( ctx: &MmArc, - wallet_name: Option, + wallet_name: Option<&str>, passphrase: Option, ) -> WalletInitResult> { match (wallet_name, passphrase) { @@ -268,7 +268,7 @@ async fn process_passphrase_logic( (Some(wallet_name), passphrase_option) => { let wallet_password = deserialize_config_field::(ctx, "wallet_password")?; - process_wallet_with_name(ctx, &wallet_name, passphrase_option, &wallet_password).await + process_wallet_with_name(ctx, wallet_name, passphrase_option, &wallet_password).await }, } } @@ -307,8 +307,8 @@ pub(crate) async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResul ctx.wallet_name .set(wallet_name.clone()) .map_to_mm(|_| WalletInitError::InternalError("Already Initialized".to_string()))?; - let passphrase = process_passphrase_logic(ctx, wallet_name, passphrase).await?; + let passphrase = process_passphrase_logic(ctx, wallet_name.as_deref(), passphrase).await?; if let Some(passphrase) = passphrase { initialize_crypto_context(ctx, &passphrase)?; } @@ -413,22 +413,25 @@ pub struct GetMnemonicResponse { pub mnemonic: MnemonicForRpc, } -#[derive(Debug, Display, Serialize, SerializeErrorType)] +#[derive(Debug, Display, Serialize, SerializeErrorType, EnumFromStringify)] #[serde(tag = "error_type", content = "error_data")] -pub enum GetMnemonicError { +pub enum MnemonicRpcError { #[display(fmt = "Invalid request error: {}", _0)] InvalidRequest(String), #[display(fmt = "Wallets storage error: {}", _0)] WalletsStorageError(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), + #[display(fmt = "Invalid password error: {}", _0)] + #[from_stringify("MnemonicError")] + InvalidPassword(String), } -impl HttpStatusCode for GetMnemonicError { +impl HttpStatusCode for MnemonicRpcError { fn status_code(&self) -> StatusCode { match self { - GetMnemonicError::InvalidRequest(_) => StatusCode::BAD_REQUEST, - GetMnemonicError::WalletsStorageError(_) | GetMnemonicError::Internal(_) => { + MnemonicRpcError::InvalidRequest(_) | MnemonicRpcError::InvalidPassword(_) => StatusCode::BAD_REQUEST, + MnemonicRpcError::WalletsStorageError(_) | MnemonicRpcError::Internal(_) => { StatusCode::INTERNAL_SERVER_ERROR }, } @@ -436,17 +439,17 @@ impl HttpStatusCode for GetMnemonicError { } #[cfg(not(target_arch = "wasm32"))] -impl From for GetMnemonicError { - fn from(e: WalletsStorageError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) } +impl From for MnemonicRpcError { + fn from(e: WalletsStorageError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } } #[cfg(target_arch = "wasm32")] -impl From for GetMnemonicError { - fn from(e: WalletsDBError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) } +impl From for MnemonicRpcError { + fn from(e: WalletsDBError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } } -impl From for GetMnemonicError { - fn from(e: ReadPassphraseError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) } +impl From for MnemonicRpcError { + fn from(e: ReadPassphraseError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) } } /// Retrieves the wallet mnemonic in the requested format. @@ -456,7 +459,7 @@ impl From for GetMnemonicError { /// A `Result` type containing: /// /// * [`Ok`]([`GetMnemonicResponse`]) - The wallet mnemonic in the requested format. -/// * [`MmError`]<[`GetMnemonicError>`]> - Returns specific [`GetMnemonicError`] variants for different failure scenarios. +/// * [`MmError`]<[`MnemonicRpcError>`]> - Returns specific [`MnemonicRpcError`] variants for different failure scenarios. /// /// # Errors /// @@ -480,12 +483,12 @@ impl From for GetMnemonicError { /// Err(e) => println!("Error: {:?}", e), /// } /// ``` -pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { +pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { match req.mnemonic_format { MnemonicFormat::Encrypted => { let encrypted_mnemonic = read_encrypted_passphrase_if_available(&ctx) .await? - .ok_or_else(|| GetMnemonicError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; + .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { mnemonic: encrypted_mnemonic.into(), }) @@ -493,7 +496,7 @@ pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult { let plaintext_mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &wallet_password) .await? - .ok_or_else(|| GetMnemonicError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; + .ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?; Ok(GetMnemonicResponse { mnemonic: plaintext_mnemonic.into(), }) @@ -508,40 +511,13 @@ pub struct GetWalletNamesResponse { activated_wallet: Option, } -#[derive(Debug, Display, Serialize, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum GetWalletsError { - #[display(fmt = "Wallets storage error: {}", _0)] - WalletsStorageError(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -impl HttpStatusCode for GetWalletsError { - fn status_code(&self) -> StatusCode { - match self { - GetWalletsError::WalletsStorageError(_) | GetWalletsError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -#[cfg(not(target_arch = "wasm32"))] -impl From for GetWalletsError { - fn from(e: WalletsStorageError) -> Self { GetWalletsError::WalletsStorageError(e.to_string()) } -} - -#[cfg(target_arch = "wasm32")] -impl From for GetWalletsError { - fn from(e: WalletsDBError) -> Self { GetWalletsError::WalletsStorageError(e.to_string()) } -} - /// Retrieves all created wallets and the currently activated wallet. -pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult { +pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult { // We want to return wallet names in the same order for both native and wasm32 targets. let wallets = read_all_wallet_names(&ctx).await?.sorted().collect(); // Note: `ok_or` is used here on `Constructible>` to handle the case where the wallet name is not set. // `wallet_name` can be `None` in the case of no-login mode. - let activated_wallet = ctx.wallet_name.get().ok_or(GetWalletsError::Internal( + let activated_wallet = ctx.wallet_name.get().ok_or(MnemonicRpcError::Internal( "`wallet_name` not initialized yet!".to_string(), ))?; @@ -550,3 +526,37 @@ pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult MmResult<(), MnemonicRpcError> { + let wallet_name = ctx + .wallet_name + .get() + .ok_or(MnemonicRpcError::Internal( + "`wallet_name` not initialized yet!".to_string(), + ))? + .as_ref() + .ok_or_else(|| MnemonicRpcError::Internal("`wallet_name` cannot be None!".to_string()))?; + // read mnemonic for a wallet_name using current user's password. + let mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &req.current_password) + .await? + .ok_or(MmError::new(MnemonicRpcError::Internal(format!( + "{wallet_name}: wallet mnemonic file not found" + ))))?; + // encrypt mnemonic with new passphrase. + let encrypted_data = encrypt_mnemonic(&mnemonic, &req.new_password)?; + // save new encrypted mnemonic data with new password + save_encrypted_passphrase(&ctx, wallet_name, &encrypted_data).await?; + + Ok(()) +} diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs index fa66cada1c..e4733a132d 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs @@ -112,7 +112,9 @@ pub(super) async fn save_encrypted_passphrase( } })?, }; - table.add_item(&mnemonics_table_item).await?; + table + .replace_item_by_unique_index("wallet_name", wallet_name, &mnemonics_table_item) + .await?; Ok(()) } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index f72b2be399..4dc4f9dae4 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -10,7 +10,7 @@ use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, s stop_version_stat_collection, update_version_stat_collection}; use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}; -use crate::lp_wallet::{get_mnemonic_rpc, get_wallet_names_rpc}; +use crate::lp_wallet::{change_mnemonic_password, get_mnemonic_rpc, get_wallet_names_rpc}; use crate::rpc::lp_commands::db_id::get_shared_db_id; use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, one_inch_v6_0_classic_swap_create_rpc, @@ -221,6 +221,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, trade_preimage_rpc).await, "trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await, "update_nft" => handle_mmrpc(ctx, request, update_nft).await, + "change_mnemonic_password" => handle_mmrpc(ctx, request, change_mnemonic_password).await, "update_version_stat_collection" => handle_mmrpc(ctx, request, update_version_stat_collection).await, "verify_message" => handle_mmrpc(ctx, request, verify_message).await, "withdraw" => handle_mmrpc(ctx, request, withdraw).await, diff --git a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs index fdb5dd9d74..f109d9c587 100644 --- a/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs +++ b/mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs @@ -5853,6 +5853,76 @@ fn test_get_wallet_names() { block_on(mm_wallet_2.stop()).unwrap(); } +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn test_change_mnemonic_password_rpc() { + let coins = json!([]); + // Initialize wallet with current_password. + let old_password = "helloworld"; + let wallet_1 = Mm2TestConf::seednode_with_wallet_name(&coins, "wallet_1", old_password); + let mm = MarketMakerIt::start(wallet_1.conf, wallet_1.rpc_password, None).unwrap(); + + // Retrieve all wallet names(should succeed). + let get_wallet_names_1 = block_on(get_wallet_names(&mm)); + assert_eq!(get_wallet_names_1.wallet_names, vec!["wallet_1"]); + assert_eq!(get_wallet_names_1.activated_wallet.unwrap(), "wallet_1"); + + // STAGE 1: send update mnemonic password using new rpc(must succeed). + let new_password_stage_1 = "worldhello"; + let request = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "change_mnemonic_password", + "mmrpc": "2.0", + "params": { + "current_password": old_password, + "new_password": new_password_stage_1 + } + }))) + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'change_mnemonic_password' failed: {}", + request.1 + ); + + // STAGE 2: Try changing wallet password using old_password(Should fail!) + let request = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "change_mnemonic_password", + "mmrpc": "2.0", + "params": { + "current_password": old_password, + "new_password": "password2" + } + }))) + .unwrap(); + assert_eq!( + request.0, + StatusCode::INTERNAL_SERVER_ERROR, + "'change_mnemonic_password' failed: {}", + request.1 + ); + + // STAGE 3: try updating password again using new_password_stage_1 password(Should pass!) + let request = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method": "change_mnemonic_password", + "mmrpc": "2.0", + "params": { + "current_password": new_password_stage_1, + "new_password": "password3" + } + }))) + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'change_mnemonic_password' failed: {}", + request.1 + ); +} + #[test] #[cfg(not(target_arch = "wasm32"))] fn test_sign_raw_transaction_rick() {