From 7d7aa077432ad8d16b6fd667f4c56db1250fd021 Mon Sep 17 00:00:00 2001 From: Cathal Garvey Date: Wed, 4 Sep 2013 19:52:30 +0100 Subject: [PATCH 01/59] Double-escape otpauth URL paramaters passed to Google Charts API (allows for spaces/special chars in name) --- lib/speakeasy.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/speakeasy.js b/lib/speakeasy.js index 3e9693b..d3d5598 100644 --- a/lib/speakeasy.js +++ b/lib/speakeasy.js @@ -225,9 +225,8 @@ speakeasy.generate_key = function(options) { // generate a QR code for use in Google Authenticator if requested // (Google Authenticator has a special style and requires base32) if (google_auth_qr) { - // first, make sure that the name doesn't have spaces, since Google Authenticator doesn't like them - name = name.replace(/ /g,''); - SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://totp/' + encodeURIComponent(name) + '%3Fsecret=' + encodeURIComponent(SecretKey.base32); + var otpauthURL = 'otpauth://totp/' + encodeURIComponent( name ) + '?secret=' + encodeURIComponent( SecretKey.base32 ); + SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent( otpauthURL ); } return SecretKey; From 459b533e212b706a301ab53e7ffe1bec0d70c341 Mon Sep 17 00:00:00 2001 From: Vincent Lombard Date: Fri, 12 Sep 2014 15:11:10 +0200 Subject: [PATCH 02/59] ajout issuer counter et type --- lib/speakeasy.js | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/speakeasy.js b/lib/speakeasy.js index 3e9693b..8badca4 100644 --- a/lib/speakeasy.js +++ b/lib/speakeasy.js @@ -1,6 +1,6 @@ // # speakeasy // ### HMAC One-Time Password module for Node.js, supporting counter-based and time-based moving factors -// +// // speakeasy makes it easy to implement HMAC one-time passwords, supporting both counter-based (HOTP) // and time-based moving factors (TOTP). It's useful for implementing two-factor authentication. // Google and Amazon use TOTP to generate codes for use with multi-factor authentication. @@ -54,7 +54,7 @@ speakeasy.hotp = function(options) { // init hmac with the key var hmac = crypto.createHmac('sha1', new Buffer(key)); - + // create an octet array from the counter var octet_array = new Array(8); @@ -85,7 +85,7 @@ speakeasy.hotp = function(options) { // compute HOTP // get offset var offset = digest_bytes[19] & 0xf; - + // calculate bin_code (RFC4226 5.4) var bin_code = (digest_bytes[offset] & 0x7f) << 24 |(digest_bytes[offset+1] & 0xff) << 16 @@ -97,7 +97,7 @@ speakeasy.hotp = function(options) { // get the chars at position bin_code - length through length chars var sub_start = bin_code.length - length; var code = bin_code.substr(sub_start, length); - + // we now have a code with `length` number of digits, so return it return(code); } @@ -113,7 +113,7 @@ speakeasy.hotp = function(options) { // .step(=30) override the step in seconds // .time (optional) override the time to calculate with // .initial_time (optional) override the initial time -// +// speakeasy.totp = function(options) { // set vars var key = options.key; @@ -121,10 +121,10 @@ speakeasy.totp = function(options) { var encoding = options.encoding || 'ascii'; var step = options.step || 30; var initial_time = options.initial_time || 0; // unix epoch by default - + // get current time in seconds since unix epoch var time = parseInt(Date.now()/1000); - + // are we forcing a specific time? if (options.time) { // override the time @@ -133,7 +133,7 @@ speakeasy.totp = function(options) { // calculate counter value var counter = Math.floor((time - initial_time)/ step); - + // pass to hotp var code = this.hotp({key: key, length: length, encoding: encoding, counter: counter}); @@ -167,7 +167,7 @@ speakeasy.hex_to_ascii = function(str) { // speakeasy.ascii_to_hex = function(str) { var hex_string = ''; - + for (var i = 0; i < str.length; i++) { hex_string += str.charCodeAt(i).toString(16); } @@ -192,7 +192,9 @@ speakeasy.ascii_to_hex = function(str) { // with the Google Authenticator app. // .name (optional) add a name. no spaces. // for use with Google Authenticator -// +// .type (optional) totp or hotp +// .counter (optional) default counter value +// .issuer (optional) default issuer speakeasy.generate_key = function(options) { // options var length = options.length || 32; @@ -200,6 +202,10 @@ speakeasy.generate_key = function(options) { var qr_codes = options.qr_codes || false; var google_auth_qr = options.google_auth_qr || false; var symbols = true; + var type = options.type || 'totp'; + var counter = options.counter || false; + var issuer = options.counter || false; + // turn off symbols only when explicity told to if (options.symbols !== undefined && options.symbols === false) { @@ -208,28 +214,33 @@ speakeasy.generate_key = function(options) { // generate an ascii key var key = this.generate_key_ascii(length, symbols); - + // return a SecretKey with ascii, hex, and base32 var SecretKey = {}; SecretKey.ascii = key; SecretKey.hex = this.ascii_to_hex(key); SecretKey.base32 = base32.encode(key).replace(/=/g,''); - + // generate some qr codes if requested if (qr_codes) { 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); } - + // generate a QR code for use in Google Authenticator if requested // (Google Authenticator has a special style and requires base32) if (google_auth_qr) { // first, make sure that the name doesn't have spaces, since Google Authenticator doesn't like them name = name.replace(/ /g,''); - SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://totp/' + encodeURIComponent(name) + '%3Fsecret=' + encodeURIComponent(SecretKey.base32); + SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://'+type+'/' + encodeURIComponent(name) + '%3Fsecret=' + encodeURIComponent(SecretKey.base32); + } + if(issuer){ + SecretKey.google_auth_qr += '&issuer='+encodeURIComponent(issuer); + } + if(counter){ + SecretKey.google_auth_qr = '&counter='+encodeURIComponent(counter); } - return SecretKey; } @@ -247,13 +258,13 @@ speakeasy.generate_key_ascii = function(length, symbols) { if (symbols) { set += '!@#$%^&*()<>?/[]{},.:;'; } - + var key = ''; for(var i=0; i < length; i++) { key += set.charAt(Math.floor(Math.random() * set.length)); } - + return key; } From 1b955f5dd983cd97ad6d2588a66258b0ab048f22 Mon Sep 17 00:00:00 2001 From: Vincent Lombard Date: Fri, 12 Sep 2014 15:53:33 +0200 Subject: [PATCH 03/59] correction erreur += = --- lib/speakeasy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/speakeasy.js b/lib/speakeasy.js index 8badca4..ea71306 100644 --- a/lib/speakeasy.js +++ b/lib/speakeasy.js @@ -204,7 +204,7 @@ speakeasy.generate_key = function(options) { var symbols = true; var type = options.type || 'totp'; var counter = options.counter || false; - var issuer = options.counter || false; + var issuer = options.issuer || false; // turn off symbols only when explicity told to @@ -239,7 +239,7 @@ speakeasy.generate_key = function(options) { SecretKey.google_auth_qr += '&issuer='+encodeURIComponent(issuer); } if(counter){ - SecretKey.google_auth_qr = '&counter='+encodeURIComponent(counter); + SecretKey.google_auth_qr += '&counter='+encodeURIComponent(counter); } return SecretKey; } From 4ce5721699b76b58391c3be289abbc09788e97da Mon Sep 17 00:00:00 2001 From: Mark Slosarek Date: Tue, 5 May 2015 15:22:20 -0500 Subject: [PATCH 04/59] - Added support for SHA256 and SHA512 - Added support for repetition of the key to the length of the hashing algorithm (20 bytes for SHA1, 32 bytes for SHA256, 64 bytes for SHA512) - Added tests for SHA256 and SHA512 test vectors as described in: https://tools.ietf.org/html/rfc6238#appendix-B --- lib/speakeasy.js | 28 ++++++++++++++++++++++------ test/test_hotp.js | 31 +++++++++++++++++++++++++++++++ test/test_totp.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/lib/speakeasy.js b/lib/speakeasy.js index 3e9693b..007236a 100644 --- a/lib/speakeasy.js +++ b/lib/speakeasy.js @@ -37,6 +37,7 @@ var speakeasy = {} // .counter moving factor // .length(=6) length of the one-time password (default 6) // .encoding(='ascii') key encoding (ascii, hex, or base32) +// .algorithm(='sha1') encytion algorithm (sha1, sha256) // speakeasy.hotp = function(options) { // set vars @@ -44,16 +45,29 @@ speakeasy.hotp = function(options) { var counter = options.counter; var length = options.length || 6; var encoding = options.encoding || 'ascii'; + var algorithm = options.algorithm || 'sha1'; + var hash_size = 160; // sha1 - // preprocessing: convert to ascii if it's not + if (algorithm === 'sha256') { + hash_size = 256; + } else if (algorithm === 'sha512') { + hash_size = 512; + } + + // preprocessing: convert to buffer if (encoding == 'hex') { - key = speakeasy.hex_to_ascii(key); + key = new Buffer(speakeasy.hex_to_ascii(key)); } else if (encoding == 'base32') { - key = base32.decode(key); + key = new Buffer(base32.decode(key)); + } else { + key = new Buffer(key); } + // repeat the key to the minumum length + key = new Buffer(Array(8).join(key.toString('hex')), 'hex').slice(0, hash_size/8); + // init hmac with the key - var hmac = crypto.createHmac('sha1', new Buffer(key)); + var hmac = crypto.createHmac(algorithm, key); // create an octet array from the counter var octet_array = new Array(8); @@ -84,7 +98,7 @@ speakeasy.hotp = function(options) { // compute HOTP // get offset - var offset = digest_bytes[19] & 0xf; + var offset = digest_bytes[(hash_size/8 - 1)] & 0xf; // calculate bin_code (RFC4226 5.4) var bin_code = (digest_bytes[offset] & 0x7f) << 24 @@ -113,6 +127,7 @@ speakeasy.hotp = function(options) { // .step(=30) override the step in seconds // .time (optional) override the time to calculate with // .initial_time (optional) override the initial time +// .algorithm (optional) override the default algorighm (default sha1) // speakeasy.totp = function(options) { // set vars @@ -121,6 +136,7 @@ speakeasy.totp = function(options) { var encoding = options.encoding || 'ascii'; var step = options.step || 30; var initial_time = options.initial_time || 0; // unix epoch by default + var algorithm = options.algorithm || 'sha1'; // get current time in seconds since unix epoch var time = parseInt(Date.now()/1000); @@ -135,7 +151,7 @@ speakeasy.totp = function(options) { var counter = Math.floor((time - initial_time)/ step); // pass to hotp - var code = this.hotp({key: key, length: length, encoding: encoding, counter: counter}); + var code = this.hotp({key: key, length: length, encoding: encoding, counter: counter, algorithm: algorithm}); // return the code return(code); diff --git a/test/test_hotp.js b/test/test_hotp.js index 7f2e39e..b503087 100644 --- a/test/test_hotp.js +++ b/test/test_hotp.js @@ -56,4 +56,35 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ assert.equal(topic, '338314'); } }, + + 'Test base32 encoding with key = \'1234567890\' at counter 3': { + topic: function() { + return speakeasy.hotp({key: '1234567890', counter: 3}); + }, + + 'correct one-time password returned': function(topic) { + assert.equal(topic, '969429'); + } + }, + + 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha256\'': { + topic: function() { + return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha256'}); + }, + + 'correct one-time password returned': function(topic) { + assert.equal(topic, '46119246'); + } + }, + + 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha512\'': { + topic: function() { + return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha512'}); + }, + + 'correct one-time password returned': function(topic) { + assert.equal(topic, '90693936'); + } + }, + }).exportTo(module); diff --git a/test/test_totp.js b/test/test_totp.js index ae0e53a..343fd85 100644 --- a/test/test_totp.js +++ b/test/test_totp.js @@ -77,4 +77,34 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ assert.equal(topic, '755224'); } }, + + 'Test base32 encoding with key = \'1234567890\' at time = 1111111109': { + topic: function() { + return speakeasy.totp({key: '1234567890', time: 1111111109}); + }, + + 'correct one-time password returned': function(topic) { + assert.equal(topic, '081804'); + } + }, + + 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109, length = 8 and algorithm as \'sha256\'': { + topic: function() { + return speakeasy.totp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109, length: 8, algorithm: 'sha256'}); + }, + + 'correct one-time password returned': function(topic) { + assert.equal(topic, '68084774'); + } + }, + + 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109, length = 8 and algorithm as \'sha512\'': { + topic: function() { + return speakeasy.totp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109, length: 8, algorithm: 'sha512'}); + }, + + 'correct one-time password returned': function(topic) { + assert.equal(topic, '25091201'); + } + }, }).exportTo(module); From db9d499477c2ddcdda45c815a89525ba73b71613 Mon Sep 17 00:00:00 2001 From: Mark Slosarek Date: Tue, 5 May 2015 15:26:11 -0500 Subject: [PATCH 05/59] Update README to include support for algorithms --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5e2b218..c98af32 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Written to follow [RFC 4226](http://tools.ietf.org/html/rfc4226). Calculated wit * `counter`: the counter position (moving factor). `C` in the algorithm. * `length` (default `6`): the length of the resulting one-time password. * `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. +* `algorithm` (default `sha1`): the hash algorithm to use. Can be `'sha1'`, `'sha256'`, or `'sha512'` #### Example @@ -92,6 +93,7 @@ Written to follow [RFC 6238](http://tools.ietf.org/html/rfc6238). Calculated wit * `initial_time` (default `0`): the starting time where we calculate the TOTP from. Usually, this is set to the UNIX epoch at 0. `T0` in the algorithm. * `length` (default `6`): the length of the resulting one-time password. * `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. +* `algorithm` (default `sha1`): the hash algorithm to use. Can be `'sha1'`, `'sha256'`, or `'sha512'` #### Example From cd855793ccd649984739bbe45d0ece28bce28677 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Thu, 11 Jun 2015 20:22:03 -0700 Subject: [PATCH 06/59] Use Buffer to convert hex to bytes --- lib/speakeasy.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/speakeasy.js b/lib/speakeasy.js index 44f3503..484875a 100644 --- a/lib/speakeasy.js +++ b/lib/speakeasy.js @@ -93,7 +93,7 @@ speakeasy.hotp = function(options) { var digest = hmac.digest('hex'); // convert the result to an array of bytes - var digest_bytes = speakeasy.hexToBytes(digest); + var digest_bytes = new Buffer(digest, "hex"); // compute HOTP // get offset @@ -163,7 +163,7 @@ speakeasy.totp = function(options) { speakeasy.hex_to_ascii = function(str) { // key is a string of hex // convert it to an array of bytes... - var bytes = speakeasy.hexToBytes(str); + var bytes = new Buffer(str, "hex"); // bytes is now an array of bytes with character codes // merge this down into a string @@ -275,14 +275,6 @@ speakeasy.generate_key_ascii = function(length, symbols) { return output; }; -speakeasy.hexToBytes = function (hex) { - var bytes = []; - for (var i = 0, l = hex.length; i < l; i += 2) { - bytes.push(parseInt(hex.slice(i, i + 2), 16)); - } - return bytes; -}; - // alias, not the TV show speakeasy.counter = speakeasy.hotp; speakeasy.time = speakeasy.totp; From 1ebb53eb8b8876f2f41855d7352321d686216149 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Thu, 11 Jun 2015 20:29:31 -0700 Subject: [PATCH 07/59] Use Buffer to handle ascii encoding --- lib/speakeasy.js | 40 +++------------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/lib/speakeasy.js b/lib/speakeasy.js index 484875a..882a358 100644 --- a/lib/speakeasy.js +++ b/lib/speakeasy.js @@ -55,7 +55,7 @@ speakeasy.hotp = function(options) { // preprocessing: convert to ascii if it's not if (encoding === 'hex') { - key = new Buffer(speakeasy.hex_to_ascii(key)); + key = new Buffer(key, 'hex'); } else if (encoding === 'base32') { key = new Buffer(base32.decode(key)); } else { @@ -93,7 +93,7 @@ speakeasy.hotp = function(options) { var digest = hmac.digest('hex'); // convert the result to an array of bytes - var digest_bytes = new Buffer(digest, "hex"); + var digest_bytes = new Buffer(digest, 'hex'); // compute HOTP // get offset @@ -156,40 +156,6 @@ speakeasy.totp = function(options) { return(code); }; -// speakeasy.hex_to_ascii(key) -// -// helper function to convert a hex key to ascii. -// -speakeasy.hex_to_ascii = function(str) { - // key is a string of hex - // convert it to an array of bytes... - var bytes = new Buffer(str, "hex"); - - // bytes is now an array of bytes with character codes - // merge this down into a string - var ascii_string = ''; - - for (var i = 0; i < bytes.length; i++) { - ascii_string += String.fromCharCode(bytes[i]); - } - - return ascii_string; -}; - -// speakeasy.ascii_to_hex(key) -// -// helper function to convert an ascii key to hex. -// -speakeasy.ascii_to_hex = function(str) { - var hex_string = ''; - - for (var i = 0; i < str.length; i++) { - hex_string += str.charCodeAt(i).toString(16); - } - - return hex_string; -}; - // speakeasy.generate_key(options) // // Generates a random key with the set A-Z a-z 0-9 and symbols, of any length @@ -233,7 +199,7 @@ speakeasy.generate_key = function(options) { // return a SecretKey with ascii, hex, and base32 var SecretKey = {}; SecretKey.ascii = key; - SecretKey.hex = this.ascii_to_hex(key); + SecretKey.hex = new Buffer(key, "ascii").toString("hex"); SecretKey.base32 = base32.encode(key).toString().replace(/=/g,''); // generate some qr codes if requested From cc7095fa3cfe30fd8e3786d6c2a9a9118be4333d Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Thu, 11 Jun 2015 20:34:36 -0700 Subject: [PATCH 08/59] Add license to package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 5dab306..b321d62 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Easy two-factor authentication with node.js. Time-based or counter-based (HOTP/TOTP), and supports the Google Authenticator mobile app. Also includes a key generator. Uses the HMAC One-Time Password algorithms.", "version": "1.0.3", "homepage": "http://github.com/markbao/speakeasy", + "license": "MIT", "repository": { "type": "git", "url": "git://github.com/markbao/speakeasy.git" From 4fe1294e326b08209bd5d5df5b2062c2170c7880 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Thu, 11 Jun 2015 20:44:17 -0700 Subject: [PATCH 09/59] Use own base32 encoder --- lib/speakeasy.js | 12 +++--------- package.json | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/lib/speakeasy.js b/lib/speakeasy.js index 882a358..166ca05 100644 --- a/lib/speakeasy.js +++ b/lib/speakeasy.js @@ -24,7 +24,7 @@ // with clear functions and parameter explanations. var crypto = require('crypto'); -var base32 = require('thirty-two'); +var base32 = require('base32.js'); var speakeasy = {}; @@ -182,17 +182,11 @@ speakeasy.generate_key = function(options) { var name = options.name || "Secret Key"; var qr_codes = options.qr_codes || false; var google_auth_qr = options.google_auth_qr || false; - var symbols = true; + var symbols = options.symbols !== false; var type = options.type || 'totp'; var counter = options.counter || false; var issuer = options.issuer || false; - - // turn off symbols only when explicity told to - if (options.symbols !== undefined && options.symbols === false) { - symbols = false; - } - // generate an ascii key var key = this.generate_key_ascii(length, symbols); @@ -200,7 +194,7 @@ speakeasy.generate_key = function(options) { var SecretKey = {}; SecretKey.ascii = key; SecretKey.hex = new Buffer(key, "ascii").toString("hex"); - SecretKey.base32 = base32.encode(key).toString().replace(/=/g,''); + SecretKey.base32 = base32.encode(key); // generate some qr codes if requested if (qr_codes) { diff --git a/package.json b/package.json index b321d62..908324c 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "node": ">= 0.3.0" }, "dependencies": { - "thirty-two": "0.0.2" + "base32.js": "0.0.1" }, "keywords": [ "two-factor", From 4f4314b6e7c5607064ab0c5df08723f524d6d3b0 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Thu, 11 Jun 2015 20:45:00 -0700 Subject: [PATCH 10/59] Move main file to index.js --- index.js | 244 +++++++++++++++++++++++++++++++++++++++++++++- lib/speakeasy.js | 242 --------------------------------------------- test/test_hotp.js | 18 ++-- test/test_totp.js | 22 ++--- 4 files changed, 262 insertions(+), 264 deletions(-) delete mode 100644 lib/speakeasy.js diff --git a/index.js b/index.js index 8854ed0..166ca05 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,242 @@ -// i has a cheezburger -module.exports = require('./lib/speakeasy'); +// # speakeasy +// ### HMAC One-Time Password module for Node.js, supporting counter-based and time-based moving factors +// +// speakeasy makes it easy to implement HMAC one-time passwords, supporting both counter-based (HOTP) +// and time-based moving factors (TOTP). It's useful for implementing two-factor authentication. +// Google and Amazon use TOTP to generate codes for use with multi-factor authentication. +// +// speakeasy also supports base32 keys/secrets, by passing `base32` in the `encoding` option. +// This is useful since Google Authenticator, Google's two-factor authentication mobile app +// available for iPhone, Android, and BlackBerry, uses base32 keys. +// +// This module was written to follow the RFC memos on HTOP and TOTP: +// +// * HOTP (HMAC-Based One-Time Password Algorithm): [RFC 4226](http://tools.ietf.org/html/rfc4226) +// * TOTP (Time-Based One-Time Password Algorithm): [RFC 6238](http://tools.ietf.org/html/rfc6238) +// +// One other useful function that this module has is a key generator, which allows you to +// generate keys, get them back in their ASCII, hexadecimal, and base32 representations. +// In addition, it also can automatically generate QR codes for you, as well as the specialized +// QR code you can use to scan in the Google Authenticator mobile app. +// +// An overarching goal of this module, other than to make it very easy to implement the +// HOTP and TOTP algorithms, is to be extensively documented. Indeed, it is well-documented, +// with clear functions and parameter explanations. + +var crypto = require('crypto'); +var base32 = require('base32.js'); + +var speakeasy = {}; + +// speakeasy.hotp(options) +// +// Calculates the one-time password given the key and a counter. +// +// options.key the key +// .counter moving factor +// .length(=6) length of the one-time password (default 6) +// .encoding(='ascii') key encoding (ascii, hex, or base32) +// .algorithm(='sha1') encytion algorithm (sha1, sha256) +// +speakeasy.hotp = function(options) { + // set vars + var key = options.key; + var counter = options.counter; + var length = options.length || 6; + var encoding = options.encoding || 'ascii'; + var algorithm = options.algorithm || 'sha1'; + var hash_size = 160; // sha1 + + if (algorithm === 'sha256') { + hash_size = 256; + } else if (algorithm === 'sha512') { + hash_size = 512; + } + + // preprocessing: convert to ascii if it's not + if (encoding === 'hex') { + key = new Buffer(key, 'hex'); + } else if (encoding === 'base32') { + key = new Buffer(base32.decode(key)); + } else { + key = new Buffer(key); + } + + // repeat the key to the minumum length + key = new Buffer(Array(8).join(key.toString('hex')), 'hex').slice(0, hash_size/8); + + // init hmac with the key + var hmac = crypto.createHmac(algorithm, key); + + // create an octet array from the counter + var octet_array = new Array(8); + + var counter_temp = counter; + + for (var i = 0; i < 8; i++) { + var i_from_right = 7 - i; + + // mask 255 over number to get last 8 + octet_array[i_from_right] = counter_temp & 255; + + // shift 8 and get ready to loop over the next batch of 8 + counter_temp = counter_temp >> 8; + } + + // create a buffer from the octet array + var counter_buffer = new Buffer(octet_array); + + // update hmac with the counter + hmac.update(counter_buffer); + + // get the digest in hex format + var digest = hmac.digest('hex'); + + // convert the result to an array of bytes + var digest_bytes = new Buffer(digest, 'hex'); + + // compute HOTP + // get offset + var offset = digest_bytes[(hash_size/8 - 1)] & 0xf; + + // calculate bin_code (RFC4226 5.4) + var bin_code = (digest_bytes[offset] & 0x7f) << 24 + |(digest_bytes[offset+1] & 0xff) << 16 + |(digest_bytes[offset+2] & 0xff) << 8 + |(digest_bytes[offset+3] & 0xff); + + bin_code = bin_code.toString(); + + // get the chars at position bin_code - length through length chars + var sub_start = bin_code.length - length; + var code = bin_code.substr(sub_start, length); + + // we now have a code with `length` number of digits, so return it + return(code); +}; + +// speakeasy.totp(options) +// +// Calculates the one-time password given the key, based on the current time +// with a 30 second step (step being the number of seconds between passwords). +// +// options.key the key +// .length(=6) length of the one-time password (default 6) +// .encoding(='ascii') key encoding (ascii, hex, or base32) +// .step(=30) override the step in seconds +// .time (optional) override the time to calculate with +// .initial_time (optional) override the initial time +// .algorithm (optional) override the default algorighm (default sha1) +// +speakeasy.totp = function(options) { + // set vars + var key = options.key; + var length = options.length || 6; + var encoding = options.encoding || 'ascii'; + var step = options.step || 30; + var initial_time = options.initial_time || 0; // unix epoch by default + var algorithm = options.algorithm || 'sha1'; + + // get current time in seconds since unix epoch + var time = parseInt(Date.now()/1000); + + // are we forcing a specific time? + if (options.time) { + // override the time + time = options.time; + } + + // calculate counter value + var counter = Math.floor((time - initial_time)/ step); + + // pass to hotp + var code = this.hotp({key: key, length: length, encoding: encoding, counter: counter, algorithm: algorithm}); + + // return the code + return(code); +}; + +// speakeasy.generate_key(options) +// +// Generates a random key with the set A-Z a-z 0-9 and symbols, of any length +// (default 32). Returns the key in ASCII, hexadecimal, and base32 format. +// Base32 format is used in Google Authenticator. Turn off symbols by setting +// symbols: false. Automatically generate links to QR codes of each encoding +// (using the Google Charts API) by setting qr_codes: true. Automatically +// generate a link to a special QR code for use with the Google Authenticator +// app, for which you can also specify a name. +// +// options.length(=32) length of key +// .symbols(=true) include symbols in the key +// .qr_codes(=false) generate links to QR codes +// .google_auth_qr(=false) generate a link to a QR code to scan +// with the Google Authenticator app. +// .name (optional) add a name. no spaces. +// for use with Google Authenticator +// .type (optional) totp or hotp +// .counter (optional) default counter value +// .issuer (optional) default issuer +speakeasy.generate_key = function(options) { + // options + var length = options.length || 32; + var name = options.name || "Secret Key"; + var qr_codes = options.qr_codes || false; + var google_auth_qr = options.google_auth_qr || false; + var symbols = options.symbols !== false; + var type = options.type || 'totp'; + var counter = options.counter || false; + var issuer = options.issuer || false; + + // generate an ascii key + var key = this.generate_key_ascii(length, symbols); + + // return a SecretKey with ascii, hex, and base32 + var SecretKey = {}; + SecretKey.ascii = key; + SecretKey.hex = new Buffer(key, "ascii").toString("hex"); + SecretKey.base32 = base32.encode(key); + + // generate some qr codes if requested + if (qr_codes) { + 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); + } + + // generate a QR code for use in Google Authenticator if requested + // (Google Authenticator has a special style and requires base32) + if (google_auth_qr) { + var otpauthURL = 'otpauth://totp/' + encodeURIComponent( name ) + '?secret=' + encodeURIComponent( SecretKey.base32 ); + if (issuer) otpauthURL += '&issuer=' + encodeURIComponent(issuer); + if (counter) otpauthURL += '&counter=' + encodeURIComponent(counter); + SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent( otpauthURL ); + } + + return SecretKey; +}; + +// speakeasy.generate_key_ascii(length, symbols) +// +// Generates a random key, of length `length` (default 32). +// Also choose whether you want symbols, default false. +// speakeasy.generate_key() wraps around this. +// +speakeasy.generate_key_ascii = function(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[~~(bytes[i] / 0xFF * set.length)]; + } + return output; +}; + +// alias, not the TV show +speakeasy.counter = speakeasy.hotp; +speakeasy.time = speakeasy.totp; + +module.exports = speakeasy; diff --git a/lib/speakeasy.js b/lib/speakeasy.js deleted file mode 100644 index 166ca05..0000000 --- a/lib/speakeasy.js +++ /dev/null @@ -1,242 +0,0 @@ -// # speakeasy -// ### HMAC One-Time Password module for Node.js, supporting counter-based and time-based moving factors -// -// speakeasy makes it easy to implement HMAC one-time passwords, supporting both counter-based (HOTP) -// and time-based moving factors (TOTP). It's useful for implementing two-factor authentication. -// Google and Amazon use TOTP to generate codes for use with multi-factor authentication. -// -// speakeasy also supports base32 keys/secrets, by passing `base32` in the `encoding` option. -// This is useful since Google Authenticator, Google's two-factor authentication mobile app -// available for iPhone, Android, and BlackBerry, uses base32 keys. -// -// This module was written to follow the RFC memos on HTOP and TOTP: -// -// * HOTP (HMAC-Based One-Time Password Algorithm): [RFC 4226](http://tools.ietf.org/html/rfc4226) -// * TOTP (Time-Based One-Time Password Algorithm): [RFC 6238](http://tools.ietf.org/html/rfc6238) -// -// One other useful function that this module has is a key generator, which allows you to -// generate keys, get them back in their ASCII, hexadecimal, and base32 representations. -// In addition, it also can automatically generate QR codes for you, as well as the specialized -// QR code you can use to scan in the Google Authenticator mobile app. -// -// An overarching goal of this module, other than to make it very easy to implement the -// HOTP and TOTP algorithms, is to be extensively documented. Indeed, it is well-documented, -// with clear functions and parameter explanations. - -var crypto = require('crypto'); -var base32 = require('base32.js'); - -var speakeasy = {}; - -// speakeasy.hotp(options) -// -// Calculates the one-time password given the key and a counter. -// -// options.key the key -// .counter moving factor -// .length(=6) length of the one-time password (default 6) -// .encoding(='ascii') key encoding (ascii, hex, or base32) -// .algorithm(='sha1') encytion algorithm (sha1, sha256) -// -speakeasy.hotp = function(options) { - // set vars - var key = options.key; - var counter = options.counter; - var length = options.length || 6; - var encoding = options.encoding || 'ascii'; - var algorithm = options.algorithm || 'sha1'; - var hash_size = 160; // sha1 - - if (algorithm === 'sha256') { - hash_size = 256; - } else if (algorithm === 'sha512') { - hash_size = 512; - } - - // preprocessing: convert to ascii if it's not - if (encoding === 'hex') { - key = new Buffer(key, 'hex'); - } else if (encoding === 'base32') { - key = new Buffer(base32.decode(key)); - } else { - key = new Buffer(key); - } - - // repeat the key to the minumum length - key = new Buffer(Array(8).join(key.toString('hex')), 'hex').slice(0, hash_size/8); - - // init hmac with the key - var hmac = crypto.createHmac(algorithm, key); - - // create an octet array from the counter - var octet_array = new Array(8); - - var counter_temp = counter; - - for (var i = 0; i < 8; i++) { - var i_from_right = 7 - i; - - // mask 255 over number to get last 8 - octet_array[i_from_right] = counter_temp & 255; - - // shift 8 and get ready to loop over the next batch of 8 - counter_temp = counter_temp >> 8; - } - - // create a buffer from the octet array - var counter_buffer = new Buffer(octet_array); - - // update hmac with the counter - hmac.update(counter_buffer); - - // get the digest in hex format - var digest = hmac.digest('hex'); - - // convert the result to an array of bytes - var digest_bytes = new Buffer(digest, 'hex'); - - // compute HOTP - // get offset - var offset = digest_bytes[(hash_size/8 - 1)] & 0xf; - - // calculate bin_code (RFC4226 5.4) - var bin_code = (digest_bytes[offset] & 0x7f) << 24 - |(digest_bytes[offset+1] & 0xff) << 16 - |(digest_bytes[offset+2] & 0xff) << 8 - |(digest_bytes[offset+3] & 0xff); - - bin_code = bin_code.toString(); - - // get the chars at position bin_code - length through length chars - var sub_start = bin_code.length - length; - var code = bin_code.substr(sub_start, length); - - // we now have a code with `length` number of digits, so return it - return(code); -}; - -// speakeasy.totp(options) -// -// Calculates the one-time password given the key, based on the current time -// with a 30 second step (step being the number of seconds between passwords). -// -// options.key the key -// .length(=6) length of the one-time password (default 6) -// .encoding(='ascii') key encoding (ascii, hex, or base32) -// .step(=30) override the step in seconds -// .time (optional) override the time to calculate with -// .initial_time (optional) override the initial time -// .algorithm (optional) override the default algorighm (default sha1) -// -speakeasy.totp = function(options) { - // set vars - var key = options.key; - var length = options.length || 6; - var encoding = options.encoding || 'ascii'; - var step = options.step || 30; - var initial_time = options.initial_time || 0; // unix epoch by default - var algorithm = options.algorithm || 'sha1'; - - // get current time in seconds since unix epoch - var time = parseInt(Date.now()/1000); - - // are we forcing a specific time? - if (options.time) { - // override the time - time = options.time; - } - - // calculate counter value - var counter = Math.floor((time - initial_time)/ step); - - // pass to hotp - var code = this.hotp({key: key, length: length, encoding: encoding, counter: counter, algorithm: algorithm}); - - // return the code - return(code); -}; - -// speakeasy.generate_key(options) -// -// Generates a random key with the set A-Z a-z 0-9 and symbols, of any length -// (default 32). Returns the key in ASCII, hexadecimal, and base32 format. -// Base32 format is used in Google Authenticator. Turn off symbols by setting -// symbols: false. Automatically generate links to QR codes of each encoding -// (using the Google Charts API) by setting qr_codes: true. Automatically -// generate a link to a special QR code for use with the Google Authenticator -// app, for which you can also specify a name. -// -// options.length(=32) length of key -// .symbols(=true) include symbols in the key -// .qr_codes(=false) generate links to QR codes -// .google_auth_qr(=false) generate a link to a QR code to scan -// with the Google Authenticator app. -// .name (optional) add a name. no spaces. -// for use with Google Authenticator -// .type (optional) totp or hotp -// .counter (optional) default counter value -// .issuer (optional) default issuer -speakeasy.generate_key = function(options) { - // options - var length = options.length || 32; - var name = options.name || "Secret Key"; - var qr_codes = options.qr_codes || false; - var google_auth_qr = options.google_auth_qr || false; - var symbols = options.symbols !== false; - var type = options.type || 'totp'; - var counter = options.counter || false; - var issuer = options.issuer || false; - - // generate an ascii key - var key = this.generate_key_ascii(length, symbols); - - // return a SecretKey with ascii, hex, and base32 - var SecretKey = {}; - SecretKey.ascii = key; - SecretKey.hex = new Buffer(key, "ascii").toString("hex"); - SecretKey.base32 = base32.encode(key); - - // generate some qr codes if requested - if (qr_codes) { - 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); - } - - // generate a QR code for use in Google Authenticator if requested - // (Google Authenticator has a special style and requires base32) - if (google_auth_qr) { - var otpauthURL = 'otpauth://totp/' + encodeURIComponent( name ) + '?secret=' + encodeURIComponent( SecretKey.base32 ); - if (issuer) otpauthURL += '&issuer=' + encodeURIComponent(issuer); - if (counter) otpauthURL += '&counter=' + encodeURIComponent(counter); - SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent( otpauthURL ); - } - - return SecretKey; -}; - -// speakeasy.generate_key_ascii(length, symbols) -// -// Generates a random key, of length `length` (default 32). -// Also choose whether you want symbols, default false. -// speakeasy.generate_key() wraps around this. -// -speakeasy.generate_key_ascii = function(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[~~(bytes[i] / 0xFF * set.length)]; - } - return output; -}; - -// alias, not the TV show -speakeasy.counter = speakeasy.hotp; -speakeasy.time = speakeasy.totp; - -module.exports = speakeasy; diff --git a/test/test_hotp.js b/test/test_hotp.js index b503087..1bda5e3 100644 --- a/test/test_hotp.js +++ b/test/test_hotp.js @@ -1,7 +1,7 @@ var vows = require('vows'), assert = require('assert'); -var speakeasy = require('../lib/speakeasy'); +var speakeasy = require('..'); // These tests use the information from RFC 4226's Appendix D: Test Values. // http://tools.ietf.org/html/rfc4226#appendix-D @@ -11,7 +11,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: '12345678901234567890', counter: 3}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '969429'); } @@ -21,7 +21,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: '12345678901234567890', counter: 7}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '162583'); } @@ -31,7 +31,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: '12345678901234567890', counter: 4, length: 8}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '40338314'); } @@ -41,7 +41,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: '3132333435363738393031323334353637383930', encoding: 'hex', counter: 4}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '338314'); } @@ -51,7 +51,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 4}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '338314'); } @@ -61,7 +61,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: '1234567890', counter: 3}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '969429'); } @@ -71,7 +71,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha256'}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '46119246'); } @@ -81,7 +81,7 @@ vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha512'}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '90693936'); } diff --git a/test/test_totp.js b/test/test_totp.js index 343fd85..8ba0efd 100644 --- a/test/test_totp.js +++ b/test/test_totp.js @@ -1,7 +1,7 @@ var vows = require('vows'), assert = require('assert'); -var speakeasy = require('../lib/speakeasy'); +var speakeasy = require('..'); // These tests use the test vectors from RFC 6238's Appendix B: Test Vectors // http://tools.ietf.org/html/rfc6238#appendix-B @@ -12,7 +12,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: '12345678901234567890', time: 59}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '287082'); } @@ -22,7 +22,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: '12345678901234567890', time: 1111111109}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '081804'); } @@ -32,7 +32,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: '12345678901234567890', time: 1111111109, length: 8}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '07081804'); } @@ -42,7 +42,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '081804'); } @@ -52,7 +52,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '081804'); } @@ -62,7 +62,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: '12345678901234567890', time: 1111111109, step: 60}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '360094'); } @@ -72,7 +72,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: '12345678901234567890', time: 1111111109, initial_time: 1111111100}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '755224'); } @@ -82,7 +82,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: '1234567890', time: 1111111109}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '081804'); } @@ -92,7 +92,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109, length: 8, algorithm: 'sha256'}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '68084774'); } @@ -102,7 +102,7 @@ vows.describe('TOTP Time-Based Algorithm Test').addBatch({ topic: function() { return speakeasy.totp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109, length: 8, algorithm: 'sha512'}); }, - + 'correct one-time password returned': function(topic) { assert.equal(topic, '25091201'); } From 349ddb7c5eaa80c53d9eb424854ef81990511942 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Thu, 11 Jun 2015 21:27:10 -0700 Subject: [PATCH 11/59] Graft notp verify functions onto library --- index.js | 96 ++++++++++++++++++-- package.json | 4 +- test/test_hotp.js | 111 ++++++++++------------- test/test_notp.js | 219 ++++++++++++++++++++++++++++++++++++++++++++++ test/test_totp.js | 138 ++++++++++++----------------- 5 files changed, 409 insertions(+), 159 deletions(-) create mode 100644 test/test_notp.js diff --git a/index.js b/index.js index 166ca05..723ed40 100644 --- a/index.js +++ b/index.js @@ -115,6 +115,55 @@ speakeasy.hotp = function(options) { return(code); }; +/** + * 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: + * + * options + * 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. + * + */ +speakeasy.hotp.verify = function(options) { + + var token = options.token; + var window = options.window || 50; + var counter = options.counter || 0; + + // Now loop through from C to C + W to determine if there is + // a correct code + for (var i = counter; i <= counter + window; ++i) { + options.counter = i; + if (speakeasy.hotp(options) === token) { + // We have found a matching code, trigger callback + // and pass offset + return {delta: i - counter}; + } + } + + // If we get to here then no codes have matched. +}; + // speakeasy.totp(options) // // Calculates the one-time password given the key, based on the current time @@ -138,13 +187,7 @@ speakeasy.totp = function(options) { var algorithm = options.algorithm || 'sha1'; // get current time in seconds since unix epoch - var time = parseInt(Date.now()/1000); - - // are we forcing a specific time? - if (options.time) { - // override the time - time = options.time; - } + var time = options.time || parseInt(Date.now()/1000); // calculate counter value var counter = Math.floor((time - initial_time)/ step); @@ -156,6 +199,45 @@ speakeasy.totp = function(options) { return(code); }; +/** + * 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: + * + * options + * 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 + * + */ +speakeasy.totp.verify = function(options) { + var step = options.step || 30; + var time = options.time || parseInt(Date.now()/1000); + var initial_time = options.initial_time || 0; + options.counter = Math.floor((time - initial_time)/ step); + return speakeasy.hotp.verify(options); +}; + // speakeasy.generate_key(options) // // Generates a random key with the set A-Z a-z 0-9 and symbols, of any length diff --git a/package.json b/package.json index 908324c..52b10f5 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "passwords" ], "devDependencies": { - "vows": "*" + "mocha": "^2.2.5" }, "scripts": { - "test": "vows --spec test/*" + "test": "mocha" } } diff --git a/test/test_hotp.js b/test/test_hotp.js index 1bda5e3..4903f71 100644 --- a/test/test_hotp.js +++ b/test/test_hotp.js @@ -1,90 +1,67 @@ -var vows = require('vows'), - assert = require('assert'); +"use scrict"; +var assert = require('assert'); var speakeasy = require('..'); // These tests use the information from RFC 4226's Appendix D: Test Values. // http://tools.ietf.org/html/rfc4226#appendix-D -vows.describe('HOTP Counter-Based Algorithm Test').addBatch({ - 'Test normal operation with key = \'12345678901234567890\' at counter 3': { - topic: function() { - return speakeasy.hotp({key: '12345678901234567890', counter: 3}); - }, +describe('HOTP Counter-Based Algorithm Test', function () { - 'correct one-time password returned': function(topic) { + describe('normal operation with key = \'12345678901234567890\' at counter 3', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: '12345678901234567890', counter: 3}); assert.equal(topic, '969429'); - } - }, + }); + }); - 'Test another counter normal operation with key = \'12345678901234567890\' at counter 7': { - topic: function() { - return speakeasy.hotp({key: '12345678901234567890', counter: 7}); - }, - - 'correct one-time password returned': function(topic) { + describe('another counter normal operation with key = \'12345678901234567890\' at counter 7', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: '12345678901234567890', counter: 7}); assert.equal(topic, '162583'); - } - }, - - 'Test length override with key = \'12345678901234567890\' at counter 4 and length = 8': { - topic: function() { - return speakeasy.hotp({key: '12345678901234567890', counter: 4, length: 8}); - }, + }); + }); - 'correct one-time password returned': function(topic) { + describe('length override with key = \'12345678901234567890\' at counter 4 and length = 8', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: '12345678901234567890', counter: 4, length: 8}); assert.equal(topic, '40338314'); - } - }, + }); + }); - 'Test hexadecimal encoding with key = \'3132333435363738393031323334353637383930\' as hexadecimal at counter 4': { - topic: function() { - return speakeasy.hotp({key: '3132333435363738393031323334353637383930', encoding: 'hex', counter: 4}); - }, - - 'correct one-time password returned': function(topic) { + describe('hexadecimal encoding with key = \'3132333435363738393031323334353637383930\' as hexadecimal at counter 4', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: '3132333435363738393031323334353637383930', encoding: 'hex', counter: 4}); assert.equal(topic, '338314'); - } - }, - - 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at counter 4': { - topic: function() { - return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 4}); - }, + }); + }); - 'correct one-time password returned': function(topic) { + describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at counter 4', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 4}); assert.equal(topic, '338314'); - } - }, + }); + }); - 'Test base32 encoding with key = \'1234567890\' at counter 3': { - topic: function() { - return speakeasy.hotp({key: '1234567890', counter: 3}); - }, - - 'correct one-time password returned': function(topic) { + describe('base32 encoding with key = \'1234567890\' at counter 3', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: '1234567890', counter: 3}); assert.equal(topic, '969429'); - } - }, - - 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha256\'': { - topic: function() { - return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha256'}); - }, + }); + }); - 'correct one-time password returned': function(topic) { + describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha256\'', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha256'}); assert.equal(topic, '46119246'); - } - }, - - 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha512\'': { - topic: function() { - return speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha512'}); - }, + }); + }); - 'correct one-time password returned': function(topic) { + describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha512\'', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha512'}); assert.equal(topic, '90693936'); - } - }, + }); + }); -}).exportTo(module); +}); diff --git a/test/test_notp.js b/test/test_notp.js new file mode 100644 index 0000000..77b0c18 --- /dev/null +++ b/test/test_notp.js @@ -0,0 +1,219 @@ +"use scrict"; + +var assert = require('assert'); +var speakeasy = require('..'); + +/* + * Test HOTtoken. Uses test values from RFcounter 4226 + * + * + * The following test data uses the AScounterII string + * "12345678901234567890" for the secret: + * + * Secret = 0x3132333435363738393031323334353637383930 + * + * Table 1 details for each count, the intermediate HMAcounter value. + * + * counterount Hexadecimal HMAcounter-SHA-1(secret, count) + * 0 cc93cf18508d94934c64b65d8ba7667fb7cde4b0 + * 1 75a48a19d4cbe100644e8ac1397eea747a2d33ab + * 2 0bacb7fa082fef30782211938bc1c5e70416ff44 + * 3 66c28227d03a2d5529262ff016a1e6ef76557ece + * 4 a904c900a64b35909874b33e61c5938a8e15ed1c + * 5 a37e783d7b7233c083d4f62926c7a25f238d0316 + * 6 bc9cd28561042c83f219324d3c607256c03272ae + * 7 a4fb960c0bc06e1eabb804e5b397cdc4b45596fa + * 8 1b3c89f65e6c9e883012052823443f048b4332db + * 9 1637409809a679dc698207310c8c7fc07290d9e5 + * + * Table 2 details for each count the truncated values (both in + * hexadecimal and decimal) and then the HOTtoken value. + * + * Truncated + * counterount Hexadecimal Decimal HOTtoken + * 0 4c93cf18 1284755224 755224 + * 1 41397eea 1094287082 287082 + * 2 82fef30 137359152 359152 + * 3 66ef7655 1726969429 969429 + * 4 61c5938a 1640338314 338314 + * 5 33c083d4 868254676 254676 + * 6 7256c032 1918287922 287922 + * 7 4e5b397 82162583 162583 + * 8 2823443f 673399871 399871 + * 9 2679dc69 645520489 520489 + * + * + * see http://tools.ietf.org/html/rfc4226 + */ + +it("HOTP", function() { + var options = { + key: '12345678901234567890', + window: 0 + }; + var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']; + + // make sure we can not pass in opt + options.token = 'WILL NOT PASS'; + speakeasy.hotp.verify(options); + + // counterheck for failure + options.counter = 0; + assert.ok(!speakeasy.hotp.verify(options), 'Should not pass'); + + // counterheck for passes + for(i=0;i= 9 + options.window = 8; + assert.ok(speakeasy.hotp.verify(options), 'Should pass for value of window >= 9'); + + // counterheck that test should pass for negative counter values + token = '755224'; + options.counter = 7 + options.window = 8; + assert.ok(speakeasy.hotp.verify(options), 'Should pass for negative counter values'); +}); + + +/* + * counterheck for codes that are out of sync + * windowe are going to use a value of T = 1999999909 (91s behind 2000000000) + */ + +it("TOTPOutOfSync", function() { + + var options = { + key: '12345678901234567890', + token: '279037', + time: 1999999909 + }; + + // counterheck that the test should fail for window < 2 + options.window = 2; + assert.ok(!speakeasy.totp.verify(options), 'Should not pass for value of window < 3'); + + // counterheck that the test should pass for window >= 3 + options.window = 3; + assert.ok(speakeasy.totp.verify(options), 'Should pass for value of window >= 3'); +}); + + +it("hotp_gen", function() { + var options = { + key: '12345678901234567890', + window: 0 + }; + + var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']; + + // make sure we can not pass in opt + speakeasy.hotp(options); + + // counterheck for passes + for(i=0;i Date: Thu, 11 Jun 2015 21:29:17 -0700 Subject: [PATCH 12/59] Add copyrights to license --- LICENSE | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index f3afb8f..0d5612d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ The MIT License (MIT) -Copyright (c) 2012-2013 Mark Bao +Copyright (c) 2015 Michael Phan-Ba +Copyright (c) 2012-2013 Mark Bao +Copyright (c) 2011 Guy Halford-Thompson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 508332e8222e13745cfe3747130ec090b3122f83 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Fri, 12 Jun 2015 18:26:08 -0700 Subject: [PATCH 13/59] Revive as Passcode --- .gitignore | 7 +- README.md | 181 ++------- index.js | 576 +++++++++++++++------------- jsdoc.json | 25 ++ package.json | 26 +- test/hotp_test.js | 67 ++++ test/{test_notp.js => notp_test.js} | 30 +- test/test_hotp.js | 67 ---- test/test_totp.js | 82 ---- test/totp_test.js | 82 ++++ test/url_test.js | 176 +++++++++ 11 files changed, 737 insertions(+), 582 deletions(-) create mode 100644 jsdoc.json create mode 100644 test/hotp_test.js rename test/{test_notp.js => notp_test.js} (92%) delete mode 100644 test/test_hotp.js delete mode 100644 test/test_totp.js create mode 100644 test/totp_test.js create mode 100644 test/url_test.js diff --git a/.gitignore b/.gitignore index 8d87b1d..465a37a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -node_modules/* +# npm +node_modules +npm-debug.log + +# Mac OS X +.DS_Store diff --git a/README.md b/README.md index c98af32..2ab1b37 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,51 @@ -# speakeasy +# Passcode -## Easy two-factor authentication for node.js. Calculate time-based or counter-based one-time passwords. Supports the Google Authenticator mobile app. +Passcode implements one-time passcode generators as standardized by the +[Initiative for Open Authentication (OATH)][oath]. The HMAC-Based One-time +Password (HOTP) algorithm defined by [RFC 4226][rfc4226] and the Time-based +One-time Password (TOTP) algorithm defined in [RFC 6238][rfc6238] are +supported. -Uses the HMAC One-Time Password algorithms, supporting counter-based and time-based moving factors (HOTP and TOTP). - -## An Introduction - -speakeasy makes it easy to implement HMAC one-time passwords (for example, for use in two-factor authentication), supporting both counter-based (HOTP) and time-based moving factors (TOTP). It's useful for implementing two-factor authentication. Google and Amazon use TOTP to generate codes for use with multi-factor authentication. - -It supports the counter-based and time-based algorithms, as well as keys encoded in ASCII, hexadecimal, and base32. It also has a random key generator which can also generate QR code links. - -This module was written to follow the RFC memos on HOTP and TOTP: - -* HOTP (HMAC-Based One-Time Password Algorithm): [RFC 4226](http:tools.ietf.org/html/rfc4226) -* TOTP (Time-Based One-Time Password Algorithm): [RFC 6238](http:tools.ietf.org/html/rfc6238) - -speakeasy's key generator allows you to generate keys, and get them back in their ASCII, hexadecimal, and base32 representations. In addition, it also can automatically generate QR codes for you. - -A useful integration is that it fully supports the popular Google Authenticator app, the virtual multi-factor authentication app available for iPhone and iOS, Android, and BlackBerry. This module's key generator can also generate a link to the specialized QR code you can use to scan in the Google Authenticator mobile app. - -An overarching goal of this module, other than to make it very easy to implement the HOTP and TOTP algorithms, is to be extensively documented. Indeed, it is well-documented, with clear functions and parameter explanations. +Passcode is a heavily modified version of [speakeasy][] incorporating the +verification functions of [notp][]. ## Install +```sh +npm install passcode ``` -npm install speakeasy -``` - -## Example (with Google Authenticator) - -```javascript -// generate a key and get a QR code you can scan with the Google Authenticator app -speakeasy.generate_key({length: 20, google_auth_qr: true}); -// => { ascii: 'V?9f6.Cq1& try this in your REPL and it should match the number on your phone -``` - -## Manual - -### speakeasy.hotp(options) | speakeasy.counter(options) - -Calculate the one-time password using the counter-based algorithm, HOTP. Specify the key and counter, and receive the one-time password for that counter position. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. - -Written to follow [RFC 4226](http://tools.ietf.org/html/rfc4226). Calculated with: `HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` - -#### Options - -* `key`: the secret key in ASCII, hexadecimal, or base32 format. `K` in the algorithm. -* `counter`: the counter position (moving factor). `C` in the algorithm. -* `length` (default `6`): the length of the resulting one-time password. -* `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. -* `algorithm` (default `sha1`): the hash algorithm to use. Can be `'sha1'`, `'sha256'`, or `'sha512'` - -#### Example - -```javascript -// normal use. -speakeasy.hotp({key: 'secret', counter: 582}); -// => 246642 - -// use a custom length. -speakeasy.hotp({key: 'secret', counter: 582, length: 8}); -// => 67246642 - -// use a custom encoding. -speakeasy.hotp({key: 'AJFIEJGEHIFIU7148SF', counter: 147, encoding: 'base32'}); -// => 974955 -``` - -### speakeasy.totp(options) | speakeasy.time(options) - -Calculate the one-time password using the time-based algorithm, TOTP. Specify the key, and receive the one-time password for that time. By default, the time step is 30 seconds, so there is a new password every 30 seconds. However, you may override the time step. You may also override the time you want to calculate the time from. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. - -Written to follow [RFC 6238](http://tools.ietf.org/html/rfc6238). Calculated with: `C = ((T - T0) / X); HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` - -#### Options - -* `key`: the secret key in ASCII, hexadecimal, or base32 format. `K` in the algorithm. -* `step` (default `30`): the time step, in seconds, between new passwords (moving factor). `X` in the algorithm. -* `time` (default current time): the time to calculate the TOTP from, by default the current time. If you're doing something clever with TOTP, you may override this (see *Techniques* below). `T` in the algorithm. -* `initial_time` (default `0`): the starting time where we calculate the TOTP from. Usually, this is set to the UNIX epoch at 0. `T0` in the algorithm. -* `length` (default `6`): the length of the resulting one-time password. -* `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. -* `algorithm` (default `sha1`): the hash algorithm to use. Can be `'sha1'`, `'sha256'`, or `'sha512'` - -#### Example - -```javascript -// normal use. -speakeasy.totp({key: 'secret'}); - -// use a custom time step. -speakeasy.totp({key: 'secret', step: 60}); - -// use a custom time. -speakeasy.totp({key: 'secret', time: 159183717}); -// => 558014 - -// use a initial time. -speakeasy.totp({key: 'secret', initial_time: 4182881485}); -// => 670417 -``` - -#### Techniques - -You can implement a double-authentication scheme, where you ask the user to input the one-time password once, wait until the next 30-second refresh, and then input the one-time password again. In this case, you can calculate the second (later) input by calculating TOTP as usual, then also verify the first (earlier) input by taking the current epoch time in seconds and subtracting 30 seconds to get to the previous step (for example: `time1 = (parseInt(new Date()/1000) - 30)`) - -### speakeasy.generate_key(options) - -Generate a random secret key. It will return the key in ASCII, hexadecimal, and base32 formats. You can specify the length, whether or not to use symbols, and ask it (nicely) to generate URLs for QR codes. Returns an object with the ASCII, hex, and base32 representations of the secret key, plus any QR codes you can optionally ask for. - -#### Options - -* `length` (default `32`): the length of the generated secret key. -* `symbols` (default `true`): include symbols in the key? if not, the key will be alphanumeric, {A-Z, a-z, 0-9} -* `qr_codes` (default `false`): generate links to QR codes for each encoding (ASCII, hexadecimal, and base32). It uses the Google Charts API and they are served over HTTPS. A future version might allow for QR code generation client-side for security. -* `google_auth_qr` (default `false`): generate a link to a QR code that you can scan using the Google Authenticator app. The contents of the QR code are in this format: `otpauth://totp/[KEY NAME]?secret=[KEY SECRET, BASE 32]`. -* `name` (optional): specify a name when you are using `google_auth_qr`, which will show up as the label after scanning. `[KEY NAME]` in the previous line. - -#### Examples -```javascript -// generate a key -speakeasy.generate_key({length: 20, symbols: true}); -// => { ascii: 'km^A?n&sOPJW.iCKPHKU', hex: '6b6d5e413f6e26734f504a572e69434b50484b55', base32: 'NNWV4QJ7NYTHGT2QJJLS42KDJNIEQS2V' } +## Usage -// generate a key and request QR code links -speakeasy.generate_key({length: 20, qr_codes: true}); -// => { ascii: 'eV:JQ1NedJkKn&]6^i>s', ... (truncated) -// qr_code_ascii: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=eV%3AJQ1NedJkKn%26%5D6%5Ei%3Es', -// qr_code_hex: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=65563a4a51314e65644a6b4b6e265d365e693e73', -// qr_code_base32: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=MVLDUSSRGFHGKZCKNNFW4JS5GZPGSPTT' } +```js +var passcode = require("passcode"); +var token = passcode.hotp({ + secret: "xyzzy", + counter: 123 +}); +// token = "378764" -// generate a key and get a QR code you can scan with the Google Authenticator app -speakeasy.generate_key({length: 20, google_auth_qr: true}); -// => { ascii: 'V?9f6.Cq1& nbytes) { + key = key.slice(0, nbytes); + } else { + i = Math.ceil(nbytes / key.length) + 1; + key = [key]; + while (--i) key.push(key[0]); + key = Buffer.concat(key).slice(0, nbytes); + } - for (var i = 0; i < 8; i++) { - var i_from_right = 7 - i; + // create an buffer from the counter + var buf = new Buffer(8); + var tmp = counter; + for (i = 0; i < 8; i++) { - // mask 255 over number to get last 8 - octet_array[i_from_right] = counter_temp & 255; + // 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 - counter_temp = counter_temp >> 8; + tmp = tmp >> 8; } - // create a buffer from the octet array - var counter_buffer = new Buffer(octet_array); + // init hmac with the key + var hmac = crypto.createHmac(algorithm, key); // update hmac with the counter - hmac.update(counter_buffer); + hmac.update(buf); - // get the digest in hex format - var digest = hmac.digest('hex'); + // return the digest + return hmac.digest(); +}; - // convert the result to an array of bytes - var digest_bytes = new Buffer(digest, 'hex'); +/** + * Generate a counter-based one-time passcode. + * + * @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"] Encytion algorithm (sha1, + * sha256, sha512). + * @return {String} The one-time passcode. + */ - // compute HOTP - // get offset - var offset = digest_bytes[(hash_size/8 - 1)] & 0xf; +exports.hotp = function hotpGenerate (options) { - // calculate bin_code (RFC4226 5.4) - var bin_code = (digest_bytes[offset] & 0x7f) << 24 - |(digest_bytes[offset+1] & 0xff) << 16 - |(digest_bytes[offset+2] & 0xff) << 8 - |(digest_bytes[offset+3] & 0xff); + // unpack options + var digits = options.digits || 6; - bin_code = bin_code.toString(); + // digest the options + var digest = options.digest || exports.digest(options); - // get the chars at position bin_code - length through length chars - var sub_start = bin_code.length - length; - var code = bin_code.substr(sub_start, length); + // compute HOTP offset + var offset = digest[digest.length - 1] & 0xf; - // we now have a code with `length` number of digits, so return it - return(code); + // 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); }; /** - * 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: - * - * options - * 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. + * Verify a counter-based One Time passcode. * + * @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=50] 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"] Encytion 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. */ -speakeasy.hotp.verify = function(options) { +exports.hotp.verify = function hotpVerify (options) { + var i; + + // shadow options + options = Object.create(options); + + // unpack options var token = options.token; var window = options.window || 50; var counter = options.counter || 0; - // Now loop through from C to C + W to determine if there is - // a correct code - for (var i = counter; i <= counter + window; ++i) { + // loop from C to C + W + for (i = counter; i <= counter + window; ++i) { options.counter = i; - if (speakeasy.hotp(options) === token) { - // We have found a matching code, trigger callback - // and pass offset + if (exports.hotp(options) == token) { + // found a matching code, return delta return {delta: i - counter}; } } - // If we get to here then no codes have matched. + // no codes have matched }; -// speakeasy.totp(options) -// -// Calculates the one-time password given the key, based on the current time -// with a 30 second step (step being the number of seconds between passwords). -// -// options.key the key -// .length(=6) length of the one-time password (default 6) -// .encoding(='ascii') key encoding (ascii, hex, or base32) -// .step(=30) override the step in seconds -// .time (optional) override the time to calculate with -// .initial_time (optional) override the initial time -// .algorithm (optional) override the default algorighm (default sha1) -// -speakeasy.totp = function(options) { - // set vars - var key = options.key; - var length = options.length || 6; - var encoding = options.encoding || 'ascii'; +/** + * Calculate counter value based on given options. + * + * @param {Object} options + * @param {Integer} [options.time] Time with which to calculate counter value + * @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 `Date.now()`. + * @return {Integer} The calculated counter value + * @private + */ + +exports._counter = function _counter (options) { var step = options.step || 30; - var initial_time = options.initial_time || 0; // unix epoch by default - var algorithm = options.algorithm || 'sha1'; + var time = options.time != null ? options.time : Date.now(); + var epoch = options.epoch || 0; + return Math.floor((time - epoch) / step / 1000); +}; + +/** + * Generate a time-based one-time passcode. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {Integer} [options.time] Time with which to calculate counter value + * @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 `Date.now()`. + * @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"] Encytion algorithm (sha1, + * sha256, sha512). + * @return {String} The one-time passcode. + */ - // get current time in seconds since unix epoch - var time = options.time || parseInt(Date.now()/1000); +exports.totp = function totpGenerate (options) { - // calculate counter value - var counter = Math.floor((time - initial_time)/ step); + // shadow options + options = Object.create(options); - // pass to hotp - var code = this.hotp({key: key, length: length, encoding: encoding, counter: counter, algorithm: algorithm}); + // calculate default counter value + if (options.counter == null) options.counter = exports._counter(options); - // return the code - return(code); + // pass to hotp + return this.hotp(options); }; /** - * 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 + * Verify a time-based One Time passcode. * - * Arguments: - * - * options - * 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 + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} [options.time] Time with which to calculate counter value + * @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 `Date.now()`. + * @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=6] 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"] Encytion 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. + */ + +exports.totp.verify = function totpVerify (options) { + + // shadow options + options = Object.create(options); + + // unpack options + var window = options.window != null ? options.window : 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.verify + return exports.hotp.verify(options); +}; + +/** + * Generate an URL for use with the Google Authenticator app. * - * time - The time step of the counter. This must be the same for - * every request and is used to calculate C. + * 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. * - * Default - 30 + * 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 {Integer} options.label Used to identify the account with which + * the secret key is associated, e.g. the user's email address. + * @param {Integer} [options.type="totp"] Either "hotp" or "totp". + * @param {Integer} [options.counter] The initial counter value, required + * for HOTP. + * @param {Integer} [options.issuer] The provider or service with which the + * secret key is associated. + * @param {String} [options.algorithm="sha1"] Encytion 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. + * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format */ -speakeasy.totp.verify = function(options) { - var step = options.step || 30; - var time = options.time || parseInt(Date.now()/1000); - var initial_time = options.initial_time || 0; - options.counter = Math.floor((time - initial_time)/ step); - return speakeasy.hotp.verify(options); -}; -// speakeasy.generate_key(options) -// -// Generates a random key with the set A-Z a-z 0-9 and symbols, of any length -// (default 32). Returns the key in ASCII, hexadecimal, and base32 format. -// Base32 format is used in Google Authenticator. Turn off symbols by setting -// symbols: false. Automatically generate links to QR codes of each encoding -// (using the Google Charts API) by setting qr_codes: true. Automatically -// generate a link to a special QR code for use with the Google Authenticator -// app, for which you can also specify a name. -// -// options.length(=32) length of key -// .symbols(=true) include symbols in the key -// .qr_codes(=false) generate links to QR codes -// .google_auth_qr(=false) generate a link to a QR code to scan -// with the Google Authenticator app. -// .name (optional) add a name. no spaces. -// for use with Google Authenticator -// .type (optional) totp or hotp -// .counter (optional) default counter value -// .issuer (optional) default issuer -speakeasy.generate_key = function(options) { - // options - var length = options.length || 32; - var name = options.name || "Secret Key"; - var qr_codes = options.qr_codes || false; - var google_auth_qr = options.google_auth_qr || false; - var symbols = options.symbols !== false; - var type = options.type || 'totp'; - var counter = options.counter || false; - var issuer = options.issuer || false; - - // generate an ascii key - var key = this.generate_key_ascii(length, symbols); - - // return a SecretKey with ascii, hex, and base32 - var SecretKey = {}; - SecretKey.ascii = key; - SecretKey.hex = new Buffer(key, "ascii").toString("hex"); - SecretKey.base32 = base32.encode(key); - - // generate some qr codes if requested - if (qr_codes) { - 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); - } +exports.url = function (options) { - // generate a QR code for use in Google Authenticator if requested - // (Google Authenticator has a special style and requires base32) - if (google_auth_qr) { - var otpauthURL = 'otpauth://totp/' + encodeURIComponent( name ) + '?secret=' + encodeURIComponent( SecretKey.base32 ); - if (issuer) otpauthURL += '&issuer=' + encodeURIComponent(issuer); - if (counter) otpauthURL += '&counter=' + encodeURIComponent(counter); - SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent( otpauthURL ); + // 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; + + // validate type + switch (type) { + case "totp": + case "hotp": + break; + default: + throw new Error("invalid type `" + type + "`"); } - return SecretKey; -}; + // validate required options + if (!secret) throw new Error("missing secret"); + if (!label) throw new Error("missing label"); -// speakeasy.generate_key_ascii(length, symbols) -// -// Generates a random key, of length `length` (default 32). -// Also choose whether you want symbols, default false. -// speakeasy.generate_key() wraps around this. -// -speakeasy.generate_key_ascii = function(length, symbols) { - var bytes = crypto.randomBytes(length || 32); - var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'; - if (symbols) { - set += '!@#$%^&*()<>?/[]{},.:;'; + // require counter for HOTP + if (type == "hotp" && counter == null) { + throw new Error("missing counter value for HOTP"); } - var output = ''; - for (var i = 0, l = bytes.length; i < l; i++) { - output += set[~~(bytes[i] / 0xFF * set.length)]; + // build query while validating + var query = {secret: secret}; + if (options.issuer) query.issuer = options.issuer; + + // validate algorithm + if (algorithm != null) { + switch (algorithm.toUpperCase()) { + case "SHA1": + case "SHA256": + case "SHA512": + break; + default: + throw new Error("invalid algorithm `" + algorithm + "`"); + } + query.algorithm = algorithm.toUpperCase(); } - return output; -}; -// alias, not the TV show -speakeasy.counter = speakeasy.hotp; -speakeasy.time = speakeasy.totp; + // validate digits + if (digits != null) { + switch (parseInt(digits, 10)) { + case 6: + case 8: + break; + default: + throw new Error("invalid digits `" + digits + "`"); + } + query.digits = digits; + } -module.exports = speakeasy; + // validate period + if (period != null) { + if (~~period != period) { + throw new Error("invalid period `" + period + "`"); + } + query.period = period; + } + + // convert secret to base32 + if (encoding != "base32") secret = new Buffer(secret, encoding); + if (Buffer.isBuffer(secret)) secret = base32.encode(secret); + + // return url + return url.format({ + protocol: "otpauth", + slashes: true, + hostname: type, + pathname: label, + query: query + }); +}; diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..85b56a1 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,25 @@ +{ + "source": { + "include": [ + "index.js", + "package.json", + "README.md" + ] + }, + "plugins": ["plugins/markdown"], + "templates": { + "applicationName": "Passcode", + "meta": { + "title": "Passcode", + "description": "Passcode - One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", + "keyword": "one-time passcode hotp totp google authenticator" + }, + "default": { + "outputSourceFiles": true + }, + "linenums": true + }, + "opts": { + "destination": "docs" + } +} diff --git a/package.json b/package.json index 52b10f5..4d792fb 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,25 @@ { - "author": "Mark Bao (http://markbao.com/)", - "name": "speakeasy", - "description": "Easy two-factor authentication with node.js. Time-based or counter-based (HOTP/TOTP), and supports the Google Authenticator mobile app. Also includes a key generator. Uses the HMAC One-Time Password algorithms.", - "version": "1.0.3", - "homepage": "http://github.com/markbao/speakeasy", + "name": "passcode", + "description": "One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", + "version": "1.0.0", + "author": { + "name": "Michael Phan-Ba", + "email": "michael@mikepb.com" + }, + "contributors": [{ + "name": "Mark Bao", + "email": "mark@markbao.com", + "url": "http://markbao.com" + }], + "homepage": "http://github.com/mikepb/passcode", "license": "MIT", "repository": { "type": "git", - "url": "git://github.com/markbao/speakeasy.git" + "url": "git://github.com/mikepb/passcode.git" }, "main": "index.js", "engines": { - "node": ">= 0.3.0" + "node": ">= 0.10.0" }, "dependencies": { "base32.js": "0.0.1" @@ -27,9 +35,11 @@ "passwords" ], "devDependencies": { + "jsdoc": "^3.3.1", "mocha": "^2.2.5" }, "scripts": { - "test": "mocha" + "test": "mocha", + "doc": "jsdoc -c jsdoc.json" } } diff --git a/test/hotp_test.js b/test/hotp_test.js new file mode 100644 index 0000000..06c57ee --- /dev/null +++ b/test/hotp_test.js @@ -0,0 +1,67 @@ +"use scrict"; + +var assert = require('assert'); +var speakeasy = require('..'); + +// These tests use the information from RFC 4226's Appendix D: Test Values. +// http://tools.ietf.org/html/rfc4226#appendix-D + +describe('HOTP Counter-Based Algorithm Test', function () { + + describe('normal operation with secret = \'12345678901234567890\' at counter 3', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 3}); + assert.equal(topic, '969429'); + }); + }); + + describe('another counter normal operation with secret = \'12345678901234567890\' at counter 7', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 7}); + assert.equal(topic, '162583'); + }); + }); + + describe('digits override with secret = \'12345678901234567890\' at counter 4 and digits = 8', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 4, digits: 8}); + assert.equal(topic, '40338314'); + }); + }); + + describe('hexadecimal encoding with secret = \'3132333435363738393031323334353637383930\' as hexadecimal at counter 4', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: '3132333435363738393031323334353637383930', encoding: 'hex', counter: 4}); + assert.equal(topic, '338314'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at counter 4', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 4}); + assert.equal(topic, '338314'); + }); + }); + + describe('base32 encoding with secret = \'1234567890\' at counter 3', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: '1234567890', counter: 3}); + assert.equal(topic, '969429'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, digits = 8 and algorithm as \'sha256\'', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha256'}); + assert.equal(topic, '46119246'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, digits = 8 and algorithm as \'sha512\'', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha512'}); + assert.equal(topic, '90693936'); + }); + }); + +}); diff --git a/test/test_notp.js b/test/notp_test.js similarity index 92% rename from test/test_notp.js rename to test/notp_test.js index 77b0c18..cea3dca 100644 --- a/test/test_notp.js +++ b/test/notp_test.js @@ -48,7 +48,7 @@ var speakeasy = require('..'); it("HOTP", function() { var options = { - key: '12345678901234567890', + secret: '12345678901234567890', window: 0 }; var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']; @@ -81,7 +81,7 @@ it("HOTP", function() { it("TOTtoken", function() { var options = { - key: '12345678901234567890', + secret: '12345678901234567890', window: 0 }; @@ -91,28 +91,28 @@ it("TOTtoken", function() { assert.ok(!speakeasy.totp.verify(options), 'Should not pass'); // counterheck for test vector at 59s - options.time = 59; + options.time = 59000; options.token = '287082'; var res = speakeasy.totp.verify(options); assert.ok(res, 'Should pass'); assert.equal(res.delta, 0, 'Should be in sync'); // counterheck for test vector at 1234567890 - options.time = 1234567890; + options.time = 1234567890000; options.token = '005924'; var res = speakeasy.totp.verify(options); assert.ok(res, 'Should pass'); assert.equal(res.delta, 0, 'Should be in sync'); // counterheck for test vector at 1111111109 - options.time = 1111111109; + options.time = 1111111109000; options.token = '081804'; var res = speakeasy.totp.verify(options); assert.ok(res, 'Should pass'); assert.equal(res.delta, 0, 'Should be in sync'); // counterheck for test vector at 2000000000 - options.time = 2000000000; + options.time = 2000000000000; options.token = '279037'; var res = speakeasy.totp.verify(options); assert.ok(res, 'Should pass'); @@ -129,7 +129,7 @@ it("TOTtoken", function() { it("HOTPOutOfSync", function() { var options = { - key: '12345678901234567890', + secret: '12345678901234567890', token: '520489', counter: 1 }; @@ -158,9 +158,9 @@ it("HOTPOutOfSync", function() { it("TOTPOutOfSync", function() { var options = { - key: '12345678901234567890', + secret: '12345678901234567890', token: '279037', - time: 1999999909 + time: 1999999909000 }; // counterheck that the test should fail for window < 2 @@ -175,7 +175,7 @@ it("TOTPOutOfSync", function() { it("hotp_gen", function() { var options = { - key: '12345678901234567890', + secret: '12345678901234567890', window: 0 }; @@ -194,7 +194,7 @@ it("hotp_gen", function() { it("totp_gen", function() { var options = { - key: '12345678901234567890', + secret: '12345678901234567890', window: 0 }; @@ -202,18 +202,18 @@ it("totp_gen", function() { speakeasy.totp(options); // counterheck for test vector at 59s - options.time = 59; + options.time = 59000; assert.equal(speakeasy.totp(options), '287082', 'TOTtoken values should match'); // counterheck for test vector at 1234567890 - options.time = 1234567890; + options.time = 1234567890000; assert.equal(speakeasy.totp(options), '005924', 'TOTtoken values should match'); // counterheck for test vector at 1111111109 - options.time = 1111111109; + options.time = 1111111109000; assert.equal(speakeasy.totp(options), '081804', 'TOTtoken values should match'); // counterheck for test vector at 2000000000 - options.time = 2000000000; + options.time = 2000000000000; assert.equal(speakeasy.totp(options), '279037', 'TOTtoken values should match'); }); diff --git a/test/test_hotp.js b/test/test_hotp.js deleted file mode 100644 index 4903f71..0000000 --- a/test/test_hotp.js +++ /dev/null @@ -1,67 +0,0 @@ -"use scrict"; - -var assert = require('assert'); -var speakeasy = require('..'); - -// These tests use the information from RFC 4226's Appendix D: Test Values. -// http://tools.ietf.org/html/rfc4226#appendix-D - -describe('HOTP Counter-Based Algorithm Test', function () { - - describe('normal operation with key = \'12345678901234567890\' at counter 3', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: '12345678901234567890', counter: 3}); - assert.equal(topic, '969429'); - }); - }); - - describe('another counter normal operation with key = \'12345678901234567890\' at counter 7', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: '12345678901234567890', counter: 7}); - assert.equal(topic, '162583'); - }); - }); - - describe('length override with key = \'12345678901234567890\' at counter 4 and length = 8', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: '12345678901234567890', counter: 4, length: 8}); - assert.equal(topic, '40338314'); - }); - }); - - describe('hexadecimal encoding with key = \'3132333435363738393031323334353637383930\' as hexadecimal at counter 4', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: '3132333435363738393031323334353637383930', encoding: 'hex', counter: 4}); - assert.equal(topic, '338314'); - }); - }); - - describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at counter 4', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 4}); - assert.equal(topic, '338314'); - }); - }); - - describe('base32 encoding with key = \'1234567890\' at counter 3', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: '1234567890', counter: 3}); - assert.equal(topic, '969429'); - }); - }); - - describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha256\'', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha256'}); - assert.equal(topic, '46119246'); - }); - }); - - describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, length = 8 and algorithm as \'sha512\'', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.hotp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, length: 8, algorithm: 'sha512'}); - assert.equal(topic, '90693936'); - }); - }); - -}); diff --git a/test/test_totp.js b/test/test_totp.js deleted file mode 100644 index 68bcc11..0000000 --- a/test/test_totp.js +++ /dev/null @@ -1,82 +0,0 @@ -"use scrict"; - -var assert = require('assert'); -var speakeasy = require('..'); - -// These tests use the test vectors from RFC 6238's Appendix B: Test Vectors -// http://tools.ietf.org/html/rfc6238#appendix-B -// They use an ASCII string of 12345678901234567890 and a time step of 30. - -describe('TOTP Time-Based Algorithm Test', function () { - - describe('normal operation with key = \'12345678901234567890\' at time = 59', function () { - it('should return correct one-time password', function() { - var topic = speakeasy.totp({key: '12345678901234567890', time: 59}); - assert.equal(topic, '287082'); - }); - }); - - describe('a different time normal operation with key = \'12345678901234567890\' at time = 1111111109', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: '12345678901234567890', time: 1111111109}); - assert.equal(topic, '081804'); - }); - }); - - describe('length parameter with key = \'12345678901234567890\' at time = 1111111109 and length = 8', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: '12345678901234567890', time: 1111111109, length: 8}); - assert.equal(topic, '07081804'); - }); - }); - - describe('hexadecimal encoding with key = \'3132333435363738393031323334353637383930\' as hexadecimal at time 1111111109', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109}); - assert.equal(topic, '081804'); - }); - }); - - describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at time 1111111109', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109}); - assert.equal(topic, '081804'); - }); - }); - - describe('a custom step with key = \'12345678901234567890\' at time = 1111111109 with step = 60', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: '12345678901234567890', time: 1111111109, step: 60}); - assert.equal(topic, '360094'); - }); - }); - - describe('initial time with key = \'12345678901234567890\' at time = 1111111109 and initial time = 1111111100', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: '12345678901234567890', time: 1111111109, initial_time: 1111111100}); - assert.equal(topic, '755224'); - }); - }); - - describe('base32 encoding with key = \'1234567890\' at time = 1111111109', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: '1234567890', time: 1111111109}); - assert.equal(topic, '081804'); - }); - }); - - describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109, length = 8 and algorithm as \'sha256\'', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109, length: 8, algorithm: 'sha256'}); - assert.equal(topic, '68084774'); - }); - }); - - describe('base32 encoding with key = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109, length = 8 and algorithm as \'sha512\'', function () { - it('should return correct one-time password', function () { - var topic = speakeasy.totp({key: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109, length: 8, algorithm: 'sha512'}); - assert.equal(topic, '25091201'); - }); - }); - -}); diff --git a/test/totp_test.js b/test/totp_test.js new file mode 100644 index 0000000..8b78cd9 --- /dev/null +++ b/test/totp_test.js @@ -0,0 +1,82 @@ +"use scrict"; + +var assert = require('assert'); +var speakeasy = require('..'); + +// These tests use the test vectors from RFC 6238's Appendix B: Test Vectors +// http://tools.ietf.org/html/rfc6238#appendix-B +// They use an ASCII string of 12345678901234567890 and a time step of 30. + +describe('TOTP Time-Based Algorithm Test', function () { + + describe('normal operation with secret = \'12345678901234567890\' at time = 59000', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 59000}); + assert.equal(topic, '287082'); + }); + }); + + describe('a different time normal operation with secret = \'12345678901234567890\' at time = 1111111109000', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('digits parameter with secret = \'12345678901234567890\' at time = 1111111109000 and digits = 8', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, digits: 8}); + assert.equal(topic, '07081804'); + }); + }); + + describe('hexadecimal encoding with secret = \'3132333435363738393031323334353637383930\' as hexadecimal at time 1111111109', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at time 1111111109', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('a custom step with secret = \'12345678901234567890\' at time = 1111111109000 with step = 60', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, step: 60}); + assert.equal(topic, '360094'); + }); + }); + + describe('initial time with secret = \'12345678901234567890\' at time = 1111111109000 and epoch = 1111111100000', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, epoch: 1111111100000}); + assert.equal(topic, '755224'); + }); + }); + + describe('base32 encoding with secret = \'1234567890\' at time = 1111111109000', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '1234567890', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha256\'', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha256'}); + assert.equal(topic, '68084774'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha512\'', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha512'}); + assert.equal(topic, '25091201'); + }); + }); + +}); diff --git a/test/url_test.js b/test/url_test.js new file mode 100644 index 0000000..65f8a4b --- /dev/null +++ b/test/url_test.js @@ -0,0 +1,176 @@ +"use scrict"; + +var assert = require('assert'); +var speakeasy = require('..'); +var url = require("url"); + +describe("#url", function () { + + it("should require options", function () { + assert.throws(function () { + speakeasy.url(); + }); + }); + + it("should validate type", function () { + assert.throws(function () { + speakeasy.url({ + type: "haha", + secret: "hello", + label: "that", + }, /invalid type `haha`/); + }); + }); + + it("should require secret", function () { + assert.throws(function () { + speakeasy.url({ + label: "that" + }, /missing secret/); + }); + }); + + it("should require label", function () { + assert.throws(function () { + speakeasy.url({ + secret: "hello" + }, /missing label/); + }); + }); + + it("should require counter for HOTP", function () { + assert.throws(function () { + speakeasy.url({ + type: "hotp", + secret: "hello", + label: "that" + }, /missing counter/); + }); + assert.ok(speakeasy.url({ + type: "hotp", + secret: "hello", + label: "that", + counter: 0 + })); + assert.ok(speakeasy.url({ + type: "hotp", + secret: "hello", + label: "that", + counter: 199 + })); + }); + + it("should validate algorithm", function () { + assert.throws(function () { + speakeasy.url({ + secret: "hello", + label: "that", + algorithm: "hello" + }, /invalid algorithm `hello`/); + }); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + algorithm: "sha1" + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + algorithm: "sha256" + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + algorithm: "sha512" + })); + }); + + it("should validate digits", function () { + assert.throws(function () { + speakeasy.url({ + secret: "hello", + label: "that", + digits: "hello" + }, /invalid digits `hello`/); + }); + assert.throws(function () { + speakeasy.url({ + secret: "hello", + label: "that", + digits: 12 + }, /invalid digits `12`/); + }); + assert.throws(function () { + speakeasy.url({ + secret: "hello", + label: "that", + digits: "7" + }, /invalid digits `7`/); + }); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + digits: 6 + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + digits: 8 + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + digits: "6" + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + digits: "8" + })); + }); + + it("should validate period", function () { + assert.throws(function () { + speakeasy.url({ + secret: "hello", + label: "that", + period: "hello" + }, /invalid period `hello`/); + }); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + period: 60 + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + period: 121 + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + period: "60" + })); + assert.ok(speakeasy.url({ + secret: "hello", + label: "that", + period: "121" + })); + }); + + it("should generate an URL compatible with the Google Authenticator app", function () { + var answer = speakeasy.url({ + secret: "JBSWY3DPEHPK3PXP", + label: "Example:alice@google.com", + issuer: "Example", + encoding: "base32" + }); + var expect = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"; + assert.deepEqual( + url.parse(answer), + url.parse(expect) + ); + }); + +}); From d6a2af2951bd5a53a2f97d85f90f87668f40d9df Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Fri, 12 Jun 2015 18:54:55 -0700 Subject: [PATCH 14/59] Fix verify methods not showing up in docs --- README.md | 4 +- docs/docco.css | 186 -------------------------------------------- docs/speakeasy.html | 164 -------------------------------------- index.js | 4 + package.json | 2 +- 5 files changed, 7 insertions(+), 353 deletions(-) delete mode 100644 docs/docco.css delete mode 100644 docs/speakeasy.html diff --git a/README.md b/README.md index 2ab1b37..328a990 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ Full documentation at http://mikepb.github.io/passcode/ ## License This project incorporates code from [speakeasy][] and [notp][], both of which -are licensed under MIT. Please see the [LICENSE][] file for the full combined -license. +are licensed under MIT. Please see the [LICENSE](LICENSE) file for the full +combined license. [speakeasy]: http://github.com/markbao/speakeasy diff --git a/docs/docco.css b/docs/docco.css deleted file mode 100644 index 5aa0a8d..0000000 --- a/docs/docco.css +++ /dev/null @@ -1,186 +0,0 @@ -/*--------------------- Layout and Typography ----------------------------*/ -body { - font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; - font-size: 15px; - line-height: 22px; - color: #252519; - margin: 0; padding: 0; -} -a { - color: #261a3b; -} - a:visited { - color: #261a3b; - } -p { - margin: 0 0 15px 0; -} -h1, h2, h3, h4, h5, h6 { - margin: 0px 0 15px 0; -} - h1 { - margin-top: 40px; - } -#container { - position: relative; -} -#background { - position: fixed; - top: 0; left: 525px; right: 0; bottom: 0; - background: #f5f5ff; - border-left: 1px solid #e5e5ee; - z-index: -1; -} -#jump_to, #jump_page { - background: white; - -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; - -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; - font: 10px Arial; - text-transform: uppercase; - cursor: pointer; - text-align: right; -} -#jump_to, #jump_wrapper { - position: fixed; - right: 0; top: 0; - padding: 5px 10px; -} - #jump_wrapper { - padding: 0; - display: none; - } - #jump_to:hover #jump_wrapper { - display: block; - } - #jump_page { - padding: 5px 0 3px; - margin: 0 0 25px 25px; - } - #jump_page .source { - display: block; - padding: 5px 10px; - text-decoration: none; - border-top: 1px solid #eee; - } - #jump_page .source:hover { - background: #f5f5ff; - } - #jump_page .source:first-child { - } -table td { - border: 0; - outline: 0; -} - td.docs, th.docs { - max-width: 450px; - min-width: 450px; - min-height: 5px; - padding: 10px 25px 1px 50px; - overflow-x: hidden; - vertical-align: top; - text-align: left; - } - .docs pre { - margin: 15px 0 15px; - padding-left: 15px; - } - .docs p tt, .docs p code { - background: #f8f8ff; - border: 1px solid #dedede; - font-size: 12px; - padding: 0 0.2em; - } - .pilwrap { - position: relative; - } - .pilcrow { - font: 12px Arial; - text-decoration: none; - color: #454545; - position: absolute; - top: 3px; left: -20px; - padding: 1px 2px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - } - td.docs:hover .pilcrow { - opacity: 1; - } - td.code, th.code { - padding: 14px 15px 16px 25px; - width: 100%; - vertical-align: top; - background: #f5f5ff; - border-left: 1px solid #e5e5ee; - } - pre, tt, code { - font-size: 12px; line-height: 18px; - font-family: Monaco, Consolas, "Lucida Console", monospace; - margin: 0; padding: 0; - } - - -/*---------------------- Syntax Highlighting -----------------------------*/ -td.linenos { background-color: #f0f0f0; padding-right: 10px; } -span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } -body .hll { background-color: #ffffcc } -body .c { color: #408080; font-style: italic } /* Comment */ -body .err { border: 1px solid #FF0000 } /* Error */ -body .k { color: #954121 } /* Keyword */ -body .o { color: #666666 } /* Operator */ -body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -body .cp { color: #BC7A00 } /* Comment.Preproc */ -body .c1 { color: #408080; font-style: italic } /* Comment.Single */ -body .cs { color: #408080; font-style: italic } /* Comment.Special */ -body .gd { color: #A00000 } /* Generic.Deleted */ -body .ge { font-style: italic } /* Generic.Emph */ -body .gr { color: #FF0000 } /* Generic.Error */ -body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -body .gi { color: #00A000 } /* Generic.Inserted */ -body .go { color: #808080 } /* Generic.Output */ -body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -body .gs { font-weight: bold } /* Generic.Strong */ -body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -body .gt { color: #0040D0 } /* Generic.Traceback */ -body .kc { color: #954121 } /* Keyword.Constant */ -body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ -body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ -body .kp { color: #954121 } /* Keyword.Pseudo */ -body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ -body .kt { color: #B00040 } /* Keyword.Type */ -body .m { color: #666666 } /* Literal.Number */ -body .s { color: #219161 } /* Literal.String */ -body .na { color: #7D9029 } /* Name.Attribute */ -body .nb { color: #954121 } /* Name.Builtin */ -body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -body .no { color: #880000 } /* Name.Constant */ -body .nd { color: #AA22FF } /* Name.Decorator */ -body .ni { color: #999999; font-weight: bold } /* Name.Entity */ -body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -body .nf { color: #0000FF } /* Name.Function */ -body .nl { color: #A0A000 } /* Name.Label */ -body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -body .nt { color: #954121; font-weight: bold } /* Name.Tag */ -body .nv { color: #19469D } /* Name.Variable */ -body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -body .w { color: #bbbbbb } /* Text.Whitespace */ -body .mf { color: #666666 } /* Literal.Number.Float */ -body .mh { color: #666666 } /* Literal.Number.Hex */ -body .mi { color: #666666 } /* Literal.Number.Integer */ -body .mo { color: #666666 } /* Literal.Number.Oct */ -body .sb { color: #219161 } /* Literal.String.Backtick */ -body .sc { color: #219161 } /* Literal.String.Char */ -body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ -body .s2 { color: #219161 } /* Literal.String.Double */ -body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -body .sh { color: #219161 } /* Literal.String.Heredoc */ -body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -body .sx { color: #954121 } /* Literal.String.Other */ -body .sr { color: #BB6688 } /* Literal.String.Regex */ -body .s1 { color: #219161 } /* Literal.String.Single */ -body .ss { color: #19469D } /* Literal.String.Symbol */ -body .bp { color: #954121 } /* Name.Builtin.Pseudo */ -body .vc { color: #19469D } /* Name.Variable.Class */ -body .vg { color: #19469D } /* Name.Variable.Global */ -body .vi { color: #19469D } /* Name.Variable.Instance */ -body .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/speakeasy.html b/docs/speakeasy.html deleted file mode 100644 index fb0ae24..0000000 --- a/docs/speakeasy.html +++ /dev/null @@ -1,164 +0,0 @@ - speakeasy.js

speakeasy.js

speakeasy

- -

HMAC One-Time Password module for Node.js, supporting counter-based and time-based moving factors

- -

speakeasy makes it easy to implement HMAC one-time passwords, supporting both counter-based (HOTP) -and time-based moving factors (TOTP). It's useful for implementing two-factor authentication. -Google and Amazon use TOTP to generate codes for use with multi-factor authentication.

- -

speakeasy also supports base32 keys/secrets, by passing base32 in the encoding option. -This is useful since Google Authenticator, Google's two-factor authentication mobile app -available for iPhone, Android, and BlackBerry, uses base32 keys.

- -

This module was written to follow the RFC memos on HTOP and TOTP:

- -
    -
  • HOTP (HMAC-Based One-Time Password Algorithm): RFC 4226
  • -
  • TOTP (Time-Based One-Time Password Algorithm): RFC 6238
  • -
- -

One other useful function that this module has is a key generator, which allows you to -generate keys, get them back in their ASCII, hexadecimal, and base32 representations. -In addition, it also can automatically generate QR codes for you, as well as the specialized -QR code you can use to scan in the Google Authenticator mobile app.

- -

An overarching goal of this module, other than to make it very easy to implement the -HOTP and TOTP algorithms, is to be extensively documented. Indeed, it is well-documented, -with clear functions and parameter explanations.

var crypto = require('crypto'),
-    ezcrypto = require('ezcrypto').Crypto,
-    base32 = require('thirty-two');
-
-speakeasy = {}

speakeasy.hotp(options)

- -

Calculates the one-time password given the key and a counter.

- -

options.key the key - .counter moving factor - .length(=6) length of the one-time password (default 6) - .encoding(='ascii') key encoding (ascii, hex, or base32)

speakeasy.hotp = function(options) {

set vars

  var key = options.key;
-  var counter = options.counter;
-  var length = options.length || 6;
-  var encoding = options.encoding || 'ascii';

preprocessing: convert to ascii if it's not

  if (encoding == 'hex') {
-    key = speakeasy.hex_to_ascii(key);
-  } else if (encoding == 'base32') {
-    key = base32.decode(key);
-  }

init hmac with the key

  var hmac = crypto.createHmac('sha1', new Buffer(key));
-  

create an octet array from the counter

  var octet_array = new Array(8);
-
-  var counter_temp = counter;
-
-  for (i = 0; i < 8; i++) {
-    i_from_right = 7 - i;

mask 255 over number to get last 8

    octet_array[i_from_right] = counter_temp & 255;

shift 8 and get ready to loop over the next batch of 8

    counter_temp = counter_temp >> 8;
-  }

create a buffer from the octet array

  var counter_buffer = new Buffer(octet_array);

update hmac with the counter

  hmac.update(counter_buffer);

get the digest in hex format

  var digest = hmac.digest('hex');

convert the result to an array of bytes

  var digest_bytes = ezcrypto.util.hexToBytes(digest);

compute HOTP -get offset

  var offset = digest_bytes[19] & 0xf;
-  

calculate bin_code (RFC4226 5.4)

  var bin_code = (digest_bytes[offset] & 0x7f)   << 24
-                |(digest_bytes[offset+1] & 0xff) << 16
-                |(digest_bytes[offset+2] & 0xff) << 8
-                |(digest_bytes[offset+3] & 0xff);
-
-  bin_code = bin_code.toString();

get the chars at position bin_code - length through length chars

  var sub_start = bin_code.length - length;
-  var code = bin_code.substr(sub_start, length);
-  

we now have a code with length number of digits, so return it

  return(code);
-}

speakeasy.totp(options)

- -

Calculates the one-time password given the key, based on the current time -with a 30 second step (step being the number of seconds between passwords).

- -

options.key the key - .length(=6) length of the one-time password (default 6) - .encoding(='ascii') key encoding (ascii, hex, or base32) - .step(=30) override the step in seconds - .time_now (optional) override the time to calculate with

speakeasy.totp = function(options) {

set vars

  var key = options.key;
-  var length = options.length || 6;
-  var encoding = options.encoding || 'ascii';
-  var step = options.step || 30;
-  

get current time in seconds since unix epoch

  var time_now = parseInt(Date.now()/1000);
-  

are we forcing a specific time?

  if (options.time_now) {

override the time

    time_now = options.time_now;
-  }

calculate counter value

  counter = Math.floor(time_now / step);
-  

pass to hotp

  code = this.hotp({key: key, length: length, encoding: encoding, counter: counter});

return the code

  return(code);
-}

speakeasy.hextoascii(key)

- -

helper function to convert a hex key to ascii.

speakeasy.hex_to_ascii = function(str) {

key is a string of hex -convert it to an array of bytes...

  var bytes = ezcrypto.util.hexToBytes(str);

bytes is now an array of bytes with character codes -merge this down into a string

  var ascii_string = new String();
-
-  for (var i = 0; i < bytes.length; i++) {
-    ascii_string += String.fromCharCode(bytes[i]);
-  }
-
-  return ascii_string;
-}

speakeasy.asciitohex(key)

- -

helper function to convert an ascii key to hex.

speakeasy.ascii_to_hex = function(str) {
-  var hex_string = '';
-  
-  for (var i = 0; i < str.length; i++) {
-    hex_string += str.charCodeAt(i).toString(16);
-  }
-
-  return hex_string;
-}

speakeasy.generate_key(options)

- -

Generates a random key with the set A-Z a-z 0-9 and symbols, of any length -(default 32). Returns the key in ASCII, hexadecimal, and base32 format. -Base32 format is used in Google Authenticator. Turn off symbols by setting -symbols: false. Automatically generate links to QR codes of each encoding -(using the Google Charts API) by setting qr_codes: true. Automatically -generate a link to a special QR code for use with the Google Authenticator -app, for which you can also specify a name.

- -

options.length(=32) length of key - .symbols(=true) include symbols in the key - .qrcodes(=false) generate links to QR codes - .googleauth_qr(=false) generate a link to a QR code to scan - with the Google Authenticator app. - .name (optional) add a name. no spaces. - for use with Google Authenticator

speakeasy.generate_key = function(options) {

options

  var length = options.length || 32;
-  var name = options.name || "Secret Key";
-  var qr_codes = options.qr_codes || false;
-  var google_auth_qr = options.google_auth_qr || false;

turn off symbols only when explicity told to

  if (options.symbols && options.symbols === false) {
-    symbols = false;
-  } else {
-    symbols = true;
-  }

generate an ascii key

  var key = this.generate_key_ascii(length, symbols);
-  

return a SecretKey with ascii, hex, and base32

  SecretKey = {};
-  SecretKey.ascii = key;
-  SecretKey.hex = this.ascii_to_hex(key);
-  SecretKey.base32 = base32.encode(key).replace(/=/g,'');
-  

generate some qr codes if requested

  if (qr_codes) {
-    SecretKey.qr_code_ascii = 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.ascii);
-    SecretKey.qr_code_hex = 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.hex);
-    SecretKey.qr_code_base32 = 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.base32);
-  }
-  

