Move third party wallet import code into its own module.

Reason is KryptoKit requires 'aesjs', which adds extra 70k of code when browserified. This change could be reverted once the code can be replaced with 'crypto'.
This commit is contained in:
Alex Beregszaszi 2016-03-08 21:56:10 +00:00
parent 6e1f951637
commit 4162a28ac6
4 changed files with 243 additions and 217 deletions

View File

@ -26,12 +26,19 @@ Constructors:
* `fromV1(input, password)` - import a wallet (Version 1 of the Ethereum wallet format)
* `fromV3(input, password)` - import a wallet (Version 3 of the Ethereum wallet format)
* `fromEthSale(input, password)` - import an Ethereum Pre Sale wallet
For the V1, V3 and EthSale formats the input is a JSON serialized string. All these formats require a password.
Third party imports:
* `fromEtherCamp(passphrase)` - import a brain wallet used by Ether.Camp
* `fromEtherWallet(input, password)` - import a wallet generated by EtherWallet
* `fromKryptoKit(seed)` - import a wallet from a KryptoKit seed
* `fromQuorumWallet(passphrase, userid)` - import a brain wallet used by Quorum Wallet
For the V1, V3 and EthSale formats the input is a JSON serialized string. All these formats require a password.
To use these, first import the appropriate submodule:
`var thirdparty = require('ethereumjs-wallet/thirdparty')`
Instance methods:

210
index.js
View File

@ -2,8 +2,6 @@ var ethUtil = require('ethereumjs-util')
var crypto = require('crypto')
var scryptsy = require('scrypt.js')
var uuid = require('uuid')
var utf8 = require('utf8')
var aesjs = require('aes-js')
function assert (val, msg) {
if (!val) {
@ -220,212 +218,4 @@ Wallet.fromEthSale = function (input, password) {
return wallet
}
/*
* opts:
* - digest - digest algorithm, defaults to md5
* - count - hash iterations
* - keysize - desired key size
* - ivsize - desired IV size
*
* Algorithm form https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html
*
* FIXME: not optimised at all
*/
function evp_kdf (data, salt, opts) {
// A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)`
function iter (block) {
var hash = crypto.createHash(opts.digest || 'md5')
hash.update(block)
hash.update(data)
hash.update(salt)
block = hash.digest()
for (var i = 1; i < (opts.count || 1); i++) {
hash = crypto.createHash(opts.digest || 'md5')
hash.update(block)
block = hash.digest()
}
return block
}
var keysize = opts.keysize || 16
var ivsize = opts.ivsize || 16
var ret = []
var i = 0
while (Buffer.concat(ret).length < (keysize + ivsize)) {
ret[i] = iter((i === 0) ? new Buffer(0) : ret[i - 1])
i++
}
var tmp = Buffer.concat(ret)
return {
key: tmp.slice(0, keysize),
iv: tmp.slice(keysize, keysize + ivsize)
}
}
// http://stackoverflow.com/questions/25288311/cryptojs-aes-pattern-always-ends-with
function decodeCryptojsSalt (input) {
var 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
}
}
}
/*
* This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts
* and used on https://www.myetherwallet.com/
*/
Wallet.fromEtherWallet = function (input, password) {
var json = (typeof input === 'object') ? input : JSON.parse(input)
var privKey
if (!json.locked) {
if (json.private.length !== 64) {
throw new Error('Invalid private key length')
}
privKey = new Buffer(json.private, 'hex')
} else {
if (typeof password !== 'string') {
throw new Error('Password required')
}
if (password.length < 7) {
throw new Error('Password must be at least 7 characters')
}
// the "encrypted" version has the low 4 bytes
// of the hash of the address appended
var cipher = json.encrypted ? json.private.slice(0, 128) : json.private
// decode openssl ciphertext + salt encoding
cipher = decodeCryptojsSalt(cipher)
// derive key/iv using OpenSSL EVP as implemented in CryptoJS
var evp = evp_kdf(new Buffer(password), cipher.salt, { keysize: 32, ivsize: 16 })
var decipher = crypto.createDecipheriv('aes-256-cbc', evp.key, evp.iv)
privKey = decipherBuffer(decipher, new Buffer(cipher.ciphertext))
// NOTE: yes, they've run it through UTF8
privKey = new Buffer(utf8.decode(privKey.toString()), 'hex')
}
var wallet = new Wallet(privKey)
if (wallet.getAddressString() !== json.address) {
throw new Error('Invalid private key or address')
}
return wallet
}
Wallet.fromEtherCamp = function (passphrase) {
return new Wallet(ethUtil.sha3(new Buffer(passphrase)))
}
Wallet.fromKryptoKit = function (entropy, password) {
function kryptoKitBrokenScryptSeed (buf) {
// js-scrypt calls `new Buffer(String(salt), 'utf8')` on the seed even though it is a buffer
//
// The `buffer`` implementation used does the below transformation (doesn't matches the current version):
// https://github.com/feross/buffer/blob/67c61181b938b17d10dbfc0a545f713b8bd59de8/index.js
function decodeUtf8Char (str) {
try {
return decodeURIComponent(str)
} catch (err) {
return String.fromCharCode(0xFFFD) // UTF 8 invalid char
}
}
var res = ''
var tmp = ''
for (var i = 0; i < buf.length; i++) {
if (buf[i] <= 0x7F) {
res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i])
tmp = ''
} else {
tmp += '%' + buf[i].toString(16)
}
}
return new Buffer(res + decodeUtf8Char(tmp))
}
if (entropy[0] === '#') {
entropy = entropy.slice(1)
}
var type = entropy[0]
entropy = entropy.slice(1)
var privKey
if (type === 'd') {
privKey = ethUtil.sha256(entropy)
} else if (type === 'q') {
if (typeof password !== 'string') {
throw new Error('Password required')
}
var encryptedSeed = ethUtil.sha256(new Buffer(entropy.slice(0, 30)))
var checksum = entropy.slice(30, 46)
var salt = kryptoKitBrokenScryptSeed(encryptedSeed)
var aesKey = scryptsy(new Buffer(password, 'utf8'), salt, 16384, 8, 1, 32)
/* FIXME: try to use `crypto` instead of `aesjs`
// NOTE: ECB doesn't use the IV, so it can be anything
var decipher = crypto.createDecipheriv("aes-256-ecb", aesKey, new Buffer(0))
// FIXME: this is a clear abuse, but seems to match how ECB in aesjs works
privKey = Buffer.concat([
decipher.update(encryptedSeed).slice(0, 16),
decipher.update(encryptedSeed).slice(0, 16),
])
*/
/* eslint-disable new-cap */
var decipher = new aesjs.ModeOfOperation.ecb(aesKey)
/* eslint-enable new-cap */
privKey = Buffer.concat([
decipher.decrypt(encryptedSeed.slice(0, 16)),
decipher.decrypt(encryptedSeed.slice(16, 32))
])
if (checksum.length > 0) {
if (checksum !== ethUtil.sha256(ethUtil.sha256(privKey)).slice(0, 8).toString('hex')) {
throw new Error('Failed to decrypt input - possibly invalid passphrase')
}
}
} else {
throw new Error('Unsupported or invalid entropy type')
}
return new Wallet(privKey)
}
Wallet.fromQuorumWallet = function (passphrase, userid) {
assert(passphrase.length >= 10)
assert(userid.length >= 10)
var seed = passphrase + userid
seed = crypto.pbkdf2Sync(seed, seed, 2000, 32, 'sha256');
return new Wallet(seed)
}
module.exports = Wallet

