Discourage private keys (pt. 1) (#780)

* Insecure wallet blocker warning before unlocking insecure wallet.

* Wrap in quotes to avoid prettier error.

* Make account the homepage. Add a link to generate on the wallet unlock component.

* Fix send routing weirdness.
This commit is contained in:
William O'Beirne 2018-01-24 17:23:20 -05:00 committed by Daniel Ternyak
parent 2ac3015ad8
commit 4fb342a757
10 changed files with 311 additions and 82 deletions

View File

@ -59,16 +59,15 @@ export default class Root extends Component<Props, State> {
const routes = ( const routes = (
<CaptureRouteNotFound> <CaptureRouteNotFound>
<Switch> <Switch>
<Route exact={true} path="/" component={GenerateWallet} /> <Redirect exact={true} from="/" to="/account" />
<Route path="/generate" component={GenerateWallet} />
<Route path="/account" component={SendTransaction} /> <Route path="/account" component={SendTransaction} />
<Route path="/generate" component={GenerateWallet} />
<Route path="/swap" component={Swap} /> <Route path="/swap" component={Swap} />
<Route path="/contracts" component={Contracts} /> <Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} /> <Route path="/ens" component={ENS} />
<Route path="/help" component={Help} /> <Route path="/help" component={Help} />
<Route path="/sign-and-verify-message" component={SignAndVerifyMessage} /> <Route path="/sign-and-verify-message" component={SignAndVerifyMessage} />
<Route path="/pushTx" component={BroadcastTx} /> <Route path="/pushTx" component={BroadcastTx} />
<Route path="/send-transaction" component={SendTransaction} />
<RouteNotFound /> <RouteNotFound />
</Switch> </Switch>
</CaptureRouteNotFound> </CaptureRouteNotFound>

View File

@ -10,15 +10,14 @@ export interface TabLink {
} }
const tabs: TabLink[] = [ const tabs: TabLink[] = [
{
name: 'NAV_GenerateWallet',
to: '/generate'
},
{ {
name: 'Account View & Send', name: 'Account View & Send',
to: '/account' to: '/account'
}, },
{
name: 'NAV_GenerateWallet',
to: '/generate'
},
{ {
name: 'NAV_Swap', name: 'NAV_Swap',
to: '/swap' to: '/swap'

View File

@ -45,6 +45,12 @@ $speed: 500ms;
margin: 0; margin: 0;
} }
} }
&-generate {
text-align: center;
font-weight: 300;
margin-top: $space;
}
} }
&-decrypt { &-decrypt {

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import { TransitionGroup, CSSTransition } from 'react-transition-group'; import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { import {
@ -27,7 +28,8 @@ import {
TrezorDecrypt, TrezorDecrypt,
ViewOnlyDecrypt, ViewOnlyDecrypt,
Web3Decrypt, Web3Decrypt,
WalletButton WalletButton,
InsecureWalletWarning
} from './components'; } from './components';
import { AppState } from 'reducers'; import { AppState } from 'reducers';
import DISABLES from './disables'; import DISABLES from './disables';
@ -52,6 +54,7 @@ import { getNetworkConfig } from '../../selectors/config';
interface OwnProps { interface OwnProps {
hidden?: boolean; hidden?: boolean;
disabledWallets?: WalletName[]; disabledWallets?: WalletName[];
showGenerateLink?: boolean;
} }
interface DispatchProps { interface DispatchProps {
@ -78,6 +81,7 @@ type UnlockParams = {} | PrivateKeyValue;
interface State { interface State {
selectedWalletKey: WalletName | null; selectedWalletKey: WalletName | null;
value: UnlockParams | null; value: UnlockParams | null;
hasAcknowledgedInsecure: boolean;
} }
interface BaseWalletInfo { interface BaseWalletInfo {
@ -121,6 +125,10 @@ type Wallets = SecureWallets & InsecureWallets & MiscWallet;
const WEB3_TYPE: string | false = const WEB3_TYPE: string | false =
(window as any).web3 && (window as any).web3.currentProvider.constructor.name; (window as any).web3 && (window as any).web3.currentProvider.constructor.name;
const SECURE_WALLETS = Object.values(SecureWalletName);
const INSECURE_WALLETS = Object.values(InsecureWalletName);
const MISC_WALLETS = Object.values(MiscWalletName);
export class WalletDecrypt extends Component<Props, State> { export class WalletDecrypt extends Component<Props, State> {
// https://github.com/Microsoft/TypeScript/issues/13042 // https://github.com/Microsoft/TypeScript/issues/13042
// index signature should become [key: Wallets] (from config) once typescript bug is fixed // index signature should become [key: Wallets] (from config) once typescript bug is fixed
@ -197,7 +205,8 @@ export class WalletDecrypt extends Component<Props, State> {
public state: State = { public state: State = {
selectedWalletKey: null, selectedWalletKey: null,
value: null value: null,
hasAcknowledgedInsecure: false
}; };
public componentWillReceiveProps(nextProps: Props) { public componentWillReceiveProps(nextProps: Props) {
@ -220,36 +229,59 @@ export class WalletDecrypt extends Component<Props, State> {
} }
public getDecryptionComponent() { public getDecryptionComponent() {
const { selectedWalletKey, hasAcknowledgedInsecure } = this.state;
const selectedWallet = this.getSelectedWallet(); const selectedWallet = this.getSelectedWallet();
if (!selectedWallet) { if (!selectedWalletKey || !selectedWallet) {
return null; return null;
} }
if (INSECURE_WALLETS.includes(selectedWalletKey) && !hasAcknowledgedInsecure) {
return (
<div className="WalletDecrypt-decrypt">
<InsecureWalletWarning
walletType={translate(selectedWallet.lid)}
onContinue={this.handleAcknowledgeInsecure}
onCancel={this.clearWalletChoice}
/>
</div>
);
}
return ( return (
<selectedWallet.component <div className="WalletDecrypt-decrypt">
value={this.state.value} <button className="WalletDecrypt-decrypt-back" onClick={this.clearWalletChoice}>
onChange={this.onChange} <i className="fa fa-arrow-left" /> {translate('Change Wallet')}
onUnlock={this.onUnlock} </button>
showNotification={this.props.showNotification} <h2 className="WalletDecrypt-decrypt-title">
isWalletPending={ {!selectedWallet.isReadOnly && 'Unlock your'} {translate(selectedWallet.lid)}
this.state.selectedWalletKey === InsecureWalletName.KEYSTORE_FILE </h2>
? this.props.isWalletPending <section className="WalletDecrypt-decrypt-form">
: undefined <selectedWallet.component
} value={this.state.value}
isPasswordPending={ onChange={this.onChange}
this.state.selectedWalletKey === InsecureWalletName.KEYSTORE_FILE onUnlock={this.onUnlock}
? this.props.isPasswordPending showNotification={this.props.showNotification}
: undefined isWalletPending={
} this.state.selectedWalletKey === InsecureWalletName.KEYSTORE_FILE
/> ? this.props.isWalletPending
: undefined
}
isPasswordPending={
this.state.selectedWalletKey === InsecureWalletName.KEYSTORE_FILE
? this.props.isPasswordPending
: undefined
}
/>
</section>
</div>
); );
} }
public buildWalletOptions() { public handleAcknowledgeInsecure = () => {
const SECURE_WALLETS = Object.values(SecureWalletName); this.setState({ hasAcknowledgedInsecure: true });
const INSECURE_WALLETS = Object.values(InsecureWalletName); };
const MISC_WALLETS = Object.values(MiscWalletName);
public buildWalletOptions() {
return ( return (
<div className="WalletDecrypt-wallets"> <div className="WalletDecrypt-wallets">
<h2 className="WalletDecrypt-wallets-title">{translate('decrypt_Access')}</h2> <h2 className="WalletDecrypt-wallets-title">{translate('decrypt_Access')}</h2>
@ -305,6 +337,12 @@ export class WalletDecrypt extends Component<Props, State> {
); );
})} })}
</div> </div>
{this.props.showGenerateLink && (
<div className="WalletDecrypt-wallets-generate">
Dont have a wallet? <Link to="/generate">Click here to get one</Link>.
</div>
)}
</div> </div>
); );
} }
@ -328,7 +366,8 @@ export class WalletDecrypt extends Component<Props, State> {
window.setTimeout(() => { window.setTimeout(() => {
this.setState({ this.setState({
selectedWalletKey: walletType, selectedWalletKey: walletType,
value: wallet.initialParams value: wallet.initialParams,
hasAcknowledgedInsecure: false
}); });
}, timeout); }, timeout);
}; };
@ -336,7 +375,8 @@ export class WalletDecrypt extends Component<Props, State> {
public clearWalletChoice = () => { public clearWalletChoice = () => {
this.setState({ this.setState({
selectedWalletKey: null, selectedWalletKey: null,
value: null value: null,
hasAcknowledgedInsecure: false
}); });
}; };
@ -352,21 +392,7 @@ export class WalletDecrypt extends Component<Props, State> {
<TransitionGroup> <TransitionGroup>
{decryptionComponent && selectedWallet ? ( {decryptionComponent && selectedWallet ? (
<CSSTransition classNames="DecryptContent" timeout={500} key="decrypt"> <CSSTransition classNames="DecryptContent" timeout={500} key="decrypt">
<div className="WalletDecrypt-decrypt"> {decryptionComponent}
<button
className="WalletDecrypt-decrypt-back"
onClick={this.clearWalletChoice}
>
<i className="fa fa-arrow-left" /> {translate('Change Wallet')}
</button>
<h2 className="WalletDecrypt-decrypt-title">
{!selectedWallet.isReadOnly && 'Unlock your'}{' '}
{translate(selectedWallet.lid)}
</h2>
<section className="WalletDecrypt-decrypt-form">
{decryptionComponent}
</section>
</div>
</CSSTransition> </CSSTransition>
) : ( ) : (
<CSSTransition classNames="DecryptContent" timeout={500} key="wallets"> <CSSTransition classNames="DecryptContent" timeout={500} key="wallets">

View File

@ -0,0 +1,51 @@
@import 'common/sass/variables';
.WalletWarning {
max-width: 780px;
margin: 0 auto;
text-align: left;
&-title {
color: $brand-danger;
margin-top: 0;
}
&-desc {
margin-bottom: $space;
}
&-check {
margin-bottom: $space * 2;
}
&-checkboxes {
margin-bottom: $space * 2;
}
&-buttons {
display: flex;
flex-wrap: wrap;
margin-bottom: -$space-sm;
.btn {
flex: 1;
min-width: 280px;
margin: 0 $space-sm $space-sm;
}
}
}
.AcknowledgeCheckbox {
margin-bottom: $space-sm;
&-checkbox[type="checkbox"] {
display: inline-block;
margin-right: $space-sm;
margin-top: 0;
}
&-label {
font-size: $font-size-bump-more;
font-weight: normal;
}
}

View File

@ -0,0 +1,138 @@
import React from 'react';
import './InsecureWalletWarning.scss';
interface Props {
walletType: string | React.ReactElement<string>;
onContinue(): void;
onCancel(): void;
}
interface State {
hasConfirmedSite: boolean;
hasAcknowledgedDownload: boolean;
hasAcknowledgedWallets: boolean;
}
interface Checkbox {
name: keyof State;
label: string | React.ReactElement<string>;
}
export class InsecureWalletWarning extends React.Component<Props, State> {
public state: State = {
hasConfirmedSite: false,
hasAcknowledgedDownload: false,
hasAcknowledgedWallets: false
};
constructor(props: Props) {
super(props);
if (process.env.BUILD_DOWNLOADABLE) {
props.onContinue();
}
}
public render() {
if (process.env.BUILD_DOWNLOADABLE) {
return null;
}
const { walletType, onContinue, onCancel } = this.props;
const checkboxes: Checkbox[] = [
{
name: 'hasAcknowledgedWallets',
label: 'I acknowledge that I can and should use MetaMask or a Hardware Wallet'
},
{
name: 'hasAcknowledgedDownload',
label: 'I acknowledge that I can and should download and run MyEtherWallet locally'
},
{
name: 'hasConfirmedSite',
label:
'I have checked the URL and SSL certificate to make sure this is the real MyEtherWallet'
}
];
const canContinue = checkboxes.reduce(
(prev, checkbox) => prev && this.state[checkbox.name],
true
);
return (
<div className="WalletWarning">
<h2 className="WalletWarning-title">
This is <u>not</u> a recommended way to access your wallet
</h2>
<p className="WalletWarning-desc">
Entering your {walletType} on a website is <strong>dangerous</strong>. If our website is
compromised, or you accidentally visit a phishing website, you could{' '}
<strong>lose all of your funds</strong>. Before you continue, please consider:
</p>
<ul className="WalletWarning-bullets">
<li>
Using{' '}
<a href="https://myetherwallet.github.io/knowledge-base/migration/moving-from-private-key-to-metamask.html">
MetaMask
</a>{' '}
or a{' '}
<a href="https://myetherwallet.github.io/knowledge-base/hardware-wallets/hardware-wallet-recommendations.html">
Hardware Wallet
</a>{' '}
to access your wallet
</li>
<li>
<a href="https://myetherwallet.github.io/knowledge-base/offline/running-myetherwallet-locally.html">
Downloading MEW and running it offline & locally
</a>
</li>
<li>
Reading{' '}
<a href="https://myetherwallet.github.io/knowledge-base/security/securing-your-ethereum.html">
How to Protect Yourself and Your Funds
</a>
</li>
</ul>
<p className="WalletWarning-check">
If you must use your {walletType} online, please double-check the URL & SSL certificate.
It should say <code>{'https://www.myetherwallet.com'}</code>
& <code>MYETHERWALLET LLC [US]</code> in your URL bar.
</p>
<div className="WalletWarning-checkboxes">{checkboxes.map(this.makeCheckbox)}</div>
<div className="WalletWarning-buttons">
<button className="WalletWarning-cancel btn btn-lg btn-default" onClick={onCancel}>
Go Back
</button>
<button
className="WalletWarning-continue btn btn-lg btn-primary"
onClick={onContinue}
disabled={!canContinue}
>
Continue
</button>
</div>
</div>
);
}
private makeCheckbox = (checkbox: Checkbox) => {
return (
<label className="AcknowledgeCheckbox">
<input
type="checkbox"
name={checkbox.name}
className="AcknowledgeCheckbox-checkbox"
onChange={this.handleCheckboxChange}
checked={this.state[checkbox.name]}
/>
<span className="AcknowledgeCheckbox-label">{checkbox.label}</span>
</label>
);
};
private handleCheckboxChange = (ev: React.FormEvent<HTMLInputElement>) => {
this.setState({
[ev.currentTarget.name as any]: !!ev.currentTarget.checked
});
};
}

View File

@ -93,6 +93,8 @@ export class PrivateKeyDecrypt extends Component<Props> {
}; };
public onPasswordChange = (e: React.FormEvent<HTMLInputElement>) => { public onPasswordChange = (e: React.FormEvent<HTMLInputElement>) => {
// NOTE: Textareas don't support password type, so we replace the value
// with an equal length number of dots. On change, we replace
const pkey = this.props.value.key; const pkey = this.props.value.key;
const pass = e.currentTarget.value; const pass = e.currentTarget.value;
const { valid } = validatePkeyAndPass(pkey, pass); const { valid } = validatePkeyAndPass(pkey, pass);

View File

@ -1,5 +1,6 @@
export * from './DeterministicWalletsModal'; export * from './DeterministicWalletsModal';
export * from './DigitalBitbox'; export * from './DigitalBitbox';
export * from './InsecureWalletWarning';
export * from './Keystore'; export * from './Keystore';
export * from './LedgerNano'; export * from './LedgerNano';
export * from './Mnemonic'; export * from './Mnemonic';

View File

@ -11,6 +11,7 @@ interface Props {
title: TranslateType; title: TranslateType;
wallet: IWallet; wallet: IWallet;
disabledWallets?: WalletName[]; disabledWallets?: WalletName[];
showGenerateLink?: boolean;
} }
interface State { interface State {
@ -29,7 +30,7 @@ export class UnlockHeader extends React.PureComponent<Props, State> {
} }
public render() { public render() {
const { title, wallet, disabledWallets } = this.props; const { title, wallet, disabledWallets, showGenerateLink } = this.props;
const { isExpanded } = this.state; const { isExpanded } = this.state;
return ( return (
@ -55,7 +56,11 @@ export class UnlockHeader extends React.PureComponent<Props, State> {
<i className="fa fa-times" /> <i className="fa fa-times" />
</button> </button>
)} )}
<WalletDecrypt hidden={!this.state.isExpanded} disabledWallets={disabledWallets} /> <WalletDecrypt
hidden={!this.state.isExpanded}
disabledWallets={disabledWallets}
showGenerateLink={showGenerateLink}
/>
</article> </article>
); );
} }

View File

@ -56,39 +56,41 @@ class SendTransaction extends React.Component<Props> {
return ( return (
<TabSection> <TabSection>
<section className="Tab-content"> <section className="Tab-content">
<UnlockHeader title={translate('Account')} /> <UnlockHeader title={translate('Account')} showGenerateLink={true} />
<div className="SubTabs row"> {wallet && (
<div className="col-sm-8">{wallet && <SubTabs tabs={tabs} match={match} />}</div> <div className="SubTabs row">
<div className="col-sm-8"> <div className="col-sm-8">
<Switch> <SubTabs tabs={tabs} match={match} />
<Route </div>
exact={true} <div className="col-sm-8">
path={currentPath} <Switch>
render={() => ( <Route
<Redirect exact={true}
from={`${currentPath}`} path={currentPath}
to={`${ render={() => (
wallet && wallet.isReadOnly ? currentPath + '/info' : currentPath + '/send' <Redirect
}`} from={`${currentPath}`}
/> to={`${wallet.isReadOnly ? `${currentPath}/info` : `${currentPath}/send`}`}
)} />
/> )}
<Route exact={true} path={`${currentPath}/send`} component={Send} /> />
<Route <Route exact={true} path={`${currentPath}/send`} component={Send} />
path={`${currentPath}/info`} <Route
exact={true} path={`${currentPath}/info`}
render={() => wallet && <WalletInfo wallet={wallet} />} exact={true}
/> render={() => <WalletInfo wallet={wallet} />}
<Route />
path={`${currentPath}/request`} <Route
exact={true} path={`${currentPath}/request`}
render={() => <RequestPayment wallet={wallet} />} exact={true}
/> render={() => <RequestPayment wallet={wallet} />}
<RouteNotFound /> />
</Switch> <RouteNotFound />
</Switch>
</div>
<SideBar />
</div> </div>
<SideBar /> )}
</div>
</section> </section>
</TabSection> </TabSection>
); );