PR review fixes

This commit is contained in:
Jack Cook 2019-07-02 11:56:56 -07:00
parent bca01ea69f
commit 288442924e
6 changed files with 151 additions and 71 deletions

View File

@ -1,8 +1,8 @@
import Wallet = require('./index') import Wallet from './index'
const HDKey = require('hdkey') const HDKey = require('hdkey')
class EthereumHDKey { export default class EthereumHDKey {
public static fromMasterSeed(seedBuffer: Buffer): EthereumHDKey { public static fromMasterSeed(seedBuffer: Buffer): EthereumHDKey {
return new EthereumHDKey(HDKey.fromMasterSeed(seedBuffer)) return new EthereumHDKey(HDKey.fromMasterSeed(seedBuffer))
} }
@ -39,5 +39,3 @@ class EthereumHDKey {
return Wallet.fromPublicKey(this._hdkey._publicKey, true) return Wallet.fromPublicKey(this._hdkey._publicKey, true)
} }
} }
export = EthereumHDKey

View File

@ -52,22 +52,48 @@ function mergeToV3ParamsWithDefaults(params?: Partial<V3Params>): V3Params {
} }
} }
interface KDFParams { // KDF params
c: number
prf: string interface ScryptKDFParams {
dklen: number dklen: number
n: number n: number
r: number
p: number p: number
r: number
salt: string salt: string
} }
interface PBKDFParams {
c: number
dklen: number
prf: string
salt: string
}
// union of both the PBKDF2 and Scrypt KDF parameters representing all possible
// parameters the user could supply
type AllKDFParams = ScryptKDFParams & PBKDFParams
type KDFParams = ScryptKDFParams | PBKDFParams
function kdfParamsForPBKDF(params: AllKDFParams): PBKDFParams {
delete params.n
delete params.p
delete params.r
return params
}
function kdfParamsForScrypt(params: AllKDFParams): ScryptKDFParams {
delete params.c
delete params.prf
return params
}
/** /**
* Based on the parameter list passed to the Wallet.prototype.toV3() method this * Based on the parameter list passed to the Wallet.prototype.toV3() method this
* returns a list of parameters for running the key derivation function. * returns a list of parameters for running the key derivation function.
* @param params params passed into the .toV3() method * @param params params passed into the .toV3() method
*/ */
function mergeKDFParamsWithDefaults(params: V3Params): KDFParams { function mergeKDFParamsWithDefaults(params: V3Params): AllKDFParams {
const kdfDefaults = { const kdfDefaults = {
c: 262144, c: 262144,
prf: 'hmac-sha256', prf: 'hmac-sha256',
@ -84,24 +110,79 @@ function mergeKDFParamsWithDefaults(params: V3Params): KDFParams {
prf: kdfDefaults.prf, prf: kdfDefaults.prf,
n: params.n || kdfDefaults.n, n: params.n || kdfDefaults.n,
r: params.r || kdfDefaults.r, r: params.r || kdfDefaults.r,
p: params.p || kdfDefaults.c, p: params.p || kdfDefaults.p,
} }
} }
function stripUnusedKDFParamsForPBKDF2(params: KDFParams): Partial<KDFParams> { // JSON keystore types
delete params.n
delete params.r // https://github.com/ethereum/homestead-guide/blob/master/old-docs-for-reference/go-ethereum-wiki.rst/Passphrase-protected-key-store-spec.rst
delete params.p interface V1Keystore {
return params Address: string
Crypto: {
CipherText: string
IV: string
KeyHeader: {
Kdf: string
KdfParams: {
DkLen: number
N: number
P: number
R: number
SaltLen: number
}
Version: string
}
MAC: string
Salt: string
}
Id: string
Version: string
} }
function stripUnusedKDFParamsForScrypt(params: KDFParams): Partial<KDFParams> { // https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
delete params.c interface V3Keystore {
delete params.prf crypto: {
return params cipher: string
cipherparams: {
iv: string
}
ciphertext: string
kdf: string
kdfparams: KDFParams
mac: string
}
id: string
version: number
} }
class Wallet { interface EthSaleKeystore {
encseed: string
ethaddr: string
btcaddr: string
email: string
}
// wallet implementation
export default class Wallet {
constructor(
private readonly privateKey?: Buffer | undefined,
private publicKey: Buffer | undefined = undefined,
) {
if (privateKey && publicKey) {
throw new Error('Cannot supply both a private and a public key to the constructor')
}
if (privateKey && !ethUtil.isValidPrivate(privateKey)) {
throw new Error('Private key does not satisfy the curve requirements (ie. it is invalid)')
}
if (publicKey && !ethUtil.isValidPublic(publicKey)) {
throw new Error('Invalid public key')
}
}
// static methods // static methods
public static generate(icapDirect: boolean = false): Wallet { public static generate(icapDirect: boolean = false): Wallet {
@ -164,10 +245,8 @@ class Wallet {
return Wallet.fromPrivateKey(tmp.slice(46)) return Wallet.fromPrivateKey(tmp.slice(46))
} }
// https://github.com/ethereum/go-ethereum/wiki/Passphrase-protected-key-store-spec public static fromV1(input: string | V1Keystore, password: string): Wallet {
public static fromV1(input: string | Object, password: string): Wallet { const json: V1Keystore = typeof input === 'object' ? input : JSON.parse(input)
const json = typeof input === 'object' ? input : JSON.parse(input)
if (json.Version !== '1') { if (json.Version !== '1') {
throw new Error('Not a V1 Wallet') throw new Error('Not a V1 Wallet')
} }
@ -187,7 +266,6 @@ class Wallet {
const ciphertext = Buffer.from(json.Crypto.CipherText, 'hex') const ciphertext = Buffer.from(json.Crypto.CipherText, 'hex')
const mac = ethUtil.keccak256(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) const mac = ethUtil.keccak256(Buffer.concat([derivedKey.slice(16, 32), ciphertext]))
if (mac.toString('hex') !== json.Crypto.MAC) { if (mac.toString('hex') !== json.Crypto.MAC) {
throw new Error('Key derivation failed - possibly wrong passphrase') throw new Error('Key derivation failed - possibly wrong passphrase')
} }
@ -198,16 +276,15 @@ class Wallet {
Buffer.from(json.Crypto.IV, 'hex'), Buffer.from(json.Crypto.IV, 'hex'),
) )
const seed = runCipherBuffer(decipher, ciphertext) const seed = runCipherBuffer(decipher, ciphertext)
return new Wallet(seed) return new Wallet(seed)
} }
public static fromV3( public static fromV3(
input: string | Object, input: string | V3Keystore,
password: string, password: string,
nonStrict: boolean = false, nonStrict: boolean = false,
): Wallet { ): Wallet {
const json = const json: V3Keystore =
typeof input === 'object' ? input : JSON.parse(nonStrict ? input.toLowerCase() : input) typeof input === 'object' ? input : JSON.parse(nonStrict ? input.toLowerCase() : input)
if (json.version !== 3) { if (json.version !== 3) {
@ -264,8 +341,8 @@ class Wallet {
* Based on https://github.com/ethereum/pyethsaletool/blob/master/pyethsaletool.py * Based on https://github.com/ethereum/pyethsaletool/blob/master/pyethsaletool.py
* JSON fields: encseed, ethaddr, btcaddr, email * JSON fields: encseed, ethaddr, btcaddr, email
*/ */
public static fromEthSale(input: string | Object, password: string): Wallet { public static fromEthSale(input: string | EthSaleKeystore, password: string): Wallet {
const json = typeof input === 'object' ? input : JSON.parse(input) const json: EthSaleKeystore = typeof input === 'object' ? input : JSON.parse(input)
const encseed = Buffer.from(json.encseed, 'hex') const encseed = Buffer.from(json.encseed, 'hex')
@ -301,26 +378,6 @@ class Wallet {
return this.privateKey return this.privateKey
} }
constructor(
private readonly privateKey?: Buffer | undefined,
private publicKey: Buffer | undefined = undefined,
) {
if (privateKey && publicKey) {
throw new Error('Cannot supply both a private and a public key to the constructor')
}
if (!privateKey && !publicKey) {
}
if (privateKey && !ethUtil.isValidPrivate(privateKey)) {
throw new Error('Private key does not satisfy the curve requirements (ie. it is invalid)')
}
if (publicKey && !ethUtil.isValidPublic(publicKey)) {
throw new Error('Invalid public key')
}
}
// public instance methods // public instance methods
public getPrivateKey(): Buffer { public getPrivateKey(): Buffer {
@ -351,7 +408,7 @@ class Wallet {
return ethUtil.toChecksumAddress(this.getAddressString()) return ethUtil.toChecksumAddress(this.getAddressString())
} }
public toV3(password: string, opts?: Partial<V3Params>) { public toV3(password: string, opts?: Partial<V3Params>): V3Keystore {
if (!keyExists(this.privateKey)) { if (!keyExists(this.privateKey)) {
throw new Error('This is a public key only wallet') throw new Error('This is a public key only wallet')
} }
@ -359,7 +416,9 @@ class Wallet {
const params = mergeToV3ParamsWithDefaults(opts) const params = mergeToV3ParamsWithDefaults(opts)
const kdfParams = mergeKDFParamsWithDefaults(params) const kdfParams = mergeKDFParamsWithDefaults(params)
let derivedKey: Buffer, finalKDFParams: Partial<KDFParams> let derivedKey: Buffer
let finalKDFParams: KDFParams
if (params.kdf === 'pbkdf2') { if (params.kdf === 'pbkdf2') {
derivedKey = crypto.pbkdf2Sync( derivedKey = crypto.pbkdf2Sync(
Buffer.from(password), Buffer.from(password),
@ -368,7 +427,7 @@ class Wallet {
kdfParams.dklen, kdfParams.dklen,
'sha256', 'sha256',
) )
finalKDFParams = stripUnusedKDFParamsForPBKDF2(kdfParams) finalKDFParams = kdfParamsForPBKDF(kdfParams)
} else if (params.kdf === 'scrypt') { } else if (params.kdf === 'scrypt') {
// FIXME: support progress reporting callback // FIXME: support progress reporting callback
derivedKey = scryptsy( derivedKey = scryptsy(
@ -379,7 +438,7 @@ class Wallet {
kdfParams.p, kdfParams.p,
kdfParams.dklen, kdfParams.dklen,
) )
finalKDFParams = stripUnusedKDFParamsForScrypt(kdfParams) finalKDFParams = kdfParamsForScrypt(kdfParams)
} else { } else {
throw new Error('Unsupported kdf') throw new Error('Unsupported kdf')
} }
@ -401,6 +460,7 @@ class Wallet {
return { return {
version: 3, version: 3,
id: uuidv4({ random: params.uuid }), id: uuidv4({ random: params.uuid }),
// @ts-ignore FIXME: official V3 keystore spec omits the address key
address: this.getAddress().toString('hex'), address: this.getAddress().toString('hex'),
crypto: { crypto: {
ciphertext: ciphertext.toString('hex'), ciphertext: ciphertext.toString('hex'),
@ -442,8 +502,6 @@ function runCipherBuffer(cipher: crypto.Cipher | crypto.Decipher, data: Buffer):
return Buffer.concat([cipher.update(data), cipher.final()]) return Buffer.concat([cipher.update(data), cipher.final()])
} }
function keyExists(k: Buffer | undefined): k is Buffer { function keyExists(k: Buffer | undefined | null): k is Buffer {
return k !== undefined return k !== undefined && k !== null
} }
export = Wallet

View File

@ -1,8 +1,8 @@
import Wallet = require('./index') import Wallet from './index'
const HookedWalletEthTxSubprovider = require('web3-provider-engine/subproviders/hooked-wallet-ethtx') const HookedWalletEthTxSubprovider = require('web3-provider-engine/subproviders/hooked-wallet-ethtx')
class WalletSubprovider extends HookedWalletEthTxSubprovider { export default class WalletSubprovider extends HookedWalletEthTxSubprovider {
constructor(wallet: Wallet, opts?: any) { constructor(wallet: Wallet, opts?: any) {
if (!opts) { if (!opts) {
opts = {} opts = {}
@ -20,5 +20,3 @@ class WalletSubprovider extends HookedWalletEthTxSubprovider {
super(opts) super(opts)
} }
} }
export = WalletSubprovider

View File

@ -1,7 +1,7 @@
import * as crypto from 'crypto' import * as crypto from 'crypto'
import * as ethUtil from 'ethereumjs-util' import * as ethUtil from 'ethereumjs-util'
import Wallet = require('./index') import Wallet from './index'
const scryptsy = require('scrypt.js') const scryptsy = require('scrypt.js')
const utf8 = require('utf8') const utf8 = require('utf8')
@ -95,12 +95,30 @@ function decodeCryptojsSalt(input: string): { ciphertext: Buffer; salt?: Buffer
return { ciphertext } return { ciphertext }
} }
// {
// "address": "0x169aab499b549eac087035e640d3f7d882ef5e2d",
// "encrypted": true,
// "locked": true,
// "hash": "342f636d174cc1caa49ce16e5b257877191b663e0af0271d2ea03ac7e139317d",
// "private": "U2FsdGVkX19ZrornRBIfl1IDdcj6S9YywY8EgOeOtLj2DHybM/CHL4Jl0jcwjT+36kDnjj+qEfUBu6J1mGQF/fNcD/TsAUgGUTEUEOsP1CKDvNHfLmWLIfxqnYHhHsG5",
// "public": "U2FsdGVkX19EaDNK52q7LEz3hL/VR3dYW5VcoP04tcVKNS0Q3JINpM4XzttRJCBtq4g22hNDrOR8RWyHuh3nPo0pRSe9r5AUfEiCLaMBAhI16kf2KqCA8ah4brkya9ZLECdIl0HDTMYfDASBnyNXd87qodt46U0vdRT3PppK+9hsyqP8yqm9kFcWqMHktqubBE937LIU0W22Rfw6cJRwIw=="
// }
interface EtherWalletOptions {
address: string
encrypted: boolean
locked: boolean
hash: string
private: string
public: string
}
/* /*
* This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts * This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts
* and used on https://www.myetherwallet.com/ * and used on https://www.myetherwallet.com/
*/ */
function fromEtherWallet(input: string | Object, password: string): Wallet { function fromEtherWallet(input: string | EtherWalletOptions, password: string): Wallet {
const json = typeof input === 'object' ? input : JSON.parse(input) const json: EtherWalletOptions = typeof input === 'object' ? input : JSON.parse(input)
let privateKey: Buffer let privateKey: Buffer
if (!json.locked) { if (!json.locked) {
@ -109,16 +127,20 @@ function fromEtherWallet(input: string | Object, password: string): Wallet {
} }
privateKey = Buffer.from(json.private, 'hex') privateKey = Buffer.from(json.private, 'hex')
} else { } else {
if (typeof password !== 'string') {
throw new Error('Password required')
}
if (password.length < 7) { if (password.length < 7) {
throw new Error('Password must be at least 7 characters') throw new Error('Password must be at least 7 characters')
} }
// the "encrypted" version has the low 4 bytes // the "encrypted" version has the low 4 bytes
// of the hash of the address appended // of the hash of the address appended
let cipher = json.encrypted ? json.private.slice(0, 128) : json.private const hash = json.encrypted ? json.private.slice(0, 128) : json.private
// decode openssl ciphertext + salt encoding // decode openssl ciphertext + salt encoding
cipher = decodeCryptojsSalt(cipher) const cipher = decodeCryptojsSalt(hash)
if (!cipher.salt) { if (!cipher.salt) {
throw new Error('Unsupported EtherWallet key format') throw new Error('Unsupported EtherWallet key format')
} }
@ -183,6 +205,10 @@ function fromKryptoKit(entropy: string, password: string): Wallet {
if (type === 'd') { if (type === 'd') {
privateKey = ethUtil.sha256(entropy) privateKey = ethUtil.sha256(entropy)
} else if (type === 'q') { } else if (type === 'q') {
if (typeof password !== 'string') {
throw new Error('Password required')
}
const encryptedSeed = ethUtil.sha256(Buffer.from(entropy.slice(0, 30))) const encryptedSeed = ethUtil.sha256(Buffer.from(entropy.slice(0, 30)))
const checksum = entropy.slice(30, 46) const checksum = entropy.slice(30, 46)
@ -246,4 +272,4 @@ const Thirdparty = {
fromQuorumWallet, fromQuorumWallet,
} }
export = Thirdparty export default Thirdparty

View File

@ -1,5 +1,5 @@
import * as assert from 'assert' import * as assert from 'assert'
import EthereumHDKey = require('../src/hdkey') import EthereumHDKey from '../src/hdkey'
// from BIP39 mnemonic: awake book subject inch gentle blur grant damage process float month clown // from BIP39 mnemonic: awake book subject inch gentle blur grant damage process float month clown
const fixtureseed = Buffer.from( const fixtureseed = Buffer.from(

View File

@ -2,8 +2,8 @@
import * as assert from 'assert' import * as assert from 'assert'
import * as ethUtil from 'ethereumjs-util' import * as ethUtil from 'ethereumjs-util'
import Wallet = require('../src') import Wallet from '../src'
import Thirdparty = require('../src/thirdparty') import Thirdparty from '../src/thirdparty'
const fixturePrivateKey = 'efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378' const fixturePrivateKey = 'efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378'
const fixturePrivateKeyStr = '0x' + fixturePrivateKey const fixturePrivateKeyStr = '0x' + fixturePrivateKey