View File

@ -1,5 +1,6 @@
var assert = require('assert')
var Wallet = require('../')
var Thirdparty = require('../thirdparty.js')
var fixturekey = new Buffer('efca4cdd31923b50f4214af5d2ae10e7ac45a5019e9431cc195482d707485378', 'hex')
var fixturewallet = Wallet.fromPrivateKey(fixturekey)
@ -121,37 +122,37 @@ describe('.fromEthSale()', function () {
describe('.fromEtherWallet()', function () {
it('should work with unencrypted input', function () {
var etherWalletUnencrypted = '{"address":"0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c","encrypted":true,"locked":false,"hash":"b7a6621e8b125a17234d3e5c35522696a84134d98d07eab2479d020a8613c4bd","private":"a2c6222146ca2269086351fda9f8d2dfc8a50331e8a05f0f400c13653a521862","public":"2ed129b50b1a4dbbc53346bf711df6893265ad0c700fd11431b0bc3a66bd383a87b10ad835804a6cbe092e0375a0cc3524acf06b1ec7bb978bf25d2d6c35d120"}'
var wallet = Wallet.fromEtherWallet(etherWalletUnencrypted)
var wallet = Thirdparty.fromEtherWallet(etherWalletUnencrypted)
assert.equal(wallet.getAddressString(), '0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c')
})
it('should work with encrypted input', function () {
var etherWalletEncrypted = '{"address":"0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c","encrypted":true,"locked":true,"hash":"b7a6621e8b125a17234d3e5c35522696a84134d98d07eab2479d020a8613c4bd","private":"U2FsdGVkX1/hGPYlTZYGhzdwvtkoZfkeII4Ga4pSd/Ak373ORnwZE4nf/FFZZFcDTSH1X1+AmewadrW7dqvwr76QMYQVlihpPaFV307hWgKckkG0Mf/X4gJIQQbDPiKdcff9","public":"U2FsdGVkX1/awUDAekZQbEiXx2ct4ugXwgBllY0Hz+IwYkHiEhhxH+obu7AF7PCU2Vq5c0lpCzBUSvk2EvFyt46bw1OYIijw0iOr7fWMJEkz3bfN5mt9pYJIiPzN0gxM8u4mrmqLPUG2SkoZhWz4NOlqRUHZq7Ep6aWKz7KlEpzP9IrvDYwGubci4h+9wsspqtY1BdUJUN59EaWZSuOw1g=="}'
var wallet = Wallet.fromEtherWallet(etherWalletEncrypted, 'testtest')
var wallet = Thirdparty.fromEtherWallet(etherWalletEncrypted, 'testtest')
assert.equal(wallet.getAddressString(), '0x9d6abd11d36cc20d4836c25967f1d9efe6b1a27c')
})
})
describe('.fromEtherCamp()', function () {
it('should work with seed text', function () {
var wallet = Wallet.fromEtherCamp('ethercamp123')
var wallet = Thirdparty.fromEtherCamp('ethercamp123')
assert.equal(wallet.getAddressString(), '0x182b6ca390224c455f11b6337d74119305014ed4')
})
})
describe('.fromKryptoKit()', function () {
it('should work with basic input (d-type)', function () {
var wallet = Wallet.fromKryptoKit('dBWfH8QZSGbg1sAYHLBhqE5R8VGAoM7')
var wallet = Thirdparty.fromKryptoKit('dBWfH8QZSGbg1sAYHLBhqE5R8VGAoM7')
assert.equal(wallet.getAddressString(), '0x3611981ad2d6fc1d7579d6ce4c6bc37e272c369c')
})
it('should work with encrypted input (q-type)', function () {
var wallet = Wallet.fromKryptoKit('qhah1VeT0RgTvff1UKrUrxtFViiQuki16dd353d59888c25', 'testtest')
var wallet = Thirdparty.fromKryptoKit('qhah1VeT0RgTvff1UKrUrxtFViiQuki16dd353d59888c25', 'testtest')
assert.equal(wallet.getAddressString(), '0x3c753e27834db67329d1ec1fab67970ec1e27112')
})
})
describe('.fromQuorumWallet()', function () {
it('should work', function () {
var wallet = Wallet.fromQuorumWallet('testtesttest', 'ethereumjs-wallet')
var wallet = Thirdparty.fromQuorumWallet('testtesttest', 'ethereumjs-wallet')
assert.equal(wallet.getAddressString(), '0x1b86ccc22e8f137f204a41a23033541242a48815')
})
})

228
thirdparty.js Normal file
View File

@ -0,0 +1,228 @@
var Wallet = require('./index.js')
var ethUtil = require('ethereumjs-util')
var crypto = require('crypto')
var scryptsy = require('scrypt.js')
var utf8 = require('utf8')
var aesjs = require('aes-js')
function assert (val, msg) {
if (!val) {
throw new Error(msg || 'Assertion failed')
}
}
function decipherBuffer (decipher, data) {
return Buffer.concat([ decipher.update(data), decipher.final() ])
}
var Thirdparty = {}
/*
* opts:
* - digest - digest algorithm, defaults to md5
* - count - hash iterations
* - keysize - desired key size
* - ivsize - desired IV size
*
* Algorithm form https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html
*
* FIXME: not optimised at all
*/
function evp_kdf (data, salt, opts) {
// A single EVP iteration, returns `D_i`, where block equlas to `D_(i-1)`
function iter (block) {
var hash = crypto.createHash(opts.digest || 'md5')
hash.update(block)
hash.update(data)
hash.update(salt)
block = hash.digest()
for (var i = 1; i < (opts.count || 1); i++) {
hash = crypto.createHash(opts.digest || 'md5')
hash.update(block)
block = hash.digest()
}
return block
}
var keysize = opts.keysize || 16
var ivsize = opts.ivsize || 16
var ret = []
var i = 0
while (Buffer.concat(ret).length < (keysize + ivsize)) {
ret[i] = iter((i === 0) ? new Buffer(0) : ret[i - 1])
i++
}
var tmp = Buffer.concat(ret)
return {
key: tmp.slice(0, keysize),
iv: tmp.slice(keysize, keysize + ivsize)
}
}
// http://stackoverflow.com/questions/25288311/cryptojs-aes-pattern-always-ends-with
function decodeCryptojsSalt (input) {
var 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
}
}
}
/*
* This wallet format is created by https://github.com/SilentCicero/ethereumjs-accounts
* and used on https://www.myetherwallet.com/
*/
Thirdparty.fromEtherWallet = function (input, password) {
var json = (typeof input === 'object') ? input : JSON.parse(input)
var privKey
if (!json.locked) {
if (json.private.length !== 64) {
throw new Error('Invalid private key length')
}
privKey = new Buffer(json.private, 'hex')
} else {
if (typeof password !== 'string') {
throw new Error('Password required')
}
if (password.length < 7) {
throw new Error('Password must be at least 7 characters')
}
// the "encrypted" version has the low 4 bytes
// of the hash of the address appended
var cipher = json.encrypted ? json.private.slice(0, 128) : json.private
// decode openssl ciphertext + salt encoding
cipher = decodeCryptojsSalt(cipher)
// derive key/iv using OpenSSL EVP as implemented in CryptoJS
var evp = evp_kdf(new Buffer(password), cipher.salt, { keysize: 32, ivsize: 16 })
var decipher = crypto.createDecipheriv('aes-256-cbc', evp.key, evp.iv)
privKey = decipherBuffer(decipher, new Buffer(cipher.ciphertext))
// NOTE: yes, they've run it through UTF8
privKey = new Buffer(utf8.decode(privKey.toString()), 'hex')
}
var wallet = new Wallet(privKey)
if (wallet.getAddressString() !== json.address) {
throw new Error('Invalid private key or address')
}
return wallet
}
Thirdparty.fromEtherCamp = function (passphrase) {
return new Wallet(ethUtil.sha3(new Buffer(passphrase)))
}
Thirdparty.fromKryptoKit = function (entropy, password) {
function kryptoKitBrokenScryptSeed (buf) {
// js-scrypt calls `new Buffer(String(salt), 'utf8')` on the seed even though it is a buffer
//
// The `buffer`` implementation used does the below transformation (doesn't matches the current version):
// https://github.com/feross/buffer/blob/67c61181b938b17d10dbfc0a545f713b8bd59de8/index.js
function decodeUtf8Char (str) {
try {
return decodeURIComponent(str)
} catch (err) {
return String.fromCharCode(0xFFFD) // UTF 8 invalid char
}
}
var res = ''
var tmp = ''
for (var i = 0; i < buf.length; i++) {
if (buf[i] <= 0x7F) {
res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i])
tmp = ''
} else {
tmp += '%' + buf[i].toString(16)
}
}
return new Buffer(res + decodeUtf8Char(tmp))
}
if (entropy[0] === '#') {
entropy = entropy.slice(1)
}
var type = entropy[0]
entropy = entropy.slice(1)
var privKey
if (type === 'd') {
privKey = ethUtil.sha256(entropy)
} else if (type === 'q') {
if (typeof password !== 'string') {
throw new Error('Password required')
}
var encryptedSeed = ethUtil.sha256(new Buffer(entropy.slice(0, 30)))
var checksum = entropy.slice(30, 46)
var salt = kryptoKitBrokenScryptSeed(encryptedSeed)
var aesKey = scryptsy(new Buffer(password, 'utf8'), salt, 16384, 8, 1, 32)
/* FIXME: try to use `crypto` instead of `aesjs`
// NOTE: ECB doesn't use the IV, so it can be anything
var decipher = crypto.createDecipheriv("aes-256-ecb", aesKey, new Buffer(0))
// FIXME: this is a clear abuse, but seems to match how ECB in aesjs works
privKey = Buffer.concat([
decipher.update(encryptedSeed).slice(0, 16),
decipher.update(encryptedSeed).slice(0, 16),
])
*/
/* eslint-disable new-cap */
var decipher = new aesjs.ModeOfOperation.ecb(aesKey)
/* eslint-enable new-cap */
privKey = Buffer.concat([
decipher.decrypt(encryptedSeed.slice(0, 16)),
decipher.decrypt(encryptedSeed.slice(16, 32))
])
if (checksum.length > 0) {
if (checksum !== ethUtil.sha256(ethUtil.sha256(privKey)).slice(0, 8).toString('hex')) {
throw new Error('Failed to decrypt input - possibly invalid passphrase')
}
}
} else {
throw new Error('Unsupported or invalid entropy type')
}
return new Wallet(privKey)
}
Thirdparty.fromQuorumWallet = function (passphrase, userid) {
assert(passphrase.length >= 10)
assert(userid.length >= 10)
var seed = passphrase + userid
seed = crypto.pbkdf2Sync(seed, seed, 2000, 32, 'sha256');
return new Wallet(seed)
}
module.exports = Thirdparty