Compare commits

...

16 Commits

Author SHA1 Message Date
Jon Häggblad d729081996 primitives: add vec conversion functions 2022-03-29 10:49:35 +02:00
Jon Häggblad 2ce1c8833f primitives: add note about more coin tests 2022-03-29 10:49:33 +02:00
Jon Häggblad 635ca745d9 primitives: move Coin type top its own file 2022-03-29 10:49:31 +02:00
Jon Häggblad 4ef7fac377 primitives: conversions 2022-03-29 10:49:28 +02:00
Jon Häggblad c1d136bd54 primitives: initial creation of files 2022-03-29 10:49:22 +02:00
Mark Sinclair 8ad3565f2c Create nym-release-publish.yml 2022-03-28 18:16:41 +01:00
Jon Häggblad 47bdf38776 wallet: fix clippy 2022-03-25 21:25:09 +01:00
Jon Häggblad cdd883c174 Merge pull request #1153 from nymtech/feature/wire-up-wallet-storage
wallet: wire up account storage
2022-03-25 21:14:19 +01:00
Jon Häggblad 2d82a51905 wallet: reject storing account with the same id 2022-03-25 21:04:35 +01:00
Jon Häggblad 38c2ce9837 wallet: tweak some type names 2022-03-25 21:04:35 +01:00
Jon Häggblad a867921fdd wallet: general wallet_storage tidy 2022-03-25 21:04:35 +01:00
Jon Häggblad 423cdb1e1b wallet: tweak error enum names 2022-03-25 21:04:35 +01:00
Jon Häggblad 7aeac58fd9 wallet: inline encryption of wallet file 2022-03-25 21:04:35 +01:00
Jon Häggblad 30fafa509c wallet: swap println to log 2022-03-25 21:04:35 +01:00
Jon Häggblad c950556506 wallet: platform_constants 2022-03-25 21:04:35 +01:00
Jon Häggblad 9a49213973 wallets: provide placeholder functions for ui password 2022-03-25 21:04:34 +01:00
21 changed files with 523 additions and 78 deletions
+43
View File
@@ -0,0 +1,43 @@
name: Publish Nym binaries
on:
release:
types: [created]
jobs:
publish-nym:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Check the release tag starts with `nym-binaries-`
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false
uses: actions/github-script@v3
with:
script: |
core.setFailed('Release tag did not start with nym-binaries-...')
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --release
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
with:
files: |
target/release/nym-client
target/release/nym-gateway
target/release/nym-mixnode
target/release/nym-socks5-client
target/release/nym-validator-api
Generated
+10
View File
@@ -3683,6 +3683,15 @@ dependencies = [
"uint", "uint",
] ]
[[package]]
name = "primitives"
version = "0.1.0"
dependencies = [
"cosmrs",
"cosmwasm-std",
"thiserror",
]
[[package]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "1.1.3" version = "1.1.3"
@@ -5999,6 +6008,7 @@ dependencies = [
"log", "log",
"mixnet-contract-common", "mixnet-contract-common",
"network-defaults", "network-defaults",
"primitives",
"prost", "prost",
"reqwest", "reqwest",
"serde", "serde",
+1
View File
@@ -44,6 +44,7 @@ members = [
"common/nymsphinx/params", "common/nymsphinx/params",
"common/nymsphinx/types", "common/nymsphinx/types",
"common/pemstore", "common/pemstore",
"common/primitives",
"common/socks5/proxy-helpers", "common/socks5/proxy-helpers",
"common/socks5/requests", "common/socks5/requests",
"common/topology", "common/topology",
@@ -21,6 +21,7 @@ url = { version = "2.2", features = ["serde"] }
coconut-interface = { path = "../../coconut-interface" } coconut-interface = { path = "../../coconut-interface" }
network-defaults = { path = "../../network-defaults" } network-defaults = { path = "../../network-defaults" }
primitives = { path = "../../primitives" }
validator-api-requests = { path = "../../../validator-api/validator-api-requests" } validator-api-requests = { path = "../../../validator-api/validator-api-requests" }
# required for nymd-client # required for nymd-client
@@ -369,14 +369,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
&self, &self,
sender_address: &AccountId, sender_address: &AccountId,
recipient_address: &AccountId, recipient_address: &AccountId,
amount: Vec<Coin>, amount: Vec<primitives::Coin>,
fee: Fee, fee: Fee,
memo: impl Into<String> + Send + 'static, memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> { ) -> Result<broadcast::tx_commit::Response, NymdError> {
let send_msg = MsgSend { let send_msg = MsgSend {
from_address: sender_address.clone(), from_address: sender_address.clone(),
to_address: recipient_address.clone(), to_address: recipient_address.clone(),
amount, amount: primitives::coin::try_into(amount)?,
} }
.to_any() .to_any()
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?; .map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
@@ -108,6 +108,9 @@ pub enum NymdError {
#[error("Abci query failed with code {0} - {1}")] #[error("Abci query failed with code {0} - {1}")]
AbciError(u32, abci::Log), AbciError(u32, abci::Log),
#[error("Failed to handle primitives")]
PrimitivesError(#[from] primitives::PrimitivesError)
} }
impl NymdError { impl NymdError {
@@ -606,7 +606,7 @@ impl<C> NymdClient<C> {
pub async fn send( pub async fn send(
&self, &self,
recipient: &AccountId, recipient: &AccountId,
amount: Vec<CosmosCoin>, amount: Vec<primitives::Coin>,
memo: impl Into<String> + Send + 'static, memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> ) -> Result<broadcast::tx_commit::Response, NymdError>
where where
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "primitives"
version = "0.1.0"
edition = "2021"
[dependencies]
cosmrs = { version = "0.4.1", features = ["rpc", "bip32", "cosmwasm"] }
cosmwasm-std = "1.0.0-beta3"
thiserror = "1.0"
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::PrimitivesError;
use core::fmt;
use cosmwasm_std::Uint128;
use std::str::FromStr;
/// Common Coin type for the backend.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Coin {
pub amount: Uint128,
pub denom: String,
}
impl Coin {
pub fn new(amount: u128, denom: impl Into<String>) -> Coin {
Coin {
amount: Uint128::new(amount),
denom: denom.into(),
}
}
}
impl fmt::Display for Coin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", self.amount, self.denom)
}
}
impl TryFrom<cosmrs::Coin> for Coin {
type Error = PrimitivesError;
fn try_from(cosmos_coin: cosmrs::Coin) -> Result<Self, Self::Error> {
Ok(Self {
amount: cosmos_coin.amount.to_string().as_str().try_into()?,
denom: cosmos_coin.denom.to_string(),
})
}
}
impl TryFrom<Coin> for cosmrs::Coin {
type Error = PrimitivesError;
fn try_from(coin: Coin) -> Result<Self, Self::Error> {
Ok(Self {
denom: cosmrs::Denom::from_str(&coin.denom)?,
amount: cosmrs::Decimal::from_str(&coin.amount.to_string())?,
})
}
}
pub fn try_into(coins: Vec<Coin>) -> Result<Vec<cosmrs::Coin>, PrimitivesError> {
coins.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()
}
pub fn into(coins: Vec<Coin>) -> Vec<cosmwasm_std::Coin> {
coins.into_iter().map(Into::into).collect()
}
impl From<cosmwasm_std::Coin> for Coin {
fn from(cosmwasm_coin: cosmwasm_std::Coin) -> Self {
Self {
amount: cosmwasm_coin.amount,
denom: cosmwasm_coin.denom,
}
}
}
impl From<Coin> for cosmwasm_std::Coin {
fn from(coin: Coin) -> Self {
Self {
amount: coin.amount,
denom: coin.denom,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn convert_to_and_from_cosmwasm_coin() {
let coin = Coin::new(42, "ucoin");
let cosmwasm_coin: cosmwasm_std::Coin = coin.clone().into();
assert_eq!(coin, Coin::from(cosmwasm_coin));
}
#[test]
fn convert_to_and_from_cosmos_coin() {
let coin = Coin::new(42, "ucoin");
let cosmos_coin: cosmrs::Coin = coin.clone().try_into().unwrap();
assert_eq!(coin, Coin::try_from(cosmos_coin).unwrap());
}
// WIP(JON): more tests
// Especially converting from cosmrs::Coin
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PrimitivesError {
#[error("{source}")]
CosmwasmError {
#[from]
source: cosmwasm_std::StdError,
},
#[error("{source}")]
CosmrsError {
#[from]
source: cosmrs::ErrorReport,
},
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod coin;
mod error;
pub use error::PrimitivesError;
pub use coin::Coin;
+1
View File
@@ -2843,6 +2843,7 @@ dependencies = [
"argon2", "argon2",
"base64", "base64",
"bip39", "bip39",
"cfg-if",
"coconut-interface", "coconut-interface",
"config", "config",
"cosmrs", "cosmrs",
+1
View File
@@ -20,6 +20,7 @@ tauri-macros = "=1.0.0-rc.1"
[dependencies] [dependencies]
bip39 = "1.0" bip39 = "1.0"
cfg-if = "1.0.0"
dirs = "4.0" dirs = "4.0"
eyre = "0.6.5" eyre = "0.6.5"
futures = "0.3.15" futures = "0.3.15"
+10 -2
View File
@@ -77,10 +77,18 @@ pub enum BackendError {
NetworkNotSupported(config::defaults::all::Network), NetworkNotSupported(config::defaults::all::Network),
#[error("Could not access the local data storage directory")] #[error("Could not access the local data storage directory")]
UnknownStorageDirectory, UnknownStorageDirectory,
#[error("No nymd validator configured")]
NoNymdValidatorConfigured,
#[error("No validator API URL configured")] #[error("No validator API URL configured")]
NoValidatorApiUrlConfigured, NoValidatorApiUrlConfigured,
#[error("The wallet file already exists")]
WalletFileAlreadyExists,
#[error("The wallet file is not found")]
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,
} }
impl Serialize for BackendError { impl Serialize for BackendError {
+4
View File
@@ -15,6 +15,7 @@ mod error;
mod menu; mod menu;
mod network; mod network;
mod operations; mod operations;
mod platform_constants;
mod state; mod state;
mod utils; mod utils;
// temporarily until it is actually used // temporarily until it is actually used
@@ -37,8 +38,11 @@ fn main() {
mixnet::account::connect_with_mnemonic, mixnet::account::connect_with_mnemonic,
mixnet::account::create_new_account, mixnet::account::create_new_account,
mixnet::account::create_new_mnemonic, mixnet::account::create_new_mnemonic,
mixnet::account::create_password,
mixnet::account::does_password_file_exist,
mixnet::account::get_balance, mixnet::account::get_balance,
mixnet::account::logout, mixnet::account::logout,
mixnet::account::sign_in_with_password,
mixnet::account::switch_network, mixnet::account::switch_network,
mixnet::account::update_validator_urls, mixnet::account::update_validator_urls,
mixnet::admin::get_contract_settings, mixnet::admin::get_contract_settings,
@@ -4,8 +4,11 @@ use crate::error::BackendError;
use crate::network::Network; use crate::network::Network;
use crate::nymd_client; use crate::nymd_client;
use crate::state::State; use crate::state::State;
use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID};
use bip39::{Language, Mnemonic}; use bip39::{Language, Mnemonic};
use config::defaults::COSMOS_DERIVATION_PATH;
use cosmrs::bip32::DerivationPath;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
@@ -227,9 +230,10 @@ async fn choose_validators(
.next() .next()
// We always have at least one hardcoded default validator // We always have at least one hardcoded default validator
.unwrap(); .unwrap();
println!( log::info!(
"Using default for {network}: {}, {}", "Using default for {network}: {}, {}",
default_validator.nymd_url, default_validator.api_url, default_validator.nymd_url,
default_validator.api_url,
); );
default_validator default_validator
}); });
@@ -305,9 +309,10 @@ async fn try_connect_to_validator(
)?; )?;
if is_validator_connection_ok(&client).await { if is_validator_connection_ok(&client).await {
println!( log::info!(
"Connection ok for {network}: {}, {}", "Connection ok for {network}: {}, {}",
validator.nymd_url, validator.api_url validator.nymd_url,
validator.api_url
); );
Ok(Some((network, validator.clone()))) Ok(Some((network, validator.clone())))
} else { } else {
@@ -322,3 +327,45 @@ async fn is_validator_connection_ok(client: &Client<SigningNymdClient>) -> bool
Err(_) | Ok(_) => true, Err(_) | Ok(_) => true,
} }
} }
#[tauri::command]
pub fn does_password_file_exist() -> Result<bool, BackendError> {
log::info!("Checking wallet file");
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)
}
}
#[tauri::command]
pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> {
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();
// Currently we only support a single, default, id in the wallet
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)
}
#[tauri::command]
pub async fn sign_in_with_password(
password: String,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Account, BackendError> {
log::info!("Signing in with password");
// Currently we only support a single, default, id in the wallet
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
}
@@ -0,0 +1,22 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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";
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 {
// 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";
}
}
@@ -8,6 +8,96 @@ use std::fmt::Formatter;
use zeroize::Zeroize; use zeroize::Zeroize;
use zeroize::Zeroizing; use zeroize::Zeroizing;
use crate::error::BackendError;
use super::encryption::EncryptedData;
use super::password::WalletAccountId;
use super::UserPassword;
const CURRENT_WALLET_FILE_VERSION: u32 = 1;
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct StoredWallet {
version: u32,
accounts: Vec<EncryptedAccount>,
}
impl StoredWallet {
pub fn version(&self) -> u32 {
self.version
}
pub fn is_empty(&self) -> bool {
self.accounts.is_empty()
}
pub fn len(&self) -> usize {
self.accounts.len()
}
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
self.accounts.get(index)
}
fn encrypted_account(
&self,
id: &WalletAccountId,
) -> Result<&EncryptedData<StoredAccount>, BackendError> {
self
.accounts
.iter()
.find(|account| &account.id == id)
.map(|account| &account.account)
.ok_or(BackendError::NoSuchIdInWallet)
}
pub fn add_encrypted_account(
&mut self,
new_account: EncryptedAccount,
) -> Result<(), BackendError> {
if self.encrypted_account(&new_account.id).is_ok() {
return Err(BackendError::IdAlreadyExistsInWallet);
}
self.accounts.push(new_account);
Ok(())
}
pub fn decrypt_account(
&self,
id: &WalletAccountId,
password: &UserPassword,
) -> Result<StoredAccount, BackendError> {
self.encrypted_account(id)?.decrypt_struct(password)
}
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredAccount>, BackendError> {
self
.accounts
.iter()
.map(|account| account.account.decrypt_struct(password))
.collect::<Result<Vec<_>, _>>()
}
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: WalletAccountId,
pub account: EncryptedData<StoredAccount>,
}
// future-proofing // future-proofing
#[derive(Serialize, Deserialize, Debug, Zeroize)] #[derive(Serialize, Deserialize, Debug, Zeroize)]
#[serde(untagged)] #[serde(untagged)]
@@ -24,6 +114,14 @@ impl StoredAccount {
) -> StoredAccount { ) -> StoredAccount {
StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path }) 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)] #[derive(Serialize, Deserialize, Debug)]
@@ -33,8 +131,6 @@ pub(crate) struct MnemonicAccount {
hd_path: DerivationPath, hd_path: DerivationPath,
} }
// we only ever want to expose those getters in the test code
#[cfg(test)]
impl MnemonicAccount { impl MnemonicAccount {
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic { pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
&self.mnemonic &self.mnemonic
@@ -42,7 +42,7 @@ pub(crate) struct EncryptedData<T> {
impl<T> Drop for EncryptedData<T> { impl<T> Drop for EncryptedData<T> {
fn drop(&mut self) { fn drop(&mut self) {
self.zeroize() self.zeroize();
} }
} }
+123 -66
View File
@@ -1,19 +1,26 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
pub(crate) use crate::wallet_storage::password::{UserPassword, WalletAccountId};
use crate::error::BackendError; 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::account_data::StoredAccount;
use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData}; use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData};
use crate::wallet_storage::password::UserPassword;
use cosmrs::bip32::DerivationPath; use cosmrs::bip32::DerivationPath;
use serde::{Deserialize, Serialize};
use std::fs::{create_dir_all, OpenOptions}; use std::fs::{create_dir_all, OpenOptions};
use std::path::PathBuf; use std::path::PathBuf;
use self::account_data::{EncryptedAccount, StoredWallet};
pub(crate) mod account_data; pub(crate) mod account_data;
pub(crate) mod encryption; pub(crate) mod encryption;
mod password; mod password;
const STORAGE_DIR_NAME: &str = "NymWallet"; pub(crate) const DEFAULT_WALLET_ACCOUNT_ID: &str = "default";
const WALLET_INFO_FILENAME: &str = "saved_wallet.json";
fn get_storage_directory() -> Result<PathBuf, BackendError> { fn get_storage_directory() -> Result<PathBuf, BackendError> {
tauri::api::path::local_data_dir() tauri::api::path::local_data_dir()
@@ -25,52 +32,76 @@ pub(crate) fn wallet_login_filepath() -> Result<PathBuf, BackendError> {
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME)) get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
} }
pub(crate) fn load_existing_wallet_login_information( pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result<StoredWallet, BackendError> {
password: &UserPassword,
) -> Result<Vec<StoredAccount>, BackendError> {
let store_dir = get_storage_directory()?; let store_dir = get_storage_directory()?;
let filepath = store_dir.join(WALLET_INFO_FILENAME); 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<StoredWallet, BackendError> {
if !filepath.exists() {
return Err(BackendError::WalletFileNotFound);
}
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: &WalletAccountId,
password: &UserPassword,
) -> Result<StoredAccount, BackendError> {
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( fn load_existing_wallet_login_information_at_file(
filepath: PathBuf, filepath: PathBuf,
id: &WalletAccountId,
password: &UserPassword, password: &UserPassword,
) -> Result<Vec<StoredAccount>, BackendError> { ) -> Result<StoredAccount, BackendError> {
if !filepath.exists() { load_existing_wallet_at_file(filepath)?.decrypt_account(id, password)
return Ok(Vec::new());
}
let file = OpenOptions::new().read(true).open(filepath)?;
let encrypted_data: EncryptedData<Vec<StoredAccount>> = serde_json::from_reader(file)?;
encrypted_data.decrypt_struct(password)
} }
pub(crate) fn store_wallet_login_information( pub(crate) fn store_wallet_login_information(
mnemonic: bip39::Mnemonic, mnemonic: bip39::Mnemonic,
hd_path: DerivationPath, hd_path: DerivationPath,
password: UserPassword, id: WalletAccountId,
password: &UserPassword,
) -> Result<(), BackendError> { ) -> Result<(), BackendError> {
// make sure the entire directory structure exists // make sure the entire directory structure exists
let store_dir = get_storage_directory()?; let store_dir = get_storage_directory()?;
create_dir_all(&store_dir)?; create_dir_all(&store_dir)?;
let filepath = store_dir.join(WALLET_INFO_FILENAME); 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( fn store_wallet_login_information_at_file(
filepath: PathBuf, filepath: PathBuf,
mnemonic: bip39::Mnemonic, mnemonic: bip39::Mnemonic,
hd_path: DerivationPath, hd_path: DerivationPath,
id: WalletAccountId,
password: &UserPassword, password: &UserPassword,
) -> Result<(), BackendError> { ) -> Result<(), BackendError> {
let mut all_accounts = let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
load_existing_wallet_login_information_at_file(filepath.clone(), password)?; Err(BackendError::WalletFileNotFound) => StoredWallet::default(),
let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path); result => result?,
all_accounts.push(new_account); };
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.add_encrypted_account(new_encrypted_account)?;
let file = OpenOptions::new() let file = OpenOptions::new()
.create(true) .create(true)
@@ -78,9 +109,7 @@ fn store_wallet_login_information_at_file(
.truncate(true) .truncate(true)
.open(filepath)?; .open(filepath)?;
serde_json::to_writer_pretty(file, &encrypted)?; Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
Ok(())
} }
// this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX // this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX
@@ -91,15 +120,11 @@ fn store_wallet_login_information_at_file(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::wallet_storage::encryption::encrypt_data;
use config::defaults::COSMOS_DERIVATION_PATH; use config::defaults::COSMOS_DERIVATION_PATH;
use std::path::Path; use std::path::Path;
use tempfile::tempdir; use tempfile::tempdir;
fn read_encrypted_blob(file: PathBuf) -> EncryptedData<Vec<StoredAccount>> {
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 // I'm not 100% sure how to feel about having to touch the file system at all
#[test] #[test]
fn storing_wallet_information() { fn storing_wallet_information() {
@@ -114,29 +139,36 @@ mod tests {
let password = UserPassword::new("password".to_string()); let password = UserPassword::new("password".to_string());
let bad_password = UserPassword::new("bad-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 let id1 = WalletAccountId::new("first".to_string());
// returned list will be empty let id2 = WalletAccountId::new("second".to_string());
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()
);
// store the first account // Nothing was stored on the disk
assert!(matches!(
load_existing_wallet_at_file(wallet_file.clone()),
Err(BackendError::WalletFileNotFound),
));
assert!(matches!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password),
Err(BackendError::WalletFileNotFound),
));
// Store the first account
store_wallet_login_information_at_file( store_wallet_login_information_at_file(
wallet_file.clone(), wallet_file.clone(),
dummy_account1.clone(), dummy_account1.clone(),
cosmos_hd_path.clone(), cosmos_hd_path.clone(),
id1.clone(),
&password, &password,
) )
.unwrap(); .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.encrypted_account_by_index(0).unwrap().id,
WalletAccountId::new("first".to_string())
);
let encrypted_blob = &stored_wallet.encrypted_account_by_index(0).unwrap().account;
// some actual ciphertext was saved // some actual ciphertext was saved
assert!(!encrypted_blob.ciphertext().is_empty()); assert!(!encrypted_blob.ciphertext().is_empty());
@@ -146,53 +178,78 @@ mod tests {
let original_salt = encrypted_blob.salt().to_vec(); let original_salt = encrypted_blob.salt().to_vec();
// trying to load it with wrong password now fails // trying to load it with wrong password now fails
assert!( assert!(matches!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password).is_err() 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::NoSuchIdInWallet),
));
let loaded_accounts = // and storing the same id again fails
load_existing_wallet_login_information_at_file(wallet_file.clone(), &password).unwrap(); assert!(matches!(
println!("{:?}", loaded_accounts); store_wallet_login_information_at_file(
assert_eq!(1, loaded_accounts.len()); wallet_file.clone(),
dummy_account1.clone(),
cosmos_hd_path.clone(),
id1.clone(),
&password,
),
Err(BackendError::IdAlreadyExistsInWallet),
));
let StoredAccount::Mnemonic(acc) = &loaded_accounts[0]; let loaded_account =
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();
let StoredAccount::Mnemonic(ref acc) = loaded_account;
assert_eq!(&dummy_account1, acc.mnemonic()); assert_eq!(&dummy_account1, acc.mnemonic());
assert_eq!(&cosmos_hd_path, acc.hd_path()); assert_eq!(&cosmos_hd_path, acc.hd_path());
// can't store extra account if you use different password // Can't store extra account if you use different password
assert!(store_wallet_login_information_at_file( assert!(matches!(
wallet_file.clone(), store_wallet_login_information_at_file(
dummy_account2.clone(), wallet_file.clone(),
cosmos_hd_path.clone(), dummy_account2.clone(),
&bad_password, cosmos_hd_path.clone(),
) id2.clone(),
.is_err()); &bad_password
),
Err(BackendError::WalletDifferentPasswordDetected),
));
// add extra account properly now // add extra account properly now
store_wallet_login_information_at_file( store_wallet_login_information_at_file(
wallet_file.clone(), wallet_file.clone(),
dummy_account2.clone(), dummy_account2.clone(),
different_hd_path.clone(), different_hd_path.clone(),
id2.clone(),
&password, &password,
) )
.unwrap(); .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
.encrypted_account_by_index(1)
.unwrap()
.account;
// fresh IV and salt are used // fresh IV and salt are used
assert_ne!(original_iv, encrypted_blob.iv()); assert_ne!(original_iv, encrypted_blob.iv());
assert_ne!(original_salt, encrypted_blob.salt()); 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());
// first account should be unchanged // 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!(&dummy_account1, acc1.mnemonic());
assert_eq!(&cosmos_hd_path, acc1.hd_path()); 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!(&dummy_account2, acc2.mnemonic());
assert_eq!(&different_hd_path, acc2.hd_path()); assert_eq!(&different_hd_path, acc2.hd_path());
} }
@@ -1,8 +1,24 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use zeroize::Zeroize; use zeroize::Zeroize;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct WalletAccountId(String);
impl WalletAccountId {
pub(crate) fn new(id: String) -> WalletAccountId {
WalletAccountId(id)
}
}
impl AsRef<str> for WalletAccountId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
// simple wrapper for String that will get zeroized on drop // simple wrapper for String that will get zeroized on drop
#[derive(Zeroize)] #[derive(Zeroize)]
#[zeroize(drop)] #[zeroize(drop)]