mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-01-10 19:16:10 +00:00
Keystore & Private Key Wallet Decrypts (#116)
* wire up keystore decrypt & build UI * add support for encrypted private keys * add check for key length * rename keystore wallet file * rename encrypted priv key wallet file * add support for presale, v1, & v2 JSON keystores * clean up TODO messages, add class files * add v3 references * add flow type * fix event bug * update privkey validators to accept whole privkey * refactor pkey/pass validation to function * move pass req detection to function, remove unnecessary state * add tests for decrypt & keystore libs
This commit is contained in:
parent
11834299a2
commit
f42837de68
@ -22,6 +22,26 @@ export function unlockPrivateKey(
|
||||
};
|
||||
}
|
||||
|
||||
/*** Unlock Keystore File ***/
|
||||
export type KeystoreUnlockParams = {
|
||||
file: string,
|
||||
password: string
|
||||
};
|
||||
|
||||
export type UnlockKeystoreAction = {
|
||||
type: 'WALLET_UNLOCK_KEYSTORE',
|
||||
payload: KeystoreUnlockParams
|
||||
};
|
||||
|
||||
export function unlockKeystore(
|
||||
value: KeystoreUnlockParams
|
||||
): UnlockKeystoreAction {
|
||||
return {
|
||||
type: 'WALLET_UNLOCK_KEYSTORE',
|
||||
payload: value
|
||||
};
|
||||
}
|
||||
|
||||
/*** Set Wallet ***/
|
||||
export type SetWalletAction = {
|
||||
type: 'WALLET_SET',
|
||||
|
@ -1,53 +1,51 @@
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import wallet from 'ethereumjs-wallet';
|
||||
import ethUtil from 'ethereumjs-util';
|
||||
import { isKeystorePassRequired } from 'libs/keystore';
|
||||
|
||||
export default class KeystoreDecrypt extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleFileSelection = event => {
|
||||
const fileReader = new FileReader();
|
||||
const inputFile = event.target.files[0];
|
||||
|
||||
fileReader.onload = () => {
|
||||
try {
|
||||
const keyStoreString = fileReader.result;
|
||||
const decryptedWallet = wallet.fromV3(
|
||||
keyStoreString,
|
||||
'asdfasdfasdf',
|
||||
true
|
||||
);
|
||||
const privateHex = ethUtil.bufferToHex(decryptedWallet._privKey);
|
||||
const publicHex = ethUtil.bufferToHex(
|
||||
ethUtil.privateToAddress(decryptedWallet._privKey)
|
||||
);
|
||||
console.log(privateHex, publicHex); // TODO: Remove console log, it's only here to let Travis pass
|
||||
} catch (e) {
|
||||
console.error('Could not parse Keystore file.', e);
|
||||
}
|
||||
export type KeystoreValue = {
|
||||
file: string,
|
||||
password: string,
|
||||
valid: boolean
|
||||
};
|
||||
|
||||
fileReader.readAsText(inputFile, 'utf-8');
|
||||
function isPassRequired(file: string): boolean {
|
||||
let passReq = false;
|
||||
try {
|
||||
passReq = isKeystorePassRequired(file);
|
||||
} catch (e) {
|
||||
//TODO: communicate invalid file to user
|
||||
}
|
||||
return passReq;
|
||||
}
|
||||
|
||||
export default class KeystoreDecrypt extends Component {
|
||||
props: {
|
||||
value: KeystoreValue,
|
||||
onChange: (value: KeystoreValue) => void,
|
||||
onUnlock: () => void
|
||||
};
|
||||
|
||||
render() {
|
||||
const { file, password } = this.props.value;
|
||||
let passReq = isPassRequired(file);
|
||||
|
||||
return (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<div id="selectedUploadKey">
|
||||
<h4>{translate('ADD_Radio_2_alt')}</h4>
|
||||
<h4>
|
||||
{translate('ADD_Radio_2_alt')}
|
||||
</h4>
|
||||
|
||||
<div className="form-group">
|
||||
<input
|
||||
className={'hidden'}
|
||||
type="file"
|
||||
id="fselector"
|
||||
onChange={this.handleFileSelection}
|
||||
/>
|
||||
<label htmlFor="fselector">
|
||||
<label htmlFor="fselector" style={{ width: '100%' }}>
|
||||
<a
|
||||
className="btn-file marg-v-sm"
|
||||
className="btn btn-default btn-block"
|
||||
id="aria1"
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
@ -55,9 +53,59 @@ export default class KeystoreDecrypt extends Component {
|
||||
{translate('ADD_Radio_2_short')}
|
||||
</a>
|
||||
</label>
|
||||
<div className={file.length && passReq ? '' : 'hidden'}>
|
||||
<p>
|
||||
{translate('ADD_Label_3')}
|
||||
</p>
|
||||
<input
|
||||
className={`form-control ${password.length > 0
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
value={password}
|
||||
onChange={this.onPasswordChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={translate('x_Password')}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
onKeyDown = (e: SyntheticKeyboardEvent) => {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onUnlock();
|
||||
}
|
||||
};
|
||||
|
||||
onPasswordChange = (e: SyntheticInputEvent) => {
|
||||
const valid = this.props.value.file.length && e.target.value.length;
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
password: e.target.value,
|
||||
valid
|
||||
});
|
||||
};
|
||||
|
||||
handleFileSelection = (e: SyntheticInputEvent) => {
|
||||
const fileReader = new FileReader();
|
||||
const inputFile = e.target.files[0];
|
||||
|
||||
fileReader.onload = () => {
|
||||
const keystore = fileReader.result;
|
||||
let passReq = isPassRequired(keystore);
|
||||
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
file: keystore,
|
||||
valid: keystore.length && !passReq
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.readAsText(inputFile, 'utf-8');
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import translate from 'translations';
|
||||
import { isValidPrivKey } from 'libs/validators';
|
||||
import { isValidPrivKey, isValidEncryptedPrivKey } from 'libs/validators';
|
||||
|
||||
export type PrivateKeyValue = {
|
||||
key: string,
|
||||
@ -16,6 +16,35 @@ function fixPkey(key) {
|
||||
return key;
|
||||
}
|
||||
|
||||
type validated = {
|
||||
fixedPkey: string,
|
||||
isValidPkey: boolean,
|
||||
isPassRequired: boolean,
|
||||
valid: boolean
|
||||
};
|
||||
|
||||
function validatePkeyAndPass(pkey: string, pass: string): validated {
|
||||
const fixedPkey = fixPkey(pkey);
|
||||
const validPkey = isValidPrivKey(fixedPkey);
|
||||
const validEncPkey = isValidEncryptedPrivKey(fixedPkey);
|
||||
const isValidPkey = validPkey || validEncPkey;
|
||||
|
||||
let isValidPass = false;
|
||||
|
||||
if (validPkey) {
|
||||
isValidPass = true;
|
||||
} else if (validEncPkey) {
|
||||
isValidPass = pass.length > 0;
|
||||
}
|
||||
|
||||
return {
|
||||
fixedPkey,
|
||||
isValidPkey,
|
||||
isPassRequired: validEncPkey,
|
||||
valid: isValidPkey && isValidPass
|
||||
};
|
||||
}
|
||||
|
||||
export default class PrivateKeyDecrypt extends Component {
|
||||
props: {
|
||||
value: PrivateKeyValue,
|
||||
@ -25,9 +54,7 @@ export default class PrivateKeyDecrypt extends Component {
|
||||
|
||||
render() {
|
||||
const { key, password } = this.props.value;
|
||||
const fixedPkey = fixPkey(key);
|
||||
const isValid = isValidPrivKey(fixedPkey.length);
|
||||
const isPassRequired = fixedPkey.length > 64;
|
||||
const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password);
|
||||
|
||||
return (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
@ -38,7 +65,9 @@ export default class PrivateKeyDecrypt extends Component {
|
||||
<div className="form-group">
|
||||
<textarea
|
||||
id="aria-private-key"
|
||||
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
|
||||
className={`form-control ${isValidPkey
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
value={key}
|
||||
onChange={this.onPkeyChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
@ -46,7 +75,7 @@ export default class PrivateKeyDecrypt extends Component {
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
{isValid &&
|
||||
{isValidPkey &&
|
||||
isPassRequired &&
|
||||
<div className="form-group">
|
||||
<p>
|
||||
@ -69,25 +98,21 @@ export default class PrivateKeyDecrypt extends Component {
|
||||
}
|
||||
|
||||
onPkeyChange = (e: SyntheticInputEvent) => {
|
||||
const fixedPkey = fixPkey(e.target.value);
|
||||
const isValid = isValidPrivKey(fixedPkey.length);
|
||||
const isPassRequired = fixedPkey.length > 64;
|
||||
const valid =
|
||||
isValid && (isPassRequired ? this.props.value.password.length > 0 : true);
|
||||
const pkey = e.target.value;
|
||||
const pass = this.props.value.password;
|
||||
const { fixedPkey, valid } = validatePkeyAndPass(pkey, pass);
|
||||
|
||||
this.props.onChange({ ...this.props.value, key: e.target.value, valid });
|
||||
this.props.onChange({ ...this.props.value, key: fixedPkey, valid });
|
||||
};
|
||||
|
||||
onPasswordChange = (e: SyntheticInputEvent) => {
|
||||
const fixedPkey = fixPkey(this.props.value.key);
|
||||
const isValid = isValidPrivKey(fixedPkey.length);
|
||||
const isPassRequired = fixedPkey.length > 64;
|
||||
const valid =
|
||||
isValid && (isPassRequired ? e.target.value.length > 0 : true);
|
||||
const pkey = this.props.value.key;
|
||||
const pass = e.target.value;
|
||||
const { valid } = validatePkeyAndPass(pkey, pass);
|
||||
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
password: e.target.value,
|
||||
password: pass,
|
||||
valid
|
||||
});
|
||||
};
|
||||
|
@ -9,14 +9,18 @@ import LedgerNanoSDecrypt from './LedgerNano';
|
||||
import TrezorDecrypt from './Trezor';
|
||||
import ViewOnlyDecrypt from './ViewOnly';
|
||||
import map from 'lodash/map';
|
||||
import { unlockPrivateKey } from 'actions/wallet';
|
||||
import { unlockPrivateKey, unlockKeystore } from 'actions/wallet';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const WALLETS = {
|
||||
'keystore-file': {
|
||||
lid: 'x_Keystore2',
|
||||
component: KeystoreDecrypt,
|
||||
initialParams: {}
|
||||
initialParams: {
|
||||
file: '',
|
||||
password: ''
|
||||
},
|
||||
unlock: unlockKeystore
|
||||
},
|
||||
'private-key': {
|
||||
lid: 'x_PrivKey2',
|
||||
|
68
common/libs/decrypt.js
Normal file
68
common/libs/decrypt.js
Normal file
@ -0,0 +1,68 @@
|
||||
//@flow
|
||||
|
||||
import { createHash, createDecipheriv } from 'crypto';
|
||||
|
||||
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L230
|
||||
export function decryptPrivKey(encprivkey: string, password: string): Buffer {
|
||||
let cipher = encprivkey.slice(0, 128);
|
||||
cipher = decodeCryptojsSalt(cipher);
|
||||
let evp = evp_kdf(new Buffer(password), cipher.salt, {
|
||||
keysize: 32,
|
||||
ivsize: 16
|
||||
});
|
||||
let decipher = createDecipheriv('aes-256-cbc', evp.key, evp.iv);
|
||||
let privKey = decipherBuffer(decipher, new Buffer(cipher.ciphertext));
|
||||
|
||||
return new Buffer(privKey.toString(), 'hex');
|
||||
}
|
||||
|
||||
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L284
|
||||
export function decodeCryptojsSalt(input: string): Object {
|
||||
let ciphertext = new Buffer(input, 'base64');
|
||||
if (ciphertext.slice(0, 8).toString() === 'Salted__') {
|
||||
return {
|
||||
salt: ciphertext.slice(8, 16),
|
||||
ciphertext: ciphertext.slice(16)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
ciphertext: ciphertext
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L297
|
||||
export function evp_kdf(data: Buffer, salt: Buffer, opts: Object) {
|
||||
// A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)`
|
||||
|
||||
function iter(block) {
|
||||
let hash = createHash(opts.digest || 'md5');
|
||||
hash.update(block);
|
||||
hash.update(data);
|
||||
hash.update(salt);
|
||||
block = hash.digest();
|
||||
for (let i = 1; i < (opts.count || 1); i++) {
|
||||
hash = createHash(opts.digest || 'md5');
|
||||
hash.update(block);
|
||||
block = hash.digest();
|
||||
}
|
||||
return block;
|
||||
}
|
||||
let keysize = opts.keysize || 16;
|
||||
let ivsize = opts.ivsize || 16;
|
||||
let ret = [];
|
||||
let i = 0;
|
||||
while (Buffer.concat(ret).length < keysize + ivsize) {
|
||||
ret[i] = iter(i === 0 ? new Buffer(0) : ret[i - 1]);
|
||||
i++;
|
||||
}
|
||||
let tmp = Buffer.concat(ret);
|
||||
return {
|
||||
key: tmp.slice(0, keysize),
|
||||
iv: tmp.slice(keysize, keysize + ivsize)
|
||||
};
|
||||
}
|
||||
|
||||
export function decipherBuffer(decipher: Object, data: Buffer): Buffer {
|
||||
return Buffer.concat([decipher.update(data), decipher.final()]);
|
||||
}
|
@ -5,10 +5,97 @@ import {
|
||||
pbkdf2Sync,
|
||||
createDecipheriv
|
||||
} from 'crypto';
|
||||
import { sha3 } from 'ethereumjs-util';
|
||||
import { decipherBuffer, decodeCryptojsSalt, evp_kdf } from './decrypt';
|
||||
import { sha3, privateToAddress } from 'ethereumjs-util';
|
||||
import scrypt from 'scryptsy';
|
||||
import uuid from 'uuid';
|
||||
|
||||
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L342
|
||||
export function determineKeystoreType(file: string): string {
|
||||
const parsed = JSON.parse(file);
|
||||
|
||||
if (parsed.encseed) return 'presale';
|
||||
else if (parsed.Crypto || parsed.crypto) return 'v2-v3-utc';
|
||||
else if (parsed.hash && parsed.locked === true) return 'v1-encrypted';
|
||||
else if (parsed.hash && parsed.locked === false) return 'v1-unencrypted';
|
||||
else if (parsed.publisher === 'MyEtherWallet') return 'v2-unencrypted';
|
||||
else throw new Error('Invalid keystore');
|
||||
}
|
||||
|
||||
export function isKeystorePassRequired(file: string): boolean {
|
||||
switch (determineKeystoreType(file)) {
|
||||
case 'presale':
|
||||
return true;
|
||||
case 'v1-unencrypted':
|
||||
return false;
|
||||
case 'v1-encrypted':
|
||||
return true;
|
||||
case 'v2-unencrypted':
|
||||
return false;
|
||||
case 'v2-v3-utc':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L218
|
||||
export function decryptPresaleToPrivKey(
|
||||
file: string,
|
||||
password: string
|
||||
): Buffer {
|
||||
let json = JSON.parse(file);
|
||||
let encseed = new Buffer(json.encseed, 'hex');
|
||||
let derivedKey = pbkdf2Sync(
|
||||
new Buffer(password),
|
||||
new Buffer(password),
|
||||
2000,
|
||||
32,
|
||||
'sha256'
|
||||
).slice(0, 16);
|
||||
let decipher = createDecipheriv(
|
||||
'aes-128-cbc',
|
||||
derivedKey,
|
||||
encseed.slice(0, 16)
|
||||
);
|
||||
let seed = decipherBuffer(decipher, encseed.slice(16));
|
||||
let privkey = sha3(seed);
|
||||
let address = privateToAddress(privkey);
|
||||
|
||||
if (address.toString('hex') !== json.ethaddr) {
|
||||
throw new Error('Decoded key mismatch - possibly wrong passphrase');
|
||||
}
|
||||
return privkey;
|
||||
}
|
||||
|
||||
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L179
|
||||
export function decryptMewV1ToPrivKey(file: string, password: string): Buffer {
|
||||
let json = JSON.parse(file);
|
||||
let privkey, address;
|
||||
|
||||
if (typeof password !== 'string') {
|
||||
throw new Error('Password required');
|
||||
}
|
||||
if (password.length < 7) {
|
||||
throw new Error('Password must be at least 7 characters');
|
||||
}
|
||||
let cipher = json.encrypted ? json.private.slice(0, 128) : json.private;
|
||||
cipher = decodeCryptojsSalt(cipher);
|
||||
let evp = evp_kdf(new Buffer(password), cipher.salt, {
|
||||
keysize: 32,
|
||||
ivsize: 16
|
||||
});
|
||||
let decipher = createDecipheriv('aes-256-cbc', evp.key, evp.iv);
|
||||
privkey = decipherBuffer(decipher, new Buffer(cipher.ciphertext));
|
||||
privkey = new Buffer(privkey.toString(), 'hex');
|
||||
address = '0x' + privateToAddress(privkey).toString('hex');
|
||||
|
||||
if (address !== json.address) {
|
||||
throw new Error('Invalid private key or address');
|
||||
}
|
||||
return privkey;
|
||||
}
|
||||
|
||||
export const scryptSettings = {
|
||||
n: 1024
|
||||
};
|
||||
@ -75,7 +162,10 @@ export function getV3Filename(address: string) {
|
||||
return ['UTC--', ts.toJSON().replace(/:/g, '-'), '--', address].join('');
|
||||
}
|
||||
|
||||
export function fromV3KeystoreToPkey(input: string, password: string): Buffer {
|
||||
export function decryptUtcKeystoreToPkey(
|
||||
input: string,
|
||||
password: string
|
||||
): Buffer {
|
||||
let kstore = JSON.parse(input.toLowerCase());
|
||||
if (kstore.version !== 3) {
|
||||
throw new Error('Not a V3 wallet');
|
||||
@ -124,7 +214,3 @@ export function fromV3KeystoreToPkey(input: string, password: string): Buffer {
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
|
||||
function decipherBuffer(decipher, data) {
|
||||
return Buffer.concat([decipher.update(data), decipher.final()]);
|
||||
}
|
||||
|
@ -75,8 +75,22 @@ function validateEtherAddress(address: string): boolean {
|
||||
else return isChecksumAddress(address);
|
||||
}
|
||||
|
||||
export function isValidPrivKey(length: number): boolean {
|
||||
return length === 64 || length === 128 || length === 132;
|
||||
export function isValidPrivKey(privkey: string | Buffer): boolean {
|
||||
if (typeof privkey === 'string') {
|
||||
return privkey.length === 64;
|
||||
} else if (privkey instanceof Buffer) {
|
||||
return privkey.length === 32;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidEncryptedPrivKey(privkey: string): boolean {
|
||||
if (typeof privkey === 'string') {
|
||||
return privkey.length === 128 || privkey.length === 132;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isPositiveIntegerOrZero(number: number): boolean {
|
||||
|
9
common/libs/wallet/encprivkey.js
Normal file
9
common/libs/wallet/encprivkey.js
Normal file
@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
import PrivKeyWallet from './privkey';
|
||||
import { decryptPrivKey } from 'libs/decrypt';
|
||||
|
||||
export default class EncryptedPrivKeyWallet extends PrivKeyWallet {
|
||||
constructor(encprivkey: string, password: string) {
|
||||
super(decryptPrivKey(encprivkey, password));
|
||||
}
|
||||
}
|
@ -2,3 +2,7 @@
|
||||
|
||||
export { default as BaseWallet } from './base';
|
||||
export { default as PrivKeyWallet } from './privkey';
|
||||
export { default as EncryptedPrivKeyWallet } from './encprivkey';
|
||||
export { default as PresaleWallet } from './presale';
|
||||
export { default as MewV1Wallet } from './mewv1';
|
||||
export { default as UtcWallet } from './utc';
|
||||
|
@ -1,9 +0,0 @@
|
||||
// @flow
|
||||
import PrivKeyWallet from './privkey';
|
||||
import { fromV3KeystoreToPkey } from 'libs/keystore';
|
||||
|
||||
export default class KeystoreWallet extends PrivKeyWallet {
|
||||
constructor(keystore: string, password: string) {
|
||||
super(fromV3KeystoreToPkey(keystore, password));
|
||||
}
|
||||
}
|
9
common/libs/wallet/mewv1.js
Normal file
9
common/libs/wallet/mewv1.js
Normal file
@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
import PrivKeyWallet from './privkey';
|
||||
import { decryptMewV1ToPrivKey } from 'libs/keystore';
|
||||
|
||||
export default class MewV1Wallet extends PrivKeyWallet {
|
||||
constructor(keystore: string, password: string) {
|
||||
super(decryptMewV1ToPrivKey(keystore, password));
|
||||
}
|
||||
}
|
9
common/libs/wallet/presale.js
Normal file
9
common/libs/wallet/presale.js
Normal file
@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
import PrivKeyWallet from './privkey';
|
||||
import { decryptPresaleToPrivKey } from 'libs/keystore';
|
||||
|
||||
export default class PresaleWallet extends PrivKeyWallet {
|
||||
constructor(keystore: string, password: string) {
|
||||
super(decryptPresaleToPrivKey(keystore, password));
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import {
|
||||
import { randomBytes } from 'crypto';
|
||||
import { pkeyToKeystore } from 'libs/keystore';
|
||||
import { signRawTxWithPrivKey, signMessageWithPrivKey } from 'libs/signing';
|
||||
|
||||
import { isValidPrivKey } from 'libs/validators';
|
||||
import type { RawTransaction } from 'libs/transaction';
|
||||
|
||||
export default class PrivKeyWallet extends BaseWallet {
|
||||
@ -15,6 +17,9 @@ export default class PrivKeyWallet extends BaseWallet {
|
||||
pubKey: Buffer;
|
||||
address: Buffer;
|
||||
constructor(privkey: Buffer) {
|
||||
if (!isValidPrivKey(privkey)) {
|
||||
throw new Error('Invalid private key');
|
||||
}
|
||||
super();
|
||||
this.privKey = privkey;
|
||||
this.pubKey = privateToPublic(this.privKey);
|
||||
|
9
common/libs/wallet/utc.js
Normal file
9
common/libs/wallet/utc.js
Normal file
@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
import PrivKeyWallet from './privkey';
|
||||
import { decryptUtcKeystoreToPkey } from 'libs/keystore';
|
||||
|
||||
export default class UtcWallet extends PrivKeyWallet {
|
||||
constructor(keystore: string, password: string) {
|
||||
super(decryptUtcKeystoreToPkey(keystore, password));
|
||||
}
|
||||
}
|
@ -2,14 +2,26 @@
|
||||
import { takeEvery, call, apply, put, select, fork } from 'redux-saga/effects';
|
||||
import type { Effect } from 'redux-saga/effects';
|
||||
import { setWallet, setBalance, setTokenBalances } from 'actions/wallet';
|
||||
import type { UnlockPrivateKeyAction } from 'actions/wallet';
|
||||
import type {
|
||||
UnlockPrivateKeyAction,
|
||||
UnlockKeystoreAction
|
||||
} from 'actions/wallet';
|
||||
import { showNotification } from 'actions/notifications';
|
||||
import translate from 'translations';
|
||||
import { PrivKeyWallet, BaseWallet } from 'libs/wallet';
|
||||
import {
|
||||
PresaleWallet,
|
||||
MewV1Wallet,
|
||||
UtcWallet,
|
||||
EncryptedPrivKeyWallet,
|
||||
PrivKeyWallet,
|
||||
BaseWallet
|
||||
} from 'libs/wallet';
|
||||
import { BaseNode } from 'libs/nodes';
|
||||
import { getNodeLib } from 'selectors/config';
|
||||
import { getWalletInst, getTokens } from 'selectors/wallet';
|
||||
|
||||
import { determineKeystoreType } from 'libs/keystore';
|
||||
|
||||
function* updateAccountBalance() {
|
||||
const node: BaseNode = yield select(getNodeLib);
|
||||
const wallet: ?BaseWallet = yield select(getWalletInst);
|
||||
@ -56,7 +68,14 @@ export function* unlockPrivateKey(
|
||||
let wallet = null;
|
||||
|
||||
try {
|
||||
if (action.payload.key.length === 64) {
|
||||
wallet = new PrivKeyWallet(Buffer.from(action.payload.key, 'hex'));
|
||||
} else {
|
||||
wallet = new EncryptedPrivKeyWallet(
|
||||
action.payload.key,
|
||||
action.payload.password
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(showNotification('danger', translate('INVALID_PKEY')));
|
||||
return;
|
||||
@ -65,11 +84,55 @@ export function* unlockPrivateKey(
|
||||
yield call(updateBalances);
|
||||
}
|
||||
|
||||
export function* unlockKeystore(
|
||||
action?: UnlockKeystoreAction
|
||||
): Generator<Effect, void, any> {
|
||||
if (!action) return;
|
||||
|
||||
const file = action.payload.file;
|
||||
const pass = action.payload.password;
|
||||
let wallet = null;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(file);
|
||||
|
||||
switch (determineKeystoreType(file)) {
|
||||
case 'presale':
|
||||
wallet = new PresaleWallet(file, pass);
|
||||
break;
|
||||
case 'v1-unencrypted':
|
||||
wallet = new PrivKeyWallet(Buffer.from(parsed.private, 'hex'));
|
||||
break;
|
||||
case 'v1-encrypted':
|
||||
wallet = new MewV1Wallet(file, pass);
|
||||
break;
|
||||
case 'v2-unencrypted':
|
||||
wallet = new PrivKeyWallet(Buffer.from(parsed.privKey, 'hex'));
|
||||
break;
|
||||
case 'v2-v3-utc':
|
||||
wallet = new UtcWallet(file, pass);
|
||||
break;
|
||||
default:
|
||||
yield put(showNotification('danger', translate('ERROR_6')));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(showNotification('danger', translate('ERROR_6')));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above
|
||||
|
||||
yield put(setWallet(wallet));
|
||||
yield call(updateBalances);
|
||||
}
|
||||
|
||||
export default function* walletSaga(): Generator<Effect | Effect[], void, any> {
|
||||
// useful for development
|
||||
yield call(updateBalances);
|
||||
yield [
|
||||
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
|
||||
takeEvery('WALLET_UNLOCK_KEYSTORE', unlockKeystore),
|
||||
takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances)
|
||||
];
|
||||
}
|
||||
|
73
spec/libs/decrypt.spec.js
Normal file
73
spec/libs/decrypt.spec.js
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
decryptPrivKey,
|
||||
decodeCryptojsSalt,
|
||||
evp_kdf,
|
||||
decipherBuffer
|
||||
} from '../../common/libs/decrypt';
|
||||
|
||||
//deconstructed elements of a V1 encrypted priv key
|
||||
const encpkey =
|
||||
'U2FsdGVkX19us8qXfYyeQhxyzV7aFlXckG/KrRLajoCGBKO4/saefxGs/3PrCLWxZEbx2vn6V0VDWrkDUkL+8S4MK7FL9LCiIKxeCq/ciwX9YQepsRRetG2MExuUWkQ6365d';
|
||||
const pass = 'testtesttest';
|
||||
const salt = 'brPKl32MnkI=';
|
||||
const ciphertext =
|
||||
'HHLNXtoWVdyQb8qtEtqOgIYEo7j+xp5/Eaz/c+sItbFkRvHa+fpXRUNauQNSQv7xLgwrsUv0sKIgrF4Kr9yLBf1hB6mxFF60bYwTG5RaRDrfrl0=';
|
||||
const iv = 'k9YWF8ZBCoyuFS6CfGS+7w==';
|
||||
const key = 'u9uhwRmBQDJ12MUBkIrO5EzMQZTYEf6hTBDzSJBKJ2k=';
|
||||
const pkey = 'a56d4f23449a10ddcdd94bad56f895640097800406840aa8fe545d324d422c02';
|
||||
|
||||
describe('decryptPrivKey', () => {
|
||||
it('should decrypt encrypted pkey string to pkey buffer', () => {
|
||||
const decrypt = decryptPrivKey(encpkey, pass);
|
||||
|
||||
expect(decrypt).toBeInstanceOf(Buffer);
|
||||
expect(decrypt.toString('hex')).toEqual(pkey);
|
||||
expect(decrypt.length).toEqual(32);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decodeCryptojsSalt', () => {
|
||||
it('should derive correct salt and ciphertext from pkey string', () => {
|
||||
const decode = decodeCryptojsSalt(encpkey);
|
||||
|
||||
expect(decode.salt).toBeInstanceOf(Buffer);
|
||||
expect(decode.ciphertext).toBeInstanceOf(Buffer);
|
||||
expect(decode.salt.toString('base64')).toEqual(salt);
|
||||
expect(decode.ciphertext.toString('base64')).toEqual(ciphertext);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evp_kdf', () => {
|
||||
it('should derive correct key and iv', () => {
|
||||
const result = evp_kdf(
|
||||
new Buffer(pass, 'utf8'),
|
||||
new Buffer(salt, 'base64'),
|
||||
{ keysize: 32, ivsize: 16 }
|
||||
);
|
||||
|
||||
expect(result.key).toBeInstanceOf(Buffer);
|
||||
expect(result.iv).toBeInstanceOf(Buffer);
|
||||
expect(result.key.toString('base64')).toEqual(key);
|
||||
expect(result.iv.toString('base64')).toEqual(iv);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decipherBuffer', () => {
|
||||
const str = 'test string';
|
||||
const data = new Buffer(str, 'utf8');
|
||||
const decipher = {
|
||||
update: jest.fn(d => d),
|
||||
final: jest.fn(() => new Buffer('!', 'utf8'))
|
||||
};
|
||||
const result = decipherBuffer(decipher, data);
|
||||
|
||||
it('should call update and final on decipher', () => {
|
||||
expect(decipher.update).toHaveBeenCalledWith(data);
|
||||
expect(decipher.final).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return concat of update and final buffers', () => {
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.toString()).toEqual(str + '!');
|
||||
});
|
||||
});
|
66
spec/libs/keystore.spec.js
Normal file
66
spec/libs/keystore.spec.js
Normal file
@ -0,0 +1,66 @@
|
||||
import {
|
||||
decryptMewV1ToPrivKey,
|
||||
decryptUtcKeystoreToPkey
|
||||
} from '../../common/libs/keystore';
|
||||
|
||||
const mewV1Keystore = {
|
||||
address: '0x15bd5b09f42ddd49a266570f165d2732f3372e7d',
|
||||
encrypted: true,
|
||||
locked: true,
|
||||
hash: '5927d16b10d5d1df8a678a6f7d4770f2ac4eafe71387126fff6c1b1e93876d7a',
|
||||
private:
|
||||
'U2FsdGVkX19us8qXfYyeQhxyzV7aFlXckG/KrRLajoCGBKO4/saefxGs/3PrCLWxZEbx2vn6V0VDWrkDUkL+8S4MK7FL9LCiIKxeCq/ciwX9YQepsRRetG2MExuUWkQ6365d',
|
||||
public:
|
||||
'U2FsdGVkX1/egEFLhHiGKzn08x+MovElanAzvwcvMEf7FUSAjDEKKt0Jc+Cnz3fsVlO0nNXDG7i4sP7gEyqdEj+vlwyMXv7ir9mwCwQ1+XWz7k5BFUg0Bw9xh2ygtnGDOBjF3TDm0YL+Gdtf9WS7rcOBD0tQWHJ7N5DIBUM5WKOa0bwdCqJgrTKX73XI5mjX/kR9VFnvv+nezVkSvb66nQ=='
|
||||
};
|
||||
const mewV1PrivKey =
|
||||
'a56d4f23449a10ddcdd94bad56f895640097800406840aa8fe545d324d422c02';
|
||||
const utcKeystore = {
|
||||
version: 3,
|
||||
id: 'cb788af4-993d-43ad-851b-0d2031e52c61',
|
||||
address: '25a24679f35e447f778cf54a3823facf39904a63',
|
||||
Crypto: {
|
||||
ciphertext:
|
||||
'4193915c560835d00b2b9ff5dd20f3e13793b2a3ca8a97df649286063f27f707',
|
||||
cipherparams: {
|
||||
iv: 'dccb8c009b11d1c6226ba19b557dce4c'
|
||||
},
|
||||
cipher: 'aes-128-ctr',
|
||||
kdf: 'scrypt',
|
||||
kdfparams: {
|
||||
dklen: 32,
|
||||
salt: '037a53e520f2d00fb70f02f39b31b77374de9e0e1d35fd7cbe9c8a8b21d6b0ab',
|
||||
n: 1024,
|
||||
r: 8,
|
||||
p: 1
|
||||
},
|
||||
mac: '774fbe4bf35e7e28df15cd6c3546e74ce6608e9ab68a88d50227858a3b05769a'
|
||||
}
|
||||
};
|
||||
const utcPrivKey =
|
||||
'8bcb4456ef0356ce062c857cefdd3ed1bab45432cf76d6d5340899cfd0f702e8';
|
||||
const password = 'testtesttest';
|
||||
|
||||
describe('decryptMewV1ToPrivKey', () => {
|
||||
it('should derive the correct private key', () => {
|
||||
const result = decryptMewV1ToPrivKey(
|
||||
JSON.stringify(mewV1Keystore),
|
||||
password
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.toString('hex')).toEqual(mewV1PrivKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptUtcKeystoreToPkey', () => {
|
||||
it('should derive the correct private key', () => {
|
||||
const result = decryptUtcKeystoreToPkey(
|
||||
JSON.stringify(utcKeystore),
|
||||
password
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.toString('hex')).toEqual(utcPrivKey);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user