942 lines
35 KiB
JavaScript
942 lines
35 KiB
JavaScript
/**
|
|
* @fileOverview Node.js Authorisation Handler for Bridge Pay
|
|
* @preserve Copyright 2016 Comcarde Ltd.
|
|
* @author Keith Symington
|
|
* @see #bridge_server-core
|
|
*/
|
|
|
|
/**
|
|
* Includes
|
|
*/
|
|
var mainDB = require(global.pathPrefix + 'mainDB.js');
|
|
var utils = require(global.pathPrefix + 'utils.js');
|
|
var valid = require(global.pathPrefix + 'valid.js');
|
|
var mailer = require(global.pathPrefix + 'mailer.js');
|
|
var log = require(global.pathPrefix + 'log.js');
|
|
var config = require(global.configFile);
|
|
var templates = require(global.pathPrefix + '../utils/templates.js');
|
|
var formattingUtils = require(global.pathPrefix + '../utils/formatting.js');
|
|
var crypto = require('crypto');
|
|
var async = require('async');
|
|
var moment = require('moment');
|
|
|
|
/**
|
|
* This function checks the client status for any blocking flags. It will return an error if the following are true:
|
|
* 1) The client is not verified.
|
|
* 2) The client is barred.
|
|
*
|
|
* @type {function} checkClientStatus
|
|
* @param {!string} ClientStatus - The ClientStatus flag from the client document.
|
|
*/
|
|
exports.checkClientStatus = function(ClientStatus) {
|
|
/**
|
|
* Valid session. Check client status.
|
|
*/
|
|
if (!utils.bitsAllSet(ClientStatus, utils.ClientEmailVerifiedMask)) {
|
|
return utils.createError(114, 'Client e-mail has not been verified - please click the link.');
|
|
}
|
|
if (utils.bitsAllSet(ClientStatus, utils.ClientBarredMask)) {
|
|
return utils.createError(117, 'Client barred by Comcarde.');
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* This function checks the device status for any blocking flags. It will return an error if the following are true:
|
|
* 1) The device is not verified.
|
|
* 2) The device is not authorised.
|
|
* 3) The device is suspended.
|
|
* 4) The device is barred.
|
|
*
|
|
* @type {function} checkDeviceStatus
|
|
* @param {!string} DeviceStatus - The DeviceStatus flag from the device document.
|
|
*/
|
|
exports.checkDeviceStatus = function(DeviceStatus) {
|
|
/**
|
|
* Valid session. Check device status.
|
|
*/
|
|
if (!utils.bitsAllSet(DeviceStatus, utils.DeviceRegister2Mask)) {
|
|
return utils.createError(109, 'Device not verified - SMS not confirmed.');
|
|
}
|
|
if (!utils.bitsAllSet(DeviceStatus, utils.DeviceRegister3Mask)) {
|
|
return utils.createError(110, 'Device not authorised - PIN not set.');
|
|
}
|
|
if (utils.bitsAllSet(DeviceStatus, utils.DeviceSuspendedMask)) {
|
|
return utils.createError(111, 'Device suspended by the user.');
|
|
}
|
|
if (utils.bitsAllSet(DeviceStatus, utils.DeviceBarredMask)) {
|
|
return utils.createError(112, 'Device barred by Comcarde.');
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* This function needs to be called with all server requests. It checks the user is currently logged in
|
|
* and if they are, it returns their client and device details. It requires two parameters:
|
|
*
|
|
* @type {function} validateCurrentSession
|
|
* @param {!string} DeviceToken - The token assigned to the device at registration.
|
|
* @param {!string} SessionToken - The token returned at login. Note that this is valid for ~5 minutes only.
|
|
* Calling this function extends the SessionToken's life.
|
|
* @param {!function} next - Not optional and should contain the code to be subsequently executed.
|
|
*/
|
|
exports.validateCurrentSession = function(DeviceToken, SessionToken, next) {
|
|
/**
|
|
* Valid input. Check to see if the database is online.
|
|
* Cyclomatic complexity is known to be high for this function.
|
|
*/
|
|
//jshint -W074
|
|
mainDB.findOneObject(mainDB.collectionDevice, {DeviceToken: DeviceToken}, undefined, false, function(err, existingDevice) {
|
|
if (err) {
|
|
next(utils.createError(104, 'Database offline.'), null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* No information returned from database.
|
|
*/
|
|
if (existingDevice === null) {
|
|
next(utils.createError(103, 'Cannot find device token.'), null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Device found. Now check the token.
|
|
*/
|
|
if (SessionToken !== existingDevice.SessionToken) {
|
|
// Session token invalid.
|
|
next(utils.createError(107, 'Invalid session token.'), null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check the session token expiry.
|
|
*/
|
|
var timestamp = new Date();
|
|
var expiry = existingDevice.SessionTokenExpiry;
|
|
if (timestamp >= expiry) {
|
|
// Session token invalid.
|
|
next(utils.createError(108, 'Session token expired.'), null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check device status.
|
|
*/
|
|
var currentDeviceStatus = exports.checkDeviceStatus(existingDevice.DeviceStatus);
|
|
if (currentDeviceStatus) {
|
|
next(currentDeviceStatus, null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Through device checks. Pull the client.
|
|
*/
|
|
mainDB.findOneObject(mainDB.collectionClient, {ClientID: existingDevice.ClientID}, undefined, false,
|
|
function(err, existingClient) {
|
|
/**
|
|
* Check for an error.
|
|
*/
|
|
if (err) {
|
|
// Database is not working.
|
|
next(utils.createError(105, 'Database offline.'), null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* If null then there is no Client account.
|
|
*/
|
|
if (existingClient === null) {
|
|
// Callback.
|
|
next(utils.createError(106, 'Cannot find account.'), null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check client status.
|
|
*/
|
|
var currentClientStatus = exports.checkClientStatus(existingClient.ClientStatus);
|
|
if (currentClientStatus) {
|
|
next(currentClientStatus, null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Great. All active. Extend token validity.
|
|
*/
|
|
var newExpiry = new Date(timestamp);
|
|
newExpiry.setMinutes(newExpiry.getMinutes() + utils.sessionTimeout);
|
|
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: DeviceToken}, {
|
|
$set: {
|
|
LastUpdate: timestamp,
|
|
SessionTokenExpiry: newExpiry
|
|
}
|
|
},
|
|
{upsert: false}, false, function(err) {
|
|
if (err) {
|
|
next(utils.createError(119, 'Database offline.'), null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Success!
|
|
*/
|
|
next(null, existingDevice, existingClient);
|
|
});
|
|
});
|
|
});
|
|
//jshint +W074
|
|
};
|
|
|
|
/**
|
|
* This function needs to be called with all server requests. It checks the user is currently logged in, esures the hmac is OK,
|
|
* and if so, it returns their client and device details. It requires multiple parameters:
|
|
*
|
|
* @type {function} validSession
|
|
* @param {!object} res - Response object for returning information. This function will respond directly on error.
|
|
* @param {!string} DeviceToken - The token assigned to the device at registration.
|
|
* @param {!string} SessionToken - The token returned at login. Note that this is valid for ~5 minutes only.
|
|
* Calling this function extends the SessionToken's life.
|
|
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
|
|
* @param {?object} hmacData - HMAC information from incoming packet.
|
|
* @param {!function} next - Not optional and should contain the code to be subsequently executed.
|
|
*/
|
|
exports.validSession = function(res, DeviceToken, SessionToken, functionInfo, hmacData, next) {
|
|
/**
|
|
* First check the session.
|
|
*/
|
|
exports.validateCurrentSession(DeviceToken, SessionToken, function(err, existingDevice, existingClient) {
|
|
if (err) {
|
|
res.status(200).json({
|
|
code: ('' + err.code),
|
|
info: err.message
|
|
});
|
|
log.system(
|
|
'WARNING',
|
|
err.message,
|
|
functionInfo.name,
|
|
err.code,
|
|
('AF [SessionToken ' + SessionToken + ' (DeviceToken ' + DeviceToken + ')]'),
|
|
(functionInfo.remote + ' (' + functionInfo.port + ')'));
|
|
/**
|
|
* Call back passing the error.
|
|
*/
|
|
next(err, null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Update the hmacData to store the ClientName from the existingClient as that
|
|
* is required for the HMAC generation and validation
|
|
*/
|
|
hmacData.ClientName = existingClient.ClientName;
|
|
|
|
/**
|
|
* Check the HMAC is fine.
|
|
*/
|
|
exports.checkHMAC(existingDevice, hmacData, functionInfo.name, function(err) {
|
|
if (err) {
|
|
res.status(200).json({
|
|
code: ('' + err.code),
|
|
info: err.message
|
|
});
|
|
log.system(
|
|
'WARNING',
|
|
err.message,
|
|
functionInfo.name,
|
|
err.code,
|
|
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'),
|
|
(functionInfo.remote + ' (' + functionInfo.port + ')'));
|
|
/**
|
|
* Call back passing the error.
|
|
*/
|
|
next(err, null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* All valid. Proceed.
|
|
*/
|
|
next(null, existingDevice, existingClient);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Checks the PIN received from the device. This function will return an error object as the
|
|
* first parameter if something went wrong.
|
|
*
|
|
* @type {function} checkDevicePIN
|
|
* @param {!string} deviceAuthorisation - SHA256 of PIN as received from device.
|
|
* @param {!object} existingDevice - Existing object in database.
|
|
* @param {!object} timestamp - Reference time when clock was pulled.
|
|
* @param {!function} next - Function to call when verification complete.
|
|
*/
|
|
exports.checkDevicePIN = function(deviceAuthorisation, existingDevice, timestamp, next) {
|
|
/**
|
|
* Check for a locked device.
|
|
*/
|
|
if (existingDevice.LoginAttempts >= utils.PINLockout) {
|
|
next(utils.createError(399, 'Device locked. Please use PIN Reset.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Split up the existing PIN and update if necessary.
|
|
*/
|
|
var receivedDeviceAuth;
|
|
var databaseDeviceAuth;
|
|
var authArray = existingDevice.DeviceAuthorisation.split('::');
|
|
async.series([
|
|
function(callback) {
|
|
/**
|
|
* Find the salt or create a new one if one doesn't exist.
|
|
*/
|
|
if (authArray[0] === '2') {
|
|
/**
|
|
* PIN encrypted using PBKDF2.
|
|
*/
|
|
crypto.pbkdf2(deviceAuthorisation, existingDevice.DeviceSalt,
|
|
config.encryptPBKDF2Rounds, config.encryptPBKDF2Bytes, config.encryptPBKDF2Protocol,
|
|
function(err, newHash) {
|
|
if (err) {
|
|
callback(err);
|
|
} else {
|
|
/**
|
|
* Update the database.
|
|
*/
|
|
receivedDeviceAuth = newHash.toString('hex');
|
|
databaseDeviceAuth = authArray[1];
|
|
callback(null);
|
|
}
|
|
});
|
|
} else {
|
|
/**
|
|
* Problem with the encryption string.
|
|
*/
|
|
callback('Unknown encryption type.');
|
|
}
|
|
}
|
|
],
|
|
/**
|
|
* Final clause which is executed after everything else or when an error is detected.
|
|
*/
|
|
function(err) {
|
|
if (err) {
|
|
next(utils.createError(400, ('Error when checking PIN: ' + err)));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check that the PIN matches.
|
|
*/
|
|
if (receivedDeviceAuth !== databaseDeviceAuth) {
|
|
/**
|
|
* Wrong PIN. Increase the fail count.
|
|
*/
|
|
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken}, {
|
|
$set: {LastUpdate: timestamp},
|
|
$inc: {LoginAttempts: 1}
|
|
},
|
|
{upsert: false}, false, function(err) {
|
|
if (err) {
|
|
next(utils.createError(401, 'Database offline.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check for maximum number of retries; if so, lock the account.
|
|
*/
|
|
if (existingDevice.LoginAttempts === (utils.PINLockout - 1)) {
|
|
/**
|
|
* Send warning e-mail.
|
|
*/
|
|
const suspendUrl = formattingUtils.formatPortalUrl('personal/devices');
|
|
var htmlEmail = templates.render('device-locked', {
|
|
DeviceNumber: existingDevice.DeviceNumber,
|
|
suspendDeviceUrl: suspendUrl
|
|
});
|
|
mailer.sendEmailByID(null, existingDevice.ClientID, 'Bridge Device Locked', htmlEmail, 'auth.checkDevicePIN',
|
|
function(err) {
|
|
if (err) {
|
|
next(utils.createError(402, 'Unable to send e-mail.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Tell the user that the device has been locked.
|
|
*/
|
|
next(utils.createError(403, 'Wrong PIN. ' + utils.PINLockout +
|
|
' failed attempts have locked this device.'));
|
|
});
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Wrong PIN - more attempts left.
|
|
*/
|
|
next(utils.createError(404, 'Wrong PIN.'));
|
|
});
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* PIN matched successfully.
|
|
*/
|
|
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken}, {
|
|
$set: {
|
|
LastUpdate: timestamp,
|
|
LoginAttempts: 0
|
|
}
|
|
},
|
|
{upsert: false}, false, function(err) {
|
|
if (err) {
|
|
next(utils.createError(405, 'Database offline.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Success!
|
|
*/
|
|
next(null);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Checks the client password. This function will return an error object as the first parameter if something went wrong.
|
|
*
|
|
* @type {function} checkClientPassword
|
|
* @param {!string} password - SHA256 of password as received from device.
|
|
* @param {!object} existingClient - Existing object in database.
|
|
* @param {!object} timestamp - Reference time when clock was pulled.
|
|
* @param {!function} next - Function to call when verification complete.
|
|
*/
|
|
exports.checkClientPassword = function(password, existingClient, timestamp, next) {
|
|
/**
|
|
* Check for a locked account.
|
|
*/
|
|
if (existingClient.LoginAttempts >= utils.passwordLockout) {
|
|
next(utils.createError(406, 'Attempted login to locked account. Please contact Comcarde.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Split up the existing password and update if necessary.
|
|
*/
|
|
var receivedPassword;
|
|
var databasePassword;
|
|
var passArray = existingClient.Password.split('::');
|
|
async.series([
|
|
function(callback) {
|
|
/**
|
|
* Find the salt or create a new one if one doesn't exist.
|
|
*/
|
|
if (passArray[0] === '2') {
|
|
/**
|
|
* Password encrypted using PBKDF2.
|
|
*/
|
|
crypto.pbkdf2(password, existingClient.ClientSalt,
|
|
config.encryptPBKDF2Rounds, config.encryptPBKDF2Bytes, config.encryptPBKDF2Protocol,
|
|
function(err, newHash) {
|
|
if (err) {
|
|
callback(err);
|
|
} else {
|
|
/**
|
|
* Update the database.
|
|
*/
|
|
receivedPassword = newHash.toString('hex');
|
|
databasePassword = passArray[1];
|
|
callback(null);
|
|
}
|
|
});
|
|
} else {
|
|
/**
|
|
* Problem with the encryption string.
|
|
*/
|
|
callback('Unknown encryption type.');
|
|
}
|
|
}
|
|
],
|
|
/**
|
|
* Final clause which is executed after everything else or when an error is detected.
|
|
*/
|
|
function(err) {
|
|
if (err) {
|
|
next(utils.createError(407, ('Error when checking password: ' + err)));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check that the password matches.
|
|
*/
|
|
if (receivedPassword !== databasePassword) {
|
|
/**
|
|
* Wrong password. Increase the fail count.
|
|
*/
|
|
mainDB.updateObject(mainDB.collectionClient, {ClientID: existingClient.ClientID}, {
|
|
$set: {LastUpdate: timestamp},
|
|
$inc: {LoginAttempts: 1}
|
|
},
|
|
{upsert: false}, false, function(err) {
|
|
if (err) {
|
|
next(utils.createError(408, 'Database offline.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check for maximum number of retries; if so, lock the account.
|
|
*/
|
|
if (existingClient.LoginAttempts === (utils.passwordLockout - 1)) {
|
|
/**
|
|
* Send warning e-mail.
|
|
*/
|
|
var htmlEmail = templates.render('account-locked', {
|
|
ClientName: existingClient.ClientName
|
|
});
|
|
mailer.sendEmail(null, existingClient.ClientName, 'Bridge Account Locked',
|
|
htmlEmail, 'auth.checkClientPassword',
|
|
function(err) {
|
|
if (err) {
|
|
next(utils.createError(409, 'Unable to send e-mail.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Tell the user that the client account has been locked.
|
|
*/
|
|
next(utils.createError(410, 'Wrong password. ' + utils.passwordLockout +
|
|
' failed attempts have locked the client account.'));
|
|
});
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Wrong password - more attempts left.
|
|
*/
|
|
next(utils.createError(411, 'Wrong password.'));
|
|
});
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Password matched successfully. Reset login attempts.
|
|
*/
|
|
mainDB.updateObject(mainDB.collectionClient, {ClientID: existingClient.ClientID}, {
|
|
$set: {
|
|
LastUpdate: timestamp,
|
|
LoginAttempts: 0
|
|
}
|
|
},
|
|
{upsert: false}, false, function(err) {
|
|
if (err) {
|
|
next(utils.createError(412, 'Database offline.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Success!
|
|
*/
|
|
next(null);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a new salt and encrypts the password hash using PBKDF2.
|
|
* This function will return an error object as the first parameter if something went wrong.
|
|
*
|
|
* @type {function} encryptPBKDF2
|
|
* @param {!string} input - SHA256 of input to be encrypted using PBKDF2.
|
|
* @param {!function} next - Function to call when verification complete.
|
|
* @param {!object} next.err - Error object. null on success.
|
|
* @param {!string} next.newSalt - Random salt for encoding.
|
|
* @param {!string} next.newHash - Hashed input using new salt.
|
|
*/
|
|
exports.encryptPBKDF2 = function(input, next) {
|
|
/**
|
|
* Create a new salt.
|
|
*/
|
|
crypto.randomBytes(config.encryptPBKDF2Bytes, function(err, salt) {
|
|
if (err) {
|
|
next(err, null, null);
|
|
return;
|
|
}
|
|
/**
|
|
* Success. Encrypt the password.
|
|
*/
|
|
var newSalt = salt.toString('hex');
|
|
crypto.pbkdf2(input, newSalt, config.encryptPBKDF2Rounds, config.encryptPBKDF2Bytes, config.encryptPBKDF2Protocol,
|
|
function(err, hash) {
|
|
if (err) {
|
|
next(err, null, null);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* All done. Convert the hash and call back with the new values.
|
|
*/
|
|
var newHash = hash.toString('hex');
|
|
next(null, newSalt, newHash);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Reponds with an HTML page.
|
|
*
|
|
* @type {function} respond
|
|
* @param {!object} res - response object. End will be called by this function.
|
|
* @param {!int} responseCode - HTML response code.
|
|
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
|
|
* @param {!string} code - The code associated with the response, e.g. '235', '10014'.
|
|
* @param {!string} fileName - Name of the Pug file that is the HTML source: e.g. 'templates/39_expired_token.pug'.
|
|
* @param {!object} data - Parameters used to render the HTML file.
|
|
* @param {!string} logType - The log entry type to be recorded - e.g. 'INFO', 'WARNING' etc.
|
|
* Omit if no system log entry is needed such as in 'Database offline'.
|
|
* @param {!string} infoString - If logType above is present, infoString is will be used as the information to be logged.
|
|
* @param {!string} altUser - If logType above is present, altUser can be used to log different user name. If not, 'UU' wil be used.
|
|
*/
|
|
exports.respondHTML = function(res, responseCode, functionInfo, code, fileName, data, logType, infoString, altUser) {
|
|
/**
|
|
* Respond to the request.
|
|
*/
|
|
var toReturn = templates.render(fileName, data);
|
|
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
res.end(toReturn);
|
|
|
|
/**
|
|
* Log what has happened if required.
|
|
*/
|
|
var logUser = '';
|
|
if (altUser) {
|
|
logUser = altUser;
|
|
} else {
|
|
logUser = 'UU';
|
|
}
|
|
if (logType) {
|
|
log.system(
|
|
logType,
|
|
infoString,
|
|
functionInfo.name,
|
|
code,
|
|
logUser,
|
|
(functionInfo.remote + ' (' + functionInfo.port + ')'));
|
|
}
|
|
|
|
/**
|
|
* Add HTML page generation details regardless.
|
|
*/
|
|
log.system(
|
|
'PAGE',
|
|
('Generated file returned [' + fileName + '].'),
|
|
functionInfo.name,
|
|
code,
|
|
logUser,
|
|
(functionInfo.remote + ' (' + functionInfo.port + ')'));
|
|
};
|
|
|
|
/**
|
|
* Uses a passed HMAC key to generate the data packet.
|
|
*
|
|
* @type {function} respond
|
|
* @param {!object} res - response object. End will be called by this function.
|
|
* @param {!int} responseCode - HTML response code.
|
|
* @param {!object} existingDevice - Existing object in database. If set to null the function works as a normal res.status() call.
|
|
* @param {!object} hmacData - hmac information {!address, !method, !body, !ClientName, ?timestamp, ?hmac}
|
|
* This function can be used to respond to non hmac calls by adding null in here.
|
|
* Typically set existingDevice and hmacData to null together.
|
|
* @param {!object} functionInfo - detail on the calling function {!name, !remote, !port}
|
|
* @param {!object} data - Body of the packet as a JSON object. Note that 'info' and 'code' must be present.
|
|
* @param {!string} logType - The log entry type to be recorded - e.g. 'INFO', 'WARNING' etc.
|
|
* Omit if no system log entry is needed such as in 'Database offline'.
|
|
* @param {!string} altString - If logType above is present, altString can be used to log different data than the user receives.
|
|
* @param {!string} altUser - If logType above is present, altUser can be used to log different user name. If not, 'UU' wil be used.
|
|
* This parameter is ignored if hmacData and existingDevice are present.
|
|
*
|
|
* Cyclomatic complexity error disabled as it seems unnecessary.
|
|
*/
|
|
// jshint -W074
|
|
exports.respond = function(res, responseCode, existingDevice, hmacData, functionInfo, data, logType, altString, altUser) {
|
|
/**
|
|
* Ensure that the function is getting the correct data to process.
|
|
* Checking disabled for speed outwith the development environment.
|
|
*/
|
|
if (config.isDevEnv) {
|
|
if (typeof data !== 'object') {
|
|
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data is not an object.');
|
|
}
|
|
if (!('code' in data)) {
|
|
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data is missing code field.');
|
|
}
|
|
if (!('info' in data)) {
|
|
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data is missing info field.');
|
|
}
|
|
if (typeof data.code !== 'string') {
|
|
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data.code is not a string.');
|
|
}
|
|
if (typeof data.info !== 'string') {
|
|
throw new Error('auth.respond received bad data from ' + functionInfo.name + ': data.info is not a string.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up variables for an HMAC based return.
|
|
*/
|
|
var key = '';
|
|
var DeviceUuid = '';
|
|
if (existingDevice) {
|
|
if (functionInfo.name === 'RotateHMAC.process') {
|
|
key = existingDevice.PendingHMAC;
|
|
DeviceUuid = existingDevice.DeviceUuid;
|
|
} else {
|
|
key = existingDevice.CurrentHMAC;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If no key is available then there is no HMAC. We do not need to sign.
|
|
*/
|
|
if ((hmacData === null) || (key === '')) {
|
|
/**
|
|
* Respond to the request.
|
|
*/
|
|
res.status(responseCode).json(data);
|
|
|
|
/**
|
|
* Log what has happened if required.
|
|
*/
|
|
if (logType) {
|
|
var logString = '';
|
|
var logUser = '';
|
|
if (altString) {
|
|
logString = altString;
|
|
} else {
|
|
logString = data.info;
|
|
}
|
|
if (altUser) {
|
|
logUser = altUser;
|
|
} else {
|
|
logUser = 'UU';
|
|
}
|
|
log.system(
|
|
logType,
|
|
logString,
|
|
functionInfo.name,
|
|
data.code,
|
|
logUser,
|
|
(functionInfo.remote + ' (' + functionInfo.port + ')'));
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Session token exception for Login1 due to the signing token not yet being saved.
|
|
*/
|
|
var sessionToken = '';
|
|
if (functionInfo.name === 'Login1.process') {
|
|
sessionToken = data.SessionToken;
|
|
} else {
|
|
sessionToken = existingDevice.SessionToken;
|
|
}
|
|
|
|
/**
|
|
* Process the data and create the hmac.
|
|
*/
|
|
var timestamp = moment().utc().format('YYYY-MM-DDTHH:mm:ss.SSS') + 'Z';
|
|
var text = JSON.stringify(data);
|
|
var fullText = hmacData.address + hmacData.method + timestamp + hmacData.ClientName + sessionToken + DeviceUuid + text;
|
|
var newkey = new Buffer(key, 'hex'); // Re-encode the key for use with the hmac.
|
|
var hmac = crypto.createHmac('sha256', newkey); // Create the HMAC object.
|
|
hmac.setEncoding('hex'); // Set encoding.
|
|
|
|
/**
|
|
* Note that the callback is attached as listener to stream's finish event.
|
|
*/
|
|
hmac.end(fullText, function() {
|
|
/**
|
|
* Read the HMAC and respond.
|
|
*/
|
|
var hash = hmac.read();
|
|
res.writeHead(responseCode, {
|
|
'bridge-hmac': hash,
|
|
'bridge-timestamp': timestamp,
|
|
'Content-Type': 'application/json; charset=utf-8' // Return that this is JSON
|
|
});
|
|
res.end(text);
|
|
|
|
/**
|
|
* Log what has happened if required.
|
|
*/
|
|
if (logType) {
|
|
var logString = '';
|
|
if (altString) {
|
|
logString = altString;
|
|
} else {
|
|
logString = data.info;
|
|
}
|
|
log.system(
|
|
logType,
|
|
logString,
|
|
functionInfo.name,
|
|
data.code,
|
|
(existingDevice.ClientID + ' (' + existingDevice.DeviceNumber + ')'),
|
|
(functionInfo.remote + ' (' + functionInfo.port + ')'));
|
|
}
|
|
});
|
|
};
|
|
// jshint +W074
|
|
|
|
/**
|
|
* Checks an incoming HMAC.
|
|
*
|
|
* @type {function} checkHMAC
|
|
* @param {!object} existingDevice - Existing object in database.
|
|
* @param {!object} hmacData - hmac information {!address, !method, !body, !ClientName, ?timestamp, ?hmac}
|
|
* @param {!string} functionName - The function that called the validation process: e.g. 'PayCodeRequest.process'.
|
|
* @param {!function} next - Function that should be called once processing is complete.
|
|
*
|
|
* Cyclomatic complexity error disabled as it seems unnecessary.
|
|
*/
|
|
// jshint -W074
|
|
exports.checkHMAC = function(existingDevice, hmacData, functionName, next) {
|
|
/**
|
|
* Check for HMAC problems first.
|
|
*/
|
|
if (existingDevice.HMACAttempts >= config.maxHMACAttempts) {
|
|
next(utils.createError(458, 'HMAC error: too many failed HMAC attempts.'));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Only Login1 and RotateHMAC are valid calls if there is a PendingHMAC.
|
|
*/
|
|
if (existingDevice.PendingHMAC !== '') {
|
|
if ((functionName !== 'RotateHMAC.process') && (functionName !== 'Login1.process')) {
|
|
next(utils.createError(459, 'HMAC error: HMAC must be rotated using RotateHMAC.'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
var key = '';
|
|
var DeviceUuid = '';
|
|
if (functionName === 'RotateHMAC.process') {
|
|
key = existingDevice.PendingHMAC;
|
|
DeviceUuid = existingDevice.DeviceUuid;
|
|
} else {
|
|
key = existingDevice.CurrentHMAC;
|
|
}
|
|
|
|
/**
|
|
* If the HMAC key is blank then no HMAC has been issued - re-register the device.
|
|
* Note there is one exception and that is on Login1 where there is a PendingHMAC.
|
|
*/
|
|
if (key === '') {
|
|
if ((functionName === 'Login1.process') && (existingDevice.PendingHMAC !== '')) {
|
|
next(null);
|
|
} else {
|
|
next(utils.createError(462, 'HMAC error: No valid HMAC key - please re-register the device.'));
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Look for timestamp errors.
|
|
*/
|
|
var output = '';
|
|
if (!('timestamp' in hmacData)) {
|
|
next(utils.createError(446, 'HMAC error: \"bridge-timestamp\" not present.'));
|
|
return;
|
|
} else {
|
|
output = valid.validateFieldTimeStamp(hmacData.timestamp);
|
|
if (output) {
|
|
next(utils.createError(449, output));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Check for desync.
|
|
*/
|
|
var upperTimestamp = new Date();
|
|
upperTimestamp.setSeconds(upperTimestamp.getSeconds() + config.HMACDesyncThreshold);
|
|
if (hmacData.timestamp > upperTimestamp) {
|
|
next(utils.createError(451, 'HMAC error: timestamp is in the future.'));
|
|
return;
|
|
}
|
|
var lowerTimestamp = new Date();
|
|
lowerTimestamp.setSeconds(lowerTimestamp.getSeconds() - config.HMACDesyncThreshold);
|
|
if (lowerTimestamp > hmacData.timestamp) {
|
|
next(utils.createError(452, 'HMAC error: timestamp has expired.'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Look for hmac errors.
|
|
*/
|
|
if (!('hmac' in hmacData)) {
|
|
next(utils.createError(447, 'HMAC error: \"bridge-hmac\" not present.'));
|
|
return;
|
|
} else {
|
|
output = valid.validateFieldHMAC(hmacData.hmac);
|
|
if (output) {
|
|
next(utils.createError(450, output));
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assemble the HMAC.
|
|
*/
|
|
var fullText = hmacData.address + hmacData.method + hmacData.timestamp + hmacData.ClientName + DeviceUuid + hmacData.body;
|
|
var newkey = new Buffer(key, 'hex'); // Re-encode the key for use with the hmac.
|
|
var hmac = crypto.createHmac('sha256', newkey); // Create the HMAC object.
|
|
hmac.setEncoding('hex'); // Set encoding.
|
|
|
|
/**
|
|
* Note that the callback is attached as listener to stream's finish event.
|
|
*/
|
|
hmac.end(fullText, function() {
|
|
/**
|
|
* Read the HMAC and respond.
|
|
*/
|
|
var hash = hmac.read();
|
|
if (hash !== hmacData.hmac) {
|
|
/**
|
|
* HMAC error. Tick up HMAC attempts or bar the device if there have been too many problems.
|
|
*/
|
|
var timestamp = new Date();
|
|
var toUpdate = {
|
|
$set: {LastUpdate: timestamp},
|
|
$inc: {HMACAttempts: 1}
|
|
};
|
|
if (existingDevice.HMACAttempts >= (config.maxHMACAttempts - 1)) {
|
|
toUpdate.$bit = {DeviceStatus: {or: utils.DeviceBarredMask}};
|
|
}
|
|
|
|
/**
|
|
* Write this information to the correct device.
|
|
*/
|
|
mainDB.updateObject(mainDB.collectionDevice, {DeviceToken: existingDevice.DeviceToken}, toUpdate,
|
|
{upsert: false}, false, function(err) {
|
|
if (err) {
|
|
next(utils.createError(460, ('HMAC error: database offline.')));
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Inform the device of the error.
|
|
*/
|
|
if (existingDevice.HMACAttempts >= (config.maxHMACAttempts - 1)) {
|
|
next(utils.createError(461, ('HMAC error: security check failed and device barred.')));
|
|
} else {
|
|
next(utils.createError(448, ('HMAC error: security check failed.')));
|
|
}
|
|
});
|
|
} else {
|
|
next(null);
|
|
}
|
|
});
|
|
};
|
|
// jshint +W074
|