Merge pull request #198 from eordano/feature/credentials-store
Add a credentials store service
This commit is contained in:
commit
c2c867c81c
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
2
plugins/config-credentialstore.js
Normal file
2
plugins/config-credentialstore.js
Normal file
@ -0,0 +1,2 @@
|
||||
module.exports = {
|
||||
};
|
||||
160
plugins/credentialstore.js
Normal file
160
plugins/credentialstore.js
Normal file
@ -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 <tt>error.message</tt>, 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;
|
||||
|
||||
})();
|
||||
101
test/test.CredentialStore.js
Normal file
101
test/test.CredentialStore.js
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user