'use strict';
var base32 = require('base32.js');
var crypto = require('crypto');
var url = require('url');
var util = require('util');
/**
* Digest the one-time passcode options.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {Integer} options.counter Counter value
* @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
* base32, base64).
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @param {String} [options.key] (DEPRECATED. Use `secret` instead.)
* Shared secret key
* @return {Buffer} The one-time passcode as a buffer.
*/
exports.digest = function digest (options) {
var i;
// unpack options
var secret = options.secret;
var counter = options.counter;
var encoding = options.encoding || 'ascii';
var algorithm = (options.algorithm || 'sha1').toLowerCase();
// Backwards compatibility - deprecated
if (options.key != null) {
console.warn('Speakeasy - Deprecation Notice - Specifying the secret using `key` is no longer supported. Use `secret` instead.');
secret = options.key;
}
// convert secret to buffer
if (!Buffer.isBuffer(secret)) {
secret = encoding === 'base32' ? base32.decode(secret)
: new Buffer(secret, encoding);
}
// create an buffer from the counter
var buf = new Buffer(8);
var tmp = counter;
for (i = 0; i < 8; i++) {
// mask 0xff over number to get last 8
buf[7 - i] = tmp & 0xff;
// shift 8 and get ready to loop over the next batch of 8
tmp = tmp >> 8;
}
// init hmac with the key
var hmac = crypto.createHmac(algorithm, secret);
// update hmac with the counter
hmac.update(buf);
// return the digest
return hmac.digest();
};
/**
* Generate a counter-based one-time token. Specify the key and counter, and
* receive the one-time password for that counter position as a string. You can
* also specify a token length, as well as the encoding (ASCII, hexadecimal, or
* base32) and the hashing algorithm to use (SHA1, SHA256, SHA512).
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {Integer} options.counter Counter value
* @param {Buffer} [options.digest] Digest, automatically generated by default
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
* @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
* base32, base64).
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @param {String} [options.key] (DEPRECATED. Use `secret` instead.)
* Shared secret key
* @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The
* number of digits for the one-time passcode.
* @return {String} The one-time passcode.
*/
exports.hotp = function hotpGenerate (options) {
// unpack digits
// backward compatibility: `length` is also accepted here, but deprecated
var digits = (options.digits != null ? options.digits : options.length) || 6;
if (options.length != null) console.warn('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.');
// digest the options
var digest = options.digest || exports.digest(options);
// compute HOTP offset
var offset = digest[digest.length - 1] & 0xf;
// calculate binary code (RFC4226 5.4)
var code = (digest[offset] & 0x7f) << 24 |
(digest[offset + 1] & 0xff) << 16 |
(digest[offset + 2] & 0xff) << 8 |
(digest[offset + 3] & 0xff);
// left-pad code
code = new Array(digits + 1).join('0') + code.toString(10);
// return length number off digits
return code.substr(-digits);
};
// Alias counter() for hotp()
exports.counter = exports.hotp;
/**
* Verify a counter-based one-time token against the secret and return the delta.
* By default, it verifies the token at the given counter value, with no leeway
* (no look-ahead or look-behind). A token validated at the current counter value
* will have a delta of 0.
*
* You can specify a window to add more leeway to the verification process.
* Setting the window param will check for the token at the given counter value
* as well as `window` tokens ahead (one-sided window). See param for more info.
*
* `verifyDelta()` will return the delta between the counter value of the token
* and the given counter value. For example, if given a counter 5 and a window
* 10, `verifyDelta()` will look at tokens from 5 to 15, inclusive. If it finds
* it at counter position 7, it will return `{ delta: 2 }`.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {String} options.token Passcode to validate
* @param {Integer} options.counter Counter value. This should be stored by
* the application and must be incremented for each request.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
* @param {Integer} [options.window=0] The allowable margin for the counter.
* The function will check "W" codes in the future against the provided
* passcode, e.g. if W = 10, and C = 5, this function will check the
* passcode against all One Time Passcodes between 5 and 15, inclusive.
* @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
* base32, base64).
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @return {Object} On success, returns an object with the counter
* difference between the client and the server as the `delta` property (i.e.
* `{ delta: 0 }`).
* @method hotp.verifyDelta
* @global
*/
exports.hotp.verifyDelta = function hotpVerifyDelta (options) {
var i;
// shadow options
options = Object.create(options);
// unpack options
var token = String(options.token);
var digits = parseInt(options.digits, 10) || 6;
var window = parseInt(options.window, 10) || 0;
var counter = parseInt(options.counter, 10) || 0;
// fail if token is not of correct length
if (token.length !== digits) {
return;
}
// parse token to integer
token = parseInt(token, 10);
// fail if token is NA
if (isNaN(token)) {
return;
}
// loop from C to C + W inclusive
for (i = counter; i <= counter + window; ++i) {
options.counter = i;
// domain-specific constant-time comparison for integer codes
if (parseInt(exports.hotp(options), 10) === token) {
// found a matching code, return delta
return {delta: i - counter};
}
}
// no codes have matched
};
/**
* Verify a counter-based one-time token against the secret and return true if
* it verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
* instead of an object. For more on how to use a window with this, see
* {@link hotp.verifyDelta}.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {String} options.token Passcode to validate
* @param {Integer} options.counter Counter value. This should be stored by
* the application and must be incremented for each request.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
* @param {Integer} [options.window=0] The allowable margin for the counter.
* The function will check "W" codes in the future against the provided
* passcode, e.g. if W = 10, and C = 5, this function will check the
* passcode against all One Time Passcodes between 5 and 15, inclusive.
* @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
* base32, base64).
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @return {Boolean} Returns true if the token matches within the given
* window, false otherwise.
* @method hotp.verify
* @global
*/
exports.hotp.verify = function hotpVerify (options) {
return exports.hotp.verifyDelta(options) != null;
};
/**
* Calculate counter value based on given options. A counter value converts a
* TOTP time into a counter value by finding the number of time steps that have
* passed since the epoch to the current time.
*
* @param {Object} options
* @param {Integer} [options.time] Time in seconds with which to calculate
* counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
* @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from
* which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.)
* Initial time in seconds since the UNIX epoch from which to calculate the
* counter value. Defaults to 0 (no offset).
* @return {Integer} The calculated counter value.
* @private
*/
exports._counter = function _counter (options) {
var step = options.step || 30;
var time = options.time != null ? (options.time * 1000) : Date.now();
// also accepts 'initial_time', but deprecated
var epoch = (options.epoch != null ? (options.epoch * 1000) : (options.initial_time * 1000)) || 0;
if (options.initial_time != null) console.warn('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.');
return Math.floor((time - epoch) / step / 1000);
};
/**
* Generate a time-based one-time token. Specify the key, and receive the
* one-time password for that time as a string. By default, it uses the current
* time and a time step of 30 seconds, so there is a new token every 30 seconds.
* You may override the time step and epoch for custom timing. You can also
* specify a token length, as well as the encoding (ASCII, hexadecimal, or
* base32) and the hashing algorithm to use (SHA1, SHA256, SHA512).
*
* Under the hood, TOTP calculates the counter value by finding how many time
* steps have passed since the epoch, and calls HOTP with that counter value.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {Integer} [options.time] Time in seconds with which to calculate
* counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
* @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX
* epoch from which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.counter] Counter value, calculated by default.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
* @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
* base32, base64).
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @param {String} [options.key] (DEPRECATED. Use `secret` instead.)
* Shared secret key
* @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.)
* Initial time in seconds since the UNIX epoch from which to calculate the
* counter value. Defaults to 0 (no offset).
* @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The
* number of digits for the one-time passcode.
* @return {String} The one-time passcode.
*/
exports.totp = function totpGenerate (options) {
// shadow options
options = Object.create(options);
// calculate default counter value
if (options.counter == null) options.counter = exports._counter(options);
// pass to hotp
return this.hotp(options);
};
// Alias time() for totp()
exports.time = exports.totp;
/**
* Verify a time-based one-time token against the secret and return the delta.
* By default, it verifies the token at the current time window, with no leeway
* (no look-ahead or look-behind). A token validated at the current time window
* will have a delta of 0.
*
* You can specify a window to add more leeway to the verification process.
* Setting the window param will check for the token at the given counter value
* as well as `window` tokens ahead and `window` tokens behind (two-sided
* window). See param for more info.
*
* `verifyDelta()` will return the delta between the counter value of the token
* and the given counter value. For example, if given a time at counter 1000 and
* a window of 5, `verifyDelta()` will look at tokens from 995 to 1005,
* inclusive. In other words, if the time-step is 30 seconds, it will look at
* tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
* If it finds it at counter position 1002, it will return `{ delta: 2 }`.
* If it finds it at counter position 997, it will return `{ delta: -3 }`.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {String} options.token Passcode to validate
* @param {Integer} [options.time] Time in seconds with which to calculate
* counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
* @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX
* epoch from which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.counter] Counter value, calculated by default.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
* @param {Integer} [options.window=0] The allowable margin for the counter.
* The function will check "W" codes in the future and the past against the
* provided passcode, e.g. if W = 5, and C = 1000, this function will check
* the passcode against all One Time Passcodes between 995 and 1005,
* inclusive.
* @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
* base32, base64).
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @return {Object} On success, returns an object with the time step
* difference between the client and the server as the `delta` property (e.g.
* `{ delta: 0 }`).
* @method totp.verifyDelta
* @global
*/
exports.totp.verifyDelta = function totpVerifyDelta (options) {
// shadow options
options = Object.create(options);
// unpack options
var window = parseInt(options.window, 10) || 0;
// calculate default counter value
if (options.counter == null) options.counter = exports._counter(options);
// adjust for two-sided window
options.counter -= window;
options.window += window;
// pass to hotp.verifyDelta
var delta = exports.hotp.verifyDelta(options);
// adjust for two-sided window
if (delta) {
delta.delta -= window;
}
return delta;
};
/**
* Verify a time-based one-time token against the secret and return true if it
* verifies. Helper function for verifyDelta() that returns a boolean instead of
* an object. For more on how to use a window with this, see
* {@link totp.verifyDelta}.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {String} options.token Passcode to validate
* @param {Integer} [options.time] Time in seconds with which to calculate
* counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
* @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX
* epoch from which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.counter] Counter value, calculated by default.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
* @param {Integer} [options.window=0] The allowable margin for the counter.
* The function will check "W" codes in the future and the past against the
* provided passcode, e.g. if W = 5, and C = 1000, this function will check
* the passcode against all One Time Passcodes between 995 and 1005,
* inclusive.
* @param {String} [options.encoding="ascii"] Key encoding (ascii, hex,
* base32, base64).
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @return {Boolean} Returns true if the token matches within the given
* window, false otherwise.
* @method totp.verify
* @global
*/
exports.totp.verify = function totpVerify (options) {
return exports.totp.verifyDelta(options) != null;
};
/**
* @typedef GeneratedSecret
* @type Object
* @property {String} ascii ASCII representation of the secret
* @property {String} hex Hex representation of the secret
* @property {String} base32 Base32 representation of the secret
* @property {String} qr_code_ascii URL for the QR code for the ASCII secret.
* @property {String} qr_code_hex URL for the QR code for the hex secret.
* @property {String} qr_code_base32 URL for the QR code for the base32 secret.
* @property {String} google_auth_qr URL for the Google Authenticator otpauth
* URL's QR code.
* @property {String} otpauth_url Google Authenticator-compatible otpauth URL.
*/
/**
* Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length
* (default 32). Returns the secret key in ASCII, hexadecimal, and base32 format,
* along with the URL used for the QR code for Google Authenticator (an otpauth
* URL). Use a QR code library to generate a QR code based on the Google
* Authenticator URL to obtain a QR code you can scan into the app.
*
* @param {Object} options
* @param {Integer} [options.length=32] Length of the secret
* @param {Boolean} [options.symbols=false] Whether to include symbols
* @param {Boolean} [options.otpauth_url=true] Whether to output a Google
* Authenticator-compatible otpauth:// URL (only returns otpauth:// URL, no
* QR code)
* @param {String} [options.name] The name to use with Google Authenticator.
* @param {Boolean} [options.qr_codes=false] (DEPRECATED. Do not use to prevent
* leaking of secret to a third party. Use your own QR code implementation.)
* Output QR code URLs for the token.
* @param {Boolean} [options.google_auth_qr=false] (DEPRECATED. Do not use to
* prevent leaking of secret to a third party. Use your own QR code
* implementation.) Output a Google Authenticator otpauth:// QR code URL.
* @return {Object}
* @return {GeneratedSecret} The generated secret key.
*/
exports.generateSecret = function generateSecret (options) {
// options
if (!options) options = {};
var length = options.length || 32;
var name = encodeURIComponent(options.name || 'SecretKey');
var qr_codes = options.qr_codes || false;
var google_auth_qr = options.google_auth_qr || false;
var otpauth_url = options.otpauth_url != null ? options.otpauth_url : true;
var symbols = true;
// turn off symbols only when explicity told to
if (options.symbols !== undefined && options.symbols === false) {
symbols = false;
}
// generate an ascii key
var key = this.generateSecretASCII(length, symbols);
// return a SecretKey with ascii, hex, and base32
var SecretKey = {};
SecretKey.ascii = key;
SecretKey.hex = Buffer(key, 'ascii').toString('hex');
SecretKey.base32 = base32.encode(Buffer(key)).toString().replace(/=/g, '');
// generate some qr codes if requested
if (qr_codes) {
console.warn('Speakeasy - Deprecation Notice - generateSecret() QR codes are deprecated and no longer supported. Please use your own QR code implementation.');
SecretKey.qr_code_ascii = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.ascii);
SecretKey.qr_code_hex = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.hex);
SecretKey.qr_code_base32 = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.base32);
}
// add in the Google Authenticator-compatible otpauth URL
if (otpauth_url) {
SecretKey.otpauth_url = exports.otpauthURL({
secret: SecretKey.ascii,
label: name
});
}
// generate a QR code for use in Google Authenticator if requested
if (google_auth_qr) {
console.warn('Speakeasy - Deprecation Notice - generateSecret() Google Auth QR code is deprecated and no longer supported. Please use your own QR code implementation.');
SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.otpauthURL({ secret: SecretKey.base32, label: name }));
}
return SecretKey;
};
// Backwards compatibility - generate_key is deprecated
exports.generate_key = util.deprecate(function (options) {
return exports.generateSecret(options);
}, 'Speakeasy - Deprecation Notice - `generate_key()` is depreciated, please use `generateSecret()` instead.');
/**
* Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and
* symbols (if requested).
*
* @param {Integer} [length=32] The length of the key.
* @param {Boolean} [symbols=false] Whether to include symbols in the key.
* @return {String} The generated key.
*/
exports.generateSecretASCII = function generateSecretASCII (length, symbols) {
var bytes = crypto.randomBytes(length || 32);
var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
if (symbols) {
set += '!@#$%^&*()<>?/[]{},.:;';
}
var output = '';
for (var i = 0, l = bytes.length; i < l; i++) {
output += set[Math.floor(bytes[i] / 255.0 * (set.length - 1))];
}
return output;
};
// Backwards compatibility - generate_key_ascii is deprecated
exports.generate_key_ascii = util.deprecate(function (length, symbols) {
return exports.generateSecretASCII(length, symbols);
}, 'Speakeasy - Deprecation Notice - `generate_key_ascii()` is depreciated, please use `generateSecretASCII()` instead.');
/**
* Generate a Google Authenticator-compatible otpauth:// URL for passing the
* secret to a mobile device to install the secret.
*
* Authenticator considers TOTP codes valid for 30 seconds. Additionally,
* the app presents 6 digits codes to the user. According to the
* documentation, the period and number of digits are currently ignored by
* the app.
*
* To generate a suitable QR Code, pass the generated URL to a QR Code
* generator, such as the `qr-image` module.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
* @param {String} options.label Used to identify the account with which
* the secret key is associated, e.g. the user's email address.
* @param {String} [options.type="totp"] Either "hotp" or "totp".
* @param {Integer} [options.counter] The initial counter value, required
* for HOTP.
* @param {String} [options.issuer] The provider or service with which the
* secret key is associated.
* @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256,
* sha512).
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode. Currently ignored by Google Authenticator.
* @param {Integer} [options.period=30] The length of time for which a TOTP
* code will be valid, in seconds. Currently ignored by Google
* Authenticator.
* @param {String} [options.encoding] Key encoding (ascii, hex, base32,
* base64). If the key is not encoded in Base-32, it will be reencoded.
* @return {String} A URL suitable for use with the Google Authenticator.
* @throws Error if secret or label is missing, or if hotp is used and a
counter is missing, if the type is not one of `hotp` or `totp`, if the
number of digits is non-numeric, or an invalid period is used. Warns if
the number of digits is not either 6 or 8 (though 6 is the only one
supported by Google Authenticator), and if the hashihng algorithm is
not one of the supported SHA1, SHA256, or SHA512.
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*/
exports.otpauthURL = function otpauthURL (options) {
// unpack options
var secret = options.secret;
var label = options.label;
var issuer = options.issuer;
var type = (options.type || 'totp').toLowerCase();
var counter = options.counter;
var algorithm = options.algorithm;
var digits = options.digits;
var period = options.period;
var encoding = options.encoding || 'ascii';
// validate type
switch (type) {
case 'totp':
case 'hotp':
break;
default:
throw new Error('Speakeasy - otpauthURL - Invalid type `' + type + '`; must be `hotp` or `totp`');
}
// validate required options
if (!secret) throw new Error('Speakeasy - otpauthURL - Missing secret');
if (!label) throw new Error('Speakeasy - otpauthURL - Missing label');
// require counter for HOTP
if (type === 'hotp' && (counter === null || typeof counter === 'undefined')) {
throw new Error('Speakeasy - otpauthURL - Missing counter value for HOTP');
}
// convert secret to base32
if (encoding !== 'base32') secret = new Buffer(secret, encoding);
if (Buffer.isBuffer(secret)) secret = base32.encode(secret);
// build query while validating
var query = {secret: secret};
if (issuer) query.issuer = issuer;
// validate algorithm
if (algorithm != null) {
switch (algorithm.toUpperCase()) {
case 'SHA1':
case 'SHA256':
case 'SHA512':
break;
default:
console.warn('Speakeasy - otpauthURL - Warning - Algorithm generally should be SHA1, SHA256, or SHA512');
}
query.algorithm = algorithm.toUpperCase();
}
// validate digits
if (digits != null) {
if (isNaN(digits)) {
throw new Error('Speakeasy - otpauthURL - Invalid digits `' + digits + '`');
} else {
switch (parseInt(digits, 10)) {
case 6:
case 8:
break;
default:
console.warn('Speakeasy - otpauthURL - Warning - Digits generally should be either 6 or 8');
}
}
query.digits = digits;
}
// validate period
if (period != null) {
period = parseInt(period, 10);
if (~~period !== period) {
throw new Error('Speakeasy - otpauthURL - Invalid period `' + period + '`');
}
query.period = period;
}
// return url
return url.format({
protocol: 'otpauth',
slashes: true,
hostname: type,
pathname: label,
query: query
});
};