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 ***/
|
/*** Set Wallet ***/
|
||||||
export type SetWalletAction = {
|
export type SetWalletAction = {
|
||||||
type: 'WALLET_SET',
|
type: 'WALLET_SET',
|
||||||
|
@ -1,53 +1,51 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import wallet from 'ethereumjs-wallet';
|
import { isKeystorePassRequired } from 'libs/keystore';
|
||||||
import ethUtil from 'ethereumjs-util';
|
|
||||||
|
|
||||||
export default class KeystoreDecrypt extends Component {
|
export type KeystoreValue = {
|
||||||
constructor(props) {
|
file: string,
|
||||||
super(props);
|
password: string,
|
||||||
}
|
valid: boolean
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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() {
|
render() {
|
||||||
|
const { file, password } = this.props.value;
|
||||||
|
let passReq = isPassRequired(file);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="col-md-4 col-sm-6">
|
<section className="col-md-4 col-sm-6">
|
||||||
<div id="selectedUploadKey">
|
<div id="selectedUploadKey">
|
||||||
<h4>{translate('ADD_Radio_2_alt')}</h4>
|
<h4>
|
||||||
|
{translate('ADD_Radio_2_alt')}
|
||||||
|
</h4>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<input
|
<input
|
||||||
|
className={'hidden'}
|
||||||
type="file"
|
type="file"
|
||||||
id="fselector"
|
id="fselector"
|
||||||
onChange={this.handleFileSelection}
|
onChange={this.handleFileSelection}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="fselector">
|
<label htmlFor="fselector" style={{ width: '100%' }}>
|
||||||
<a
|
<a
|
||||||
className="btn-file marg-v-sm"
|
className="btn btn-default btn-block"
|
||||||
id="aria1"
|
id="aria1"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
role="button"
|
role="button"
|
||||||
@ -55,9 +53,59 @@ export default class KeystoreDecrypt extends Component {
|
|||||||
{translate('ADD_Radio_2_short')}
|
{translate('ADD_Radio_2_short')}
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
import { isValidPrivKey } from 'libs/validators';
|
import { isValidPrivKey, isValidEncryptedPrivKey } from 'libs/validators';
|
||||||
|
|
||||||
export type PrivateKeyValue = {
|
export type PrivateKeyValue = {
|
||||||
key: string,
|
key: string,
|
||||||
@ -16,6 +16,35 @@ function fixPkey(key) {
|
|||||||
return 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 {
|
export default class PrivateKeyDecrypt extends Component {
|
||||||
props: {
|
props: {
|
||||||
value: PrivateKeyValue,
|
value: PrivateKeyValue,
|
||||||
@ -25,9 +54,7 @@ export default class PrivateKeyDecrypt extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { key, password } = this.props.value;
|
const { key, password } = this.props.value;
|
||||||
const fixedPkey = fixPkey(key);
|
const { isValidPkey, isPassRequired } = validatePkeyAndPass(key, password);
|
||||||
const isValid = isValidPrivKey(fixedPkey.length);
|
|
||||||
const isPassRequired = fixedPkey.length > 64;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="col-md-4 col-sm-6">
|
<section className="col-md-4 col-sm-6">
|
||||||
@ -38,7 +65,9 @@ export default class PrivateKeyDecrypt extends Component {
|
|||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<textarea
|
<textarea
|
||||||
id="aria-private-key"
|
id="aria-private-key"
|
||||||
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
|
className={`form-control ${isValidPkey
|
||||||
|
? 'is-valid'
|
||||||
|
: 'is-invalid'}`}
|
||||||
value={key}
|
value={key}
|
||||||
onChange={this.onPkeyChange}
|
onChange={this.onPkeyChange}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
@ -46,7 +75,7 @@ export default class PrivateKeyDecrypt extends Component {
|
|||||||
rows="4"
|
rows="4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isValid &&
|
{isValidPkey &&
|
||||||
isPassRequired &&
|
isPassRequired &&
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<p>
|
<p>
|
||||||
@ -69,25 +98,21 @@ export default class PrivateKeyDecrypt extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPkeyChange = (e: SyntheticInputEvent) => {
|
onPkeyChange = (e: SyntheticInputEvent) => {
|
||||||
const fixedPkey = fixPkey(e.target.value);
|
const pkey = e.target.value;
|
||||||
const isValid = isValidPrivKey(fixedPkey.length);
|
const pass = this.props.value.password;
|
||||||
const isPassRequired = fixedPkey.length > 64;
|
const { fixedPkey, valid } = validatePkeyAndPass(pkey, pass);
|
||||||
const valid =
|
|
||||||
isValid && (isPassRequired ? this.props.value.password.length > 0 : true);
|
|
||||||
|
|
||||||
this.props.onChange({ ...this.props.value, key: e.target.value, valid });
|
this.props.onChange({ ...this.props.value, key: fixedPkey, valid });
|
||||||
};
|
};
|
||||||
|
|
||||||
onPasswordChange = (e: SyntheticInputEvent) => {
|
onPasswordChange = (e: SyntheticInputEvent) => {
|
||||||
const fixedPkey = fixPkey(this.props.value.key);
|
const pkey = this.props.value.key;
|
||||||
const isValid = isValidPrivKey(fixedPkey.length);
|
const pass = e.target.value;
|
||||||
const isPassRequired = fixedPkey.length > 64;
|
const { valid } = validatePkeyAndPass(pkey, pass);
|
||||||
const valid =
|
|
||||||
isValid && (isPassRequired ? e.target.value.length > 0 : true);
|
|
||||||
|
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.value,
|
...this.props.value,
|
||||||
password: e.target.value,
|
password: pass,
|
||||||
valid
|
valid
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -9,14 +9,18 @@ import LedgerNanoSDecrypt from './LedgerNano';
|
|||||||
import TrezorDecrypt from './Trezor';
|
import TrezorDecrypt from './Trezor';
|
||||||
import ViewOnlyDecrypt from './ViewOnly';
|
import ViewOnlyDecrypt from './ViewOnly';
|
||||||
import map from 'lodash/map';
|
import map from 'lodash/map';
|
||||||
import { unlockPrivateKey } from 'actions/wallet';
|
import { unlockPrivateKey, unlockKeystore } from 'actions/wallet';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
const WALLETS = {
|
const WALLETS = {
|
||||||
'keystore-file': {
|
'keystore-file': {
|
||||||
lid: 'x_Keystore2',
|
lid: 'x_Keystore2',
|
||||||
component: KeystoreDecrypt,
|
component: KeystoreDecrypt,
|
||||||
initialParams: {}
|
initialParams: {
|
||||||
|
file: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
unlock: unlockKeystore
|
||||||
},
|
},
|
||||||
'private-key': {
|
'private-key': {
|
||||||
lid: 'x_PrivKey2',
|
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,
|
pbkdf2Sync,
|
||||||
createDecipheriv
|
createDecipheriv
|
||||||
} from 'crypto';
|
} 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 scrypt from 'scryptsy';
|
||||||
import uuid from 'uuid';
|
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 = {
|
export const scryptSettings = {
|
||||||
n: 1024
|
n: 1024
|
||||||
};
|
};
|
||||||
@ -75,7 +162,10 @@ export function getV3Filename(address: string) {
|
|||||||
return ['UTC--', ts.toJSON().replace(/:/g, '-'), '--', address].join('');
|
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());
|
let kstore = JSON.parse(input.toLowerCase());
|
||||||
if (kstore.version !== 3) {
|
if (kstore.version !== 3) {
|
||||||
throw new Error('Not a V3 wallet');
|
throw new Error('Not a V3 wallet');
|
||||||
@ -124,7 +214,3 @@ export function fromV3KeystoreToPkey(input: string, password: string): Buffer {
|
|||||||
}
|
}
|
||||||
return seed;
|
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);
|
else return isChecksumAddress(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidPrivKey(length: number): boolean {
|
export function isValidPrivKey(privkey: string | Buffer): boolean {
|
||||||
return length === 64 || length === 128 || length === 132;
|
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 {
|
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 BaseWallet } from './base';
|
||||||
export { default as PrivKeyWallet } from './privkey';
|
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 { randomBytes } from 'crypto';
|
||||||
import { pkeyToKeystore } from 'libs/keystore';
|
import { pkeyToKeystore } from 'libs/keystore';
|
||||||
import { signRawTxWithPrivKey, signMessageWithPrivKey } from 'libs/signing';
|
import { signRawTxWithPrivKey, signMessageWithPrivKey } from 'libs/signing';
|
||||||
|
|
||||||
|
import { isValidPrivKey } from 'libs/validators';
|
||||||
import type { RawTransaction } from 'libs/transaction';
|
import type { RawTransaction } from 'libs/transaction';
|
||||||
|
|
||||||
export default class PrivKeyWallet extends BaseWallet {
|
export default class PrivKeyWallet extends BaseWallet {
|
||||||
@ -15,6 +17,9 @@ export default class PrivKeyWallet extends BaseWallet {
|
|||||||
pubKey: Buffer;
|
pubKey: Buffer;
|
||||||
address: Buffer;
|
address: Buffer;
|
||||||
constructor(privkey: Buffer) {
|
constructor(privkey: Buffer) {
|
||||||
|
if (!isValidPrivKey(privkey)) {
|
||||||
|
throw new Error('Invalid private key');
|
||||||
|
}
|
||||||
super();
|
super();
|
||||||
this.privKey = privkey;
|
this.privKey = privkey;
|
||||||
this.pubKey = privateToPublic(this.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 { takeEvery, call, apply, put, select, fork } from 'redux-saga/effects';
|
||||||
import type { Effect } from 'redux-saga/effects';
|
import type { Effect } from 'redux-saga/effects';
|
||||||
import { setWallet, setBalance, setTokenBalances } from 'actions/wallet';
|
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 { showNotification } from 'actions/notifications';
|
||||||
import translate from 'translations';
|
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 { BaseNode } from 'libs/nodes';
|
||||||
import { getNodeLib } from 'selectors/config';
|
import { getNodeLib } from 'selectors/config';
|
||||||
import { getWalletInst, getTokens } from 'selectors/wallet';
|
import { getWalletInst, getTokens } from 'selectors/wallet';
|
||||||
|
|
||||||
|
import { determineKeystoreType } from 'libs/keystore';
|
||||||
|
|
||||||
function* updateAccountBalance() {
|
function* updateAccountBalance() {
|
||||||
const node: BaseNode = yield select(getNodeLib);
|
const node: BaseNode = yield select(getNodeLib);
|
||||||
const wallet: ?BaseWallet = yield select(getWalletInst);
|
const wallet: ?BaseWallet = yield select(getWalletInst);
|
||||||
@ -56,7 +68,14 @@ export function* unlockPrivateKey(
|
|||||||
let wallet = null;
|
let wallet = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (action.payload.key.length === 64) {
|
||||||
wallet = new PrivKeyWallet(Buffer.from(action.payload.key, 'hex'));
|
wallet = new PrivKeyWallet(Buffer.from(action.payload.key, 'hex'));
|
||||||
|
} else {
|
||||||
|
wallet = new EncryptedPrivKeyWallet(
|
||||||
|
action.payload.key,
|
||||||
|
action.payload.password
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(showNotification('danger', translate('INVALID_PKEY')));
|
yield put(showNotification('danger', translate('INVALID_PKEY')));
|
||||||
return;
|
return;
|
||||||
@ -65,11 +84,55 @@ export function* unlockPrivateKey(
|
|||||||
yield call(updateBalances);
|
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> {
|
export default function* walletSaga(): Generator<Effect | Effect[], void, any> {
|
||||||
// useful for development
|
// useful for development
|
||||||
yield call(updateBalances);
|
yield call(updateBalances);
|
||||||
yield [
|
yield [
|
||||||
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
|
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
|
||||||
|
takeEvery('WALLET_UNLOCK_KEYSTORE', unlockKeystore),
|
||||||
takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances)
|
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