bridge-node-server/node_server/ComServe/auth.js
Martin Donnelly 57bd6c8e6a init
2018-06-24 21:15:03 +01:00

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