Merge pull request #1785 from lbryio/wallet-encryption

Add basic wallet encryption flows
This commit is contained in:
Shawn Khameneh 2018-07-25 14:30:38 -05:00 committed by GitHub
commit 71bc347224
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 483 additions and 34 deletions

View file

@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
## [Unreleased]
### Added
* Wallet Encryption/Decryption user flows ([#1785](https://github.com/lbryio/lbry-desktop/pull/1785))
### Changed

View file

@ -48,7 +48,7 @@
"formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#e0909b08647a790d155f3189b9f9bf0b3e55bd17",
"lbry-redux": "lbryio/lbry-redux#b4fffe863df316bc73183567ab978221ee623b8c",
"localforage": "^1.7.1",
"mime": "^2.3.1",
"mixpanel-browser": "^2.17.1",

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import { selectDaemonVersionMatched } from 'redux/selectors/app';
import { selectNotification } from 'lbry-redux';
import { doCheckDaemonVersion } from 'redux/actions/app';
import { doCheckDaemonVersion, doNotifyUnlockWallet } from 'redux/actions/app';
import SplashScreen from './view';
const select = state => ({
@ -11,6 +11,10 @@ const select = state => ({
const perform = dispatch => ({
checkDaemonVersion: () => dispatch(doCheckDaemonVersion()),
notifyUnlockWallet: () => dispatch(doNotifyUnlockWallet()),
});
export default connect(select, perform)(SplashScreen);
export default connect(
select,
perform
)(SplashScreen);

View file

@ -1,12 +1,14 @@
import * as React from 'react';
import { Lbry, MODALS } from 'lbry-redux';
import LoadScreen from './internal/load-screen';
import ModalWalletUnlock from 'modal/modalWalletUnlock';
import ModalIncompatibleDaemon from 'modal/modalIncompatibleDaemon';
import ModalUpgrade from 'modal/modalUpgrade';
import ModalDownloading from 'modal/modalDownloading';
type Props = {
checkDaemonVersion: () => Promise<any>,
notifyUnlockWallet: () => Promise<any>,
notification: ?{
id: string,
},
@ -17,6 +19,7 @@ type State = {
message: string,
isRunning: boolean,
isLagging: boolean,
launchedModal: boolean,
};
export class SplashScreen extends React.PureComponent<Props, State> {
@ -28,6 +31,7 @@ export class SplashScreen extends React.PureComponent<Props, State> {
message: __('Connecting'),
isRunning: false,
isLagging: false,
launchedModal: false,
};
}
@ -38,8 +42,11 @@ export class SplashScreen extends React.PureComponent<Props, State> {
}
_updateStatusCallback(status) {
const { notifyUnlockWallet } = this.props;
const { launchedModal } = this.state;
const startupStatus = status.startup_status;
if (startupStatus.code == 'started') {
if (startupStatus.code === 'started') {
// Wait until we are able to resolve a name before declaring
// that we are done.
// TODO: This is a hack, and the logic should live in the daemon
@ -61,6 +68,7 @@ export class SplashScreen extends React.PureComponent<Props, State> {
});
return;
}
if (status.blockchain_status && status.blockchain_status.blocks_behind > 0) {
const format =
status.blockchain_status.blocks_behind == 1 ? '%s block behind' : '%s blocks behind';
@ -69,6 +77,17 @@ export class SplashScreen extends React.PureComponent<Props, State> {
details: __(format, status.blockchain_status.blocks_behind),
isLagging: startupStatus.is_lagging,
});
} else if (startupStatus.code === 'waiting_for_wallet_unlock') {
this.setState({
message: __('Unlock Wallet'),
details: __('Please unlock your wallet to proceed.'),
isLagging: false,
isRunning: true,
});
if (launchedModal === false) {
this.setState({ launchedModal: true }, () => notifyUnlockWallet());
}
} else {
this.setState({
message: __('Network Loading'),
@ -114,6 +133,7 @@ export class SplashScreen extends React.PureComponent<Props, State> {
in the modals won't work. */}
{isRunning && (
<React.Fragment>
{notificationId === MODALS.WALLET_UNLOCK && <ModalWalletUnlock />}
{notificationId === MODALS.INCOMPATIBLE_DAEMON && <ModalIncompatibleDaemon />}
{notificationId === MODALS.UPGRADE && <ModalUpgrade />}
{notificationId === MODALS.DOWNLOADING && <ModalDownloading />}

View file

@ -23,6 +23,9 @@ import ModalSendTip from '../modalSendTip';
import ModalPublish from '../modalPublish';
import ModalOpenExternalLink from '../modalOpenExternalLink';
import ModalConfirmThumbnailUpload from 'modal/modalConfirmThumbnailUpload';
import ModalWalletEncrypt from 'modal/modalWalletEncrypt';
import ModalWalletDecrypt from 'modal/modalWalletDecrypt';
import ModalWalletUnlock from 'modal/modalWalletUnlock';
type Props = {
modal: string,
@ -165,6 +168,12 @@ class ModalRouter extends React.PureComponent<Props> {
return <ModalConfirmTransaction {...notificationProps} />;
case MODALS.CONFIRM_THUMBNAIL_UPLOAD:
return <ModalConfirmThumbnailUpload {...notificationProps} />;
case MODALS.WALLET_ENCRYPT:
return <ModalWalletEncrypt {...notificationProps} />;
case MODALS.WALLET_DECRYPT:
return <ModalWalletDecrypt {...notificationProps} />;
case MODALS.WALLET_UNLOCK:
return <ModalWalletUnlock {...notificationProps} />;
default:
return null;
}

View file

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import {
doHideNotification,
doWalletStatus,
doWalletDecrypt,
selectWalletDecryptSucceeded,
} from 'lbry-redux';
import ModalWalletDecrypt from './view';
const select = state => ({
walletDecryptSucceded: selectWalletDecryptSucceeded(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doHideNotification()),
decryptWallet: password => dispatch(doWalletDecrypt(password)),
updateWalletStatus: () => dispatch(doWalletStatus()),
});
export default connect(
select,
perform
)(ModalWalletDecrypt);

View file

@ -0,0 +1,63 @@
// @flow
import React from 'react';
import { Form, FormRow, FormField } from 'component/common/form';
import { Modal } from 'modal/modal';
import Button from 'component/button';
type Props = {
closeModal: () => void,
unlockWallet: string => void,
walletDecryptSucceded: boolean,
updateWalletStatus: boolean,
};
class ModalWalletDecrypt extends React.PureComponent<Props> {
state = {
submitted: false, // Prior actions could be marked complete
};
submitDecryptForm() {
this.setState({ submitted: true });
this.props.decryptWallet();
}
componentDidUpdate() {
const { props, state } = this;
if (state.submitted && props.walletDecryptSucceded === true) {
props.closeModal();
props.updateWalletStatus();
}
}
render() {
const { closeModal, walletDecryptSucceded } = this.props;
return (
<Modal
isOpen
contentLabel={__('Decrypt Wallet')}
type="confirm"
confirmButtonLabel={__('Decrypt Wallet')}
abortButtonLabel={__('Cancel')}
onConfirmed={() => this.submitDecryptForm()}
onAborted={closeModal}
>
<Form onSubmit={() => this.submitDecryptForm()}>
{__(
'Your wallet has been encrypted with a local password, performing this action will remove this password.'
)}
<div className="card__actions">
<Button
button="link"
label={__('Learn more')}
href="https://lbry.io/faq/wallet-encryption"
/>
</div>
</Form>
</Modal>
);
}
}
export default ModalWalletDecrypt;

View file

@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import {
doHideNotification,
doWalletStatus,
doWalletEncrypt,
selectWalletEncryptPending,
selectWalletEncryptSucceeded,
selectWalletEncryptResult,
} from 'lbry-redux';
import ModalWalletEncrypt from './view';
const select = state => ({
walletEncryptSucceded: selectWalletEncryptSucceeded(state),
walletEncryptResult: selectWalletEncryptResult(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doHideNotification()),
encryptWallet: password => dispatch(doWalletEncrypt(password)),
updateWalletStatus: () => dispatch(doWalletStatus()),
});
export default connect(
select,
perform
)(ModalWalletEncrypt);

View file

@ -0,0 +1,144 @@
// @flow
import React from 'react';
import { Form, FormRow, FormField } from 'component/common/form';
import { Modal } from 'modal/modal';
import Button from 'component/button';
type Props = {
closeModal: () => void,
unlockWallet: string => void,
walletEncryptSucceded: boolean,
walletEncryptResult: boolean,
updateWalletStatus: boolean,
};
class ModalWalletEncrypt extends React.PureComponent<Props> {
state = {
newPassword: null,
newPasswordConfirm: null,
passwordMismatch: false,
understandConfirmed: false,
understandError: false,
submitted: false, // Prior actions could be marked complete
failMessage: false,
};
onChangeNewPassword(event) {
this.setState({ newPassword: event.target.value });
}
onChangeNewPasswordConfirm(event) {
this.setState({ newPasswordConfirm: event.target.value });
}
onChangeUnderstandConfirm(event) {
this.setState({
understandConfirmed: /^.?i understand.?$/i.test(event.target.value),
});
}
submitEncryptForm() {
const { state } = this;
let invalidEntries = false;
if (state.newPassword !== state.newPasswordConfirm) {
this.setState({ passwordMismatch: true });
invalidEntries = true;
}
if (state.understandConfirmed === false) {
this.setState({ understandError: true });
invalidEntries = true;
}
if (invalidEntries === true) {
return;
}
this.setState({ submitted: true });
this.props.encryptWallet(state.newPassword);
}
componentDidUpdate() {
const { props, state } = this;
if (state.submitted) {
if (props.walletEncryptSucceded === true) {
props.closeModal();
props.updateWalletStatus();
} else if (props.walletEncryptSucceded === false) {
// See https://github.com/lbryio/lbry/issues/1307
this.setState({ failMessage: 'Unable to encrypt wallet.' });
}
}
}
render() {
const { closeModal } = this.props;
const { passwordMismatch, understandError, failMessage } = this.state;
return (
<Modal
isOpen
contentLabel={__('Encrypt Wallet')}
type="confirm"
confirmButtonLabel={__('Encrypt Wallet')}
abortButtonLabel={__('Cancel')}
onConfirmed={() => this.submitEncryptForm()}
onAborted={closeModal}
>
<Form onSubmit={() => this.submitEncryptForm()}>
{__(
'Encrypting your wallet will require a password to access your local wallet data when LBRY starts. Please enter a new password for your wallet.'
)}
<FormRow padded>
<FormField
stretch
error={passwordMismatch === true ? 'Passwords do not match' : false}
label={__('New Password')}
type="password"
name="wallet-new-password"
onChange={event => this.onChangeNewPassword(event)}
/>
</FormRow>
<FormRow padded>
<FormField
stretch
error={passwordMismatch === true ? 'Passwords do not match' : false}
label={__('Confirm Password')}
type="password"
name="wallet-new-password-confirm"
onChange={event => this.onChangeNewPasswordConfirm(event)}
/>
</FormRow>
<br />
{__(
'If your password is lost, it cannot be recovered. You will not be able to access your wallet without a password.'
)}
<FormRow padded>
<FormField
stretch
error={understandError === true ? 'You must enter "I understand"' : false}
label={__('Enter "I understand"')}
type="text"
name="wallet-understand"
onChange={event => this.onChangeUnderstandConfirm(event)}
/>
</FormRow>
<div className="card__actions">
<Button
button="link"
label={__('Learn more')}
href="https://lbry.io/faq/wallet-encryption"
/>
</div>
{failMessage && <div className="error-text">{__(failMessage)}</div>}
</Form>
</Modal>
);
}
}
export default ModalWalletEncrypt;

View file

@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import {
doHideNotification,
doWalletUnlock,
selectWalletUnlockPending,
selectWalletUnlockSucceeded,
} from 'lbry-redux';
import { doQuit } from 'redux/actions/app';
import ModalWalletUnlock from './view';
const select = state => ({
walletUnlockSucceded: selectWalletUnlockSucceeded(state),
});
const perform = dispatch => ({
closeModal: () => dispatch(doHideNotification()),
quit: () => dispatch(doQuit()),
unlockWallet: password => dispatch(doWalletUnlock(password)),
});
export default connect(
select,
perform
)(ModalWalletUnlock);

View file

@ -0,0 +1,73 @@
// @flow
import React from 'react';
import { Form, FormRow, FormField } from 'component/common/form';
import { Modal } from 'modal/modal';
import Button from 'component/button';
type Props = {
closeModal: () => void,
quit: () => void,
unlockWallet: string => void,
walletUnlockSucceded: boolean,
};
class ModalWalletUnlock extends React.PureComponent<Props> {
state = {
password: null,
};
componentDidUpdate() {
const { props } = this;
if (props.walletUnlockSucceded === true) {
props.closeModal();
}
}
onChangePassword(event) {
this.setState({ password: event.target.value });
}
render() {
const { quit, unlockWallet, walletUnlockSucceded, closeModal } = this.props;
const { password } = this.state;
return (
<Modal
isOpen
contentLabel={__('Unlock Wallet')}
type="confirm"
confirmButtonLabel={__('Unlock')}
abortButtonLabel={__('Exit')}
onConfirmed={() => unlockWallet(password)}
onAborted={quit}
>
<Form onSubmit={() => unlockWallet(password)}>
{__(
'Your wallet has been encrypted with a local password. Please enter your wallet password to proceed.'
)}
<FormRow padded>
<FormField
stretch
error={walletUnlockSucceded === false ? 'Incorrect Password' : false}
label={__('Wallet Password')}
type="password"
name="wallet-password"
onChange={event => this.onChangePassword(event)}
/>
</FormRow>
<div className="card__actions">
<Button
button="link"
label={__('Learn more')}
href="https://lbry.io/faq/wallet-encryption"
/>
</div>
</Form>
</Modal>
);
}
}
export default ModalWalletUnlock;

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { doClearCache } from 'redux/actions/app';
import { doClearCache, doNotifyEncryptWallet, doNotifyDecryptWallet } from 'redux/actions/app';
import {
doSetDaemonSetting,
doSetClientSetting,
@ -13,6 +13,7 @@ import {
selectLanguages,
} from 'redux/selectors/settings';
import { selectCurrentLanguage } from 'redux/selectors/app';
import { doWalletStatus, selectWalletIsEncrypted } from 'lbry-redux';
import SettingsPage from './view';
const select = state => ({
@ -26,6 +27,7 @@ const select = state => ({
languages: selectLanguages(state),
automaticDarkModeEnabled: makeSelectClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED)(state),
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
walletEncrypted: selectWalletIsEncrypted(state),
});
const perform = dispatch => ({
@ -34,6 +36,9 @@ const perform = dispatch => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
getThemes: () => dispatch(doGetThemes()),
changeLanguage: newLanguage => dispatch(doChangeLanguage(newLanguage)),
encryptWallet: () => dispatch(doNotifyEncryptWallet()),
decryptWallet: () => dispatch(doNotifyDecryptWallet()),
updateWalletStatus: () => dispatch(doWalletStatus()),
});
export default connect(

View file

@ -31,6 +31,9 @@ type Props = {
themes: Array<string>,
automaticDarkModeEnabled: boolean,
autoplay: boolean,
encryptWallet: () => void,
decryptWallet: () => void,
walletEncrypted: boolean,
};
type State = {
@ -58,7 +61,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
}
componentDidMount() {
this.props.getThemes();
const { props } = this;
props.getThemes();
props.updateWalletStatus();
}
onRunOnStartChange(event: SyntheticInputEvent<*>) {
@ -111,6 +117,11 @@ class SettingsPage extends React.PureComponent<Props, State> {
this.props.setClientSetting(settings.SHOW_NSFW, event.target.checked);
}
onChangeEncryptWallet() {
const { props } = this;
props.walletEncrypted ? props.decryptWallet() : props.encryptWallet();
}
setDaemonSetting(name: string, value: boolean | string | Price) {
this.props.setDaemonSetting(name, value);
}
@ -138,6 +149,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
themes,
automaticDarkModeEnabled,
autoplay,
walletEncrypted,
} = this.props;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
@ -272,32 +284,43 @@ class SettingsPage extends React.PureComponent<Props, State> {
)}
/>
</section>
{
<section className="card card--section">
<div className="card__title">{__('Theme')}</div>
<FormField
name="theme_select"
type="select"
onChange={this.onThemeChange}
value={currentTheme}
disabled={automaticDarkModeEnabled}
>
{themes.map(theme => (
<option key={theme} value={theme}>
{theme}
</option>
))}
</FormField>
<FormField
type="checkbox"
name="automatic_dark_mode"
onChange={e => this.onAutomaticDarkModeChange(e.target.checked)}
checked={automaticDarkModeEnabled}
disabled={isDarkModeEnabled}
postfix={__('Automatic dark mode (9pm to 8am)')}
/>
</section>
}
<section className="card card--section">
<div className="card__title">{__('Theme')}</div>
<FormField
name="theme_select"
type="select"
onChange={this.onThemeChange}
value={currentTheme}
disabled={automaticDarkModeEnabled}
>
{themes.map(theme => (
<option key={theme} value={theme}>
{theme}
</option>
))}
</FormField>
<FormField
type="checkbox"
name="automatic_dark_mode"
onChange={e => this.onAutomaticDarkModeChange(e.target.checked)}
checked={automaticDarkModeEnabled}
disabled={isDarkModeEnabled}
postfix={__('Automatic dark mode (9pm to 8am)')}
/>
</section>
<section className="card card--section">
<div className="card__title">{__('Wallet Security')}</div>
<FormField
type="checkbox"
name="encrypt_wallet"
onChange={e => this.onChangeEncryptWallet(e)}
checked={walletEncrypted}
postfix={__('Encrypt my wallet with a custom password.')}
helper={__(
'Secure your local wallet data with a custom password. Lost passwords cannot be recovered.'
)}
/>
</section>
<section className="card card--section">
<div className="card__title">{__('Application Cache')}</div>
<span className="card__subtitle">

View file

@ -274,6 +274,36 @@ export function doCheckDaemonVersion() {
};
}
export function doNotifyEncryptWallet() {
return dispatch => {
dispatch(
doNotify({
id: MODALS.WALLET_ENCRYPT,
})
);
};
}
export function doNotifyDecryptWallet() {
return dispatch => {
dispatch(
doNotify({
id: MODALS.WALLET_DECRYPT,
})
);
};
}
export function doNotifyUnlockWallet() {
return dispatch => {
dispatch(
doNotify({
id: MODALS.WALLET_UNLOCK,
})
);
};
}
export function doAlertError(errorList) {
return dispatch => {
dispatch(

View file

@ -370,6 +370,10 @@ p {
}
}
.error-text {
color: var(--color-error);
}
.thumbnail-preview {
height: var(--thumbnail-preview-height);
width: var(--thumbnail-preview-width);

View file

@ -5221,9 +5221,9 @@ lazy-val@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc"
lbry-redux@lbryio/lbry-redux#e0909b08647a790d155f3189b9f9bf0b3e55bd17:
lbry-redux@lbryio/lbry-redux#b4fffe863df316bc73183567ab978221ee623b8c:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/e0909b08647a790d155f3189b9f9bf0b3e55bd17"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/b4fffe863df316bc73183567ab978221ee623b8c"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"