rework api
make two subobjects `hotp` and `totp` with gen and verify methods
This commit is contained in:
parent
e633d857fd
commit
cfa2ecddb1
64
Readme.md
64
Readme.md
@ -14,15 +14,13 @@ IMPORTANT: The NOTP library accepts ASCII strings as keys, but the Google Authen
|
||||
```javascript
|
||||
var notp = require('notp');
|
||||
|
||||
var args = {};
|
||||
|
||||
//.... some initial login code, that receives the TOTP / HTOP
|
||||
// token from the user
|
||||
args.key = 'TOTP key for user... could be stored in DB';
|
||||
args.token = 'User supplied TOTP value';
|
||||
var key = 'TOTP key for user... could be stored in DB';
|
||||
var token = 'User supplied TOTP value';
|
||||
|
||||
// Check TOTP is correct
|
||||
var login = notp.checkTOTP(args);
|
||||
var login = notp.totp.verify(token, key);
|
||||
|
||||
// invalid token
|
||||
if (!login) {
|
||||
@ -34,7 +32,7 @@ console.log('Token valid, sync value is %s', login.delta);
|
||||
```
|
||||
|
||||
# API
|
||||
##notp.checkHOTP(args)
|
||||
##hotp.verify(token, key, opt)
|
||||
|
||||
Check a One Time Password based on a counter.
|
||||
|
||||
@ -47,12 +45,8 @@ console.log('Token valid, sync value is %s', login.delta);
|
||||
|
||||
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.
|
||||
|
||||
opt
|
||||
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
|
||||
@ -71,12 +65,10 @@ console.log('Token valid, sync value is %s', login.delta);
|
||||
**Example**
|
||||
|
||||
```javascript
|
||||
var opt = {
|
||||
key : 'USER SPECIFIC KEY', // Should be ASCII string
|
||||
token : 'USER SUPPLIED PASSCODE'
|
||||
};
|
||||
var key = 'USER SPECIFIC KEY', // Should be ASCII string
|
||||
var token = 'USER SUPPLIED PASSCODE'
|
||||
|
||||
var res = notp.checkHOTP(opt);
|
||||
var res = notp.hotp.verify(token, key, opt);
|
||||
|
||||
// not valid
|
||||
if (!res) {
|
||||
@ -86,7 +78,7 @@ if (!res) {
|
||||
console.log('valid, counter is out of sync by %d steps', res.delta);
|
||||
```
|
||||
|
||||
##notp.checkTOTP(args, err, cb)
|
||||
##totp.verify(token, key, opt)
|
||||
|
||||
|
||||
Check a One Time Password based on a timer.
|
||||
@ -100,12 +92,7 @@ console.log('valid, counter is out of sync by %d steps', res.delta);
|
||||
|
||||
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.
|
||||
|
||||
opt
|
||||
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
|
||||
@ -126,12 +113,10 @@ console.log('valid, counter is out of sync by %d steps', res.delta);
|
||||
**Example**
|
||||
|
||||
```javascript
|
||||
var opt = {
|
||||
key : 'USER SPECIFIC KEY', // Should be ASCII string
|
||||
token : 'USER SUPPLIED PASSCODE'
|
||||
};
|
||||
var key = 'USER SPECIFIC KEY', // Should be ASCII string
|
||||
var token = 'USER SUPPLIED PASSCODE'
|
||||
|
||||
var res = notp.checkTOTP(opt);
|
||||
var res = notp.totp.verify(token, key, opt);
|
||||
|
||||
// not valid
|
||||
if (!res) {
|
||||
@ -141,7 +126,7 @@ if (!res) {
|
||||
console.log('valid, counter is out of sync by %d steps', res.delta);
|
||||
```
|
||||
|
||||
##notp.getHOTP(args, err, cb)
|
||||
##hotp.gen(key, opt)
|
||||
|
||||
Generate a counter based One Time Password
|
||||
|
||||
@ -149,23 +134,19 @@ console.log('valid, counter is out of sync by %d steps', res.delta);
|
||||
|
||||
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
|
||||
|
||||
opt
|
||||
counter - Counter value. This should be stored by the application, must
|
||||
be user specific, and be incremented for each request.
|
||||
|
||||
**Example**
|
||||
|
||||
```javascript
|
||||
var token = notp.getHOTP({
|
||||
key : 'USER SPECIFIC KEY', // Should be ASCII string
|
||||
token : 5 // COUNTER VALUE
|
||||
var token = notp.hotp.gen(key, {
|
||||
counter : 5 // COUNTER VALUE
|
||||
});
|
||||
```
|
||||
|
||||
##notp.getTOTP(args, err, cb)
|
||||
##totp.gen(key, opt)
|
||||
|
||||
Generate a time based One Time Password
|
||||
|
||||
@ -173,10 +154,7 @@ var token = notp.getHOTP({
|
||||
|
||||
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
|
||||
|
||||
opt
|
||||
time - The time step of the counter. This must be the same for
|
||||
every request and is used to calculate C.
|
||||
|
||||
@ -185,9 +163,7 @@ var token = notp.getHOTP({
|
||||
**Example**
|
||||
|
||||
```javascript
|
||||
var token = notp.getTOTP({
|
||||
key : 'USER SPECIFIC KEY' // Should be ASCII string
|
||||
});
|
||||
var token = notp.totp.gen(key);
|
||||
```
|
||||
|
||||
## License
|
||||
|
181
index.js
181
index.js
@ -1,6 +1,52 @@
|
||||
|
||||
var crypto = require('crypto');
|
||||
|
||||
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 it is the seed 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) {
|
||||
var key = key || '';
|
||||
var counter = opt.counter || 0;
|
||||
|
||||
var p = 6;
|
||||
|
||||
// Create the byte array
|
||||
var b = new Buffer(intToBytes(counter));
|
||||
|
||||
var hmac = crypto.createHmac('SHA1', new Buffer(key));
|
||||
|
||||
// Update the HMAC witht he byte array
|
||||
var digest = hmac.update(b).digest('hex');
|
||||
|
||||
// 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 + '';
|
||||
|
||||
return v.substr(v.length - p, p);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check a One Time Password based on a counter.
|
||||
*
|
||||
@ -30,17 +76,15 @@ var crypto = require('crypto');
|
||||
* be user specific, and be incremented for each request.
|
||||
*
|
||||
*/
|
||||
module.exports.checkHOTP = function(opt) {
|
||||
|
||||
hotp.verify = function(token, key, opt) {
|
||||
var window = opt.window || 50;
|
||||
var counter = opt.counter || 0;
|
||||
var token = opt.token || '';
|
||||
|
||||
// Now loop through from C to C + W to determine if there is
|
||||
// a correct code
|
||||
for(var i = counter; i <= counter + window; ++i) {
|
||||
opt.counter = i;
|
||||
if(this.getHOTP(opt) === token) {
|
||||
if(this.gen(key, opt) === token) {
|
||||
// We have found a matching code, trigger callback
|
||||
// and pass offset
|
||||
return { delta: i - counter };
|
||||
@ -51,6 +95,45 @@ module.exports.checkHOTP = function(opt) {
|
||||
return false;
|
||||
};
|
||||
|
||||
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) {
|
||||
var time = opt.time || 30;
|
||||
var _t = new Date().getTime();;
|
||||
|
||||
// Time has been overwritten.
|
||||
if(opt._t) {
|
||||
console.log('#####################################');
|
||||
console.log('# NOTE: TOTP TIME VARIABLE HAS BEEN #');
|
||||
console.log('# OVERWRITTEN. THIS SHOULD ONLY BE #');
|
||||
console.log('# USED FOR TEST PURPOSES. #');
|
||||
console.log('#####################################');
|
||||
_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.
|
||||
@ -83,8 +166,7 @@ module.exports.checkHOTP = function(opt) {
|
||||
* Default - 30
|
||||
*
|
||||
*/
|
||||
module.exports.checkTOTP = function(opt) {
|
||||
|
||||
totp.verify = function(token, key, opt) {
|
||||
var time = opt.time || 30;
|
||||
var _t = new Date().getTime();
|
||||
|
||||
@ -102,92 +184,11 @@ module.exports.checkTOTP = function(opt) {
|
||||
// This is the number of time steps in seconds since T0
|
||||
opt.counter = Math.floor((_t / 1000) / time);
|
||||
|
||||
return module.exports.checkHOTP(opt);
|
||||
return hotp.verify(token, key, opt);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 it is the seed 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.
|
||||
*
|
||||
*/
|
||||
module.exports.getHOTP = function(opt) {
|
||||
var key = opt.key || '';
|
||||
var counter = opt.counter || 0;
|
||||
|
||||
var p = 6;
|
||||
|
||||
// Create the byte array
|
||||
var b = new Buffer(intToBytes(counter));
|
||||
|
||||
var hmac = crypto.createHmac('SHA1', new Buffer(key));
|
||||
|
||||
// Update the HMAC witht he byte array
|
||||
var digest = hmac.update(b).digest('hex');
|
||||
|
||||
// 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 + '';
|
||||
|
||||
return v.substr(v.length - p, p);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
module.exports.getTOTP = function(opt) {
|
||||
var time = opt.time || 30;
|
||||
var _t = new Date().getTime();;
|
||||
|
||||
// Time has been overwritten.
|
||||
if(opt._t) {
|
||||
console.log('#####################################');
|
||||
console.log('# NOTE: TOTP TIME VARIABLE HAS BEEN #');
|
||||
console.log('# OVERWRITTEN. THIS SHOULD ONLY BE #');
|
||||
console.log('# USED FOR TEST PURPOSES. #');
|
||||
console.log('#####################################');
|
||||
_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 this.getHOTP(opt);
|
||||
};
|
||||
module.exports.hotp = hotp;
|
||||
module.exports.totp = totp;
|
||||
|
||||
/**
|
||||
* convert an integer to a byte array
|
||||
|
120
test/notp.js
120
test/notp.js
@ -44,22 +44,20 @@ var notp = require('..');
|
||||
* see http://tools.ietf.org/html/rfc4226
|
||||
*/
|
||||
exports.testHOTP = function(beforeExit, assert) {
|
||||
var args = {
|
||||
var key = '12345678901234567890';
|
||||
var opt = {
|
||||
window : 0,
|
||||
key : '12345678901234567890'
|
||||
};
|
||||
var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489'];
|
||||
|
||||
// counterheck for failure
|
||||
args.counter = 0;
|
||||
args.token = 'windowILLNOTtokenASS';
|
||||
assert.ok(!notp.checkHOTP(args), 'Should not pass');
|
||||
opt.counter = 0;
|
||||
assert.ok(!notp.hotp.verify('WILL NOT PASS', key, opt), 'Should not pass');
|
||||
|
||||
// counterheck for passes
|
||||
for(i=0;i<HOTP.length;i++) {
|
||||
args.counter = i;
|
||||
args.token = HOTP[i];
|
||||
var res = notp.checkHOTP(args);
|
||||
opt.counter = i;
|
||||
var res = notp.hotp.verify(HOTP[i], key, opt);
|
||||
|
||||
assert.ok(res, 'Should pass');
|
||||
assert.eql(res.delta, 0, 'Should be in sync');
|
||||
@ -73,41 +71,41 @@ exports.testHOTP = function(beforeExit, assert) {
|
||||
* see http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt
|
||||
*/
|
||||
exports.testTOTtoken = function(beforeExit, assert) {
|
||||
var args = {
|
||||
var key = '12345678901234567890';
|
||||
var opt = {
|
||||
window : 0,
|
||||
key : '12345678901234567890'
|
||||
};
|
||||
|
||||
// counterheck for failure
|
||||
args.T = 0;
|
||||
args.token = 'windowILLNOTtokenASS';
|
||||
assert.ok(!notp.checkTOTP(args), 'Should not pass');
|
||||
opt.time = 0;
|
||||
var token = 'windowILLNOTtokenASS';
|
||||
assert.ok(!notp.totp.verify(token, key, opt), 'Should not pass');
|
||||
|
||||
// counterheck for test vector at 59s
|
||||
args._t = 59*1000;
|
||||
args.token = '287082';
|
||||
var res = notp.checkTOTP(args);
|
||||
opt._t = 59*1000;
|
||||
var token = '287082';
|
||||
var res = notp.totp.verify(token, key, opt);
|
||||
assert.ok(res, 'Should pass');
|
||||
assert.eql(res.delta, 0, 'Should be in sync');
|
||||
|
||||
// counterheck for test vector at 1234567890
|
||||
args._t = 1234567890*1000;
|
||||
args.token = '005924';
|
||||
var res = notp.checkTOTP(args);
|
||||
opt._t = 1234567890*1000;
|
||||
var token = '005924';
|
||||
var res = notp.totp.verify(token, key, opt);
|
||||
assert.ok(res, 'Should pass');
|
||||
assert.eql(res.delta, 0, 'Should be in sync');
|
||||
|
||||
// counterheck for test vector at 1111111109
|
||||
args._t = 1111111109*1000;
|
||||
args.token = '081804';
|
||||
var res = notp.checkTOTP(args);
|
||||
opt._t = 1111111109*1000;
|
||||
var token = '081804';
|
||||
var res = notp.totp.verify(token, key, opt);
|
||||
assert.ok(res, 'Should pass');
|
||||
assert.eql(res.delta, 0, 'Should be in sync');
|
||||
|
||||
// counterheck for test vector at 2000000000
|
||||
args._t = 2000000000*1000;
|
||||
args.token = '279037';
|
||||
var res = notp.checkTOTP(args);
|
||||
opt._t = 2000000000*1000;
|
||||
var token = '279037';
|
||||
var res = notp.totp.verify(token, key, opt);
|
||||
assert.ok(res, 'Should pass');
|
||||
assert.eql(res.delta, 0, 'Should be in sync');
|
||||
};
|
||||
@ -118,21 +116,22 @@ exports.testTOTtoken = function(beforeExit, assert) {
|
||||
* windowe are going to use a value of counter = 1 and test against
|
||||
* a code for counter = 9
|
||||
*/
|
||||
exports.testHOTPtokenOutOfSync = function(beforeExit, assert) {
|
||||
exports.testHOTPOutOfSync = function(beforeExit, assert) {
|
||||
|
||||
var args = {
|
||||
key : '12345678901234567890',
|
||||
token : '520489',
|
||||
var key = '12345678901234567890';
|
||||
var token = '520489';
|
||||
|
||||
var opt = {
|
||||
counter : 1
|
||||
};
|
||||
|
||||
// counterheck that the test should fail for window < 8
|
||||
args.window = 7;
|
||||
assert.ok(!notp.checkHOTP(args), 'Should not pass for value of window < 8');
|
||||
opt.window = 7;
|
||||
assert.ok(!notp.hotp.verify(token, key, opt), 'Should not pass for value of window < 8');
|
||||
|
||||
// counterheck that the test should pass for window >= 9
|
||||
args.window = 8;
|
||||
assert.ok(notp.checkHOTP(args), 'Should pass for value of window >= 9');
|
||||
opt.window = 8;
|
||||
assert.ok(notp.hotp.verify(token, key, opt), 'Should pass for value of window >= 9');
|
||||
};
|
||||
|
||||
|
||||
@ -140,66 +139,61 @@ exports.testHOTPtokenOutOfSync = function(beforeExit, assert) {
|
||||
* counterheck for codes that are out of sync
|
||||
* windowe are going to use a value of T = 1999999909 (91s behind 2000000000)
|
||||
*/
|
||||
exports.testTOTtokenOutOfSync = function(beforeExit, assert) {
|
||||
exports.testTOTPOutOfSync = function(beforeExit, assert) {
|
||||
|
||||
var args = {
|
||||
key : '12345678901234567890',
|
||||
token : '279037',
|
||||
var key = '12345678901234567890';
|
||||
var token = '279037';
|
||||
|
||||
var opt = {
|
||||
_t : 1999999909*1000
|
||||
};
|
||||
|
||||
// counterheck that the test should fail for window < 2
|
||||
args.window = 2;
|
||||
assert.ok(!notp.checkTOTP(args), 'Should not pass for value of window < 3');
|
||||
opt.window = 2;
|
||||
assert.ok(!notp.totp.verify(token, key, opt), 'Should not pass for value of window < 3');
|
||||
|
||||
// counterheck that the test should pass for window >= 3
|
||||
args.window = 3;
|
||||
assert.ok(notp.checkTOTP(args), 'Should pass for value of window >= 3');
|
||||
opt.window = 3;
|
||||
assert.ok(notp.totp.verify(token, key, opt), 'Should pass for value of window >= 3');
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Test getHOTtoken function. Uses same test values as for checkHOTtoken
|
||||
*/
|
||||
exports.testGetHOTtoken = function(beforeExit, assert) {
|
||||
var args = {
|
||||
exports.hotp_gen = function(beforeExit, assert) {
|
||||
var key = '12345678901234567890';
|
||||
var opt = {
|
||||
window : 0,
|
||||
key : '12345678901234567890'
|
||||
};
|
||||
|
||||
var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489'];
|
||||
|
||||
// counterheck for passes
|
||||
for(i=0;i<HOTP.length;i++) {
|
||||
args.counter = i;
|
||||
assert.eql(notp.getHOTP(args), HOTP[i], 'HTOtoken value should be correct');
|
||||
opt.counter = i;
|
||||
assert.eql(notp.hotp.gen(key, opt), HOTP[i], 'HOTP value should be correct');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Test getTOTtoken function. Uses same test values as for checkTOTtoken
|
||||
*/
|
||||
exports.testGetTOTtoken = function(beforeExit, assert) {
|
||||
var args = {
|
||||
exports.totp_gen = function(beforeExit, assert) {
|
||||
var key = '12345678901234567890';
|
||||
var opt = {
|
||||
window : 0,
|
||||
key : '12345678901234567890'
|
||||
};
|
||||
|
||||
// counterheck for test vector at 59s
|
||||
args._t = 59*1000;
|
||||
assert.eql(notp.getTOTP(args), '287082', 'TOTtoken values should match');
|
||||
opt._t = 59*1000;
|
||||
assert.eql(notp.totp.gen(key, opt), '287082', 'TOTtoken values should match');
|
||||
|
||||
// counterheck for test vector at 1234567890
|
||||
args._t = 1234567890*1000;
|
||||
assert.eql(notp.getTOTP(args), '005924', 'TOTtoken values should match');
|
||||
opt._t = 1234567890*1000;
|
||||
assert.eql(notp.totp.gen(key, opt), '005924', 'TOTtoken values should match');
|
||||
|
||||
// counterheck for test vector at 1111111109
|
||||
args._t = 1111111109*1000;
|
||||
assert.eql(notp.getTOTP(args), '081804', 'TOTtoken values should match');
|
||||
opt._t = 1111111109*1000;
|
||||
assert.eql(notp.totp.gen(key, opt), '081804', 'TOTtoken values should match');
|
||||
|
||||
// counterheck for test vector at 2000000000
|
||||
args._t = 2000000000*1000;
|
||||
assert.eql(notp.getTOTP(args), '279037', 'TOTtoken values should match');
|
||||
opt._t = 2000000000*1000;
|
||||
assert.eql(notp.totp.gen(key, opt), '279037', 'TOTtoken values should match');
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user