Add Private key to V3 keystore functionality (#336)

* setup components, reducers, actions, and added routes

* removed redux, using local state and ethereumjs-wallet

* added validation and state reset

* added visibility options and changed btn colors

* updated isValidPrivKey and updated some components to stateless functional comp

* componentize input and add placeholder message

* removed cn from KeystoreDetails

* adds isValidPrivate to buffer check and min pw length to 0

* remove packagelock to fix merge conflict

* added utilities tab removed keystore tab

* adds fixpkey in validators and uses it across two components

* added checksum removal and btn css fixes

* Fixed en.json formatting - also removed fixedPkey

* Added unit tests for isValidPrivKey

* add runtime checks and rename stripHexPrefix to strippedPrivateKey

* switch back to stripHexPrefix

* Add constant for n-factor

* enforce 9 char minimum
This commit is contained in:
Eddie Wang 2017-11-30 15:16:30 -05:00 committed by Daniel Ternyak
parent 4f48eee99e
commit 818ad9fef5
14 changed files with 336 additions and 40 deletions

View File

@ -11,6 +11,7 @@ import Swap from 'containers/Tabs/Swap';
import ViewWallet from 'containers/Tabs/ViewWallet'; import ViewWallet from 'containers/Tabs/ViewWallet';
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage'; import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
import BroadcastTx from 'containers/Tabs/BroadcastTx'; import BroadcastTx from 'containers/Tabs/BroadcastTx';
import RestoreKeystore from 'containers/Tabs/RestoreKeystore';
// TODO: fix this // TODO: fix this
interface Props { interface Props {
@ -33,12 +34,12 @@ export default class Root extends Component<Props, {}> {
<Route path="/send-transaction" component={SendTransaction} /> <Route path="/send-transaction" component={SendTransaction} />
<Route path="/contracts" component={Contracts} /> <Route path="/contracts" component={Contracts} />
<Route path="/ens" component={ENS} /> <Route path="/ens" component={ENS} />
<Route path="/utilities" component={RestoreKeystore} />
<Route <Route
path="/sign-and-verify-message" path="/sign-and-verify-message"
component={SignAndVerifyMessage} component={SignAndVerifyMessage}
/> />
<Route path="/pushTx" component={BroadcastTx} /> <Route path="/pushTx" component={BroadcastTx} />
<LegacyRoutes /> <LegacyRoutes />
</div> </div>
</Router> </Router>

View File

@ -8,6 +8,7 @@ const tabs = [
name: 'NAV_GenerateWallet', name: 'NAV_GenerateWallet',
to: '/' to: '/'
}, },
{ {
name: 'NAV_SendEther', name: 'NAV_SendEther',
to: 'send-transaction' to: 'send-transaction'
@ -36,6 +37,10 @@ const tabs = [
name: 'Broadcast Transaction', name: 'Broadcast Transaction',
to: 'pushTx' to: 'pushTx'
}, },
{
name: 'NAV_Utilities',
to: 'utilities'
},
{ {
name: 'NAV_Help', name: 'NAV_Help',
to: 'https://myetherwallet.groovehq.com/help_center', to: 'https://myetherwallet.groovehq.com/help_center',

View File

@ -1,4 +1,5 @@
import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators'; import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import React, { Component } from 'react'; import React, { Component } from 'react';
import translate, { translateRaw } from 'translations'; import translate, { translateRaw } from 'translations';
@ -8,13 +9,6 @@ export interface PrivateKeyValue {
valid: boolean; valid: boolean;
} }
function fixPkey(key) {
if (key.indexOf('0x') === 0) {
return key.slice(2);
}
return key;
}
interface Validated { interface Validated {
fixedPkey: string; fixedPkey: string;
isValidPkey: boolean; isValidPkey: boolean;
@ -23,7 +17,7 @@ interface Validated {
} }
function validatePkeyAndPass(pkey: string, pass: string): Validated { function validatePkeyAndPass(pkey: string, pass: string): Validated {
const fixedPkey = fixPkey(pkey); const fixedPkey = stripHexPrefix(pkey);
const validPkey = isValidPrivKey(fixedPkey); const validPkey = isValidPrivKey(fixedPkey);
const validEncPkey = isValidEncryptedPrivKey(fixedPkey); const validEncPkey = isValidEncryptedPrivKey(fixedPkey);
const isValidPkey = validPkey || validEncPkey; const isValidPkey = validPkey || validEncPkey;
@ -58,15 +52,13 @@ export default class PrivateKeyDecrypt extends Component {
return ( return (
<section className="col-md-4 col-sm-6"> <section className="col-md-4 col-sm-6">
<div id="selectedTypeKey"> <div id="selectedTypeKey">
<h4> <h4>{translate('ADD_Radio_3')}</h4>
{translate('ADD_Radio_3')}
</h4>
<div className="form-group"> <div className="form-group">
<textarea <textarea
id="aria-private-key" id="aria-private-key"
className={`form-control ${isValidPkey className={`form-control ${
? 'is-valid' isValidPkey ? 'is-valid' : 'is-invalid'
: 'is-invalid'}`} }`}
value={key} value={key}
onChange={this.onPkeyChange} onChange={this.onPkeyChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
@ -75,22 +67,21 @@ export default class PrivateKeyDecrypt extends Component {
/> />
</div> </div>
{isValidPkey && {isValidPkey &&
isPassRequired && isPassRequired && (
<div className="form-group"> <div className="form-group">
<p> <p>{translate('ADD_Label_3')}</p>
{translate('ADD_Label_3')}
</p>
<input <input
className={`form-control ${password.length > 0 className={`form-control ${
? 'is-valid' password.length > 0 ? 'is-valid' : 'is-invalid'
: 'is-invalid'}`} }`}
value={password} value={password}
onChange={this.onPasswordChange} onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
placeholder={translateRaw('x_Password')} placeholder={translateRaw('x_Password')}
type="password" type="password"
/> />
</div>} </div>
)}
</div> </div>
</section> </section>
); );

View File

@ -3,6 +3,7 @@ import { networkIdToName } from 'libs/values';
export const languages = require('./languages.json'); export const languages = require('./languages.json');
// Displays in the header // Displays in the header
export const VERSION = '4.0.0 (Alpha 0.0.4)'; export const VERSION = '4.0.0 (Alpha 0.0.4)';
export const N_FACTOR = 1024;
// 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.

View File

@ -7,6 +7,7 @@ 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/data';
interface Props { interface Props {
wallet: IFullWallet; wallet: IFullWallet;
@ -47,7 +48,7 @@ export default class DownloadWallet extends Component<Props, State> {
role="button" role="button"
className="DlWallet-download btn btn-primary btn-lg" className="DlWallet-download btn btn-primary btn-lg"
aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)" aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)"
aria-describedby="x_KeystoreDesc" aria-describedby={translate('x_KeystoreDesc')}
download={filename} download={filename}
href={this.getBlob()} href={this.getBlob()}
onClick={this.handleDownloadKeystore} onClick={this.handleDownloadKeystore}
@ -130,7 +131,7 @@ export default class DownloadWallet extends Component<Props, State> {
this.state.hasDownloadedWallet && this.props.continueToPaper(); this.state.hasDownloadedWallet && this.props.continueToPaper();
private setWallet(wallet: IFullWallet, password: string) { private setWallet(wallet: IFullWallet, password: string) {
const keystore = wallet.toV3(password, { n: 1024 }); const keystore = wallet.toV3(password, { n: N_FACTOR });
keystore.address = toChecksumAddress(keystore.address); keystore.address = toChecksumAddress(keystore.address);
this.setState({ keystore }); this.setState({ keystore });
} }

View File

@ -0,0 +1,23 @@
@import 'common/sass/variables';
.KeystoreDetails {
&-title {
margin: $space auto $space * 2.5;
}
&-password,
&-key {
max-width: 40rem;
margin: 0 auto $space;
&-label {
margin-bottom: $space;
}
}
&-submit,
&-download {
max-width: 16rem;
margin: 0 auto $space * 3;
}
}

View File

@ -0,0 +1,177 @@
import React, { Component } from 'react';
import Template from './Template';
import KeystoreInput from './KeystoreInput';
import { fromPrivateKey, IFullWallet, fromV3 } from 'ethereumjs-wallet';
import { makeBlob } from 'utils/blob';
import { isValidPrivKey } from 'libs/validators';
import { stripHexPrefix } from 'libs/values';
import translate from 'translations';
import './KeystoreDetails.scss';
import { N_FACTOR } from 'config/data';
interface State {
secretKey: string;
password: string;
fileName: string;
isPasswordVisible: boolean;
isPrivateKeyVisible: boolean;
wallet: IFullWallet | null | undefined;
}
const initialState: State = {
secretKey: '',
password: '',
isPasswordVisible: false,
isPrivateKeyVisible: false,
fileName: '',
wallet: null
};
const minLength = min => value => value && value.length >= min;
const minLength9 = minLength(9);
class KeystoreDetails extends Component<{}, State> {
public state = initialState;
public componentWillUnmount() {
this.resetState();
}
public render() {
const {
secretKey,
isPasswordVisible,
isPrivateKeyVisible,
password,
wallet,
fileName
} = this.state;
const privateKey = stripHexPrefix(secretKey);
const privateKeyValid = isValidPrivKey(privateKey);
const content = (
<div className="KeystoreDetails">
<div>
<label className="KeystoreDetails-key">
<h4 className="KeystoreDetails-label">Private Key</h4>
<KeystoreInput
isValid={privateKeyValid}
isVisible={isPrivateKeyVisible}
name="secretKey"
value={secretKey}
handleInput={this.handleInput}
placeholder="Enter your saved private key here"
handleToggle={this.togglePrivateKey}
/>
</label>
</div>
<div>
<label className="KeystoreDetails-password">
<h4 className="KeystoreDetails-label">Password</h4>
<KeystoreInput
isValid={minLength9(password)}
isVisible={isPasswordVisible}
name="password"
value={password}
placeholder="Enter your encryption password here."
handleInput={this.handleInput}
handleToggle={this.togglePassword}
/>
</label>
</div>
{!wallet ? (
<button
onClick={this.handleKeystoreGeneration}
className="KeystoreDetails-submit btn btn-primary btn-block"
disabled={!privateKeyValid || !minLength9(password)}
>
Generate Keystore
</button>
) : this.runtimeKeystoreCheck() ? (
<a
onClick={this.resetState}
href={this.getBlob()}
className="KeystoreDetails-download btn btn-success btn-block"
aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)"
aria-describedby={translate('x_KeystoreDesc')}
download={fileName}
>
Download Keystore
</a>
) : (
<p>
Error generating a valid keystore that matches your private key. In
order to protect our users, if our runtime check fails, we prevent
you from downloading a potentially corrupted wallet.
</p>
)}
</div>
);
return (
<div>
<Template title="Regenerate Keystore File" content={content} />
</div>
);
}
private togglePrivateKey = () => {
this.setState({
isPrivateKeyVisible: !this.state.isPrivateKeyVisible
});
};
private togglePassword = () => {
this.setState({
isPasswordVisible: !this.state.isPasswordVisible
});
};
private resetState = () => {
this.setState(initialState);
};
private handleKeystoreGeneration = () => {
const { secretKey } = this.state;
const removeChecksumPkey = stripHexPrefix(secretKey);
const keyBuffer = Buffer.from(removeChecksumPkey, 'hex');
const wallet = fromPrivateKey(keyBuffer);
const fileName = wallet.getV3Filename();
this.setState({
wallet,
fileName
});
};
private handleInput = (e: React.FormEvent<HTMLInputElement>) => {
const name = e.currentTarget.name;
const value = e.currentTarget.value;
if (name === 'secretKey') {
this.setState({
wallet: null
});
}
this.setState({ [name as any]: value });
};
private runtimeKeystoreCheck(): boolean {
const { wallet, password, secretKey } = this.state;
if (wallet) {
const keystore = wallet.toV3(password, { n: N_FACTOR });
const backToWallet = fromV3(keystore, password, true);
if (stripHexPrefix(backToWallet.getPrivateKeyString()) === secretKey) {
return true;
}
}
return false;
}
private getBlob() {
const { wallet, password } = this.state;
if (wallet) {
const keystore = wallet.toV3(password, { n: N_FACTOR });
return makeBlob('text/json;charset=UTF-8', keystore);
}
}
}
export default KeystoreDetails;

View File

@ -0,0 +1,43 @@
import React from 'react';
import classnames from 'classnames';
interface Props {
isValid: boolean;
isVisible: boolean;
name: string;
value: string;
placeholder: string;
handleInput(e: React.FormEvent<HTMLInputElement>): void;
handleToggle(): void;
}
const KeystoreInput: React.SFC<Props> = ({
isValid,
isVisible,
handleInput,
name,
value,
placeholder,
handleToggle
}) => (
<div className="input-group">
<input
className={classnames(
'form-control',
isValid ? 'is-valid' : 'is-invalid'
)}
type={isVisible ? 'text' : 'password'}
name={name}
placeholder={placeholder}
value={value}
onChange={handleInput}
/>
<span
onClick={handleToggle}
role="button"
className="input-group-addon eye"
/>
</div>
);
export default KeystoreInput;

View File

@ -0,0 +1,16 @@
import React from 'react';
interface Props {
content: React.ReactElement<any>;
title: string;
}
const RestoreKeystoreTemplate: React.SFC<Props> = ({ title, content }) => (
<div className="Tab-content">
<div className="Tab-content-pane text-center">
<h1>{title}</h1>
{content}
</div>
</div>
);
export default RestoreKeystoreTemplate;

View File

@ -0,0 +1,11 @@
import React from 'react';
import TabSection from 'containers/TabSection';
import KeystoreDetails from './components/KeystoreDetails';
const RestoreKeystore: React.SFC<{}> = () => (
<TabSection>
<KeystoreDetails />
</TabSection>
);
export default RestoreKeystore;

View File

@ -1,5 +1,6 @@
import { toChecksumAddress } from 'ethereumjs-util'; import { toChecksumAddress, isValidPrivate } from 'ethereumjs-util';
import { RawTransaction } from 'libs/transaction'; import { RawTransaction } from 'libs/transaction';
import { stripHexPrefix } from 'libs/values';
import WalletAddressValidator from 'wallet-address-validator'; import WalletAddressValidator from 'wallet-address-validator';
import { normalise } from './ens'; import { normalise } from './ens';
import { Validator } from 'jsonschema'; import { Validator } from 'jsonschema';
@ -88,9 +89,15 @@ function validateEtherAddress(address: string): boolean {
export function isValidPrivKey(privkey: string | Buffer): boolean { export function isValidPrivKey(privkey: string | Buffer): boolean {
if (typeof privkey === 'string') { if (typeof privkey === 'string') {
return privkey.length === 64; const strippedKey = stripHexPrefix(privkey);
const initialCheck = strippedKey.length === 64;
if (initialCheck) {
const keyBuffer = Buffer.from(strippedKey, 'hex');
return isValidPrivate(keyBuffer);
}
return false;
} else if (privkey instanceof Buffer) { } else if (privkey instanceof Buffer) {
return privkey.length === 32; return privkey.length === 32 && isValidPrivate(privkey);
} else { } else {
return false; return false;
} }

View File

@ -72,6 +72,7 @@
"NAV_Swap": "Swap ", "NAV_Swap": "Swap ",
"NAV_ViewWallet": "View Wallet Info ", "NAV_ViewWallet": "View Wallet Info ",
"NAV_YourWallets": "Your Wallets ", "NAV_YourWallets": "Your Wallets ",
"NAV_Utilities": "Utilities",
"x_Access": "Access ", "x_Access": "Access ",
"x_AddessDesc": "Your Address can also be known as you `Account #` or your `Public Key`. It is what you share with people so they can send you Ether or Tokens. Find the colorful address icon. Make sure it matches your paper wallet & whenever you enter your address somewhere.", "x_AddessDesc": "Your Address can also be known as you `Account #` or your `Public Key`. It is what you share with people so they can send you Ether or Tokens. Find the colorful address icon. Make sure it matches your paper wallet & whenever you enter your address somewhere.",
"x_Address": "Your Address ", "x_Address": "Your Address ",

View File

@ -1,11 +1,19 @@
import { import {
isValidBTCAddress, isValidBTCAddress,
isValidETHAddress, isValidETHAddress,
isValidPath isValidPath,
isValidPrivKey
} from '../../common/libs/validators'; } from '../../common/libs/validators';
const VALID_BTC_ADDRESS = '1MEWT2SGbqtz6mPCgFcnea8XmWV5Z4Wc6'; const VALID_BTC_ADDRESS = '1MEWT2SGbqtz6mPCgFcnea8XmWV5Z4Wc6';
const VALID_ETH_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8'; const VALID_ETH_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8';
const VALID_ETH_PRIVATE_KEY =
'3f4fd89ea4970cc77bfd2d07a95786575ea62e183857afe6301578e1a3c5c782';
const INVALID_ETH_PRIVATE_KEY =
'3f4fd89ea4970cc77bfd2d07a95786575ea62e183857afe6301578e1a3c5ZZZZ';
const VALID_ETH_PRIVATE_BUFFER = Buffer.from(VALID_ETH_PRIVATE_KEY, 'hex');
const VALID_ETH_PRIVATE_0X =
'0x3f4fd89ea4970cc77bfd2d07a95786575ea62e183857afe6301578e1a3c5c782';
describe('Validator', () => { describe('Validator', () => {
it('should validate correct BTC address as true', () => { it('should validate correct BTC address as true', () => {
@ -25,11 +33,22 @@ describe('Validator', () => {
isValidETHAddress('nonsense' + VALID_ETH_ADDRESS + 'nonsense') isValidETHAddress('nonsense' + VALID_ETH_ADDRESS + 'nonsense')
).toBeFalsy(); ).toBeFalsy();
}); });
it('should validate a correct DPath as true', () => { it('should validate a correct DPath as true', () => {
expect(isValidPath("m/44'/60'/0'/0")).toBeTruthy(); expect(isValidPath("m/44'/60'/0'/0")).toBeTruthy();
}); });
it('should validate an incorrect DPath as false', () => { it('should validate an incorrect DPath as false', () => {
expect(isValidPath('m/44/60/0/0')).toBeFalsy(); expect(isValidPath('m/44/60/0/0')).toBeFalsy();
}); });
it('should validate private key as true', () => {
expect(isValidPrivKey(VALID_ETH_PRIVATE_KEY)).toBeTruthy();
});
it('should validate invalid private key as false', () => {
expect(isValidPrivKey(INVALID_ETH_PRIVATE_KEY)).toBeFalsy();
});
it('should validate 0x private keys as true', () => {
expect(isValidPrivKey(VALID_ETH_PRIVATE_0X)).toBeTruthy();
});
it('should validate private key buffer type as true', () => {
expect(isValidPrivKey(VALID_ETH_PRIVATE_BUFFER)).toBeTruthy();
});
}); });

View File

@ -12,7 +12,7 @@ import {
import { Wei } from 'libs/units'; import { Wei } from 'libs/units';
import { changeNodeIntent } from 'actions/config'; import { changeNodeIntent } from 'actions/config';
import { INode } from 'libs/nodes/INode'; import { INode } from 'libs/nodes/INode';
import { initWeb3Node, Token } from 'config/data'; import { initWeb3Node, Token, N_FACTOR } from 'config/data';
import { apply, call, cps, fork, put, select } from 'redux-saga/effects'; import { apply, call, cps, fork, put, select } from 'redux-saga/effects';
import { getNetworkConfig, getNodeLib } from 'selectors/config'; import { getNetworkConfig, getNodeLib } from 'selectors/config';
import { getTokens, getWalletInst } from 'selectors/wallet'; import { getTokens, getWalletInst } from 'selectors/wallet';
@ -65,7 +65,7 @@ const utcKeystore = {
kdfparams: { kdfparams: {
dklen: 32, dklen: 32,
salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab', salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab',
n: 1024, n: N_FACTOR,
r: 8, r: 8,
p: 1 p: 1
}, },