250 lines
6.5 KiB
JavaScript
250 lines
6.5 KiB
JavaScript
|
(function (exports, TEST) {
|
||
|
'use strict';
|
||
|
|
||
|
var crypto;
|
||
|
var sha1Hmac = exports.sha1Hmac || function (key, bytes) {
|
||
|
if (!crypto) { crypto = require('crypto'); }
|
||
|
|
||
|
var hmac = crypto.createHmac('sha1', new Buffer(key));
|
||
|
// Update the HMAC with the byte array
|
||
|
return hmac.update(new Buffer(bytes)).digest('hex');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* convert an integer to a byte array
|
||
|
* @param {Integer} num
|
||
|
* @return {Array} bytes
|
||
|
*/
|
||
|
function intToBytes(num) {
|
||
|
var bytes = [];
|
||
|
|
||
|
for(var i=7 ; i>=0 ; --i) {
|
||
|
bytes[i] = num & (255);
|
||
|
num = num >> 8;
|
||
|
}
|
||
|
|
||
|
return bytes;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* convert a hex value to a byte array
|
||
|
* @param {String} hex string of hex to convert to a byte array
|
||
|
* @return {Array} bytes
|
||
|
*/
|
||
|
function hexToBytes(hex) {
|
||
|
var bytes = [];
|
||
|
for(var c = 0, C = hex.length; c < C; c += 2) {
|
||
|
bytes.push(parseInt(hex.substr(c, 2), 16));
|
||
|
}
|
||
|
return bytes;
|
||
|
}
|
||
|
|
||
|
var hotp = {};
|
||
|
|
||
|
/**
|
||
|
* Generate a counter based One Time Password
|
||
|
*
|
||
|
* @return {String} the one time password
|
||
|
*
|
||
|
* Arguments:
|
||
|
*
|
||
|
* args
|
||
|
* key - Key for the one time password. This should be unique and secret for
|
||
|
* every user as this is the seed that is used to calculate the HMAC
|
||
|
*
|
||
|
* counter - Counter value. This should be stored by the application, must
|
||
|
* be user specific, and be incremented for each request.
|
||
|
*
|
||
|
*/
|
||
|
hotp.gen = function(key, opt) {
|
||
|
key = key || '';
|
||
|
opt = opt || {};
|
||
|
var counter = opt.counter || 0;
|
||
|
|
||
|
// Create the byte array
|
||
|
return sha1Hmac(key, intToBytes(counter)).then(function (digest) {
|
||
|
// Get byte array
|
||
|
var h = hexToBytes(digest);
|
||
|
|
||
|
// Truncate
|
||
|
var offset = h[19] & 0xf;
|
||
|
var v = (h[offset] & 0x7f) << 24 |
|
||
|
(h[offset + 1] & 0xff) << 16 |
|
||
|
(h[offset + 2] & 0xff) << 8 |
|
||
|
(h[offset + 3] & 0xff);
|
||
|
|
||
|
v = (v % 1000000) + '';
|
||
|
|
||
|
return new Array(7-v.length).join('0') + v;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check a One Time Password based on a counter.
|
||
|
*
|
||
|
* @return {Object} null if failure, { delta: # } on success
|
||
|
* delta is the time step difference between the client and the server
|
||
|
*
|
||
|
* Arguments:
|
||
|
*
|
||
|
* args
|
||
|
* key - Key for the one time password. This should be unique and secret for
|
||
|
* every user as it is the seed used to calculate the HMAC
|
||
|
*
|
||
|
* token - Passcode to validate.
|
||
|
*
|
||
|
* window - The allowable margin for the counter. The function will check
|
||
|
* 'W' codes in the future against the provided passcode. Note,
|
||
|
* it is the calling applications responsibility to keep track of
|
||
|
* 'W' and increment it for each password check, and also to adjust
|
||
|
* it accordingly in the case where the client and server become
|
||
|
* out of sync (second argument returns non zero).
|
||
|
* E.g. if W = 100, and C = 5, this function will check the passcode
|
||
|
* against all One Time Passcodes between 5 and 105.
|
||
|
*
|
||
|
* Default - 50
|
||
|
*
|
||
|
* counter - Counter value. This should be stored by the application, must
|
||
|
* be user specific, and be incremented for each request.
|
||
|
*
|
||
|
*/
|
||
|
hotp.verify = function(token, key, opt) {
|
||
|
opt = opt || {};
|
||
|
var window = opt.window || 50;
|
||
|
var counter = opt.counter || 0;
|
||
|
var i = counter - window;
|
||
|
var len = counter + window;
|
||
|
|
||
|
// Now loop through from C to C + W to determine if there is
|
||
|
// a correct code
|
||
|
function check(t) {
|
||
|
opt.counter = i + 1;
|
||
|
|
||
|
if (!t) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (i > len) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if(t === token) {
|
||
|
// We have found a matching code, trigger callback
|
||
|
// and pass offset
|
||
|
return i;
|
||
|
}
|
||
|
|
||
|
// TODO count 0, -1, 1, -2, 2, ... instead of -2, -1, 0, 1, ...
|
||
|
i += 1;
|
||
|
|
||
|
return hotp.gen(key, opt).then(check);
|
||
|
}
|
||
|
|
||
|
opt.counter = i;
|
||
|
return hotp.gen(key, opt).then(check).then(function (i) {
|
||
|
if('number' === typeof i) {
|
||
|
return { delta: i - counter };
|
||
|
}
|
||
|
|
||
|
// If we get to here then no codes have matched, return null
|
||
|
return null;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
var totp = {};
|
||
|
|
||
|
/**
|
||
|
* Generate a time based One Time Password
|
||
|
*
|
||
|
* @return {String} the one time password
|
||
|
*
|
||
|
* Arguments:
|
||
|
*
|
||
|
* args
|
||
|
* key - Key for the one time password. This should be unique and secret for
|
||
|
* every user as it is the seed used to calculate the HMAC
|
||
|
*
|
||
|
* time - The time step of the counter. This must be the same for
|
||
|
* every request and is used to calculat C.
|
||
|
*
|
||
|
* Default - 30
|
||
|
*
|
||
|
*/
|
||
|
totp.gen = function(key, opt) {
|
||
|
opt = opt || {};
|
||
|
var time = opt.time || 30;
|
||
|
var _t = Date.now();
|
||
|
|
||
|
// Time has been overwritten.
|
||
|
if(opt._t) {
|
||
|
if(!TEST) {
|
||
|
console.warn('Overwriting time in non-test environment!');
|
||
|
}
|
||
|
_t = opt._t;
|
||
|
}
|
||
|
|
||
|
// Determine the value of the counter, C
|
||
|
// This is the number of time steps in seconds since T0
|
||
|
opt.counter = Math.floor((_t / 1000) / time);
|
||
|
|
||
|
return hotp.gen(key, opt);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check a One Time Password based on a timer.
|
||
|
*
|
||
|
* @return {Object} null if failure, { delta: # } on success
|
||
|
* delta is the time step difference between the client and the server
|
||
|
*
|
||
|
* Arguments:
|
||
|
*
|
||
|
* args
|
||
|
* key - Key for the one time password. This should be unique and secret for
|
||
|
* every user as it is the seed used to calculate the HMAC
|
||
|
*
|
||
|
* token - Passcode to validate.
|
||
|
*
|
||
|
* window - The allowable margin for the counter. The function will check
|
||
|
* 'W' codes either side of the provided counter. Note,
|
||
|
* it is the calling applications responsibility to keep track of
|
||
|
* 'W' and increment it for each password check, and also to adjust
|
||
|
* it accordingly in the case where the client and server become
|
||
|
* out of sync (second argument returns non zero).
|
||
|
* E.g. if W = 5, and C = 1000, this function will check the passcode
|
||
|
* against all One Time Passcodes between 995 and 1005.
|
||
|
*
|
||
|
* Default - 6
|
||
|
*
|
||
|
* time - The time step of the counter. This must be the same for
|
||
|
* every request and is used to calculate C.
|
||
|
*
|
||
|
* Default - 30
|
||
|
*
|
||
|
*/
|
||
|
totp.verify = function(token, key, opt) {
|
||
|
opt = opt || {};
|
||
|
var time = opt.time || 30;
|
||
|
var _t = Date.now();
|
||
|
|
||
|
// Time has been overwritten.
|
||
|
if(opt._t) {
|
||
|
if(!TEST) {
|
||
|
console.warn('Overwriting time in non-test environment!');
|
||
|
}
|
||
|
_t = opt._t;
|
||
|
}
|
||
|
|
||
|
// Determine the value of the counter, C
|
||
|
// This is the number of time steps in seconds since T0
|
||
|
opt.counter = Math.floor((_t / 1000) / time);
|
||
|
|
||
|
return hotp.verify(token, key, opt);
|
||
|
};
|
||
|
|
||
|
exports.hotp = hotp;
|
||
|
exports.totp = totp;
|
||
|
}(
|
||
|
'undefined' !== typeof window ? window : module.exports
|
||
|
, 'undefined' !== typeof process ? process.env.NODE_ENV : false
|
||
|
));
|