311 lines
9.3 KiB
JavaScript
311 lines
9.3 KiB
JavaScript
var Buffer = require('safe-buffer').Buffer
|
|
var ethUtil = require('ethereumjs-util')
|
|
var crypto = require('crypto')
|
|
var scryptsy = require('scrypt.js')
|
|
var uuidv4 = require('uuid/v4')
|
|
var bs58check = require('bs58check')
|
|
|
|
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 Wallet = function (priv, pub) {
|
|
if (priv && pub) {
|
|
throw new Error('Cannot supply both a private and a public key to the constructor')
|
|
}
|
|
|
|
if (priv && !ethUtil.isValidPrivate(priv)) {
|
|
throw new Error('Private key does not satisfy the curve requirements (ie. it is invalid)')
|
|
}
|
|
|
|
if (pub && !ethUtil.isValidPublic(pub)) {
|
|
throw new Error('Invalid public key')
|
|
}
|
|
|
|
this._privKey = priv
|
|
this._pubKey = pub
|
|
}
|
|
|
|
Object.defineProperty(Wallet.prototype, 'privKey', {
|
|
get: function () {
|
|
assert(this._privKey, 'This is a public key only wallet')
|
|
return this._privKey
|
|
}
|
|
})
|
|
|
|
Object.defineProperty(Wallet.prototype, 'pubKey', {
|
|
get: function () {
|
|
if (!this._pubKey) {
|
|
this._pubKey = ethUtil.privateToPublic(this.privKey)
|
|
}
|
|
return this._pubKey
|
|
}
|
|
})
|
|
|
|
Wallet.generate = function (icapDirect) {
|
|
if (icapDirect) {
|
|
var max = new ethUtil.BN('088f924eeceeda7fe92e1f5b0fffffffffffffff', 16)
|
|
while (true) {
|
|
var privKey = crypto.randomBytes(32)
|
|
if (new ethUtil.BN(ethUtil.privateToAddress(privKey)).lte(max)) {
|
|
return new Wallet(privKey)
|
|
}
|
|
}
|
|
} else {
|
|
return new Wallet(crypto.randomBytes(32))
|
|
}
|
|
}
|
|
|
|
Wallet.generateVanityAddress = function (pattern) {
|
|
if (typeof pattern !== 'object') {
|
|
pattern = new RegExp(pattern)
|
|
}
|
|
|
|
while (true) {
|
|
var privKey = crypto.randomBytes(32)
|
|
var address = ethUtil.privateToAddress(privKey)
|
|
|
|
if (pattern.test(address.toString('hex'))) {
|
|
return new Wallet(privKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
Wallet.prototype.getPrivateKey = function () {
|
|
return this.privKey
|
|
}
|
|
|
|
Wallet.prototype.getPrivateKeyString = function () {
|
|
return ethUtil.bufferToHex(this.getPrivateKey())
|
|
}
|
|
|
|
Wallet.prototype.getPublicKey = function () {
|
|
return this.pubKey
|
|
}
|
|
|
|
Wallet.prototype.getPublicKeyString = function () {
|
|
return ethUtil.bufferToHex(this.getPublicKey())
|
|
}
|
|
|
|
Wallet.prototype.getAddress = function () {
|
|
return ethUtil.publicToAddress(this.pubKey)
|
|
}
|
|
|
|
Wallet.prototype.getAddressString = function () {
|
|
return ethUtil.bufferToHex(this.getAddress())
|
|
}
|
|
|
|
Wallet.prototype.getChecksumAddressString = function () {
|
|
return ethUtil.toChecksumAddress(this.getAddressString())
|
|
}
|
|
|
|
// https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
|
|
Wallet.prototype.toV3 = function (password, opts) {
|
|
assert(this._privKey, 'This is a public key only wallet')
|
|
|
|
opts = opts || {}
|
|
var salt = opts.salt || crypto.randomBytes(32)
|
|
var iv = opts.iv || crypto.randomBytes(16)
|
|
|
|
var derivedKey
|
|
var kdf = opts.kdf || 'scrypt'
|
|
var kdfparams = {
|
|
dklen: opts.dklen || 32,
|
|
salt: salt.toString('hex')
|
|
}
|
|
|
|
if (kdf === 'pbkdf2') {
|
|
kdfparams.c = opts.c || 262144
|
|
kdfparams.prf = 'hmac-sha256'
|
|
derivedKey = crypto.pbkdf2Sync(Buffer.from(password), salt, kdfparams.c, kdfparams.dklen, 'sha256')
|
|
} else if (kdf === 'scrypt') {
|
|
// FIXME: support progress reporting callback
|
|
kdfparams.n = opts.n || 262144
|
|
kdfparams.r = opts.r || 8
|
|
kdfparams.p = opts.p || 1
|
|
derivedKey = scryptsy(Buffer.from(password), salt, kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen)
|
|
} else {
|
|
throw new Error('Unsupported kdf')
|
|
}
|
|
|
|
var cipher = crypto.createCipheriv(opts.cipher || 'aes-128-ctr', derivedKey.slice(0, 16), iv)
|
|
if (!cipher) {
|
|
throw new Error('Unsupported cipher')
|
|
}
|
|
|
|
var ciphertext = Buffer.concat([ cipher.update(this.privKey), cipher.final() ])
|
|
|
|
var mac = ethUtil.sha3(Buffer.concat([ derivedKey.slice(16, 32), Buffer.from(ciphertext, 'hex') ]))
|
|
|
|
return {
|
|
version: 3,
|
|
id: uuidv4({ random: opts.uuid || crypto.randomBytes(16) }),
|
|
address: this.getAddress().toString('hex'),
|
|
crypto: {
|
|
ciphertext: ciphertext.toString('hex'),
|
|
cipherparams: {
|
|
iv: iv.toString('hex')
|
|
},
|
|
cipher: opts.cipher || 'aes-128-ctr',
|
|
kdf: kdf,
|
|
kdfparams: kdfparams,
|
|
mac: mac.toString('hex')
|
|
}
|
|
}
|
|
}
|
|
|
|
Wallet.prototype.getV3Filename = function (timestamp) {
|
|
/*
|
|
* We want a timestamp like 2016-03-15T17-11-33.007598288Z. Date formatting
|
|
* is a pain in Javascript, everbody knows that. We could use moment.js,
|
|
* but decide to do it manually in order to save space.
|
|
*
|
|
* toJSON() returns a pretty close version, so let's use it. It is not UTC though,
|
|
* but does it really matter?
|
|
*
|
|
* Alternative manual way with padding and Date fields: http://stackoverflow.com/a/7244288/4964819
|
|
*
|
|
*/
|
|
var ts = timestamp ? new Date(timestamp) : new Date()
|
|
|
|
return [
|
|
'UTC--',
|
|
ts.toJSON().replace(/:/g, '-'),
|
|
'--',
|
|
this.getAddress().toString('hex')
|
|
].join('')
|
|
}
|
|
|
|
Wallet.prototype.toV3String = function (password, opts) {
|
|
return JSON.stringify(this.toV3(password, opts))
|
|
}
|
|
|
|
Wallet.fromPublicKey = function (pub, nonStrict) {
|
|
if (nonStrict) {
|
|
pub = ethUtil.importPublic(pub)
|
|
}
|
|
return new Wallet(null, pub)
|
|
}
|
|
|
|
Wallet.fromExtendedPublicKey = function (pub) {
|
|
assert(pub.slice(0, 4) === 'xpub', 'Not an extended public key')
|
|
pub = bs58check.decode(pub).slice(45)
|
|
// Convert to an Ethereum public key
|
|
return Wallet.fromPublicKey(pub, true)
|
|
}
|
|
|
|
Wallet.fromPrivateKey = function (priv) {
|
|
return new Wallet(priv)
|
|
}
|
|
|
|
Wallet.fromExtendedPrivateKey = function (priv) {
|
|
assert(priv.slice(0, 4) === 'xprv', 'Not an extended private key')
|
|
var tmp = bs58check.decode(priv)
|
|
assert(tmp[45] === 0, 'Invalid extended private key')
|
|
return Wallet.fromPrivateKey(tmp.slice(46))
|
|
}
|
|
|
|
// https://github.com/ethereum/go-ethereum/wiki/Passphrase-protected-key-store-spec
|
|
Wallet.fromV1 = function (input, password) {
|
|
assert(typeof password === 'string')
|
|
var json = (typeof input === 'object') ? input : JSON.parse(input)
|
|
|
|
if (json.Version !== '1') {
|
|
throw new Error('Not a V1 wallet')
|
|
}
|
|
|
|
if (json.Crypto.KeyHeader.Kdf !== 'scrypt') {
|
|
throw new Error('Unsupported key derivation scheme')
|
|
}
|
|
|
|
var kdfparams = json.Crypto.KeyHeader.KdfParams
|
|
var derivedKey = scryptsy(Buffer.from(password), Buffer.from(json.Crypto.Salt, 'hex'), kdfparams.N, kdfparams.R, kdfparams.P, kdfparams.DkLen)
|
|
|
|
var ciphertext = Buffer.from(json.Crypto.CipherText, 'hex')
|
|
|
|
var mac = ethUtil.sha3(Buffer.concat([ derivedKey.slice(16, 32), ciphertext ]))
|
|
|
|
if (mac.toString('hex') !== json.Crypto.MAC) {
|
|
throw new Error('Key derivation failed - possibly wrong passphrase')
|
|
}
|
|
|
|
var decipher = crypto.createDecipheriv('aes-128-cbc', ethUtil.sha3(derivedKey.slice(0, 16)).slice(0, 16), Buffer.from(json.Crypto.IV, 'hex'))
|
|
var seed = decipherBuffer(decipher, ciphertext)
|
|
|
|
return new Wallet(seed)
|
|
}
|
|
|
|
Wallet.fromV3 = function (input, password, nonStrict) {
|
|
assert(typeof password === 'string')
|
|
var json = (typeof input === 'object') ? input : JSON.parse(nonStrict ? input.toLowerCase() : input)
|
|
|
|
if (json.version !== 3) {
|
|
throw new Error('Not a V3 wallet')
|
|
}
|
|
|
|
var derivedKey
|
|
var kdfparams
|
|
if (json.crypto.kdf === 'scrypt') {
|
|
kdfparams = json.crypto.kdfparams
|
|
|
|
// FIXME: support progress reporting callback
|
|
derivedKey = scryptsy(Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen)
|
|
} else if (json.crypto.kdf === 'pbkdf2') {
|
|
kdfparams = json.crypto.kdfparams
|
|
|
|
if (kdfparams.prf !== 'hmac-sha256') {
|
|
throw new Error('Unsupported parameters to PBKDF2')
|
|
}
|
|
|
|
derivedKey = crypto.pbkdf2Sync(Buffer.from(password), Buffer.from(kdfparams.salt, 'hex'), kdfparams.c, kdfparams.dklen, 'sha256')
|
|
} else {
|
|
throw new Error('Unsupported key derivation scheme')
|
|
}
|
|
|
|
var ciphertext = Buffer.from(json.crypto.ciphertext, 'hex')
|
|
|
|
var mac = ethUtil.sha3(Buffer.concat([ derivedKey.slice(16, 32), ciphertext ]))
|
|
if (mac.toString('hex') !== json.crypto.mac) {
|
|
throw new Error('Key derivation failed - possibly wrong passphrase')
|
|
}
|
|
|
|
var decipher = crypto.createDecipheriv(json.crypto.cipher, derivedKey.slice(0, 16), Buffer.from(json.crypto.cipherparams.iv, 'hex'))
|
|
var seed = decipherBuffer(decipher, ciphertext, 'hex')
|
|
|
|
return new Wallet(seed)
|
|
}
|
|
|
|
/*
|
|
* Based on https://github.com/ethereum/pyethsaletool/blob/master/pyethsaletool.py
|
|
* JSON fields: encseed, ethaddr, btcaddr, email
|
|
*/
|
|
Wallet.fromEthSale = function (input, password) {
|
|
assert(typeof password === 'string')
|
|
var json = (typeof input === 'object') ? input : JSON.parse(input)
|
|
|
|
var encseed = Buffer.from(json.encseed, 'hex')
|
|
|
|
// key derivation
|
|
var derivedKey = crypto.pbkdf2Sync(password, password, 2000, 32, 'sha256').slice(0, 16)
|
|
|
|
// seed decoding (IV is first 16 bytes)
|
|
// NOTE: crypto (derived from openssl) when used with aes-*-cbc will handle PKCS#7 padding internally
|
|
// see also http://stackoverflow.com/a/31614770/4964819
|
|
var decipher = crypto.createDecipheriv('aes-128-cbc', derivedKey, encseed.slice(0, 16))
|
|
var seed = decipherBuffer(decipher, encseed.slice(16))
|
|
|
|
var wallet = new Wallet(ethUtil.sha3(seed))
|
|
if (wallet.getAddress().toString('hex') !== json.ethaddr) {
|
|
throw new Error('Decoded key mismatch - possibly wrong passphrase')
|
|
}
|
|
return wallet
|
|
}
|
|
|
|
module.exports = Wallet
|