feat(wallet): add security settings (#3262)

This commit is contained in:
Pierre Dommerc
2023-04-17 16:36:39 +02:00
committed by GitHub
parent de64da8e20
commit cde53c02e7
17 changed files with 655 additions and 126 deletions
+1
View File
@@ -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(&current_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()
+365 -6
View File
@@ -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 dont 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';
+1 -1
View File
@@ -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;
+3 -1
View File
@@ -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>
+8
View File
@@ -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 });