Wallet Decrypt - Mnemonic (#180)

* add 'bip39' package

* add mnemonic decrypt, wallet wrapper

* add mnemonic path config

* add mnemonic support to deterministic components

* add mnemonic support

* accomodate for ledger ETH path

* remove comments

* update comments regarding path length

* rename modal open handler

* make several props optional

* add basic tests for mnemonic decrypt

* make flow happy, add user error notifications

* convert dpaths to js file, update references

* add ledger path to test

* Trezor DPath Fix (#196)

* Match v3 more closely.

* Require wallet index on deterministic wallets, update trezor to send index.

* remove redundent stripAndLower function and rename existing stripHex to stripHexPrefixAndLower
This commit is contained in:
skubakdj 2017-09-15 15:29:38 -04:00 committed by Daniel Ternyak
parent 7a460960d7
commit c88e96d603
21 changed files with 488 additions and 101 deletions

View File

@ -14,18 +14,20 @@ export type DeterministicWalletData = {
export type GetDeterministicWalletsAction = {
type: 'DW_GET_WALLETS',
payload: {
seed: ?string,
dPath: string,
publicKey: string,
chainCode: string,
publicKey: ?string,
chainCode: ?string,
limit: number,
offset: number
}
};
export type GetDeterministicWalletsArgs = {
seed: ?string,
dPath: string,
publicKey: string,
chainCode: string,
publicKey: ?string,
chainCode: ?string,
limit?: number,
offset?: number
};
@ -33,10 +35,11 @@ export type GetDeterministicWalletsArgs = {
export function getDeterministicWallets(
args: GetDeterministicWalletsArgs
): GetDeterministicWalletsAction {
const { dPath, publicKey, chainCode, limit, offset } = args;
const { seed, dPath, publicKey, chainCode, limit, offset } = args;
return {
type: 'DW_GET_WALLETS',
payload: {
seed,
dPath,
publicKey,
chainCode,

View File

@ -43,6 +43,28 @@ export function unlockKeystore(
};
}
/*** Unlock Mnemonic ***/
export type MnemonicUnlockParams = {
phrase: string,
pass: string,
path: string,
address: string
};
export type UnlockMnemonicAction = {
type: 'WALLET_UNLOCK_MNEMONIC',
payload: MnemonicUnlockParams
};
export function unlockMnemonic(
value: MnemonicUnlockParams
): UnlockMnemonicAction {
return {
type: 'WALLET_UNLOCK_MNEMONIC',
payload: value
};
}
/*** Set Wallet ***/
export type SetWalletAction = {
type: 'WALLET_SET',

View File

@ -36,15 +36,17 @@ type Props = {
walletType: ?string,
dPath: string,
dPaths: { label: string, value: string }[],
publicKey: string,
chainCode: string,
publicKey: ?string,
chainCode: ?string,
seed: ?string,
onCancel: () => void,
onConfirmAddress: string => void,
onConfirmAddress: (string, number) => void,
onPathChange: string => void
};
type State = {
selectedAddress: string,
selectedAddrIndex: number,
isCustomPath: boolean,
customPath: string,
page: number
@ -54,6 +56,7 @@ class DeterministicWalletsModal extends React.Component {
props: Props;
state: State = {
selectedAddress: '',
selectedAddrIndex: 0,
isCustomPath: false,
customPath: '',
page: 0
@ -64,20 +67,23 @@ class DeterministicWalletsModal extends React.Component {
}
componentWillReceiveProps(nextProps) {
const { publicKey, chainCode } = this.props;
const { publicKey, chainCode, seed, dPath } = this.props;
if (
nextProps.publicKey !== publicKey ||
nextProps.chainCode !== chainCode
nextProps.chainCode !== chainCode ||
nextProps.dPath !== dPath ||
nextProps.seed !== seed
) {
this._getAddresses(nextProps);
}
}
_getAddresses(props: Props = this.props) {
const { dPath, publicKey, chainCode } = props;
const { dPath, publicKey, chainCode, seed } = props;
if (dPath && publicKey && chainCode && isValidPath(dPath)) {
if (dPath && ((publicKey && chainCode) || seed) && isValidPath(dPath)) {
this.props.getDeterministicWallets({
seed,
dPath,
publicKey,
chainCode,
@ -116,12 +122,15 @@ class DeterministicWalletsModal extends React.Component {
_handleConfirmAddress = () => {
if (this.state.selectedAddress) {
this.props.onConfirmAddress(this.state.selectedAddress);
this.props.onConfirmAddress(
this.state.selectedAddress,
this.state.selectedAddrIndex
);
}
};
_selectAddress(selectedAddress) {
this.setState({ selectedAddress });
_selectAddress(selectedAddress, selectedAddrIndex) {
this.setState({ selectedAddress, selectedAddrIndex });
}
_nextPage = () => {
@ -148,7 +157,7 @@ class DeterministicWalletsModal extends React.Component {
return (
<tr
key={wallet.address}
onClick={this._selectAddress.bind(this, wallet.address)}
onClick={this._selectAddress.bind(this, wallet.address, wallet.index)}
>
<td>
{wallet.index + 1}

View File

@ -1,27 +1,129 @@
import React, { Component } from 'react';
import translate from 'translations';
import translate, { translateRaw } from 'translations';
import { validateMnemonic, mnemonicToSeed } from 'bip39';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import DPATHS from 'config/dpaths.js';
const DEFAULT_PATH = DPATHS.MNEMONIC[0].value;
type State = {
phrase: string,
pass: string,
seed: string,
dPath: string
};
export default class MnemonicDecrypt extends Component {
props: { onUnlock: any => void };
state: State = {
phrase: '',
pass: '',
seed: '',
dPath: DEFAULT_PATH
};
render() {
const { phrase, seed, dPath, pass } = this.state;
const isValidMnemonic = validateMnemonic(phrase);
return (
<section className="col-md-4 col-sm-6">
<div id="selectedUploadKey">
<h4>{translate('ADD_Radio_2_alt')}</h4>
<div id="selectedTypeKey">
<h4>
{translate('ADD_Radio_5')}
</h4>
<div className="form-group">
<input type="file" id="fselector" />
<a
className="btn-file marg-v-sm"
id="aria1"
tabIndex="0"
role="button"
>
{translate('ADD_Radio_2_short')}
</a>
<textarea
id="aria-private-key"
className={`form-control ${isValidMnemonic
? 'is-valid'
: 'is-invalid'}`}
value={phrase}
onChange={this.onMnemonicChange}
placeholder={translateRaw('x_Mnemonic')}
rows="4"
/>
</div>
<div className="form-group">
<p>Password (optional):</p>
<input
className="form-control"
value={pass}
onChange={this.onPasswordChange}
placeholder={translateRaw('x_Password')}
type="password"
/>
</div>
{isValidMnemonic &&
<div className="form-group">
<button
style={{ width: '100%' }}
onClick={this.onDWModalOpen}
className="btn btn-primary btn-lg"
>
{translate('Choose Address')}
</button>
</div>}
</div>
<DeterministicWalletsModal
isOpen={!!seed}
seed={seed}
dPath={dPath}
dPaths={DPATHS.MNEMONIC}
onCancel={this._handleCancel}
onConfirmAddress={this._handleUnlock}
onPathChange={this._handlePathChange}
walletType={translateRaw('x_Mnemonic')}
/>
</section>
);
}
onPasswordChange = (e: SyntheticInputEvent) => {
this.setState({ pass: e.target.value });
};
onMnemonicChange = (e: SyntheticInputEvent) => {
this.setState({ phrase: e.target.value });
};
onDWModalOpen = (e: SyntheticInputEvent) => {
const { phrase, pass } = this.state;
if (!validateMnemonic(phrase)) return;
try {
let seed = mnemonicToSeed(phrase.trim(), pass).toString('hex');
this.setState({ seed });
} catch (err) {
console.log(err);
}
};
_handleCancel = () => {
this.setState({ seed: '' });
};
_handlePathChange = (dPath: string) => {
this.setState({ dPath });
};
_handleUnlock = (address, index) => {
const { phrase, pass, dPath } = this.state;
this.props.onUnlock({
path: `${dPath}/${index}`,
pass,
phrase,
address
});
this.setState({
seed: '',
pass: '',
phrase: ''
});
};
}

View File

@ -5,7 +5,7 @@ import translate from 'translations';
import TrezorConnect from 'vendor/trezor-connect';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import TrezorWallet from 'libs/wallet/trezor';
import DPATHS from 'config/dpaths.json';
import DPATHS from 'config/dpaths.js';
const DEFAULT_PATH = DPATHS.TREZOR[0].value;
type State = {
@ -65,8 +65,8 @@ export default class TrezorDecrypt extends Component {
});
};
_handleUnlock = (address: string) => {
this.props.onUnlock(new TrezorWallet(address, this.state.dPath));
_handleUnlock = (address: string, index: number) => {
this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index));
};
render() {

View File

@ -9,7 +9,12 @@ import LedgerNanoSDecrypt from './LedgerNano';
import TrezorDecrypt from './Trezor';
import ViewOnlyDecrypt from './ViewOnly';
import map from 'lodash/map';
import { unlockPrivateKey, unlockKeystore, setWallet } from 'actions/wallet';
import {
unlockPrivateKey,
unlockKeystore,
unlockMnemonic,
setWallet
} from 'actions/wallet';
import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty';
@ -35,7 +40,8 @@ const WALLETS = {
'mnemonic-phrase': {
lid: 'x_Mnemonic',
component: MnemonicDecrypt,
disabled: true
initialParams: {},
unlock: unlockMnemonic
},
'ledger-nano-s': {
lid: 'x_Ledger',

50
common/config/dpaths.js Normal file
View File

@ -0,0 +1,50 @@
const ETH_DEFAULT = {
label: 'Default (ETH)',
value: "m/44'/60'/0'/0"
};
const ETH_TREZOR = {
label: 'TREZOR (ETH)',
value: "m/44'/60'/0'/0"
};
const ETH_LEDGER = {
label: 'Ledger (ETH)',
value: "m/44'/60'/0'"
};
const ETC_LEDGER = {
label: 'Ledger (ETC)',
value: "m/44'/60'/160720'/0'"
};
const ETC_TREZOR = {
label: 'TREZOR (ETC)',
value: "m/44'/61'/0'/0"
};
const TESTNET = {
label: 'Testnet',
value: "m/44'/1'/0'/0"
};
const EXPANSE = {
label: 'Expanse',
value: "m/44'/40'/0'/0"
};
const TREZOR = [ETH_TREZOR, ETC_TREZOR, TESTNET];
const MNEMONIC = [
ETH_DEFAULT,
ETH_LEDGER,
ETC_LEDGER,
ETC_TREZOR,
TESTNET,
EXPANSE
];
export default {
TREZOR,
MNEMONIC
};

View File

@ -1,16 +0,0 @@
{
"TREZOR": [
{
"label": "TREZOR (ETH)",
"value": "m/44'/60'/0'/0"
},
{
"label": "TREZOR (ETC)",
"value": "m/44'/61'/0'/0"
},
{
"label": "Testnet",
"value": "m/44'/1'/0'/0"
}
]
}

View File

@ -1,6 +1,11 @@
//@flow
import { createHash, createDecipheriv } from 'crypto';
import { validateMnemonic, mnemonicToSeed } from 'bip39';
import { fromMasterSeed } from 'hdkey';
import { stripHexPrefixAndLower } from 'libs/values';
import { privateToAddress } from 'ethereumjs-util';
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L230
export function decryptPrivKey(encprivkey: string, password: string): Buffer {
@ -66,3 +71,28 @@ export function evp_kdf(data: Buffer, salt: Buffer, opts: Object) {
export function decipherBuffer(decipher: Object, data: Buffer): Buffer {
return Buffer.concat([decipher.update(data), decipher.final()]);
}
export function decryptMnemonicToPrivKey(
phrase: string,
pass: string,
path: string,
address: string
): Buffer {
phrase = phrase.trim();
address = stripHexPrefixAndLower(address);
if (!validateMnemonic(phrase)) {
throw new Error('Invalid mnemonic');
}
const seed = mnemonicToSeed(phrase, pass);
const derived = fromMasterSeed(seed).derive(path);
const dPrivKey = derived.privateKey;
const dAddress = privateToAddress(dPrivKey).toString('hex');
if (dAddress !== address) {
throw new Error(`Derived ${dAddress}, expected ${address}`);
}
return dPrivKey;
}

View File

@ -4,7 +4,7 @@ import translate from 'translations';
import { padToEven, addHexPrefix, toChecksumAddress } from 'ethereumjs-util';
import { isValidETHAddress } from 'libs/validators';
import ERC20 from 'libs/erc20';
import { stripHex, valueToHex } from 'libs/values';
import { stripHexPrefixAndLower, valueToHex } from 'libs/values';
import { Wei, Ether, toTokenUnit } from 'libs/units';
import { RPCNode } from 'libs/nodes';
import { TransactionWithoutGas } from 'libs/messages';
@ -132,12 +132,12 @@ export async function generateCompleteTransactionFromRawTransaction(
}
// Taken from v3's `sanitizeHex`, ensures that the value is a %2 === 0
// prefix'd hex value.
const cleanHex = hex => addHexPrefix(padToEven(stripHex(hex)));
const cleanHex = hex => addHexPrefix(padToEven(stripHexPrefixAndLower(hex)));
const cleanedRawTx = {
nonce: cleanHex(nonce),
gasPrice: cleanHex(gasPrice.toString(16)),
gasLimit: cleanHex(gasLimit.toString(16)),
to: cleanHex(to),
to: toChecksumAddress(cleanHex(to)),
value: token ? '0x00' : cleanHex(value.toString(16)),
data: data ? cleanHex(data) : '',
chainId: chainId || 1

View File

@ -140,8 +140,11 @@ export function isValidRawTx(rawTx: RawTransaction): boolean {
return true;
}
// Full length deterministic wallet paths from BIP32
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
// Full length deterministic wallet paths from BIP44
// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
// normal path length is 4, ledger is the exception at 3
export function isValidPath(dPath: string) {
return dPath.split("'/").length === 4;
//TODO: use a regex to detect proper paths
const len = dPath.split("'/").length;
return len === 3 || len === 4;
}

View File

@ -1,7 +1,7 @@
// @flow
import { Ether } from 'libs/units';
export function stripHex(address: string): string {
export function stripHexPrefixAndLower(address: string): string {
return address.replace('0x', '').toLowerCase();
}

View File

@ -4,10 +4,12 @@ import type { IWallet } from './IWallet';
export default class DeterministicWallet implements IWallet {
address: string;
dPath: string;
index: number;
constructor(address: string, dPath: string) {
constructor(address: string, dPath: string, index: number) {
this.address = address;
this.dPath = dPath;
this.index = index;
}
getAddress(): Promise<string> {
@ -15,6 +17,6 @@ export default class DeterministicWallet implements IWallet {
}
getPath(): string {
return this.dPath;
return `${this.dPath}/${this.index}`;
}
}

View File

@ -6,3 +6,4 @@ export { default as EncryptedPrivKeyWallet } from './encprivkey';
export { default as PresaleWallet } from './presale';
export { default as MewV1Wallet } from './mewv1';
export { default as UtcWallet } from './utc';
export { default as MnemonicWallet } from './mnemonic';

View File

@ -0,0 +1,9 @@
// @flow
import PrivKeyWallet from './privkey';
import { decryptMnemonicToPrivKey } from 'libs/decrypt';
export default class MnemonicWallet extends PrivKeyWallet {
constructor(phrase: string, pass: string, path: string, address: string) {
super(decryptMnemonicToPrivKey(phrase, pass, path, address));
}
}

View File

@ -11,7 +11,7 @@ import { signRawTxWithPrivKey, signMessageWithPrivKey } from 'libs/signing';
import { isValidPrivKey } from 'libs/validators';
import type { RawTransaction } from 'libs/transaction';
import type { UtcKeystore } from 'libs/keystore';
import { stripHex } from 'libs/values';
import { stripHexPrefixAndLower } from 'libs/values';
export default class PrivKeyWallet implements IWallet {
privKey: Buffer;
@ -43,7 +43,7 @@ export default class PrivKeyWallet implements IWallet {
getNakedAddress(): Promise<string> {
return new Promise(resolve => {
this.getAddress().then(address => {
resolve(stripHex(address));
resolve(stripHexPrefixAndLower(address));
});
});
}

View File

@ -4,8 +4,7 @@ import EthTx from 'ethereumjs-tx';
import Big from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util';
import DeterministicWallet from './deterministic';
import { stripHex } from 'libs/values';
import { stripHexPrefixAndLower } from 'libs/values';
import type { RawTransaction } from 'libs/transaction';
export default class TrezorWallet extends DeterministicWallet {
@ -14,12 +13,13 @@ export default class TrezorWallet extends DeterministicWallet {
TrezorConnect.ethereumSignTx(
// Args
this.getPath(),
stripHex(tx.nonce),
stripHex(tx.gasPrice.toString()),
stripHex(tx.gasLimit.toString()),
stripHex(tx.to),
stripHex(tx.value),
stripHex(tx.data),
// stripHexPrefixAndLower identical to ethFuncs.getNakedAddress
stripHexPrefixAndLower(tx.nonce),
stripHexPrefixAndLower(tx.gasPrice.toString()),
stripHexPrefixAndLower(tx.gasLimit.toString()),
stripHexPrefixAndLower(tx.to),
stripHexPrefixAndLower(tx.value),
stripHexPrefixAndLower(tx.data),
tx.chainId,
// Callback
result => {

View File

@ -30,21 +30,34 @@ import { getTokens } from 'selectors/wallet';
import type { INode } from 'libs/nodes/INode';
import type { Token } from 'config/data';
// TODO: BIP39 for mnemonic wallets?
import { showNotification } from 'actions/notifications';
import translate from 'translations';
function* getDeterministicWallets(
action?: GetDeterministicWalletsAction
): Generator<Yield, Return, Next> {
if (!action) return;
const { publicKey, chainCode, limit, offset } = action.payload;
const hdk = new HDKey();
hdk.publicKey = new Buffer(publicKey, 'hex');
hdk.chainCode = new Buffer(chainCode, 'hex');
const { seed, dPath, publicKey, chainCode, limit, offset } = action.payload;
let pathBase, hdk;
//if seed present, treat as mnemonic
//if pubKey & chainCode present, treat as HW wallet
if (seed) {
hdk = HDKey.fromMasterSeed(new Buffer(seed, 'hex'));
pathBase = dPath;
} else if (publicKey && chainCode) {
hdk = new HDKey();
hdk.publicKey = new Buffer(publicKey, 'hex');
hdk.chainCode = new Buffer(chainCode, 'hex');
pathBase = 'm';
} else return;
const wallets = [];
for (let i = 0; i < limit; i++) {
const index = i + offset;
const dkey = hdk.derive(`m/${index}`);
const dkey = hdk.derive(`${pathBase}/${index}`);
const address = publicToAddress(dkey.publicKey, true).toString('hex');
wallets.push({
index,
@ -62,16 +75,22 @@ function* getDeterministicWallets(
function* updateWalletValues(): Generator<Yield, Return, Next> {
const node: INode = yield select(getNodeLib);
const wallets: DeterministicWalletData[] = yield select(getWallets);
const calls = wallets.map(w => apply(node, node.getBalance, [w.address]));
const balances = yield all(calls);
for (let i = 0; i < wallets.length; i++) {
yield put(
updateDeterministicWallet({
...wallets[i],
value: balances[i]
})
);
try {
const calls = wallets.map(w => apply(node, node.getBalance, [w.address]));
const balances = yield all(calls);
for (let i = 0; i < wallets.length; i++) {
yield put(
updateDeterministicWallet({
...wallets[i],
value: balances[i]
})
);
}
} catch (err) {
console.log(err);
yield put(showNotification('danger', translate('ERROR_32')));
}
}
@ -86,21 +105,27 @@ function* updateWalletTokenValues(): Generator<Yield, Return, Next> {
const node: INode = yield select(getNodeLib);
const wallets: DeterministicWalletData[] = yield select(getWallets);
const calls = wallets.map(w => {
return apply(node, node.getTokenBalance, [w.address, token]);
});
const tokenBalances = yield all(calls);
for (let i = 0; i < wallets.length; i++) {
yield put(
updateDeterministicWallet({
...wallets[i],
tokenValues: {
...wallets[i].tokenValues,
[desiredToken]: tokenBalances[i]
}
})
);
try {
const calls = wallets.map(w => {
return apply(node, node.getTokenBalance, [w.address, token]);
});
const tokenBalances = yield all(calls);
for (let i = 0; i < wallets.length; i++) {
yield put(
updateDeterministicWallet({
...wallets[i],
tokenValues: {
...wallets[i].tokenValues,
[desiredToken]: tokenBalances[i]
}
})
);
}
} catch (err) {
console.log(err);
yield put(showNotification('danger', translate('ERROR_32')));
}
}

View File

@ -5,7 +5,8 @@ import { takeEvery, call, apply, put, select, fork } from 'redux-saga/effects';
import { setWallet, setBalance, setTokenBalances } from 'actions/wallet';
import type {
UnlockPrivateKeyAction,
UnlockKeystoreAction
UnlockKeystoreAction,
UnlockMnemonicAction
} from 'actions/wallet';
import { showNotification } from 'actions/notifications';
import type { BroadcastTxRequestedAction } from 'actions/wallet';
@ -19,6 +20,7 @@ import {
UtcWallet,
EncryptedPrivKeyWallet,
PrivKeyWallet,
MnemonicWallet,
IWallet
} from 'libs/wallet';
import { INode } from 'libs/nodes/INode';
@ -141,6 +143,25 @@ export function* unlockKeystore(
yield put(setWallet(wallet));
}
function* unlockMnemonic(
action?: UnlockMnemonicAction
): Generator<Yield, Return, Next> {
if (!action) return;
let wallet;
const { phrase, pass, path, address } = action.payload;
try {
wallet = new MnemonicWallet(phrase, pass, path, address);
} catch (err) {
// TODO: use better error than 'ERROR_14' (wallet not found)
yield put(showNotification('danger', translate('ERROR_14')));
return;
}
yield put(setWallet(wallet));
}
function* broadcastTx(
action: BroadcastTxRequestedAction
): Generator<Yield, Return, Next> {
@ -176,6 +197,7 @@ export default function* walletSaga(): Generator<Yield, Return, Next> {
yield [
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
takeEvery('WALLET_UNLOCK_KEYSTORE', unlockKeystore),
takeEvery('WALLET_UNLOCK_MNEMONIC', unlockMnemonic),
takeEvery('WALLET_SET', updateBalances),
takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances),
// $FlowFixMe but how do I specify param types here flow?

View File

@ -10,6 +10,7 @@
"dependencies": {
"bignumber.js": "^4.0.2",
"bootstrap-sass": "^3.3.7",
"bip39": "^2.4.0",
"classnames": "^2.2.5",
"ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git",
"ethereumjs-abi": "^0.6.4",

View File

@ -2,7 +2,8 @@ import {
decryptPrivKey,
decodeCryptojsSalt,
evp_kdf,
decipherBuffer
decipherBuffer,
decryptMnemonicToPrivKey
} from '../../common/libs/decrypt';
//deconstructed elements of a V1 encrypted priv key
@ -71,3 +72,120 @@ describe('decipherBuffer', () => {
expect(result.toString()).toEqual(str + '!');
});
});
describe('decryptMnemonicToPrivKey', () => {
const mocks = [
{
phrase:
'first catalog away faculty jelly now life kingdom pigeon raise gain accident',
pass: '',
path: "m/44'/60'/0'/0/8",
address: '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854',
privKey:
'31e97f395cabc6faa37d8a9d6bb185187c35704e7b976c7a110e2f0eab37c344'
},
{
phrase:
'grace near jewel celery divorce unlock thumb segment since photo cushion meat sketch tooth edit',
pass: '',
path: "m/44'/60'/0'/0/18",
address: '0xB20f8aCA62e18f4586aAEf4720daCac23cC29954',
privKey:
'594ee624ebad54b9469915c3f5eb22127727a5e380a17d24780dbe272996b401'
},
{
phrase:
'airport bid shop easy tiger rule under afraid lobster adapt ranch story elbow album rifle turtle earn witness',
pass: '',
path: "m/44'/60'/0'/0/24",
address: '0xE6D0932fFDDcB45bf0e18dE4716137dEdD2E4c2c',
privKey:
'6aba8bb6018a85af7cb552325b52e397f83cfb56f68cf8937aa14c3875bbb0aa'
},
{
phrase:
'plug strong practice prize crater private together anchor horror nasty option exhibit position engage pledge giggle soda lecture syrup ocean barrel',
pass: '',
path: "m/44'/60'/0'/0/0",
address: '0xd163f4d95782608b251c4d985846A1754c53D32C',
privKey:
'88046b4bdbb1c88945662cb0984258ca1b09df0bb0b38fdc55bcb8998f28aad4'
},
{
phrase:
'laptop pool call below prepare car alley wheel bunker valve soul misery buffalo home hobby timber enlist country mind guilt drastic castle cable federal',
pass: '',
path: "m/44'/60'/0'/0/4",
address: '0x04E2df6Fe2a28dd24dbCC49485ff30Fc3ea04822',
privKey:
'fc9ad0931a3aee167179c1fd31825b7a7b558b4bb2eb3fb0c04028c98d495907'
},
{
phrase:
'stadium river pigeon midnight grit truck fiscal eight hello rescue destroy eyebrow',
pass: 'password',
path: "m/44'/60'/0'/0/5",
address: '0xe74908668F594f327fd2215A2564Cf79298a136e',
privKey:
'b65abfb2660f71b4b46aed98975f0cc1ebe1fcb3835a7a10b236e4012c93f306'
},
{
phrase:
'oval save glimpse regret decline pottery wealth canal post sing congress bounce run unable stove',
pass: 'password',
path: "m/44'/60'/0'/0/10",
address: '0x0d20865AfAE9B8a1F867eCd60684FBCDA3Bd1FA5',
privKey:
'29eb9ec0f5586d1935bc4c6bd89e6fb3de76b4fad345fa844efc5432885cfe73'
},
{
phrase:
'lecture defy often frog young blush exact tomato culture north urge rescue resemble require bring dismiss actress fog',
pass: 'password',
path: "m/44'/60'/0'/0/7",
address: '0xdd5d6e5dEfD09c3F2BD6d994EE43B59df88c7187',
privKey:
'd13404b9b05f6b5bf8e5cf810aa903e4b60ac654b0acf09a8ea0efe174746ae5'
},
{
phrase:
'supreme famous violin such option marriage arctic genius member rare siege circle round field weather humble fame buffalo one control marble',
pass: 'password',
path: "m/44'/60'/0'/0/11",
address: '0x6d95e7cC28113F9491b2Ec6b621575a5565Fd208',
privKey:
'a52329aa3d6f2426f8783a1e5f419997e2628ec9a89cc2b7b182d2eaf7f95a24'
},
{
phrase:
'next random ready come great start beyond learn supply chimney include grocery fee phrase margin adult ocean craft topple subject satoshi angry mystery liar',
pass: 'password',
path: "m/44'/60'/0'/0/4",
address: '0x3e583eF3d3cE5Dd483c86A1E00A479cE11Ca21Cf',
privKey:
'450538d4181c4d8ce076ecb34785198316adebe959d6f9462cfb68a58b1819bc'
},
{
phrase:
'champion pitch profit beyond know imitate weasel gift escape bullet price barely crime renew hurry',
pass: 'password123',
path: "m/44'/60'/0'/1",
address: '0x7545D615643F933c34C3E083E68CC831167F31af',
privKey:
'0a43098da5ae737843e385b76b44266a9f8f856cb1b943055b5a96188d306d97'
}
];
it('should derive correct private key from variable phrase lengths/passwords/paths', () => {
mocks.forEach(mock => {
const { phrase, pass, path, privKey, address } = mock;
const derivedPrivKey = decryptMnemonicToPrivKey(
phrase,
pass,
path,
address
);
expect(derivedPrivKey.toString('hex')).toEqual(privKey);
});
});
});