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 { IFullWallet } from 'ethereumjs-wallet';
|
||||
import React from 'react';
|
||||
import { translateRaw } from 'translations';
|
||||
import printElement from 'utils/printElement';
|
||||
|
@ -26,23 +25,23 @@ export const print = (address: string, privateKey: string) => () =>
|
|||
`
|
||||
});
|
||||
|
||||
const PrintableWallet: React.SFC<{ wallet: IFullWallet }> = ({ wallet }) => {
|
||||
const address = wallet.getAddressString();
|
||||
const privateKey = stripHexPrefix(wallet.getPrivateKeyString());
|
||||
interface Props {
|
||||
address: string;
|
||||
privateKey: string;
|
||||
}
|
||||
|
||||
if (!address || !privateKey) {
|
||||
return null;
|
||||
}
|
||||
const PrintableWallet: React.SFC<Props> = ({ address, privateKey }) => {
|
||||
const pkey = stripHexPrefix(privateKey);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PaperWallet address={address} privateKey={privateKey} />
|
||||
<PaperWallet address={address} privateKey={pkey} />
|
||||
<a
|
||||
role="button"
|
||||
aria-label={translateRaw('x_Print')}
|
||||
aria-describedby="x_PrintDesc"
|
||||
className="btn btn-lg btn-primary btn-block"
|
||||
onClick={print(address, privateKey)}
|
||||
onClick={print(address, pkey)}
|
||||
style={{ margin: '10px auto 0', maxWidth: '260px' }}
|
||||
>
|
||||
{translateRaw('x_Print')}
|
||||
|
|
|
@ -15,6 +15,7 @@ interface Props {
|
|||
toggleAriaLabel?: string;
|
||||
isValid?: boolean;
|
||||
isVisible?: boolean;
|
||||
validity?: 'valid' | 'invalid' | 'semivalid';
|
||||
|
||||
// Textarea-only props
|
||||
isTextareaWhenVisible?: boolean;
|
||||
|
@ -23,6 +24,8 @@ interface Props {
|
|||
|
||||
// Shared callbacks
|
||||
onChange?(ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>): void;
|
||||
onFocus?(ev: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>): void;
|
||||
onBlur?(ev: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>): void;
|
||||
handleToggleVisibility?(): void;
|
||||
}
|
||||
|
||||
|
@ -48,14 +51,19 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
name,
|
||||
disabled,
|
||||
ariaLabel,
|
||||
toggleAriaLabel,
|
||||
validity,
|
||||
isTextareaWhenVisible,
|
||||
isValid,
|
||||
onChange,
|
||||
onFocus,
|
||||
onBlur,
|
||||
handleToggleVisibility
|
||||
} = this.props;
|
||||
const { isVisible } = this.state;
|
||||
const validClass =
|
||||
isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
|
||||
const validClass = validity
|
||||
? `is-${validity}`
|
||||
: isValid === null || isValid === undefined ? '' : isValid ? 'is-valid' : 'is-invalid';
|
||||
|
||||
return (
|
||||
<div className="TogglablePassword input-group">
|
||||
|
@ -67,6 +75,8 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
onKeyDown={this.handleTextareaKeyDown}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
placeholder={placeholder}
|
||||
rows={this.props.rows || 3}
|
||||
aria-label={ariaLabel}
|
||||
|
@ -80,12 +90,14 @@ export default class TogglablePassword extends React.PureComponent<Props, State>
|
|||
className={`form-control ${validClass}`}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
onClick={handleToggleVisibility || this.toggleVisibility}
|
||||
aria-label="show private key"
|
||||
aria-label={toggleAriaLabel}
|
||||
role="button"
|
||||
className="TogglablePassword-toggle input-group-addon"
|
||||
>
|
||||
|
|
|
@ -5,7 +5,7 @@ export const languages = require('./languages.json');
|
|||
|
||||
// Displays in the header
|
||||
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.
|
||||
// Type can be primary, warning, danger, success, or info.
|
||||
|
@ -47,7 +47,7 @@ export const gasPriceDefaults = {
|
|||
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 bityReferralURL = 'https://bity.com/af/jshkb37v';
|
||||
|
|
|
@ -1,42 +1,28 @@
|
|||
import { IFullWallet, IV3Wallet } from 'ethereumjs-wallet';
|
||||
import { toChecksumAddress } from 'ethereumjs-util';
|
||||
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { makeBlob } from 'utils/blob';
|
||||
import './DownloadWallet.scss';
|
||||
import Template from '../Template';
|
||||
import { N_FACTOR } from 'config';
|
||||
|
||||
interface Props {
|
||||
wallet: IFullWallet;
|
||||
password: string;
|
||||
keystore: IV3Wallet;
|
||||
filename: string;
|
||||
continue(): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasDownloadedWallet: boolean;
|
||||
keystore: IV3Wallet | null;
|
||||
}
|
||||
|
||||
export default class DownloadWallet extends Component<Props, State> {
|
||||
public state: State = {
|
||||
hasDownloadedWallet: false,
|
||||
keystore: null
|
||||
hasDownloadedWallet: false
|
||||
};
|
||||
|
||||
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() {
|
||||
const { filename } = this.props;
|
||||
const { hasDownloadedWallet } = this.state;
|
||||
const filename = this.props.wallet.getV3Filename();
|
||||
|
||||
return (
|
||||
<Template>
|
||||
|
@ -82,20 +68,9 @@ export default class DownloadWallet extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
public getBlob = () =>
|
||||
(this.state.keystore && makeBlob('text/json;charset=UTF-8', this.state.keystore)) || undefined;
|
||||
|
||||
private markDownloaded = () =>
|
||||
this.state.keystore && this.setState({ hasDownloadedWallet: true });
|
||||
public getBlob = () => makeBlob('text/json;charset=UTF-8', this.props.keystore);
|
||||
|
||||
private handleContinue = () => this.state.hasDownloadedWallet && this.props.continue();
|
||||
|
||||
private setWallet(wallet: IFullWallet, password: string) {
|
||||
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();
|
||||
private handleDownloadKeystore = () => this.setState({ hasDownloadedWallet: true });
|
||||
}
|
||||
|
|
|
@ -1,22 +1,33 @@
|
|||
@import "common/sass/variables";
|
||||
|
||||
$pw-max-width: 40rem;
|
||||
|
||||
.EnterPw {
|
||||
&-title {
|
||||
margin: $space auto $space * 2.5;
|
||||
}
|
||||
|
||||
&-password {
|
||||
max-width: 40rem;
|
||||
position: relative;
|
||||
max-width: $pw-max-width;
|
||||
width: 100%;
|
||||
margin: 0 auto $space;
|
||||
|
||||
&-label {
|
||||
margin-bottom: $space;
|
||||
}
|
||||
|
||||
&-feedback {
|
||||
position: absolute;
|
||||
bottom: -$space;
|
||||
top: 100%;
|
||||
text-align: left;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
&-submit {
|
||||
max-width: 16rem;
|
||||
margin: 0 auto $space * 3;
|
||||
max-width: $pw-max-width;
|
||||
margin: $space auto $space * 3;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,42 @@
|
|||
import React, { Component } from 'react';
|
||||
import zxcvbn, { ZXCVBNResult } from 'zxcvbn';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from 'config';
|
||||
import { TogglablePassword } from 'components';
|
||||
import { Spinner } from 'components/ui';
|
||||
import Template from '../Template';
|
||||
import './EnterPassword.scss';
|
||||
|
||||
interface Props {
|
||||
isGenerating: boolean;
|
||||
continue(pw: string): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
password: string;
|
||||
isPasswordValid: boolean;
|
||||
confirmedPassword: string;
|
||||
passwordValidation: ZXCVBNResult | null;
|
||||
feedback: string;
|
||||
}
|
||||
export default class EnterPassword extends Component<Props, State> {
|
||||
public state = {
|
||||
public state: State = {
|
||||
password: '',
|
||||
isPasswordValid: false
|
||||
confirmedPassword: '',
|
||||
passwordValidation: null,
|
||||
feedback: ''
|
||||
};
|
||||
|
||||
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 (
|
||||
<Template>
|
||||
<div className="EnterPw">
|
||||
<form className="EnterPw" onSubmit={canSubmit ? this.handleSubmit : undefined}>
|
||||
<h1 className="EnterPw-title" aria-live="polite">
|
||||
Generate a {translate('x_Keystore2')}
|
||||
</h1>
|
||||
|
@ -33,36 +45,114 @@ export default class EnterPassword extends Component<Props, State> {
|
|||
<h4 className="EnterPw-password-label">{translate('GEN_Label_1')}</h4>
|
||||
<TogglablePassword
|
||||
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')}
|
||||
toggleAriaLabel={translateRaw('GEN_Aria_2')}
|
||||
isValid={isPasswordValid}
|
||||
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>
|
||||
|
||||
<button
|
||||
onClick={this.onClickGenerateFile}
|
||||
disabled={!isPasswordValid}
|
||||
className="EnterPw-submit btn btn-primary btn-block"
|
||||
>
|
||||
{translate('NAV_GenerateWallet')}
|
||||
<button disabled={!canSubmit} className="EnterPw-submit btn btn-primary btn-lg btn-block">
|
||||
{isGenerating ? <Spinner light={true} /> : translate('NAV_GenerateWallet')}
|
||||
</button>
|
||||
|
||||
<p className="EnterPw-warning">{translate('x_PasswordDesc')}</p>
|
||||
</div>
|
||||
</form>
|
||||
</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);
|
||||
};
|
||||
|
||||
private onPasswordChange = (e: any) => {
|
||||
const password = e.target.value;
|
||||
this.setState({
|
||||
isPasswordValid: password.length >= MINIMUM_PASSWORD_LENGTH,
|
||||
password
|
||||
});
|
||||
private onPasswordChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const password = e.currentTarget.value;
|
||||
const passwordValidation = password ? zxcvbn(password) : null;
|
||||
|
||||
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 { generateKeystore } from 'libs/web-workers';
|
||||
import { WalletType } from '../../GenerateWallet';
|
||||
import Template from '../Template';
|
||||
import DownloadWallet from './DownloadWallet';
|
||||
|
@ -17,36 +18,54 @@ export enum Steps {
|
|||
interface State {
|
||||
activeStep: Steps;
|
||||
password: string;
|
||||
wallet: IFullWallet | null | undefined;
|
||||
keystore: IV3Wallet | null | undefined;
|
||||
filename: string;
|
||||
privateKey: string;
|
||||
isGenerating: boolean;
|
||||
}
|
||||
|
||||
export default class GenerateKeystore extends Component<{}, State> {
|
||||
public state: State = {
|
||||
activeStep: Steps.Password,
|
||||
password: '',
|
||||
wallet: null
|
||||
keystore: null,
|
||||
filename: '',
|
||||
privateKey: '',
|
||||
isGenerating: false
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { activeStep, wallet, password } = this.state;
|
||||
const { activeStep, keystore, privateKey, filename, isGenerating } = this.state;
|
||||
let content;
|
||||
|
||||
switch (activeStep) {
|
||||
case Steps.Password:
|
||||
content = <EnterPassword continue={this.generateWalletAndContinue} />;
|
||||
content = (
|
||||
<EnterPassword continue={this.generateWalletAndContinue} isGenerating={isGenerating} />
|
||||
);
|
||||
break;
|
||||
|
||||
case Steps.Download:
|
||||
if (wallet) {
|
||||
if (keystore) {
|
||||
content = (
|
||||
<DownloadWallet wallet={wallet} password={password} continue={this.continueToPaper} />
|
||||
<DownloadWallet
|
||||
keystore={keystore}
|
||||
filename={filename}
|
||||
continue={this.continueToPaper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case Steps.Paper:
|
||||
if (wallet) {
|
||||
content = <PaperWallet wallet={wallet} continue={this.continueToFinal} />;
|
||||
if (keystore) {
|
||||
content = (
|
||||
<PaperWallet
|
||||
keystore={keystore}
|
||||
privateKey={privateKey}
|
||||
continue={this.continueToFinal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -66,10 +85,17 @@ export default class GenerateKeystore extends Component<{}, State> {
|
|||
}
|
||||
|
||||
private generateWalletAndContinue = (password: string) => {
|
||||
this.setState({
|
||||
password,
|
||||
activeStep: Steps.Download,
|
||||
wallet: generate()
|
||||
this.setState({ isGenerating: true });
|
||||
|
||||
generateKeystore(password).then(res => {
|
||||
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 { IFullWallet } from 'ethereumjs-wallet';
|
||||
import { IV3Wallet } from 'ethereumjs-wallet';
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
|
@ -7,7 +7,8 @@ import './PaperWallet.scss';
|
|||
import Template from '../Template';
|
||||
|
||||
interface Props {
|
||||
wallet: IFullWallet;
|
||||
keystore: IV3Wallet;
|
||||
privateKey: string;
|
||||
continue(): void;
|
||||
}
|
||||
|
||||
|
@ -18,7 +19,7 @@ const PaperWallet: React.SFC<Props> = props => (
|
|||
<h1 className="GenPaper-title">{translate('GEN_Label_5')}</h1>
|
||||
<input
|
||||
className="GenPaper-private form-control"
|
||||
value={stripHexPrefix(props.wallet.getPrivateKeyString())}
|
||||
value={stripHexPrefix(props.privateKey)}
|
||||
aria-label={translate('x_PrivKey')}
|
||||
aria-describedby="x_PrivKeyDesc"
|
||||
type="text"
|
||||
|
@ -28,7 +29,7 @@ const PaperWallet: React.SFC<Props> = props => (
|
|||
{/* Download Paper Wallet */}
|
||||
<h1 className="GenPaper-title">{translate('x_Print')}</h1>
|
||||
<div className="GenPaper-paper">
|
||||
<PrintableWallet wallet={props.wallet} />
|
||||
<PrintableWallet address={props.keystore.address} privateKey={props.privateKey} />
|
||||
</div>
|
||||
|
||||
{/* Warning */}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { fromPrivateKey, fromEthSale } from 'ethereumjs-wallet';
|
|||
import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty';
|
||||
import { signWrapper } from './helpers';
|
||||
import { decryptPrivKey } from 'libs/decrypt';
|
||||
import { fromV3 } from 'libs/web-workers/scrypt-wrapper';
|
||||
import { fromV3 } from 'libs/web-workers';
|
||||
import Web3Wallet from './web3';
|
||||
import AddressOnlyWallet from './address';
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { IFullWallet, fromPrivateKey } from 'ethereumjs-wallet';
|
||||
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,
|
||||
password: string,
|
||||
nonStrict: boolean
|
||||
): Promise<IFullWallet> => {
|
||||
): Promise<IFullWallet> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const scryptWorker = new Worker();
|
||||
scryptWorker.postMessage({ keystore, password, nonStrict });
|
||||
scryptWorker.onmessage = event => {
|
||||
const data: string = event.data;
|
||||
const worker = new Worker();
|
||||
worker.postMessage({ keystore, password, nonStrict });
|
||||
worker.onmessage = (ev: MessageEvent) => {
|
||||
const data = ev.data;
|
||||
try {
|
||||
const wallet = fromPrivateKey(toBuffer(data));
|
||||
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';
|
||||
|
||||
const scryptWorker: Worker = self as any;
|
||||
const worker: Worker = self as any;
|
||||
interface DecryptionParameters {
|
||||
keystore: string;
|
||||
password: string;
|
||||
nonStrict: boolean;
|
||||
}
|
||||
|
||||
scryptWorker.onmessage = (event: MessageEvent) => {
|
||||
worker.onmessage = (event: MessageEvent) => {
|
||||
const info: DecryptionParameters = event.data;
|
||||
try {
|
||||
const rawKeystore: IFullWallet = fromV3(info.keystore, info.password, info.nonStrict);
|
||||
scryptWorker.postMessage(rawKeystore.getPrivateKeyString());
|
||||
worker.postMessage(rawKeystore.getPrivateKeyString());
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
id: string;
|
||||
address: string;
|
||||
Crypto: {
|
||||
crypto: {
|
||||
ciphertext: string;
|
||||
cipherParams: {
|
||||
cipherparams: {
|
||||
iv: string;
|
||||
};
|
||||
cipher: string | 'aes-128-ctr';
|
||||
kdf: 'scrypt' | 'pbkdf2';
|
||||
kfdparams: IScryptKdfParams | IPbkdf2KdfParams;
|
||||
kdfparams: IScryptKdfParams | IPbkdf2KdfParams;
|
||||
mac: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -50,7 +50,8 @@
|
|||
"scryptsy": "2.0.0",
|
||||
"uuid": "3.2.1",
|
||||
"wallet-address-validator": "0.1.1",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
"whatwg-fetch": "2.0.3",
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/classnames": "2.2.3",
|
||||
|
@ -70,6 +71,7 @@
|
|||
"@types/redux-promise-middleware": "0.0.9",
|
||||
"@types/uuid": "3.4.3",
|
||||
"@types/webpack-env": "1.13.4",
|
||||
"@types/zxcvbn": "4.4.0",
|
||||
"autodll-webpack-plugin": "0.3.8",
|
||||
"awesome-typescript-loader": "3.4.1",
|
||||
"babel-minify-webpack-plugin": "0.2.0",
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
import { Wei } from 'libs/units';
|
||||
import { changeNodeIntent, web3UnsetNode } from 'actions/config';
|
||||
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 { getNodeLib, getOffline } from 'selectors/config';
|
||||
import { getWalletInst, getWalletConfigTokens } from 'selectors/wallet';
|
||||
|
@ -36,7 +36,7 @@ import Web3Node from 'libs/nodes/web3';
|
|||
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
|
||||
import { showNotification } from 'actions/notifications';
|
||||
import translate from 'translations';
|
||||
import { IFullWallet, fromV3 } from 'ethereumjs-wallet';
|
||||
import { IFullWallet, IV3Wallet, fromV3 } from 'ethereumjs-wallet';
|
||||
|
||||
// init module
|
||||
configuredStore.getState();
|
||||
|
@ -59,11 +59,11 @@ const token2: Token = {
|
|||
};
|
||||
const tokens = [token1, token2];
|
||||
|
||||
const utcKeystore = {
|
||||
const utcKeystore: IV3Wallet = {
|
||||
version: 3,
|
||||
id: 'cb788af4-993d-43ad-851b-0d2031e52c61',
|
||||
address: '25a24679f35e447f778cf54a3823facf39904a63',
|
||||
Crypto: {
|
||||
crypto: {
|
||||
ciphertext: '4193915c560835d00b2b9ff5dd20f3e13793b2a3ca8a97df649286063f27f707',
|
||||
cipherparams: {
|
||||
iv: 'dccb8c009b11d1c6226ba19b557dce4c'
|
||||
|
@ -73,7 +73,7 @@ const utcKeystore = {
|
|||
kdfparams: {
|
||||
dklen: 32,
|
||||
salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab',
|
||||
n: N_FACTOR,
|
||||
n: 1024,
|
||||
r: 8,
|
||||
p: 1
|
||||
},
|
||||
|
|
|
@ -69,7 +69,6 @@ module.exports = {
|
|||
'redux-promise-middleware',
|
||||
'redux-saga',
|
||||
'scryptsy',
|
||||
'store2',
|
||||
'uuid',
|
||||
'wallet-address-validator',
|
||||
'whatwg-fetch'
|
||||
|
|
Loading…
Reference in New Issue