diff --git a/README.md b/README.md index d3f1b7c..9d4833a 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/index.js b/index.js index be55ea6..3c92c47 100644 --- a/index.js +++ b/index.js @@ -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 diff --git a/test/index.js b/test/index.js index 60ef537..d8222be 100644 --- a/test/index.js +++ b/test/index.js @@ -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') }) }) diff --git a/thirdparty.js b/thirdparty.js new file mode 100644 index 0000000..230b6bf --- /dev/null +++ b/thirdparty.js @@ -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