master key. use locks to avoid race conditions in wallet.

This commit is contained in:
Christopher Jeffrey 2016-06-01 14:59:23 -07:00
parent c3ba9808b1
commit 64af74fe4a
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
4 changed files with 360 additions and 75 deletions

View File

@ -925,7 +925,7 @@ HDPrivateKey.parseBase58 = function parseBase58(xkey) {
*/
HDPrivateKey.parseRaw = function parseRaw(raw) {
var p = new BufferReader(raw, true);
var p = new BufferReader(raw);
var data = {};
var i, type, prefix;
@ -1426,7 +1426,7 @@ HDPublicKey.parseBase58 = function parseBase58(xkey) {
*/
HDPublicKey.parseRaw = function parseRaw(raw) {
var p = new BufferReader(raw, true);
var p = new BufferReader(raw);
var data = {};
data.version = p.readU32BE();

View File

@ -53,6 +53,7 @@ function Wallet(options) {
this.options = options;
this.network = bcoin.network.get(options.network);
this.db = options.db;
this.locker = new bcoin.locker(this);
if (!master)
master = bcoin.hd.fromMnemonic(null, this.network);
@ -146,6 +147,8 @@ Wallet.prototype.destroy = function destroy(callback) {
assert(!this.loading);
this.master.destroy();
try {
this.db.unregister(this);
} catch (e) {
@ -219,12 +222,20 @@ Wallet.prototype.init = function init(callback) {
*/
Wallet.prototype.addKey = function addKey(account, key, callback) {
var unlock;
if (typeof key === 'function') {
callback = key;
key = account;
account = 0;
}
unlock = this.locker.lock(addKey, [account, key, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
this.getAccount(account, function(err, account) {
if (err)
return callback(err);
@ -233,7 +244,7 @@ Wallet.prototype.addKey = function addKey(account, key, callback) {
return callback(new Error('Account not found.'));
account.addKey(key, callback);
});
}, true);
};
/**
@ -244,12 +255,20 @@ Wallet.prototype.addKey = function addKey(account, key, callback) {
*/
Wallet.prototype.removeKey = function removeKey(account, key, callback) {
var unlock;
if (typeof key === 'function') {
callback = key;
key = account;
account = 0;
}
unlock = this.locker.lock(removeKey, [account, key, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
this.getAccount(account, function(err, account) {
if (err)
return callback(err);
@ -258,7 +277,7 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) {
return callback(new Error('Account not found.'));
account.addKey(key, callback);
});
}, true);
};
/**
@ -294,6 +313,25 @@ Wallet.prototype.setPassphrase = function setPassphrase(old, new_, callback) {
return this.save(callback);
};
/**
* Lock the wallet, destroy decrypted key.
*/
Wallet.prototype.lock = function lock() {
this.master.destroy();
};
/**
* Unlock the key for `timeout` milliseconds.
* @param {Buffer|String} passphrase
* @param {Number?} [timeout=60000] - ms.
*/
Wallet.prototype.unlock = function unlock(passphrase, timeout) {
this.master.toKey(passphrase, timeout);
};
/**
* Generate the wallet ID if none was passed in.
* It is represented as `m/44'` (public) hashed
@ -325,12 +363,18 @@ Wallet.prototype.getID = function getID() {
* @param {Function} callback - Returns [Error, {@link Account}].
*/
Wallet.prototype.createAccount = function createAccount(options, callback) {
Wallet.prototype.createAccount = function createAccount(options, callback, force) {
var self = this;
var master, key;
var master, key, unlock;
unlock = this.locker.lock(createAccount, [options, callback], force);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
try {
master = this.master.toKey(options.passphrase);
master = this.master.toKey(options.passphrase, options.timeout);
} catch (e) {
return callback(e);
}
@ -364,17 +408,33 @@ Wallet.prototype.createAccount = function createAccount(options, callback) {
});
};
/**
* List account names and indexes from the db.
* @param {Function} callback - Returns [Error, Array].
*/
Wallet.prototype.getAccounts = function getAccounts(callback) {
this.db.getAccounts(this.id, callback);
};
/**
* Retrieve an account from the database.
* @param {Number|String} account
* @param {Function} callback - Returns [Error, {@link Account}].
*/
Wallet.prototype.getAccount = function getAccount(account, callback) {
Wallet.prototype.getAccount = function getAccount(account, callback, force) {
var unlock = this.locker.lock(getAccount, [account, callback], force);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
if (this.account) {
if (account === 0 || account === 'default')
return callback(null, this.account);
}
return this.db.getAccount(this.id, account, callback);
};
@ -414,11 +474,20 @@ Wallet.prototype.createChange = function createChange(account, callback) {
*/
Wallet.prototype.createAddress = function createAddress(account, change, callback) {
var unlock;
if (typeof change === 'function') {
callback = change;
change = account;
account = 0;
}
unlock = this.locker.lock(createAddress, [account, change, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
this.getAccount(account, function(err, account) {
if (err)
return callback(err);
@ -427,7 +496,7 @@ Wallet.prototype.createAddress = function createAddress(account, change, callbac
return callback(new Error('Account not found.'));
account.createAddress(change, callback);
});
}, true);
};
/**
@ -767,7 +836,13 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) {
var self = this;
var accounts = {};
var result = false;
var i, path;
var i, path, unlock;
unlock = this.locker.lock(syncOutputDepth, [tx, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
this.getOutputPaths(tx, function(err, paths) {
if (err)
@ -825,7 +900,7 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(tx, callback) {
next();
});
});
});
}, true);
}, function(err) {
if (err)
return callback(err);
@ -876,6 +951,7 @@ Wallet.prototype.getRedeem = function getRedeem(hash, callback) {
/**
* Zap stale TXs from wallet (accesses db).
* @param {(Number|String)?} account
* @param {Number} age - Age threshold (unix time, default=72 hours).
* @param {Function} callback - Returns [Error].
*/
@ -906,6 +982,13 @@ Wallet.prototype.zap = function zap(account, age, callback) {
Wallet.prototype.scan = function scan(getByAddress, callback) {
var self = this;
var total = 0;
var unlock;
unlock = this.locker.lock(scan, [getByAddress, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
if (!this.initialized)
return callback(new Error('Wallet is not initialized.'));
@ -923,7 +1006,7 @@ Wallet.prototype.scan = function scan(getByAddress, callback) {
total += result;
self.createAccount(self.options, next);
self.createAccount(self.options, next, true);
});
})(null, this.account);
};
@ -990,7 +1073,7 @@ Wallet.prototype.sign = function sign(tx, options, callback) {
return callback(err);
try {
master = self.master.toKey(options.passphrase);
master = self.master.toKey(options.passphrase, options.timeout);
} catch (e) {
return callback(e);
}
@ -1526,7 +1609,10 @@ Wallet.isWallet = function isWallet(obj) {
};
/**
* BIP44 Account
* Represents a BIP44 Account belonging to a {@link Wallet}.
* Note that this object does not enforce locks. Any method
* that does a write is internal API only and will lead
* to race conditions if used elsewhere.
* @exports Account
* @constructor
* @param {Object} options
@ -1998,11 +2084,11 @@ Account.prototype.deriveAddress = function deriveAddress(change, index) {
return new bcoin.keyring({
network: this.network,
key: key.publicKey,
name: this.name,
account: this.accountIndex,
change: change,
index: index,
type: this.type,
name: this.name,
witness: this.witness,
m: this.m,
n: this.n,
@ -2373,8 +2459,12 @@ Account.isAccount = function isAccount(obj) {
&& obj.deriveAddress === 'function';
};
/*
* Master Key
/**
* Master BIP32 key which can exist
* in an timed out encrypted state.
* @exports Master
* @constructor
* @param {Object} options
*/
function MasterKey(options) {
@ -2386,30 +2476,101 @@ function MasterKey(options) {
this.phrase = options.phrase;
this.passphrase = options.passphrase;
this.key = options.key || null;
this.timer = null;
this._destroy = this.destroy.bind(this);
assert(this.encrypted ? !this.key : this.key);
}
MasterKey.prototype.toKey = function toKey(passphrase) {
/**
* Decrypt the key and set a timeout to destroy decrypted data.
* @param {Buffer|String} passphrase - Zero this yourself.
* @param {Number} [timeout=60000] timeout in ms.
* @returns {HDPrivateKey}
*/
MasterKey.prototype.toKey = function toKey(passphrase, timeout) {
var self = this;
var xprivkey;
if (this.key)
return this.key;
if (this.encrypted) {
assert(passphrase, 'Passphrase is required.');
if (!this.key) {
assert(this.encrypted);
xprivkey = utils.decrypt(this.xprivkey, passphrase);
} else {
xprivkey = this.xprivkey;
this.key = bcoin.hd.fromRaw(xprivkey);
xprivkey.fill(0);
this.start(timeout);
}
return bcoin.hd.fromRaw(xprivkey);
return this.key;
};
MasterKey.prototype.decrypt = function decrypt(passphrase) {
if (!this.encrypted)
/**
* Start the destroy timer.
* @private
* @param {Number} [timeout=60000] timeout in ms.
*/
MasterKey.prototype.start = function start(timeout) {
if (!timeout)
timeout = 60000;
this.stop();
if (timeout === -1)
return;
this.timer = setTimeout(this._destroy, timeout);
};
/**
* Stop the destroy timer.
* @private
*/
MasterKey.prototype.stop = function stop() {
if (this.timer != null) {
clearTimeout(this.timer);
this.timer = null;
}
};
/**
* Destroy the key by zeroing the
* privateKey and chainCode. Stop
* the timer if there is one.
*/
MasterKey.prototype.destroy = function destroy() {
if (!this.encrypted) {
assert(this.timer == null);
assert(this.key);
return;
}
this.stop();
if (this.key) {
this.key.chainCode.fill(0);
this.key.privateKey.fill(0);
this.key = null;
}
};
/**
* Decrypt the key permanently.
* @param {Buffer|String} passphrase - Zero this yourself.
*/
MasterKey.prototype.decrypt = function decrypt(passphrase) {
if (!this.encrypted) {
assert(this.key);
return;
}
assert(passphrase, 'Passphrase is required.');
this.destroy();
this.encrypted = false;
this.xprivkey = utils.decrypt(this.xprivkey, passphrase);
@ -2418,10 +2579,19 @@ MasterKey.prototype.decrypt = function decrypt(passphrase) {
this.passphrase = utils.decrypt(this.passphrase, passphrase);
}
this.key = this.toKey();
this.key = bcoin.hd.fromRaw(this.xprivkey);
};
/**
* Encrypt the key permanently.
* @param {Buffer|String} passphrase - Zero this yourself.
*/
MasterKey.prototype.encrypt = function encrypt(passphrase) {
var xprivkey = this.xprivkey;
var phrase = this.phrase;
var pass = this.passphrase;
if (this.encrypted)
return;
@ -2429,14 +2599,23 @@ MasterKey.prototype.encrypt = function encrypt(passphrase) {
this.key = null;
this.encrypted = true;
this.xprivkey = utils.encrypt(this.xprivkey, passphrase);
this.xprivkey = utils.encrypt(xprivkey, passphrase);
xprivkey.fill(0);
if (this.phrase) {
this.phrase = utils.encrypt(this.phrase, passphrase);
this.passphrase = utils.encrypt(this.passphrase, passphrase);
this.phrase = utils.encrypt(phrase, passphrase);
this.passphrase = utils.encrypt(pass, passphrase);
phrase.fill(0);
pass.fill(0);
}
};
/**
* Serialize the key in the form of:
* `[enc-flag][phrase-marker][phrase?][passphrase?][xprivkey]`
* @returns {Buffer}
*/
MasterKey.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer);
@ -2458,89 +2637,129 @@ MasterKey.prototype.toRaw = function toRaw(writer) {
return p;
};
MasterKey.fromRaw = function fromRaw(raw) {
var data = {};
var p = new BufferReader(raw);
/**
* Instantiate master key from serialized data.
* @returns {MasterKey}
*/
data.encrypted = p.readU8() === 1;
MasterKey.fromRaw = function fromRaw(raw) {
var p = new BufferReader(raw);
var encrypted, phrase, passphrase, xprivkey, key;
encrypted = p.readU8() === 1;
if (p.readU8() === 1) {
data.phrase = p.readVarBytes();
data.passphrase = p.readVarBytes();
phrase = p.readVarBytes();
passphrase = p.readVarBytes();
}
data.xprivkey = p.readBytes(82);
xprivkey = p.readBytes(82);
if (!data.encrypted)
data.key = bcoin.hd.fromRaw(data.xprivkey);
if (!encrypted)
key = bcoin.hd.fromRaw(xprivkey);
return new MasterKey(data);
return new MasterKey({
encrypted: encrypted,
phrase: phrase,
passphrase: passphrase,
xprivkey: xprivkey,
key: key
});
};
/**
* Instantiate master key from an HDPrivateKey.
* @param {HDPrivateKey} key
* @returns {MasterKey}
*/
MasterKey.fromKey = function fromKey(key) {
var data = {};
data.encrypted = false;
var phrase, passphrase;
if (key.mnemonic) {
data.phrase = new Buffer(key.mnemonic.phrase, 'utf8');
data.passphrase = new Buffer(key.mnemonic.passphrase, 'utf8');
phrase = new Buffer(key.mnemonic.phrase, 'utf8');
passphrase = new Buffer(key.mnemonic.passphrase, 'utf8');
}
data.xprivkey = key.toRaw();
data.key = key;
return new MasterKey(data);
return new MasterKey({
encrypted: false,
phrase: phrase,
passphrase: passphrase,
xprivkey: key.toRaw(),
key: key
});
};
MasterKey.prototype.toJSON = function toJSON() {
var json = {};
/**
* Convert master key to a jsonifiable object.
* @returns {Object}
*/
json.encrypted = this.encrypted;
MasterKey.prototype.toJSON = function toJSON() {
var phrase, passphrase, xprivkey;
if (this.encrypted) {
if (this.phrase) {
json.phrase = this.phrase.toString('hex');
json.passphrase = this.passphrase.toString('hex');
phrase = this.phrase.toString('hex');
passphrase = this.passphrase.toString('hex');
}
json.xprivkey = this.xprivkey.toString('hex');
xprivkey = this.xprivkey.toString('hex');
} else {
if (this.phrase) {
json.phrase = this.phrase.toString('utf8');
json.passphrase = this.passphrase.toString('utf8');
phrase = this.phrase.toString('utf8');
passphrase = this.passphrase.toString('utf8');
}
json.xprivkey = utils.toBase58(this.xprivkey);
xprivkey = utils.toBase58(this.xprivkey);
}
return json;
return {
encrypted: this.encrypted,
phrase: phrase,
passphrase: passphrase,
xprivkey: xprivkey
};
};
MasterKey.fromJSON = function fromJSON(json) {
var data = {};
/**
* Instantiate master key from jsonified object.
* @returns {MasterKey}
*/
data.encrypted = json.encrypted;
MasterKey.fromJSON = function fromJSON(json) {
var phrase, passphrase, xprivkey, key;
if (json.encrypted) {
if (json.phrase) {
data.phrase = new Buffer(json.phrase, 'hex');
data.passphrase = new Buffer(json.passphrase, 'hex');
phrase = new Buffer(json.phrase, 'hex');
passphrase = new Buffer(json.passphrase, 'hex');
}
data.xprivkey = new Buffer(json.xprivkey, 'hex');
xprivkey = new Buffer(json.xprivkey, 'hex');
} else {
if (json.phrase) {
data.phrase = new Buffer(json.phrase, 'utf8');
data.passphrase = new Buffer(json.passphrase, 'utf8');
phrase = new Buffer(json.phrase, 'utf8');
passphrase = new Buffer(json.passphrase, 'utf8');
}
data.xprivkey = utils.fromBase58(json.xprivkey);
xprivkey = utils.fromBase58(json.xprivkey);
}
if (!data.encrypted)
data.key = bcoin.hd.fromRaw(data.xprivkey);
if (!json.encrypted)
key = bcoin.hd.fromRaw(xprivkey);
return new MasterKey(data);
return new MasterKey({
encrypted: json.encrypted,
phrase: phrase,
passphrase: passphrase,
xprivkey: xprivkey,
key: key
});
};
/**
* Test whether an object is a MasterKey.
* @param {Object} obj
* @returns {Boolean}
*/
MasterKey.isMasterKey = function isMasterKey(obj) {
return obj
&& typeof obj.encrypted === 'boolean'

View File

@ -505,6 +505,31 @@ WalletDB.prototype.getAccount = function getAccount(id, name, callback) {
});
};
/**
* List account names and indexes from the db.
* @param {WalletID} id
* @param {Function} callback - Returns [Error, Array].
*/
WalletDB.prototype.getAccounts = function getAccounts(id, callback) {
var accounts = [];
this.db.iterate({
gte: 'i/' + id + '/',
lte: 'i/' + id + '/~',
values: true,
parse: function(value, key) {
var name = key.split('/')[2];
var index = value.readUInt32LE(0, true);
accounts[index] = name;
}
}, function(err) {
if (err)
return callback(err);
return callback(null, accounts);
});
};
/**
* Lookup the corresponding account name's index.
* @param {WalletID} id

View File

@ -768,7 +768,11 @@ describe('Wallet', function() {
w1.fill(t3, { rate: 10000, round: true }, function(err) {
assert(err);
assert.equal(err.requiredFunds, 25000);
cb();
w1.getAccounts(function(err, accounts) {
assert.ifError(err);
assert.deepEqual(accounts, ['default', 'foo']);
cb();
});
});
});
});
@ -843,6 +847,43 @@ describe('Wallet', function() {
});
});
it('should fill tx with inputs when encrypted', function(cb) {
wdb.create({ passphrase: 'foo' }, function(err, w1) {
assert.ifError(err);
w1.master.destroy();
// Coinbase
var t1 = bcoin.mtx()
.addOutput(w1, 5460)
.addOutput(w1, 5460)
.addOutput(w1, 5460)
.addOutput(w1, 5460);
t1.addInput(dummyInput);
wdb.addTX(t1, function(err) {
assert.ifError(err);
// Create new transaction
var t2 = bcoin.mtx().addOutput(w1, 5460);
w1.fill(t2, { rate: 10000, round: true }, function(err) {
assert.ifError(err);
// Should fail
w1.sign(t2, 'bar', function(err) {
assert(err);
assert(!t2.verify());
// Should succeed
w1.sign(t2, 'foo', function(err) {
assert.ifError(err);
assert(t2.verify());
cb();
});
});
});
});
});
});
it('should cleanup', function(cb) {
constants.tx.COINBASE_MATURITY = 100;
cb();