From 7d7aa077432ad8d16b6fd667f4c56db1250fd021 Mon Sep 17 00:00:00 2001
From: Cathal Garvey
Date: Wed, 4 Sep 2013 19:52:30 +0100
Subject: [PATCH 01/85] 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/85] 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/85] 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/85] - 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/85] 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/85] 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/85] 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/85] 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/85] 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/85] 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/85] 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/85] 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/85] 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/85] 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
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:
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.
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
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
Generates a random key, of length length (default 32).
-Also choose whether you want symbols, default false.
-speakeasy.generate_key() wraps around this.
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 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.
+
+
+### 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/85] [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/85] [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/85] [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/
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.
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.
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.
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.
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.
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
From ec44e6b9c219362d0379f4aa3052ea17f02e55ef Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:02:48 -0500
Subject: [PATCH 60/85] [test] NOTP - Fix test checking for negative counter
value
---
test/notp_test.js | 21 +++++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/test/notp_test.js b/test/notp_test.js
index 7da78ad..33632f9 100644
--- a/test/notp_test.js
+++ b/test/notp_test.js
@@ -7,6 +7,9 @@ var assert = chai.assert;
var speakeasy = require('..');
/*
+ * Tests originally from the notp module with specific changes and bugfixes for
+ * Speakeasy: https://github.com/guyht/notp
+ *
* Test HOTtoken. Uses test values from RFcounter 4226
*
*
@@ -163,12 +166,19 @@ it("TOTtoken", function() {
/*
* countercheck for codes that are out of sync
- * windowe are going to use a value of counter = 1 and test against
+ * window are going to use a value of counter = 1 and test against
* a code for counter = 9
*/
it("HOTPOutOfSync", function() {
+ /*
+ * for secret 12345678901234567890:
+ * 755224 = counter 0
+ * 287082 = counter 1
+ * 520489 = counter 8
+ */
+
var options = {
secret: '12345678901234567890',
token: '520489',
@@ -183,11 +193,14 @@ it("HOTPOutOfSync", function() {
options.window = 8;
assert.ok(speakeasy.hotp.verify(options), 'Should pass for value of window >= 9');
- // countercheck that test should pass for negative counter values
- // token = '755224';
+ // countercheck that test should not pass for tokens behind the current counter
+ // 755224 is counter 0, and unlike notp (which has a two-sided window),
+ // Speakeasy will only allow a one-sided window, so counter = 7 and window = 8
+ // will not look at counter 0.
+ options.token = '755224';
options.counter = 7
options.window = 8;
- assert.ok(speakeasy.hotp.verify(options), 'Should pass for negative counter values');
+ assert.notOk(speakeasy.hotp.verify(options), 'Should not pass for tokens behind the current counter');
});
From 361207d466947ab9f6e4bad12b497b58737c7760 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:04:22 -0500
Subject: [PATCH 61/85] [doc] Update documentation for hotp/totp.verifyDelta()
and .verify() to add more info about windows
---
index.js | 32 +++++++++++++++++++++++---------
1 file changed, 23 insertions(+), 9 deletions(-)
diff --git a/index.js b/index.js
index a1bb161..7ce0bec 100644
--- a/index.js
+++ b/index.js
@@ -14,7 +14,7 @@ var url = require("url");
* 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
* @return {Buffer} The one-time passcode as a buffer.
*/
@@ -72,7 +72,7 @@ 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.)
+ * @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.
@@ -111,8 +111,13 @@ exports.hotp = function hotpGenerate (options) {
* 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.
+ * Setting the window param will check for the token at the given counter value
+ * as well as `window` tokens ahead (one-sided window). See param for more info.
+ *
+ * `verifyDelta()` will return the delta between the counter value of the token
+ * and the given counter value. For example, if given a counter 5 and a window
+ * 10, `verifyDelta()` will look at tokens from 5 to 15, inclusive. If it finds
+ * it at counter position 7, it will return `{ delta: 2 }`.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
@@ -161,8 +166,9 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) {
/**
* 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.
+ * verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
+ * instead of an object. For more on how to use a window with this, see
+ * {@link hotp.verifyDelta}.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
@@ -261,8 +267,16 @@ exports.totp = function totpGenerate (options) {
* 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.
+ * Setting the window param will check for the token at the given counter value
+ * as well as `window` tokens ahead and `window` tokens behind (two-sided
+ * window). See param for more info.
+ *
+ * `verifyDelta()` will return the delta between the counter value of the token
+ * and the given counter value. For example, if given a time at counter 1000 and
+ * a window of 5, `verifyDelta()` will look at tokens from 995 to 1005,
+ * inclusive. In other words, if the time-step is 30 seconds, it will look at
+ * tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
+ * If it finds it at counter position 1002, it will return `{ delta: 2 }`.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
@@ -313,7 +327,7 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) {
/**
* 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.
+ * an object. For more on how to use a window with this, see {@link totp.verify}.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
From 7c77a0479772a59ca8869ea62ac45c37b0f522c2 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:19:54 -0500
Subject: [PATCH 62/85] [refactor] Move all method names to camelCase and make
vocabulary consistent, add backwards compatibility - generate_key >
generateSecret (deprecation notice) - generate_key_ascii >
generateSecretASCII (deprecation notice) - google_auth_url > googleAuthURL
---
index.js | 19 +++++++++++++++----
test/url_test.js | 48 ++++++++++++++++++++++++------------------------
2 files changed, 39 insertions(+), 28 deletions(-)
diff --git a/index.js b/index.js
index 7ce0bec..e0af369 100644
--- a/index.js
+++ b/index.js
@@ -3,6 +3,7 @@
var base32 = require("base32.js");
var crypto = require("crypto");
var url = require("url");
+var util = require("util");
/**
* Digest the one-time passcode options.
@@ -393,7 +394,7 @@ exports.totp.verify = function totpVerify (options) {
* @return {Object}
* @return {GeneratedSecret} The generated secret key.
*/
-exports.generate_key = function generateKey (options) {
+exports.generateSecret = function generateSecret (options) {
// options
if(!options) options = {};
var length = options.length || 32;
@@ -425,7 +426,7 @@ exports.generate_key = function generateKey (options) {
}
if (google_auth_url) {
- SecretKey.google_auth_url = exports.google_auth_url({
+ SecretKey.google_auth_url = exports.googleAuthURL({
secret: SecretKey.hex,
label: name
});
@@ -442,6 +443,11 @@ exports.generate_key = function generateKey (options) {
return SecretKey;
};
+// Backwards compatibility - generate_key is deprecated
+exports.generate_key = util.deprecate(function(options) {
+ return exports.generateSecret(options);
+}, 'Speakeasy - Deprecation Notice - `generate_key()` is depreciated, please use `generateSecret()` instead.');
+
/**
* Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and
* symbols (if requested).
@@ -450,7 +456,7 @@ exports.generate_key = function generateKey (options) {
* @param {Boolean} [symbols=false] Whether to include symbols in the key.
* @return {String} The generated key.
*/
-exports.generate_key_ascii = function(length, symbols) {
+exports.generateSecretASCII = function generateSecretASCII(length, symbols) {
var bytes = crypto.randomBytes(length || 32);
var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
if (symbols) {
@@ -464,6 +470,11 @@ exports.generate_key_ascii = function(length, symbols) {
return output;
};
+// Backwards compatibility - generate_key_ascii is deprecated
+exports.generate_key_ascii = util.deprecate(function(length, symbols) {
+ return exports.generateSecretASCII(length, symbols);
+}, 'Speakeasy - Deprecation Notice - `generate_key_ascii()` is depreciated, please use `generateSecretASCII()` instead.');
+
/**
* Generate an URL for use with the Google Authenticator app.
*
@@ -497,7 +508,7 @@ exports.generate_key_ascii = function(length, symbols) {
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*/
-exports.google_auth_url = function (options) {
+exports.googleAuthURL = function googleAuthURL(options) {
// unpack options
var secret = options.secret;
diff --git a/test/url_test.js b/test/url_test.js
index f1133ba..1974e54 100644
--- a/test/url_test.js
+++ b/test/url_test.js
@@ -11,13 +11,13 @@ describe("#url", function () {
it("should require options", function () {
assert.throws(function () {
- speakeasy.google_auth_url();
+ speakeasy.googleAuthURL();
});
});
it("should validate type", function () {
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
type: "haha",
secret: "hello",
label: "that",
@@ -27,7 +27,7 @@ describe("#url", function () {
it("should require secret", function () {
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
label: "that"
}, /missing secret/);
});
@@ -35,7 +35,7 @@ describe("#url", function () {
it("should require label", function () {
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
secret: "hello"
}, /missing label/);
});
@@ -43,19 +43,19 @@ describe("#url", function () {
it("should require counter for HOTP", function () {
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
type: "hotp",
secret: "hello",
label: "that"
}, /missing counter/);
});
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
type: "hotp",
secret: "hello",
label: "that",
counter: 0
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
type: "hotp",
secret: "hello",
label: "that",
@@ -65,23 +65,23 @@ describe("#url", function () {
it("should validate algorithm", function () {
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
secret: "hello",
label: "that",
algorithm: "hello"
}, /invalid algorithm `hello`/);
});
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
algorithm: "sha1"
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
algorithm: "sha256"
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
algorithm: "sha512"
@@ -90,42 +90,42 @@ describe("#url", function () {
it("should validate digits", function () {
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
secret: "hello",
label: "that",
digits: "hello"
}, /invalid digits `hello`/);
});
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
secret: "hello",
label: "that",
digits: 12
}, /invalid digits `12`/);
});
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
secret: "hello",
label: "that",
digits: "7"
}, /invalid digits `7`/);
});
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
digits: 6
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
digits: 8
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
digits: "6"
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
digits: "8"
@@ -134,28 +134,28 @@ describe("#url", function () {
it("should validate period", function () {
assert.throws(function () {
- speakeasy.google_auth_url({
+ speakeasy.googleAuthURL({
secret: "hello",
label: "that",
period: "hello"
}, /invalid period `hello`/);
});
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
period: 60
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
period: 121
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
period: "60"
}));
- assert.ok(speakeasy.google_auth_url({
+ assert.ok(speakeasy.googleAuthURL({
secret: "hello",
label: "that",
period: "121"
@@ -163,7 +163,7 @@ describe("#url", function () {
});
it("should generate an URL compatible with the Google Authenticator app", function () {
- var answer = speakeasy.google_auth_url({
+ var answer = speakeasy.googleAuthURL({
secret: "JBSWY3DPEHPK3PXP",
label: "Example:alice@google.com",
issuer: "Example",
From 57c2bb75bd5dc7234536e6955079a3d1188391ae Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:20:20 -0500
Subject: [PATCH 63/85] [fix] HOTP/TOTP - Add Speakeasy 1.x aliases for
speakeasy.time() and speakeasy.counter()
---
index.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/index.js b/index.js
index e0af369..6b3c1e7 100644
--- a/index.js
+++ b/index.js
@@ -105,6 +105,9 @@ exports.hotp = function hotpGenerate (options) {
return code.substr(-digits);
};
+// Alias counter() for hotp()
+exports.counter = exports.hotp;
+
/**
* Verify a counter-based one-time token against the secret and return the delta.
* By default, it verifies the token at the given counter value, with no leeway
@@ -261,6 +264,9 @@ exports.totp = function totpGenerate (options) {
return this.hotp(options);
};
+// Alias time() for totp()
+exports.time = exports.totp;
+
/**
* Verify a time-based one-time token against the secret and return the delta.
* By default, it verifies the token at the current time window, with no leeway
From 9353f683afacedb4be1edfaae3667409a1be121d Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:27:52 -0500
Subject: [PATCH 64/85] [fix] Generate - Deprecated QR code functionality and
ensured that `name` for Google Auth URLs will be URL-encoded.
---
index.js | 25 +++++++++++++------------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/index.js b/index.js
index 6b3c1e7..d10d327 100644
--- a/index.js
+++ b/index.js
@@ -383,20 +383,21 @@ 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,
* 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.
+ * URL). Use a QR code library to generate a QR code based on the Google
+ * Authenticator URL to obtain a QR code you can scan into the app.
*
* @param {Object} options
* @param {Integer} [options.length=32] Length of the secret
* @param {Boolean} [options.symbols=false] Whether to include symbols
- * @param {Boolean} [options.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.
+ * @param {Boolean} [options.qr_codes=false] (DEPRECATED. Do not use to prevent
+ * leaking of secret to a third party. Use your own QR code implementation.)
+ * Output QR code URLs for the token.
+ * @param {Boolean} [options.google_auth_qr=false] (DEPRECATED. Do not use to
+ * prevent leaking of secret to a third party. Use your own QR code
+ * implementation.) Output a Google Authenticator otpauth:// QR code URL.
* @return {Object}
* @return {GeneratedSecret} The generated secret key.
*/
@@ -404,7 +405,7 @@ exports.generateSecret = function generateSecret (options) {
// options
if(!options) options = {};
var length = options.length || 32;
- var name = options.name || "SecretKey";
+ var name = encodeURIComponent(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 != null ? options.google_auth_url : true;
@@ -426,11 +427,13 @@ exports.generateSecret = function generateSecret (options) {
// generate some qr codes if requested
if (qr_codes) {
+ console.log('Speakeasy - Deprecation Notice - generateSecret() QR codes are deprecated and no longer supported. Please use your own QR code implementation.');
SecretKey.qr_code_ascii = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.ascii);
SecretKey.qr_code_hex = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.hex);
SecretKey.qr_code_base32 = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.base32);
}
+ // add in the Google Authenticator-compatible otpauth URL
if (google_auth_url) {
SecretKey.google_auth_url = exports.googleAuthURL({
secret: SecretKey.hex,
@@ -439,11 +442,9 @@ exports.generateSecret = function generateSecret (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);
+ console.log('Speakeasy - Deprecation Notice - generateSecret() Google Auth QR code is deprecated and no longer supported. Please use your own QR code implementation.');
+ SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.googleAuthURL({ secret: SecretKey.hex, label: name }));
}
return SecretKey;
From 205845416016e31c79c90dbac97d2fca52a0b9c4 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:32:07 -0500
Subject: [PATCH 65/85] [fix] Generate - Generate should use base32 encoding
for googleAuthURL
---
index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/index.js b/index.js
index d10d327..ab7051d 100644
--- a/index.js
+++ b/index.js
@@ -436,7 +436,7 @@ exports.generateSecret = function generateSecret (options) {
// add in the Google Authenticator-compatible otpauth URL
if (google_auth_url) {
SecretKey.google_auth_url = exports.googleAuthURL({
- secret: SecretKey.hex,
+ secret: SecretKey.base32,
label: name
});
}
@@ -444,7 +444,7 @@ exports.generateSecret = function generateSecret (options) {
// generate a QR code for use in Google Authenticator if requested
if (google_auth_qr) {
console.log('Speakeasy - Deprecation Notice - generateSecret() Google Auth QR code is deprecated and no longer supported. Please use your own QR code implementation.');
- SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.googleAuthURL({ secret: SecretKey.hex, label: name }));
+ SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.googleAuthURL({ secret: SecretKey.base32, label: name }));
}
return SecretKey;
From f9a9059c8de831d0eb4cedb9fa0be7e3e7f40f62 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:36:27 -0500
Subject: [PATCH 66/85] [maint] Add deprecation notices
---
index.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/index.js b/index.js
index ab7051d..9a587b1 100644
--- a/index.js
+++ b/index.js
@@ -30,7 +30,10 @@ exports.digest = function digest (options) {
var algorithm = (options.algorithm || "sha1").toLowerCase();
// Backwards compatibility - deprecated
- if (options.key) key = options.key;
+ if (options.key) {
+ console.log('Speakeasy - Deprecation Notice - Specifying the secret using `key` is no longer supported. Use `secret` instead.')
+ key = options.key;
+ }
// convert key to buffer
if (!Buffer.isBuffer(key)) {
@@ -85,6 +88,7 @@ exports.hotp = function hotpGenerate (options) {
// unpack digits
// backward compatibility: `length` is also accepted here, but deprecated
var digits = (options.digits != null ? options.digits : options.length) || 6;
+ if options.length console.log('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.');
// digest the options
var digest = options.digest || exports.digest(options);
@@ -220,6 +224,7 @@ exports._counter = function _counter (options) {
// also accepts 'initial_time', but deprecated
var epoch = (options.epoch != null ? options.epoch : options.initial_time) || 0;
+ if options.initial_time console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.')
return Math.floor((time - epoch) / step / 1000);
};
From b25eddee0cfb0714e7fc2c6be0571f4e8af5b423 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:37:18 -0500
Subject: [PATCH 67/85] [maint] Removed History.md
---
History.md | 16 ----------------
1 file changed, 16 deletions(-)
delete mode 100644 History.md
diff --git a/History.md b/History.md
deleted file mode 100644
index e6742be..0000000
--- a/History.md
+++ /dev/null
@@ -1,16 +0,0 @@
-1.0.2 / 2015-07-13
-==================
-
- * [Fixed] Don't repeat the secret key generating the digest.
-
-
-1.0.1 / 2015-07-13
-==================
-
- * [Fixed] Ignore case on algorithm option.
-
-
-1.0.0 / 2015-07-12
-==================
-
- * Initial release based on speakeasy and notp.
From efc40eee73b4365e236f1472405d4f7b730c1597 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:42:31 -0500
Subject: [PATCH 68/85] [fix] Fix deprecation notice syntax
---
index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/index.js b/index.js
index 9a587b1..f4054ab 100644
--- a/index.js
+++ b/index.js
@@ -88,7 +88,7 @@ exports.hotp = function hotpGenerate (options) {
// unpack digits
// backward compatibility: `length` is also accepted here, but deprecated
var digits = (options.digits != null ? options.digits : options.length) || 6;
- if options.length console.log('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.');
+ if (options.length) console.log('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.');
// digest the options
var digest = options.digest || exports.digest(options);
@@ -224,7 +224,7 @@ exports._counter = function _counter (options) {
// also accepts 'initial_time', but deprecated
var epoch = (options.epoch != null ? options.epoch : options.initial_time) || 0;
- if options.initial_time console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.')
+ if (options.initial_time) console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.')
return Math.floor((time - epoch) / step / 1000);
};
From 001bb6327fff0212cc07d95359b1a5c7453a3b2f Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 20:52:56 -0500
Subject: [PATCH 69/85] [doc] Updated documentation with updated JSDoc
---
README.md | 97 ++++++++++++++++++++++++-------------------------------
1 file changed, 43 insertions(+), 54 deletions(-)
diff --git a/README.md b/README.md
index 7d33f28..a70cd41 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ npm install --save speakeasy
## Demo
-This demo uses the `generate_key` method of Speakeasy to generate a secret key,
+This demo uses the `generateSecret` 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. https://sedemo-mktb.rhcloud.com/
@@ -132,57 +132,34 @@ Full API documentation (in JSDoc format) is available below and at http://speake
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.
+
Verify a counter-based one-time token against the secret and return the delta.
Verify a time-based one-time token against the secret and return true if it
-verifies. Helper function for verifyDelta() that returns a boolean instead of
-an object.
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.
+
Verify a time-based one-time token against the secret and return the delta.
Verify a time-based one-time token against the secret and return true if it
verifies. Helper function for verifyDelta() that returns a boolean instead of
-an object.
+an object. For more on how to use a window with this, see totp.verify.
Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length
-(default 32). Returns the secret key in ASCII, hexadecimal, and base32 format,
-along with the URL used for the QR code for Google Authenticator (an otpauth
-URL).
-
Can also optionally return QR codes for the secret and for the Google
-Authenticator URL.
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.
@@ -207,7 +184,7 @@ Digest the one-time passcode options.
| 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 |
+| [options.key] | String | | (DEPRECATED. Use `secret` instead.) Shared secret key |
### hotp(options) ⇒ String
@@ -225,7 +202,7 @@ Generate a counter-based one-time token.
| [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.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. |
@@ -236,12 +213,17 @@ By default, it verifies the token at the given counter value, with no leeway
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.
+Setting the window param will check for the token at the given counter value
+as well as `window` tokens ahead (one-sided window). See param for more info.
+
+`verifyDelta()` will return the delta between the counter value of the token
+and the given counter value. For example, if given a counter 5 and a window
+10, `verifyDelta()` will look at tokens from 5 to 15, inclusive. If it finds
+it at counter position 7, it will return `{ delta: 2 }`.
**Kind**: global function
**Returns**: Object - On success, returns an object with the counter
- difference between the client and the server as the `delta` property (i.e.
+ difference between the client and the server as the `delta` property (i.e.
`{ delta: 0 }`).
| Param | Type | Default | Description |
@@ -258,8 +240,9 @@ given counter value.
### hotp․verify(options) ⇒ Boolean
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.
+verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
+instead of an object. For more on how to use a window with this, see
+[hotp.verifyDelta](hotp.verifyDelta).
**Kind**: global function
**Returns**: Boolean - Returns true if the token matches within the given
@@ -307,8 +290,16 @@ By default, it verifies the token at the current time window, with no leeway
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.
+Setting the window param will check for the token at the given counter value
+as well as `window` tokens ahead and `window` tokens behind (two-sided
+window). See param for more info.
+
+`verifyDelta()` will return the delta between the counter value of the token
+and the given counter value. For example, if given a time at counter 1000 and
+a window of 5, `verifyDelta()` will look at tokens from 995 to 1005,
+inclusive. In other words, if the time-step is 30 seconds, it will look at
+tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
+If it finds it at counter position 1002, it will return `{ delta: 2 }`.
**Kind**: global function
**Returns**: Object - On success, returns an object with the time step
@@ -333,7 +324,7 @@ current time in time steps.
### totp․verify(options) ⇒ Boolean
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.
+an object. For more on how to use a window with this, see [totp.verify](totp.verify).
**Kind**: global function
**Returns**: Boolean - Returns true if the token matches within the given
@@ -353,15 +344,13 @@ an object.
| [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)
+
+### generateSecret(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,
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.
+URL). Use a QR code library to generate a QR code based on the Google
+Authenticator URL to obtain a QR code you can scan into the app.
**Kind**: global function
@@ -370,13 +359,13 @@ Authenticator URL.
| 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. |
+| [options.qr_codes] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output QR code URLs for the token. |
+| [options.google_auth_qr] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output a Google Authenticator otpauth:// QR code URL. |
-
-### generate_key_ascii([length], [symbols]) ⇒ String
+
+### generateSecretASCII([length], [symbols]) ⇒ String
Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and
symbols (if requested).
@@ -388,8 +377,8 @@ symbols (if requested).
| [length] | Integer | 32 | The length of the key. |
| [symbols] | Boolean | false | Whether to include symbols in the key. |
-
-### google_auth_url(options) ⇒ String
+
+### googleAuthURL(options) ⇒ String
Generate an URL for use with the Google Authenticator app.
Authenticator considers TOTP codes valid for 30 seconds. Additionally,
From 2e80da4547651feda88129e8d255ac8be91baf22 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 21:32:48 -0500
Subject: [PATCH 70/85] [readme] Added documentation for two-factor usage
---
README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++----
index.js | 3 +-
2 files changed, 82 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index a70cd41..0e81006 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
---
-**Jump to** — [Install](#install) · [Demo](#demo) · [Usage](#usage) · [Documentation](#documentation) · [Contributing](#contributing) · [License](#license)
+**Jump to** — [Install](#install) · [Demo](#demo) · [Two-Factor Usage](#two-factor) · [General Usage](#general-usage) · [Documentation](#documentation) · [Contributing](#contributing) · [License](#license)
---
@@ -40,8 +40,82 @@ phone. Includes sample code. https://sedemo-mktb.rhcloud.com/
-
-## Usage
+
+## Two-Factor Usage
+
+Let's say you have a user that wants to enable two-factor authentication, and you intend to do two-factor authentication using an app like Google Authenticator, Duo Security, Authy, etc. This is a three-step process:
+
+1. Generate a secret
+2. Show a QR code for the user to scan in
+3. Authenticate the token
+
+### Generating a key
+
+Use Speakeasy's key generator to get a key.
+
+```js
+var secret = speakeasy.generateSecret();
+// Returns an object with secret.ascii, secret.hex, and secret.base32.
+// Also returns secret.google_auth_url, which we'll use later.
+```
+
+This will generate a secret key of length 32, which will be the secret key for the user. We don't want to set this as the user's secret key just yet – we first want to verify their token for the first time. So, store one of the encodings for the secret, preferably `secret.base32`, somewhere temporary.
+
+### Displaying a QR code
+
+Next, we'll want to display a QR code to the user so they can scan in the secret into their app. Google Authenticator and similar apps take in a QR code that holds a URL with the protocol `otpauth://`, which you get automatically from `secret.google_auth_url`.
+
+Use a QR code module to generate a QR code that stores the data in `secret.google_auth_url`, and then display the QR code to the user. This is one simple way to do it, which generates a PNG data URL which you can put into an `` tag on a webpage:
+
+```js
+// Use the node-qrcode package
+// npm install --save node-qrcode
+var QRCode = require('qrcode');
+
+// Get the data URL of the authenticator URL
+QRCode.toDataURL(secret.google_auth_url, function(err, data_url) {
+ console.log(data_url);
+});
+
+// Display this data URL to the user in an tag
+// Example:
+write('');
+```
+
+Ask the user to scan this QR code into their authenticator app.
+
+### Verifying the token
+
+Finally, we want to make sure that the token on the server side and the token on the client side match. The best practice is to do a token check before fully enabling two-factor authenticaton for the user. This code applies to the first and subsequent token checks.
+
+After the user scans the QR code, ask the user to enter in the token that they see in their app. Then, verify it against the secret.
+
+```js
+// Let's say the user says that the token they have is 132890
+userToken = 132890;
+```
+```js
+// Option A: use verify() to check the token against the secret
+var auth = speakeasy.totp.verify({ secret: secret.base32,
+ encoding: 'base32',
+ token: userToken });
+```
+```js
+// Option B: get the token at the current time and compare
+// to the token that the user gave
+var serverToken = speakeasy.totp({ secret: secret.base32,
+ encoding: 'base32' });
+var auth = userToken == serverToken;
+```
+
+`auth` will be true if the token is verified, false if not.
+
+If successfully verified, you can now save the secret to the user's account and use the same process above whenever you need to use two-factor to authenticate the user, like during login.
+
+Now you're done implementing two-factor authentication!
+
+
+## General Usage
```js
var speakeasy = require("speakeasy");
@@ -51,7 +125,7 @@ var speakeasy = require("speakeasy");
```js
// Generate a secret key.
-var secret = speakeasy.generate_key({length: 20});
+var secret = speakeasy.generateSecret({length: 20});
// Access using secret.ascii, secret.hex, or secret.base32.
```
@@ -242,7 +316,7 @@ it at counter position 7, it will return `{ delta: 2 }`.
Verify a time-based one-time token against the secret and return true if it
verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
instead of an object. For more on how to use a window with this, see
-[hotp.verifyDelta](hotp.verifyDelta).
+[hotp.verifyDelta](#hotp.verifyDelta).
**Kind**: global function
**Returns**: Boolean - Returns true if the token matches within the given
@@ -324,7 +398,7 @@ If it finds it at counter position 1002, it will return `{ delta: 2 }`.
### totp․verify(options) ⇒ Boolean
Verify a time-based one-time token against the secret and return true if it
verifies. Helper function for verifyDelta() that returns a boolean instead of
-an object. For more on how to use a window with this, see [totp.verify](totp.verify).
+an object. For more on how to use a window with this, see [totp.verifyDelta](#totp.verifyDelta).
**Kind**: global function
**Returns**: Boolean - Returns true if the token matches within the given
diff --git a/index.js b/index.js
index f4054ab..d96c7d5 100644
--- a/index.js
+++ b/index.js
@@ -339,7 +339,8 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) {
/**
* Verify a time-based one-time token against the secret and return true if it
* verifies. Helper function for verifyDelta() that returns a boolean instead of
- * an object. For more on how to use a window with this, see {@link totp.verify}.
+ * an object. For more on how to use a window with this, see
+ * {@link totp.verifyDelta}.
*
* @param {Object} options
* @param {String} options.secret Shared secret key
From a8c306ff14018cfd39ac59779ef5eadcd57c5213 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 21:55:52 -0500
Subject: [PATCH 71/85] [refactor] Conform to JavaScript Semistandard style
---
index.js | 100 ++++++++++++++---------------
test/generate.js | 24 ++++---
test/hotp_test.js | 36 +++++------
test/notp_test.js | 54 +++++++---------
test/rfc4226_test.js | 60 +++++++++--------
test/rfc6238_test.js | 129 +++++++++++++++++++------------------
test/totp_test.js | 38 ++++++-----
test/url_test.js | 150 +++++++++++++++++++++----------------------
8 files changed, 285 insertions(+), 306 deletions(-)
diff --git a/index.js b/index.js
index d96c7d5..7673489 100644
--- a/index.js
+++ b/index.js
@@ -1,9 +1,9 @@
-"use strict";
+'use strict';
-var base32 = require("base32.js");
-var crypto = require("crypto");
-var url = require("url");
-var util = require("util");
+var base32 = require('base32.js');
+var crypto = require('crypto');
+var url = require('url');
+var util = require('util');
/**
* Digest the one-time passcode options.
@@ -26,26 +26,25 @@ exports.digest = function digest (options) {
// unpack options
var key = options.secret;
var counter = options.counter;
- var encoding = options.encoding || "ascii";
- var algorithm = (options.algorithm || "sha1").toLowerCase();
+ var encoding = options.encoding || 'ascii';
+ var algorithm = (options.algorithm || 'sha1').toLowerCase();
// Backwards compatibility - deprecated
if (options.key) {
- console.log('Speakeasy - Deprecation Notice - Specifying the secret using `key` is no longer supported. Use `secret` instead.')
+ console.log('Speakeasy - Deprecation Notice - Specifying the secret using `key` is no longer supported. Use `secret` instead.');
key = options.key;
}
// convert key to buffer
if (!Buffer.isBuffer(key)) {
- key = encoding == "base32" ? base32.decode(key)
- : new Buffer(key, encoding);
+ key = encoding === 'base32' ? base32.decode(key)
+ : new Buffer(key, encoding);
}
// create an buffer from the counter
var buf = new Buffer(8);
var tmp = counter;
for (i = 0; i < 8; i++) {
-
// mask 0xff over number to get last 8
buf[7 - i] = tmp & 0xff;
@@ -84,7 +83,6 @@ exports.digest = function digest (options) {
*/
exports.hotp = function hotpGenerate (options) {
-
// unpack digits
// backward compatibility: `length` is also accepted here, but deprecated
var digits = (options.digits != null ? options.digits : options.length) || 6;
@@ -97,13 +95,13 @@ exports.hotp = function hotpGenerate (options) {
var offset = digest[digest.length - 1] & 0xf;
// calculate binary code (RFC4226 5.4)
- var code = (digest[offset] & 0x7f) << 24
- | (digest[offset + 1] & 0xff) << 16
- | (digest[offset + 2] & 0xff) << 8
- | (digest[offset + 3] & 0xff);
+ 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);
+ code = new Array(digits + 1).join('0') + code.toString(10);
// return length number off digits
return code.substr(-digits);
@@ -163,13 +161,13 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) {
// loop from C to C + W
for (i = counter; i <= counter + window; ++i) {
options.counter = i;
- if (exports.hotp(options) == token) {
+ if (exports.hotp(options) === token) {
// found a matching code, return delta
return {delta: i - counter};
}
}
- // no codes have matched
+// no codes have matched
};
/**
@@ -200,7 +198,7 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) {
*/
exports.hotp.verify = function hotpVerify (options) {
return exports.hotp.verifyDelta(options) != null;
-}
+};
/**
* Calculate counter value based on given options.
@@ -224,7 +222,7 @@ exports._counter = function _counter (options) {
// also accepts 'initial_time', but deprecated
var epoch = (options.epoch != null ? options.epoch : options.initial_time) || 0;
- if (options.initial_time) console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.')
+ if (options.initial_time) console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.');
return Math.floor((time - epoch) / step / 1000);
};
@@ -258,7 +256,6 @@ exports._counter = function _counter (options) {
*/
exports.totp = function totpGenerate (options) {
-
// shadow options
options = Object.create(options);
@@ -285,7 +282,7 @@ exports.time = exports.totp;
*
* `verifyDelta()` will return the delta between the counter value of the token
* and the given counter value. For example, if given a time at counter 1000 and
- * a window of 5, `verifyDelta()` will look at tokens from 995 to 1005,
+ * a window of 5, `verifyDelta()` will look at tokens from 995 to 1005,
* inclusive. In other words, if the time-step is 30 seconds, it will look at
* tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
* If it finds it at counter position 1002, it will return `{ delta: 2 }`.
@@ -318,7 +315,6 @@ exports.time = exports.totp;
*/
exports.totp.verifyDelta = function totpVerifyDelta (options) {
-
// shadow options
options = Object.create(options);
@@ -369,7 +365,7 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) {
*/
exports.totp.verify = function totpVerify (options) {
return exports.totp.verifyDelta(options) != null;
-}
+};
/**
* @typedef GeneratedSecret
@@ -409,9 +405,9 @@ exports.totp.verify = function totpVerify (options) {
*/
exports.generateSecret = function generateSecret (options) {
// options
- if(!options) options = {};
+ if (!options) options = {};
var length = options.length || 32;
- var name = encodeURIComponent(options.name) || "SecretKey";
+ var name = encodeURIComponent(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 != null ? options.google_auth_url : true;
@@ -429,7 +425,7 @@ exports.generateSecret = function generateSecret (options) {
var SecretKey = {};
SecretKey.ascii = key;
SecretKey.hex = Buffer(key, 'ascii').toString('hex');
- SecretKey.base32 = base32.encode(Buffer(key)).toString().replace(/=/g,'');
+ SecretKey.base32 = base32.encode(Buffer(key)).toString().replace(/=/g, '');
// generate some qr codes if requested
if (qr_codes) {
@@ -457,7 +453,7 @@ exports.generateSecret = function generateSecret (options) {
};
// Backwards compatibility - generate_key is deprecated
-exports.generate_key = util.deprecate(function(options) {
+exports.generate_key = util.deprecate(function (options) {
return exports.generateSecret(options);
}, 'Speakeasy - Deprecation Notice - `generate_key()` is depreciated, please use `generateSecret()` instead.');
@@ -469,7 +465,7 @@ exports.generate_key = util.deprecate(function(options) {
* @param {Boolean} [symbols=false] Whether to include symbols in the key.
* @return {String} The generated key.
*/
-exports.generateSecretASCII = function generateSecretASCII(length, symbols) {
+exports.generateSecretASCII = function generateSecretASCII (length, symbols) {
var bytes = crypto.randomBytes(length || 32);
var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz';
if (symbols) {
@@ -478,13 +474,13 @@ exports.generateSecretASCII = function generateSecretASCII(length, symbols) {
var output = '';
for (var i = 0, l = bytes.length; i < l; i++) {
- output += set[Math.floor(bytes[i] / 255.0 * (set.length-1))];
+ output += set[Math.floor(bytes[i] / 255.0 * (set.length - 1))];
}
return output;
};
// Backwards compatibility - generate_key_ascii is deprecated
-exports.generate_key_ascii = util.deprecate(function(length, symbols) {
+exports.generate_key_ascii = util.deprecate(function (length, symbols) {
return exports.generateSecretASCII(length, symbols);
}, 'Speakeasy - Deprecation Notice - `generate_key_ascii()` is depreciated, please use `generateSecretASCII()` instead.');
@@ -521,13 +517,12 @@ exports.generate_key_ascii = util.deprecate(function(length, symbols) {
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*/
-exports.googleAuthURL = function googleAuthURL(options) {
-
+exports.googleAuthURL = function googleAuthURL (options) {
// unpack options
var secret = options.secret;
var label = options.label;
var issuer = options.issuer;
- var type = (options.type || "totp").toLowerCase();
+ var type = (options.type || 'totp').toLowerCase();
var counter = options.counter;
var algorithm = options.algorithm;
var digits = options.digits;
@@ -536,35 +531,35 @@ exports.googleAuthURL = function googleAuthURL(options) {
// validate type
switch (type) {
- case "totp":
- case "hotp":
+ case 'totp':
+ case 'hotp':
break;
default:
- throw new Error("invalid type `" + type + "`");
+ throw new Error('invalid type `' + type + '`');
}
// validate required options
- if (!secret) throw new Error("missing secret");
- if (!label) throw new Error("missing label");
+ if (!secret) throw new Error('missing secret');
+ if (!label) throw new Error('missing label');
// require counter for HOTP
- if (type == "hotp" && counter == null) {
- throw new Error("missing counter value for HOTP");
+ if (type === 'hotp' && (counter === null || typeof counter === 'undefined')) {
+ throw new Error('missing counter value for HOTP');
}
// build query while validating
var query = {secret: secret};
- if (options.issuer) query.issuer = options.issuer;
+ if (issuer) query.issuer = issuer;
// validate algorithm
if (algorithm != null) {
switch (algorithm.toUpperCase()) {
- case "SHA1":
- case "SHA256":
- case "SHA512":
+ case 'SHA1':
+ case 'SHA256':
+ case 'SHA512':
break;
default:
- throw new Error("invalid algorithm `" + algorithm + "`");
+ throw new Error('invalid algorithm `' + algorithm + '`');
}
query.algorithm = algorithm.toUpperCase();
}
@@ -576,26 +571,27 @@ exports.googleAuthURL = function googleAuthURL(options) {
case 8:
break;
default:
- throw new Error("invalid digits `" + digits + "`");
+ throw new Error('invalid digits `' + digits + '`');
}
query.digits = digits;
}
// validate period
if (period != null) {
- if (~~period != period) {
- throw new Error("invalid period `" + period + "`");
+ period = parseInt(period, 10);
+ 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 (encoding !== 'base32') secret = new Buffer(secret, encoding);
if (Buffer.isBuffer(secret)) secret = base32.encode(secret);
// return url
return url.format({
- protocol: "otpauth",
+ protocol: 'otpauth',
slashes: true,
hostname: type,
pathname: label,
diff --git a/test/generate.js b/test/generate.js
index 0ced736..19a3acd 100644
--- a/test/generate.js
+++ b/test/generate.js
@@ -1,4 +1,4 @@
-"use strict";
+'use strict';
/* global describe, it */
@@ -11,17 +11,16 @@ var speakeasy = require('..');
// 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');
// 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')
+ 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');
@@ -40,19 +39,19 @@ describe('Generator tests', function () {
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')
+ 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')
+ 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')
+ assert.isDefined(secret.google_auth_qr, 'Google Auth QR should be returned');
});
it('Testing generate_key_ascii with defaults', function () {
@@ -66,5 +65,4 @@ describe('Generator tests', function () {
assert.equal(secret.length, 20, 'Should return the correct length');
assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key');
});
-
});
diff --git a/test/hotp_test.js b/test/hotp_test.js
index 73eb4d0..b3cf2e4 100644
--- a/test/hotp_test.js
+++ b/test/hotp_test.js
@@ -1,4 +1,4 @@
-"use strict";
+'use strict';
/* global describe, it */
@@ -10,61 +10,59 @@ var speakeasy = require('..');
// 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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() {
+ 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 = \'12345678901234567890\' at counter 3', function () {
- it('should return correct one-time password', function() {
+ describe("base32 encoding 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('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() {
+ 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: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha256'});
assert.equal(topic, '46119246');
});
});
- 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() {
+ 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: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha512'});
assert.equal(topic, '90693936');
});
});
-
});
diff --git a/test/notp_test.js b/test/notp_test.js
index 33632f9..4f391e8 100644
--- a/test/notp_test.js
+++ b/test/notp_test.js
@@ -1,6 +1,6 @@
-"use strict";
+'use strict';
-/* global describe, it */
+/* global it */
var chai = require('chai');
var assert = chai.assert;
@@ -52,12 +52,12 @@ var speakeasy = require('..');
* see http://tools.ietf.org/html/rfc4226
*/
-it("HOTP", function() {
+it('HOTP', function () {
var options = {
secret: '12345678901234567890',
window: 0
};
- var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489'];
+ 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';
@@ -68,7 +68,7 @@ it("HOTP", function() {
assert.ok(!speakeasy.hotp.verify(options), 'Should not pass');
// countercheck for passes
- for(var i=0;i= 3');
});
-
-it("hotp_gen", function() {
+it('hotp_gen', function () {
var options = {
secret: '12345678901234567890',
window: 0
};
- var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489'];
+ var HOTP = ['755224', '287082', '359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489'];
// make sure we can not pass in opt
speakeasy.hotp(options);
// countercheck for passes
- for(var i=0;i
Date: Sat, 23 Jan 2016 22:02:55 -0500
Subject: [PATCH 72/85] [refactor] Rename googleAuthURL to otpauthURL
---
README.md | 20 +--
asdf.md | 336 +++++++++++++++++++++++++++++++++++++++++++++++
index.js | 17 +--
test/generate.js | 8 +-
test/url_test.js | 48 +++----
5 files changed, 383 insertions(+), 46 deletions(-)
create mode 100644 asdf.md
diff --git a/README.md b/README.md
index 0e81006..ec4ba46 100644
--- a/README.md
+++ b/README.md
@@ -56,16 +56,16 @@ Use Speakeasy's key generator to get a key.
```js
var secret = speakeasy.generateSecret();
// Returns an object with secret.ascii, secret.hex, and secret.base32.
-// Also returns secret.google_auth_url, which we'll use later.
+// Also returns secret.otpauth_url, which we'll use later.
```
This will generate a secret key of length 32, which will be the secret key for the user. We don't want to set this as the user's secret key just yet – we first want to verify their token for the first time. So, store one of the encodings for the secret, preferably `secret.base32`, somewhere temporary.
### Displaying a QR code
-Next, we'll want to display a QR code to the user so they can scan in the secret into their app. Google Authenticator and similar apps take in a QR code that holds a URL with the protocol `otpauth://`, which you get automatically from `secret.google_auth_url`.
+Next, we'll want to display a QR code to the user so they can scan in the secret into their app. Google Authenticator and similar apps take in a QR code that holds a URL with the protocol `otpauth://`, which you get automatically from `secret.otpauth_url`.
-Use a QR code module to generate a QR code that stores the data in `secret.google_auth_url`, and then display the QR code to the user. This is one simple way to do it, which generates a PNG data URL which you can put into an `` tag on a webpage:
+Use a QR code module to generate a QR code that stores the data in `secret.otpauth_url`, and then display the QR code to the user. This is one simple way to do it, which generates a PNG data URL which you can put into an `` tag on a webpage:
```js
// Use the node-qrcode package
@@ -73,7 +73,7 @@ Use a QR code module to generate a QR code that stores the data in `secret.googl
var QRCode = require('qrcode');
// Get the data URL of the authenticator URL
-QRCode.toDataURL(secret.google_auth_url, function(err, data_url) {
+QRCode.toDataURL(secret.otpauth_url, function(err, data_url) {
console.log(data_url);
});
@@ -232,7 +232,7 @@ an object. For more on how to use a window with this, see
Generate an URL for use with the Google Authenticator app.
@@ -433,7 +433,7 @@ Authenticator URL to obtain a QR code you can scan into the app.
| options | Object | | |
| [options.length] | Integer | 32 | Length of the secret |
| [options.symbols] | Boolean | false | Whether to include symbols |
-| [options.google_auth_url] | Boolean | true | Whether to output a Google Authenticator otpauth:// URL (only returns otpauth:// URL, no QR code) |
+| [options.otpauth_url] | Boolean | true | Whether to output a Google Authenticator-compatible otpauth:// URL (only returns otpauth:// URL, no QR code) |
| [options.name] | String | | The name to use with Google Authenticator. |
| [options.qr_codes] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output QR code URLs for the token. |
| [options.google_auth_qr] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output a Google Authenticator otpauth:// QR code URL. |
@@ -451,8 +451,8 @@ symbols (if requested).
| [length] | Integer | 32 | The length of the key. |
| [symbols] | Boolean | false | Whether to include symbols in the key. |
-
-### googleAuthURL(options) ⇒ String
+
+### otpauthURL(options) ⇒ String
Generate an URL for use with the Google Authenticator app.
Authenticator considers TOTP codes valid for 30 seconds. Additionally,
@@ -494,7 +494,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. |
+| otpauth_url | String | Google Authenticator-compatible otpauth URL. |
## Contributing
@@ -526,4 +526,4 @@ Project.
[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
+[mikepb]: https://github.com/mikepb
diff --git a/asdf.md b/asdf.md
new file mode 100644
index 0000000..38f6393
--- /dev/null
+++ b/asdf.md
@@ -0,0 +1,336 @@
+## Functions
+
+
Verify a counter-based one-time token against the secret and return the delta.
+By default, it verifies the token at the given counter value, with no leeway
+(no look-ahead or look-behind). A token validated at the current counter value
+will have a delta of 0.
+
You can specify a window to add more leeway to the verification process.
+Setting the window param will check for the token at the given counter value
+as well as window tokens ahead (one-sided window). See param for more info.
+
verifyDelta() will return the delta between the counter value of the token
+and the given counter value. For example, if given a counter 5 and a window
+10, verifyDelta() will look at tokens from 5 to 15, inclusive. If it finds
+it at counter position 7, it will return { delta: 2 }.
Verify a time-based one-time token against the secret and return true if it
+verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
+instead of an object. For more on how to use a window with this, see
+hotp.verifyDelta.
Verify a time-based one-time token against the secret and return the delta.
+By default, it verifies the token at the current time window, with no leeway
+(no look-ahead or look-behind). A token validated at the current time window
+will have a delta of 0.
+
You can specify a window to add more leeway to the verification process.
+Setting the window param will check for the token at the given counter value
+as well as window tokens ahead and window tokens behind (two-sided
+window). See param for more info.
+
verifyDelta() will return the delta between the counter value of the token
+and the given counter value. For example, if given a time at counter 1000 and
+a window of 5, verifyDelta() will look at tokens from 995 to 1005,
+inclusive. In other words, if the time-step is 30 seconds, it will look at
+tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
+If it finds it at counter position 1002, it will return { delta: 2 }.
Verify a time-based one-time token against the secret and return true if it
+verifies. Helper function for verifyDelta() that returns a boolean instead of
+an object. For more on how to use a window with this, see
+totp.verifyDelta.
Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length
+(default 32). Returns the secret key in ASCII, hexadecimal, and base32 format,
+along with the URL used for the QR code for Google Authenticator (an otpauth
+URL). Use a QR code library to generate a QR code based on the Google
+Authenticator URL to obtain a QR code you can scan into the app.
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.
+
+
+## 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 token.
+
+**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 token against the secret and return the delta.
+By default, it verifies the token at the given counter value, with no leeway
+(no look-ahead or look-behind). A token validated at the current counter value
+will have a delta of 0.
+
+You can specify a window to add more leeway to the verification process.
+Setting the window param will check for the token at the given counter value
+as well as `window` tokens ahead (one-sided window). See param for more info.
+
+`verifyDelta()` will return the delta between the counter value of the token
+and the given counter value. For example, if given a counter 5 and a window
+10, `verifyDelta()` will look at tokens from 5 to 15, inclusive. If it finds
+it at counter position 7, it will return `{ delta: 2 }`.
+
+**Kind**: global function
+**Returns**: Object - On success, returns an object with the counter
+ difference between the client and the server as the `delta` property (i.e.
+ `{ delta: 0 }`).
+
+| 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 time-based one-time token against the secret and return true if it
+verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
+instead of an object. For more on how to use a window with this, see
+[hotp.verifyDelta](hotp.verifyDelta).
+
+**Kind**: global function
+**Returns**: Boolean - Returns true if the token matches within the given
+ 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 token. By default, it returns the token for
+the current time.
+
+**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 token against the secret and return the delta.
+By default, it verifies the token at the current time window, with no leeway
+(no look-ahead or look-behind). A token validated at the current time window
+will have a delta of 0.
+
+You can specify a window to add more leeway to the verification process.
+Setting the window param will check for the token at the given counter value
+as well as `window` tokens ahead and `window` tokens behind (two-sided
+window). See param for more info.
+
+`verifyDelta()` will return the delta between the counter value of the token
+and the given counter value. For example, if given a time at counter 1000 and
+a window of 5, `verifyDelta()` will look at tokens from 995 to 1005,
+inclusive. In other words, if the time-step is 30 seconds, it will look at
+tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
+If it finds it at counter position 1002, it will return `{ delta: 2 }`.
+
+**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 (e.g.
+ `{ delta: 0 }`).
+
+| 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 token against the secret and return true if it
+verifies. Helper function for verifyDelta() that returns a boolean instead of
+an object. For more on how to use a window with this, see
+[totp.verifyDelta](totp.verifyDelta).
+
+**Kind**: global function
+**Returns**: Boolean - Returns true if the token matches within the given
+ window, 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). |
+
+
+## generateSecret(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,
+along with the URL used for the QR code for Google Authenticator (an otpauth
+URL). Use a QR code library to generate a QR code based on the Google
+Authenticator URL to obtain a QR code you can scan into the app.
+
+**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.otpauth_url] | Boolean | true | Whether to output a Google Authenticator-compatible otpauth:// URL (only returns otpauth:// URL, no QR code) |
+| [options.name] | String | | The name to use with Google Authenticator. |
+| [options.qr_codes] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output QR code URLs for the token. |
+| [options.google_auth_qr] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output a Google Authenticator otpauth:// QR code URL. |
+
+
+## generateSecretASCII([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. |
+
+
+## otpauthURL(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. |
+| otpauth_url | String | Google Authenticator-compatible otpauth URL. |
+
diff --git a/index.js b/index.js
index 7673489..6a8f3cf 100644
--- a/index.js
+++ b/index.js
@@ -378,7 +378,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.
+ * @property {String} otpauth_url Google Authenticator-compatible otpauth URL.
*/
/**
@@ -391,8 +391,9 @@ exports.totp.verify = function totpVerify (options) {
* @param {Object} options
* @param {Integer} [options.length=32] Length of the secret
* @param {Boolean} [options.symbols=false] Whether to include symbols
- * @param {Boolean} [options.google_auth_url=true] Whether to output a Google
- * Authenticator otpauth:// URL (only returns otpauth:// URL, no QR code)
+ * @param {Boolean} [options.otpauth_url=true] Whether to output a Google
+ * Authenticator-compatible otpauth:// URL (only returns otpauth:// URL, no
+ * QR code)
* @param {String} [options.name] The name to use with Google Authenticator.
* @param {Boolean} [options.qr_codes=false] (DEPRECATED. Do not use to prevent
* leaking of secret to a third party. Use your own QR code implementation.)
@@ -410,7 +411,7 @@ exports.generateSecret = function generateSecret (options) {
var name = encodeURIComponent(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 != null ? options.google_auth_url : true;
+ var otpauth_url = options.otpauth_url != null ? options.otpauth_url : true;
var symbols = true;
// turn off symbols only when explicity told to
@@ -436,8 +437,8 @@ exports.generateSecret = function generateSecret (options) {
}
// add in the Google Authenticator-compatible otpauth URL
- if (google_auth_url) {
- SecretKey.google_auth_url = exports.googleAuthURL({
+ if (otpauth_url) {
+ SecretKey.otpauth_url = exports.otpauthURL({
secret: SecretKey.base32,
label: name
});
@@ -446,7 +447,7 @@ exports.generateSecret = function generateSecret (options) {
// generate a QR code for use in Google Authenticator if requested
if (google_auth_qr) {
console.log('Speakeasy - Deprecation Notice - generateSecret() Google Auth QR code is deprecated and no longer supported. Please use your own QR code implementation.');
- SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.googleAuthURL({ secret: SecretKey.base32, label: name }));
+ SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.otpauthURL({ secret: SecretKey.base32, label: name }));
}
return SecretKey;
@@ -517,7 +518,7 @@ exports.generate_key_ascii = util.deprecate(function (length, symbols) {
* @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format
*/
-exports.googleAuthURL = function googleAuthURL (options) {
+exports.otpauthURL = function otpauthURL (options) {
// unpack options
var secret = options.secret;
var label = options.label;
diff --git a/test/generate.js b/test/generate.js
index 19a3acd..c1ac06b 100644
--- a/test/generate.js
+++ b/test/generate.js
@@ -16,7 +16,7 @@ describe('Generator tests', function () {
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.isDefined(secret.otpauth_url, 'otpauth:// 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');
@@ -44,9 +44,9 @@ describe('Generator tests', function () {
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 otpath:// URL output disabled', function () {
+ var secret = speakeasy.generate_key({otpauth_url: false});
+ assert.isUndefined(secret.otpauth_url, 'Google Auth URL should not be returned');
});
it('Generation with Google Auth QR URL output enabled', function () {
diff --git a/test/url_test.js b/test/url_test.js
index 874c84f..afee050 100644
--- a/test/url_test.js
+++ b/test/url_test.js
@@ -10,13 +10,13 @@ var url = require('url');
describe('#url', function () {
it('should require options', function () {
assert.throws(function () {
- speakeasy.googleAuthURL();
+ speakeasy.otpauthURL();
});
});
it('should validate type', function () {
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
type: 'haha',
secret: 'hello',
label: 'that'
@@ -26,7 +26,7 @@ describe('#url', function () {
it('should require secret', function () {
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
label: 'that'
}, /missing secret/);
});
@@ -34,7 +34,7 @@ describe('#url', function () {
it('should require label', function () {
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
secret: 'hello'
}, /missing label/);
});
@@ -42,19 +42,19 @@ describe('#url', function () {
it('should require counter for HOTP', function () {
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
type: 'hotp',
secret: 'hello',
label: 'that'
}, /missing counter/);
});
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
type: 'hotp',
secret: 'hello',
label: 'that',
counter: 0
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
type: 'hotp',
secret: 'hello',
label: 'that',
@@ -64,23 +64,23 @@ describe('#url', function () {
it('should validate algorithm', function () {
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
algorithm: 'hello'
}, /invalid algorithm `hello`/);
});
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
algorithm: 'sha1'
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
algorithm: 'sha256'
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
algorithm: 'sha512'
@@ -89,42 +89,42 @@ describe('#url', function () {
it('should validate digits', function () {
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
digits: 'hello'
}, /invalid digits `hello`/);
});
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
digits: 12
}, /invalid digits `12`/);
});
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
digits: '7'
}, /invalid digits `7`/);
});
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
digits: 6
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
digits: 8
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
digits: '6'
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
digits: '8'
@@ -133,28 +133,28 @@ describe('#url', function () {
it('should validate period', function () {
assert.throws(function () {
- speakeasy.googleAuthURL({
+ speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
period: 'hello'
}, /invalid period `hello`/);
});
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
period: 60
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
period: 121
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
period: '60'
}));
- assert.ok(speakeasy.googleAuthURL({
+ assert.ok(speakeasy.otpauthURL({
secret: 'hello',
label: 'that',
period: '121'
@@ -162,7 +162,7 @@ describe('#url', function () {
});
it('should generate an URL compatible with the Google Authenticator app', function () {
- var answer = speakeasy.googleAuthURL({
+ var answer = speakeasy.otpauthURL({
secret: 'JBSWY3DPEHPK3PXP',
label: 'Example:alice@google.com',
issuer: 'Example',
From 271234f99f8833efc1e205b6de160f53adbf0813 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 22:20:22 -0500
Subject: [PATCH 73/85] [refactor] TOTP - time param is now defined in seconds,
not milliseconds, to keep in line with 1.0 API
---
index.js | 24 ++++++++++++------------
test/notp_test.js | 18 +++++++++---------
test/rfc6238_test.js | 38 +++++++++++++++++++-------------------
test/totp_test.js | 40 ++++++++++++++++++++--------------------
4 files changed, 60 insertions(+), 60 deletions(-)
diff --git a/index.js b/index.js
index 6a8f3cf..62e3ae7 100644
--- a/index.js
+++ b/index.js
@@ -171,8 +171,8 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) {
};
/**
- * Verify a time-based one-time token against the secret and return true if it
- * verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
+ * Verify a counter-based one-time token against the secret and return true if
+ * it verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
* instead of an object. For more on how to use a window with this, see
* {@link hotp.verifyDelta}.
*
@@ -204,8 +204,8 @@ 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.
- * Defaults to `Date.now()`.
+ * @param {Integer} [options.time] Time in seconds with which to calculate
+ * counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
* @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from
* which to calculate the counter value. Defaults to 0 (no offset).
@@ -218,10 +218,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 time = options.time != null ? (options.time * 1000) : Date.now();
// also accepts 'initial_time', but deprecated
- var epoch = (options.epoch != null ? options.epoch : options.initial_time) || 0;
+ var epoch = (options.epoch != null ? (options.epoch * 1000) : (options.initial_time * 1000)) || 0;
if (options.initial_time) console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.');
return Math.floor((time - epoch) / step / 1000);
@@ -233,11 +233,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.
- * Defaults to `Date.now()`.
+ * @param {Integer} [options.time] Time in seconds with which to calculate
+ * counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
- * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from
- * which to calculate the counter value. Defaults to 0 (no offset).
+ * @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX
+ * epoch from which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.counter] Counter value, calculated by default.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
@@ -248,8 +248,8 @@ exports._counter = function _counter (options) {
* @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
- * value. Defaults to 0 (no offset).
+ * Initial time in seconds since the UNIX epoch from which to calculate the
+ * counter value. Defaults to 0 (no offset).
* @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The
* number of digits for the one-time passcode.
* @return {String} The one-time passcode.
diff --git a/test/notp_test.js b/test/notp_test.js
index 4f391e8..64d958b 100644
--- a/test/notp_test.js
+++ b/test/notp_test.js
@@ -106,7 +106,7 @@ it('TOTtoken', function () {
assert.ok(!speakeasy.totp.verifyDelta(options), 'Should not pass');
// countercheck for test vector at 59s with verifyDelta
- options.time = 59000;
+ options.time = 59;
options.token = '287082';
var res = speakeasy.totp.verifyDelta(options);
assert.ok(res, 'Should pass');
@@ -117,7 +117,7 @@ it('TOTtoken', function () {
assert.ok(res, 'Should pass');
// countercheck for test vector at 1234567890 with delta
- options.time = 1234567890000;
+ options.time = 1234567890;
options.token = '005924';
res = speakeasy.totp.verifyDelta(options);
assert.ok(res, 'Should pass');
@@ -128,7 +128,7 @@ it('TOTtoken', function () {
assert.ok(res, 'Should pass');
// countercheck for test vector at 1111111109 with delta
- options.time = 1111111109000;
+ options.time = 1111111109;
options.token = '081804';
res = speakeasy.totp.verifyDelta(options);
assert.ok(res, 'Should pass');
@@ -139,7 +139,7 @@ it('TOTtoken', function () {
assert.ok(res, 'Should pass');
// countercheck for test vector at 2000000000 with delta
- options.time = 2000000000000;
+ options.time = 2000000000;
options.token = '279037';
res = speakeasy.totp.verifyDelta(options);
assert.ok(res, 'Should pass');
@@ -208,7 +208,7 @@ it('TOTPOutOfSync', function () {
var options = {
secret: '12345678901234567890',
token: '279037',
- time: 1999999909000
+ time: 1999999909
};
// countercheck that the test should fail for window < 2
@@ -248,18 +248,18 @@ it('totp_gen', function () {
speakeasy.totp(options);
// countercheck for test vector at 59s
- options.time = 59000;
+ options.time = 59;
assert.equal(speakeasy.totp(options), '287082', 'TOTtoken values should match');
// countercheck for test vector at 1234567890
- options.time = 1234567890000;
+ options.time = 1234567890;
assert.equal(speakeasy.totp(options), '005924', 'TOTtoken values should match');
// countercheck for test vector at 1111111109
- options.time = 1111111109000;
+ options.time = 1111111109;
assert.equal(speakeasy.totp(options), '081804', 'TOTtoken values should match');
// countercheck for test vector at 2000000000
- options.time = 2000000000000;
+ options.time = 2000000000;
assert.equal(speakeasy.totp(options), '279037', 'TOTtoken values should match');
});
diff --git a/test/rfc6238_test.js b/test/rfc6238_test.js
index 56fe9e1..0905f82 100644
--- a/test/rfc6238_test.js
+++ b/test/rfc6238_test.js
@@ -64,109 +64,109 @@ var speakeasy = require('..');
describe('RFC 6238 test vector', function () {
[{
- time: 59000,
+ time: 59,
date: new Date('1970-01-01T00:00:59Z'),
counter: 0x01,
code: '94287082',
algorithm: 'SHA1'
}, {
- time: 59000,
+ time: 59,
date: new Date('1970-01-01T00:00:59Z'),
counter: 0x01,
code: '46119246',
algorithm: 'SHA256'
}, {
- time: 59000,
+ time: 59,
date: new Date('1970-01-01T00:00:59Z'),
counter: 0x01,
code: '90693936',
algorithm: 'SHA512'
}, {
- time: 1111111109000,
+ time: 1111111109,
date: new Date('2005-03-18T01:58:29Z'),
counter: 0x023523EC,
code: '07081804',
algorithm: 'SHA1'
}, {
- time: 1111111109000,
+ time: 1111111109,
date: new Date('2005-03-18T01:58:29Z'),
counter: 0x023523EC,
code: '68084774',
algorithm: 'SHA256'
}, {
- time: 1111111109000,
+ time: 1111111109,
date: new Date('2005-03-18T01:58:29Z'),
counter: 0x023523EC,
code: '25091201',
algorithm: 'SHA512'
}, {
- time: 1111111111000,
+ time: 1111111111,
date: new Date('2005-03-18T01:58:31Z'),
counter: 0x023523ED,
code: '14050471',
algorithm: 'SHA1'
}, {
- time: 1111111111000,
+ time: 1111111111,
date: new Date('2005-03-18T01:58:31Z'),
counter: 0x023523ED,
code: '67062674',
algorithm: 'SHA256'
}, {
- time: 1111111111000,
+ time: 1111111111,
date: new Date('2005-03-18T01:58:31Z'),
counter: 0x023523ED,
code: '99943326',
algorithm: 'SHA512'
}, {
- time: 1234567890000,
+ time: 1234567890,
date: new Date('2009-02-13T23:31:30Z'),
counter: 0x0273EF07,
code: '89005924',
algorithm: 'SHA1'
}, {
- time: 1234567890000,
+ time: 1234567890,
date: new Date('2009-02-13T23:31:30Z'),
counter: 0x0273EF07,
code: '91819424',
algorithm: 'SHA256'
}, {
- time: 1234567890000,
+ time: 1234567890,
date: new Date('2009-02-13T23:31:30Z'),
counter: 0x0273EF07,
code: '93441116',
algorithm: 'SHA512'
}, {
- time: 2000000000000,
+ time: 2000000000,
date: new Date('2033-05-18T03:33:20Z'),
counter: 0x03F940AA,
code: '69279037',
algorithm: 'SHA1'
}, {
- time: 2000000000000,
+ time: 2000000000,
date: new Date('2033-05-18T03:33:20Z'),
counter: 0x03F940AA,
code: '90698825',
algorithm: 'SHA256'
}, {
- time: 2000000000000,
+ time: 2000000000,
date: new Date('2033-05-18T03:33:20Z'),
counter: 0x03F940AA,
code: '38618901',
algorithm: 'SHA512'
}, {
- time: 20000000000000,
+ time: 20000000000,
date: new Date('2603-10-11T11:33:20Z'),
counter: 0x27BC86AA,
code: '65353130',
algorithm: 'SHA1'
}, {
- time: 20000000000000,
+ time: 20000000000,
date: new Date('2603-10-11T11:33:20Z'),
counter: 0x27BC86AA,
code: '77737706',
algorithm: 'SHA256'
}, {
- time: 20000000000000,
+ time: 20000000000,
date: new Date('2603-10-11T11:33:20Z'),
counter: 0x27BC86AA,
code: '47863826',
@@ -203,7 +203,7 @@ describe('RFC 6238 test vector', function () {
it('should calculate counter value for date ' + subject.date, function () {
var counter = speakeasy._counter({
- time: subject.date
+ time: Math.floor(subject.date / 1000)
});
assert.equal(counter, subject.counter);
});
diff --git a/test/totp_test.js b/test/totp_test.js
index ad08c65..cde73a4 100644
--- a/test/totp_test.js
+++ b/test/totp_test.js
@@ -11,79 +11,79 @@ var speakeasy = require('..');
// 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 () {
+ describe("normal operation with secret = '12345678901234567890' at time = 59", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: '12345678901234567890', time: 59000});
+ var topic = speakeasy.totp({secret: '12345678901234567890', time: 59});
assert.equal(topic, '287082');
});
});
- describe("normal operation with secret = '12345678901234567890' at time = 59000 using key (deprecated)", function () {
+ describe("normal operation with secret = '12345678901234567890' at time = 59 using key (deprecated)", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({key: '12345678901234567890', time: 59000});
+ var topic = speakeasy.totp({key: '12345678901234567890', time: 59});
assert.equal(topic, '287082');
});
});
- describe("a different time normal operation with secret = '12345678901234567890' at time = 1111111109000", function () {
+ describe("a different time normal operation with secret = '12345678901234567890' at time = 1111111109", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000});
+ var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109});
assert.equal(topic, '081804');
});
});
- describe("digits parameter with secret = '12345678901234567890' at time = 1111111109000 and digits = 8", function () {
+ describe("digits parameter with secret = '12345678901234567890' at time = 1111111109 and digits = 8", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, digits: 8});
+ var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109, 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});
+ var topic = speakeasy.totp({secret: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109});
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});
+ var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109});
assert.equal(topic, '081804');
});
});
- describe("a custom step with secret = '12345678901234567890' at time = 1111111109000 with step = 60", function () {
+ describe("a custom step with secret = '12345678901234567890' at time = 1111111109 with step = 60", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, step: 60});
+ var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109, step: 60});
assert.equal(topic, '360094');
});
});
- describe("initial time with secret = '12345678901234567890' at time = 1111111109000 and epoch = 1111111100000", function () {
+ describe("initial time with secret = '12345678901234567890' at time = 1111111109 and epoch = 1111111100", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, epoch: 1111111100000});
+ var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109, epoch: 1111111100});
assert.equal(topic, '755224');
});
});
- describe("base32 encoding with secret = '1234567890' at time = 1111111109000", function () {
+ describe("base32 encoding with secret = '1234567890' at time = 1111111109", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000});
+ var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109});
assert.equal(topic, '081804');
});
});
- describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA' as base32 at time = 1111111109000, digits = 8 and algorithm as 'sha256'", function () {
+ describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA' as base32 at time = 1111111109, digits = 8 and algorithm as 'sha256'", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha256'});
+ var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', time: 1111111109, digits: 8, algorithm: 'sha256'});
assert.equal(topic, '68084774');
});
});
- describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA' as base32 at time = 1111111109000, digits = 8 and algorithm as 'sha512'", function () {
+ describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA' as base32 at time = 1111111109, digits = 8 and algorithm as 'sha512'", function () {
it('should return correct one-time password', function () {
- var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha512'});
+ var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', time: 1111111109, digits: 8, algorithm: 'sha512'});
assert.equal(topic, '25091201');
});
});
From 83e7c71f640bcba72f2b63f8c43b685e8d66c019 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sat, 23 Jan 2016 22:25:33 -0500
Subject: [PATCH 74/85] [readme] Fix README typos and JSDoc updates
---
README.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index ec4ba46..69023e9 100644
--- a/README.md
+++ b/README.md
@@ -209,7 +209,7 @@ Full API documentation (in JSDoc format) is available below and at http://speake
Verify a counter-based one-time token against the secret and return the delta.
@@ -313,7 +313,7 @@ it at counter position 7, it will return `{ delta: 2 }`.
### hotp․verify(options) ⇒ Boolean
-Verify a time-based one-time token against the secret and return true if it
+Verify a counter-based one-time token against the secret and return true if it
verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
instead of an object. For more on how to use a window with this, see
[hotp.verifyDelta](#hotp.verifyDelta).
@@ -345,7 +345,7 @@ the current time.
| --- | --- | --- | --- |
| options | Object | | |
| options.secret | String | | Shared secret key |
-| [options.time] | Integer | | Time with which to calculate counter value. Defaults to `Date.now()`. |
+| [options.time] | Integer | | Time in seconds 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. |
@@ -385,7 +385,7 @@ If it finds it at counter position 1002, it will return `{ delta: 2 }`.
| 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.time] | Integer | | Time in seconds 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. |
@@ -409,7 +409,7 @@ an object. For more on how to use a window with this, see [totp.verifyDelta](#to
| 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.time] | Integer | | Time in seconds 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. |
From 37d4937122cfcd4cb96f64db3e371dfe424a4691 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 12:21:40 -0500
Subject: [PATCH 75/85] [fix] Minor changes after code review - Remove asdf.md
jsdoc2md generated file - Generate - Replace deprecated generate_key_ascii
with generateSecretASCII - README - Correct closure placement in two-factor
guide
---
README.md | 10 +-
asdf.md | 336 ------------------------------------------------------
index.js | 2 +-
3 files changed, 6 insertions(+), 342 deletions(-)
delete mode 100644 asdf.md
diff --git a/README.md b/README.md
index 69023e9..c747fd8 100644
--- a/README.md
+++ b/README.md
@@ -74,12 +74,12 @@ var QRCode = require('qrcode');
// Get the data URL of the authenticator URL
QRCode.toDataURL(secret.otpauth_url, function(err, data_url) {
- console.log(data_url);
+ console.log(data_url);
+
+ // Display this data URL to the user in an tag
+ // Example:
+ write('');
});
-
-// Display this data URL to the user in an tag
-// Example:
-write('');
```
Ask the user to scan this QR code into their authenticator app.
diff --git a/asdf.md b/asdf.md
deleted file mode 100644
index 38f6393..0000000
--- a/asdf.md
+++ /dev/null
@@ -1,336 +0,0 @@
-## Functions
-
-
Verify a counter-based one-time token against the secret and return the delta.
-By default, it verifies the token at the given counter value, with no leeway
-(no look-ahead or look-behind). A token validated at the current counter value
-will have a delta of 0.
-
You can specify a window to add more leeway to the verification process.
-Setting the window param will check for the token at the given counter value
-as well as window tokens ahead (one-sided window). See param for more info.
-
verifyDelta() will return the delta between the counter value of the token
-and the given counter value. For example, if given a counter 5 and a window
-10, verifyDelta() will look at tokens from 5 to 15, inclusive. If it finds
-it at counter position 7, it will return { delta: 2 }.
Verify a time-based one-time token against the secret and return true if it
-verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
-instead of an object. For more on how to use a window with this, see
-hotp.verifyDelta.
Verify a time-based one-time token against the secret and return the delta.
-By default, it verifies the token at the current time window, with no leeway
-(no look-ahead or look-behind). A token validated at the current time window
-will have a delta of 0.
-
You can specify a window to add more leeway to the verification process.
-Setting the window param will check for the token at the given counter value
-as well as window tokens ahead and window tokens behind (two-sided
-window). See param for more info.
-
verifyDelta() will return the delta between the counter value of the token
-and the given counter value. For example, if given a time at counter 1000 and
-a window of 5, verifyDelta() will look at tokens from 995 to 1005,
-inclusive. In other words, if the time-step is 30 seconds, it will look at
-tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
-If it finds it at counter position 1002, it will return { delta: 2 }.
Verify a time-based one-time token against the secret and return true if it
-verifies. Helper function for verifyDelta() that returns a boolean instead of
-an object. For more on how to use a window with this, see
-totp.verifyDelta.
Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length
-(default 32). Returns the secret key in ASCII, hexadecimal, and base32 format,
-along with the URL used for the QR code for Google Authenticator (an otpauth
-URL). Use a QR code library to generate a QR code based on the Google
-Authenticator URL to obtain a QR code you can scan into the app.
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.
-
-
-## 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 token.
-
-**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 token against the secret and return the delta.
-By default, it verifies the token at the given counter value, with no leeway
-(no look-ahead or look-behind). A token validated at the current counter value
-will have a delta of 0.
-
-You can specify a window to add more leeway to the verification process.
-Setting the window param will check for the token at the given counter value
-as well as `window` tokens ahead (one-sided window). See param for more info.
-
-`verifyDelta()` will return the delta between the counter value of the token
-and the given counter value. For example, if given a counter 5 and a window
-10, `verifyDelta()` will look at tokens from 5 to 15, inclusive. If it finds
-it at counter position 7, it will return `{ delta: 2 }`.
-
-**Kind**: global function
-**Returns**: Object - On success, returns an object with the counter
- difference between the client and the server as the `delta` property (i.e.
- `{ delta: 0 }`).
-
-| 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 time-based one-time token against the secret and return true if it
-verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean
-instead of an object. For more on how to use a window with this, see
-[hotp.verifyDelta](hotp.verifyDelta).
-
-**Kind**: global function
-**Returns**: Boolean - Returns true if the token matches within the given
- 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 token. By default, it returns the token for
-the current time.
-
-**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 token against the secret and return the delta.
-By default, it verifies the token at the current time window, with no leeway
-(no look-ahead or look-behind). A token validated at the current time window
-will have a delta of 0.
-
-You can specify a window to add more leeway to the verification process.
-Setting the window param will check for the token at the given counter value
-as well as `window` tokens ahead and `window` tokens behind (two-sided
-window). See param for more info.
-
-`verifyDelta()` will return the delta between the counter value of the token
-and the given counter value. For example, if given a time at counter 1000 and
-a window of 5, `verifyDelta()` will look at tokens from 995 to 1005,
-inclusive. In other words, if the time-step is 30 seconds, it will look at
-tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive.
-If it finds it at counter position 1002, it will return `{ delta: 2 }`.
-
-**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 (e.g.
- `{ delta: 0 }`).
-
-| 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 token against the secret and return true if it
-verifies. Helper function for verifyDelta() that returns a boolean instead of
-an object. For more on how to use a window with this, see
-[totp.verifyDelta](totp.verifyDelta).
-
-**Kind**: global function
-**Returns**: Boolean - Returns true if the token matches within the given
- window, 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). |
-
-
-## generateSecret(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,
-along with the URL used for the QR code for Google Authenticator (an otpauth
-URL). Use a QR code library to generate a QR code based on the Google
-Authenticator URL to obtain a QR code you can scan into the app.
-
-**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.otpauth_url] | Boolean | true | Whether to output a Google Authenticator-compatible otpauth:// URL (only returns otpauth:// URL, no QR code) |
-| [options.name] | String | | The name to use with Google Authenticator. |
-| [options.qr_codes] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output QR code URLs for the token. |
-| [options.google_auth_qr] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output a Google Authenticator otpauth:// QR code URL. |
-
-
-## generateSecretASCII([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. |
-
-
-## otpauthURL(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. |
-| otpauth_url | String | Google Authenticator-compatible otpauth URL. |
-
diff --git a/index.js b/index.js
index 62e3ae7..b34392b 100644
--- a/index.js
+++ b/index.js
@@ -420,7 +420,7 @@ exports.generateSecret = function generateSecret (options) {
}
// generate an ascii key
- var key = this.generate_key_ascii(length, symbols);
+ var key = this.generateSecretASCII(length, symbols);
// return a SecretKey with ascii, hex, and base32
var SecretKey = {};
From d8196efeec77e75e741536351f981e8e0e77ba8c Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 12:26:10 -0500
Subject: [PATCH 76/85] [test] Generate - Move tests away from deprecated
functions
---
test/generate.js | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/test/generate.js b/test/generate.js
index c1ac06b..8f694bc 100644
--- a/test/generate.js
+++ b/test/generate.js
@@ -12,7 +12,7 @@ var speakeasy = require('..');
describe('Generator tests', function () {
it('Normal generation with defaults', function () {
- var secret = speakeasy.generate_key();
+ var secret = speakeasy.generateSecret();
assert.equal(secret.ascii.length, 32, 'Should return the correct length');
// check returned fields
@@ -28,40 +28,40 @@ describe('Generator tests', function () {
});
it('Generation with custom key length', function () {
- var secret = speakeasy.generate_key({length: 50});
+ var secret = speakeasy.generateSecret({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});
+ var secret = speakeasy.generateSecret({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});
+ var secret = speakeasy.generateSecret({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 otpath:// URL output disabled', function () {
- var secret = speakeasy.generate_key({otpauth_url: false});
+ var secret = speakeasy.generateSecret({otpauth_url: false});
assert.isUndefined(secret.otpauth_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});
+ var secret = speakeasy.generateSecret({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();
+ it('Testing generateSecret_ascii with defaults', function () {
+ var secret = speakeasy.generateSecretASCII();
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);
+ it('Testing generateSecret_ascii with custom length', function () {
+ var secret = speakeasy.generateSecretASCII(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 a8d62a5d7dbab194d4500071d396c9e51ecbb807 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 12:54:06 -0500
Subject: [PATCH 77/85] [test] Generate - Add tests for deprecated
generate_key[_ascii]
---
test/generate.js | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/test/generate.js b/test/generate.js
index 8f694bc..b24cae8 100644
--- a/test/generate.js
+++ b/test/generate.js
@@ -54,15 +54,27 @@ describe('Generator tests', function () {
assert.isDefined(secret.google_auth_qr, 'Google Auth QR should be returned');
});
- it('Testing generateSecret_ascii with defaults', function () {
+ it('Testing generateSecretASCII with defaults', function () {
var secret = speakeasy.generateSecretASCII();
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 generateSecret_ascii with custom length', function () {
+ it('Testing generateSecretASCII with custom length', function () {
var secret = speakeasy.generateSecretASCII(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');
});
+
+ it('Testing backward compatibility (deprecated function) generate_key', function () {
+ var secret = speakeasy.generate_key();
+ assert.ok(secret, 'Should return a secret');
+ assert.equal(secret.ascii.length, 32, 'Should return default secret length');
+ });
+
+ it('Testing backward compatibility (deprecated function) generate_key_ascii', function () {
+ var secret = speakeasy.generate_key_ascii(20);
+ assert.ok(secret, 'Should return a secret');
+ assert.equal(secret.length, 20, 'Should return the correct length');
+ });
});
From f78237482f97b3f49f3f3a236adf10e8f42b3976 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 13:28:58 -0500
Subject: [PATCH 78/85] [readme] Fix two-factor example userToken type
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index c747fd8..c505ad4 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ var QRCode = require('qrcode');
// Get the data URL of the authenticator URL
QRCode.toDataURL(secret.otpauth_url, function(err, data_url) {
console.log(data_url);
-
+
// Display this data URL to the user in an tag
// Example:
write('');
@@ -92,7 +92,7 @@ After the user scans the QR code, ask the user to enter in the token that they s
```js
// Let's say the user says that the token they have is 132890
-userToken = 132890;
+userToken = '132890';
```
```js
// Option A: use verify() to check the token against the secret
From 87da5f860ee2c48da2e35758c641f02557c80a27 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 13:35:59 -0500
Subject: [PATCH 79/85] [readme] Fix two-factor example code global leak
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index c505ad4..11af2b7 100644
--- a/README.md
+++ b/README.md
@@ -92,7 +92,7 @@ After the user scans the QR code, ask the user to enter in the token that they s
```js
// Let's say the user says that the token they have is 132890
-userToken = '132890';
+var userToken = '132890';
```
```js
// Option A: use verify() to check the token against the secret
From a7230cb6b0363f2b74370bd08691da0c6c41469c Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 13:43:38 -0500
Subject: [PATCH 80/85] [fix] Update deprecation checker conditionals
---
index.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/index.js b/index.js
index b34392b..2cf956d 100644
--- a/index.js
+++ b/index.js
@@ -30,7 +30,7 @@ exports.digest = function digest (options) {
var algorithm = (options.algorithm || 'sha1').toLowerCase();
// Backwards compatibility - deprecated
- if (options.key) {
+ if (options.key != null) {
console.log('Speakeasy - Deprecation Notice - Specifying the secret using `key` is no longer supported. Use `secret` instead.');
key = options.key;
}
@@ -86,7 +86,7 @@ exports.hotp = function hotpGenerate (options) {
// unpack digits
// backward compatibility: `length` is also accepted here, but deprecated
var digits = (options.digits != null ? options.digits : options.length) || 6;
- if (options.length) console.log('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.');
+ if (options.length != null) console.log('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.');
// digest the options
var digest = options.digest || exports.digest(options);
@@ -222,7 +222,7 @@ exports._counter = function _counter (options) {
// also accepts 'initial_time', but deprecated
var epoch = (options.epoch != null ? (options.epoch * 1000) : (options.initial_time * 1000)) || 0;
- if (options.initial_time) console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.');
+ if (options.initial_time != null) console.log('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.');
return Math.floor((time - epoch) / step / 1000);
};
From f12bd0aac950ff49f1a0a9cbea23095582917202 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 13:46:55 -0500
Subject: [PATCH 81/85] [doc] Update remaining time/initial_time/epoch doc
options to specify seconds
---
index.js | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/index.js b/index.js
index 2cf956d..66af4ef 100644
--- a/index.js
+++ b/index.js
@@ -210,8 +210,8 @@ exports.hotp.verify = function hotpVerify (options) {
* @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from
* which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.)
- * Initial time since the UNIX epoch from which to calculate the counter
- * value. Defaults to 0 (no offset).
+ * Initial time in seconds since the UNIX epoch from which to calculate the
+ * counter value. Defaults to 0 (no offset).
* @return {Integer} The calculated counter value
* @private
*/
@@ -290,11 +290,11 @@ exports.time = exports.totp;
* @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.
- * Defaults to `Date.now()`.
+ * @param {Integer} [options.time] Time in seconds with which to calculate
+ * counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
- * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from
- * which to calculate the counter value. Defaults to 0 (no offset).
+ * @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX
+ * epoch from which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.counter] Counter value, calculated by default.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
@@ -341,11 +341,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.
- * Defaults to `Date.now()`.
+ * @param {Integer} [options.time] Time in seconds with which to calculate
+ * counter value. Defaults to `Date.now()`.
* @param {Integer} [options.step=30] Time step in seconds
- * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from
- * which to calculate the counter value. Defaults to 0 (no offset).
+ * @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX
+ * epoch from which to calculate the counter value. Defaults to 0 (no offset).
* @param {Integer} [options.counter] Counter value, calculated by default.
* @param {Integer} [options.digits=6] The number of digits for the one-time
* passcode.
From f653975f6e3559d1f5fd899e7bf94b1ea9d48033 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 15:22:16 -0500
Subject: [PATCH 82/85] [fix] Correct parseInt defaults
---
index.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/index.js b/index.js
index 66af4ef..abf707c 100644
--- a/index.js
+++ b/index.js
@@ -155,8 +155,8 @@ exports.hotp.verifyDelta = function hotpVerifyDelta (options) {
// unpack options
var token = options.token;
- var window = parseInt(options.window || 0, 10);
- var counter = parseInt(options.counter || 0, 10);
+ var window = parseInt(options.window, 10) || 0;
+ var counter = parseInt(options.counter, 10) || 0;
// loop from C to C + W
for (i = counter; i <= counter + window; ++i) {
@@ -319,7 +319,7 @@ exports.totp.verifyDelta = function totpVerifyDelta (options) {
options = Object.create(options);
// unpack options
- var window = parseInt(options.window || 0, 10);
+ var window = parseInt(options.window, 10) || 0;
// calculate default counter value
if (options.counter == null) options.counter = exports._counter(options);
From 79b4888c26e7417b956998b034468cacb0364ed7 Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 15:42:46 -0500
Subject: [PATCH 83/85] [readme] Updated two-factor guide and added more usage
examples
---
README.md | 101 +++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 85 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index 11af2b7..b6907b2 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ Let's say you have a user that wants to enable two-factor authentication, and yo
1. Generate a secret
2. Show a QR code for the user to scan in
-3. Authenticate the token
+3. Authenticate the token for the first time
### Generating a key
@@ -59,7 +59,16 @@ var secret = speakeasy.generateSecret();
// Also returns secret.otpauth_url, which we'll use later.
```
-This will generate a secret key of length 32, which will be the secret key for the user. We don't want to set this as the user's secret key just yet – we first want to verify their token for the first time. So, store one of the encodings for the secret, preferably `secret.base32`, somewhere temporary.
+This will generate a secret key of length 32, which will be the secret key for the user.
+
+Now, we want to make sure that this secret works by validating the token that the user gets from it for the first time. In other words, we don't want to set this as the user's secret key just yet – we first want to verify their token for the first time. We need to persist the secret so that we can validate it later.
+
+So, store one of the encodings for the secret, preferably `secret.base32`, somewhere temporary, since we'll use that in the future to authenticate the user's first token.
+
+```js
+// Example for storing the secret key somewhere (varies by implementation):
+user.two_factor_temp_secret = secret.base32;
+```
### Displaying a QR code
@@ -93,25 +102,28 @@ After the user scans the QR code, ask the user to enter in the token that they s
```js
// Let's say the user says that the token they have is 132890
var userToken = '132890';
+
+// Let's say we stored the user's temporary secret in a user object like above:
+// (This is specific to your implementation)
+var base32secret = user.two_factor_temp_secret;
```
```js
-// Option A: use verify() to check the token against the secret
-var auth = speakeasy.totp.verify({ secret: secret.base32,
+// Use verify() to check the token against the secret
+var auth = speakeasy.totp.verify({ secret: base32secret,
encoding: 'base32',
token: userToken });
```
-```js
-// Option B: get the token at the current time and compare
-// to the token that the user gave
-var serverToken = speakeasy.totp({ secret: secret.base32,
- encoding: 'base32' });
-var auth = userToken == serverToken;
-```
`auth` will be true if the token is verified, false if not.
If successfully verified, you can now save the secret to the user's account and use the same process above whenever you need to use two-factor to authenticate the user, like during login.
+```js
+// Example for saving user's token (varies by implementation):
+user.two_factor_secret = user.two_factor_temp_secret;
+user.two_factor_enabled = true
+```
+
Now you're done implementing two-factor authentication!
@@ -136,7 +148,8 @@ var secret = speakeasy.generateSecret({length: 20});
// 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
+ secret: secret.base32,
+ encoding: 'base32'
});
// Returns token for the secret at the current time
@@ -148,7 +161,8 @@ var token = speakeasy.totp({
```js
// Verify a given token
var tokenValidates = speakeasy.totp.verify({
- secret: base32secret,
+ secret: secret.base32,
+ encoding: 'base32',
token: '123456',
window: 6
});
@@ -165,7 +179,8 @@ in seconds.
// Verify a given token is within 3 time-steps (+/- 2 minutes) from the server
// time-step.
var tokenDelta = speakeasy.totp.verifyDelta({
- secret: base32secret,
+ secret: secret.base32,
+ encoding: 'base32',
token: '123456',
window: 2,
step: 60
@@ -174,21 +189,75 @@ var tokenDelta = speakeasy.totp.verifyDelta({
// between the given token and the current time
```
+#### Getting a time-based token for a custom time
+
+```js
+var token = speakeasy.totp({
+ secret: secret.base32,
+ encoding: 'base32',
+ time: 1453667708 // specified in seconds
+});
+```
+
#### Calculating a counter-based token
```js
+// Get a counter-based token
var token = speakeasy.hotp({
- secret: base32secret,
+ secret: secret.base32,
+ encoding: 'base32',
counter: 123
});
+// Verify a counter-based token
var tokenValidates = speakeasy.hotp.verify({
secret: secret.base32,
- token: token,
+ encoding: 'base32',
+ token: '123456',
counter: 123
});
```
+#### Using other encodings
+
+The default encoding (when `encoding` is not specified) is `ascii`.
+
+```js
+// Specifying an ASCII token for TOTP
+// (encoding is 'ascii' by default)
+var token = speakeasy.totp({
+ secret: secret.ascii
+});
+```
+
+```js
+// Specifying a hex token for TOTP
+var token = speakeasy.totp({
+ secret: secret.hex,
+ encoding: 'hex'
+});
+```
+
+#### Using other hash algorithms
+
+The default hash algorithm is SHA1.
+
+```js
+// Specifying SHA256
+var token = speakeasy.totp({
+ secret: secret.ascii,
+ algorithm: 'sha256'
+});
+```
+
+```js
+// Specifying SHA512
+var token = speakeasy.totp({
+ secret: secret.ascii,
+ algorithm: 'sha512'
+});
+```
+
## Documentation
From cd14b0f0b648643f97ea9b9a543fbc4ed44da35c Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 16:29:42 -0500
Subject: [PATCH 84/85] [maint] Added CHANGELOG.md, updated README.md copy
---
CHANGELOG.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++
README.md | 3 +-
2 files changed, 101 insertions(+), 1 deletion(-)
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a4ccc85
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,99 @@
+# Change Log
+All notable changes to this project will be documented in this file.
+This project adheres to [Semantic Versioning](http://semver.org/) and the [Keep a Changelog template](https://github.com/olivierlacan/keep-a-changelog/blob/master/CHANGELOG.md).
+
+## [2.0.0] - 2016-
+
+Speakeasy 2.0.0 is a major update based on a Speakeasy fork, [Passcode](https://github.com/mikepb/passcode), by [Michael Phan-Ba](https://github.com/mikepb), which also incorporates code from another Node.js HOTP/TOTP module, [notp](https://github.com/guyht/notp), by [Guy Halford-Thompson](https://github.com/guyht), with additional functionality and API compatibility changes made by [Mark Bao](https://github.com/markbao). Speakeasy is now also moving to its own GitHub organization.
+
+Speakeasy 2.0.0 is API-compatible with Speakeasy 1.x.x, but a number of functions are renamed and deprecated for consistency. See below. Future versions of Speakeasy 2.x.x may not be API-compatible with Speakeasy 1.x.x. Deprecation notices have been added.
+
+### Added
+
+- Added support for SHA256 and SHA512 hashing algorithms, and general support for other hashing algorithms. Thanks, JHTWebAdmin.
+- Added `verify` functions from notp, adding verification window functionality which allows for the verification of tokens across a window (e.g. in HOTP, x tokens ahead, or in TOTP, x tokens ahead or behind).
+- Added `verifyDelta` functions which calculate a delta between a given token and where it was found within the window.
+- Added `verify` functions which wrap `verifyDelta` to return a boolean.
+- Added tests for key generator.
+- Added many more tests from Passcode and notp. All the above thanks to work from mikepb, guyht, and markbao.
+- Added `issuer`, `counter`, and `type` to Google Authenticator otpauth:// URL. Thanks, Vincent Lombard.
+- Added the output of a Google Authenticator–compatible otpauth:// URL to the key generator.
+- Added a new function, `otpuathURL()`, to output an otpauth:// URL.
+- Added a new demo and a guide for how to use Speakeasy to implement two-factor authentication.
+- Added code coverage testing with Istanbul.
+- Now conforms to JavaScript Semistandard code style.
+
+### API Changes
+
+v2.0.0 does not introduce any breaking changes, but deprecates a number of functions and parameters. Backwards compatibility is maintained for v2.0.0 but may not be maintained for future versions. While we highly recommend updating to 2.x.x, please make sure to update your `package.json` to use Speakeasy at versions `^1.0.5` if you'd like to use the 1.x.x API.
+
+- `generate_key()` is now `generateSecret()`. `generate_key()` deprecated.
+- `generate_key_ascii()` is now `generateSecretASCII()`. `generate_key_ascii()` deprecated.
+- `totp()` and `hotp()` now take the `key` parameter as `secret` (`key` deprecated).
+- `totp()` and `hotp()` now take the `length` parameter as `digits` (`length` deprecated).
+- `totp()` now takes the `initial_time` parameter as `epoch` (`initial_time` deprecated).
+- `generateSecret()` no longer supports returning URLs to QR codes using `qr_codes` and `google_auth_qr` since passing the secret to a third party may be a security risk. Implement QR code generation on your own instead, such as by using a QR module like `qr-image` or `node-qrcode`.
+
+### Changed
+
+- Now uses native Node.js buffers for converting encodings.
+- Now uses `base32.js` Node package for base32 conversions.
+- Moved location of main file to `index.js`.
+- Moved digesting into a separate function.
+- Documentation now uses JSDoc.
+
+
+### Fixed
+
+- Double-escape otpauth:// parameters for Google Authenticator otpauth:// URL. Thanks, cgarvey.
+
+## [1.0.5] - 2016-01-23
+
+### Fixed
+
+- Fixed key generator random selector overflow introduced in 1.0.4. Thanks, cmaster11.
+
+## [1.0.4] - 2016-01-08
+
+### Changed
+
+- Removed ezcrypto in favor of native Node crypto. Thanks, connor4312.
+- Move to a more secure key generator using `crypto.randomBytes`. Thanks, connor4312.
+- Allow `generate_key` to be called with no options. Thanks, PeteJodo.
+
+### Fixed
+
+- Fixed zero-padding bug in hotp. Thanks, haarvardw.
+
+## [1.0.3] - 2013-02-05
+
+### Changed
+
+- Add vows to devDependencies and support `npm test` in package.json. Thanks, freewill!
+
+## [1.0.2] - 2012-10-21
+
+### Fixed
+
+- Remove global leaks. Thanks for the fix, mashihua.
+
+## [1.0.1] - 2012-09-10
+
+### Fixed
+
+- 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] - 2011-11-03
+
+### Added
+
+- Initial release.
+
+[1.0.5]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.4...v1.0.5
+[1.0.4]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.3...v1.0.4
+[1.0.3]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.2...v1.0.3
+[1.0.2]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.1...v1.0.2
+[1.0.1]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.0...v1.0.1
+[1.0.1]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.0...v1.0.1
+[1.0.0]: https://github.com/speakeasyjs/speakeasy/compare/3de0a0f887d5146f0e90176263e8984c20ee2478...v1.0.0
\ No newline at end of file
diff --git a/README.md b/README.md
index b6907b2..3b63717 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,8 @@ 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.
+hash algorithms like SHA256 and SHA512, 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
From 533193814bcbab26fee49bc29f9c598546961c8a Mon Sep 17 00:00:00 2001
From: Mark Bao
Date: Sun, 24 Jan 2016 16:48:04 -0500
Subject: [PATCH 85/85] [fix] URL - Various fixes for otpauthURL - Convert
secret to base32 before constructing query - Default encoding to ASCII (used
to be null, couldn't be passed into new Buffer()) - Fix call from
generateSecret() to otpauthURL() to specify ascii - Added new test for
correct conversion for ASCII to Base32 in otpauthURL()
---
index.js | 16 ++++++++--------
test/url_test.js | 13 +++++++++++++
2 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/index.js b/index.js
index abf707c..2fc1e1c 100644
--- a/index.js
+++ b/index.js
@@ -408,7 +408,7 @@ exports.generateSecret = function generateSecret (options) {
// options
if (!options) options = {};
var length = options.length || 32;
- var name = encodeURIComponent(options.name) || 'SecretKey';
+ var name = encodeURIComponent(options.name || 'SecretKey');
var qr_codes = options.qr_codes || false;
var google_auth_qr = options.google_auth_qr || false;
var otpauth_url = options.otpauth_url != null ? options.otpauth_url : true;
@@ -439,8 +439,8 @@ exports.generateSecret = function generateSecret (options) {
// add in the Google Authenticator-compatible otpauth URL
if (otpauth_url) {
SecretKey.otpauth_url = exports.otpauthURL({
- secret: SecretKey.base32,
- label: name
+ secret: SecretKey.ascii,
+ label: name,
});
}
@@ -528,7 +528,7 @@ exports.otpauthURL = function otpauthURL (options) {
var algorithm = options.algorithm;
var digits = options.digits;
var period = options.period;
- var encoding = options.encoding;
+ var encoding = options.encoding || 'ascii';
// validate type
switch (type) {
@@ -548,6 +548,10 @@ exports.otpauthURL = function otpauthURL (options) {
throw new Error('missing counter value for HOTP');
}
+ // convert secret to base32
+ if (encoding !== 'base32') secret = new Buffer(secret, encoding);
+ if (Buffer.isBuffer(secret)) secret = base32.encode(secret);
+
// build query while validating
var query = {secret: secret};
if (issuer) query.issuer = issuer;
@@ -586,10 +590,6 @@ exports.otpauthURL = function otpauthURL (options) {
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',
diff --git a/test/url_test.js b/test/url_test.js
index afee050..ae1e49e 100644
--- a/test/url_test.js
+++ b/test/url_test.js
@@ -174,4 +174,17 @@ describe('#url', function () {
url.parse(expect)
);
});
+
+ it('should generate an URL compatible with the Google Authenticator app and convert an ASCII-encoded string', function () {
+ var answer = speakeasy.otpauthURL({
+ secret: 'MKiNHTvmfQ',
+ label: 'Example:alice@google.com',
+ issuer: 'Example'
+ });
+ var expect = 'otpauth://totp/Example:alice@google.com?secret=JVFWSTSIKR3G2ZSR&issuer=Example';
+ assert.deepEqual(
+ url.parse(answer),
+ url.parse(expect)
+ );
+ });
});