generate a QR code for use in Google Authenticator if requested -(Google Authenticator has a special style and requires base32)

  if (google_auth_qr) {

first, make sure that the name doesn't have spaces, since Google Authenticator doesn't like them

    name = name.replace(/ /g,'');
-    SecretKey.google_auth_qr = 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://totp/' + encodeURIComponent(name) + '%3Fsecret=' + encodeURIComponent(SecretKey.base32);
-  }
-
-  return SecretKey;
-}

speakeasy.generatekeyascii(length, symbols)

- -

Generates a random key, of length length (default 32). -Also choose whether you want symbols, default false. -speakeasy.generate_key() wraps around this.

speakeasy.generate_key_ascii = function(length, symbols) {
-  if (!length) length = 32;
-
-  var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
-
-  if (symbols) {
-    set += '!@#$%^&*()<>?/[]{},.:;';
-  }
-  
-  var key = '';
-
-  for(var i=0; i < length; i++) {
-    key += set.charAt(Math.floor(Math.random() * set.length));
-  }
-  
-  return key;
-}

alias, not the TV show

speakeasy.counter = speakeasy.hotp;
-speakeasy.time = speakeasy.totp;
-
-module.exports = speakeasy;
-
-
\ No newline at end of file diff --git a/index.js b/index.js index 560fa2b..bd188b2 100644 --- a/index.js +++ b/index.js @@ -138,6 +138,8 @@ exports.hotp = function hotpGenerate (options) { * sha256, sha512). * @return {Object} On success, returns an object with the counter * difference between the client and the server as the `delta` property. + * @method hotp․verify + * @global */ exports.hotp.verify = function hotpVerify (options) { @@ -237,6 +239,8 @@ exports.totp = function totpGenerate (options) { * sha256, sha512). * @return {Object} On success, returns an object with the time step * difference between the client and the server as the `delta` property. + * @method totp․verify + * @global */ exports.totp.verify = function totpVerify (options) { diff --git a/package.json b/package.json index 4d792fb..6cf6d40 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ }, "scripts": { "test": "mocha", - "doc": "jsdoc -c jsdoc.json" + "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/passcode/*/*.html" } } From 1c5da71ff7ab46920d4844f31023fa4aab7cf98b Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Fri, 12 Jun 2015 22:08:12 -0700 Subject: [PATCH 15/59] Update History.md with history of passcode --- History.md | 32 +++----------------------------- 1 file changed, 3 insertions(+), 29 deletions(-) diff --git a/History.md b/History.md index 6abdc5b..bb6a1dd 100644 --- a/History.md +++ b/History.md @@ -1,30 +1,4 @@ -Speakeasy +1.0.0 / 2015-07-12 +================== -Version History - -1.0.3 -===== - -Convenience release. Sabaidee from Luang Prabang, Laos. - -- Add vows to devDependencies and support `npm test` in package.json. Thanks, freewill! - -1.0.2 -===== - -Bugfix release. - -- Remove global leaks. Thanks for the fix, mashihua. - -1.0.1 -===== - -Bugfix release. Ciao from Florence, Italy. - -- Fixes issue where Google Chart API was being called at a deprecated URL. Thanks for the fix, sakkaku. -- Fixes issue where `generate_key`'s `symbols` option was not working, and was also causing pollution with global var. Thanks for reporting the bug, ARAtlas. - -1.0.0 -===== - -Initial release. + * Initial release based on speakeasy and notp. From 32a0fa37c347b67b383c8925482d1a47caa83f10 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Sat, 13 Jun 2015 08:50:26 -0700 Subject: [PATCH 16/59] Ignore case on algorithm option --- index.js | 34 ++++---- test/rfc4226_test.js | 93 ++++++++++++++++++++ test/rfc6238_test.js | 198 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+), 18 deletions(-) create mode 100644 test/rfc4226_test.js create mode 100644 test/rfc6238_test.js diff --git a/index.js b/index.js index bd188b2..c5cb392 100644 --- a/index.js +++ b/index.js @@ -14,12 +14,10 @@ var url = require("url"); * @param {Object} options * @param {String} options.secret Shared secret key * @param {Integer} options.counter Counter value - * @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"] Encytion algorithm (sha1, - * sha256, sha512). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). * @return {Buffer} The one-time passcode as a buffer. */ @@ -30,7 +28,7 @@ exports.digest = function digest (options) { var key = options.secret; var counter = options.counter; var encoding = options.encoding || "ascii"; - var algorithm = options.algorithm || "sha1"; + var algorithm = (options.algorithm || "sha1").toLowerCase(); var nbytes; // set hash size based on algorithm @@ -46,13 +44,13 @@ exports.digest = function digest (options) { : new Buffer(key, encoding); } - // repeat the key to the minumum length + // repeat the key to the minimum length if (key.length > nbytes) { key = key.slice(0, nbytes); } else { - i = Math.ceil(nbytes / key.length) + 1; + i = ~~(nbytes / key.length); key = [key]; - while (--i) key.push(key[0]); + while (i--) key.push(key[0]); key = Buffer.concat(key).slice(0, nbytes); } @@ -89,8 +87,8 @@ exports.digest = function digest (options) { * passcode. * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, * base32, base64). - * @param {String} [options.algorithm="sha1"] Encytion algorithm (sha1, - * sha256, sha512). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). * @return {String} The one-time passcode. */ @@ -134,8 +132,8 @@ exports.hotp = function hotpGenerate (options) { * 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"] Encytion algorithm (sha1, - * sha256, sha512). + * @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. * @method hotp․verify @@ -198,8 +196,8 @@ exports._counter = function _counter (options) { * passcode. * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, * base32, base64). - * @param {String} [options.algorithm="sha1"] Encytion algorithm (sha1, - * sha256, sha512). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). * @return {String} The one-time passcode. */ @@ -235,8 +233,8 @@ exports.totp = function totpGenerate (options) { * inclusive. * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, * base32, base64). - * @param {String} [options.algorithm="sha1"] Encytion algorithm (sha1, - * sha256, sha512). + * @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. * @method totp․verify @@ -282,8 +280,8 @@ exports.totp.verify = function totpVerify (options) { * for HOTP. * @param {Integer} [options.issuer] The provider or service with which the * secret key is associated. - * @param {String} [options.algorithm="sha1"] Encytion algorithm (sha1, - * sha256, sha512). + * @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 diff --git a/test/rfc4226_test.js b/test/rfc4226_test.js new file mode 100644 index 0000000..26ff568 --- /dev/null +++ b/test/rfc4226_test.js @@ -0,0 +1,93 @@ +"use strict"; + +var assert = require('assert'); +var passcode = require('..'); + +/* + + The following test data uses the ASCII string + "12345678901234567890" for the secret: + + Secret = 0x3132333435363738393031323334353637383930 + + Table 1 details for each count, the intermediate HMAC value. + + Count Hexadecimal HMAC-SHA-1(secret, count) + 0 cc93cf18508d94934c64b65d8ba7667fb7cde4b0 + 1 75a48a19d4cbe100644e8ac1397eea747a2d33ab + 2 0bacb7fa082fef30782211938bc1c5e70416ff44 + 3 66c28227d03a2d5529262ff016a1e6ef76557ece + 4 a904c900a64b35909874b33e61c5938a8e15ed1c + 5 a37e783d7b7233c083d4f62926c7a25f238d0316 + 6 bc9cd28561042c83f219324d3c607256c03272ae + 7 a4fb960c0bc06e1eabb804e5b397cdc4b45596fa + 8 1b3c89f65e6c9e883012052823443f048b4332db + 9 1637409809a679dc698207310c8c7fc07290d9e5 + + Table 2 details for each count the truncated values (both in + hexadecimal and decimal) and then the HOTP value. + + Truncated + Count Hexadecimal Decimal HOTP + 0 4c93cf18 1284755224 755224 + 1 41397eea 1094287082 287082 + 2 82fef30 137359152 359152 + 3 66ef7655 1726969429 969429 + 4 61c5938a 1640338314 338314 + 5 33c083d4 868254676 254676 + 6 7256c032 1918287922 287922 + 7 4e5b397 82162583 162583 + 8 2823443f 673399871 399871 + 9 2679dc69 645520489 520489 + +*/ + +describe("RFC 4226 test values", function () { + + describe("intermediate HMAC values", function () { + [ + "cc93cf18508d94934c64b65d8ba7667fb7cde4b0", + "75a48a19d4cbe100644e8ac1397eea747a2d33ab", + "0bacb7fa082fef30782211938bc1c5e70416ff44", + "66c28227d03a2d5529262ff016a1e6ef76557ece", + "a904c900a64b35909874b33e61c5938a8e15ed1c", + "a37e783d7b7233c083d4f62926c7a25f238d0316", + "bc9cd28561042c83f219324d3c607256c03272ae", + "a4fb960c0bc06e1eabb804e5b397cdc4b45596fa", + "1b3c89f65e6c9e883012052823443f048b4332db", + "1637409809a679dc698207310c8c7fc07290d9e5" + ].forEach(function (expect, count) { + it("should match for counter = " + count, function () { + var hash = passcode.digest({ + secret: "12345678901234567890", + counter: count + }).toString("hex"); + assert.equal(hash, expect); + }); + }); + }); + + describe("HOTP values", function () { + [ + "755224", + "287082", + "359152", + "969429", + "338314", + "254676", + "287922", + "162583", + "399871", + "520489" + ].forEach(function (expect, count) { + it("should match for count = " + count, function () { + var code = passcode.hotp({ + secret: "12345678901234567890", + counter: count + }); + assert.equal(code, expect); + }); + }); + }); + +}); diff --git a/test/rfc6238_test.js b/test/rfc6238_test.js new file mode 100644 index 0000000..06ac898 --- /dev/null +++ b/test/rfc6238_test.js @@ -0,0 +1,198 @@ +"use strict"; + +var assert = require('assert'); +var passcode = require('..'); + +/* + + This section provides test values that can be used for the HOTP time- + based variant algorithm interoperability test. + + The test token shared secret uses the ASCII string value + "12345678901234567890". With Time Step X = 30, and the Unix epoch as + the initial value to count time steps, where T0 = 0, the TOTP + algorithm will display the following values for specified modes and + timestamps. + + +-------------+--------------+------------------+----------+--------+ + | Time (sec) | UTC Time | Value of T (hex) | TOTP | Mode | + +-------------+--------------+------------------+----------+--------+ + | 59 | 1970-01-01 | 0000000000000001 | 94287082 | SHA1 | + | | 00:00:59 | | | | + | 59 | 1970-01-01 | 0000000000000001 | 46119246 | SHA256 | + | | 00:00:59 | | | | + | 59 | 1970-01-01 | 0000000000000001 | 90693936 | SHA512 | + | | 00:00:59 | | | | + | 1111111109 | 2005-03-18 | 00000000023523EC | 07081804 | SHA1 | + | | 01:58:29 | | | | + | 1111111109 | 2005-03-18 | 00000000023523EC | 68084774 | SHA256 | + | | 01:58:29 | | | | + | 1111111109 | 2005-03-18 | 00000000023523EC | 25091201 | SHA512 | + | | 01:58:29 | | | | + | 1111111111 | 2005-03-18 | 00000000023523ED | 14050471 | SHA1 | + | | 01:58:31 | | | | + | 1111111111 | 2005-03-18 | 00000000023523ED | 67062674 | SHA256 | + | | 01:58:31 | | | | + | 1111111111 | 2005-03-18 | 00000000023523ED | 99943326 | SHA512 | + | | 01:58:31 | | | | + | 1234567890 | 2009-02-13 | 000000000273EF07 | 89005924 | SHA1 | + | | 23:31:30 | | | | + | 1234567890 | 2009-02-13 | 000000000273EF07 | 91819424 | SHA256 | + | | 23:31:30 | | | | + | 1234567890 | 2009-02-13 | 000000000273EF07 | 93441116 | SHA512 | + | | 23:31:30 | | | | + | 2000000000 | 2033-05-18 | 0000000003F940AA | 69279037 | SHA1 | + | | 03:33:20 | | | | + | 2000000000 | 2033-05-18 | 0000000003F940AA | 90698825 | SHA256 | + | | 03:33:20 | | | | + | 2000000000 | 2033-05-18 | 0000000003F940AA | 38618901 | SHA512 | + | | 03:33:20 | | | | + | 20000000000 | 2603-10-11 | 0000000027BC86AA | 65353130 | SHA1 | + | | 11:33:20 | | | | + | 20000000000 | 2603-10-11 | 0000000027BC86AA | 77737706 | SHA256 | + | | 11:33:20 | | | | + | 20000000000 | 2603-10-11 | 0000000027BC86AA | 47863826 | SHA512 | + | | 11:33:20 | | | | + +-------------+--------------+------------------+----------+--------+ + + Table 1: TOTP Table + +*/ + +describe("RFC 6238 test vector", function () { + [{ + time: 59000, + date: new Date("1970-01-01T00:00:59Z"), + counter: 0x01, + code: "94287082", + algorithm: "SHA1" + }, { + time: 59000, + date: new Date("1970-01-01T00:00:59Z"), + counter: 0x01, + code: "46119246", + algorithm: "SHA256" + }, { + time: 59000, + date: new Date("1970-01-01T00:00:59Z"), + counter: 0x01, + code: "90693936", + algorithm: "SHA512" + }, { + time: 1111111109000, + date: new Date("2005-03-18T01:58:29Z"), + counter: 0x023523EC, + code: "07081804", + algorithm: "SHA1" + }, { + time: 1111111109000, + date: new Date("2005-03-18T01:58:29Z"), + counter: 0x023523EC, + code: "68084774", + algorithm: "SHA256" + }, { + time: 1111111109000, + date: new Date("2005-03-18T01:58:29Z"), + counter: 0x023523EC, + code: "25091201", + algorithm: "SHA512" + }, { + time: 1111111111000, + date: new Date("2005-03-18T01:58:31Z"), + counter: 0x023523ED, + code: "14050471", + algorithm: "SHA1" + }, { + time: 1111111111000, + date: new Date("2005-03-18T01:58:31Z"), + counter: 0x023523ED, + code: "67062674", + algorithm: "SHA256" + }, { + time: 1111111111000, + date: new Date("2005-03-18T01:58:31Z"), + counter: 0x023523ED, + code: "99943326", + algorithm: "SHA512" + }, { + time: 1234567890000, + date: new Date("2009-02-13T23:31:30Z"), + counter: 0x0273EF07, + code: "89005924", + algorithm: "SHA1" + }, { + time: 1234567890000, + date: new Date("2009-02-13T23:31:30Z"), + counter: 0x0273EF07, + code: "91819424", + algorithm: "SHA256" + }, { + time: 1234567890000, + date: new Date("2009-02-13T23:31:30Z"), + counter: 0x0273EF07, + code: "93441116", + algorithm: "SHA512" + }, { + time: 2000000000000, + date: new Date("2033-05-18T03:33:20Z"), + counter: 0x03F940AA, + code: "69279037", + algorithm: "SHA1" + }, { + time: 2000000000000, + date: new Date("2033-05-18T03:33:20Z"), + counter: 0x03F940AA, + code: "90698825", + algorithm: "SHA256" + }, { + time: 2000000000000, + date: new Date("2033-05-18T03:33:20Z"), + counter: 0x03F940AA, + code: "38618901", + algorithm: "SHA512" + }, { + time: 20000000000000, + date: new Date("2603-10-11T11:33:20Z"), + counter: 0x27BC86AA, + code: "65353130", + algorithm: "SHA1" + }, { + time: 20000000000000, + date: new Date("2603-10-11T11:33:20Z"), + counter: 0x27BC86AA, + code: "77737706", + algorithm: "SHA256" + }, { + time: 20000000000000, + date: new Date("2603-10-11T11:33:20Z"), + counter: 0x27BC86AA, + code: "47863826", + algorithm: "SHA512" + }].forEach(function (subject) { + + it("should calculate counter value for time " + subject.time, function () { + var counter = passcode._counter({ + time: subject.time + }); + assert.equal(counter, subject.counter); + }); + + it("should calculate counter value for date " + subject.date, function () { + var counter = passcode._counter({ + time: subject.date + }); + assert.equal(counter, subject.counter); + }); + + it("should generate TOTP code for time " + subject.time + " and algorithm " + subject.algorithm, function () { + var counter = passcode.totp({ + secret: "12345678901234567890", + time: subject.time, + algorithm: subject.algorithm, + digits: 8 + }); + assert.equal(counter, subject.code); + }); + + }); +}); From 2aceeb4102e94f689047a3f7cbfefa8e7e94488e Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Sat, 13 Jun 2015 08:58:54 -0700 Subject: [PATCH 17/59] v1.0.1 --- History.md | 6 ++++++ package.json | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index bb6a1dd..ee69fcb 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +1.0.1 / 2015-07-13 +================== + + * [Fixed] Ignore case on algorithm option. + + 1.0.0 / 2015-07-12 ================== diff --git a/package.json b/package.json index 6cf6d40..129aa81 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "passcode", "description": "One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", - "version": "1.0.0", + "version": "1.0.1", "author": { "name": "Michael Phan-Ba", "email": "michael@mikepb.com" @@ -10,6 +10,9 @@ "name": "Mark Bao", "email": "mark@markbao.com", "url": "http://markbao.com" + }, { + "name": "Guy Halford-Thompson", + "email": "guy@cach.me" }], "homepage": "http://github.com/mikepb/passcode", "license": "MIT", From a83c348b6d99619daae0fdf3b5dfffe3008ad5fe Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Sat, 13 Jun 2015 20:28:01 -0700 Subject: [PATCH 18/59] Update instructions to use `npm install --save passcode` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 328a990..16a36df 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ verification functions of [notp][]. ## Install ```sh -npm install passcode +npm install --save passcode ``` ## Usage From 5b84a6f344b7c122a6b6da85ab32bf64028c2977 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Sat, 13 Jun 2015 22:15:27 -0700 Subject: [PATCH 19/59] [Fixed] Don't repeat the secret key generating the digest. --- index.js | 18 ------------------ test/hotp_test.js | 12 ++++++------ test/rfc6238_test.js | 21 ++++++++++++++++++++- test/totp_test.js | 10 +++++----- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/index.js b/index.js index c5cb392..6f3ea0d 100644 --- a/index.js +++ b/index.js @@ -29,14 +29,6 @@ exports.digest = function digest (options) { var counter = options.counter; var encoding = options.encoding || "ascii"; var algorithm = (options.algorithm || "sha1").toLowerCase(); - var nbytes; - - // set hash size based on algorithm - switch (algorithm) { - case "sha256": nbytes = 32; break; - case "sha512": nbytes = 64; break; - default: nbytes = 20; - } // convert key to buffer if (!Buffer.isBuffer(key)) { @@ -44,16 +36,6 @@ exports.digest = function digest (options) { : new Buffer(key, encoding); } - // repeat the key to the minimum length - if (key.length > nbytes) { - key = key.slice(0, nbytes); - } else { - i = ~~(nbytes / key.length); - key = [key]; - while (i--) key.push(key[0]); - key = Buffer.concat(key).slice(0, nbytes); - } - // create an buffer from the counter var buf = new Buffer(8); var tmp = counter; diff --git a/test/hotp_test.js b/test/hotp_test.js index 06c57ee..6a04627 100644 --- a/test/hotp_test.js +++ b/test/hotp_test.js @@ -43,23 +43,23 @@ describe('HOTP Counter-Based Algorithm Test', function () { }); }); - describe('base32 encoding with secret = \'1234567890\' at counter 3', function () { + describe('base32 encoding with secret = \'12345678901234567890\' at counter 3', function () { it('should return correct one-time password', function() { - var topic = speakeasy.hotp({secret: '1234567890', counter: 3}); + var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 3}); assert.equal(topic, '969429'); }); }); - describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, digits = 8 and algorithm as \'sha256\'', function () { + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA\' as base32 at counter 1, digits = 8 and algorithm as \'sha256\'', function () { it('should return correct one-time password', function() { - var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha256'}); + var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha256'}); assert.equal(topic, '46119246'); }); }); - describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at counter 1, digits = 8 and algorithm as \'sha512\'', function () { + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA\' as base32 at counter 1, digits = 8 and algorithm as \'sha512\'', function () { it('should return correct one-time password', function() { - var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha512'}); + var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha512'}); assert.equal(topic, '90693936'); }); }); diff --git a/test/rfc6238_test.js b/test/rfc6238_test.js index 06ac898..5645de2 100644 --- a/test/rfc6238_test.js +++ b/test/rfc6238_test.js @@ -169,6 +169,25 @@ describe("RFC 6238 test vector", function () { code: "47863826", algorithm: "SHA512" }].forEach(function (subject) { + var key = new Buffer("12345678901234567890"); + var nbytes, i; + + // set hash size based on algorithm + switch (subject.algorithm) { + case "SHA256": nbytes = 32; break; + case "SHA512": nbytes = 64; break; + default: nbytes = 20; + } + + // repeat the key to the minimum length + if (key.length > nbytes) { + key = key.slice(0, nbytes); + } else { + i = ~~(nbytes / key.length); + key = [key]; + while (i--) key.push(key[0]); + key = Buffer.concat(key).slice(0, nbytes); + } it("should calculate counter value for time " + subject.time, function () { var counter = passcode._counter({ @@ -186,7 +205,7 @@ describe("RFC 6238 test vector", function () { it("should generate TOTP code for time " + subject.time + " and algorithm " + subject.algorithm, function () { var counter = passcode.totp({ - secret: "12345678901234567890", + secret: key, time: subject.time, algorithm: subject.algorithm, digits: 8 diff --git a/test/totp_test.js b/test/totp_test.js index 8b78cd9..b6f9ec6 100644 --- a/test/totp_test.js +++ b/test/totp_test.js @@ -60,21 +60,21 @@ describe('TOTP Time-Based Algorithm Test', function () { describe('base32 encoding with secret = \'1234567890\' at time = 1111111109000', function () { it('should return correct one-time password', function () { - var topic = speakeasy.totp({secret: '1234567890', time: 1111111109000}); + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000}); assert.equal(topic, '081804'); }); }); - describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha256\'', function () { + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha256\'', function () { it('should return correct one-time password', function () { - var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha256'}); + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha256'}); assert.equal(topic, '68084774'); }); }); - describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQ\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha512\'', function () { + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha512\'', function () { it('should return correct one-time password', function () { - var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha512'}); + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha512'}); assert.equal(topic, '25091201'); }); }); From bb61c95e4f0e9423d0631e13ba5b272e3a1f27c9 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Sat, 13 Jun 2015 22:18:39 -0700 Subject: [PATCH 20/59] v1.0.2 --- History.md | 6 ++++++ package.json | 13 ++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/History.md b/History.md index ee69fcb..e6742be 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +1.0.2 / 2015-07-13 +================== + + * [Fixed] Don't repeat the secret key generating the digest. + + 1.0.1 / 2015-07-13 ================== diff --git a/package.json b/package.json index 129aa81..f4e99b6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "passcode", "description": "One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", - "version": "1.0.1", + "version": "1.0.2", "author": { "name": "Michael Phan-Ba", "email": "michael@mikepb.com" @@ -15,6 +15,17 @@ "email": "guy@cach.me" }], "homepage": "http://github.com/mikepb/passcode", + "keywords": [ + "authentication", + "google authenticator", + "hmac", + "hotp", + "multi-factor", + "one-time password", + "passwords", + "totp", + "two-factor" + ], "license": "MIT", "repository": { "type": "git", From 7f62d5a6a911e9bab4522a54d798a801aec57eb2 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Sat, 13 Jun 2015 22:19:55 -0700 Subject: [PATCH 21/59] Add .npmignore --- .npmignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +test From 30a71588b3254ed26b8fcff59a834d5e28078bdf Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 23 Dec 2015 01:07:39 -0500 Subject: [PATCH 22/59] Updated README and changed names. --- README.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 16a36df..00ec707 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,28 @@ -# Passcode + -Passcode implements one-time passcode generators as standardized by the +Speakeasy implements one-time passcode generators as standardized by the [Initiative for Open Authentication (OATH)][oath]. The HMAC-Based One-time Password (HOTP) algorithm defined by [RFC 4226][rfc4226] and the Time-based One-time Password (TOTP) algorithm defined in [RFC 6238][rfc6238] are supported. -Passcode is a heavily modified version of [speakeasy][] incorporating the -verification functions of [notp][]. - ## Install ```sh -npm install --save passcode +npm install --save speakeasy ``` ## Usage ```js -var passcode = require("passcode"); -var token = passcode.hotp({ +var speakeasy = require("speakeasy"); +var token = speakeasy.hotp({ secret: "xyzzy", counter: 123 }); // token = "378764" -var ok = passcode.hotp.verify({ +var ok = speakeasy.hotp.verify({ secret: "xyzzy", token: token, counter: 123 @@ -39,12 +36,12 @@ Full documentation at http://mikepb.github.io/passcode/ ## License -This project incorporates code from [speakeasy][] and [notp][], both of which -are licensed under MIT. Please see the [LICENSE](LICENSE) file for the full -combined license. +This project incorporates code from [passcode][], which was originally a +fork of speakeasy, and [notp][], both of which are licensed under MIT. +Please see the [LICENSE](LICENSE) file for the full combined license. -[speakeasy]: http://github.com/markbao/speakeasy +[passcode]: http://github.com/mikepb/passcode [notp]: https://github.com/guyht/notp [oath]: http://www.openauthentication.org/ [rfc4226]: https://tools.ietf.org/html/rfc4226 From 859f3d4c3ca46a467f4a1ef8b56bebbf2136700d Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 23 Dec 2015 01:09:23 -0500 Subject: [PATCH 23/59] Updated .travis.yml with new Node versions --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6064ca0..4ad0861 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: node_js node_js: - - "0.10" + - "5.3" + - "5.0" + - "4.1" + - "4.0" - "0.12" - - "iojs" From f93ed29564013ed36680726cdcda0da9d6f4ec4a Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 23 Dec 2015 01:11:27 -0500 Subject: [PATCH 24/59] Added Travis CI badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 00ec707..a8b2ffe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ +[![Build Status](https://travis-ci.org/speakeasyjs/speakeasy.svg?branch=v2)](https://travis-ci.org/speakeasyjs/speakeasy) + Speakeasy implements one-time passcode generators as standardized by the [Initiative for Open Authentication (OATH)][oath]. The HMAC-Based One-time Password (HOTP) algorithm defined by [RFC 4226][rfc4226] and the Time-based From 5ab4d662152e1b558fce08f97aa4379882518e81 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 23 Dec 2015 01:12:28 -0500 Subject: [PATCH 25/59] Added icon attribution --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a8b2ffe..d9636f8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ This project incorporates code from [passcode][], which was originally a fork of speakeasy, and [notp][], both of which are licensed under MIT. Please see the [LICENSE](LICENSE) file for the full combined license. +Icon created by Gregor Črešnar from the Noun Project. [passcode]: http://github.com/mikepb/passcode [notp]: https://github.com/guyht/notp From 69942dc112c80bc97fac1e5188db85cc01722b54 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 23 Dec 2015 01:19:10 -0500 Subject: [PATCH 26/59] Updated badges --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d9636f8..229579d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ [![Build Status](https://travis-ci.org/speakeasyjs/speakeasy.svg?branch=v2)](https://travis-ci.org/speakeasyjs/speakeasy) +[![NPM downloads](https://img.shields.io/npm/dt/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) +[![NPM version](https://img.shields.io/npm/v/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) Speakeasy implements one-time passcode generators as standardized by the [Initiative for Open Authentication (OATH)][oath]. The HMAC-Based One-time From 4608a4dfd34e84d27119699e997271d643b59564 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Tue, 29 Dec 2015 14:14:31 -0800 Subject: [PATCH 27/59] Added demo to README and updated package.json author and package names --- README.md | 15 +++++++++++++-- package.json | 19 +++++++++---------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 229579d..ecb6b9a 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,14 @@ [![NPM downloads](https://img.shields.io/npm/dt/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) [![NPM version](https://img.shields.io/npm/v/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) +Speakeasy is a one-time passcode generator, suitable for use in two-factor +authentication, and supports Google Authenticator and other two-factor apps. +It includes robust support for custom token lengths, authentication windows, +and other features, and includes helpers like a secret key generator. + Speakeasy implements one-time passcode generators as standardized by the -[Initiative for Open Authentication (OATH)][oath]. The HMAC-Based One-time -Password (HOTP) algorithm defined by [RFC 4226][rfc4226] and the Time-based +[Initiative for Open Authentication (OATH)][oath]. The HMAC-Based One-Time +Password (HOTP) algorithm defined by [RFC 4226][rfc4226] and the Time-Based One-time Password (TOTP) algorithm defined in [RFC 6238][rfc6238] are supported. @@ -16,6 +21,12 @@ supported. npm install --save speakeasy ``` +## Demo + +This demo uses the `generate_key` method of Speakeasy to generate a secret key, displays a Google Authenticator–compatible QR code which you can scan into your phone's two-factor app, and shows the token, which you can verify with your phone. + + + ## Usage ```js diff --git a/package.json b/package.json index f4e99b6..1f74529 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,19 @@ { - "name": "passcode", - "description": "One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", - "version": "1.0.2", + "name": "speakeasy", + "description": "Two-factor authentication for Node. One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", + "version": "2.0.0", "author": { - "name": "Michael Phan-Ba", - "email": "michael@mikepb.com" + "name": "Mark Bao & Speakeasy Contributors", + "email": "mark@markbao.com" }, "contributors": [{ - "name": "Mark Bao", - "email": "mark@markbao.com", - "url": "http://markbao.com" + "name": "Michael Phan-Ba", + "email": "michael@mikepb.com" }, { "name": "Guy Halford-Thompson", "email": "guy@cach.me" }], - "homepage": "http://github.com/mikepb/passcode", + "homepage": "http://github.com/speakeasyjs/speakeasy", "keywords": [ "authentication", "google authenticator", @@ -29,7 +28,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git://github.com/mikepb/passcode.git" + "url": "git://github.com/speakeasyjs/speakeasy.git" }, "main": "index.js", "engines": { From 9e1df2b4c734808c6d279ec34486c154dbe6908d Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Tue, 29 Dec 2015 14:19:30 -0800 Subject: [PATCH 28/59] Minor updates to README and jsdoc --- README.md | 5 ++++- jsdoc.json | 6 +++--- package.json | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ecb6b9a..1fae004 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,10 @@ npm install --save speakeasy ## Demo -This demo uses the `generate_key` method of Speakeasy to generate a secret key, displays a Google Authenticator–compatible QR code which you can scan into your phone's two-factor app, and shows the token, which you can verify with your phone. +This demo uses the `generate_key` method of Speakeasy to generate a secret key, +displays a Google Authenticator–compatible QR code which you can scan into your +phone's two-factor app, and shows the token, which you can verify with your +phone. Includes sample code. diff --git a/jsdoc.json b/jsdoc.json index 85b56a1..6a92809 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -8,10 +8,10 @@ }, "plugins": ["plugins/markdown"], "templates": { - "applicationName": "Passcode", + "applicationName": "Speakeasy", "meta": { - "title": "Passcode", - "description": "Passcode - One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", + "title": "Speakeasy", + "description": "Speakeasy - Two-factor authentication for Node.js. One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", "keyword": "one-time passcode hotp totp google authenticator" }, "default": { diff --git a/package.json b/package.json index 1f74529..5c4f602 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,6 @@ }, "scripts": { "test": "mocha", - "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/passcode/*/*.html" + "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/speakeasy/*/*.html" } } From 055d802cb1b068046184f316020eb3b7452dd670 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Tue, 29 Dec 2015 14:33:21 -0800 Subject: [PATCH 29/59] Updated documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1fae004..1cde3d5 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ var ok = speakeasy.hotp.verify({ ## Documentation -Full documentation at http://mikepb.github.io/passcode/ +Full documentation at http://speakeasyjs.github.io/speakeasy/ ## License From ca8cb5864b47b34ace860a7bce4268f1d027da98 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Tue, 29 Dec 2015 14:42:05 -0800 Subject: [PATCH 30/59] Minor updates to README formatting --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cde3d5..d44a5ed 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,10 @@ [![NPM downloads](https://img.shields.io/npm/dt/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) [![NPM version](https://img.shields.io/npm/v/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) +--- + Speakeasy is a one-time passcode generator, suitable for use in two-factor -authentication, and supports Google Authenticator and other two-factor apps. +authentication, that supports Google Authenticator and other two-factor apps. It includes robust support for custom token lengths, authentication windows, and other features, and includes helpers like a secret key generator. From 3f0d9dc48473d59424efc485f6a31572be7947c5 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 30 Dec 2015 18:23:02 -0800 Subject: [PATCH 31/59] Updated README with updated usage, buttons, and quick reference --- README.md | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 173 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d44a5ed..1256ee3 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,13 @@ --- +**Jump to** — [Install](#install) · [Demo](#demo) · [Usage](#usage) · [Documentation](#documentation) · [Quick Reference](#quick-reference) · [License](#license) + +--- + Speakeasy is a one-time passcode generator, suitable for use in two-factor authentication, that supports Google Authenticator and other two-factor apps. + It includes robust support for custom token lengths, authentication windows, and other features, and includes helpers like a secret key generator. @@ -17,42 +22,200 @@ Password (HOTP) algorithm defined by [RFC 4226][rfc4226] and the Time-Based One-time Password (TOTP) algorithm defined in [RFC 6238][rfc6238] are supported. + ## Install ```sh npm install --save speakeasy ``` + ## Demo This demo uses the `generate_key` method of Speakeasy to generate a secret key, displays a Google Authenticator–compatible QR code which you can scan into your phone's two-factor app, and shows the token, which you can verify with your -phone. Includes sample code. +phone. Includes sample code. https://sedemo-mktb.rhcloud.com/ - + + ## Usage ```js var speakeasy = require("speakeasy"); +``` + +#### Generating a key + +```js +// Generate a secret key. +var secret = speakeasy.generate_key({length: 20}); +// Access using secret.ascii, secret.hex, or secret.base32. +``` + +#### Getting a time-based token for the current time + +```js +// Generate a time-based token based on the base-32 key. +// HOTP (counter-based tokens) can also be used if `totp` is replaced by +// `hotp` (i.e. speakeasy.hotp()) and a `counter` is given in the options. +var token = speakeasy.totp({ + secret: base32secret +}); + +// Returns token for the secret at the current time +// Compare this to user input +``` + +#### Verifying a token + +```js +// Verify a given token +var tokenValidates = speakeasy.totp.verify({ + secret: base32secret, + token: '123456' +}); +// Returns true if the token matches +``` + +#### Verifying a token and calculating a delta +```js +// Verify a given token and check nearby tokens +var tokenDelta = speakeasy.totp.verifyDelta({ + secret: base32secret, + token: '123456' +}); +// Returns {delta: 0} where the delta is the time step difference +// between the given token and the current time +``` + +#### Calculating a counter-based token + +```js var token = speakeasy.hotp({ - secret: "xyzzy", + secret: base32secret, counter: 123 }); -// token = "378764" -var ok = speakeasy.hotp.verify({ - secret: "xyzzy", +var tokenValidates = speakeasy.hotp.verify({ + secret: secret.base32, token: token, counter: 123 }); -// ok = {delta: 0} ``` + ## Documentation -Full documentation at http://speakeasyjs.github.io/speakeasy/ +A quick reference can be found below. Full API documentation (in JSDoc format) +is available at http://speakeasyjs.github.io/speakeasy/ + + + + + +## Quick Reference + +TODO: Update + +### speakeasy.hotp(options) | speakeasy.counter(options) + +Calculate the one-time password using the counter-based algorithm, HOTP. Specify the key and counter, and receive the one-time password for that counter position. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. + +Written to follow [RFC 4226](http://tools.ietf.org/html/rfc4226). Calculated with: `HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` + +#### Options + +* `secret`: the secret key in ASCII, hexadecimal, or base32 format. +* `counter`: the counter position (moving factor). +* `length` (default `6`): the length of the resulting one-time password. +* `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. + +#### Example + +```javascript +// normal use. +speakeasy.hotp({key: 'secret', counter: 582}); +// => 246642 + +// use a custom length. +speakeasy.hotp({key: 'secret', counter: 582, length: 8}); +// => 67246642 + +// use a custom encoding. +speakeasy.hotp({key: 'AJFIEJGEHIFIU7148SF', counter: 147, encoding: 'base32'}); +// => 974955 +``` + +### speakeasy.totp(options) | speakeasy.time(options) + +Calculate the one-time password using the time-based algorithm, TOTP. Specify the key, and receive the one-time password for that time. By default, the time step is 30 seconds, so there is a new password every 30 seconds. However, you may override the time step. You may also override the time you want to calculate the time from. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. + +Written to follow [RFC 6238](http://tools.ietf.org/html/rfc6238). Calculated with: `C = ((T - T0) / X); HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` + +#### Options + +* `secret`: the secret key in ASCII, hexadecimal, or base32 format. +* `step` (default `30`): the time step, in seconds, between new passwords (moving factor). +* `time` (default current time): the time to calculate the TOTP from, by default the current time. If you're doing something clever with TOTP, you may override this (see *Techniques* below). +* `initial_time` (default `0`): the starting time where we calculate the TOTP from. Usually, this is set to the UNIX epoch at 0. +* `length` (default `6`): the length of the resulting one-time password. +* `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. + +#### Example + +```javascript +// normal use. +speakeasy.totp({key: 'secret'}); + +// use a custom time step. +speakeasy.totp({key: 'secret', step: 60}); + +// use a custom time. +speakeasy.totp({key: 'secret', time: 159183717}); +// => 558014 + +// use a initial time. +speakeasy.totp({key: 'secret', initial_time: 4182881485}); +// => 670417 +``` + +#### Techniques + +You can implement a double-authentication scheme, where you ask the user to input the one-time password once, wait until the next 30-second refresh, and then input the one-time password again. In this case, you can calculate the second (later) input by calculating TOTP as usual, then also verify the first (earlier) input by taking the current epoch time in seconds and subtracting 30 seconds to get to the previous step (for example: `time1 = (parseInt(new Date()/1000) - 30)`) + +### speakeasy.generate_key(options) + +Generate a random secret key. It will return the key in ASCII, hexadecimal, and base32 formats. You can specify the length, whether or not to use symbols, and ask it (nicely) to generate URLs for QR codes. Returns an object with the ASCII, hex, and base32 representations of the secret key, plus any QR codes you can optionally ask for. + +#### Options + +* `length` (default `32`): the length of the generated secret key. +* `symbols` (default `true`): include symbols in the key? if not, the key will be alphanumeric, {A-Z, a-z, 0-9} +* `qr_codes` (default `false`): generate links to QR codes for each encoding (ASCII, hexadecimal, and base32). It uses the Google Charts API and they are served over HTTPS. A future version might allow for QR code generation client-side for security. +* `google_auth_qr` (default `false`): generate a link to a QR code that you can scan using the Google Authenticator app. The contents of the QR code are in this format: `otpauth://totp/[KEY NAME]?secret=[KEY SECRET, BASE 32]`. +* `name` (optional): specify a name when you are using `google_auth_qr`, which will show up as the label after scanning. `[KEY NAME]` in the previous line. + +#### Examples + +```javascript +// generate a key +speakeasy.generate_key({length: 20, symbols: true}); +// => { ascii: 'km^A?n&sOPJW.iCKPHKU', hex: '6b6d5e413f6e26734f504a572e69434b50484b55', base32: 'NNWV4QJ7NYTHGT2QJJLS42KDJNIEQS2V' } + +// generate a key and request QR code links +speakeasy.generate_key({length: 20, qr_codes: true}); +// => { ascii: 'eV:JQ1NedJkKn&]6^i>s', ... (truncated) +// qr_code_ascii: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=eV%3AJQ1NedJkKn%26%5D6%5Ei%3Es', +// qr_code_hex: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=65563a4a51314e65644a6b4b6e265d365e693e73', +// qr_code_base32: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=MVLDUSSRGFHGKZCKNNFW4JS5GZPGSPTT' } + +// generate a key and get a QR code you can scan with the Google Authenticator app +speakeasy.generate_key({length: 20, google_auth_qr: true}); +// => { ascii: 'V?9f6.Cq1& Date: Wed, 30 Dec 2015 18:56:16 -0800 Subject: [PATCH 32/59] [fix] Resolved some API breakage with key/secret and epoch/initial_time --- index.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 6f3ea0d..7653d92 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,8 @@ var url = require("url"); * 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. */ @@ -30,6 +32,9 @@ exports.digest = function digest (options) { var encoding = options.encoding || "ascii"; var algorithm = (options.algorithm || "sha1").toLowerCase(); + // Backwards compatibility - deprecated + if (options.key) key = options.key; + // convert key to buffer if (!Buffer.isBuffer(key)) { key = encoding == "base32" ? base32.decode(key) @@ -71,14 +76,22 @@ exports.digest = function digest (options) { * 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 options + // unpack digits var digits = options.digits || 6; + // Backwards compatibility (Deprecated) + // unpack length, if it exists + if (options.length) digits = options.length; + // digest the options var digest = options.digest || exports.digest(options); @@ -99,7 +112,7 @@ exports.hotp = function hotpGenerate (options) { }; /** - * Verify a counter-based One Time passcode. + * Verify a counter-based One Time passcode and return the delta. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -153,6 +166,9 @@ exports.hotp.verify = function hotpVerify (options) { * @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 `Date.now()`. + * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.) + * Initial time since the UNIX epoch from which to calculate the counter + * value. Defaults to `Date.now()`. * @return {Integer} The calculated counter value * @private */ @@ -161,6 +177,7 @@ exports._counter = function _counter (options) { var step = options.step || 30; var time = options.time != null ? options.time : Date.now(); var epoch = options.epoch || 0; + if (options.initial_time) epoch = initial_time; // Deprecated return Math.floor((time - epoch) / step / 1000); }; @@ -180,6 +197,13 @@ exports._counter = function _counter (options) { * 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.epoch=0] (DEPRECATED. Use `epoch` instead.) Initial + * time since the UNIX epoch from which to calculate the counter value. + * Defaults to `Date.now()`. + * @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The + * number of digits for the one-time passcode. * @return {String} The one-time passcode. */ @@ -245,7 +269,7 @@ exports.totp.verify = function totpVerify (options) { /** * Generate an URL for use with the Google Authenticator app. * - * Authenticator considers TOTP codes valid for 30 seconds. Additionally, + * 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. From a98479d022f681aa3c386fe5473da35a32be6868 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 30 Dec 2015 19:24:04 -0800 Subject: [PATCH 33/59] [refactor] Moved verify to verifyDelta and added verify to check truthiness --- index.js | 78 ++++++++++++++++++++++++++++++++++++++++++++--- test/notp_test.js | 56 +++++++++++++++++++++++++--------- 2 files changed, 114 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 7653d92..4b64561 100644 --- a/index.js +++ b/index.js @@ -135,7 +135,7 @@ exports.hotp = function hotpGenerate (options) { * @global */ -exports.hotp.verify = function hotpVerify (options) { +exports.hotp.verifyDelta = function hotpVerifyDelta (options) { var i; // shadow options @@ -158,6 +158,38 @@ exports.hotp.verify = function hotpVerify (options) { // no codes have matched }; +/** + * Verify a counter-based One Time passcode via strict comparison (i.e. + * delta = 0). + * + * @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=50] 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 token strictly matches (delta = 0), + * false otherwise. + * @method hotp․verify + * @global + */ +exports.hotp.verify = function hotpVerify (options) { + // Check against verifyDelta + var verify = exports.hotp.verifyDelta(options); + console.log(verify) + + return (verify && typeof verify.delta !== 'undefined' && verify.delta === 0); +} + /** * Calculate counter value based on given options. * @@ -220,7 +252,7 @@ exports.totp = function totpGenerate (options) { }; /** - * Verify a time-based One Time passcode. + * Verify a time-based One Time passcode and return the delta. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -247,7 +279,7 @@ exports.totp = function totpGenerate (options) { * @global */ -exports.totp.verify = function totpVerify (options) { +exports.totp.verifyDelta = function totpVerifyDelta (options) { // shadow options options = Object.create(options); @@ -262,10 +294,46 @@ exports.totp.verify = function totpVerify (options) { options.counter -= window; options.window += window; - // pass to hotp.verify - return exports.hotp.verify(options); + // pass to hotp.verifyDelta + return exports.hotp.verifyDelta(options); }; +/** + * Verify a time-based One Time passcode via strict comparison (i.e. + * delta = 0). + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} [options.time] Time with which to calculate counter value + * @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 `Date.now()`. + * @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=6] 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 token strictly matches (delta = 0), + * false otherwise. + * @method totp․verify + * @global + */ +exports.totp.verify = function totpVerify (options) { + // Check against verifyDelta + var verify = exports.totp.verifyDelta(options); + + // Return true if delta is 0 (exact) + return (verify && typeof verify.delta !== 'undefined' && verify.delta === 0); +} + /** * Generate an URL for use with the Google Authenticator app. * diff --git a/test/notp_test.js b/test/notp_test.js index cea3dca..842ba26 100644 --- a/test/notp_test.js +++ b/test/notp_test.js @@ -65,10 +65,15 @@ it("HOTP", function() { for(i=0;i= 9'); - // counterheck that test should pass for negative counter values - token = '755224'; - options.counter = 7 - options.window = 8; - assert.ok(speakeasy.hotp.verify(options), 'Should pass for negative counter values'); + // counterheck that test should pass for negative counter values + token = '755224'; + options.counter = 7 + options.window = 8; + assert.ok(speakeasy.hotp.verify(options), 'Should pass for negative counter values'); }); From 407996b1184f792afdbe285c345772a8fa0e7749 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 30 Dec 2015 19:57:22 -0800 Subject: [PATCH 34/59] [new] Moved over generate_key and updated documentation, moved url to google_auth_url --- index.js | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 4b64561..cea79c5 100644 --- a/index.js +++ b/index.js @@ -334,6 +334,105 @@ exports.totp.verify = function totpVerify (options) { return (verify && typeof verify.delta !== 'undefined' && verify.delta === 0); } +/** + * @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. + */ + +/** + * 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. + * @param {Object} options + * @param {Integer} [options.length=32] Length of the secret + * @param {Boolean} [options.symbols=false] Whether to include symbols + * @param {Boolean} [options.qr_codes=false] Whether to output QR code URLs + * @param {Boolean} [options.google_auth_qr=false] Whether to output a Google + * Authenticator otpauth:// QR code URL (returns the URL to the QR code) + * @param {Boolean} [options.google_auth_url=true] Whether to output a Google + * Authenticator otpauth:// URL (only returns otpauth:// URL, no QR code) + * @param {String} [options.name] The name to use with Google Authenticator. + * @return {Object} + * @return {GeneratedSecret} The generated secret key. + */ +exports.generate_key = function generateKey (options) { + // options + if(!options) options = {}; + var length = options.length || 32; + var name = options.name || "Secret Key"; + var qr_codes = options.qr_codes || false; + var google_auth_qr = options.google_auth_qr || false; + var google_auth_url = options.google_auth_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.generate_key_ascii(length, symbols); + + // return a SecretKey with ascii, hex, and base32 + var SecretKey = {}; + SecretKey.ascii = key; + SecretKey.hex = this.ascii_to_hex(key); + SecretKey.base32 = base32.encode(key).toString().replace(/=/g,''); + + // generate some qr codes if requested + if (qr_codes) { + 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); + } + + if (google_auth_url) { + SecretKey.google_auth_url = exports.google_auth_url({ + secret: key, + label: name + }); + } + + // generate a QR code for use in Google Authenticator if requested + // (Google Authenticator has a special style and requires base32) + if (google_auth_qr) { + // first, make sure that the name doesn't have spaces, since Google Authenticator doesn't like them + name = name.replace(/ /g,''); + SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://totp/' + encodeURIComponent(name) + '%3Fsecret=' + encodeURIComponent(SecretKey.base32); + } + + return SecretKey; +}; + +/** + * 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.generate_key_ascii = function(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[~~(bytes[i] / 0xFF * set.length)]; + } + return output; +}; + /** * Generate an URL for use with the Google Authenticator app. * @@ -347,12 +446,12 @@ exports.totp.verify = function totpVerify (options) { * * @param {Object} options * @param {String} options.secret Shared secret key - * @param {Integer} options.label Used to identify the account with which + * @param {String} options.label Used to identify the account with which * the secret key is associated, e.g. the user's email address. - * @param {Integer} [options.type="totp"] Either "hotp" or "totp". + * @param {String} [options.type="totp"] Either "hotp" or "totp". * @param {Integer} [options.counter] The initial counter value, required * for HOTP. - * @param {Integer} [options.issuer] The provider or service with which the + * @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). @@ -367,7 +466,7 @@ exports.totp.verify = function totpVerify (options) { * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format */ -exports.url = function (options) { +exports.google_auth_url = function (options) { // unpack options var secret = options.secret; From d58fdce819f123eea89530bbb501bbe53e72f824 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Wed, 30 Dec 2015 19:58:13 -0800 Subject: [PATCH 35/59] [fix] Updated tests to reflect move from url to google_auth_url --- test/url_test.js | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/url_test.js b/test/url_test.js index 65f8a4b..d86a1f5 100644 --- a/test/url_test.js +++ b/test/url_test.js @@ -8,13 +8,13 @@ describe("#url", function () { it("should require options", function () { assert.throws(function () { - speakeasy.url(); + speakeasy.google_auth_url(); }); }); it("should validate type", function () { assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ type: "haha", secret: "hello", label: "that", @@ -24,7 +24,7 @@ describe("#url", function () { it("should require secret", function () { assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ label: "that" }, /missing secret/); }); @@ -32,7 +32,7 @@ describe("#url", function () { it("should require label", function () { assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ secret: "hello" }, /missing label/); }); @@ -40,19 +40,19 @@ describe("#url", function () { it("should require counter for HOTP", function () { assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ type: "hotp", secret: "hello", label: "that" }, /missing counter/); }); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ type: "hotp", secret: "hello", label: "that", counter: 0 })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ type: "hotp", secret: "hello", label: "that", @@ -62,23 +62,23 @@ describe("#url", function () { it("should validate algorithm", function () { assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ secret: "hello", label: "that", algorithm: "hello" }, /invalid algorithm `hello`/); }); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", algorithm: "sha1" })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", algorithm: "sha256" })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", algorithm: "sha512" @@ -87,42 +87,42 @@ describe("#url", function () { it("should validate digits", function () { assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ secret: "hello", label: "that", digits: "hello" }, /invalid digits `hello`/); }); assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ secret: "hello", label: "that", digits: 12 }, /invalid digits `12`/); }); assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ secret: "hello", label: "that", digits: "7" }, /invalid digits `7`/); }); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", digits: 6 })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", digits: 8 })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", digits: "6" })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", digits: "8" @@ -131,28 +131,28 @@ describe("#url", function () { it("should validate period", function () { assert.throws(function () { - speakeasy.url({ + speakeasy.google_auth_url({ secret: "hello", label: "that", period: "hello" }, /invalid period `hello`/); }); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", period: 60 })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", period: 121 })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", period: "60" })); - assert.ok(speakeasy.url({ + assert.ok(speakeasy.google_auth_url({ secret: "hello", label: "that", period: "121" @@ -160,7 +160,7 @@ describe("#url", function () { }); it("should generate an URL compatible with the Google Authenticator app", function () { - var answer = speakeasy.url({ + var answer = speakeasy.google_auth_url({ secret: "JBSWY3DPEHPK3PXP", label: "Example:alice@google.com", issuer: "Example", From 7105f4a9f7d7174cd0f32aef93f992aa1148c823 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 2 Jan 2016 19:13:28 -0800 Subject: [PATCH 36/59] [fix] Minor fixes for review comments for updated API --- index.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index cea79c5..51d11c7 100644 --- a/index.js +++ b/index.js @@ -86,11 +86,8 @@ exports.digest = function digest (options) { exports.hotp = function hotpGenerate (options) { // unpack digits - var digits = options.digits || 6; - - // Backwards compatibility (Deprecated) - // unpack length, if it exists - if (options.length) digits = options.length; + // backward compatibility: `length` is also accepted here, but deprecated + var digits = (options.digits != null ? options.digits : options.length) || 6; // digest the options var digest = options.digest || exports.digest(options); @@ -185,7 +182,6 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) { exports.hotp.verify = function hotpVerify (options) { // Check against verifyDelta var verify = exports.hotp.verifyDelta(options); - console.log(verify) return (verify && typeof verify.delta !== 'undefined' && verify.delta === 0); } @@ -208,8 +204,10 @@ exports.hotp.verify = function hotpVerify (options) { exports._counter = function _counter (options) { var step = options.step || 30; var time = options.time != null ? options.time : Date.now(); - var epoch = options.epoch || 0; - if (options.initial_time) epoch = initial_time; // Deprecated + + // also accepts 'initial_time', but deprecated + var epoch = (options.epoch != null ? options.epoch : options.initial_time) || 0; + return Math.floor((time - epoch) / step / 1000); }; From 03ef53b7f180da6bd668066a6133659478f84c3c Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 2 Jan 2016 19:33:28 -0800 Subject: [PATCH 37/59] [maint] Updated LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 0d5612d..49d1fee 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) +Copyright (c) 2012-2016 Mark Bao Copyright (c) 2015 Michael Phan-Ba -Copyright (c) 2012-2013 Mark Bao Copyright (c) 2011 Guy Halford-Thompson Permission is hereby granted, free of charge, to any person obtaining a copy From fcbf999fd9cbd08d16fe7a54abb6b9448d79526d Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 2 Jan 2016 19:46:47 -0800 Subject: [PATCH 38/59] [fix] Fixed minor documentation bugs --- index.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 51d11c7..107356b 100644 --- a/index.js +++ b/index.js @@ -190,13 +190,14 @@ exports.hotp.verify = function hotpVerify (options) { * Calculate counter value based on given options. * * @param {Object} options - * @param {Integer} [options.time] Time with which to calculate counter value + * @param {Integer} [options.time] Time 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 `Date.now()`. + * which to calculate the counter value. Defaults to 0 (no offset). * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.) * Initial time since the UNIX epoch from which to calculate the counter - * value. Defaults to `Date.now()`. + * value. Defaults to 0 (no offset). * @return {Integer} The calculated counter value * @private */ @@ -216,10 +217,11 @@ exports._counter = function _counter (options) { * * @param {Object} options * @param {String} options.secret Shared secret key - * @param {Integer} [options.time] Time with which to calculate counter value + * @param {Integer} [options.time] Time 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 `Date.now()`. + * 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. @@ -229,9 +231,9 @@ exports._counter = function _counter (options) { * sha512). * @param {String} options.key (DEPRECATED. Use `secret` instead.) * Shared secret key - * @param {Integer} [options.epoch=0] (DEPRECATED. Use `epoch` instead.) Initial - * time since the UNIX epoch from which to calculate the counter value. - * Defaults to `Date.now()`. + * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.) + * Initial time 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. @@ -255,10 +257,11 @@ exports.totp = function totpGenerate (options) { * @param {Object} options * @param {String} options.secret Shared secret key * @param {String} options.token Passcode to validate - * @param {Integer} [options.time] Time with which to calculate counter value + * @param {Integer} [options.time] Time 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 `Date.now()`. + * 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. @@ -303,10 +306,11 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) { * @param {Object} options * @param {String} options.secret Shared secret key * @param {String} options.token Passcode to validate - * @param {Integer} [options.time] Time with which to calculate counter value + * @param {Integer} [options.time] Time 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 `Date.now()`. + * 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. From d945e79dbea1f36e6946858055c6fed810638ff5 Mon Sep 17 00:00:00 2001 From: Michael Phan-Ba Date: Fri, 22 Jan 2016 19:09:27 -0800 Subject: [PATCH 39/59] Update default window to 0 --- README.md | 15 ++++++++++++--- index.js | 34 +++++++++++++--------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1256ee3..312df87 100644 --- a/README.md +++ b/README.md @@ -74,17 +74,26 @@ var token = speakeasy.totp({ // Verify a given token var tokenValidates = speakeasy.totp.verify({ secret: base32secret, - token: '123456' + token: '123456', + window: 6 }); // Returns true if the token matches ``` #### Verifying a token and calculating a delta + +A TOTP is incremented every `step` time-step seconds. By default, the time-step +is 30 seconds. You may change the time-step using the `step` option, with units +in seconds. + ```js -// Verify a given token and check nearby tokens +// Verify a given token is within 3 time-steps (+/- 2 minutes) from the server +// time-step. var tokenDelta = speakeasy.totp.verifyDelta({ secret: base32secret, - token: '123456' + token: '123456', + window: 2, + step: 60 }); // Returns {delta: 0} where the delta is the time step difference // between the given token and the current time diff --git a/index.js b/index.js index 107356b..75c4843 100644 --- a/index.js +++ b/index.js @@ -118,7 +118,7 @@ exports.hotp = function hotpGenerate (options) { * 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=50] The allowable margin for the counter. + * @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. @@ -128,7 +128,7 @@ exports.hotp = function hotpGenerate (options) { * sha512). * @return {Object} On success, returns an object with the counter * difference between the client and the server as the `delta` property. - * @method hotp․verify + * @method hotp․verifyDelta * @global */ @@ -140,8 +140,8 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) { // unpack options var token = options.token; - var window = options.window || 50; - var counter = options.counter || 0; + var window = parseInt(options.window || 0, 10); + var counter = parseInt(options.counter || 0, 10); // loop from C to C + W for (i = counter; i <= counter + window; ++i) { @@ -156,8 +156,7 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) { }; /** - * Verify a counter-based One Time passcode via strict comparison (i.e. - * delta = 0). + * Verify a counter-based One Time passcode. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -166,7 +165,7 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) { * 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=50] The allowable margin for the counter. + * @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. @@ -174,16 +173,13 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) { * base32, base64). * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, * sha512). - * @return {Boolean} Returns true if token strictly matches (delta = 0), - * false otherwise. + * @return {Boolean} Returns true if the token matches within the configured + * window, false otherwise. * @method hotp․verify * @global */ exports.hotp.verify = function hotpVerify (options) { - // Check against verifyDelta - var verify = exports.hotp.verifyDelta(options); - - return (verify && typeof verify.delta !== 'undefined' && verify.delta === 0); + return exports.hotp.verifyDelta(options) != null; } /** @@ -265,7 +261,7 @@ exports.totp = function totpGenerate (options) { * @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=6] The allowable margin for the counter. + * @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, @@ -276,7 +272,7 @@ exports.totp = function totpGenerate (options) { * sha512). * @return {Object} On success, returns an object with the time step * difference between the client and the server as the `delta` property. - * @method totp․verify + * @method totp․verifyDelta * @global */ @@ -314,7 +310,7 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) { * @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=6] The allowable margin for the counter. + * @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, @@ -329,11 +325,7 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) { * @global */ exports.totp.verify = function totpVerify (options) { - // Check against verifyDelta - var verify = exports.totp.verifyDelta(options); - - // Return true if delta is 0 (exact) - return (verify && typeof verify.delta !== 'undefined' && verify.delta === 0); + return exports.totp.verifyDelta(options) != null; } /** From bc6eb668fff31980e74eab6baf24921ce16be5d3 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Fri, 22 Jan 2016 22:58:14 -0500 Subject: [PATCH 40/59] [fix] Tests - Fixed typos --- test/notp_test.js | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/notp_test.js b/test/notp_test.js index 842ba26..ea73e95 100644 --- a/test/notp_test.js +++ b/test/notp_test.js @@ -57,15 +57,15 @@ it("HOTP", function() { options.token = 'WILL NOT PASS'; speakeasy.hotp.verify(options); - // counterheck for failure + // countercheck for failure options.counter = 0; assert.ok(!speakeasy.hotp.verify(options), 'Should not pass'); - // counterheck for passes + // countercheck for passes for(i=0;i= 9 + // countercheck that the test should pass for window >= 9 options.window = 8; assert.ok(speakeasy.hotp.verify(options), 'Should pass for value of window >= 9'); - // counterheck that test should pass for negative counter values + // countercheck that test should pass for negative counter values token = '755224'; options.counter = 7 options.window = 8; @@ -177,7 +177,7 @@ it("HOTPOutOfSync", function() { /* - * counterheck for codes that are out of sync + * countercheck for codes that are out of sync * windowe are going to use a value of T = 1999999909 (91s behind 2000000000) */ @@ -189,11 +189,11 @@ it("TOTPOutOfSync", function() { time: 1999999909000 }; - // counterheck that the test should fail for window < 2 + // countercheck that the test should fail for window < 2 options.window = 2; assert.ok(!speakeasy.totp.verify(options), 'Should not pass for value of window < 3'); - // counterheck that the test should pass for window >= 3 + // countercheck that the test should pass for window >= 3 options.window = 3; assert.ok(speakeasy.totp.verify(options), 'Should pass for value of window >= 3'); }); @@ -210,7 +210,7 @@ it("hotp_gen", function() { // make sure we can not pass in opt speakeasy.hotp(options); - // counterheck for passes + // countercheck for passes for(i=0;i Date: Fri, 22 Jan 2016 23:06:44 -0500 Subject: [PATCH 41/59] [new] Add code coverage with Istanbul and a command for it --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5c4f602..6511c53 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,12 @@ ], "devDependencies": { "jsdoc": "^3.3.1", - "mocha": "^2.2.5" + "mocha": "^2.2.5", + "istanbul": "^0.4.2" }, "scripts": { "test": "mocha", - "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/speakeasy/*/*.html" + "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/speakeasy/*/*.html", + "cover": "istanbul cover _mocha -- test/* -R spec" } } From bbeb9e3a1f73f555b26e0610ebbca5303fb6c09a Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Fri, 22 Jan 2016 23:12:16 -0500 Subject: [PATCH 42/59] [new] Add style checking with Semistandard --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6511c53..82e1027 100644 --- a/package.json +++ b/package.json @@ -50,11 +50,14 @@ "devDependencies": { "jsdoc": "^3.3.1", "mocha": "^2.2.5", - "istanbul": "^0.4.2" + "istanbul": "^0.4.2", + "semistandard": "^7.0.5", + "snazzy": "^2.0.1" }, "scripts": { "test": "mocha", "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/speakeasy/*/*.html", - "cover": "istanbul cover _mocha -- test/* -R spec" + "cover": "istanbul cover _mocha -- test/* -R spec", + "lint": "semistandard --verbose | snazzy" } } From 2dedddca27f589ec5a003f4c4a222bcc4210f19b Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 00:12:58 -0500 Subject: [PATCH 43/59] [fix] Generate - Fixed key generation random ASCII string generator --- index.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 75c4843..7813ca4 100644 --- a/index.js +++ b/index.js @@ -422,11 +422,25 @@ exports.generate_key_ascii = function(length, symbols) { var output = ''; for (var i = 0, l = bytes.length; i < l; i++) { - output += set[~~(bytes[i] / 0xFF * set.length)]; + output += set[~~(bytes[i] / 0xFF * (set.length-1))]; } return output; }; +// speakeasy.ascii_to_hex(key) +// +// helper function to convert an ascii key to hex. +// +exports.ascii_to_hex = function(str) { + var hex_string = ''; + + for (var i = 0; i < str.length; i++) { + hex_string += str.charCodeAt(i).toString(16); + } + + return hex_string; +}; + /** * Generate an URL for use with the Google Authenticator app. * From 145573d4d5a376a19046e124891acffd06d15070 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 00:42:27 -0500 Subject: [PATCH 44/59] [fix] Generate - Use correct key format when returning Google Auth URL --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 7813ca4..59b65be 100644 --- a/index.js +++ b/index.js @@ -389,7 +389,7 @@ exports.generate_key = function generateKey (options) { if (google_auth_url) { SecretKey.google_auth_url = exports.google_auth_url({ - secret: key, + secret: SecretKey.hex, label: name }); } From 7a56b6920c53f5f16a6d89a93b8d254d2aa2105d Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 00:48:47 -0500 Subject: [PATCH 45/59] [fix] Generate - Fix google_auth_url default setting and override --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 59b65be..046a76c 100644 --- a/index.js +++ b/index.js @@ -360,10 +360,10 @@ exports.generate_key = function generateKey (options) { // options if(!options) options = {}; var length = options.length || 32; - var name = options.name || "Secret Key"; + var name = options.name || "SecretKey"; var qr_codes = options.qr_codes || false; var google_auth_qr = options.google_auth_qr || false; - var google_auth_url = options.google_auth_url || true; + var google_auth_url = options.google_auth_url != null ? options.google_auth_url : true; var symbols = true; // turn off symbols only when explicity told to From ae9e35db5c2d0180fa1ac1decd9b53a7290b4e69 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 01:36:32 -0500 Subject: [PATCH 46/59] [fix] TOTP - Make unpacking options consistent with HOTP --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 046a76c..e0f75ec 100644 --- a/index.js +++ b/index.js @@ -282,7 +282,7 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) { options = Object.create(options); // unpack options - var window = options.window != null ? options.window : 0; + var window = parseInt(options.window || 0, 10); // calculate default counter value if (options.counter == null) options.counter = exports._counter(options); From cae925680a72a876218349f926903ed2ba73b943 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 01:37:01 -0500 Subject: [PATCH 47/59] [test] TOTP - Adding test for TOTP with custom counter --- test/notp_test.js | 12 ++++++++++++ test/totp_test.js | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/test/notp_test.js b/test/notp_test.js index ea73e95..427bac4 100644 --- a/test/notp_test.js +++ b/test/notp_test.js @@ -143,6 +143,18 @@ it("TOTtoken", function() { // countercheck for test vector at 2000000000 with verify var res = speakeasy.totp.verify(options); assert.ok(res, 'Should pass'); + + // countercheck for test vector at 1234567890 with custom counter with delta + options.token = '005924'; + options.counter = 41152263; + var res = speakeasy.totp.verifyDelta(options); + assert.ok(res, 'Should pass'); + assert.equal(res.delta, 0, 'Should be in sync'); + + // countercheck for test vector at 1234567890 with verify + var res = speakeasy.totp.verify(options); + assert.ok(res, 'Should pass'); + }); diff --git a/test/totp_test.js b/test/totp_test.js index b6f9ec6..3c1138d 100644 --- a/test/totp_test.js +++ b/test/totp_test.js @@ -16,6 +16,13 @@ describe('TOTP Time-Based Algorithm Test', function () { }); }); + describe('normal operation with secret = \'12345678901234567890\' at time = 59000 using key (deprecated)', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.totp({key: '12345678901234567890', time: 59000}); + assert.equal(topic, '287082'); + }); + }); + describe('a different time normal operation with secret = \'12345678901234567890\' at time = 1111111109000', function () { it('should return correct one-time password', function () { var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000}); @@ -79,4 +86,18 @@ describe('TOTP Time-Based Algorithm Test', function () { }); }); + describe('normal operation with secret = \'12345678901234567890\' with overridden counter 3', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.totp({secret: '12345678901234567890', counter: 3}); + assert.equal(topic, '969429'); + }); + }); + + describe('normal operation with secret = \'12345678901234567890\' with overridden counter 3', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.totp({secret: '12345678901234567890', counter: 3}); + assert.equal(topic, '969429'); + }); + }); + }); From 1720db5ad702b97bffddf5dd5388fc3e0af36d0d Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 01:38:27 -0500 Subject: [PATCH 48/59] [test] Generate - Added tests for generate and added Chai --- package.json | 1 + test/generate.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 test/generate.js diff --git a/package.json b/package.json index 82e1027..7944e69 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "passwords" ], "devDependencies": { + "chai": "^3.4.1", "jsdoc": "^3.3.1", "mocha": "^2.2.5", "istanbul": "^0.4.2", diff --git a/test/generate.js b/test/generate.js new file mode 100644 index 0000000..c59f1da --- /dev/null +++ b/test/generate.js @@ -0,0 +1,61 @@ +"use scrict"; + +var chai = require('chai'); +var speakeasy = require('..'); +var assert = chai.assert; + +// These tests use the information from RFC 4226's Appendix D: Test Values. +// http://tools.ietf.org/html/rfc4226#appendix-D + +describe('Generator tests', function () { + + it('Normal generation with defaults', function () { + var secret = speakeasy.generate_key(); + assert.equal(secret.ascii.length, 32, 'Should return the correct length'); + assert.isDefined(secret.google_auth_url, 'Google Auth URL should be returned') + assert.isUndefined(secret.qr_code_ascii, 'QR Code ASCII should not be returned') + assert.isUndefined(secret.qr_code_hex, 'QR Code Hex should not be returned') + assert.isUndefined(secret.qr_code_base32, 'QR Code Base 32 should not be returned') + assert.isUndefined(secret.google_auth_qr, 'Google Auth QR should not be returned') + }); + + it('Generation with custom key length', function () { + var secret = speakeasy.generate_key({length: 50}); + assert.equal(secret.ascii.length, 50, 'Should return the correct length'); + }); + + it('Generation with symbols disabled', function () { + var secret = speakeasy.generate_key({symbols: false}); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Generation with QR URL output enabled', function () { + var secret = speakeasy.generate_key({qr_codes: true}); + assert.isDefined(secret.qr_code_ascii, 'QR Code ASCII should be returned') + assert.isDefined(secret.qr_code_hex, 'QR Code Hex should be returned') + assert.isDefined(secret.qr_code_base32, 'QR Code Base 32 should be returned') + }); + + it('Generation with Google Auth URL output disabled', function () { + var secret = speakeasy.generate_key({google_auth_url: false}); + assert.isUndefined(secret.google_auth_url, 'Google Auth URL should not be returned') + }); + + it('Generation with Google Auth QR URL output enabled', function () { + var secret = speakeasy.generate_key({google_auth_qr: true}); + assert.isDefined(secret.google_auth_qr, 'Google Auth QR should be returned') + }); + + it('Testing generate_key_ascii with defaults', function () { + var secret = speakeasy.generate_key_ascii(); + assert.equal(secret.length, 32, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Testing generate_key_ascii with custom length', function () { + var secret = speakeasy.generate_key_ascii(20); + assert.equal(secret.length, 20, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + +}); From 15ef6d036dd64d922e4e81d687e0c8ae980c5548 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 01:42:17 -0500 Subject: [PATCH 49/59] [proj] Added Coveralls and updated Travis script --- .travis.yml | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4ad0861..61da3b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,4 @@ node_js: - "4.1" - "4.0" - "0.12" +after_script: 'istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage' diff --git a/package.json b/package.json index 7944e69..4067135 100644 --- a/package.json +++ b/package.json @@ -49,9 +49,10 @@ ], "devDependencies": { "chai": "^3.4.1", + "coveralls": "^2.11.6", + "istanbul": "^0.4.2", "jsdoc": "^3.3.1", "mocha": "^2.2.5", - "istanbul": "^0.4.2", "semistandard": "^7.0.5", "snazzy": "^2.0.1" }, From 5e4a96f0c36d00a36b2d27febdce7f1591b3eba1 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 01:45:45 -0500 Subject: [PATCH 50/59] [readme] Added code coverage badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 312df87..a632a88 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.org/speakeasyjs/speakeasy.svg?branch=v2)](https://travis-ci.org/speakeasyjs/speakeasy) [![NPM downloads](https://img.shields.io/npm/dt/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) +[![Coverage Status](https://coveralls.io/repos/github/speakeasyjs/speakeasy/badge.svg?branch=v2)](https://coveralls.io/github/speakeasyjs/speakeasy?branch=v2) [![NPM version](https://img.shields.io/npm/v/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) --- From a98ff494d48bfa71ba9b73e668647960bf693503 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 01:57:42 -0500 Subject: [PATCH 51/59] [test] Update tests to use Chai and move passcode -> speakeasy --- test/generate.js | 2 +- test/hotp_test.js | 3 ++- test/notp_test.js | 3 ++- test/rfc4226_test.js | 9 +++++---- test/rfc6238_test.js | 11 ++++++----- test/totp_test.js | 3 ++- test/url_test.js | 3 ++- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/test/generate.js b/test/generate.js index c59f1da..35a90cb 100644 --- a/test/generate.js +++ b/test/generate.js @@ -1,8 +1,8 @@ "use scrict"; var chai = require('chai'); -var speakeasy = require('..'); var assert = chai.assert; +var speakeasy = require('..'); // These tests use the information from RFC 4226's Appendix D: Test Values. // http://tools.ietf.org/html/rfc4226#appendix-D diff --git a/test/hotp_test.js b/test/hotp_test.js index 6a04627..c8005a6 100644 --- a/test/hotp_test.js +++ b/test/hotp_test.js @@ -1,6 +1,7 @@ "use scrict"; -var assert = require('assert'); +var chai = require('chai'); +var assert = chai.assert; var speakeasy = require('..'); // These tests use the information from RFC 4226's Appendix D: Test Values. diff --git a/test/notp_test.js b/test/notp_test.js index 427bac4..7e24740 100644 --- a/test/notp_test.js +++ b/test/notp_test.js @@ -1,6 +1,7 @@ "use scrict"; -var assert = require('assert'); +var chai = require('chai'); +var assert = chai.assert; var speakeasy = require('..'); /* diff --git a/test/rfc4226_test.js b/test/rfc4226_test.js index 26ff568..a9c8e1d 100644 --- a/test/rfc4226_test.js +++ b/test/rfc4226_test.js @@ -1,7 +1,8 @@ "use strict"; -var assert = require('assert'); -var passcode = require('..'); +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); /* @@ -58,7 +59,7 @@ describe("RFC 4226 test values", function () { "1637409809a679dc698207310c8c7fc07290d9e5" ].forEach(function (expect, count) { it("should match for counter = " + count, function () { - var hash = passcode.digest({ + var hash = speakeasy.digest({ secret: "12345678901234567890", counter: count }).toString("hex"); @@ -81,7 +82,7 @@ describe("RFC 4226 test values", function () { "520489" ].forEach(function (expect, count) { it("should match for count = " + count, function () { - var code = passcode.hotp({ + var code = speakeasy.hotp({ secret: "12345678901234567890", counter: count }); diff --git a/test/rfc6238_test.js b/test/rfc6238_test.js index 5645de2..7008521 100644 --- a/test/rfc6238_test.js +++ b/test/rfc6238_test.js @@ -1,7 +1,8 @@ "use strict"; -var assert = require('assert'); -var passcode = require('..'); +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); /* @@ -190,21 +191,21 @@ describe("RFC 6238 test vector", function () { } it("should calculate counter value for time " + subject.time, function () { - var counter = passcode._counter({ + var counter = speakeasy._counter({ time: subject.time }); assert.equal(counter, subject.counter); }); it("should calculate counter value for date " + subject.date, function () { - var counter = passcode._counter({ + var counter = speakeasy._counter({ time: subject.date }); assert.equal(counter, subject.counter); }); it("should generate TOTP code for time " + subject.time + " and algorithm " + subject.algorithm, function () { - var counter = passcode.totp({ + var counter = speakeasy.totp({ secret: key, time: subject.time, algorithm: subject.algorithm, diff --git a/test/totp_test.js b/test/totp_test.js index 3c1138d..e06331d 100644 --- a/test/totp_test.js +++ b/test/totp_test.js @@ -1,6 +1,7 @@ "use scrict"; -var assert = require('assert'); +var chai = require('chai'); +var assert = chai.assert; var speakeasy = require('..'); // These tests use the test vectors from RFC 6238's Appendix B: Test Vectors diff --git a/test/url_test.js b/test/url_test.js index d86a1f5..c2b5e24 100644 --- a/test/url_test.js +++ b/test/url_test.js @@ -1,6 +1,7 @@ "use scrict"; -var assert = require('assert'); +var chai = require('chai'); +var assert = chai.assert; var speakeasy = require('..'); var url = require("url"); From 4cef296907038a17403e54770cef1f5c582f1ab4 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 02:10:08 -0500 Subject: [PATCH 52/59] [test] Added global declaration (required for standard), fixed strict typo, fixed missing var errors --- test/generate.js | 4 +++- test/hotp_test.js | 4 +++- test/notp_test.js | 10 ++++++---- test/rfc4226_test.js | 2 ++ test/rfc6238_test.js | 2 ++ test/totp_test.js | 4 +++- test/url_test.js | 4 +++- 7 files changed, 22 insertions(+), 8 deletions(-) diff --git a/test/generate.js b/test/generate.js index 35a90cb..1790c49 100644 --- a/test/generate.js +++ b/test/generate.js @@ -1,4 +1,6 @@ -"use scrict"; +"use strict"; + +/* global describe, it */ var chai = require('chai'); var assert = chai.assert; diff --git a/test/hotp_test.js b/test/hotp_test.js index c8005a6..73eb4d0 100644 --- a/test/hotp_test.js +++ b/test/hotp_test.js @@ -1,4 +1,6 @@ -"use scrict"; +"use strict"; + +/* global describe, it */ var chai = require('chai'); var assert = chai.assert; diff --git a/test/notp_test.js b/test/notp_test.js index 7e24740..7da78ad 100644 --- a/test/notp_test.js +++ b/test/notp_test.js @@ -1,4 +1,6 @@ -"use scrict"; +"use strict"; + +/* global describe, it */ var chai = require('chai'); var assert = chai.assert; @@ -63,7 +65,7 @@ it("HOTP", function() { assert.ok(!speakeasy.hotp.verify(options), 'Should not pass'); // countercheck for passes - for(i=0;i= 9'); // countercheck that test should pass for negative counter values - token = '755224'; + // token = '755224'; options.counter = 7 options.window = 8; assert.ok(speakeasy.hotp.verify(options), 'Should pass for negative counter values'); @@ -224,7 +226,7 @@ it("hotp_gen", function() { speakeasy.hotp(options); // countercheck for passes - for(i=0;i Date: Sat, 23 Jan 2016 02:21:42 -0500 Subject: [PATCH 53/59] [fix] Generate - Fix base32 encoding and move hex encoding to native Buffer --- index.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index e0f75ec..f1f7247 100644 --- a/index.js +++ b/index.js @@ -377,8 +377,8 @@ exports.generate_key = function generateKey (options) { // return a SecretKey with ascii, hex, and base32 var SecretKey = {}; SecretKey.ascii = key; - SecretKey.hex = this.ascii_to_hex(key); - SecretKey.base32 = base32.encode(key).toString().replace(/=/g,''); + 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) { @@ -427,20 +427,6 @@ exports.generate_key_ascii = function(length, symbols) { return output; }; -// speakeasy.ascii_to_hex(key) -// -// helper function to convert an ascii key to hex. -// -exports.ascii_to_hex = function(str) { - var hex_string = ''; - - for (var i = 0; i < str.length; i++) { - hex_string += str.charCodeAt(i).toString(16); - } - - return hex_string; -}; - /** * Generate an URL for use with the Google Authenticator app. * From 14cb7e9d289e03c00bc7b55ca7a5e26fa537bfdf Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 02:22:03 -0500 Subject: [PATCH 54/59] [test] Update tests to test returned encodings and remove generate_key_ascii tests --- test/generate.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/test/generate.js b/test/generate.js index 1790c49..7bd9593 100644 --- a/test/generate.js +++ b/test/generate.js @@ -4,6 +4,7 @@ var chai = require('chai'); var assert = chai.assert; +var base32 = require('base32.js'); var speakeasy = require('..'); // These tests use the information from RFC 4226's Appendix D: Test Values. @@ -14,11 +15,17 @@ describe('Generator tests', function () { it('Normal generation with defaults', function () { var secret = speakeasy.generate_key(); assert.equal(secret.ascii.length, 32, 'Should return the correct length'); + + // check returned fields assert.isDefined(secret.google_auth_url, 'Google Auth URL should be returned') assert.isUndefined(secret.qr_code_ascii, 'QR Code ASCII should not be returned') assert.isUndefined(secret.qr_code_hex, 'QR Code Hex should not be returned') assert.isUndefined(secret.qr_code_base32, 'QR Code Base 32 should not be returned') assert.isUndefined(secret.google_auth_qr, 'Google Auth QR should not be returned') + + // check encodings + assert.equal(Buffer(secret.hex, 'hex').toString('ascii'), secret.ascii, 'Should have encoded correct hex string'); + assert.equal(base32.decode(secret.base32).toString('ascii'), secret.ascii, 'Should have encoded correct base32 string'); }); it('Generation with custom key length', function () { @@ -48,16 +55,4 @@ describe('Generator tests', function () { assert.isDefined(secret.google_auth_qr, 'Google Auth QR should be returned') }); - it('Testing generate_key_ascii with defaults', function () { - var secret = speakeasy.generate_key_ascii(); - assert.equal(secret.length, 32, 'Should return the correct length'); - assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); - }); - - it('Testing generate_key_ascii with custom length', function () { - var secret = speakeasy.generate_key_ascii(20); - assert.equal(secret.length, 20, 'Should return the correct length'); - assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); - }); - }); From cd4e03deb46cdb51b95f2c55021644e0569d8449 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 02:43:27 -0500 Subject: [PATCH 55/59] [test] Restore generate_key_ascii tests --- test/generate.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/generate.js b/test/generate.js index 7bd9593..0ced736 100644 --- a/test/generate.js +++ b/test/generate.js @@ -55,4 +55,16 @@ describe('Generator tests', function () { assert.isDefined(secret.google_auth_qr, 'Google Auth QR should be returned') }); + it('Testing generate_key_ascii with defaults', function () { + var secret = speakeasy.generate_key_ascii(); + assert.equal(secret.length, 32, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Testing generate_key_ascii with custom length', function () { + var secret = speakeasy.generate_key_ascii(20); + assert.equal(secret.length, 20, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + }); From c27c8c7a6375794867921abbc230b071d9a1d7c1 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 02:45:51 -0500 Subject: [PATCH 56/59] [readme] Updated README with full documentation --- README.md | 396 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 293 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index a632a88..01aadf6 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ --- -**Jump to** — [Install](#install) · [Demo](#demo) · [Usage](#usage) · [Documentation](#documentation) · [Quick Reference](#quick-reference) · [License](#license) +**Jump to** — [Install](#install) · [Demo](#demo) · [Usage](#usage) · [Documentation](#documentation) · [Contributing](#contributing) · [License](#license) --- @@ -123,109 +123,296 @@ is available at http://speakeasyjs.github.io/speakeasy/ +### Functions + +
+
digest(options)Buffer
+

Digest the one-time passcode options.

+
+
hotp(options)String
+

Generate a counter-based one-time passcode.

+
+
hotp․verifyDelta(options)Object
+

Verify a counter-based One Time passcode and return the delta.

+
+
hotp․verify(options)Boolean
+

Verify a counter-based One Time passcode.

+
+
totp(options)String
+

Generate a time-based one-time passcode.

+
+
totp․verifyDelta(options)Object
+

Verify a time-based One Time passcode and return the delta.

+
+
totp․verify(options)Boolean
+

Verify a time-based One Time passcode via strict comparison (i.e. +delta = 0).

+
+
generate_key(options)Object | GeneratedSecret
+

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.

+
+
generate_key_ascii([length], [symbols])String
+

Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and +symbols (if requested).

+
+
google_auth_url(options)String
+

Generate an URL for use with the Google Authenticator app.

+

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.

+
+
+ +### Typedefs + +
+
GeneratedSecret : Object
+
+
+ + +### digest(options) ⇒ Buffer +Digest the one-time passcode options. + +**Kind**: global function + +**Returns**: Buffer - The one-time passcode as a buffer. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.counter | Integer | | Counter value | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| options.key | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | + + +### hotp(options) ⇒ String +Generate a counter-based one-time passcode. + +**Kind**: global function + +**Returns**: String - The one-time passcode. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.counter | Integer | | Counter value | +| [options.digest] | Buffer | | Digest, automatically generated by default | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| options.key | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | +| [options.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. | + + +### hotp․verifyDelta(options) ⇒ Object +Verify a counter-based One Time passcode and return the delta. + +**Kind**: global function + +**Returns**: Object - On success, returns an object with the counter + difference between the client and the server as the `delta` property. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| options.counter | Integer | | Counter value. This should be stored by the application and must be incremented for each request. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 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. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### hotp․verify(options) ⇒ Boolean +Verify a counter-based One Time passcode. + +**Kind**: global function + +**Returns**: Boolean - Returns true if the token matches within the configured + window, false otherwise. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| options.counter | Integer | | Counter value. This should be stored by the application and must be incremented for each request. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 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. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### totp(options) ⇒ String +Generate a time-based one-time passcode. + +**Kind**: global function + +**Returns**: String - The one-time passcode. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| [options.time] | Integer | | Time with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| options.key | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | +| [options.initial_time] | Integer | 0 | (DEPRECATED. Use `epoch` instead.) Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. | + + +### totp․verifyDelta(options) ⇒ Object +Verify a time-based One Time passcode and return the delta. + +**Kind**: global function + +**Returns**: Object - On success, returns an object with the time step + difference between the client and the server as the `delta` property. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| [options.time] | Integer | | Time with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 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. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### totp․verify(options) ⇒ Boolean +Verify a time-based One Time passcode via strict comparison (i.e. +delta = 0). + +**Kind**: global function + +**Returns**: Boolean - Returns true if token strictly matches (delta = 0), + false otherwise. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| [options.time] | Integer | | Time with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 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. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### generate_key(options) ⇒ Object | [GeneratedSecret](#GeneratedSecret) +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. + +**Kind**: global function + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| [options.length] | Integer | 32 | Length of the secret | +| [options.symbols] | Boolean | false | Whether to include symbols | +| [options.qr_codes] | Boolean | false | Whether to output QR code URLs | +| [options.google_auth_qr] | Boolean | false | Whether to output a Google Authenticator otpauth:// QR code URL (returns the URL to the QR code) | +| [options.google_auth_url] | Boolean | true | Whether to output a Google Authenticator otpauth:// URL (only returns otpauth:// URL, no QR code) | +| [options.name] | String | | The name to use with Google Authenticator. | + + +### generate_key_ascii([length], [symbols]) ⇒ String +Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and +symbols (if requested). + +**Kind**: global function + +**Returns**: String - The generated key. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [length] | Integer | 32 | The length of the key. | +| [symbols] | Boolean | false | Whether to include symbols in the key. | + + +### google_auth_url(options) ⇒ String +Generate an URL for use with the Google Authenticator app. + +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. + +**Kind**: global function + +**Returns**: String - A URL suitable for use with the Google Authenticator. + +**See**: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.label | String | | Used to identify the account with which the secret key is associated, e.g. the user's email address. | +| [options.type] | String | "totp" | Either "hotp" or "totp". | +| [options.counter] | Integer | | The initial counter value, required for HOTP. | +| [options.issuer] | String | | The provider or service with which the secret key is associated. | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. Currently ignored by Google Authenticator. | +| [options.period] | Integer | 30 | The length of time for which a TOTP code will be valid, in seconds. Currently ignored by Google Authenticator. | +| [options.encoding] | String | | Key encoding (ascii, hex, base32, base64). If the key is not encoded in Base-32, it will be reencoded. | + + +### GeneratedSecret : Object + +**Kind**: global typedef + +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| ascii | String | ASCII representation of the secret | +| hex | String | Hex representation of the secret | +| base32 | String | Base32 representation of the secret | +| qr_code_ascii | String | URL for the QR code for the ASCII secret. | +| qr_code_hex | String | URL for the QR code for the hex secret. | +| qr_code_base32 | String | URL for the QR code for the base32 secret. | +| google_auth_qr | String | URL for the Google Authenticator otpauth URL's QR code. | - -## Quick Reference - -TODO: Update - -### speakeasy.hotp(options) | speakeasy.counter(options) - -Calculate the one-time password using the counter-based algorithm, HOTP. Specify the key and counter, and receive the one-time password for that counter position. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. - -Written to follow [RFC 4226](http://tools.ietf.org/html/rfc4226). Calculated with: `HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` - -#### Options - -* `secret`: the secret key in ASCII, hexadecimal, or base32 format. -* `counter`: the counter position (moving factor). -* `length` (default `6`): the length of the resulting one-time password. -* `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. - -#### Example - -```javascript -// normal use. -speakeasy.hotp({key: 'secret', counter: 582}); -// => 246642 - -// use a custom length. -speakeasy.hotp({key: 'secret', counter: 582, length: 8}); -// => 67246642 - -// use a custom encoding. -speakeasy.hotp({key: 'AJFIEJGEHIFIU7148SF', counter: 147, encoding: 'base32'}); -// => 974955 -``` - -### speakeasy.totp(options) | speakeasy.time(options) - -Calculate the one-time password using the time-based algorithm, TOTP. Specify the key, and receive the one-time password for that time. By default, the time step is 30 seconds, so there is a new password every 30 seconds. However, you may override the time step. You may also override the time you want to calculate the time from. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. - -Written to follow [RFC 6238](http://tools.ietf.org/html/rfc6238). Calculated with: `C = ((T - T0) / X); HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` - -#### Options - -* `secret`: the secret key in ASCII, hexadecimal, or base32 format. -* `step` (default `30`): the time step, in seconds, between new passwords (moving factor). -* `time` (default current time): the time to calculate the TOTP from, by default the current time. If you're doing something clever with TOTP, you may override this (see *Techniques* below). -* `initial_time` (default `0`): the starting time where we calculate the TOTP from. Usually, this is set to the UNIX epoch at 0. -* `length` (default `6`): the length of the resulting one-time password. -* `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. - -#### Example - -```javascript -// normal use. -speakeasy.totp({key: 'secret'}); - -// use a custom time step. -speakeasy.totp({key: 'secret', step: 60}); - -// use a custom time. -speakeasy.totp({key: 'secret', time: 159183717}); -// => 558014 - -// use a initial time. -speakeasy.totp({key: 'secret', initial_time: 4182881485}); -// => 670417 -``` - -#### Techniques - -You can implement a double-authentication scheme, where you ask the user to input the one-time password once, wait until the next 30-second refresh, and then input the one-time password again. In this case, you can calculate the second (later) input by calculating TOTP as usual, then also verify the first (earlier) input by taking the current epoch time in seconds and subtracting 30 seconds to get to the previous step (for example: `time1 = (parseInt(new Date()/1000) - 30)`) - -### speakeasy.generate_key(options) - -Generate a random secret key. It will return the key in ASCII, hexadecimal, and base32 formats. You can specify the length, whether or not to use symbols, and ask it (nicely) to generate URLs for QR codes. Returns an object with the ASCII, hex, and base32 representations of the secret key, plus any QR codes you can optionally ask for. - -#### Options - -* `length` (default `32`): the length of the generated secret key. -* `symbols` (default `true`): include symbols in the key? if not, the key will be alphanumeric, {A-Z, a-z, 0-9} -* `qr_codes` (default `false`): generate links to QR codes for each encoding (ASCII, hexadecimal, and base32). It uses the Google Charts API and they are served over HTTPS. A future version might allow for QR code generation client-side for security. -* `google_auth_qr` (default `false`): generate a link to a QR code that you can scan using the Google Authenticator app. The contents of the QR code are in this format: `otpauth://totp/[KEY NAME]?secret=[KEY SECRET, BASE 32]`. -* `name` (optional): specify a name when you are using `google_auth_qr`, which will show up as the label after scanning. `[KEY NAME]` in the previous line. - -#### Examples - -```javascript -// generate a key -speakeasy.generate_key({length: 20, symbols: true}); -// => { ascii: 'km^A?n&sOPJW.iCKPHKU', hex: '6b6d5e413f6e26734f504a572e69434b50484b55', base32: 'NNWV4QJ7NYTHGT2QJJLS42KDJNIEQS2V' } - -// generate a key and request QR code links -speakeasy.generate_key({length: 20, qr_codes: true}); -// => { ascii: 'eV:JQ1NedJkKn&]6^i>s', ... (truncated) -// qr_code_ascii: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=eV%3AJQ1NedJkKn%26%5D6%5Ei%3Es', -// qr_code_hex: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=65563a4a51314e65644a6b4b6e265d365e693e73', -// qr_code_base32: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=MVLDUSSRGFHGKZCKNNFW4JS5GZPGSPTT' } - -// generate a key and get a QR code you can scan with the Google Authenticator app -speakeasy.generate_key({length: 20, google_auth_qr: true}); -// => { ascii: 'V?9f6.Cq1& +## Contributing + +We're very happy to have your contributions in Speakeasy. + +**Contributing code** — First, make sure you've added tests if adding new functionality. Then, run `npm test` to run all the tests to make sure they pass. Next, make a pull request to this repo. Thanks! + +**Filing an issue** — Submit issues to the [GitHub Issues][issues] page. + +**Maintainers** — + +- Mark Bao ([markbao][markbao]) +- Michael Phan-Ba ([mikepb][mikepb]) ## License @@ -236,8 +423,11 @@ Please see the [LICENSE](LICENSE) file for the full combined license. Icons created by Gregor Črešnar, iconoci, and Danny Sturgess from the Noun Project. +[issues]: https://github.com/speakeasyjs/speakeasy [passcode]: http://github.com/mikepb/passcode [notp]: https://github.com/guyht/notp [oath]: http://www.openauthentication.org/ [rfc4226]: https://tools.ietf.org/html/rfc4226 [rfc6238]: https://tools.ietf.org/html/rfc6238 +[markbao]: https://github.com/markbao +[mikepb]: https://github.com/mikepb \ No newline at end of file From 1399eb8e316202c1c82981079e56b1573d654d24 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 02:53:14 -0500 Subject: [PATCH 57/59] [doc] Add google_auth_url to the GeneratedSecret typedef --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index f1f7247..3ae1c38 100644 --- a/index.js +++ b/index.js @@ -339,6 +339,7 @@ exports.totp.verify = function totpVerify (options) { * @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} google_auth_url Google Authenticator otpauth URL. */ /** From ee299b3c17b14dc0f1ce6b2e7d6fdd17a2cfa308 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 03:11:39 -0500 Subject: [PATCH 58/59] [doc] Updated documentation with more detail and fixed updated API --- index.js | 58 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 3ae1c38..47f2ce9 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,5 @@ "use strict"; -/** - * Module dependencies. - */ - var base32 = require("base32.js"); var crypto = require("crypto"); var url = require("url"); @@ -64,7 +60,7 @@ exports.digest = function digest (options) { }; /** - * Generate a counter-based one-time passcode. + * Generate a counter-based one-time token. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -109,7 +105,14 @@ exports.hotp = function hotpGenerate (options) { }; /** - * Verify a counter-based One Time passcode and return the delta. + * 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. + * `verifyDelta()` will then return the delta between the given token and the + * given counter value. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -127,7 +130,8 @@ exports.hotp = function hotpGenerate (options) { * @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. + * difference between the client and the server as the `delta` property (i.e. + * `{ delta: 0 }`). * @method hotp․verifyDelta * @global */ @@ -156,7 +160,9 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) { }; /** - * Verify a counter-based One Time passcode. + * 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. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -173,7 +179,7 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) { * base32, base64). * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, * sha512). - * @return {Boolean} Returns true if the token matches within the configured + * @return {Boolean} Returns true if the token matches within the given * window, false otherwise. * @method hotp․verify * @global @@ -209,7 +215,8 @@ exports._counter = function _counter (options) { }; /** - * Generate a time-based one-time passcode. + * Generate a time-based one-time token. By default, it returns the token for + * the current time. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -225,7 +232,7 @@ exports._counter = function _counter (options) { * base32, base64). * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, * sha512). - * @param {String} options.key (DEPRECATED. Use `secret` instead.) + * @param {String} [options.key] (DEPRECATED. Use `secret` instead.) * Shared secret key * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.) * Initial time since the UNIX epoch from which to calculate the counter @@ -248,7 +255,14 @@ exports.totp = function totpGenerate (options) { }; /** - * Verify a time-based One Time passcode and return the delta. + * 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. + * `verifyDelta()` will then return the delta between the given token and the + * current time in time steps. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -271,7 +285,8 @@ exports.totp = function totpGenerate (options) { * @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. + * difference between the client and the server as the `delta` property (e.g. + * `{ delta: 0 }`). * @method totp․verifyDelta * @global */ @@ -296,8 +311,9 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) { }; /** - * Verify a time-based One Time passcode via strict comparison (i.e. - * delta = 0). + * 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. * * @param {Object} options * @param {String} options.secret Shared secret key @@ -319,8 +335,8 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) { * base32, base64). * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, * sha512). - * @return {Boolean} Returns true if token strictly matches (delta = 0), - * false otherwise. + * @return {Boolean} Returns true if the token matches within the given + * window, false otherwise. * @method totp․verify * @global */ @@ -344,7 +360,13 @@ exports.totp.verify = function totpVerify (options) { /** * 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. + * (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). + * + * Can also optionally return QR codes for the secret and for the Google + * Authenticator URL. + * * @param {Object} options * @param {Integer} [options.length=32] Length of the secret * @param {Boolean} [options.symbols=false] Whether to include symbols From 98230f53fede1aac8e425caf87c7dbd74c861bd9 Mon Sep 17 00:00:00 2001 From: Mark Bao Date: Sat, 23 Jan 2016 03:13:35 -0500 Subject: [PATCH 59/59] [doc] Updated README.md documentation --- README.md | 105 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 01aadf6..7d33f28 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,7 @@ var tokenValidates = speakeasy.hotp.verify({ ## Documentation -A quick reference can be found below. Full API documentation (in JSDoc format) -is available at http://speakeasyjs.github.io/speakeasy/ +Full API documentation (in JSDoc format) is available below and at http://speakeasyjs.github.io/speakeasy/ @@ -130,27 +129,47 @@ is available at http://speakeasyjs.github.io/speakeasy/

Digest the one-time passcode options.

hotp(options)String
-

Generate a counter-based one-time passcode.

+

Generate a counter-based one-time token.

hotp․verifyDelta(options)Object
-

Verify a counter-based One Time passcode and return the delta.

+

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. +verifyDelta() will then return the delta between the given token and the +given counter value.

hotp․verify(options)Boolean
-

Verify a counter-based One Time passcode.

+

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.

totp(options)String
-

Generate a time-based one-time passcode.

+

Generate a time-based one-time token. By default, it returns the token for +the current time.

totp․verifyDelta(options)Object
-

Verify a time-based One Time passcode and return the delta.

+

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. +verifyDelta() will then return the delta between the given token and the +current time in time steps.

totp․verify(options)Boolean
-

Verify a time-based One Time passcode via strict comparison (i.e. -delta = 0).

+

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.

generate_key(options)Object | GeneratedSecret

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.

+(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).

+

Can also optionally return QR codes for the secret and for the Google +Authenticator URL.

generate_key_ascii([length], [symbols])String

Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and @@ -179,7 +198,6 @@ generator, such as the qr-image module.

Digest the one-time passcode options. **Kind**: global function - **Returns**: Buffer - The one-time passcode as a buffer. | Param | Type | Default | Description | @@ -193,10 +211,9 @@ Digest the one-time passcode options. ### hotp(options) ⇒ String -Generate a counter-based one-time passcode. +Generate a counter-based one-time token. **Kind**: global function - **Returns**: String - The one-time passcode. | Param | Type | Default | Description | @@ -213,12 +230,19 @@ Generate a counter-based one-time passcode. ### hotp․verifyDelta(options) ⇒ Object -Verify a counter-based One Time passcode and return the delta. +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. -**Kind**: global function +You can specify a window to add more leeway to the verification process. +`verifyDelta()` will then return the delta between the given token and the +given counter value. +**Kind**: global function **Returns**: Object - On success, returns an object with the counter - difference between the client and the server as the `delta` property. + difference between the client and the server as the `delta` property (i.e. + `{ delta: 0 }`). | Param | Type | Default | Description | | --- | --- | --- | --- | @@ -233,11 +257,12 @@ Verify a counter-based One Time passcode and return the delta. ### hotp․verify(options) ⇒ Boolean -Verify a counter-based One Time passcode. +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. **Kind**: global function - -**Returns**: Boolean - Returns true if the token matches within the configured +**Returns**: Boolean - Returns true if the token matches within the given window, false otherwise. | Param | Type | Default | Description | @@ -253,10 +278,10 @@ Verify a counter-based One Time passcode. ### totp(options) ⇒ String -Generate a time-based one-time passcode. +Generate a time-based one-time token. By default, it returns the token for +the current time. **Kind**: global function - **Returns**: String - The one-time passcode. | Param | Type | Default | Description | @@ -270,18 +295,25 @@ Generate a time-based one-time passcode. | [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | | [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | | [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | -| options.key | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | +| [options.key] | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | | [options.initial_time] | Integer | 0 | (DEPRECATED. Use `epoch` instead.) Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | | [options.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. | ### totp․verifyDelta(options) ⇒ Object -Verify a time-based One Time passcode and return the delta. +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. -**Kind**: global function +You can specify a window to add more leeway to the verification process. +`verifyDelta()` will then return the delta between the given token and the +current time in time steps. +**Kind**: global function **Returns**: Object - On success, returns an object with the time step - difference between the client and the server as the `delta` property. + difference between the client and the server as the `delta` property (e.g. + `{ delta: 0 }`). | Param | Type | Default | Description | | --- | --- | --- | --- | @@ -299,13 +331,13 @@ Verify a time-based One Time passcode and return the delta. ### totp․verify(options) ⇒ Boolean -Verify a time-based One Time passcode via strict comparison (i.e. -delta = 0). +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. **Kind**: global function - -**Returns**: Boolean - Returns true if token strictly matches (delta = 0), - false otherwise. +**Returns**: Boolean - Returns true if the token matches within the given + window, false otherwise. | Param | Type | Default | Description | | --- | --- | --- | --- | @@ -324,7 +356,12 @@ delta = 0). ### generate_key(options) ⇒ Object | [GeneratedSecret](#GeneratedSecret) 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. +(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). + +Can also optionally return QR codes for the secret and for the Google +Authenticator URL. **Kind**: global function @@ -344,7 +381,6 @@ Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and symbols (if requested). **Kind**: global function - **Returns**: String - The generated key. | Param | Type | Default | Description | @@ -365,9 +401,7 @@ To generate a suitable QR Code, pass the generated URL to a QR Code generator, such as the `qr-image` module. **Kind**: global function - **Returns**: String - A URL suitable for use with the Google Authenticator. - **See**: https://github.com/google/google-authenticator/wiki/Key-Uri-Format | Param | Type | Default | Description | @@ -385,9 +419,7 @@ generator, such as the `qr-image` module. ### GeneratedSecret : Object - **Kind**: global typedef - **Properties** | Name | Type | Description | @@ -399,6 +431,7 @@ generator, such as the `qr-image` module. | qr_code_hex | String | URL for the QR code for the hex secret. | | qr_code_base32 | String | URL for the QR code for the base32 secret. | | google_auth_qr | String | URL for the Google Authenticator otpauth URL's QR code. | +| google_auth_url | String | Google Authenticator otpauth URL. | ## Contributing