api keys.

This commit is contained in:
Christopher Jeffrey 2016-07-12 11:52:16 -07:00
parent 2b41cfb9bd
commit 4a8768bcd2
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
6 changed files with 381 additions and 58 deletions

View File

@ -45,7 +45,6 @@ var USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)'
function request(options, callback, stream) {
var qs = require('querystring');
var url = require('url');
var uri = options.uri;
var query = options.query;
var body = options.body;

View File

@ -43,6 +43,16 @@ function HTTPServer(options) {
this.pool = this.node.pool;
this.logger = options.logger || this.node.logger;
this.loaded = false;
this.apiKey = options.apiKey;
if (this.apiKey) {
if (typeof this.apiKey === 'string') {
assert(utils.isHex(this.apiKey), 'API key must be a hex string.');
this.apiKey = new Buffer(this.apiKey, 'hex');
}
assert(Buffer.isBuffer(this.apiKey));
assert(this.apiKey.length === 32, 'API key must be 32 bytes.');
}
options.sockets = true;
@ -62,8 +72,8 @@ HTTPServer.prototype._init = function _init() {
var self = this;
this.server.on('request', function(req, res) {
self.logger.debug('Request from %s path=%s',
req.socket.remoteAddress, req.pathname);
self.logger.debug('Request for path=%s (%s).',
req.pathname, req.socket.remoteAddress);
});
this.use(function(req, res, next, send) {
@ -87,6 +97,30 @@ HTTPServer.prototype._init = function _init() {
next();
});
this.use(function(req, res, next, send) {
var auth = req.headers['authorization'];
var parts;
if (!auth) {
req.username = null;
req.password = null;
return next();
}
parts = auth.split(' ');
assert(parts.length === 2, 'Invalid auth token.');
assert(parts[0] === 'Basic', 'Invalid auth token.');
auth = new Buffer(parts[1], 'base64').toString('utf8');
parts = auth.split(':');
assert(parts.length >= 2, 'Invalid auth token.');
req.username = parts.shift();
req.password = parts.join(':');
next();
});
this.use(function(req, res, next, send) {
var params = utils.merge({}, req.params, req.query, req.body);
var options = {};
@ -184,16 +218,52 @@ HTTPServer.prototype._init = function _init() {
if (params.passphrase)
options.passphrase = params.passphrase;
if (req.password) {
assert(utils.isHex(req.password), 'API key must be a hex string.');
assert(req.password.length === 64, 'API key must be 32 bytes.');
options.apiKey = new Buffer(req.password, 'hex');
}
req.options = options;
next();
});
this.use(function(req, res, next, send) {
if (req.path.length < 2 || req.path[0] !== 'wallet') {
if (self.apiKey) {
if (!utils.ccmp(req.options.apiKey, self.apiKey)) {
res.setHeader('WWW-Authenticate', 'Basic realm="node"');
send(401, { error: 'Unauthorized.' });
return;
}
}
return next();
}
if (!self.options.auth)
return next();
self.walletdb.auth(req.options.id, req.options.apiKey, function(err) {
if (err) {
if (err.message === 'Wallet not found.')
return next();
self.logger.info('Auth failure for %s: %s.',
req.options.id, err.message);
res.setHeader('WWW-Authenticate', 'Basic realm="wallet"');
send(401, { error: err.message });
return;
}
next();
});
});
function decodeScript(script) {
if (!script)
return;
if (typeof script === 'string')
return new bcoin.script(new Buffer(script, 'hex'));
return bcoin.script.fromRaw(script, 'hex');
return new bcoin.script(script);
}
@ -356,9 +426,7 @@ HTTPServer.prototype._init = function _init() {
if (err)
return next(err);
send(200, {
success: true
});
send(200, { success: true });
});
});
@ -421,6 +489,42 @@ HTTPServer.prototype._init = function _init() {
});
});
// Change passphrase
this.post('/wallet/:id/passphrase', function(req, res, next, send) {
var id = req.options.id;
var options = req.options;
var old = options.old;
var new_ = options.passphrase;
self.walletdb.setPassphrase(id, old, _new, function(err) {
if (err)
return next(err);
send(200, { success: true });
});
});
// Generate new token
this.post('/wallet/:id/retoken', function(req, res, next, send) {
var id = req.options.id;
var options = req.options;
self.walletdb.retoken(id, options.passphrase, function(err, token) {
if (err)
return next(err);
send(200, { token: token.toString('hex') });
});
});
// Broadcast TX
this.post('/wallet/:id/broadcast', function(req, res, next, send) {
self.node.sendTX(req.options.tx, function(err) {
if (err)
return next(err);
send(200, { success: true });
});
});
// Send TX
this.post('/wallet/:id/send', function(req, res, next, send) {
var id = req.options.id;
@ -498,9 +602,7 @@ HTTPServer.prototype._init = function _init() {
if (err)
return next(err);
send(200, {
success: true
});
send(200, { success: true });
});
});
@ -731,8 +833,27 @@ HTTPServer.prototype._initIO = function _initIO() {
self.emit('error', err);
});
socket.on('join', function(id) {
socket.join(id);
socket.on('join', function(id, apiKey) {
if (!self.options.auth) {
socket.join(id);
return;
}
if (id === '!all') {
if (self.apiKey) {
if (!utils.ccmp(apiKey, self.apiKey)) {
self.logger.info('Auth failure for %s.', id);
return;
}
}
return socket.join(id);
}
self.walletdb.auth(id, apiKey, function(err) {
if (err) {
self.logger.info('Auth failure for %s: %s.', id, err.message);
return;
}
socket.join(id);
});
});
socket.on('leave', function(id) {
@ -748,37 +869,30 @@ HTTPServer.prototype._initIO = function _initIO() {
});
this.walletdb.on('tx', function(tx, map) {
var summary = map.toJSON();
tx = tx.toJSON();
map.all.forEach(function(id) {
self.server.io.to(id).emit('tx', tx);
map.getWallets().forEach(function(id) {
self.server.io.to(id).emit('tx', tx, summary);
});
self.server.io.to('!all').emit('tx', tx, map);
self.server.io.to('!all').emit('tx', tx, summary);
});
this.walletdb.on('confirmed', function(tx, map) {
var summary = map.toJSON();
tx = tx.toJSON();
map.all.forEach(function(id) {
self.server.io.to(id).emit('confirmed', tx);
map.getWallets().forEach(function(id) {
self.server.io.to(id).emit('confirmed', tx, summary);
});
self.server.io.to('!all').emit('confirmed', tx, map);
self.server.io.to('!all').emit('confirmed', tx, summary);
});
this.walletdb.on('updated', function(tx, map) {
var summary = map.toJSON();
tx = tx.toJSON();
map.all.forEach(function(id) {
self.server.io.to(id).emit('updated', tx);
map.getWallets().forEach(function(id) {
self.server.io.to(id).emit('updated', tx, summary);
});
self.server.io.to('!all').emit('updated', tx, map);
});
this.walletdb.on('balance', function(balance, id) {
var json = {
confirmed: utils.btc(balance.confirmed),
unconfirmed: utils.btc(balance.unconfirmed),
total: utils.btc(balance.total)
};
self.server.io.to(id).emit('balance', json);
self.server.io.to('!all').emit('balance', json, id);
self.server.io.to('!all').emit('updated', tx, summary);
});
this.walletdb.on('balances', function(balances) {
@ -789,11 +903,15 @@ HTTPServer.prototype._initIO = function _initIO() {
unconfirmed: utils.btc(balances[id].unconfirmed),
total: utils.btc(balances[id].total)
};
self.server.io.to(id).emit('balance', json[id], id);
self.server.io.to('!all').emit('balance', json[id], id);
});
self.server.io.to('!all').emit('balances', json);
});
this.walletdb.on('address', function(receive, change, map) {
var summary = map.toJSON();
if (receive) {
receive = receive.map(function(address) {
return address.toJSON();
@ -806,11 +924,11 @@ HTTPServer.prototype._initIO = function _initIO() {
});
}
map.all.forEach(function(id) {
self.server.io.to(id).emit('address', receive, change, map);
map.getWallets().forEach(function(id) {
self.server.io.to(id).emit('address', receive, change, summary);
});
self.server.io.to('!all').emit('address', receive, change, map);
self.server.io.to('!all').emit('address', receive, change, summary);
});
};

View File

@ -2031,6 +2031,58 @@ WalletMap.prototype.hasPaths = function hasPaths(address) {
return paths && paths.length !== 0;
};
/**
* Return a unique list of wallet IDs for the map.
* @returns {WalletID[]}
*/
WalletMap.prototype.getWallets = function getWallets() {
var ids = {};
var i, member;
for (i = 0; i < this.accounts.length; i++) {
member = this.accounts[i];
ids[member.id] = true;
}
return Object.keys(ids);
};
/**
* Return a unique list of wallet IDs for the map.
* @returns {WalletID[]}
*/
WalletMap.prototype.getInputWallets = function getInputWallets() {
var ids = {};
var i, member;
for (i = 0; i < this.inputs.length; i++) {
member = this.inputs[i];
ids[member.id] = true;
}
return Object.keys(ids);
};
/**
* Return a unique list of wallet IDs for the map.
* @returns {WalletID[]}
*/
WalletMap.prototype.getOutputWallets = function getOutputWallets() {
var ids = {};
var i, member;
for (i = 0; i < this.outputs.length; i++) {
member = this.outputs[i];
ids[member.id] = true;
}
return Object.keys(ids);
};
/**
* Get paths for a given address hash.
* @param {Hash} address

View File

@ -1928,6 +1928,11 @@ utils.ccmp = function ccmp(a, b) {
if (!Buffer.isBuffer(b))
return false;
// It's assumed the target length
// would be known to an attacker anyway.
if (a.length !== b.length)
return false;
for (i = 0; i < a.length; i++)
res |= a[i] ^ b[i];

View File

@ -9,6 +9,7 @@
var bcoin = require('./env');
var EventEmitter = require('events').EventEmitter;
var constants = bcoin.protocol.constants;
var utils = require('./utils');
var assert = utils.assert;
var BufferReader = require('./reader');
@ -55,6 +56,8 @@ function Wallet(db, options) {
this.master = null;
this.initialized = false;
this.accountDepth = 0;
this.token = constants.ZERO_HASH;
this.tokenDepth = 0;
this.account = null;
@ -72,6 +75,7 @@ utils.inherits(Wallet, EventEmitter);
Wallet.prototype.fromOptions = function fromOptions(options) {
var master = options.master;
var id, token;
if (!master)
master = bcoin.hd.fromMnemonic(null, this.network);
@ -79,9 +83,11 @@ Wallet.prototype.fromOptions = function fromOptions(options) {
if (!bcoin.hd.isHD(master) && !MasterKey.isMasterKey(master))
master = bcoin.hd.from(master, this.network);
if (!MasterKey.isMasterKey(master))
if (bcoin.hd.isHD(master))
master = MasterKey.fromKey(master);
assert(MasterKey.isMasterKey(master));
this.master = master;
if (options.initialized != null) {
@ -96,11 +102,28 @@ Wallet.prototype.fromOptions = function fromOptions(options) {
if (options.id) {
assert(utils.isAlpha(options.id), 'Wallet ID must be alphanumeric.');
this.id = options.id;
id = options.id;
}
if (!this.id)
this.id = this.getID();
if (!id)
id = this.getID();
if (options.token) {
assert(Buffer.isBuffer(options.token));
assert(options.token.length === 32);
token = options.token;
}
if (options.tokenDepth != null) {
assert(utils.isNumber(options.tokenDepth));
this.tokenDepth = options.tokenDepth;
}
if (!token)
token = this.getToken(this.master.key, this.tokenDepth);
this.id = id;
this.token = token;
return this;
};
@ -294,6 +317,43 @@ Wallet.prototype.setPassphrase = function setPassphrase(old, new_, callback) {
});
};
/**
* Generate a new token.
* @param {(String|Buffer)?} passphrase
* @param {Function} callback
*/
Wallet.prototype.retoken = function retoken(passphrase, callback) {
var self = this;
var unlock;
if (typeof passphrase === 'function') {
callback = passphrase;
passphrase = null;
}
unlock = this.locker.lock(retoken, [passphrase, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
this.unlock(passphrase, null, function(err, master) {
if (err)
return callback(err);
self.tokenDepth++;
self.token = self.getToken(master, self.tokenDepth);
self.save(function(err) {
if (err)
return callback(err);
return callback(null, self.token);
});
});
};
/**
* Lock the wallet, destroy decrypted key.
*/
@ -314,7 +374,7 @@ Wallet.prototype.unlock = function unlock(passphrase, timeout, callback) {
/**
* Generate the wallet ID if none was passed in.
* It is represented as `m/44'` (public) hashed
* It is represented as `m/44` (public) hashed
* and converted to an address with a prefix
* of `0x03be04` (`WLT` in base58).
* @returns {Base58String}
@ -325,7 +385,7 @@ Wallet.prototype.getID = function getID() {
assert(this.master.key, 'Cannot derive id.');
key = this.master.key.derive(44, true);
key = this.master.key.derive(44);
p = new BufferWriter();
p.writeU8(0x03);
@ -337,6 +397,27 @@ Wallet.prototype.getID = function getID() {
return utils.toBase58(p.render());
};
/**
* Generate the wallet api key if none was passed in.
* It is represented as HASH256(m/44'->public|nonce).
* @param {Number} nonce
* @returns {Buffer}
*/
Wallet.prototype.getToken = function getToken(master, nonce) {
var key, p;
assert(master, 'Cannot derive token.');
key = master.derive(44, true);
p = new BufferWriter();
p.writeBytes(key.publicKey);
p.writeU32(nonce);
return utils.hash256(p.render());
};
/**
* Create an account. Requires passphrase if master key is encrypted.
* @param {Object} options - See {@link Account} options.
@ -1437,6 +1518,8 @@ Wallet.prototype.inspect = function inspect() {
network: this.network.type,
initialized: this.initialized,
accountDepth: this.accountDepth,
token: this.token.toString('hex'),
tokenDepth: this.tokenDepth,
master: this.master,
account: this.account
};
@ -1455,6 +1538,8 @@ Wallet.prototype.toJSON = function toJSON() {
id: this.id,
initialized: this.initialized,
accountDepth: this.accountDepth,
token: this.token.toString('hex'),
tokenDepth: this.tokenDepth,
master: this.master.toJSON(),
account: this.account ? this.account.toJSON() : null
};
@ -1470,11 +1555,15 @@ Wallet.prototype.fromJSON = function fromJSON(json) {
assert(utils.isAlpha(json.id), 'Wallet ID must be alphanumeric.');
assert(typeof json.initialized === 'boolean');
assert(utils.isNumber(json.accountDepth));
assert(typeof json.token === 'string');
assert(json.token.length === 64);
assert(utils.isNumber(json.tokenDepth));
this.network = bcoin.network.get(json.network);
this.id = json.id;
this.initialized = json.initialized;
this.accountDepth = json.accountDepth;
this.token = new Buffer(json.token, 'hex');
this.master = MasterKey.fromJSON(json.master);
return this;
@ -1492,6 +1581,8 @@ Wallet.prototype.toRaw = function toRaw(writer) {
p.writeVarString(this.id, 'utf8');
p.writeU8(this.initialized ? 1 : 0);
p.writeU32(this.accountDepth);
p.writeBytes(this.token);
p.writeU32(this.tokenDepth);
p.writeVarBytes(this.master.toRaw());
if (!writer)
@ -1512,6 +1603,8 @@ Wallet.prototype.fromRaw = function fromRaw(data) {
this.id = p.readVarString('utf8');
this.initialized = p.readU8() === 1;
this.accountDepth = p.readU32();
this.token = p.readBytes(32);
this.tokenDepth = p.readU32();
this.master = MasterKey.fromRaw(p.readVarBytes());
return this;
};
@ -1585,8 +1678,6 @@ function Account(db, options) {
this.network = db.network;
this.lookahead = Account.MAX_LOOKAHEAD;
this.loaded = false;
this.loading = false;
this.receiveAddress = null;
this.changeAddress = null;
@ -2268,13 +2359,13 @@ Account.prototype.toJSON = function toJSON() {
receiveDepth: this.receiveDepth,
changeDepth: this.changeDepth,
receiveAddress: this.receiveAddress
? this.receiveAddress.getAddress()
? this.receiveAddress.getAddress('base58')
: null,
programAddress: this.receiveAddress
? this.receiveAddress.getProgramAddress()
? this.receiveAddress.getProgramAddress('base58')
: null,
changeAddress: this.changeAddress
? this.changeAddress.getAddress()
? this.changeAddress.getAddress('base58')
: null,
accountKey: this.accountKey.xpubkey,
keys: this.keys.map(function(key) {
@ -2469,7 +2560,7 @@ MasterKey.prototype.fromOptions = function fromOptions(options) {
}
if (options.key) {
assert(Buffer.isBuffer(options.key));
assert(bcoin.hd.isHD(options.key));
this.key = options.key;
}

View File

@ -185,17 +185,12 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) {
var balances = {};
var i, id, keys;
utils.forEachSerial(map.outputs, function(output, next) {
id = output.id;
if (self.listeners('balance').length === 0
utils.forEachSerial(map.getOutputWallets(), function(id, next) {
if (self.listeners('balances').length === 0
&& !self.hasListener(id, 'balance')) {
return next();
}
if (balances[id] != null)
return next();
self.getBalance(id, function(err, balance) {
if (err)
return next(err);
@ -215,7 +210,7 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) {
self.fire(id, 'balance', balances[id]);
}
self.emit('balance', balances, map);
self.emit('balances', balances, map);
return callback(null, balances);
});
@ -231,11 +226,8 @@ WalletDB.prototype.updateBalances = function updateBalances(tx, map, callback) {
WalletDB.prototype.syncOutputs = function syncOutputs(tx, map, callback) {
var self = this;
var id;
utils.forEachSerial(map.outputs, function(output, next) {
id = output.id;
utils.forEachSerial(map.getOutputWallets(), function(id, next) {
self.syncOutputDepth(id, tx, function(err, receive, change) {
if (err)
return next(err);
@ -474,6 +466,46 @@ WalletDB.prototype.save = function save(wallet, callback) {
this.db.put('w/' + wallet.id, wallet.toRaw(), callback);
};
/**
* Test an api key against a wallet's api key.
* @param {WalletID} id
* @param {String} token
* @param {Function} callback
*/
WalletDB.prototype.auth = function auth(id, token, callback) {
var wallet;
if (!id)
return callback(new Error('Wallet not found.'));
this.db.get('w/' + id, function(err, data) {
if (err)
return callback(err);
if (!data)
return callback(new Error('Wallet not found.'));
try {
wallet = bcoin.wallet.fromRaw(self, data);
} catch (e) {
return callback(e);
}
if (typeof token === 'string') {
if (!utils.isHex(token))
return callback(new Error('Authentication error.'));
token = new Buffer(token, 'hex');
}
// Compare in constant time:
if (!utils.ccmp(token, wallet.token))
return callback(new Error('Authentication error.'));
return callback();
});
};
/**
* Create a new wallet, save to database, setup watcher.
* @param {Object} options - See {@link Wallet}.
@ -613,6 +645,9 @@ WalletDB.prototype.getAccount = function getAccount(id, name, callback) {
WalletDB.prototype.getAccounts = function getAccounts(id, callback) {
var accounts = [];
if (!utils.isAlpha(id))
return callback(new Error('Wallet IDs must be alphanumeric.'));
this.db.iterate({
gte: 'i/' + id + '/',
lte: 'i/' + id + '/~',
@ -1184,6 +1219,29 @@ WalletDB.prototype.removeKey = function removeKey(id, name, key, callback) {
});
};
WalletDB.prototype.setPassphrase = function setPassphrase(id, old, new_, callback) {
if (typeof new_ === 'function') {
callback = new_;
new_ = old;
old = null;
}
this.fetchWallet(id, callback, function(wallet, callback) {
wallet.setPassphrase(old, new_, callback);
});
};
WalletDB.prototype.retoken = function retoken(id, passphrase, callback) {
if (typeof passphrase === 'function') {
callback = passphrase;
passphrase = null;
}
this.fetchWallet(id, callback, function(wallet, callback) {
wallet.retoken(passphrase, callback);
});
};
WalletDB.prototype.getInfo = function getInfo(id, callback) {
this.fetchWallet(id, callback, function(wallet, callback) {
callback(null, wallet);