MEW-01-004 - Stronger Keystores (#981)
* Add better password checking, confirm password, feedback, and up the minimum to 12. * Move wallet generation off to a web worker, and bump up the n value to 8192. Refactor workers a wee bit. * tscheck cleanup * Make keystore password a form. Replace text with spinner on load. * Center align again. * Hard code n factor of test wallet, fix some misspelled type definitions for IV3Wallet.
This commit is contained in:
parent
ca2284b20e
commit
174dea8a29
|
@ -1,5 +1,4 @@
|
||||||
import { PaperWallet } from 'components';
|
import { PaperWallet } from 'components';
|
||||||
import { IFullWallet } from 'ethereumjs-wallet';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { translateRaw } from 'translations';
|
import { translateRaw } from 'translations';
|
||||||
import printElement from 'utils/printElement';
|
import printElement from 'utils/printElement';
|
||||||
|
@ -26,23 +25,23 @@ export const print = (address: string, privateKey: string) => () =>
|
||||||
`
|
`
|
||||||
});
|
});
|
||||||
|
|
||||||
const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => {
|
interface Props {
|
||||||
const address = wallet.getAddressString();
|
address: string;
|
||||||
const privateKey = stripHexPrefix(wallet.getPrivateKeyString());
|
privateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
if (!address || !privateKey) {
|
const PrintableWallet: React.SFC<Props> = ({ address, privateKey }) => {
|
||||||
return null;
|
const pkey = stripHexPrefix(privateKey);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PaperWallet address={address} privateKey={privateKey} />
|
<PaperWallet address={address} privateKey={pkey} />
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
aria-label={translateRaw('x_Print')}
|
aria-label={translateRaw('x_Print')}
|
||||||
aria-describedby="x_PrintDesc"
|
aria-describedby="x_PrintDesc"
|
||||||
className="btn btn-lg btn-primary btn-block"
|
className="btn btn-lg btn-primary btn-block"
|
||||||
onClick={print(address, privateKey)}
|
onClick={print(address, pkey)}
|
||||||
style={{ margin: '10px auto 0', maxWidth: '260px' }}
|
style={{ margin: '10px auto 0', maxWidth: '260px' }}
|
||||||
>
|
>
|
||||||
{translateRaw('x_Print')}
|
{translateRaw('x_Print')}
|
||||||
|
|
|
@ -15,6 +15,7 @@ interface Props {
|
||||||
toggleAriaLabel?: string;
|
toggleAriaLabel?: string;
|
||||||
isValid?: boolean;
|
isValid?: boolean;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
validity?: 'valid' | 'invalid' | 'semivalid';
|
||||||
|
|
||||||
// Textarea-only props
|
// Textarea-only props
|
||||||
isTextareaWhenVisible?: boolean;
|
isTextareaWhenVisible?: boolean;
|
||||||
|
@ -23,6 +24,8 @@ interface Props {
|
||||||
|
|
||||||
// Shared callbacks
|
// Shared callbacks
|
||||||
onChange?(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>): void;
|
onChange?(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>): void;
|
||||||
|
onFocus?(ev: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>): void;
|
||||||
|
onBlur?(ev: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>): void;
|
||||||
handleToggleVisibility?(): void;
|
handleToggleVisibility?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,14 +51,19 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
||||||
name,
|
name,
|
||||||
disabled,
|
disabled,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
toggleAriaLabel,
|
||||||
|
validity,
|
||||||
isTextareaWhenVisible,
|
isTextareaWhenVisible,
|
||||||
isValid,
|
isValid,
|
||||||
onChange,
|
onChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
handleToggleVisibility
|
handleToggleVisibility
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { isVisible } = this.state;
|
const { isVisible } = this.state;
|
||||||
const validClass =
|
const validClass = validity
|
||||||
isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
|
? `is-${validity}`
|
||||||
|
: isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="TogglablePassword input-group">
|
<div className="TogglablePassword input-group">
|
||||||
|
@ -67,6 +75,8 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={this.handleTextareaKeyDown}
|
onKeyDown={this.handleTextareaKeyDown}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
rows={this.props.rows || 3}
|
rows={this.props.rows || 3}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
|
@ -80,12 +90,14 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
||||||
className={`form-control ${validClass}`}
|
className={`form-control ${validClass}`}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
onClick={handleToggleVisibility || this.toggleVisibility}
|
onClick={handleToggleVisibility || this.toggleVisibility}
|
||||||
aria-label="show private key"
|
aria-label={toggleAriaLabel}
|
||||||
role="button"
|
role="button"
|
||||||
className="TogglablePassword-toggle input-group-addon"
|
className="TogglablePassword-toggle input-group-addon"
|
||||||
>
|
>
|
||||||
|
|
|
@ -5,7 +5,7 @@ export const languages = require('./languages.json');
|
||||||
|
|
||||||
// Displays in the header
|
// Displays in the header
|
||||||
export const VERSION = '4.0.0 (Alpha 0.1.0)';
|
export const VERSION = '4.0.0 (Alpha 0.1.0)';
|
||||||
export const N_FACTOR = 1024;
|
export const N_FACTOR = 8192;
|
||||||
|
|
||||||
// Displays at the top of the site, make message empty string to remove.
|
// Displays at the top of the site, make message empty string to remove.
|
||||||
// Type can be primary, warning, danger, success, or info.
|
// Type can be primary, warning, danger, success, or info.
|
||||||
|
@ -47,7 +47,7 @@ export const gasPriceDefaults = {
|
||||||
gasPriceMaxGwei: 60
|
gasPriceMaxGwei: 60
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MINIMUM_PASSWORD_LENGTH = 9;
|
export const MINIMUM_PASSWORD_LENGTH = 12;
|
||||||
|
|
||||||
export const knowledgeBaseURL = 'https://myetherwallet.github.io/knowledge-base';
|
export const knowledgeBaseURL = 'https://myetherwallet.github.io/knowledge-base';
|
||||||
export const bityReferralURL = 'https://bity.com/af/jshkb37v';
|
export const bityReferralURL = 'https://bity.com/af/jshkb37v';
|
||||||
|
|
|
@ -1,42 +1,28 @@
|
||||||
import { IFullWallet, IV3Wallet } from 'ethereumjs-wallet';
|
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||||
import { toChecksumAddress } from 'ethereumjs-util';
|
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { makeBlob } from 'utils/blob';
|
import { makeBlob } from 'utils/blob';
|
||||||
import './DownloadWallet.scss';
|
import './DownloadWallet.scss';
|
||||||
import Template from '../Template';
|
import Template from '../Template';
|
||||||
import { N_FACTOR } from 'config';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wallet: IFullWallet;
|
keystore: IV3Wallet;
|
||||||
password: string;
|
filename: string;
|
||||||
continue(): void;
|
continue(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
hasDownloadedWallet: boolean;
|
hasDownloadedWallet: boolean;
|
||||||
keystore: IV3Wallet | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DownloadWallet extends Component<Props, State> {
|
export default class DownloadWallet extends Component<Props, State> {
|
||||||
public state: State = {
|
public state: State = {
|
||||||
hasDownloadedWallet: false,
|
hasDownloadedWallet: false
|
||||||
keystore: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentWillMount() {
|
|
||||||
this.setWallet(this.props.wallet, this.props.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUpdate(nextProps: Props) {
|
|
||||||
if (this.props.wallet !== nextProps.wallet) {
|
|
||||||
this.setWallet(nextProps.wallet, nextProps.password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const { filename } = this.props;
|
||||||
const { hasDownloadedWallet } = this.state;
|
const { hasDownloadedWallet } = this.state;
|
||||||
const filename = this.props.wallet.getV3Filename();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template>
|
<Template>
|
||||||
|
@ -82,20 +68,9 @@ export default class DownloadWallet extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getBlob = () =>
|
public getBlob = () => makeBlob('text/json;charset=UTF-8', this.props.keystore);
|
||||||
(this.state.keystore && makeBlob('text/json;charset=UTF-8', this.state.keystore)) || undefined;
|
|
||||||
|
|
||||||
private markDownloaded = () =>
|
|
||||||
this.state.keystore && this.setState({ hasDownloadedWallet: true });
|
|
||||||
|
|
||||||
private handleContinue = () => this.state.hasDownloadedWallet && this.props.continue();
|
private handleContinue = () => this.state.hasDownloadedWallet && this.props.continue();
|
||||||
|
|
||||||
private setWallet(wallet: IFullWallet, password: string) {
|
private handleDownloadKeystore = () => this.setState({ hasDownloadedWallet: true });
|
||||||
const keystore = wallet.toV3(password, { n: N_FACTOR });
|
|
||||||
keystore.address = toChecksumAddress(keystore.address);
|
|
||||||
this.setState({ keystore });
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDownloadKeystore = (e: React.FormEvent<HTMLAnchorElement>) =>
|
|
||||||
this.state.keystore ? this.markDownloaded() : e.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,33 @@
|
||||||
@import "common/sass/variables";
|
@import "common/sass/variables";
|
||||||
|
|
||||||
|
$pw-max-width: 40rem;
|
||||||
|
|
||||||
.EnterPw {
|
.EnterPw {
|
||||||
&-title {
|
&-title {
|
||||||
margin: $space auto $space * 2.5;
|
margin: $space auto $space * 2.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-password {
|
&-password {
|
||||||
max-width: 40rem;
|
position: relative;
|
||||||
|
max-width: $pw-max-width;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto $space;
|
margin: 0 auto $space;
|
||||||
|
|
||||||
&-label {
|
&-label {
|
||||||
margin-bottom: $space;
|
margin-bottom: $space;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-feedback {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -$space;
|
||||||
|
top: 100%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-submit {
|
&-submit {
|
||||||
max-width: 16rem;
|
max-width: $pw-max-width;
|
||||||
margin: 0 auto $space * 3;
|
margin: $space auto $space * 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,42 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import zxcvbn, { ZXCVBNResult } from 'zxcvbn';
|
||||||
import translate, { translateRaw } from 'translations';
|
import translate, { translateRaw } from 'translations';
|
||||||
import { MINIMUM_PASSWORD_LENGTH } from 'config';
|
import { MINIMUM_PASSWORD_LENGTH } from 'config';
|
||||||
import { TogglablePassword } from 'components';
|
import { TogglablePassword } from 'components';
|
||||||
|
import { Spinner } from 'components/ui';
|
||||||
import Template from '../Template';
|
import Template from '../Template';
|
||||||
import './EnterPassword.scss';
|
import './EnterPassword.scss';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
isGenerating: boolean;
|
||||||
continue(pw: string): void;
|
continue(pw: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
password: string;
|
password: string;
|
||||||
isPasswordValid: boolean;
|
confirmedPassword: string;
|
||||||
|
passwordValidation: ZXCVBNResult | null;
|
||||||
|
feedback: string;
|
||||||
}
|
}
|
||||||
export default class EnterPassword extends Component<Props, State> {
|
export default class EnterPassword extends Component<Props, State> {
|
||||||
public state = {
|
public state: State = {
|
||||||
password: '',
|
password: '',
|
||||||
isPasswordValid: false
|
confirmedPassword: '',
|
||||||
|
passwordValidation: null,
|
||||||
|
feedback: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { password, isPasswordValid } = this.state;
|
const { isGenerating } = this.props;
|
||||||
|
const { password, confirmedPassword, feedback } = this.state;
|
||||||
|
const passwordValidity = this.getPasswordValidity();
|
||||||
|
const isPasswordValid = passwordValidity === 'valid';
|
||||||
|
const isConfirmValid = confirmedPassword ? password === confirmedPassword : undefined;
|
||||||
|
const canSubmit = isPasswordValid && isConfirmValid && !isGenerating;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template>
|
<Template>
|
||||||
<div className="EnterPw">
|
<form className="EnterPw" onSubmit={canSubmit ? this.handleSubmit : undefined}>
|
||||||
<h1 className="EnterPw-title" aria-live="polite">
|
<h1 className="EnterPw-title" aria-live="polite">
|
||||||
Generate a {translate('x_Keystore2')}
|
Generate a {translate('x_Keystore2')}
|
||||||
</h1>
|
</h1>
|
||||||
|
@ -33,36 +45,114 @@ export default class EnterPassword extends Component<Props, State> {
|
||||||
<h4 className="EnterPw-password-label">{translate('GEN_Label_1')}</h4>
|
<h4 className="EnterPw-password-label">{translate('GEN_Label_1')}</h4>
|
||||||
<TogglablePassword
|
<TogglablePassword
|
||||||
value={password}
|
value={password}
|
||||||
placeholder={translateRaw('GEN_Placeholder_1')}
|
placeholder={`Password must be uncommon and ${MINIMUM_PASSWORD_LENGTH}+ characters long`}
|
||||||
|
validity={passwordValidity}
|
||||||
ariaLabel={translateRaw('GEN_Aria_1')}
|
ariaLabel={translateRaw('GEN_Aria_1')}
|
||||||
toggleAriaLabel={translateRaw('GEN_Aria_2')}
|
toggleAriaLabel={translateRaw('GEN_Aria_2')}
|
||||||
isValid={isPasswordValid}
|
|
||||||
onChange={this.onPasswordChange}
|
onChange={this.onPasswordChange}
|
||||||
|
onBlur={this.showFeedback}
|
||||||
|
/>
|
||||||
|
{!isPasswordValid &&
|
||||||
|
feedback && (
|
||||||
|
<p className={`EnterPw-password-feedback help-block is-${passwordValidity}`}>
|
||||||
|
{feedback}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="EnterPw-password">
|
||||||
|
<h4 className="EnterPw-password-label">Confirm password</h4>
|
||||||
|
<TogglablePassword
|
||||||
|
value={confirmedPassword}
|
||||||
|
placeholder={translateRaw('GEN_Placeholder_1')}
|
||||||
|
ariaLabel="Confirm Password"
|
||||||
|
toggleAriaLabel="toggle confirm password visibility"
|
||||||
|
isValid={isConfirmValid}
|
||||||
|
onChange={this.onConfirmChange}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button
|
<button disabled={!canSubmit} className="EnterPw-submit btn btn-primary btn-lg btn-block">
|
||||||
onClick={this.onClickGenerateFile}
|
{isGenerating ? <Spinner light={true} /> : translate('NAV_GenerateWallet')}
|
||||||
disabled={!isPasswordValid}
|
|
||||||
className="EnterPw-submit btn btn-primary btn-block"
|
|
||||||
>
|
|
||||||
{translate('NAV_GenerateWallet')}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p className="EnterPw-warning">{translate('x_PasswordDesc')}</p>
|
<p className="EnterPw-warning">{translate('x_PasswordDesc')}</p>
|
||||||
</div>
|
</form>
|
||||||
</Template>
|
</Template>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
private onClickGenerateFile = () => {
|
|
||||||
|
private getPasswordValidity(): 'valid' | 'invalid' | 'semivalid' | undefined {
|
||||||
|
const { password, passwordValidation } = this.state;
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < MINIMUM_PASSWORD_LENGTH) {
|
||||||
|
return 'invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (passwordValidation && passwordValidation.score < 3) {
|
||||||
|
return 'semivalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'valid';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFeedback() {
|
||||||
|
let feedback = '';
|
||||||
|
const validity = this.getPasswordValidity();
|
||||||
|
|
||||||
|
if (validity !== 'valid') {
|
||||||
|
const { password, passwordValidation } = this.state;
|
||||||
|
|
||||||
|
if (password.length < MINIMUM_PASSWORD_LENGTH) {
|
||||||
|
feedback = `Password must be ${MINIMUM_PASSWORD_LENGTH}+ characters`;
|
||||||
|
} else if (passwordValidation && passwordValidation.feedback) {
|
||||||
|
feedback = `This password is not strong enough. ${passwordValidation.feedback.warning}.`;
|
||||||
|
} else {
|
||||||
|
feedback = 'There is something invalid about your password. Please try another.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
ev.preventDefault();
|
||||||
this.props.continue(this.state.password);
|
this.props.continue(this.state.password);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onPasswordChange = (e: any) => {
|
private onPasswordChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
const password = e.target.value;
|
const password = e.currentTarget.value;
|
||||||
this.setState({
|
const passwordValidation = password ? zxcvbn(password) : null;
|
||||||
isPasswordValid: password.length >= MINIMUM_PASSWORD_LENGTH,
|
|
||||||
password
|
this.setState(
|
||||||
});
|
{
|
||||||
|
password,
|
||||||
|
passwordValidation,
|
||||||
|
feedback: ''
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (password.length >= MINIMUM_PASSWORD_LENGTH) {
|
||||||
|
this.showFeedback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onConfirmChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
this.setState({ confirmedPassword: e.currentTarget.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
private showFeedback = () => {
|
||||||
|
const { password, passwordValidation } = this.state;
|
||||||
|
if (!password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = this.getFeedback();
|
||||||
|
this.setState({ passwordValidation, feedback });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { generate, IFullWallet } from 'ethereumjs-wallet';
|
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { generateKeystore } from 'libs/web-workers';
|
||||||
import { WalletType } from '../../GenerateWallet';
|
import { WalletType } from '../../GenerateWallet';
|
||||||
import Template from '../Template';
|
import Template from '../Template';
|
||||||
import DownloadWallet from './DownloadWallet';
|
import DownloadWallet from './DownloadWallet';
|
||||||
|
@ -17,36 +18,54 @@ export enum Steps {
|
||||||
interface State {
|
interface State {
|
||||||
activeStep: Steps;
|
activeStep: Steps;
|
||||||
password: string;
|
password: string;
|
||||||
wallet: IFullWallet | null | undefined;
|
keystore: IV3Wallet | null | undefined;
|
||||||
|
filename: string;
|
||||||
|
privateKey: string;
|
||||||
|
isGenerating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GenerateKeystore extends Component<{}, State> {
|
export default class GenerateKeystore extends Component<{}, State> {
|
||||||
public state: State = {
|
public state: State = {
|
||||||
activeStep: Steps.Password,
|
activeStep: Steps.Password,
|
||||||
password: '',
|
password: '',
|
||||||
wallet: null
|
keystore: null,
|
||||||
|
filename: '',
|
||||||
|
privateKey: '',
|
||||||
|
isGenerating: false
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { activeStep, wallet, password } = this.state;
|
const { activeStep, keystore, privateKey, filename, isGenerating } = this.state;
|
||||||
let content;
|
let content;
|
||||||
|
|
||||||
switch (activeStep) {
|
switch (activeStep) {
|
||||||
case Steps.Password:
|
case Steps.Password:
|
||||||
content = <EnterPassword continue={this.generateWalletAndContinue} />;
|
content = (
|
||||||
|
<EnterPassword continue={this.generateWalletAndContinue} isGenerating={isGenerating} />
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Steps.Download:
|
case Steps.Download:
|
||||||
if (wallet) {
|
if (keystore) {
|
||||||
content = (
|
content = (
|
||||||
<DownloadWallet wallet={wallet} password={password} continue={this.continueToPaper} />
|
<DownloadWallet
|
||||||
|
keystore={keystore}
|
||||||
|
filename={filename}
|
||||||
|
continue={this.continueToPaper}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Steps.Paper:
|
case Steps.Paper:
|
||||||
if (wallet) {
|
if (keystore) {
|
||||||
content = <PaperWallet wallet={wallet} continue={this.continueToFinal} />;
|
content = (
|
||||||
|
<PaperWallet
|
||||||
|
keystore={keystore}
|
||||||
|
privateKey={privateKey}
|
||||||
|
continue={this.continueToFinal}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -66,10 +85,17 @@ export default class GenerateKeystore extends Component<{}, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateWalletAndContinue = (password: string) => {
|
private generateWalletAndContinue = (password: string) => {
|
||||||
this.setState({
|
this.setState({ isGenerating: true });
|
||||||
password,
|
|
||||||
activeStep: Steps.Download,
|
generateKeystore(password).then(res => {
|
||||||
wallet: generate()
|
this.setState({
|
||||||
|
password,
|
||||||
|
activeStep: Steps.Download,
|
||||||
|
keystore: res.keystore,
|
||||||
|
filename: res.filename,
|
||||||
|
privateKey: res.privateKey,
|
||||||
|
isGenerating: false
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PrintableWallet from 'components/PrintableWallet';
|
import PrintableWallet from 'components/PrintableWallet';
|
||||||
import { IFullWallet } from 'ethereumjs-wallet';
|
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { stripHexPrefix } from 'libs/values';
|
import { stripHexPrefix } from 'libs/values';
|
||||||
|
@ -7,7 +7,8 @@ import './PaperWallet.scss';
|
||||||
import Template from '../Template';
|
import Template from '../Template';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wallet: IFullWallet;
|
keystore: IV3Wallet;
|
||||||
|
privateKey: string;
|
||||||
continue(): void;
|
continue(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +19,7 @@ const PaperWallet: React.SFC<Props> = props => (
|
||||||
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
|
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
|
||||||
<input
|
<input
|
||||||
className="GenPaper-private form-control"
|
className="GenPaper-private form-control"
|
||||||
value={stripHexPrefix(props.wallet.getPrivateKeyString())}
|
value={stripHexPrefix(props.privateKey)}
|
||||||
aria-label={translate('x_PrivKey')}
|
aria-label={translate('x_PrivKey')}
|
||||||
aria-describedby="x_PrivKeyDesc"
|
aria-describedby="x_PrivKeyDesc"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -28,7 +29,7 @@ const PaperWallet: React.SFC<Props> = props => (
|
||||||
{/* Download Paper Wallet */}
|
{/* Download Paper Wallet */}
|
||||||
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
|
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
|
||||||
<div className="GenPaper-paper">
|
<div className="GenPaper-paper">
|
||||||
<PrintableWallet wallet={props.wallet} />
|
<PrintableWallet address={props.keystore.address} privateKey={props.privateKey} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Warning */}
|
{/* Warning */}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { fromPrivateKey, fromEthSale } from 'ethereumjs-wallet';
|
||||||
import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty';
|
import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty';
|
||||||
import { signWrapper } from './helpers';
|
import { signWrapper } from './helpers';
|
||||||
import { decryptPrivKey } from 'libs/decrypt';
|
import { decryptPrivKey } from 'libs/decrypt';
|
||||||
import { fromV3 } from 'libs/web-workers/scrypt-wrapper';
|
import { fromV3 } from 'libs/web-workers';
|
||||||
import Web3Wallet from './web3';
|
import Web3Wallet from './web3';
|
||||||
import AddressOnlyWallet from './address';
|
import AddressOnlyWallet from './address';
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { IFullWallet, fromPrivateKey } from 'ethereumjs-wallet';
|
import { IFullWallet, fromPrivateKey } from 'ethereumjs-wallet';
|
||||||
import { toBuffer } from 'ethereumjs-util';
|
import { toBuffer } from 'ethereumjs-util';
|
||||||
import Worker from 'worker-loader!./workers/scrypt-worker.worker.ts';
|
import Worker from 'worker-loader!./workers/fromV3.worker.ts';
|
||||||
|
|
||||||
export const fromV3 = (
|
export default function fromV3(
|
||||||
keystore: string,
|
keystore: string,
|
||||||
password: string,
|
password: string,
|
||||||
nonStrict: boolean
|
nonStrict: boolean
|
||||||
): Promise<IFullWallet> => {
|
): Promise<IFullWallet> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const scryptWorker = new Worker();
|
const worker = new Worker();
|
||||||
scryptWorker.postMessage({ keystore, password, nonStrict });
|
worker.postMessage({ keystore, password, nonStrict });
|
||||||
scryptWorker.onmessage = event => {
|
worker.onmessage = (ev: MessageEvent) => {
|
||||||
const data: string = event.data;
|
const data = ev.data;
|
||||||
try {
|
try {
|
||||||
const wallet = fromPrivateKey(toBuffer(data));
|
const wallet = fromPrivateKey(toBuffer(data));
|
||||||
resolve(wallet);
|
resolve(wallet);
|
||||||
|
@ -20,4 +20,4 @@ export const fromV3 = (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||||
|
import { N_FACTOR } from 'config';
|
||||||
|
import Worker from 'worker-loader!./workers/generateKeystore.worker.ts';
|
||||||
|
|
||||||
|
interface KeystorePayload {
|
||||||
|
filename: string;
|
||||||
|
keystore: IV3Wallet;
|
||||||
|
privateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function generateKeystore(password: string): Promise<KeystorePayload> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const worker = new Worker();
|
||||||
|
worker.postMessage({ password, N_FACTOR });
|
||||||
|
worker.onmessage = (ev: MessageEvent) => {
|
||||||
|
const filename: string = ev.data.filename;
|
||||||
|
const privateKey: string = ev.data.privateKey;
|
||||||
|
const keystore: IV3Wallet = ev.data.keystore;
|
||||||
|
resolve({ keystore, filename, privateKey });
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as fromV3 } from './fromV3';
|
||||||
|
export { default as generateKeystore } from './generateKeystore';
|
|
@ -1,18 +1,18 @@
|
||||||
import { fromV3, IFullWallet } from 'ethereumjs-wallet';
|
import { fromV3, IFullWallet } from 'ethereumjs-wallet';
|
||||||
|
|
||||||
const scryptWorker: Worker = self as any;
|
const worker: Worker = self as any;
|
||||||
interface DecryptionParameters {
|
interface DecryptionParameters {
|
||||||
keystore: string;
|
keystore: string;
|
||||||
password: string;
|
password: string;
|
||||||
nonStrict: boolean;
|
nonStrict: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
scryptWorker.onmessage = (event: MessageEvent) => {
|
worker.onmessage = (event: MessageEvent) => {
|
||||||
const info: DecryptionParameters = event.data;
|
const info: DecryptionParameters = event.data;
|
||||||
try {
|
try {
|
||||||
const rawKeystore: IFullWallet = fromV3(info.keystore, info.password, info.nonStrict);
|
const rawKeystore: IFullWallet = fromV3(info.keystore, info.password, info.nonStrict);
|
||||||
scryptWorker.postMessage(rawKeystore.getPrivateKeyString());
|
worker.postMessage(rawKeystore.getPrivateKeyString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
scryptWorker.postMessage(e.message);
|
worker.postMessage(e.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { generate } from 'ethereumjs-wallet';
|
||||||
|
import { toChecksumAddress } from 'ethereumjs-util';
|
||||||
|
|
||||||
|
const worker: Worker = self as any;
|
||||||
|
|
||||||
|
interface GenerateParameters {
|
||||||
|
password: string;
|
||||||
|
N_FACTOR: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onmessage = (event: MessageEvent) => {
|
||||||
|
const info: GenerateParameters = event.data;
|
||||||
|
const wallet = generate();
|
||||||
|
const filename = wallet.getV3Filename();
|
||||||
|
const privateKey = wallet.getPrivateKeyString();
|
||||||
|
const keystore = wallet.toV3(info.password, { n: info.N_FACTOR });
|
||||||
|
keystore.address = toChecksumAddress(keystore.address);
|
||||||
|
worker.postMessage({ keystore, filename, privateKey });
|
||||||
|
};
|
|
@ -83,3 +83,17 @@ select.form-control {
|
||||||
@include form-control-state($brand-warning);
|
@include form-control-state($brand-warning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help-block {
|
||||||
|
&.is-valid {
|
||||||
|
color: $brand-success;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-invalid {
|
||||||
|
color: $brand-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-semivalid {
|
||||||
|
color: $brand-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -165,14 +165,14 @@ declare module 'ethereumjs-wallet' {
|
||||||
version: 3;
|
version: 3;
|
||||||
id: string;
|
id: string;
|
||||||
address: string;
|
address: string;
|
||||||
Crypto: {
|
crypto: {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
cipherParams: {
|
cipherparams: {
|
||||||
iv: string;
|
iv: string;
|
||||||
};
|
};
|
||||||
cipher: string | 'aes-128-ctr';
|
cipher: string | 'aes-128-ctr';
|
||||||
kdf: 'scrypt' | 'pbkdf2';
|
kdf: 'scrypt' | 'pbkdf2';
|
||||||
kfdparams: IScryptKdfParams | IPbkdf2KdfParams;
|
kdfparams: IScryptKdfParams | IPbkdf2KdfParams;
|
||||||
mac: string;
|
mac: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,8 @@
|
||||||
"scryptsy": "2.0.0",
|
"scryptsy": "2.0.0",
|
||||||
"uuid": "3.2.1",
|
"uuid": "3.2.1",
|
||||||
"wallet-address-validator": "0.1.1",
|
"wallet-address-validator": "0.1.1",
|
||||||
"whatwg-fetch": "2.0.3"
|
"whatwg-fetch": "2.0.3",
|
||||||
|
"zxcvbn": "4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/classnames": "2.2.3",
|
"@types/classnames": "2.2.3",
|
||||||
|
@ -70,6 +71,7 @@
|
||||||
"@types/redux-promise-middleware": "0.0.9",
|
"@types/redux-promise-middleware": "0.0.9",
|
||||||
"@types/uuid": "3.4.3",
|
"@types/uuid": "3.4.3",
|
||||||
"@types/webpack-env": "1.13.4",
|
"@types/webpack-env": "1.13.4",
|
||||||
|
"@types/zxcvbn": "4.4.0",
|
||||||
"autodll-webpack-plugin": "0.3.8",
|
"autodll-webpack-plugin": "0.3.8",
|
||||||
"awesome-typescript-loader": "3.4.1",
|
"awesome-typescript-loader": "3.4.1",
|
||||||
"babel-minify-webpack-plugin": "0.2.0",
|
"babel-minify-webpack-plugin": "0.2.0",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
import { Wei } from 'libs/units';
|
import { Wei } from 'libs/units';
|
||||||
import { changeNodeIntent, web3UnsetNode } from 'actions/config';
|
import { changeNodeIntent, web3UnsetNode } from 'actions/config';
|
||||||
import { INode } from 'libs/nodes/INode';
|
import { INode } from 'libs/nodes/INode';
|
||||||
import { initWeb3Node, Token, N_FACTOR } from 'config';
|
import { initWeb3Node, Token } from 'config';
|
||||||
import { apply, call, fork, put, select, take, cancel } from 'redux-saga/effects';
|
import { apply, call, fork, put, select, take, cancel } from 'redux-saga/effects';
|
||||||
import { getNodeLib, getOffline } from 'selectors/config';
|
import { getNodeLib, getOffline } from 'selectors/config';
|
||||||
import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet';
|
import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet';
|
||||||
|
@ -36,7 +36,7 @@ import Web3Node from 'libs/nodes/web3';
|
||||||
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
|
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
|
||||||
import { showNotification } from 'actions/notifications';
|
import { showNotification } from 'actions/notifications';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { IFullWallet, fromV3 } from 'ethereumjs-wallet';
|
import { IFullWallet, IV3Wallet, fromV3 } from 'ethereumjs-wallet';
|
||||||
|
|
||||||
// init module
|
// init module
|
||||||
configuredStore.getState();
|
configuredStore.getState();
|
||||||
|
@ -59,11 +59,11 @@ const token2: Token = {
|
||||||
};
|
};
|
||||||
const tokens = [token1, token2];
|
const tokens = [token1, token2];
|
||||||
|
|
||||||
const utcKeystore = {
|
const utcKeystore: IV3Wallet = {
|
||||||
version: 3,
|
version: 3,
|
||||||
id: 'cb788af4-993d-43ad-851b-0d2031e52c61',
|
id: 'cb788af4-993d-43ad-851b-0d2031e52c61',
|
||||||
address: '25a24679f35e447f778cf54a3823facf39904a63',
|
address: '25a24679f35e447f778cf54a3823facf39904a63',
|
||||||
Crypto: {
|
crypto: {
|
||||||
ciphertext: '4193915c560835d00b2b9ff5dd20f3e13793b2a3ca8a97df649286063f27f707',
|
ciphertext: '4193915c560835d00b2b9ff5dd20f3e13793b2a3ca8a97df649286063f27f707',
|
||||||
cipherparams: {
|
cipherparams: {
|
||||||
iv: 'dccb8c009b11d1c6226ba19b557dce4c'
|
iv: 'dccb8c009b11d1c6226ba19b557dce4c'
|
||||||
|
@ -73,7 +73,7 @@ const utcKeystore = {
|
||||||
kdfparams: {
|
kdfparams: {
|
||||||
dklen: 32,
|
dklen: 32,
|
||||||
salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab',
|
salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab',
|
||||||
n: N_FACTOR,
|
n: 1024,
|
||||||
r: 8,
|
r: 8,
|
||||||
p: 1
|
p: 1
|
||||||
},
|
},
|
||||||
|
|
|
@ -69,7 +69,6 @@ module.exports = {
|
||||||
'redux-promise-middleware',
|
'redux-promise-middleware',
|
||||||
'redux-saga',
|
'redux-saga',
|
||||||
'scryptsy',
|
'scryptsy',
|
||||||
'store2',
|
|
||||||
'uuid',
|
'uuid',
|
||||||
'wallet-address-validator',
|
'wallet-address-validator',
|
||||||
'whatwg-fetch'
|
'whatwg-fetch'
|
||||||
|
|
Loading…
Reference in New Issue