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:
parent
4f48eee99e
commit
818ad9fef5
|
@ -11,6 +11,7 @@ import Swap from 'containers/Tabs/Swap';
|
|||
import ViewWallet from 'containers/Tabs/ViewWallet';
|
||||
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
|
||||
import BroadcastTx from 'containers/Tabs/BroadcastTx';
|
||||
import RestoreKeystore from 'containers/Tabs/RestoreKeystore';
|
||||
|
||||
// TODO: fix this
|
||||
interface Props {
|
||||
|
@ -33,12 +34,12 @@ export default class Root extends Component<Props, {}> {
|
|||
<Route path="/send-transaction" component={SendTransaction} />
|
||||
<Route path="/contracts" component={Contracts} />
|
||||
<Route path="/ens" component={ENS} />
|
||||
<Route path="/utilities" component={RestoreKeystore} />
|
||||
<Route
|
||||
path="/sign-and-verify-message"
|
||||
component={SignAndVerifyMessage}
|
||||
/>
|
||||
<Route path="/pushTx" component={BroadcastTx} />
|
||||
|
||||
<LegacyRoutes />
|
||||
</div>
|
||||
</Router>
|
||||
|
|
|
@ -8,6 +8,7 @@ const tabs = [
|
|||
name: 'NAV_GenerateWallet',
|
||||
to: '/'
|
||||
},
|
||||
|
||||
{
|
||||
name: 'NAV_SendEther',
|
||||
to: 'send-transaction'
|
||||
|
@ -36,6 +37,10 @@ const tabs = [
|
|||
name: 'Broadcast Transaction',
|
||||
to: 'pushTx'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Utilities',
|
||||
to: 'utilities'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Help',
|
||||
to: 'https://myetherwallet.groovehq.com/help_center',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { isValidEncryptedPrivKey, isValidPrivKey } from 'libs/validators';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import React, { Component } from 'react';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
|
||||
|
@ -8,13 +9,6 @@ export interface PrivateKeyValue {
|
|||
valid: boolean;
|
||||
}
|
||||
|
||||
function fixPkey(key) {
|
||||
if (key.indexOf('0x') === 0) {
|
||||
return key.slice(2);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
interface Validated {
|
||||
fixedPkey: string;
|
||||
isValidPkey: boolean;
|
||||
|
@ -23,7 +17,7 @@ interface Validated {
|
|||
}
|
||||
|
||||
function validatePkeyAndPass(pkey: string, pass: string): Validated {
|
||||
const fixedPkey = fixPkey(pkey);
|
||||
const fixedPkey = stripHexPrefix(pkey);
|
||||
const validPkey = isValidPrivKey(fixedPkey);
|
||||
const validEncPkey = isValidEncryptedPrivKey(fixedPkey);
|
||||
const isValidPkey = validPkey || validEncPkey;
|
||||
|
@ -58,15 +52,13 @@ export default class PrivateKeyDecrypt extends Component {
|
|||
return (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<div id="selectedTypeKey">
|
||||
<h4>
|
||||
{translate('ADD_Radio_3')}
|
||||
</h4>
|
||||
<h4>{translate('ADD_Radio_3')}</h4>
|
||||
<div className="form-group">
|
||||
<textarea
|
||||
id="aria-private-key"
|
||||
className={`form-control ${isValidPkey
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
className={`form-control ${
|
||||
isValidPkey ? 'is-valid' : 'is-invalid'
|
||||
}`}
|
||||
value={key}
|
||||
onChange={this.onPkeyChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
|
@ -75,22 +67,21 @@ export default class PrivateKeyDecrypt extends Component {
|
|||
/>
|
||||
</div>
|
||||
{isValidPkey &&
|
||||
isPassRequired &&
|
||||
isPassRequired && (
|
||||
<div className="form-group">
|
||||
<p>
|
||||
{translate('ADD_Label_3')}
|
||||
</p>
|
||||
<p>{translate('ADD_Label_3')}</p>
|
||||
<input
|
||||
className={`form-control ${password.length > 0
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
className={`form-control ${
|
||||
password.length > 0 ? 'is-valid' : 'is-invalid'
|
||||
}`}
|
||||
value={password}
|
||||
onChange={this.onPasswordChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={translateRaw('x_Password')}
|
||||
type="password"
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import { networkIdToName } from 'libs/values';
|
|||
export const languages = require('./languages.json');
|
||||
// Displays in the header
|
||||
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.
|
||||
// Type can be primary, warning, danger, success, or info.
|
||||
|
|
|
@ -7,6 +7,7 @@ import translate from 'translations';
|
|||
import { makeBlob } from 'utils/blob';
|
||||
import './DownloadWallet.scss';
|
||||
import Template from './Template';
|
||||
import { N_FACTOR } from 'config/data';
|
||||
|
||||
interface Props {
|
||||
wallet: IFullWallet;
|
||||
|
@ -47,7 +48,7 @@ export default class DownloadWallet extends Component<Props, State> {
|
|||
role="button"
|
||||
className="DlWallet-download btn btn-primary btn-lg"
|
||||
aria-label="Download Keystore File (UTC / JSON · Recommended · Encrypted)"
|
||||
aria-describedby="x_KeystoreDesc"
|
||||
aria-describedby={translate('x_KeystoreDesc')}
|
||||
download={filename}
|
||||
href={this.getBlob()}
|
||||
onClick={this.handleDownloadKeystore}
|
||||
|
@ -130,7 +131,7 @@ export default class DownloadWallet extends Component<Props, State> {
|
|||
this.state.hasDownloadedWallet && this.props.continueToPaper();
|
||||
|
||||
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);
|
||||
this.setState({ keystore });
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,5 +1,6 @@
|
|||
import { toChecksumAddress } from 'ethereumjs-util';
|
||||
import { toChecksumAddress, isValidPrivate } from 'ethereumjs-util';
|
||||
import { RawTransaction } from 'libs/transaction';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import WalletAddressValidator from 'wallet-address-validator';
|
||||
import { normalise } from './ens';
|
||||
import { Validator } from 'jsonschema';
|
||||
|
@ -88,9 +89,15 @@ function validateEtherAddress(address: string): boolean {
|
|||
|
||||
export function isValidPrivKey(privkey: string | Buffer): boolean {
|
||||
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) {
|
||||
return privkey.length === 32;
|
||||
return privkey.length === 32 && isValidPrivate(privkey);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
"NAV_Swap": "Swap ",
|
||||
"NAV_ViewWallet": "View Wallet Info ",
|
||||
"NAV_YourWallets": "Your Wallets ",
|
||||
"NAV_Utilities": "Utilities",
|
||||
"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_Address": "Your Address ",
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import {
|
||||
isValidBTCAddress,
|
||||
isValidETHAddress,
|
||||
isValidPath
|
||||
isValidPath,
|
||||
isValidPrivKey
|
||||
} from '../../common/libs/validators';
|
||||
|
||||
const VALID_BTC_ADDRESS = '1MEWT2SGbqtz6mPCgFcnea8XmWV5Z4Wc6';
|
||||
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', () => {
|
||||
it('should validate correct BTC address as true', () => {
|
||||
|
@ -25,11 +33,22 @@ describe('Validator', () => {
|
|||
isValidETHAddress('nonsense' + VALID_ETH_ADDRESS + 'nonsense')
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should validate a correct DPath as true', () => {
|
||||
expect(isValidPath("m/44'/60'/0'/0")).toBeTruthy();
|
||||
});
|
||||
it('should validate an incorrect DPath as false', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { Wei } from 'libs/units';
|
||||
import { changeNodeIntent } from 'actions/config';
|
||||
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 { getNetworkConfig, getNodeLib } from 'selectors/config';
|
||||
import { getTokens, getWalletInst } from 'selectors/wallet';
|
||||
|
@ -65,7 +65,7 @@ const utcKeystore = {
|
|||
kdfparams: {
|
||||
dklen: 32,
|
||||
salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab',
|
||||
n: 1024,
|
||||
n: N_FACTOR,
|
||||
r: 8,
|
||||
p: 1
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue