From dd37a3561deff48ca4548541b2ada4b142d45091 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Sun, 21 Sep 2014 15:30:05 -0300 Subject: [PATCH 1/2] credentialstore: service to store login info --- config/config.js | 5 +- insight.js | 3 + plugins/config-credentialstore.js | 2 + plugins/credentialstore.js | 160 ++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 plugins/config-credentialstore.js create mode 100644 plugins/credentialstore.js diff --git a/config/config.js b/config/config.js index 052347c..b0bf216 100644 --- a/config/config.js +++ b/config/config.js @@ -83,8 +83,9 @@ var enableMonitor = process.env.ENABLE_MONITOR === 'true'; var enableCleaner = process.env.ENABLE_CLEANER === 'true'; var enableMailbox = process.env.ENABLE_MAILBOX === 'true'; var enableRatelimiter = process.env.ENABLE_RATELIMITER === 'true'; +var enableCredentialstore = process.env.ENABLE_CREDSTORE === 'true'; var loggerLevel = process.env.LOGGER_LEVEL || 'info'; -var enableHTTPS = process.env.ENABLE_HTTPS === 'true'; +var enableHTTPS = process.env.ENABLE_HTTPS === 'true'; if (!fs.existsSync(db)) { mkdirp.sync(db); @@ -99,6 +100,8 @@ module.exports = { mailbox: require('../plugins/config-mailbox.js'), enableRatelimiter: enableRatelimiter, ratelimiter: require('../plugins/config-ratelimiter.js'), + enableCredentialstore: enableCredentialstore, + credentialstore: require('../plugins/config-credentialstore'), loggerLevel: loggerLevel, enableHTTPS: enableHTTPS, version: version, diff --git a/insight.js b/insight.js index 4d4d9a0..12b64d4 100755 --- a/insight.js +++ b/insight.js @@ -143,6 +143,9 @@ if (config.enableMonitor) { require('./plugins/monitor').init(config.monitor); } +if (config.enableCredentialstore) { + require('./plugins/credentialstore').init(expressApp, config.credentialstore); +} // express settings require('./config/express')(expressApp, historicSync, peerSync); diff --git a/plugins/config-credentialstore.js b/plugins/config-credentialstore.js new file mode 100644 index 0000000..7be35b6 --- /dev/null +++ b/plugins/config-credentialstore.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/credentialstore.js b/plugins/credentialstore.js new file mode 100644 index 0000000..5deff52 --- /dev/null +++ b/plugins/credentialstore.js @@ -0,0 +1,160 @@ +/** + * Credentials storage service + * + * Allows users to store encrypted data on the server, useful to store the user's credentials. + * + * Steps for the user would be: + * + * 1. Choose an username + * 2. Choose a password + * 3. Create a strong key for encryption using PBKDF2 or scrypt with the username and password + * 4. Use that key to AES-CRT encrypt the private key + * 5. Take the double SHA256 hash of "salt"+"username"+"password" and use that as a secret + * 6. Send a POST request to resource /credentials with the params: + * username=johndoe + * secret=2413fb3709b05939f04cf2e92f7d0897fc2596f9ad0b8a9ea855c7bfebaae892 + * record=YjU1MTI2YTM5ZjliMTE3MGEzMmU2ZjYxZTRhNjk0YzQ1MjM1ZTVhYzExYzA1ZWNkNmZm + * NjM5NWRlNmExMTE4NzIzYzYyYWMwODU1MTdkNWMyNjRiZTVmNmJjYTMxMGQyYmFiNjc4YzdiODV + * lZjg5YWIxYzQ4YjJmY2VkYWJjMDQ2NDYzODhkODFiYTU1NjZmMzgwYzhiODdiMzlmYjQ5ZTc1Nz + * FjYzQzYjk1YTEyYWU1OGMxYmQ3OGFhOTZmNGMz + * + * To retrieve data: + * + * 1. Recover the secret from the double sha256 of the salt, username, and password + * 2. Send a GET request to resource /credentials/username?secret=...... + * 3. Decrypt the data received + */ +(function() { + +'use strict'; + +var logger = require('../lib/logger').logger, + levelup = require('levelup'), + querystring = require('querystring'); + +var storePlugin = {}; + +/** + * Constant enum with the errors that the application may return + */ +var errors = { + MISSING_PARAMETER: { + code: 400, + message: 'Missing required parameter' + }, + INVALID_REQUEST: { + code: 400, + message: 'Invalid request parameter' + }, + NOT_FOUND: { + code: 404, + message: 'Credentials were not found' + } +}; + +var NAMESPACE = 'credentials-store-'; +var MAX_ALLOWED_STORAGE = 1024 /* no more than 1 kb */; + +/** + * Initializes the plugin + * + * @param {Express} expressApp + * @param {Object} config + */ +storePlugin.init = function(expressApp, config) { + var globalConfig = require('../config/config'); + logger.info('Using credentialstore plugin'); + + var path = globalConfig.leveldb + '/credentialstore' + (globalConfig.name ? ('-' + globalConfig.name) : ''); + storePlugin.db = config.db || globalConfig.db || levelup(path); + + expressApp.post(globalConfig.apiPrefix + '/credentials', storePlugin.post); + expressApp.get(globalConfig.apiPrefix + '/credentials/:username', storePlugin.get); +}; + +/** + * Helper function that ends a requests showing the user an error. The response body will be a JSON + * encoded object with only one property with key "error" and value error.message, one of + * the parameters of the function + * + * @param {Object} error - The error that caused the request to be terminated + * @param {number} error.code - the HTTP code to return + * @param {string} error.message - the message to send in the body + * @param {Express.Response} response - the express.js response. the methods status, json, and end + * will be called, terminating the request. + */ +var returnError = function(error, response) { + response.status(error.code).json({error: error.message}).end(); +}; + +/** + * Store a record in the database. The underlying database is merely a levelup instance (a key + * value store) that uses the username concatenated with the secret as a key to store the record. + * The request is expected to contain the parameters: + * * username + * * secret + * * record + * + * @param {Express.Request} request + * @param {Express.Response} response + */ +storePlugin.post = function(request, response) { + + var queryData = ''; + + request.on('data', function(data) { + queryData += data; + if (queryData.length > MAX_ALLOWED_STORAGE) { + queryData = ''; + response.writeHead(413, {'Content-Type': 'text/plain'}).end(); + request.connection.destroy(); + } + }).on('end', function() { + var params = querystring.parse(queryData); + var username = params.username; + var secret = params.secret; + var record = params.record; + if (!username || !secret || !record) { + return returnError(errors.MISSING_PARAMETER, response); + } + + storePlugin.db.put(NAMESPACE + username + secret, record, function (err) { + if (err) { + return returnError({code: 500, message: err}, response); + } + response.json({success: true}).end(); + }); + }); +}; + +/** + * Retrieve a record from the database. + * + * The request is expected to contain the parameters: + * * username + * * secret + * + * @param {Express.Request} request + * @param {Express.Response} response + */ +storePlugin.get = function(request, response) { + var username = request.param('username'); + var secret = request.param('secret'); + if (!username || !secret) { + return returnError(errors.MISSING_PARAMETER, response); + } + + storePlugin.db.get(NAMESPACE + username + secret, function (err, value) { + if (err) { + if (err.notFound) { + return returnError(errors.NOT_FOUND, response); + } + return returnError({code: 500, message: err}, response); + } + response.send(value).end(); + }); +}; + +module.exports = storePlugin; + +})(); From bcd154fbc11f2fca7bd418f2de0a96dabc7ae956 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Mon, 22 Sep 2014 11:17:19 -0300 Subject: [PATCH 2/2] credentialstore: add tests --- package.json | 4 +- test/test.CredentialStore.js | 101 +++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 test/test.CredentialStore.js diff --git a/package.json b/package.json index 8c77b2b..142bd2f 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,6 @@ "moment": "~2.5.0", "preconditions": "^1.0.7", "should": "~2.1.1", - "sinon": "~1.7.3", "socket.io": "1.0.6", "socket.io-client": "1.0.6", "soop": "=0.1.5", @@ -88,6 +87,7 @@ "grunt-mocha-test": "~0.8.1", "grunt-nodemon": "~0.2.0", "memdown": "^0.10.2", - "should": "2.1.1" + "should": "2.1.1", + "sinon": "^1.10.3" } } diff --git a/test/test.CredentialStore.js b/test/test.CredentialStore.js new file mode 100644 index 0000000..608453d --- /dev/null +++ b/test/test.CredentialStore.js @@ -0,0 +1,101 @@ +'use strict'; + +var chai = require('chai'); +var assert = require('assert'); +var sinon = require('sinon'); +var should = chai.should; +var expect = chai.expect; + +describe('credentialstore test', function() { + + var globalConfig = require('../config/config'); + var leveldb_stub = sinon.stub(); + leveldb_stub.post = sinon.stub(); + leveldb_stub.get = sinon.stub(); + var plugin = require('../plugins/credentialstore'); + var express_mock = null; + var request = null; + var response = null; + + beforeEach(function() { + + express_mock = sinon.stub(); + express_mock.post = sinon.stub(); + express_mock.get = sinon.stub(); + + plugin.init(express_mock, {db: leveldb_stub}); + + request = sinon.stub(); + request.on = sinon.stub(); + request.param = sinon.stub(); + response = sinon.stub(); + response.send = sinon.stub(); + response.status = sinon.stub(); + response.json = sinon.stub(); + response.end = sinon.stub(); + }); + + it('initializes correctly', function() { + assert(plugin.db === leveldb_stub); + assert(express_mock.post.calledWith( + globalConfig.apiPrefix + '/credentials', plugin.post + )); + assert(express_mock.get.calledWith( + globalConfig.apiPrefix + '/credentials/:username', plugin.get + )); + }); + + it('writes a message correctly', function() { + + var data = 'username=1&secret=2&record=3'; + request.on.onFirstCall().callsArgWith(1, data); + request.on.onFirstCall().returnsThis(); + request.on.onSecondCall().callsArg(1); + leveldb_stub.put = sinon.stub(); + + leveldb_stub.put.onFirstCall().callsArg(2); + response.json.returnsThis(); + + plugin.post(request, response); + + assert(leveldb_stub.put.firstCall.args[0] === 'credentials-store-12'); + assert(leveldb_stub.put.firstCall.args[1] === '3'); + assert(response.json.calledWith({success: true})); + }); + + it('retrieves a message correctly', function() { + + request.param.onFirstCall().returns('username'); + request.param.onSecondCall().returns('secret'); + + var returnValue = '!@#$%'; + leveldb_stub.get.onFirstCall().callsArgWith(1, null, returnValue); + response.send.returnsThis(); + + plugin.get(request, response); + + assert(leveldb_stub.get.firstCall.args[0] === 'credentials-store-usernamesecret'); + assert(response.send.calledWith(returnValue)); + assert(response.end.calledOnce); + }); + + it('fails with messages that are too long', function() { + + response.writeHead = sinon.stub(); + request.connection = {}; + request.connection.destroy = sinon.stub(); + var data = 'blob'; + for (var i = 0; i < 2048; i++) { + data += '----'; + } + request.on.onFirstCall().callsArgWith(1, data); + request.on.onFirstCall().returnsThis(); + response.writeHead.returnsThis(); + + plugin.post(request, response); + + assert(response.writeHead.calledWith(413, {'Content-Type': 'text/plain'})); + assert(response.end.calledOnce); + assert(request.connection.destroy.calledOnce); + }); +});