From 323ea3eda59e604fb0b7179725bf395947b845f3 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 5 Oct 2016 07:11:13 -0700 Subject: [PATCH] http: handle api keys better. --- bin/cli | 4 +- etc/sample.conf | 2 +- lib/http/rpc.js | 18 +------ lib/http/server.js | 117 +++++++++++++++++++++++-------------------- lib/node/config.js | 2 +- lib/node/fullnode.js | 2 +- lib/node/spvnode.js | 2 +- 7 files changed, 71 insertions(+), 76 deletions(-) diff --git a/bin/cli b/bin/cli index 0961774f..7557c5ef 100755 --- a/bin/cli +++ b/bin/cli @@ -506,8 +506,8 @@ CLI.prototype.handleNode = co(function* handleNode() { this.log(' $ tx [hash/address]: View transactions.'); this.log(' $ coin [hash+index/address]: View coins.'); this.log(' $ block [hash/height]: View block.'); - this.log(' $ rescan [height/hash]: Rescan for transactions (admin-only).'); - this.log(' $ backup [path]: Backup the wallet db (admin-only).'); + this.log(' $ rescan [height/hash]: Rescan for transactions.'); + this.log(' $ backup [path]: Backup the wallet db.'); this.log(' $ rpc [command] [args]: Execute RPC command.'); return; } diff --git a/etc/sample.conf b/etc/sample.conf index d8fce512..9c9f7836 100644 --- a/etc/sample.conf +++ b/etc/sample.conf @@ -58,7 +58,7 @@ known-peers: ./known-peers # ssl-key: @/ssl/priv.key # http-port: 8332 # http-host: 0.0.0.0 +# service-key: bikeshed api-key: bikeshed -admin-key: bikeshed2 wallet-auth: false # no-auth: false diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 6a6dc06b..f36de44d 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -69,11 +69,9 @@ function RPC(node) { utils.inherits(RPC, EventEmitter); -RPC.prototype.execute = function execute(json, admin) { +RPC.prototype.execute = function execute(json) { switch (json.method) { case 'stop': - if (!admin) - return Promise.resolve('Not authorized.'); return this.stop(json.params); case 'help': return this.help(json.params); @@ -109,8 +107,6 @@ RPC.prototype.execute = function execute(json, admin) { case 'gettxoutsetinfo': return this.gettxoutsetinfo(json.params); case 'verifychain': - if (!admin) - return Promise.resolve('Not authorized.'); return this.verifychain(json.params); case 'invalidateblock': @@ -134,18 +130,12 @@ RPC.prototype.execute = function execute(json, admin) { return this.submitblock(json.params); case 'setgenerate': - if (!admin) - return Promise.resolve('Not authorized.'); return this.setgenerate(json.params); case 'getgenerate': return this.getgenerate(json.params); case 'generate': - if (!admin) - return Promise.resolve('Not authorized.'); return this.generate(json.params); case 'generatetoaddress': - if (!admin) - return Promise.resolve('Not authorized.'); return this.generatetoaddress(json.params); case 'estimatefee': @@ -225,14 +215,10 @@ RPC.prototype.execute = function execute(json, admin) { case 'addwitnessaddress': return this.addwitnessaddress(json.params); case 'backupwallet': - if (!admin) - return Promise.resolve('Not authorized.'); return this.backupwallet(json.params); case 'dumpprivkey': return this.dumpprivkey(json.params); case 'dumpwallet': - if (!admin) - return Promise.resolve('Not authorized.'); return this.dumpwallet(json.params); case 'encryptwallet': return this.encryptwallet(json.params); @@ -261,8 +247,6 @@ RPC.prototype.execute = function execute(json, admin) { case 'importprivkey': return this.importprivkey(json.params); case 'importwallet': - if (!admin) - return Promise.resolve('Not authorized.'); return this.importwallet(json.params); case 'importaddress': return this.importaddress(json.params); diff --git a/lib/http/server.js b/lib/http/server.js index d26e3243..277aaf97 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -62,28 +62,24 @@ function HTTPServer(options) { this.loaded = false; this.apiKey = options.apiKey; this.apiHash = null; - this.adminHash = null; + this.serviceKey = options.serviceKey; + this.serviceHash = null; this.rpc = null; if (!this.apiKey) this.apiKey = utils.toBase58(crypto.randomBytes(20)); + if (!this.serviceKey) + this.serviceKey = this.apiKey; + assert(typeof this.apiKey === 'string', 'API key must be a string.'); assert(this.apiKey.length <= 200, 'API key must be under 200 bytes.'); + assert(typeof this.serviceKey === 'string', 'API key must be a string.'); + assert(this.serviceKey.length <= 200, 'API key must be under 200 bytes.'); + this.apiHash = hash256(this.apiKey); - this.adminHash = this.apiHash; - - if (options.adminKey) { - assert(typeof options.adminKey === 'string', 'API key must be a string.'); - assert(options.adminKey.length <= 200, 'API key must be under 200 bytes.'); - this.adminHash = hash256(options.adminKey); - } - - if (options.noAuth) { - this.apiKey = null; - this.apiHash = null; - } + this.serviceHash = hash256(this.serviceKey); options.sockets = true; @@ -143,7 +139,6 @@ HTTPServer.prototype._init = function _init() { if (!auth) { req.username = null; req.password = null; - req.admin = false; return next(); } @@ -156,7 +151,6 @@ HTTPServer.prototype._init = function _init() { req.username = parts.shift(); req.password = parts.join(':'); - req.admin = false; next(); }); @@ -164,21 +158,22 @@ HTTPServer.prototype._init = function _init() { this.use(function(req, res, send, next) { var hash; - if (!this.apiHash) { - req.admin = true; + if (this.options.noAuth) return next(); - } hash = hash256(req.password); - if (crypto.ccmp(hash, this.adminHash)) { - req.admin = true; - return next(); - } - + // Regular API key gives access to everything. if (crypto.ccmp(hash, this.apiHash)) return next(); + // If they're hitting the wallet services, + // they can use the less powerful API key. + if (isWalletPath(req)) { + if (crypto.ccmp(hash, this.serviceHash)) + return next(); + } + res.setHeader('WWW-Authenticate', 'Basic realm="node"'); if (req.method === 'POST' @@ -445,6 +440,7 @@ HTTPServer.prototype._init = function _init() { }); this.use(con(function* (req, res, send, next) { + var options = req.options; var wallet; if (req.path.length < 2 || req.path[0] !== 'wallet') { @@ -453,7 +449,7 @@ HTTPServer.prototype._init = function _init() { } if (!this.options.walletAuth) { - wallet = yield this.walletdb.get(req.options.id); + wallet = yield this.walletdb.get(options.id); if (!wallet) { send(404); @@ -467,7 +463,7 @@ HTTPServer.prototype._init = function _init() { } try { - wallet = yield this.walletdb.auth(req.options.id, req.options.token); + wallet = yield this.walletdb.auth(options.id, options.token); } catch (err) { this.logger.info('Auth failure for %s: %s.', req.options.id, err.message); @@ -482,7 +478,7 @@ HTTPServer.prototype._init = function _init() { req.wallet = wallet; - this.logger.info('Successful auth for %s.', req.options.id); + this.logger.info('Successful auth for %s.', options.id); next(); })); @@ -503,7 +499,7 @@ HTTPServer.prototype._init = function _init() { } try { - json = yield this.rpc.execute(req.body, req.admin); + json = yield this.rpc.execute(req.body); } catch (err) { this.logger.error(err); @@ -698,9 +694,6 @@ HTTPServer.prototype._init = function _init() { enforce(height != null, 'Hash or height is required.'); - if (!req.admin) - throw new Error('Cannot scan.'); - send(200, { success: true }); yield this.node.scan(height); })); @@ -712,9 +705,6 @@ HTTPServer.prototype._init = function _init() { enforce(path, 'Path is required.'); - if (!req.admin) - throw new Error('Cannot backup.'); - yield this.walletdb.backup(path); send(200, { success: true }); })); @@ -1091,24 +1081,21 @@ HTTPServer.prototype._initIO = function _initIO() { }); socket.on('auth', function(args, callback) { - var apiKey = args[0]; - var hash; + var key = args[0]; + var hash, api, service; if (socket.auth) return callback({ error: 'Already authed.' }); socket.stop(); - if (self.apiHash) { - hash = hash256(apiKey); - if (crypto.ccmp(hash, self.adminHash)) { - socket.admin = true; - } else { - if (!crypto.ccmp(hash, self.apiHash)) - return callback({ error: 'Bad key.' }); - } - } else { - socket.admin = true; + if (!self.options.noAuth) { + hash = hash256(key); + api = crypto.ccmp(hash, self.apiHash); + service = crypto.ccmp(hash, self.serviceHash); + if (!api && !service) + return callback({ error: 'Bad key.' }); + socket.api = api; } socket.auth = true; @@ -1170,11 +1157,15 @@ HTTPServer.prototype._initIO = function _initIO() { }); socket.on('watch chain', function(args, callback) { + if (!socket.api) + return callback({ error: 'Not authorized.' }); socket.watchChain(); callback(); }); socket.on('unwatch chain', function(args, callback) { + if (!socket.api) + return callback({ error: 'Not authorized.' }); socket.unwatchChain(); callback(); }); @@ -1185,6 +1176,9 @@ HTTPServer.prototype._initIO = function _initIO() { if (!Array.isArray(addresses)) return callback({ error: 'Invalid parameter.' }); + if (!socket.api) + return callback({ error: 'Not authorized.' }); + try { socket.addFilter(addresses); } catch (e) { @@ -1200,6 +1194,9 @@ HTTPServer.prototype._initIO = function _initIO() { if (!Array.isArray(addresses)) return callback({ error: 'Invalid parameter.' }); + if (!socket.api) + return callback({ error: 'Not authorized.' }); + try { socket.removeFilter(addresses); } catch (e) { @@ -1212,12 +1209,12 @@ HTTPServer.prototype._initIO = function _initIO() { socket.on('scan chain', function(args, callback) { var start = args[0]; - if (!socket.admin) - return callback({ error: 'Cannot scan.' }); - if (!utils.isHex256(start) && !utils.isUInt32(start)) return callback({ error: 'Invalid parameter.' }); + if (!socket.api) + return callback({ error: 'Not authorized.' }); + if (typeof start === 'string') start = utils.revHex(start); @@ -1276,12 +1273,16 @@ HTTPServer.prototype.open = co(function* open() { this.logger.info('HTTP server loaded.'); - if (this.apiKey) { - this.logger.info('HTTP API key: %s', this.apiKey); - this.apiKey = null; - } else if (!this.apiHash) { + if (this.options.noAuth) { this.logger.warning('WARNING: Your http server is open to the world.'); + return; } + + this.logger.info('HTTP API key: %s', this.apiKey); + this.logger.info('HTTP Service API key: %s', this.serviceKey); + + this.apiKey = null; + this.serviceKey = null; }); /** @@ -1361,7 +1362,7 @@ function ClientSocket(server, socket) { this.auth = false; this.filter = {}; this.filterCount = 0; - this.admin = false; + this.api = false; this.chain = this.server.chain; this.mempool = this.server.mempool; @@ -1650,6 +1651,16 @@ function sortCoins(coins) { }); } +function isWalletPath(req) { + if (req.path.length >= 1 && req.path[0] === 'wallet') + return true; + + if (req.method === 'GET' && req.pathname === '/') + return true; + + return false; +} + /* * Expose */ diff --git a/lib/node/config.js b/lib/node/config.js index 4f6b037d..4c1fb710 100644 --- a/lib/node/config.js +++ b/lib/node/config.js @@ -199,7 +199,7 @@ config.parseData = function parseData(data, prefix, dirname) { options.httpPort = num(data.httpport); options.httpHost = str(data.httphost); options.apiKey = str(data.apikey); - options.adminKey = str(data.adminkey); + options.serviceKey = str(data.servicekey); options.walletAuth = bool(data.walletauth); options.noAuth = bool(data.noauth); diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 6f17e7ef..74501ce6 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -162,7 +162,7 @@ function Fullnode(options) { port: this.options.httpPort || this.network.rpcPort, host: this.options.httpHost || '0.0.0.0', apiKey: this.options.apiKey, - adminKey: this.options.adminKey, + serviceKey: this.options.serviceKey, walletAuth: this.options.walletAuth, noAuth: this.options.noAuth }); diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 5ccdb3ca..6218dcc8 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -99,7 +99,7 @@ function SPVNode(options) { port: this.options.httpPort || this.network.rpcPort, host: this.options.httpHost || '0.0.0.0', apiKey: this.options.apiKey, - adminKey: this.options.adminKey, + serviceKey: this.options.serviceKey, walletAuth: this.options.walletAuth, noAuth: this.options.noAuth });