diff --git a/examples/explorer/.eslintrc b/examples/explorer/.eslintrc index a44d541..8dd7fb2 100644 --- a/examples/explorer/.eslintrc +++ b/examples/explorer/.eslintrc @@ -15,7 +15,7 @@ }, "rules": { "func-names": "error", - "new-cap": "off", + "new-cap": ["off", { "capIsNewExceptionPattern": "^Aesjs\.." }], "arrow-parens": "off", "consistent-return": "off", "comma-dangle": "off", diff --git a/examples/wallet/app/Routes.js b/examples/wallet/app/Routes.js index 5764ab0..4e96b62 100644 --- a/examples/wallet/app/Routes.js +++ b/examples/wallet/app/Routes.js @@ -9,7 +9,8 @@ import Settings from './pages/Settings'; import ChooseRestoreOrImport from './containers/ChooseRestoreOrImport'; import Delegate from './pages/Delegate'; import Index from './containers/Index'; -import InputKeys from './containers/InputKeys'; +import InputKeys from './pages/InputKeys'; +import UnlockWallet from './containers/UnlockWallet'; export default () => ( @@ -19,6 +20,7 @@ export default () => ( + { return function setAccountThunk(dispatch) { - return getAccountFromPrivateKey(privateKey).then( - curry(initializeKeysAndRedirect)(dispatch) + return getAccountFromPrivateKey(privateKey).then(keys => + curry(initializeKeysAndRedirect)(dispatch, keys, unlockWalletPassword) ); }; } -export function setAccountFromPrivateKey(privateKey: string): Thunk { - return function setAccountFromPrivateKeyThunk(dispatch) { - getAccountFromPrivateKey(privateKey) - .then(loadedPrivateKey => { - dispatch({ - type: SET_KEYS, - ...loadedPrivateKey - }); - return dispatch(updateAccountTransactionsAndState()); - }) - .catch(error => { - console.error(error); - throw new Error(PRIVATE_KEY_ERROR); - }); - }; -} - -export function updateAccountTransactionsAndState(): Thunk { - return function updateAccountTransactionsAndStateThunk(dispatch) { - return Promise.all([ - dispatch(updateAccountTransactions()), - dispatch(updateNodeSettings()), - dispatch(updateAccountState()) - ]).then(() => dispatch(push(routes.WALLET))); +export function setKeysWithUnlockWalletPassword( + unlockWalletPassword: string = '' +): Thunk { + return function setKeysWithUnlockWalletPasswordThunk(dispatch) { + const accountKeys = readEncryptedAccountInfo(unlockWalletPassword); + if (accountKeys) { + return getAccountFromPrivateKey(accountKeys.privateKey).then(keys => + curry(initializeKeysAndRedirect)(dispatch, keys, unlockWalletPassword) + ); + } + throw new Error('Invalid password'); }; } @@ -83,30 +74,36 @@ export function updateAccountTransactionsAndState(): Thunk { * @param {boolean} saveAccount If true, the keys are stored in the local storage */ const initializeKeysAndRedirect = ( - dispatch: Dispatch, + dispatch: Dispatch, keys: AccountKeys, - saveAccount?: boolean = true + unlockWalletPassword: string = '' ) => { dispatch({ type: SET_KEYS, ...keys }); - if (saveAccount) { - saveAccountInfoInLocalStorage(keys); - } - return dispatch(updateAccountTransactionsAndState()); + saveEncryptedAccountInfo(unlockWalletPassword, keys); + + return Promise.all([ + dispatch(updateAccountTransactions()), + dispatch(updateNodeSettings()), + dispatch(updateAccountState()) + ]) + .catch(console.error) + .then(() => dispatch(push(routes.WALLET))); }; export function setAccountFromMnemonic( mnemonicPhrase: string, - mnemonicPassword?: string -) { + mnemonicPassword: string, + unlockWalletPassword: string +): Thunk { if (isValidMnemonic(mnemonicPhrase)) { const seed = createSeedFromMnemonic(mnemonicPhrase, mnemonicPassword); return function setAccountThunk(dispatch) { - return getAccountFromSeed(seed).then( - curry(initializeKeysAndRedirect)(dispatch) + return getAccountFromSeed(seed).then(keys => + curry(initializeKeysAndRedirect)(dispatch, keys, unlockWalletPassword) ); }; } @@ -119,7 +116,7 @@ export type SetAccountStateAction = { } & AccountState; export const SET_ACCOUNT_STATE = 'SET_ACCOUNT_STATE'; -export function updateAccountState(): Thunk { +export function updateAccountState(): Thunk { return function updateAccountStateThunk(dispatch, getState) { const { identifier }: { identifier: Identifier } = getState().account; if (!identifier) { @@ -139,7 +136,7 @@ export function updateAccountState(): Thunk { // TODO: display a notification or something .catch(() => { console.error('there was an error fetching account info'); - return Promise.reject(new Error(ACCOUNT_STATE_ERROR)); + return Promise.reject(); }) ); }; @@ -151,7 +148,7 @@ export type SetTransactionsAction = { }; export const SET_TRANSACTIONS = 'SET_TRANSACTIONS'; -export function updateAccountTransactions(): Thunk { +export function updateAccountTransactions(): Thunk { return function updateAccountTransactionsThunk(dispatch, getState) { const { address }: { address: Address } = getState().account; if (!address) { @@ -171,7 +168,7 @@ export function updateAccountTransactions(): Thunk { // TODO: display a notification or something .catch(() => { console.error('there was an error fetching transactions'); - return Promise.reject(new Error(ACCOUNT_STATE_ERROR)); + return Promise.reject(); }) ); }; diff --git a/examples/wallet/app/actions/router.js b/examples/wallet/app/actions/router.js index 669c12b..cdb14bb 100644 --- a/examples/wallet/app/actions/router.js +++ b/examples/wallet/app/actions/router.js @@ -2,32 +2,15 @@ import { push } from 'connected-react-router'; import type { Dispatch, GetState } from '../reducers/types'; import type { Address } from '../models'; -import routes from '../constants/routes.json'; -import { - updateAccountTransactionsAndState, - ACCOUNT_STATE_ERROR -} from './account'; -import { readAccountKeysFromLocalStorage } from '../utils/storage'; +import routes from '../constants/routes'; -export const SET_KEYS = 'SET_KEYS'; +import { isUnlockWalletPasswordCreated } from '../utils/storage'; // eslint-disable-next-line import/prefer-default-export export const redirectToFirstAppPage = () => { return (dispatch: Dispatch, getState: GetState) => { - const accountKeys = readAccountKeysFromLocalStorage(); - if (accountKeys) { - dispatch({ - type: SET_KEYS, - ...accountKeys - }); - return dispatch(updateAccountTransactionsAndState()).catch(error => { - if (error.message === ACCOUNT_STATE_ERROR) { - console.error('There was an error retrieving account status'); - } - - return dispatch(push(routes.WALLET)); - }); - } + if (isUnlockWalletPasswordCreated()) + return dispatch(push(routes.UNLOCK_WALLET)); const { account: { address } }: { account: { address: Address } } = getState(); diff --git a/examples/wallet/app/components/CreateUnlockWalletPassword.js b/examples/wallet/app/components/CreateUnlockWalletPassword.js new file mode 100644 index 0000000..f6bb2db --- /dev/null +++ b/examples/wallet/app/components/CreateUnlockWalletPassword.js @@ -0,0 +1,97 @@ +// @flow +import React, { useState } from 'react'; +import Form from 'react-bootstrap/Form'; +import PropTypes from 'prop-types'; + +CreateUnlockWalletPassword.propTypes = { + setValidCreateUnlockWalletPassword: PropTypes.func +}; +export default function CreateUnlockWalletPassword({ + setValidCreateUnlockWalletPassword +}) { + const checkValidUnlockWalletPassword = function checkValidUnlockWalletPassword( + pass + ) { + setIsValidPassword(true); + if (!pass) { + setIsValidPassword(false); + return false; + } + if (pass.length < 8) { + setIsValidPassword(false); + return false; + } + }; + + const checkValidUnlockWalletConfirmation = function checkValidUnlockWalletConfirmation( + unlockWalletPassword, + confirmation + ) { + setArePasswordAndConfirmationEqual(true); + let isValidUnlockPassword = true; + if (!confirmation || confirmation.length < 8) { + isValidUnlockPassword = false; + } + + if (unlockWalletPassword !== confirmation) { + setArePasswordAndConfirmationEqual(false); + isValidUnlockPassword = false; + } + + setValidCreateUnlockWalletPassword( + unlockWalletPassword, + isValidUnlockPassword + ); + return isValidUnlockPassword; + }; + + const [ + arePasswordAndConfirmationEqual, + setArePasswordAndConfirmationEqual + ] = useState(true); + + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isValidPassword, setIsValidPassword] = useState(true); + + return ( + + Unlock wallet: + + setPassword(event.target.value)} + onBlur={() => checkValidUnlockWalletPassword(password)} + /> + + The password must have at least 8 chars. + + setConfirmPassword(event.target.value)} + onBlur={() => + checkValidUnlockWalletConfirmation(password, confirmPassword) + } + className="mt-3" + /> + + This key allows you to unlock your wallet every time you start it and + to keep your account data in a more secure way. + + + password and confirmation must be the same. + + + + ); +} diff --git a/examples/wallet/app/components/RestoreWalletFromMnemonic.js b/examples/wallet/app/components/RestoreWalletFromMnemonic.js new file mode 100644 index 0000000..f715c8d --- /dev/null +++ b/examples/wallet/app/components/RestoreWalletFromMnemonic.js @@ -0,0 +1,106 @@ +// @flow +import React, { useState } from 'react'; +import Form from 'react-bootstrap/Form'; +import Button from 'react-bootstrap/Button'; +import Container from 'react-bootstrap/Container'; +import Row from 'react-bootstrap/Row'; +import typeof { setAccountFromMnemonic as SetAccountFromMnemonic } from '../actions/account'; +import { isValidMnemonic } from '../utils/mnemonic'; +import CreateUnlockWalletPassword from '../containers/CreateUnlockWalletPassword'; + +type Props = { + setAccountFromMnemonic: SetAccountFromMnemonic +}; + +export default ({ setAccountFromMnemonic }: Props) => { + const checkIsValidMnemonicPhrase = function checkIsValidMnemonicPhrase() { + setIsMnemonicValid(isValidMnemonic(newMnemonicPhrase)); + }; + + const handleSubmitMnemonic = function handleSubmitMnemonic(event) { + event.preventDefault(); + if (isValidMnemonic(newMnemonicPhrase)) { + // TODO: ADD IF SENTENCE TO CHECK IF PASSWORD AND CONFIRM IS OK + if (isValidUnlockPassword) { + return setAccountFromMnemonic( + newMnemonicPhrase, + newMnemonicPassword, + unlockWalletPassword + ); + } + } + setIsMnemonicValid(false); + }; + + const setValidCreateUnlockWalletPassword = function setValidCreateUnlockWalletPassword( + unlockPwd: string, + isValid: boolean + ): void { + setUnlockWalletPassword(unlockPwd); + setIsValidUnlockPassword(isValid); + }; + + const [unlockWalletPassword, setUnlockWalletPassword] = useState(''); + const [isValidUnlockPassword, setIsValidUnlockPassword] = useState(false); + + const [isMnemonicValid, setIsMnemonicValid] = useState(true); + + const [newMnemonicPhrase, setNewMnemonicPhrase] = useState(''); + + const [newMnemonicPassword, setNewMnemonicPassword] = useState(''); + + return ( + +
+ + Wallet Seed: + setNewMnemonicPhrase(event.target.value)} + onBlur={() => checkIsValidMnemonicPhrase()} + /> + + The phrase can have 12, 15, 18, 21 or 24 valid words. + + + Example: +
+ decade panther require cruise robust mail gadget advice tonight post + inner snack +
+ setNewMnemonicPassword(event.target.value)} + className="mt-3" + /> + + This secret password is part of the BIP39 standard to generate safer + wallet seeds. + + +
+ + + + +
+
+ ); +}; diff --git a/examples/wallet/app/components/RestoreWalletFromPrivateKey.js b/examples/wallet/app/components/RestoreWalletFromPrivateKey.js new file mode 100644 index 0000000..2f746f6 --- /dev/null +++ b/examples/wallet/app/components/RestoreWalletFromPrivateKey.js @@ -0,0 +1,87 @@ +// @flow +import React, { useState } from 'react'; +import Form from 'react-bootstrap/Form'; +import Button from 'react-bootstrap/Button'; +import Container from 'react-bootstrap/Container'; +import Row from 'react-bootstrap/Row'; +import typeof { setAccount as SetAccount } from '../actions/account'; +import CreateUnlockWalletPassword from '../containers/CreateUnlockWalletPassword'; +import { getAccountFromPrivateKey } from '../utils/wasmWrapper'; + +type Props = { + setAccount: SetAccount +}; + +// FIXME: this has no error handling, neither while parsing the address +// nor when fetching the balance. +export default ({ setAccount }: Props) => { + const handleSubmit = function handleSubmit(event) { + event.preventDefault(); + if (isValidUnlockPassword) { + return setAccount(newPrivateKey, unlockWalletPassword).catch(error => { + console.error(error); + setPrivateKeyErrorMessage('Invalid private key'); + }); + } + }; + + const checkValidPrivateKey = function checkValidPrivateKey() { + getAccountFromPrivateKey(newPrivateKey) + .then(() => setPrivateKeyErrorMessage('')) + .catch(error => { + console.error(error); + setPrivateKeyErrorMessage('Invalid private key'); + }); + }; + + const setValidCreateUnlockWalletPassword = function setValidCreateUnlockWalletPassword( + unlockPwd: string, + isValid: boolean + ): void { + setUnlockWalletPassword(unlockPwd); + setIsValidUnlockPassword(isValid); + }; + + const [unlockWalletPassword, setUnlockWalletPassword] = useState(''); + const [isValidUnlockPassword, setIsValidUnlockPassword] = useState(false); + + const [newPrivateKey, setNewPrivateKey] = useState(''); + const [privateKeyErrorMessage, setPrivateKeyErrorMessage] = useState(''); + + return ( + +
+ + Private key: + setNewPrivateKey(event.target.value)} + onBlur={() => checkValidPrivateKey()} + /> + + {privateKeyErrorMessage} + + + It's a string like: +
+ ed25519e_sk15psr45hyqnpwcl8xd4lv0m32prenhh8kcltgte2305h5jgynndxect9274j0am0qmmd0snjuadnm6xkgssnkn2njvkg8et8qg0vevsgnwvmpl +
+ +
+ + + +
+
+ ); +}; diff --git a/examples/wallet/app/constants/routes.json b/examples/wallet/app/constants/routes.json index 40494a1..958937b 100644 --- a/examples/wallet/app/constants/routes.json +++ b/examples/wallet/app/constants/routes.json @@ -5,5 +5,6 @@ "INPUT_KEYS": "/input_keys", "SEND": "/send", "STAKING": "/staking", - "SETTINGS": "/settings" + "SETTINGS": "/settings", + "UNLOCK_WALLET": "/unlock" } diff --git a/examples/wallet/app/containers/CreateUnlockWalletPassword.js b/examples/wallet/app/containers/CreateUnlockWalletPassword.js new file mode 100644 index 0000000..010a6ce --- /dev/null +++ b/examples/wallet/app/containers/CreateUnlockWalletPassword.js @@ -0,0 +1,5 @@ +// @flow +import { connect } from 'react-redux'; +import CreateUnlockWalletPassword from '../components/CreateUnlockWalletPassword'; + +export default connect(undefined, undefined)(CreateUnlockWalletPassword); diff --git a/examples/wallet/app/containers/InputKeys.js b/examples/wallet/app/containers/InputKeys.js deleted file mode 100644 index fd065c0..0000000 --- a/examples/wallet/app/containers/InputKeys.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import InputKeys from '../pages/InputKeys'; -import { setAccount, setAccountFromMnemonic } from '../actions/account'; - -function mapDispatchToProps(dispatch) { - return bindActionCreators({ setAccount, setAccountFromMnemonic }, dispatch); -} - -export default connect(undefined, mapDispatchToProps)(InputKeys); diff --git a/examples/wallet/app/containers/RestoreWalletFromMnemonic.js b/examples/wallet/app/containers/RestoreWalletFromMnemonic.js new file mode 100644 index 0000000..f2b5558 --- /dev/null +++ b/examples/wallet/app/containers/RestoreWalletFromMnemonic.js @@ -0,0 +1,20 @@ +// @flow +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import type { AppState } from '../reducers/types'; +import RestoreWalletFromMnemonic from '../components/RestoreWalletFromMnemonic'; +import { setAccountFromMnemonic } from '../actions/account'; + +function mapStateToProps(state: AppState) { + const { unlockWalletPassword, isValidUnlockPassword } = state.account; + return { unlockWalletPassword, isValidUnlockPassword }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ setAccountFromMnemonic }, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(RestoreWalletFromMnemonic); diff --git a/examples/wallet/app/containers/RestoreWalletFromPrivateKey.js b/examples/wallet/app/containers/RestoreWalletFromPrivateKey.js new file mode 100644 index 0000000..e41480d --- /dev/null +++ b/examples/wallet/app/containers/RestoreWalletFromPrivateKey.js @@ -0,0 +1,20 @@ +// @flow +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import type { AppState } from '../reducers/types'; +import RestoreWalletFromPrivateKey from '../components/RestoreWalletFromPrivateKey'; +import { setAccount } from '../actions/account'; + +function mapStateToProps(state: AppState) { + const { unlockWalletPassword, isValidUnlockPassword } = state.account; + return { unlockWalletPassword, isValidUnlockPassword }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ setAccount }, dispatch); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(RestoreWalletFromPrivateKey); diff --git a/examples/wallet/app/containers/UnlockWallet.js b/examples/wallet/app/containers/UnlockWallet.js new file mode 100644 index 0000000..c50cdb3 --- /dev/null +++ b/examples/wallet/app/containers/UnlockWallet.js @@ -0,0 +1,14 @@ +// @flow +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import UnlockWallet from '../pages/UnlockWallet'; +import { setKeysWithUnlockWalletPassword } from '../actions/account'; + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ setKeysWithUnlockWalletPassword }, dispatch); +} + +export default connect( + undefined, + mapDispatchToProps +)(UnlockWallet); diff --git a/examples/wallet/app/pages/InputKeys.js b/examples/wallet/app/pages/InputKeys.js index d469613..2a98894 100644 --- a/examples/wallet/app/pages/InputKeys.js +++ b/examples/wallet/app/pages/InputKeys.js @@ -1,132 +1,18 @@ // @flow -import React, { useState } from 'react'; -import Form from 'react-bootstrap/Form'; -import Button from 'react-bootstrap/Button'; -import Container from 'react-bootstrap/Container'; +import React from 'react'; import Tabs from 'react-bootstrap/Tabs'; import Tab from 'react-bootstrap/Tab'; -import Row from 'react-bootstrap/Row'; -import typeof { - setAccountFromMnemonic as SetAccountFromMnemonic, - setAccount as SetAccount -} from '../actions/account'; -import { isValidMnemonic } from '../utils/mnemonic'; - -type Props = { - setAccountFromMnemonic: SetAccountFromMnemonic, - setAccount: SetAccount -}; - -// FIXME: this has no error handling, neither while parsing the address -// nor when fetching the balance. -export default ({ setAccount, setAccountFromMnemonic }: Props) => { - const handleSubmitKeyString = function handleSubmitKeyString(event) { - event.preventDefault(); - return setAccount(newPrivateKey); - }; - const checkIsValidMnemonicPhrase = function checkIsValidMnemonicPhrase() { - setIsMnemonicValid(isValidMnemonic(newMnemonicPhrase)); - }; - - const handleSubmitMnemonic = function handleSubmitMnemonic(event) { - event.preventDefault(); - if (isValidMnemonic(newMnemonicPhrase)) { - setAccountFromMnemonic(newMnemonicPhrase, newMnemonicPassword); - } - setIsMnemonicValid(false); - }; - - const [isMnemonicValid, setIsMnemonicValid] = useState(true); - - const [newPrivateKey, setNewPrivateKey] = useState(''); - - const [newMnemonicPhrase, setNewMnemonicPhrase] = useState(''); - - const [newMnemonicPassword, setNewMnemonicPassword] = useState(''); +import RestoreWalletFromPrivateKey from '../containers/RestoreWalletFromPrivateKey'; +import RestoreWalletFromMnemonic from '../containers/RestoreWalletFromMnemonic'; +export default () => { return ( - -
- - Private key: - setNewPrivateKey(event.target.value)} - /> - - It's a string like: -
- - ed25519e_sk15psr45hyqnpwcl8xd4lv0m32prenhh8kcltgte2305h5jgynndxect9274j0am0qmmd0snjuadnm6xkgssnkn2njvkg8et8qg0vevsgnwvmpl - -
-
- - {/* TODO: bind this button */} - - - -
-
+
- -
- - Wallet Seed: - setNewMnemonicPhrase(event.target.value)} - onBlur={() => checkIsValidMnemonicPhrase()} - /> - - - Example: -
- - decade panther require cruise robust mail gadget advice - tonight post inner snack - -
- setNewMnemonicPassword(event.target.value)} - className="mt-3" - /> -
- - {/* TODO: bind this button */} - - - -
-
+
); diff --git a/examples/wallet/app/pages/UnlockWallet.js b/examples/wallet/app/pages/UnlockWallet.js new file mode 100644 index 0000000..e4c3795 --- /dev/null +++ b/examples/wallet/app/pages/UnlockWallet.js @@ -0,0 +1,57 @@ +// @flow +import React, { useState } from 'react'; +import Form from 'react-bootstrap/Form'; +import Button from 'react-bootstrap/Button'; +import Container from 'react-bootstrap/Container'; +import Row from 'react-bootstrap/Row'; +import styles from './UnlockWallet.scss'; +import typeof { setKeysWithUnlockWalletPassword as SetKeysWithSpendingPassword } from '../actions/account'; + +type Props = { + setKeysWithUnlockWalletPassword: SetKeysWithSpendingPassword +}; + +export default ({ setKeysWithUnlockWalletPassword }: Props) => { + const handleSubmit = function handleSubmit(event) { + event.preventDefault(); + try { + return setKeysWithUnlockWalletPassword(unlockWalletPassword); + } catch (error) { + setIsWrongUnlockWalletPassword(true); + } + }; + + const [unlockWalletPassword, setUnlockWalletPassword] = useState(''); + const [isWrongSpendingPassword, setIsWrongUnlockWalletPassword] = useState( + false + ); + + return ( + +
+ + + Welcome! Please insert you password to unlock wallet + + setUnlockWalletPassword(event.target.value)} + /> + + Incorrect password + + + + + +
+
+ ); +}; diff --git a/examples/wallet/app/pages/UnlockWallet.scss b/examples/wallet/app/pages/UnlockWallet.scss new file mode 100644 index 0000000..141cf97 --- /dev/null +++ b/examples/wallet/app/pages/UnlockWallet.scss @@ -0,0 +1,4 @@ +.container { + vertical-align: middle; + padding-top: 20%; +} diff --git a/examples/wallet/app/utils/decrypt.js b/examples/wallet/app/utils/decrypt.js new file mode 100644 index 0000000..7aaac9d --- /dev/null +++ b/examples/wallet/app/utils/decrypt.js @@ -0,0 +1,24 @@ +/* eslint-disable new-cap */ +import aesjs from 'aes-js'; +import blakejs from 'blakejs'; + +const blake2b = data => blakejs.blake2b(data, null, 32); + +export function aesEncrypt(unlockWalletPassword: string, text: string): string { + const aesKey = blake2b(unlockWalletPassword); + const aesCtr = new aesjs.ModeOfOperation.ctr(aesKey, new aesjs.Counter(5)); + const encryptedBytes = aesCtr.encrypt(aesjs.utils.utf8.toBytes(text)); + const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes); + return encryptedHex; +} + +export function aesDecrypt( + unlockWalletPassword: string, + encryptedHex: string +): string { + const aesKey = blake2b(unlockWalletPassword); + const aesCtr = new aesjs.ModeOfOperation.ctr(aesKey, new aesjs.Counter(5)); + const decryptedBytes = aesCtr.decrypt(aesjs.utils.hex.toBytes(encryptedHex)); + const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes); + return decryptedText; +} diff --git a/examples/wallet/app/utils/storage.js b/examples/wallet/app/utils/storage.js index b716a38..e1bd0fb 100644 --- a/examples/wallet/app/utils/storage.js +++ b/examples/wallet/app/utils/storage.js @@ -1,18 +1,43 @@ // @flow import type { AccountKeys } from '../reducers/types'; -import type { Address, Identifier, PrivateKey } from '../models'; +import { aesEncrypt, aesDecrypt } from './decrypt'; -export function saveAccountInfoInLocalStorage(keys: AccountKeys): void { - Object.keys(keys).forEach(key => localStorage.setItem(key, keys[key])); +const WALLET_ENCRYPTED_KEYS = 'wallet.encrypted.account.info'; + +export function isUnlockWalletPasswordCreated(): boolean { + const encryptedAccountInfo = localStorage.getItem(WALLET_ENCRYPTED_KEYS); + if (encryptedAccountInfo) return true; + return false; +} + +export function saveEncryptedAccountInfo( + unlockWalletPassword: string, + keys: AccountKeys +): void { + const plainTextAccountInfo = JSON.stringify(keys); + if (!unlockWalletPassword) { + throw new Error('Invalid unlock password'); + } + const encryptedTextAccountInfo = aesEncrypt( + unlockWalletPassword, + plainTextAccountInfo + ); + localStorage.setItem(WALLET_ENCRYPTED_KEYS, encryptedTextAccountInfo); } -export function readAccountKeysFromLocalStorage(): ?AccountKeys { - if (localStorage.getItem('privateKey')) { - return { - address: (localStorage.getItem('address'): Address), - identifier: (localStorage.getItem('identifier'): Identifier), - privateKey: (localStorage.getItem('privateKey'): PrivateKey) - }; +// eslint-disable-next-line flowtype/space-after-type-colon +export function readEncryptedAccountInfo( + unlockWalletPassword: string +): ?AccountKeys { + const encryptedHex = localStorage.getItem(WALLET_ENCRYPTED_KEYS); + try { + if (encryptedHex && encryptedHex.length > 0) { + const plainText = aesDecrypt(unlockWalletPassword, encryptedHex); + const accountKeys = JSON.parse(plainText); + return accountKeys; + } + } catch (e) { + console.error('There was an error unlocking wallet', e.toString()); } return undefined; } diff --git a/examples/wallet/package.json b/examples/wallet/package.json index 18307de..d4ebd3e 100644 --- a/examples/wallet/package.json +++ b/examples/wallet/package.json @@ -262,8 +262,10 @@ "@forevolve/bootstrap-dark": "1.0.0-alpha.863", "@fortawesome/fontawesome-free": "5.12.0", "@hot-loader/react-dom": "16.11.0", + "aes-js": "3.1.2", "axios": "0.19.0", "bip39": "3.0.2", + "blakejs": "1.1.0", "bootstrap": "4.4.1", "classnames": "2.2.6", "clickable-box": "1.1.0", diff --git a/examples/wallet/yarn.lock b/examples/wallet/yarn.lock index 7a1a18d..b125225 100644 --- a/examples/wallet/yarn.lock +++ b/examples/wallet/yarn.lock @@ -1812,6 +1812,11 @@ address@^1.0.1: resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== +aes-js@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.1.2.tgz#db9aabde85d5caabbfc0d4f2a4446960f627146a" + integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== + aggregate-error@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" @@ -3253,6 +3258,11 @@ bl@^1.0.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +blakejs@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.0.tgz#69df92ef953aa88ca51a32df6ab1c54a155fc7a5" + integrity sha1-ad+S75U6qIylGjLfarHFShVfx6U= + block-stream@*: version "0.0.9" resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"