feat(wallet): add security settings (#3262)
This commit is contained in:
@@ -42,6 +42,7 @@ fn main() {
|
||||
mixnet::account::connect_with_mnemonic,
|
||||
mixnet::account::create_new_mnemonic,
|
||||
mixnet::account::create_password,
|
||||
mixnet::account::update_password,
|
||||
mixnet::account::does_password_file_exist,
|
||||
mixnet::account::get_balance,
|
||||
mixnet::account::list_accounts,
|
||||
|
||||
@@ -307,6 +307,16 @@ pub fn create_password(mnemonic: Mnemonic, password: UserPassword) -> Result<(),
|
||||
wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_password(
|
||||
current_password: UserPassword,
|
||||
new_password: UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
log::info!("Updating password");
|
||||
|
||||
wallet_storage::update_encrypted_logins(¤t_password, &new_password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sign_in_with_password(
|
||||
password: UserPassword,
|
||||
|
||||
@@ -108,6 +108,22 @@ impl StoredWallet {
|
||||
self.get_encrypted_login(id)?.decrypt_struct(password)
|
||||
}
|
||||
|
||||
pub fn reencrypt_all(
|
||||
&mut self,
|
||||
current_password: &UserPassword,
|
||||
new_password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
if current_password == new_password {
|
||||
return Ok(());
|
||||
}
|
||||
for encrypted_login in &mut self.accounts {
|
||||
let login = encrypted_login.account.decrypt_struct(current_password)?;
|
||||
*encrypted_login =
|
||||
EncryptedLogin::encrypt(encrypted_login.id.clone(), &login, new_password)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredLogin>, BackendError> {
|
||||
self.accounts
|
||||
.iter()
|
||||
|
||||
@@ -138,6 +138,7 @@ fn store_login_at_file(
|
||||
write_to_file(filepath, &stored_wallet)
|
||||
}
|
||||
|
||||
/// Store the login with multiple accounts support
|
||||
pub(crate) fn store_login_with_multiple_accounts(
|
||||
mnemonic: Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
@@ -152,6 +153,43 @@ pub(crate) fn store_login_with_multiple_accounts(
|
||||
store_login_with_multiple_accounts_at_file(&filepath, mnemonic, hd_path, id, password)
|
||||
}
|
||||
|
||||
/// Update all logins with multiple accounts support
|
||||
pub(crate) fn update_encrypted_logins(
|
||||
current_password: &UserPassword,
|
||||
new_password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
let store_dir = get_storage_directory()?;
|
||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||
|
||||
update_encrypted_logins_at_file(&filepath, current_password, new_password)
|
||||
}
|
||||
|
||||
fn update_encrypted_logins_at_file(
|
||||
filepath: &Path,
|
||||
current_password: &UserPassword,
|
||||
new_password: &UserPassword,
|
||||
) -> Result<(), BackendError> {
|
||||
if current_password == new_password {
|
||||
return Ok(());
|
||||
}
|
||||
let mut stored_wallet = load_existing_wallet_at_file(filepath)?;
|
||||
|
||||
stored_wallet.reencrypt_all(current_password, new_password)?;
|
||||
write_to_file(filepath, &stored_wallet)
|
||||
}
|
||||
|
||||
fn new_encrypted_login(
|
||||
mnemonic: Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
id: LoginId,
|
||||
password: &UserPassword,
|
||||
) -> Result<EncryptedLogin, BackendError> {
|
||||
let mut new_accounts = MultipleAccounts::new();
|
||||
new_accounts.add(DEFAULT_FIRST_ACCOUNT_NAME.into(), mnemonic, hd_path)?;
|
||||
let new_login = StoredLogin::Multiple(new_accounts);
|
||||
EncryptedLogin::encrypt(id, &new_login, password)
|
||||
}
|
||||
|
||||
fn store_login_with_multiple_accounts_at_file(
|
||||
filepath: &Path,
|
||||
mnemonic: Mnemonic,
|
||||
@@ -170,12 +208,8 @@ fn store_login_with_multiple_accounts_at_file(
|
||||
return Err(BackendError::WalletDifferentPasswordDetected);
|
||||
}
|
||||
|
||||
let mut new_accounts = MultipleAccounts::new();
|
||||
new_accounts.add(DEFAULT_FIRST_ACCOUNT_NAME.into(), mnemonic, hd_path)?;
|
||||
let new_login = StoredLogin::Multiple(new_accounts);
|
||||
let new_encrypted_login = EncryptedLogin::encrypt(id, &new_login, password)?;
|
||||
|
||||
stored_wallet.add_encrypted_login(new_encrypted_login)?;
|
||||
let new_login = new_encrypted_login(mnemonic, hd_path, id, password)?;
|
||||
stored_wallet.add_encrypted_login(new_login)?;
|
||||
|
||||
write_to_file(filepath, &stored_wallet)
|
||||
}
|
||||
@@ -467,6 +501,37 @@ mod tests {
|
||||
assert!(!login.account.ciphertext().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_single_login_with_multi_then_update_pwd_and_load() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = Mnemonic::generate(24).unwrap();
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_with_multiple_accounts_at_file(
|
||||
&wallet_file,
|
||||
account1,
|
||||
cosmos_hd_path,
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
update_encrypted_logins_at_file(&wallet_file, &password, &new_password).unwrap();
|
||||
|
||||
let stored_wallet = load_existing_wallet_at_file(&wallet_file).unwrap();
|
||||
assert_eq!(stored_wallet.len(), 1);
|
||||
|
||||
let login = stored_wallet.get_encrypted_login_by_index(0).unwrap();
|
||||
assert_eq!(login.id, id1);
|
||||
|
||||
// some actual ciphertext was saved
|
||||
assert!(!login.account.ciphertext().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_twice_for_the_same_id_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
@@ -633,6 +698,109 @@ mod tests {
|
||||
assert_eq!(&hd_path, acc.hd_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_single_login_then_update_pwd_and_load() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
update_encrypted_logins_at_file(&wallet_file, &password, &new_password).unwrap();
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &new_password).unwrap();
|
||||
let acc = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc.mnemonic());
|
||||
assert_eq!(&hd_path, acc.hd_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_single_login_then_update_pwd_with_identical_pwd_is_noop_but_okay() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
update_encrypted_logins_at_file(&wallet_file, &password, &password).unwrap();
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap();
|
||||
let acc = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc.mnemonic());
|
||||
assert_eq!(&hd_path, acc.hd_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_single_login_then_update_pwd_with_wrong_current_pwd_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let wrong_password = UserPassword::new("wrong_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = update_encrypted_logins_at_file(&wallet_file, &wrong_password, &new_password)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, BackendError::DecryptionError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_single_login_then_update_pwd_and_load_with_wrong_pwd_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
update_encrypted_logins_at_file(&wallet_file, &password, &new_password).unwrap();
|
||||
|
||||
let err = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap_err();
|
||||
assert!(matches!(err, BackendError::DecryptionError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_and_load_a_single_login_with_multi() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
@@ -662,6 +830,88 @@ mod tests {
|
||||
assert_eq!(account.hd_path(), &hd_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_single_login_with_multi_then_update_pwd_and_load() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let acc1 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_with_multiple_accounts_at_file(
|
||||
&wallet_file,
|
||||
acc1.clone(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
update_encrypted_logins_at_file(&wallet_file, &password, &new_password).unwrap();
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet_file, &id1, &new_password).unwrap();
|
||||
let accounts = loaded_login.as_multiple_accounts().unwrap();
|
||||
assert_eq!(accounts.len(), 1);
|
||||
let account = accounts
|
||||
.get_account(&DEFAULT_FIRST_ACCOUNT_NAME.into())
|
||||
.unwrap();
|
||||
assert_eq!(account.id().as_ref(), DEFAULT_FIRST_ACCOUNT_NAME);
|
||||
assert_eq!(account.mnemonic(), &acc1);
|
||||
assert_eq!(account.hd_path(), &hd_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_single_login_with_multi_then_update_pwd_with_wrong_current_pwd_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let acc1 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let wrong_password = UserPassword::new("wrong_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_with_multiple_accounts_at_file(
|
||||
&wallet_file,
|
||||
acc1.clone(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let err = update_encrypted_logins_at_file(&wallet_file, &wrong_password, &new_password)
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, BackendError::DecryptionError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_single_login_with_multi_then_update_pwd_and_load_with_wrong_pwd_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let acc1 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
|
||||
store_login_with_multiple_accounts_at_file(
|
||||
&wallet_file,
|
||||
acc1.clone(),
|
||||
hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
update_encrypted_logins_at_file(&wallet_file, &password, &new_password).unwrap();
|
||||
|
||||
let err = load_existing_login_at_file(&wallet_file, &id1, &password).unwrap_err();
|
||||
assert!(matches!(err, BackendError::DecryptionError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_a_second_login_with_a_different_password_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
@@ -813,6 +1063,58 @@ mod tests {
|
||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_two_mnemonic_accounts_using_two_logins_then_update_pwd_and_load() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = Mnemonic::generate(24).unwrap();
|
||||
let account2 = Mnemonic::generate(24).unwrap();
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let different_hd_path: DerivationPath = "m".parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let id1 = LoginId::new("first".to_string());
|
||||
let id2 = LoginId::new("second".to_string());
|
||||
|
||||
// Store the first login with an account
|
||||
store_login_at_file(
|
||||
&wallet,
|
||||
account1.clone(),
|
||||
cosmos_hd_path.clone(),
|
||||
id1.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let login = load_existing_login_at_file(&wallet, &id1, &password).unwrap();
|
||||
let acc = login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc.hd_path());
|
||||
|
||||
// Store a second login, also with an account
|
||||
store_login_at_file(
|
||||
&wallet,
|
||||
account2.clone(),
|
||||
different_hd_path.clone(),
|
||||
id2.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
update_encrypted_logins_at_file(&wallet, &password, &new_password).unwrap();
|
||||
|
||||
// first account should be unchanged
|
||||
let loaded_login = load_existing_login_at_file(&wallet, &id1, &new_password).unwrap();
|
||||
let acc1 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account1, acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
|
||||
let loaded_login = load_existing_login_at_file(&wallet, &id2, &new_password).unwrap();
|
||||
let acc2 = loaded_login.as_mnemonic_account().unwrap();
|
||||
assert_eq!(&account2, acc2.mnemonic());
|
||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_one_mnemonic_account_and_one_multi_account() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
@@ -1157,6 +1459,63 @@ mod tests {
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_account_to_existing_login_with_multi_then_update_pwd_and_load() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
let account1 = Mnemonic::generate(24).unwrap();
|
||||
let account2 = Mnemonic::generate(24).unwrap();
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
let password = UserPassword::new("password".to_string());
|
||||
let new_password = UserPassword::new("new_password".to_string());
|
||||
let login_id = LoginId::new("first".to_string());
|
||||
let appended_account = AccountId::new("second".to_string());
|
||||
|
||||
store_login_with_multiple_accounts_at_file(
|
||||
&wallet_file,
|
||||
account1.clone(),
|
||||
hd_path.clone(),
|
||||
login_id.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Append a second mnenonic to the same login
|
||||
append_account_to_login_at_file(
|
||||
&wallet_file,
|
||||
account2.clone(),
|
||||
hd_path.clone(),
|
||||
login_id.clone(),
|
||||
appended_account.clone(),
|
||||
&password,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Update the password
|
||||
update_encrypted_logins_at_file(&wallet_file, &password, &new_password).unwrap();
|
||||
|
||||
// Expect that we can load these 2 accounts with the new password
|
||||
let loaded_login =
|
||||
load_existing_login_at_file(&wallet_file, &login_id, &new_password).unwrap();
|
||||
let loaded_accounts = loaded_login.as_multiple_accounts().unwrap();
|
||||
let expected = vec![
|
||||
WalletAccount::new(
|
||||
DEFAULT_FIRST_ACCOUNT_NAME.into(),
|
||||
MnemonicAccount::new(account1, hd_path.clone()),
|
||||
),
|
||||
WalletAccount::new(
|
||||
appended_account,
|
||||
MnemonicAccount::new(account2, hd_path.clone()),
|
||||
),
|
||||
]
|
||||
.into();
|
||||
assert_eq!(loaded_accounts, &expected);
|
||||
|
||||
// Expect that trying to load these 2 accounts with the old password fails
|
||||
let err = load_existing_login_at_file(&wallet_file, &login_id, &password).unwrap_err();
|
||||
assert!(matches!(err, BackendError::DecryptionError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_the_same_mnemonic_twice_fails() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { AccountsContext, AppContext } from 'src/context';
|
||||
import { isPasswordCreated } from 'src/requests';
|
||||
import { EditAccountModal } from './modals/EditAccountModal';
|
||||
import { AddAccountModal } from './modals/AddAccountModal';
|
||||
import { AccountsModal } from './modals/AccountsModal';
|
||||
import { MnemonicModal } from './modals/MnemonicModal';
|
||||
import { AccountOverview } from './AccountOverview';
|
||||
import { MultiAccountHowTo } from './MultiAccountHowTo';
|
||||
import { MultiAccountWithPwdHowTo } from './MultiAccountWithPwdHowTo';
|
||||
import { MultiAccountHowTo } from './modals/MultiAccountHowTo';
|
||||
|
||||
export const Accounts = () => {
|
||||
const { accounts, selectedAccount, setDialogToDisplay } = useContext(AccountsContext);
|
||||
@@ -25,29 +23,15 @@ export const Accounts = () => {
|
||||
|
||||
export const SingleAccount = () => {
|
||||
const [showHowToDialog, setShowHowToDialog] = useState(false);
|
||||
const [passwordExist, setPasswordExist] = useState(false);
|
||||
const { clientDetails } = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPassword = async () => {
|
||||
if (await isPasswordCreated()) {
|
||||
setPasswordExist(true);
|
||||
}
|
||||
};
|
||||
checkPassword();
|
||||
}, [clientDetails]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccountOverview
|
||||
account={{ id: 'Account 1', address: clientDetails?.client_address || '' }}
|
||||
onClick={() => setShowHowToDialog(true)}
|
||||
/>
|
||||
{passwordExist ? (
|
||||
<MultiAccountWithPwdHowTo show={showHowToDialog} handleClose={() => setShowHowToDialog(false)} />
|
||||
) : (
|
||||
<MultiAccountHowTo show={showHowToDialog} handleClose={() => setShowHowToDialog(false)} />
|
||||
)}
|
||||
<MultiAccountHowTo show={showHowToDialog} handleClose={() => setShowHowToDialog(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
import { Warning } from '../Warning';
|
||||
|
||||
const passwordCreationSteps = [
|
||||
'Log out from the wallet',
|
||||
'Sign in using “Sign in with mnemonic” button',
|
||||
'On the next screen select “Create a password"',
|
||||
'Type in the mnemonic you want to create a password for and follow the next steps',
|
||||
'Sign back in the wallet using your new password',
|
||||
'Come back to this page to import or create new accounts',
|
||||
];
|
||||
|
||||
// TODO add the link href value
|
||||
export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => (
|
||||
<SimpleModal
|
||||
open={show}
|
||||
onClose={handleClose}
|
||||
header="Create account"
|
||||
okLabel="Ok"
|
||||
onOk={handleClose as () => Promise<void>}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Warning sx={{ textAlign: 'center' }}>
|
||||
<Typography fontWeight={600}>
|
||||
In order to import or create account(s) you first need to create a password
|
||||
</Typography>
|
||||
</Warning>
|
||||
<Typography fontWeight={600}>How to create a password for your account</Typography>
|
||||
{passwordCreationSteps.map((step, index) => (
|
||||
<Stack key={step} direction="row" spacing={1}>
|
||||
<Typography fontWeight={600}>{`${index + 1}.`}</Typography>
|
||||
<Typography>{`${step}`}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
<Link
|
||||
href="https://nymtech.net/docs/stable/wallet/#importing-or-creating-accounts-when-you-have-signed-in-with-mnemonic"
|
||||
target="_blank"
|
||||
text="Open Nym docs for this guide in a browser window"
|
||||
fontWeight={600}
|
||||
/>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { SimpleModal } from '../Modals/SimpleModal';
|
||||
import { Warning } from '../Warning';
|
||||
|
||||
const passwordCreationSteps = [
|
||||
'Log out',
|
||||
'Click on “Forgot password?” ',
|
||||
'On the next screen select “Create new password” ',
|
||||
'Create a new password and use it to sign in to your wallet and create multiple accounts',
|
||||
];
|
||||
|
||||
// TODO add the link href value
|
||||
export const MultiAccountWithPwdHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => (
|
||||
<SimpleModal
|
||||
open={show}
|
||||
onClose={handleClose}
|
||||
header="Create account"
|
||||
okLabel="Ok"
|
||||
onOk={handleClose as () => Promise<void>}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Warning sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body2" fontWeight={600} sx={{ mb: 1 }}>
|
||||
This machine already has a password set on it
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
In order to import or create account(s) you need to log in with your password or create a new one. Creating a
|
||||
new password will overwrite any old one. Make sure your mnemonics are all written down before creating a new
|
||||
password.
|
||||
</Typography>
|
||||
</Warning>
|
||||
<Typography fontWeight={600}>How to create a new password for this account</Typography>
|
||||
{passwordCreationSteps.map((step, index) => (
|
||||
<Stack key={step} direction="row" spacing={1}>
|
||||
<Typography fontWeight={600}>{`${index + 1}.`}</Typography>
|
||||
<Typography variant="body2">{`${step}`}</Typography>
|
||||
</Stack>
|
||||
))}
|
||||
<Link
|
||||
href="https://nymtech.net/docs/stable/wallet#importing-or-creating-accounts-when-you-have-signed-in-with-mnemonic-but-a-password-already-exists-on-your-machine"
|
||||
target="_blank"
|
||||
text="Open Nym docs for this guide in a browser window"
|
||||
variant="body2"
|
||||
fontWeight={600}
|
||||
/>
|
||||
</Stack>
|
||||
</SimpleModal>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { ConfirmationModal } from '../../Modals/ConfirmationModal';
|
||||
import { Alert } from '../../Alert';
|
||||
|
||||
export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => (
|
||||
<ConfirmationModal
|
||||
open={show}
|
||||
onClose={handleClose}
|
||||
confirmButton="Ok"
|
||||
onConfirm={handleClose as () => Promise<void>}
|
||||
title=""
|
||||
maxWidth="xs"
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Alert
|
||||
title={
|
||||
<Typography sx={{ fontWeight: 600 }}>
|
||||
In order to import or create account(s) you need to log in with password
|
||||
</Typography>
|
||||
}
|
||||
bgColor="#fff"
|
||||
/>
|
||||
<Typography>
|
||||
If you don’t have a password set for your account, go to the Settings, under Security tab create a password
|
||||
</Typography>
|
||||
<Typography>
|
||||
If you already have a password, log in to the wallet using your password then try create/import accounts
|
||||
</Typography>
|
||||
</Stack>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
@@ -19,12 +19,13 @@ const headers: Header[] = [
|
||||
{
|
||||
header: 'Routing score',
|
||||
id: 'routing-score',
|
||||
tooltipText: 'Routing score',
|
||||
tooltipText:
|
||||
"Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test",
|
||||
},
|
||||
{
|
||||
header: 'Average score',
|
||||
id: 'average-score',
|
||||
tooltipText: 'Average score',
|
||||
tooltipText: "Gateway's average routing score in the last 24 hours",
|
||||
},
|
||||
{
|
||||
header: 'IP',
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, FormControl, Stack } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { createPassword } from '../../requests';
|
||||
import { PasswordStrength } from '../../pages/auth/components';
|
||||
import { MnemonicInput, PasswordInput } from '../textfields';
|
||||
|
||||
const PasswordCreateForm = ({ onPwdSaved }: { onPwdSaved: () => void }) => {
|
||||
const [mnemonic, setMnemonic] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSafePassword, setIsSafePassword] = useState(false);
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const reset = () => {
|
||||
setMnemonic('');
|
||||
setPassword('');
|
||||
setConfirmedPassword('');
|
||||
};
|
||||
|
||||
const savePassword = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await createPassword({ mnemonic, password });
|
||||
reset();
|
||||
onPwdSaved();
|
||||
} catch (e) {
|
||||
enqueueSnackbar(e as string, { variant: 'error' });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={3} alignItems="center">
|
||||
<FormControl fullWidth>
|
||||
<Stack spacing={3} mt={2}>
|
||||
<MnemonicInput mnemonic={mnemonic} onUpdateMnemonic={(m) => setMnemonic(m)} />
|
||||
<PasswordInput password={password} onUpdatePassword={(pwd) => setPassword(pwd)} label="Password" />
|
||||
<PasswordStrength password={password} handleIsSafePassword={setIsSafePassword} withWarnings />
|
||||
<PasswordInput
|
||||
password={confirmedPassword}
|
||||
onUpdatePassword={(pwd) => setConfirmedPassword(pwd)}
|
||||
label="Confirm password"
|
||||
/>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
disabled={
|
||||
mnemonic.length === 0 ||
|
||||
password !== confirmedPassword ||
|
||||
password.length === 0 ||
|
||||
isLoading ||
|
||||
!isSafePassword
|
||||
}
|
||||
onClick={savePassword}
|
||||
>
|
||||
Save Password
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordCreateForm;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, FormControl, Stack } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { updatePassword } from '../../requests';
|
||||
import { PasswordStrength } from '../../pages/auth/components';
|
||||
import { PasswordInput } from '../textfields';
|
||||
|
||||
const PasswordUpdateForm = ({ onPwdSaved }: { onPwdSaved: () => void }) => {
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSafePassword, setIsSafePassword] = useState(false);
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const reset = () => {
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmedPassword('');
|
||||
};
|
||||
|
||||
const savePassword = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await updatePassword({ currentPassword, newPassword });
|
||||
reset();
|
||||
onPwdSaved();
|
||||
} catch (e) {
|
||||
enqueueSnackbar(e as string, { variant: 'error' });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={3} alignItems="center">
|
||||
<FormControl fullWidth>
|
||||
<Stack spacing={3} mt={2}>
|
||||
<PasswordInput
|
||||
password={currentPassword}
|
||||
onUpdatePassword={(pwd) => setCurrentPassword(pwd)}
|
||||
label="Current password"
|
||||
autoFocus
|
||||
/>
|
||||
<PasswordInput password={newPassword} onUpdatePassword={(pwd) => setNewPassword(pwd)} label="New password" />
|
||||
<PasswordStrength password={newPassword} handleIsSafePassword={setIsSafePassword} withWarnings />
|
||||
<PasswordInput
|
||||
password={confirmedPassword}
|
||||
onUpdatePassword={(pwd) => setConfirmedPassword(pwd)}
|
||||
label="Confirm password"
|
||||
/>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
disabled={
|
||||
currentPassword.length === 0 ||
|
||||
currentPassword === newPassword ||
|
||||
newPassword !== confirmedPassword ||
|
||||
newPassword.length === 0 ||
|
||||
isLoading ||
|
||||
!isSafePassword
|
||||
}
|
||||
onClick={savePassword}
|
||||
>
|
||||
Save New Password
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordUpdateForm;
|
||||
@@ -1,2 +1,4 @@
|
||||
export { default as ThemeSwitcher } from './ThemeSwitcher';
|
||||
export { default as AppVersion } from './AppVersion';
|
||||
export { default as PasswordCreateForm } from './PasswordCreateForm';
|
||||
export { default as PasswordUpdateForm } from './PasswordUpdateForm';
|
||||
|
||||
@@ -12,7 +12,7 @@ export type TAuthContext = {
|
||||
setError: (err?: string) => void;
|
||||
setMnemonic: (mnc: string) => void;
|
||||
generateMnemonic: () => Promise<void>;
|
||||
setPassword: (paswd: string) => void;
|
||||
setPassword: (pwd: string) => void;
|
||||
resetState: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ const getPasswordStrength = (score: ZXCVBNScore) => {
|
||||
};
|
||||
|
||||
export const PasswordStrength = ({
|
||||
password,
|
||||
password = '',
|
||||
withWarnings,
|
||||
handleIsSafePassword,
|
||||
}: {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Grid, Stack, Typography } from '@mui/material';
|
||||
import { isPasswordCreated } from '../../requests';
|
||||
import { PasswordCreateForm, PasswordUpdateForm } from '../../components/Settings';
|
||||
import { ConfirmationModal } from '../../components';
|
||||
|
||||
const SecuritySettings = () => {
|
||||
const [passwordExists, setPasswordExists] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState<{ open: boolean; state?: 'created' | 'changed' }>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
const checkForPassword = async () => {
|
||||
const hasPassword = await isPasswordCreated();
|
||||
setPasswordExists(hasPassword);
|
||||
};
|
||||
|
||||
const onPasswordCreated = () => {
|
||||
setPasswordExists(true);
|
||||
setModalOpen({ open: true, state: 'created' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkForPassword();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmationModal
|
||||
title={`Password successfully ${modalOpen.state}`}
|
||||
onClose={() => setModalOpen({ open: false })}
|
||||
onConfirm={() => setModalOpen({ open: false })}
|
||||
maxWidth="xs"
|
||||
confirmButton="OK"
|
||||
open={modalOpen.open}
|
||||
/>
|
||||
<Grid container spacing={2} padding={3}>
|
||||
<Grid item sm={12} md={6} lg={8}>
|
||||
<Stack direction="column" gap={1}>
|
||||
{passwordExists ? (
|
||||
<Typography variant="h6">Change your password</Typography>
|
||||
) : (
|
||||
<Typography variant="h6">Create a password</Typography>
|
||||
)}
|
||||
|
||||
{passwordExists ? (
|
||||
<Typography variant="caption" sx={{ color: 'nym.text.muted', maxWidth: '220px' }}>
|
||||
Change your existing password. A strong password should have at least 8 characters, one capital letter,
|
||||
a number and a special character
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: 'nym.text.muted', maxWidth: '220px' }}>
|
||||
Create a strong password for your wallet. A strong password should have at least 8 characters, one
|
||||
capital letter, a number and a special character
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item sm={12} md={6} lg={4}>
|
||||
{!passwordExists && <PasswordCreateForm onPwdSaved={() => onPasswordCreated()} />}
|
||||
{passwordExists && <PasswordUpdateForm onPwdSaved={() => setModalOpen({ open: true, state: 'changed' })} />}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecuritySettings;
|
||||
@@ -8,8 +8,9 @@ import { PageLayout } from '../../layouts';
|
||||
import { Tabs } from '../../components/Tabs';
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import AdvancedSettings from './AdvancedSettings';
|
||||
import SecuritySettings from './SecuritySettings';
|
||||
|
||||
const tabs = ['General', 'Advanced'] as const;
|
||||
const tabs = ['General', 'Security', 'Advanced'] as const;
|
||||
type SettingsTabs = (typeof tabs)[number];
|
||||
|
||||
const Settings = () => {
|
||||
@@ -78,6 +79,7 @@ const Settings = () => {
|
||||
>
|
||||
<Divider />
|
||||
{currentTab === 'General' && <GeneralSettings />}
|
||||
{currentTab === 'Security' && <SecuritySettings />}
|
||||
{currentTab === 'Advanced' && <AdvancedSettings />}
|
||||
</NymCard>
|
||||
</PageLayout>
|
||||
|
||||
@@ -17,6 +17,14 @@ export const isPasswordCreated = async () => invokeWrapper<boolean>('does_passwo
|
||||
export const createPassword = async ({ mnemonic, password }: { mnemonic: string; password: string }) =>
|
||||
invokeWrapper<void>('create_password', { mnemonic, password });
|
||||
|
||||
export const updatePassword = async ({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
}: {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}) => invokeWrapper<void>('update_password', { currentPassword, newPassword });
|
||||
|
||||
export const signInWithPassword = async (password: string) =>
|
||||
invokeWrapper<Account>('sign_in_with_password', { password });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user