Skip to content

Commit

Permalink
feat: allow forward and backward time windows
Browse files Browse the repository at this point in the history
  • Loading branch information
yeojz committed Apr 21, 2018
1 parent a5dbf52 commit e4c12e5
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 49 deletions.
3 changes: 2 additions & 1 deletion packages/otplib-authenticator/Authenticator.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ class Authenticator extends TOTP {
return {
encoding: 'hex',
epoch: null,
step: 30
step: 30,
window: 0
};
}

Expand Down
3 changes: 2 additions & 1 deletion packages/otplib-authenticator/Authenticator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ describe('Authenticator', () => {
expect(options).toEqual({
encoding: 'hex',
epoch: null,
step: 30
step: 30,
window: 0
});
});

Expand Down
3 changes: 2 additions & 1 deletion packages/otplib-authenticator/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import decodeKey from './decodeKey';
* @return {boolean}
*/
function check(token, secret, options) {
return totpCheckWithWindow(token, decodeKey(secret), options) >= 0;
const delta = totpCheckWithWindow(token, decodeKey(secret), options);
return Number.isInteger(delta);
}

export default check;
50 changes: 33 additions & 17 deletions packages/otplib-core/totpCheckWithWindow.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
import totpCheck from './totpCheck';

function createChecker(token, secret, opt) {
const delta = opt.step * 1000;
const epoch = opt.epoch;

return (direction, start, bounds) => {
for (let i = start; i <= bounds; i++) {
opt.epoch = epoch + direction * i * delta;

if (totpCheck(token, secret, opt)) {
return i === 0 ? 0 : direction * i;
}
}
return null;
};
}

function getWindowBounds(opt) {
const bounds = Array.isArray(opt.window)
? opt.window
: [parseInt(opt.window, 10), parseInt(opt.window, 10)];

if (!Number.isInteger(bounds[0]) || !Number.isInteger(bounds[1])) {
throw new Error('Expecting options.window to be a number or an array');
}

return bounds;
}

/**
* Checks the provided OTP token against system generated token
* with support for checking previous x time-step windows
Expand All @@ -8,27 +36,15 @@ import totpCheck from './totpCheck';
* @param {string} token - the OTP token to check
* @param {string} secret - your secret that is used to generate the token
* @param {object} options - options which was used to generate it originally
* @return {integer} - the number of windows back it was successful. -1 otherwise
* @return {integer} - the number of windows back (-) or forward it was successful. null otherwise
*/
function totpCheckWithWindow(token, secret, options) {
let opt = Object.assign({}, options);

if (typeof opt.window !== 'number') {
throw new Error('Expecting options.window to be a number');
}

const decrement = opt.step * 1000;
const epoch = opt.epoch;

for (let i = 0; i <= opt.window; i++) {
opt.epoch = epoch - i * decrement;

if (totpCheck(token, secret, opt)) {
return i;
}
}

return -1;
const bounds = getWindowBounds(opt);
const checker = createChecker(token, secret, opt);
const backward = checker(-1, 0, bounds[0]);
return backward !== null ? backward : checker(1, 1, bounds[1]);
}

