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