wallet: inline encryption of wallet file
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -326,26 +326,28 @@ async fn is_validator_connection_ok(client: &Client<SigningNymdClient>) -> bool
|
||||
pub fn does_password_file_exist() -> Result<bool, BackendError> {
|
||||
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]
|
||||
@@ -355,10 +357,9 @@ pub async fn sign_in_with_password(
|
||||
) -> Result<Account, BackendError> {
|
||||
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(),
|
||||
};
|
||||
|
||||
@@ -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<EncryptedAccount>,
|
||||
}
|
||||
|
||||
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<StoredAccount>, 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<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: UserId,
|
||||
pub account: EncryptedData<StoredAccount>,
|
||||
}
|
||||
|
||||
// future-proofing
|
||||
#[derive(Serialize, Deserialize, Debug, Zeroize)]
|
||||
#[serde(untagged)]
|
||||
|
||||
@@ -42,7 +42,7 @@ pub(crate) struct EncryptedData<T> {
|
||||
|
||||
impl<T> Drop for EncryptedData<T> {
|
||||
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)?;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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<PathBuf, BackendError> {
|
||||
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
|
||||
}
|
||||
|
||||
pub(crate) fn load_existing_wallet_login_information(
|
||||
password: &UserPassword,
|
||||
) -> Result<Vec<StoredAccount>, BackendError> {
|
||||
pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result<StoredWallet, BackendError> {
|
||||
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<StoredWallet, BackendError> {
|
||||
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<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(
|
||||
filepath: PathBuf,
|
||||
id: &UserId,
|
||||
password: &UserPassword,
|
||||
) -> Result<Vec<StoredAccount>, BackendError> {
|
||||
if !filepath.exists() {
|
||||
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)
|
||||
) -> Result<StoredAccount, BackendError> {
|
||||
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<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
|
||||
#[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());
|
||||
}
|
||||
|
||||
@@ -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<str> 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)]
|
||||
|
||||
Reference in New Issue
Block a user