Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallet): add change mnemonic password rpc #2317

Open
wants to merge 19 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 64 additions & 45 deletions mm2src/mm2_main/src/lp_wallet.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -413,40 +413,45 @@ 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 WalletsStorageRpcError {
shamardy marked this conversation as resolved.
Show resolved Hide resolved
#[display(fmt = "Invalid request error: {}", _0)]
dimxy marked this conversation as resolved.
Show resolved Hide resolved
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")]
dimxy marked this conversation as resolved.
Show resolved Hide resolved
InvalidPassword(String),
}

impl HttpStatusCode for GetMnemonicError {
impl HttpStatusCode for WalletsStorageRpcError {
fn status_code(&self) -> StatusCode {
match self {
GetMnemonicError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
GetMnemonicError::WalletsStorageError(_) | GetMnemonicError::Internal(_) => {
WalletsStorageRpcError::InvalidRequest(_) | WalletsStorageRpcError::InvalidPassword(_) => {
StatusCode::BAD_REQUEST
},
WalletsStorageRpcError::WalletsStorageError(_) | WalletsStorageRpcError::Internal(_) => {
StatusCode::INTERNAL_SERVER_ERROR
},
}
}
}

#[cfg(not(target_arch = "wasm32"))]
impl From<WalletsStorageError> for GetMnemonicError {
fn from(e: WalletsStorageError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) }
impl From<WalletsStorageError> for WalletsStorageRpcError {
fn from(e: WalletsStorageError) -> Self { WalletsStorageRpcError::WalletsStorageError(e.to_string()) }
}

#[cfg(target_arch = "wasm32")]
impl From<WalletsDBError> for GetMnemonicError {
fn from(e: WalletsDBError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) }
impl From<WalletsDBError> for WalletsStorageRpcError {
fn from(e: WalletsDBError) -> Self { WalletsStorageRpcError::WalletsStorageError(e.to_string()) }
}

impl From<ReadPassphraseError> for GetMnemonicError {
fn from(e: ReadPassphraseError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) }
impl From<ReadPassphraseError> for WalletsStorageRpcError {
fn from(e: ReadPassphraseError) -> Self { WalletsStorageRpcError::WalletsStorageError(e.to_string()) }
}

/// Retrieves the wallet mnemonic in the requested format.
Expand All @@ -456,7 +461,7 @@ impl From<ReadPassphraseError> 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`]<[`WalletsStorageRpcError>`]> - Returns specific [`WalletsStorageRpcError`] variants for different failure scenarios.
///
/// # Errors
///
Expand All @@ -480,20 +485,23 @@ impl From<ReadPassphraseError> for GetMnemonicError {
/// Err(e) => println!("Error: {:?}", e),
/// }
/// ```
pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult<GetMnemonicResponse, GetMnemonicError> {
pub async fn get_mnemonic_rpc(
ctx: MmArc,
req: GetMnemonicRequest,
) -> MmResult<GetMnemonicResponse, WalletsStorageRpcError> {
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(|| WalletsStorageRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
Ok(GetMnemonicResponse {
mnemonic: encrypted_mnemonic.into(),
})
},
MnemonicFormat::PlainText(wallet_password) => {
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(|| WalletsStorageRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
Ok(GetMnemonicResponse {
mnemonic: plaintext_mnemonic.into(),
})
Expand All @@ -508,40 +516,13 @@ pub struct GetWalletNamesResponse {
activated_wallet: Option<String>,
}

#[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<WalletsStorageError> for GetWalletsError {
fn from(e: WalletsStorageError) -> Self { GetWalletsError::WalletsStorageError(e.to_string()) }
}

#[cfg(target_arch = "wasm32")]
impl From<WalletsDBError> 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<GetWalletNamesResponse, GetWalletsError> {
pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult<GetWalletNamesResponse, WalletsStorageRpcError> {
// 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<Option<String>>` 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(WalletsStorageRpcError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?;

Expand All @@ -550,3 +531,41 @@ pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult<GetWalletN
activated_wallet: activated_wallet.clone(),
})
}

/// `SeedPasswordUpdateRequest` represents a request to update
/// the password for the seed storage.
shamardy marked this conversation as resolved.
Show resolved Hide resolved
/// It includes the current password and the new password to be set.
#[derive(Debug, Deserialize)]
pub struct SeedPasswordUpdateRequest {
shamardy marked this conversation as resolved.
Show resolved Hide resolved
/// The current password for the seed storage.
pub current_password: String,
/// The new password to replace the current password.
pub new_password: String,
}

/// RPC function to handle a request for updating the seed storage password.
pub async fn update_seed_storage_password_rpc(
ctx: MmArc,
req: SeedPasswordUpdateRequest,
) -> MmResult<(), WalletsStorageRpcError> {
let wallet_name = ctx
.wallet_name
.get()
.ok_or(WalletsStorageRpcError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?
.clone()
.ok_or_else(|| WalletsStorageRpcError::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(WalletsStorageRpcError::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::default() with new password
Copy link
Collaborator

Choose a reason for hiding this comment

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

data::default() what?

Copy link
Member Author

Choose a reason for hiding this comment

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

typo, thanks for the catch

Copy link
Collaborator

Choose a reason for hiding this comment

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

well actually this is blocking 🤦‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

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

done

save_encrypted_passphrase(&ctx, &wallet_name, &encrypted_data).await?;

Ok(())
}
4 changes: 3 additions & 1 deletion mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
3 changes: 2 additions & 1 deletion mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,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::{get_mnemonic_rpc, get_wallet_names_rpc, update_seed_storage_password_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,
Expand Down Expand Up @@ -217,6 +217,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult<Re
"trade_preimage" => 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,
"update_seed_storage_password" => handle_mmrpc(ctx, request, update_seed_storage_password_rpc).await,
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think we better drop the word storage here.

storage makes it feel as if the whole seed store (multiple seeds) is encrypted using a single pass and not just the single seed in question.
better alternatives imo: update_seed_password or update_seedphrase/passphrase_password

that's a non-blocking comment though since im not a certified linguist.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We should use the word mnemonic not seed since we have get_mnemonic RPC. How about change_mnemonic_password

Copy link
Collaborator

Choose a reason for hiding this comment

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

aha better

"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,
Expand Down
Loading