export default totpCheckWithWindow;
84 changes: 73 additions & 11 deletions packages/otplib-core/totpCheckWithWindow.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,56 +38,118 @@ describe('totpCheck', () => {

it('should throw an error when opt.window is undefined', () => {
expect(() => totpCheckWithWindow('a', 'b', {})).toThrowError(
'Expecting options.window to be a number'
'Expecting options.window to be a number or an array'
);
});

it('should call totpCheck 1 time when window is 0', () => {
totpCheck.mockImplementation(() => false);

totpCheckWithWindow(token(0), secret, getOptions(1, 0));

totpCheckWithWindow(token(0), secret, getOptions(1, [0, 0]));
expect(totpCheck).toHaveBeenCalledTimes(1);
});

it('should call totpCheck 2 times when window is 1', () => {
it('(backward) should call totpCheck 2 times when window is -1', () => {
totpCheck.mockImplementation(() => false);

totpCheckWithWindow('', secret, getOptions(1, 1));
totpCheckWithWindow('', secret, getOptions(1, [1, 0]));

expect(totpCheck).toHaveBeenCalledTimes(2);
});

it('time 2, window 1, token 0, called 2, return -1', () => {
it('(backward) time 2, window -1, token 0, called 2, return null', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(0), secret, getOptions(2, 1));
const result = totpCheckWithWindow(token(0), secret, getOptions(2, [1, 0]));

expect(result).toBe(null);
expect(totpCheck).toHaveBeenCalledTimes(2);
});

it('(backward) time 1, window -1, token 0, called 2, return -1', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(0), secret, getOptions(1, [1, 0]));

expect(result).toBe(-1);
expect(totpCheck).toHaveBeenCalledTimes(2);
});

it('time 1, window 1, token 1, called 2, return 1', () => {
it('(backward) time 2, window -2, token 0, called 3, return -2', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(0), secret, getOptions(1, 1));
const result = totpCheckWithWindow(token(0), secret, getOptions(2, [2, 0]));

expect(result).toBe(-2);
expect(totpCheck).toHaveBeenCalledTimes(3);
});

it('(forward) should call totpCheck 2 times when window is 1', () => {
totpCheck.mockImplementation(() => false);

totpCheckWithWindow('', secret, getOptions(1, [0, 1]));

expect(totpCheck).toHaveBeenCalledTimes(2);
});

it('(forward) time 0, window 1, token 2, called 2, return null', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(2), secret, getOptions(0, [0, 1]));

expect(result).toBe(null);
expect(totpCheck).toHaveBeenCalledTimes(2);
});

it('(forward) time 1, window 1, token 2, called 2, return 1', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(2), secret, getOptions(1, [0, 1]));

expect(result).toBe(1);
expect(totpCheck).toHaveBeenCalledTimes(2);
});

it('time 2, window 2, token 0, called 3, return 2', () => {
it('(forward) time 0, window 2, token 2, called 3, return 2', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(0), secret, getOptions(2, 2));
const result = totpCheckWithWindow(token(2), secret, getOptions(0, [0, 2]));

expect(result).toBe(2);
expect(totpCheck).toHaveBeenCalledTimes(3);
});

it('(both) time 1, window 1, token 2, called 3, return 1', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(2), secret, getOptions(1, 1));

expect(result).toBe(1);
expect(totpCheck).toHaveBeenCalledTimes(3);
});

it('(both) time 1, window 1, token 0, called 2, return 1', () => {
totpCheck.mockImplementation((...args) => {
return totpCheckOriginal(...args);
});

const result = totpCheckWithWindow(token(2), secret, getOptions(1, 1));

expect(result).toBe(1);
expect(totpCheck).toHaveBeenCalledTimes(3);
});
});
5 changes: 1 addition & 4 deletions packages/otplib-core/totpOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,11 @@ const defaultOptions = {
* @param {number} options.digits - the output token length
* @param {string} options.epoch - starting time since the UNIX epoch (seconds)
* @param {number} options.step - time step (seconds)
* @param {number} options.window - acceptable window where codes a valid. Will be rounded down to nearest integer
* @param {number|array} options.window - acceptable window where codes a valid.
* @return {object}
*/
function totpOptions(options = {}) {
let opt = Object.assign(hotpOptions(), defaultOptions, options);

opt.window = Math.floor(opt.window || 0);

opt.epoch = typeof opt.epoch === 'number' ? opt.epoch * 1000 : Date.now();

return opt;
Expand Down
14 changes: 1 addition & 13 deletions packages/otplib-core/totpOptions.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import totpSecret from './totpSecret';
import totpOptions from './totpOptions';
import totpSecret from './totpSecret';

describe('totpOptions', () => {
const DateNow = global.Date.now;
Expand Down Expand Up @@ -48,16 +48,4 @@ describe('totpOptions', () => {

expect(totpOptions(opt)).toEqual(expected);
});

it('should return window with rounded down number', () => {
const opt = Object.assign({}, defaults, {
window: 1.5
});

const expected = Object.assign({}, opt, epoch, {
window: 1
});

expect(totpOptions(opt)).toEqual(expected);
});
});
3 changes: 2 additions & 1 deletion packages/otplib-totp/TOTP.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class TOTP extends HOTP {
*/
check(token, secret) {
const opt = this.optionsAll;
return totpCheckWithWindow(token, secret || opt.secret, opt) >= 0;
const delta = totpCheckWithWindow(token, secret || opt.secret, opt);
return Number.isInteger(delta);
}

/**
Expand Down

0 comments on commit e4c12e5

Please sign in to comment.