From 9a49213973a5c8e0c3d1e763c9ae1ae6d528e2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Mar 2022 09:30:42 +0100 Subject: [PATCH 1/8] wallets: provide placeholder functions for ui password --- nym-wallet/src-tauri/src/error.rs | 2 + nym-wallet/src-tauri/src/main.rs | 3 ++ .../src/operations/mixnet/account.rs | 47 +++++++++++++++++++ .../src/wallet_storage/account_data.rs | 3 +- .../src-tauri/src/wallet_storage/mod.rs | 5 +- 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 0f3aa9bd10..92cfc19562 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -81,6 +81,8 @@ pub enum BackendError { NoNymdValidatorConfigured, #[error("No validator API URL configured")] NoValidatorApiUrlConfigured, + #[error("The wallet file already exists")] + WalletFileAlreadyExists, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 61d78ed3de..fc620c46a4 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -37,8 +37,11 @@ fn main() { mixnet::account::connect_with_mnemonic, mixnet::account::create_new_account, mixnet::account::create_new_mnemonic, + mixnet::account::create_password, + mixnet::account::does_password_file_exist, mixnet::account::get_balance, mixnet::account::logout, + mixnet::account::sign_in_with_password, mixnet::account::switch_network, mixnet::account::update_validator_urls, mixnet::admin::get_contract_settings, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index e369f9c976..dde2f9023f 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -4,8 +4,11 @@ use crate::error::BackendError; use crate::network::Network; use crate::nymd_client; use crate::state::State; +use crate::wallet_storage; use bip39::{Language, Mnemonic}; +use config::defaults::COSMOS_DERIVATION_PATH; +use cosmrs::bip32::DerivationPath; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::convert::TryInto; @@ -322,3 +325,47 @@ async fn is_validator_connection_ok(client: &Client) -> bool Err(_) | Ok(_) => true, } } + +#[tauri::command] +pub fn does_password_file_exist() -> Result { + println!("Checking wallet file"); + let file = wallet_storage::wallet_login_filepath()?; + if file.is_file() { + println!("Exists: {}", file.to_string_lossy()); + } else { + println!("Does not exist: {}", file.to_string_lossy()); + } + Ok(file.is_file()) +} + +#[tauri::command] +pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> { + println!("Creating password"); + if does_password_file_exist()? { + return Err(BackendError::WalletFileAlreadyExists); + } + + let mnemonic = Mnemonic::from_str(&mnemonic)?; + let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + let password = wallet_storage::UserPassword::new(password); + wallet_storage::store_wallet_login_information(mnemonic, hd_path, password)?; + Ok(()) +} + +#[tauri::command] +pub async fn sign_in_with_password( + password: String, + state: tauri::State<'_, Arc>>, +) -> Result { + println!("Signing in with password"); + let password = wallet_storage::UserPassword::new(password); + let stored_accounts = wallet_storage::load_existing_wallet_login_information(&password)?; + + // WIP: we are assuming just a single password is stored + let stored_account = stored_accounts.into_iter().next().unwrap(); + let mnemonic = match stored_account { + wallet_storage::account_data::StoredAccount::Mnemonic(ref mn) => mn.mnemonic().clone(), + }; + + _connect_with_mnemonic(mnemonic, state).await +} diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs index 487b029eb0..f7ba738b93 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -34,7 +34,8 @@ pub(crate) struct MnemonicAccount { } // we only ever want to expose those getters in the test code -#[cfg(test)] +// WIP(JON): temporarily comment out +//#[cfg(test)] impl MnemonicAccount { pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { &self.mnemonic diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index de5c95c1e0..3d3b628046 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -1,15 +1,18 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub(crate) use crate::wallet_storage::password::UserPassword; + use crate::error::BackendError; use crate::wallet_storage::account_data::StoredAccount; use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData}; -use crate::wallet_storage::password::UserPassword; use cosmrs::bip32::DerivationPath; use std::fs::{create_dir_all, OpenOptions}; use std::path::PathBuf; + pub(crate) mod account_data; pub(crate) mod encryption; + mod password; const STORAGE_DIR_NAME: &str = "NymWallet"; From c950556506e4a7b0b4f6e9e2d933ff5a9613c0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Mar 2022 10:02:46 +0100 Subject: [PATCH 2/8] wallet: platform_constants --- nym-wallet/Cargo.lock | 1 + nym-wallet/src-tauri/Cargo.toml | 1 + nym-wallet/src-tauri/src/main.rs | 1 + .../src-tauri/src/platform_constants.rs | 19 +++++++++++++++++++ .../src-tauri/src/wallet_storage/mod.rs | 8 +++----- 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 nym-wallet/src-tauri/src/platform_constants.rs diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index e7616df351..172ea63da2 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2843,6 +2843,7 @@ dependencies = [ "argon2", "base64", "bip39", + "cfg-if", "coconut-interface", "config", "cosmrs", diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index e2118fb646..9b2dd4a8a6 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ tauri-macros = "=1.0.0-rc.1" [dependencies] bip39 = "1.0" +cfg-if = "1.0.0" dirs = "4.0" eyre = "0.6.5" futures = "0.3.15" diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index fc620c46a4..316db50ac7 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -15,6 +15,7 @@ mod error; mod menu; mod network; mod operations; +mod platform_constants; mod state; mod utils; // temporarily until it is actually used diff --git a/nym-wallet/src-tauri/src/platform_constants.rs b/nym-wallet/src-tauri/src/platform_constants.rs new file mode 100644 index 0000000000..445c87fe69 --- /dev/null +++ b/nym-wallet/src-tauri/src/platform_constants.rs @@ -0,0 +1,19 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + pub const STORAGE_DIR_NAME: &str = "nym-wallet"; + pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; + } else if #[cfg(taret_os = "macos")] { + pub const STORAGE_DIR_NAME: &str = "nym-wallet"; + pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; + } else if #[cfg(taret_os = "windows")] { + pub const STORAGE_DIR_NAME: &str = "NymWallet"; + pub const WALLET_INFO_FILENAME: &str = "saved_wallet.json"; + } else { + pub const STORAGE_DIR_NAME: &str = "nym-wallet"; + pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; + } +} + diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index 3d3b628046..c9705ed464 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -4,6 +4,7 @@ pub(crate) use crate::wallet_storage::password::UserPassword; use crate::error::BackendError; +use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME}; use crate::wallet_storage::account_data::StoredAccount; use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData}; use cosmrs::bip32::DerivationPath; @@ -15,9 +16,6 @@ pub(crate) mod encryption; mod password; -const STORAGE_DIR_NAME: &str = "NymWallet"; -const WALLET_INFO_FILENAME: &str = "saved_wallet.json"; - fn get_storage_directory() -> Result { tauri::api::path::local_data_dir() .map(|dir| dir.join(STORAGE_DIR_NAME)) @@ -52,14 +50,14 @@ fn load_existing_wallet_login_information_at_file( pub(crate) fn store_wallet_login_information( mnemonic: bip39::Mnemonic, hd_path: DerivationPath, - password: UserPassword, + password: &UserPassword, ) -> Result<(), BackendError> { // make sure the entire directory structure exists let store_dir = get_storage_directory()?; create_dir_all(&store_dir)?; let filepath = store_dir.join(WALLET_INFO_FILENAME); - store_wallet_login_information_at_file(filepath, mnemonic, hd_path, &password) + store_wallet_login_information_at_file(filepath, mnemonic, hd_path, password) } fn store_wallet_login_information_at_file( From 30fafa509c0d74b45783e606911cf64782e9c8fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 16 Mar 2022 11:48:10 +0100 Subject: [PATCH 3/8] wallet: swap println to log --- .../src/operations/mixnet/account.rs | 22 ++++++++++--------- .../src-tauri/src/platform_constants.rs | 1 - 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index dde2f9023f..b7e4f1a489 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -230,9 +230,10 @@ async fn choose_validators( .next() // We always have at least one hardcoded default validator .unwrap(); - println!( + log::info!( "Using default for {network}: {}, {}", - default_validator.nymd_url, default_validator.api_url, + default_validator.nymd_url, + default_validator.api_url, ); default_validator }); @@ -308,9 +309,10 @@ async fn try_connect_to_validator( )?; if is_validator_connection_ok(&client).await { - println!( + log::info!( "Connection ok for {network}: {}, {}", - validator.nymd_url, validator.api_url + validator.nymd_url, + validator.api_url ); Ok(Some((network, validator.clone()))) } else { @@ -328,19 +330,19 @@ async fn is_validator_connection_ok(client: &Client) -> bool #[tauri::command] pub fn does_password_file_exist() -> Result { - println!("Checking wallet file"); + log::info!("Checking wallet file"); let file = wallet_storage::wallet_login_filepath()?; if file.is_file() { - println!("Exists: {}", file.to_string_lossy()); + log::info!("Exists: {}", file.to_string_lossy()); } else { - println!("Does not exist: {}", file.to_string_lossy()); + log::info!("Does not exist: {}", file.to_string_lossy()); } Ok(file.is_file()) } #[tauri::command] pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> { - println!("Creating password"); + log::info!("Creating password"); if does_password_file_exist()? { return Err(BackendError::WalletFileAlreadyExists); } @@ -348,7 +350,7 @@ pub fn create_password(mnemonic: String, password: String) -> Result<(), Backend let mnemonic = Mnemonic::from_str(&mnemonic)?; let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); let password = wallet_storage::UserPassword::new(password); - wallet_storage::store_wallet_login_information(mnemonic, hd_path, password)?; + wallet_storage::store_wallet_login_information(mnemonic, hd_path, &password)?; Ok(()) } @@ -357,7 +359,7 @@ pub async fn sign_in_with_password( password: String, state: tauri::State<'_, Arc>>, ) -> Result { - println!("Signing in with password"); + log::info!("Signing in with password"); let password = wallet_storage::UserPassword::new(password); let stored_accounts = wallet_storage::load_existing_wallet_login_information(&password)?; diff --git a/nym-wallet/src-tauri/src/platform_constants.rs b/nym-wallet/src-tauri/src/platform_constants.rs index 445c87fe69..5565a8ac96 100644 --- a/nym-wallet/src-tauri/src/platform_constants.rs +++ b/nym-wallet/src-tauri/src/platform_constants.rs @@ -16,4 +16,3 @@ cfg_if::cfg_if! { pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; } } - From 7aeac58fd9c0cd6f918a9fbe322389c2ce23031d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 18 Mar 2022 15:23:22 +0100 Subject: [PATCH 4/8] wallet: inline encryption of wallet file --- nym-wallet/src-tauri/src/error.rs | 6 + .../src/operations/mixnet/account.rs | 17 +- .../src/wallet_storage/account_data.rs | 68 +++++++ .../src/wallet_storage/encryption.rs | 3 +- .../src-tauri/src/wallet_storage/mod.rs | 166 +++++++++++------- .../src-tauri/src/wallet_storage/password.rs | 18 ++ 6 files changed, 207 insertions(+), 71 deletions(-) diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 92cfc19562..64a74028b7 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -83,6 +83,12 @@ pub enum BackendError { NoValidatorApiUrlConfigured, #[error("The wallet file already exists")] WalletFileAlreadyExists, + #[error("The wallet file is not found")] + WalletNotFound, + #[error("Account ID not found in wallet")] + NoSuchWalletId, + #[error("Adding a different password to the wallet not currently supported")] + WalletDifferentPasswordDetected, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index b7e4f1a489..328474b787 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -332,26 +332,28 @@ async fn is_validator_connection_ok(client: &Client) -> bool pub fn does_password_file_exist() -> Result { log::info!("Checking wallet file"); let file = wallet_storage::wallet_login_filepath()?; - if file.is_file() { + if file.exists() { log::info!("Exists: {}", file.to_string_lossy()); } else { log::info!("Does not exist: {}", file.to_string_lossy()); } - Ok(file.is_file()) + Ok(file.exists()) } #[tauri::command] pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> { - log::info!("Creating password"); if does_password_file_exist()? { return Err(BackendError::WalletFileAlreadyExists); } + log::info!("Creating password"); let mnemonic = Mnemonic::from_str(&mnemonic)?; let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); let password = wallet_storage::UserPassword::new(password); - wallet_storage::store_wallet_login_information(mnemonic, hd_path, &password)?; - Ok(()) + // WIP(JON): extract this one out + let id = wallet_storage::UserId::new("default".to_string()); + + wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password) } #[tauri::command] @@ -361,10 +363,9 @@ pub async fn sign_in_with_password( ) -> Result { log::info!("Signing in with password"); let password = wallet_storage::UserPassword::new(password); - let stored_accounts = wallet_storage::load_existing_wallet_login_information(&password)?; + let id = wallet_storage::UserId::new("default".to_string()); + let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?; - // WIP: we are assuming just a single password is stored - let stored_account = stored_accounts.into_iter().next().unwrap(); let mnemonic = match stored_account { wallet_storage::account_data::StoredAccount::Mnemonic(ref mn) => mn.mnemonic().clone(), }; diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs index f7ba738b93..972019503d 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -8,6 +8,74 @@ use std::fmt::Formatter; use zeroize::Zeroize; use zeroize::Zeroizing; +use crate::error::BackendError; + +use super::encryption::EncryptedData; +use super::password::UserId; +use super::UserPassword; + +const CURRENT_WALLET_FILE_VERSION: u32 = 1; + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct StoredWallet { + pub version: u32, + pub accounts: Vec, +} + +impl StoredWallet { + pub fn is_empty(&self) -> bool { + self.accounts.is_empty() + } + + pub fn len(&self) -> usize { + self.accounts.len() + } + + fn encrypted_account(&self, id: &UserId) -> Result<&EncryptedData, BackendError> { + self + .accounts + .iter() + .find(|account| &account.id == id) + .map(|account| &account.account) + .ok_or(BackendError::NoSuchWalletId) + } + + pub fn decrypt_account( + &self, + id: &UserId, + password: &UserPassword, + ) -> Result { + self.encrypted_account(id)?.decrypt_struct(password) + } + + pub fn decrypt_all(&self, password: &UserPassword) -> Result, BackendError> { + self + .accounts + .iter() + .map(|account| account.account.decrypt_struct(password)) + .collect::, _>>() + } + + pub fn password_can_decrypt_all(&self, password: &UserPassword) -> bool { + self.decrypt_all(password).is_ok() + } +} + +impl Default for StoredWallet { + fn default() -> Self { + StoredWallet { + version: CURRENT_WALLET_FILE_VERSION, + accounts: Vec::new(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct EncryptedAccount { + pub id: UserId, + pub account: EncryptedData, +} + // future-proofing #[derive(Serialize, Deserialize, Debug, Zeroize)] #[serde(untagged)] diff --git a/nym-wallet/src-tauri/src/wallet_storage/encryption.rs b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs index af30b350af..fbac118f89 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/encryption.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs @@ -42,7 +42,7 @@ pub(crate) struct EncryptedData { impl Drop for EncryptedData { fn drop(&mut self) { - self.zeroize() + self.zeroize(); } } @@ -182,6 +182,7 @@ where T: Serialize, { let bytes = serde_json::to_vec(data).map_err(|_| BackendError::EncryptionError)?; + //dbg!(&bytes); let (salt, iv) = random_salt_and_iv(); let ciphertext = encrypt(&bytes, password, &salt, &iv)?; diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index c9705ed464..875f50a390 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -1,16 +1,20 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub(crate) use crate::wallet_storage::password::UserPassword; +use crate::operations::mixnet::account::create_new_account; +pub(crate) use crate::wallet_storage::password::{UserId, UserPassword}; use crate::error::BackendError; use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME}; use crate::wallet_storage::account_data::StoredAccount; use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData}; use cosmrs::bip32::DerivationPath; +use serde::{Deserialize, Serialize}; use std::fs::{create_dir_all, OpenOptions}; use std::path::PathBuf; +use self::account_data::{EncryptedAccount, StoredWallet}; + pub(crate) mod account_data; pub(crate) mod encryption; @@ -26,30 +30,42 @@ pub(crate) fn wallet_login_filepath() -> Result { get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME)) } -pub(crate) fn load_existing_wallet_login_information( - password: &UserPassword, -) -> Result, BackendError> { +pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result { let store_dir = get_storage_directory()?; let filepath = store_dir.join(WALLET_INFO_FILENAME); + load_existing_wallet_at_file(filepath) +} - load_existing_wallet_login_information_at_file(filepath, password) +fn load_existing_wallet_at_file(filepath: PathBuf) -> Result { + if !filepath.exists() { + return Err(BackendError::WalletNotFound); + } + let file = OpenOptions::new().read(true).open(filepath)?; + let wallet: StoredWallet = serde_json::from_reader(file)?; + Ok(wallet) +} + +pub(crate) fn load_existing_wallet_login_information( + id: &UserId, + password: &UserPassword, +) -> Result { + let store_dir = get_storage_directory()?; + let filepath = store_dir.join(WALLET_INFO_FILENAME); + load_existing_wallet_login_information_at_file(filepath, id, password) } fn load_existing_wallet_login_information_at_file( filepath: PathBuf, + id: &UserId, password: &UserPassword, -) -> Result, BackendError> { - if !filepath.exists() { - return Ok(Vec::new()); - } - let file = OpenOptions::new().read(true).open(filepath)?; - let encrypted_data: EncryptedData> = serde_json::from_reader(file)?; - encrypted_data.decrypt_struct(password) +) -> Result { + load_existing_wallet_at_file(filepath)?.decrypt_account(id, password) } pub(crate) fn store_wallet_login_information( mnemonic: bip39::Mnemonic, hd_path: DerivationPath, + id: UserId, password: &UserPassword, ) -> Result<(), BackendError> { // make sure the entire directory structure exists @@ -57,21 +73,33 @@ pub(crate) fn store_wallet_login_information( create_dir_all(&store_dir)?; let filepath = store_dir.join(WALLET_INFO_FILENAME); - store_wallet_login_information_at_file(filepath, mnemonic, hd_path, password) + store_wallet_login_information_at_file(filepath, mnemonic, hd_path, id, password) } fn store_wallet_login_information_at_file( filepath: PathBuf, mnemonic: bip39::Mnemonic, hd_path: DerivationPath, + id: UserId, password: &UserPassword, ) -> Result<(), BackendError> { - let mut all_accounts = - load_existing_wallet_login_information_at_file(filepath.clone(), password)?; - let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path); - all_accounts.push(new_account); + let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) { + Err(BackendError::WalletNotFound) => StoredWallet::default(), + result => result?, + }; - let encrypted = encrypt_struct(&all_accounts, password)?; + // Confirm that the given password also can unlock the other entries + if !stored_wallet.password_can_decrypt_all(password) { + return Err(BackendError::WalletDifferentPasswordDetected); + } + + let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path); + let new_encrypted_account = EncryptedAccount { + id, + account: encrypt_struct(&new_account, password)?, + }; + + stored_wallet.accounts.push(new_encrypted_account); let file = OpenOptions::new() .create(true) @@ -79,9 +107,7 @@ fn store_wallet_login_information_at_file( .truncate(true) .open(filepath)?; - serde_json::to_writer_pretty(file, &encrypted)?; - - Ok(()) + Ok(serde_json::to_writer_pretty(file, &stored_wallet)?) } // this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX @@ -91,16 +117,13 @@ fn store_wallet_login_information_at_file( #[cfg(test)] mod tests { + use crate::wallet_storage::encryption::encrypt_data; + use super::*; use config::defaults::COSMOS_DERIVATION_PATH; use std::path::Path; use tempfile::tempdir; - fn read_encrypted_blob(file: PathBuf) -> EncryptedData> { - let file = OpenOptions::new().read(true).open(&file).unwrap(); - serde_json::from_reader(file).unwrap() - } - // I'm not 100% sure how to feel about having to touch the file system at all #[test] fn storing_wallet_information() { @@ -115,29 +138,36 @@ mod tests { let password = UserPassword::new("password".to_string()); let bad_password = UserPassword::new("bad-password".to_string()); - // nothing was stored on the disk, so regardless of password used, there will be no error, but - // returned list will be empty - assert!( - load_existing_wallet_login_information_at_file(wallet_file.clone(), &password) - .unwrap() - .is_empty() - ); - assert!( - load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password) - .unwrap() - .is_empty() - ); + let id1 = UserId::new("first".to_string()); + let id2 = UserId::new("second".to_string()); - // store the first account + // Nothing was stored on the disk + assert!(matches!( + load_existing_wallet_at_file(wallet_file.clone()), + Err(BackendError::WalletNotFound), + )); + assert!(matches!( + load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password), + Err(BackendError::WalletNotFound), + )); + + // Store the first account store_wallet_login_information_at_file( wallet_file.clone(), dummy_account1.clone(), cosmos_hd_path.clone(), + id1.clone(), &password, ) .unwrap(); - let encrypted_blob = read_encrypted_blob(wallet_file.clone()); + let stored_wallet = load_existing_wallet_at_file(wallet_file.clone()).unwrap(); + assert_eq!(stored_wallet.len(), 1); + assert_eq!( + stored_wallet.accounts[0].id, + UserId::new("first".to_string()) + ); + let encrypted_blob = &stored_wallet.accounts[0].account; // some actual ciphertext was saved assert!(!encrypted_blob.ciphertext().is_empty()); @@ -147,53 +177,65 @@ mod tests { let original_salt = encrypted_blob.salt().to_vec(); // trying to load it with wrong password now fails - assert!( - load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password).is_err() - ); + assert!(matches!( + load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &bad_password), + Err(BackendError::DecryptionError), + )); + // and with the wrong id also fails + assert!(matches!( + load_existing_wallet_login_information_at_file(wallet_file.clone(), &id2, &password), + Err(BackendError::NoSuchWalletId), + )); - let loaded_accounts = - load_existing_wallet_login_information_at_file(wallet_file.clone(), &password).unwrap(); - println!("{:?}", loaded_accounts); - assert_eq!(1, loaded_accounts.len()); + let loaded_account = + load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap(); - let StoredAccount::Mnemonic(acc) = &loaded_accounts[0]; + let StoredAccount::Mnemonic(ref acc) = loaded_account; assert_eq!(&dummy_account1, acc.mnemonic()); assert_eq!(&cosmos_hd_path, acc.hd_path()); - // can't store extra account if you use different password - assert!(store_wallet_login_information_at_file( - wallet_file.clone(), - dummy_account2.clone(), - cosmos_hd_path.clone(), - &bad_password, - ) - .is_err()); + // Can't store extra account if you use different password + assert!(matches!( + store_wallet_login_information_at_file( + wallet_file.clone(), + dummy_account2.clone(), + cosmos_hd_path.clone(), + id2.clone(), + &bad_password + ), + Err(BackendError::WalletDifferentPasswordDetected), + )); // add extra account properly now store_wallet_login_information_at_file( wallet_file.clone(), dummy_account2.clone(), different_hd_path.clone(), + id2.clone(), &password, ) .unwrap(); - let encrypted_blob = read_encrypted_blob(wallet_file.clone()); + let loaded_accounts = load_existing_wallet_at_file(wallet_file.clone()).unwrap(); + assert_eq!(2, loaded_accounts.len()); + let encrypted_blob = &loaded_accounts.accounts[1].account; // fresh IV and salt are used assert_ne!(original_iv, encrypted_blob.iv()); assert_ne!(original_salt, encrypted_blob.salt()); - let loaded_accounts = - load_existing_wallet_login_information_at_file(wallet_file, &password).unwrap(); - assert_eq!(2, loaded_accounts.len()); + // WIP(JON): test that a re-saved account has new IV and salt // first account should be unchanged - let StoredAccount::Mnemonic(acc1) = &loaded_accounts[0]; + let loaded_account = + load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap(); + let StoredAccount::Mnemonic(ref acc1) = loaded_account; assert_eq!(&dummy_account1, acc1.mnemonic()); assert_eq!(&cosmos_hd_path, acc1.hd_path()); - let StoredAccount::Mnemonic(acc2) = &loaded_accounts[1]; + let loaded_account = + load_existing_wallet_login_information_at_file(wallet_file, &id2, &password).unwrap(); + let StoredAccount::Mnemonic(ref acc2) = loaded_account; assert_eq!(&dummy_account2, acc2.mnemonic()); assert_eq!(&different_hd_path, acc2.hd_path()); } diff --git a/nym-wallet/src-tauri/src/wallet_storage/password.rs b/nym-wallet/src-tauri/src/wallet_storage/password.rs index 0dd2b0f67c..eb4025cdf8 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/password.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/password.rs @@ -3,6 +3,24 @@ use zeroize::Zeroize; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct UserId(String); + +impl UserId { + // WIP(JON): consider making inner String pub + pub(crate) fn new(id: String) -> UserId { + UserId(id) + } +} + +impl AsRef for UserId { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + // simple wrapper for String that will get zeroized on drop #[derive(Zeroize)] #[zeroize(drop)] From 423cdb1e1bd9f08a5a4d3a7a02b3fe78c834b432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 21 Mar 2022 10:36:41 +0100 Subject: [PATCH 5/8] wallet: tweak error enum names --- nym-wallet/src-tauri/src/error.rs | 6 ++---- .../src-tauri/src/wallet_storage/account_data.rs | 2 +- nym-wallet/src-tauri/src/wallet_storage/mod.rs | 10 +++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 64a74028b7..ffaa6736f2 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -77,16 +77,14 @@ pub enum BackendError { NetworkNotSupported(config::defaults::all::Network), #[error("Could not access the local data storage directory")] UnknownStorageDirectory, - #[error("No nymd validator configured")] - NoNymdValidatorConfigured, #[error("No validator API URL configured")] NoValidatorApiUrlConfigured, #[error("The wallet file already exists")] WalletFileAlreadyExists, #[error("The wallet file is not found")] - WalletNotFound, + WalletFileNotFound, #[error("Account ID not found in wallet")] - NoSuchWalletId, + NoSuchIdInWallet, #[error("Adding a different password to the wallet not currently supported")] WalletDifferentPasswordDetected, } diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs index 972019503d..dc6a4e5835 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -37,7 +37,7 @@ impl StoredWallet { .iter() .find(|account| &account.id == id) .map(|account| &account.account) - .ok_or(BackendError::NoSuchWalletId) + .ok_or(BackendError::NoSuchIdInWallet) } pub fn decrypt_account( diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index 875f50a390..a4720e13e3 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -38,7 +38,7 @@ pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result Result { if !filepath.exists() { - return Err(BackendError::WalletNotFound); + return Err(BackendError::WalletFileNotFound); } let file = OpenOptions::new().read(true).open(filepath)?; let wallet: StoredWallet = serde_json::from_reader(file)?; @@ -84,7 +84,7 @@ fn store_wallet_login_information_at_file( password: &UserPassword, ) -> Result<(), BackendError> { let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) { - Err(BackendError::WalletNotFound) => StoredWallet::default(), + Err(BackendError::WalletFileNotFound) => StoredWallet::default(), result => result?, }; @@ -144,11 +144,11 @@ mod tests { // Nothing was stored on the disk assert!(matches!( load_existing_wallet_at_file(wallet_file.clone()), - Err(BackendError::WalletNotFound), + Err(BackendError::WalletFileNotFound), )); assert!(matches!( load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password), - Err(BackendError::WalletNotFound), + Err(BackendError::WalletFileNotFound), )); // Store the first account @@ -184,7 +184,7 @@ mod tests { // and with the wrong id also fails assert!(matches!( load_existing_wallet_login_information_at_file(wallet_file.clone(), &id2, &password), - Err(BackendError::NoSuchWalletId), + Err(BackendError::NoSuchIdInWallet), )); let loaded_account = From a867921fdd35b20548bb222270bc4791da8e588d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 21 Mar 2022 11:07:21 +0100 Subject: [PATCH 6/8] wallet: general wallet_storage tidy --- .../src/operations/mixnet/account.rs | 21 ++++++++----------- .../src-tauri/src/platform_constants.rs | 4 ++++ .../src/wallet_storage/account_data.rs | 11 +++++++--- .../src/wallet_storage/encryption.rs | 1 - .../src-tauri/src/wallet_storage/mod.rs | 7 ++++--- .../src-tauri/src/wallet_storage/password.rs | 4 +--- 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 328474b787..f980138ddf 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -4,7 +4,7 @@ use crate::error::BackendError; use crate::network::Network; use crate::nymd_client; use crate::state::State; -use crate::wallet_storage; +use crate::wallet_storage::{self, DEFAULT_WALLET_ID}; use bip39::{Language, Mnemonic}; use config::defaults::COSMOS_DERIVATION_PATH; @@ -334,10 +334,11 @@ pub fn does_password_file_exist() -> Result { let file = wallet_storage::wallet_login_filepath()?; if file.exists() { log::info!("Exists: {}", file.to_string_lossy()); + Ok(true) } else { log::info!("Does not exist: {}", file.to_string_lossy()); + Ok(false) } - Ok(file.exists()) } #[tauri::command] @@ -349,10 +350,9 @@ pub fn create_password(mnemonic: String, password: String) -> Result<(), Backend let mnemonic = Mnemonic::from_str(&mnemonic)?; let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); + // Currently we only support a single, default, id in the wallet + let id = wallet_storage::UserId::new(DEFAULT_WALLET_ID.to_string()); let password = wallet_storage::UserPassword::new(password); - // WIP(JON): extract this one out - let id = wallet_storage::UserId::new("default".to_string()); - wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password) } @@ -362,13 +362,10 @@ pub async fn sign_in_with_password( state: tauri::State<'_, Arc>>, ) -> Result { log::info!("Signing in with password"); + + // Currently we only support a single, default, id in the wallet + let id = wallet_storage::UserId::new(DEFAULT_WALLET_ID.to_string()); let password = wallet_storage::UserPassword::new(password); - let id = wallet_storage::UserId::new("default".to_string()); let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?; - - let mnemonic = match stored_account { - wallet_storage::account_data::StoredAccount::Mnemonic(ref mn) => mn.mnemonic().clone(), - }; - - _connect_with_mnemonic(mnemonic, state).await + _connect_with_mnemonic(stored_account.mnemonic().clone(), state).await } diff --git a/nym-wallet/src-tauri/src/platform_constants.rs b/nym-wallet/src-tauri/src/platform_constants.rs index 5565a8ac96..309a5fee7c 100644 --- a/nym-wallet/src-tauri/src/platform_constants.rs +++ b/nym-wallet/src-tauri/src/platform_constants.rs @@ -1,6 +1,9 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +// Specify filenames and other platform specific constants to respect platform conventions, or at +// least, something popular on each respective platform. + cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { pub const STORAGE_DIR_NAME: &str = "nym-wallet"; @@ -12,6 +15,7 @@ cfg_if::cfg_if! { pub const STORAGE_DIR_NAME: &str = "NymWallet"; pub const WALLET_INFO_FILENAME: &str = "saved_wallet.json"; } else { + // This case is likely to be a unix-y system pub const STORAGE_DIR_NAME: &str = "nym-wallet"; pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json"; } diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs index dc6a4e5835..ef641f311d 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -92,6 +92,14 @@ impl StoredAccount { ) -> StoredAccount { StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path }) } + + // If we add accounts backed by something that is not a mnemonic, this should probably be changed + // to return `Option<..>`. + pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { + match self { + StoredAccount::Mnemonic(account) => account.mnemonic(), + } + } } #[derive(Serialize, Deserialize, Debug)] @@ -101,9 +109,6 @@ pub(crate) struct MnemonicAccount { hd_path: DerivationPath, } -// we only ever want to expose those getters in the test code -// WIP(JON): temporarily comment out -//#[cfg(test)] impl MnemonicAccount { pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { &self.mnemonic diff --git a/nym-wallet/src-tauri/src/wallet_storage/encryption.rs b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs index fbac118f89..a8aa27fe84 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/encryption.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/encryption.rs @@ -182,7 +182,6 @@ where T: Serialize, { let bytes = serde_json::to_vec(data).map_err(|_| BackendError::EncryptionError)?; - //dbg!(&bytes); let (salt, iv) = random_salt_and_iv(); let ciphertext = encrypt(&bytes, password, &salt, &iv)?; diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index a4720e13e3..e899a4b1e3 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -1,10 +1,10 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::operations::mixnet::account::create_new_account; pub(crate) use crate::wallet_storage::password::{UserId, UserPassword}; use crate::error::BackendError; +use crate::operations::mixnet::account::create_new_account; use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME}; use crate::wallet_storage::account_data::StoredAccount; use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData}; @@ -20,6 +20,8 @@ pub(crate) mod encryption; mod password; +pub(crate) const DEFAULT_WALLET_ID: &str = "default"; + fn get_storage_directory() -> Result { tauri::api::path::local_data_dir() .map(|dir| dir.join(STORAGE_DIR_NAME)) @@ -117,9 +119,8 @@ fn store_wallet_login_information_at_file( #[cfg(test)] mod tests { - use crate::wallet_storage::encryption::encrypt_data; - use super::*; + use crate::wallet_storage::encryption::encrypt_data; use config::defaults::COSMOS_DERIVATION_PATH; use std::path::Path; use tempfile::tempdir; diff --git a/nym-wallet/src-tauri/src/wallet_storage/password.rs b/nym-wallet/src-tauri/src/wallet_storage/password.rs index eb4025cdf8..8e1f9face9 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/password.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/password.rs @@ -1,15 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use zeroize::Zeroize; - use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use zeroize::Zeroize; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct UserId(String); impl UserId { - // WIP(JON): consider making inner String pub pub(crate) fn new(id: String) -> UserId { UserId(id) } From 38c2ce9837cc1c15412b47b93600e8d6fdd3ecb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 21 Mar 2022 11:13:01 +0100 Subject: [PATCH 7/8] wallet: tweak some type names --- .../src-tauri/src/operations/mixnet/account.rs | 6 +++--- .../src/wallet_storage/account_data.rs | 11 +++++++---- nym-wallet/src-tauri/src/wallet_storage/mod.rs | 18 +++++++++--------- .../src-tauri/src/wallet_storage/password.rs | 10 +++++----- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index f980138ddf..02b632466a 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -4,7 +4,7 @@ use crate::error::BackendError; use crate::network::Network; use crate::nymd_client; use crate::state::State; -use crate::wallet_storage::{self, DEFAULT_WALLET_ID}; +use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID}; use bip39::{Language, Mnemonic}; use config::defaults::COSMOS_DERIVATION_PATH; @@ -351,7 +351,7 @@ pub fn create_password(mnemonic: String, password: String) -> Result<(), Backend let mnemonic = Mnemonic::from_str(&mnemonic)?; let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap(); // Currently we only support a single, default, id in the wallet - let id = wallet_storage::UserId::new(DEFAULT_WALLET_ID.to_string()); + let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string()); let password = wallet_storage::UserPassword::new(password); wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password) } @@ -364,7 +364,7 @@ pub async fn sign_in_with_password( log::info!("Signing in with password"); // Currently we only support a single, default, id in the wallet - let id = wallet_storage::UserId::new(DEFAULT_WALLET_ID.to_string()); + let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string()); let password = wallet_storage::UserPassword::new(password); let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?; _connect_with_mnemonic(stored_account.mnemonic().clone(), state).await diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs index ef641f311d..14e1d5e1cb 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -11,7 +11,7 @@ use zeroize::Zeroizing; use crate::error::BackendError; use super::encryption::EncryptedData; -use super::password::UserId; +use super::password::WalletAccountId; use super::UserPassword; const CURRENT_WALLET_FILE_VERSION: u32 = 1; @@ -31,7 +31,10 @@ impl StoredWallet { self.accounts.len() } - fn encrypted_account(&self, id: &UserId) -> Result<&EncryptedData, BackendError> { + fn encrypted_account( + &self, + id: &WalletAccountId, + ) -> Result<&EncryptedData, BackendError> { self .accounts .iter() @@ -42,7 +45,7 @@ impl StoredWallet { pub fn decrypt_account( &self, - id: &UserId, + id: &WalletAccountId, password: &UserPassword, ) -> Result { self.encrypted_account(id)?.decrypt_struct(password) @@ -72,7 +75,7 @@ impl Default for StoredWallet { #[derive(Serialize, Deserialize, Debug)] pub(crate) struct EncryptedAccount { - pub id: UserId, + pub id: WalletAccountId, pub account: EncryptedData, } diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index e899a4b1e3..13ce72beae 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub(crate) use crate::wallet_storage::password::{UserId, UserPassword}; +pub(crate) use crate::wallet_storage::password::{UserPassword, WalletAccountId}; use crate::error::BackendError; use crate::operations::mixnet::account::create_new_account; @@ -20,7 +20,7 @@ pub(crate) mod encryption; mod password; -pub(crate) const DEFAULT_WALLET_ID: &str = "default"; +pub(crate) const DEFAULT_WALLET_ACCOUNT_ID: &str = "default"; fn get_storage_directory() -> Result { tauri::api::path::local_data_dir() @@ -48,7 +48,7 @@ fn load_existing_wallet_at_file(filepath: PathBuf) -> Result Result { let store_dir = get_storage_directory()?; @@ -58,7 +58,7 @@ pub(crate) fn load_existing_wallet_login_information( fn load_existing_wallet_login_information_at_file( filepath: PathBuf, - id: &UserId, + id: &WalletAccountId, password: &UserPassword, ) -> Result { load_existing_wallet_at_file(filepath)?.decrypt_account(id, password) @@ -67,7 +67,7 @@ fn load_existing_wallet_login_information_at_file( pub(crate) fn store_wallet_login_information( mnemonic: bip39::Mnemonic, hd_path: DerivationPath, - id: UserId, + id: WalletAccountId, password: &UserPassword, ) -> Result<(), BackendError> { // make sure the entire directory structure exists @@ -82,7 +82,7 @@ fn store_wallet_login_information_at_file( filepath: PathBuf, mnemonic: bip39::Mnemonic, hd_path: DerivationPath, - id: UserId, + id: WalletAccountId, password: &UserPassword, ) -> Result<(), BackendError> { let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) { @@ -139,8 +139,8 @@ mod tests { let password = UserPassword::new("password".to_string()); let bad_password = UserPassword::new("bad-password".to_string()); - let id1 = UserId::new("first".to_string()); - let id2 = UserId::new("second".to_string()); + let id1 = WalletAccountId::new("first".to_string()); + let id2 = WalletAccountId::new("second".to_string()); // Nothing was stored on the disk assert!(matches!( @@ -166,7 +166,7 @@ mod tests { assert_eq!(stored_wallet.len(), 1); assert_eq!( stored_wallet.accounts[0].id, - UserId::new("first".to_string()) + WalletAccountId::new("first".to_string()) ); let encrypted_blob = &stored_wallet.accounts[0].account; diff --git a/nym-wallet/src-tauri/src/wallet_storage/password.rs b/nym-wallet/src-tauri/src/wallet_storage/password.rs index 8e1f9face9..f81b5bf7c1 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/password.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/password.rs @@ -5,15 +5,15 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use zeroize::Zeroize; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub(crate) struct UserId(String); +pub(crate) struct WalletAccountId(String); -impl UserId { - pub(crate) fn new(id: String) -> UserId { - UserId(id) +impl WalletAccountId { + pub(crate) fn new(id: String) -> WalletAccountId { + WalletAccountId(id) } } -impl AsRef for UserId { +impl AsRef for WalletAccountId { fn as_ref(&self) -> &str { self.0.as_ref() } From 2d82a5190570c3b66350161ff69985bd9cc7692e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 25 Mar 2022 14:27:42 +0100 Subject: [PATCH 8/8] wallet: reject storing account with the same id --- nym-wallet/src-tauri/src/error.rs | 2 ++ .../src/wallet_storage/account_data.rs | 23 +++++++++++++++-- .../src-tauri/src/wallet_storage/mod.rs | 25 ++++++++++++++----- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index ffaa6736f2..077e949939 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -85,6 +85,8 @@ pub enum BackendError { WalletFileNotFound, #[error("Account ID not found in wallet")] NoSuchIdInWallet, + #[error("Account ID already found in wallet")] + IdAlreadyExistsInWallet, #[error("Adding a different password to the wallet not currently supported")] WalletDifferentPasswordDetected, } diff --git a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs index 14e1d5e1cb..817bcc067b 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/account_data.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/account_data.rs @@ -18,11 +18,15 @@ const CURRENT_WALLET_FILE_VERSION: u32 = 1; #[derive(Serialize, Deserialize, Debug)] pub(crate) struct StoredWallet { - pub version: u32, - pub accounts: Vec, + version: u32, + accounts: Vec, } impl StoredWallet { + pub fn version(&self) -> u32 { + self.version + } + pub fn is_empty(&self) -> bool { self.accounts.is_empty() } @@ -31,6 +35,10 @@ impl StoredWallet { self.accounts.len() } + pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> { + self.accounts.get(index) + } + fn encrypted_account( &self, id: &WalletAccountId, @@ -43,6 +51,17 @@ impl StoredWallet { .ok_or(BackendError::NoSuchIdInWallet) } + pub fn add_encrypted_account( + &mut self, + new_account: EncryptedAccount, + ) -> Result<(), BackendError> { + if let Ok(_) = self.encrypted_account(&new_account.id) { + return Err(BackendError::IdAlreadyExistsInWallet); + } + self.accounts.push(new_account); + Ok(()) + } + pub fn decrypt_account( &self, id: &WalletAccountId, diff --git a/nym-wallet/src-tauri/src/wallet_storage/mod.rs b/nym-wallet/src-tauri/src/wallet_storage/mod.rs index 13ce72beae..1ac9c14084 100644 --- a/nym-wallet/src-tauri/src/wallet_storage/mod.rs +++ b/nym-wallet/src-tauri/src/wallet_storage/mod.rs @@ -101,7 +101,7 @@ fn store_wallet_login_information_at_file( account: encrypt_struct(&new_account, password)?, }; - stored_wallet.accounts.push(new_encrypted_account); + stored_wallet.add_encrypted_account(new_encrypted_account)?; let file = OpenOptions::new() .create(true) @@ -165,10 +165,10 @@ mod tests { let stored_wallet = load_existing_wallet_at_file(wallet_file.clone()).unwrap(); assert_eq!(stored_wallet.len(), 1); assert_eq!( - stored_wallet.accounts[0].id, + stored_wallet.encrypted_account_by_index(0).unwrap().id, WalletAccountId::new("first".to_string()) ); - let encrypted_blob = &stored_wallet.accounts[0].account; + let encrypted_blob = &stored_wallet.encrypted_account_by_index(0).unwrap().account; // some actual ciphertext was saved assert!(!encrypted_blob.ciphertext().is_empty()); @@ -188,6 +188,18 @@ mod tests { Err(BackendError::NoSuchIdInWallet), )); + // and storing the same id again fails + assert!(matches!( + store_wallet_login_information_at_file( + wallet_file.clone(), + dummy_account1.clone(), + cosmos_hd_path.clone(), + id1.clone(), + &password, + ), + Err(BackendError::IdAlreadyExistsInWallet), + )); + let loaded_account = load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap(); @@ -219,14 +231,15 @@ mod tests { let loaded_accounts = load_existing_wallet_at_file(wallet_file.clone()).unwrap(); assert_eq!(2, loaded_accounts.len()); - let encrypted_blob = &loaded_accounts.accounts[1].account; + let encrypted_blob = &loaded_accounts + .encrypted_account_by_index(1) + .unwrap() + .account; // fresh IV and salt are used assert_ne!(original_iv, encrypted_blob.iv()); assert_ne!(original_salt, encrypted_blob.salt()); - // WIP(JON): test that a re-saved account has new IV and salt - // first account should be unchanged let loaded_account = load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();