├── .gitignore ├── .npmignore ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── index.js ├── jsdoc.json ├── package.json └── test ├── hotp_test.js ├── notp_test.js ├── rfc4226_test.js ├── rfc6238_test.js ├── totp_test.js └── url_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | npm-debug.log 4 | 5 | # Mac OS X 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "iojs" 6 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 1.0.2 / 2015-07-13 2 | ================== 3 | 4 | * [Fixed] Don't repeat the secret key generating the digest. 5 | 6 | 7 | 1.0.1 / 2015-07-13 8 | ================== 9 | 10 | * [Fixed] Ignore case on algorithm option. 11 | 12 | 13 | 1.0.0 / 2015-07-12 14 | ================== 15 | 16 | * Initial release based on speakeasy and notp. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michael Phan-Ba 4 | Copyright (c) 2012-2013 Mark Bao 5 | Copyright (c) 2011 Guy Halford-Thompson 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Passcode has been merged into 2 | [Speakeasy 2.0](https://github.com/speakeasyjs/speakeasy)! Please update 3 | to Speakeasy for continuing support and updates. Speakeasy incorporates 4 | the following API changes from Passcode: 5 | 6 | - `options.time` and `options.epoch` are now specified in seconds instead 7 | of milliseconds. 8 | - `url()` has been renamed to `otpauthURL()` 9 | - `.verify()` functions now return `true` on valid and `false` otherwise; 10 | use `.verifyDelta()` to retrieve the delta from verification. 11 | 12 | # Passcode 13 | 14 | Passcode implements one-time passcode generators as standardized by the 15 | [Initiative for Open Authentication (OATH)][oath]. The HMAC-Based One-time 16 | Password (HOTP) algorithm defined by [RFC 4226][rfc4226] and the Time-based 17 | One-time Password (TOTP) algorithm defined in [RFC 6238][rfc6238] are 18 | supported. 19 | 20 | Passcode is a heavily modified version of [speakeasy][] incorporating the 21 | verification functions of [notp][]. 22 | 23 | ## Install 24 | 25 | ```sh 26 | npm install --save passcode 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```js 32 | var passcode = require("passcode"); 33 | var token = passcode.hotp({ 34 | secret: "xyzzy", 35 | counter: 123 36 | }); 37 | // token = "378764" 38 | 39 | var ok = passcode.hotp.verify({ 40 | secret: "xyzzy", 41 | token: token, 42 | counter: 123 43 | }); 44 | // ok = {delta: 0} 45 | ``` 46 | 47 | ## Documentation 48 | 49 | Full documentation at http://mikepb.github.io/passcode/ 50 | 51 | ## License 52 | 53 | This project incorporates code from [speakeasy][] and [notp][], both of which 54 | are licensed under MIT. Please see the [LICENSE](LICENSE) file for the full 55 | combined license. 56 | 57 | 58 | [speakeasy]: http://github.com/markbao/speakeasy 59 | [notp]: https://github.com/guyht/notp 60 | [oath]: http://www.openauthentication.org/ 61 | [rfc4226]: https://tools.ietf.org/html/rfc4226 62 | [rfc6238]: https://tools.ietf.org/html/rfc6238 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var base32 = require("base32.js"); 8 | var crypto = require("crypto"); 9 | var url = require("url"); 10 | 11 | /** 12 | * Digest the one-time passcode options. 13 | * 14 | * @param {Object} options 15 | * @param {String} options.secret Shared secret key 16 | * @param {Integer} options.counter Counter value 17 | * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, 18 | * base32, base64). 19 | * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, 20 | * sha512). 21 | * @return {Buffer} The one-time passcode as a buffer. 22 | */ 23 | 24 | exports.digest = function digest (options) { 25 | var i; 26 | 27 | // unpack options 28 | var key = options.secret; 29 | var counter = options.counter; 30 | var encoding = options.encoding || "ascii"; 31 | var algorithm = (options.algorithm || "sha1").toLowerCase(); 32 | 33 | // convert key to buffer 34 | if (!Buffer.isBuffer(key)) { 35 | key = encoding == "base32" ? base32.decode(key) 36 | : new Buffer(key, encoding); 37 | } 38 | 39 | // create an buffer from the counter 40 | var buf = new Buffer(8); 41 | var tmp = counter; 42 | for (i = 0; i < 8; i++) { 43 | 44 | // mask 0xff over number to get last 8 45 | buf[7 - i] = tmp & 0xff; 46 | 47 | // shift 8 and get ready to loop over the next batch of 8 48 | tmp = tmp >> 8; 49 | } 50 | 51 | // init hmac with the key 52 | var hmac = crypto.createHmac(algorithm, key); 53 | 54 | // update hmac with the counter 55 | hmac.update(buf); 56 | 57 | // return the digest 58 | return hmac.digest(); 59 | }; 60 | 61 | /** 62 | * Generate a counter-based one-time passcode. 63 | * 64 | * @param {Object} options 65 | * @param {String} options.secret Shared secret key 66 | * @param {Integer} options.counter Counter value 67 | * @param {Buffer} [options.digest] Digest, automatically generated by default 68 | * @param {Integer} [options.digits=6] The number of digits for the one-time 69 | * passcode. 70 | * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, 71 | * base32, base64). 72 | * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, 73 | * sha512). 74 | * @return {String} The one-time passcode. 75 | */ 76 | 77 | exports.hotp = function hotpGenerate (options) { 78 | 79 | // unpack options 80 | var digits = options.digits || 6; 81 | 82 | // digest the options 83 | var digest = options.digest || exports.digest(options); 84 | 85 | // compute HOTP offset 86 | var offset = digest[digest.length - 1] & 0xf; 87 | 88 | // calculate binary code (RFC4226 5.4) 89 | var code = (digest[offset] & 0x7f) << 24 90 | | (digest[offset + 1] & 0xff) << 16 91 | | (digest[offset + 2] & 0xff) << 8 92 | | (digest[offset + 3] & 0xff); 93 | 94 | // left-pad code 95 | code = new Array(digits + 1).join("0") + code.toString(10); 96 | 97 | // return length number off digits 98 | return code.substr(-digits); 99 | }; 100 | 101 | /** 102 | * Verify a counter-based One Time passcode. 103 | * 104 | * @param {Object} options 105 | * @param {String} options.secret Shared secret key 106 | * @param {String} options.token Passcode to validate 107 | * @param {Integer} options.counter Counter value. This should be stored by 108 | * the application and must be incremented for each request. 109 | * @param {Integer} [options.digits=6] The number of digits for the one-time 110 | * passcode. 111 | * @param {Integer} [options.window=50] The allowable margin for the counter. 112 | * The function will check "W" codes in the future against the provided 113 | * passcode, e.g. if W = 10, and C = 5, this function will check the 114 | * passcode against all One Time Passcodes between 5 and 15, inclusive. 115 | * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, 116 | * base32, base64). 117 | * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, 118 | * sha512). 119 | * @return {Object} On success, returns an object with the counter 120 | * difference between the client and the server as the `delta` property. 121 | * @method hotp․verify 122 | * @global 123 | */ 124 | 125 | exports.hotp.verify = function hotpVerify (options) { 126 | var i; 127 | 128 | // shadow options 129 | options = Object.create(options); 130 | 131 | // unpack options 132 | var token = options.token; 133 | var window = options.window || 50; 134 | var counter = options.counter || 0; 135 | 136 | // loop from C to C + W 137 | for (i = counter; i <= counter + window; ++i) { 138 | options.counter = i; 139 | if (exports.hotp(options) == token) { 140 | // found a matching code, return delta 141 | return {delta: i - counter}; 142 | } 143 | } 144 | 145 | // no codes have matched 146 | }; 147 | 148 | /** 149 | * Calculate counter value based on given options. 150 | * 151 | * @param {Object} options 152 | * @param {Integer} [options.time] Time with which to calculate counter value 153 | * @param {Integer} [options.step=30] Time step in seconds 154 | * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from 155 | * which to calculate the counter value. Defaults to `Date.now()`. 156 | * @return {Integer} The calculated counter value 157 | * @private 158 | */ 159 | 160 | exports._counter = function _counter (options) { 161 | var step = options.step || 30; 162 | var time = options.time != null ? options.time : Date.now(); 163 | var epoch = options.epoch || 0; 164 | return Math.floor((time - epoch) / step / 1000); 165 | }; 166 | 167 | /** 168 | * Generate a time-based one-time passcode. 169 | * 170 | * @param {Object} options 171 | * @param {String} options.secret Shared secret key 172 | * @param {Integer} [options.time] Time with which to calculate counter value 173 | * @param {Integer} [options.step=30] Time step in seconds 174 | * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from 175 | * which to calculate the counter value. Defaults to `Date.now()`. 176 | * @param {Integer} [options.counter] Counter value, calculated by default. 177 | * @param {Integer} [options.digits=6] The number of digits for the one-time 178 | * passcode. 179 | * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, 180 | * base32, base64). 181 | * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, 182 | * sha512). 183 | * @return {String} The one-time passcode. 184 | */ 185 | 186 | exports.totp = function totpGenerate (options) { 187 | 188 | // shadow options 189 | options = Object.create(options); 190 | 191 | // calculate default counter value 192 | if (options.counter == null) options.counter = exports._counter(options); 193 | 194 | // pass to hotp 195 | return this.hotp(options); 196 | }; 197 | 198 | /** 199 | * Verify a time-based One Time passcode. 200 | * 201 | * @param {Object} options 202 | * @param {String} options.secret Shared secret key 203 | * @param {String} options.token Passcode to validate 204 | * @param {Integer} [options.time] Time with which to calculate counter value 205 | * @param {Integer} [options.step=30] Time step in seconds 206 | * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from 207 | * which to calculate the counter value. Defaults to `Date.now()`. 208 | * @param {Integer} [options.counter] Counter value, calculated by default. 209 | * @param {Integer} [options.digits=6] The number of digits for the one-time 210 | * passcode. 211 | * @param {Integer} [options.window=6] The allowable margin for the counter. 212 | * The function will check "W" codes in the future and the past against the 213 | * provided passcode, e.g. if W = 5, and C = 1000, this function will check 214 | * the passcode against all One Time Passcodes between 995 and 1005, 215 | * inclusive. 216 | * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, 217 | * base32, base64). 218 | * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, 219 | * sha512). 220 | * @return {Object} On success, returns an object with the time step 221 | * difference between the client and the server as the `delta` property. 222 | * @method totp․verify 223 | * @global 224 | */ 225 | 226 | exports.totp.verify = function totpVerify (options) { 227 | 228 | // shadow options 229 | options = Object.create(options); 230 | 231 | // unpack options 232 | var window = options.window != null ? options.window : 0; 233 | 234 | // calculate default counter value 235 | if (options.counter == null) options.counter = exports._counter(options); 236 | 237 | // adjust for two-sided window 238 | options.counter -= window; 239 | options.window += window; 240 | 241 | // pass to hotp.verify 242 | return exports.hotp.verify(options); 243 | }; 244 | 245 | /** 246 | * Generate an URL for use with the Google Authenticator app. 247 | * 248 | * Authenticator considers TOTP codes valid for 30 seconds. Additionally, 249 | * the app presents 6 digits codes to the user. According to the 250 | * documentation, the period and number of digits are currently ignored by 251 | * the app. 252 | * 253 | * To generate a suitable QR Code, pass the generated URL to a QR Code 254 | * generator, such as the `qr-image` module. 255 | * 256 | * @param {Object} options 257 | * @param {String} options.secret Shared secret key 258 | * @param {Integer} options.label Used to identify the account with which 259 | * the secret key is associated, e.g. the user's email address. 260 | * @param {Integer} [options.type="totp"] Either "hotp" or "totp". 261 | * @param {Integer} [options.counter] The initial counter value, required 262 | * for HOTP. 263 | * @param {Integer} [options.issuer] The provider or service with which the 264 | * secret key is associated. 265 | * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, 266 | * sha512). 267 | * @param {Integer} [options.digits=6] The number of digits for the one-time 268 | * passcode. Currently ignored by Google Authenticator. 269 | * @param {Integer} [options.period=30] The length of time for which a TOTP 270 | * code will be valid, in seconds. Currently ignored by Google 271 | * Authenticator. 272 | * @param {String} [options.encoding] Key encoding (ascii, hex, base32, 273 | * base64). If the key is not encoded in Base-32, it will be reencoded. 274 | * @return {String} A URL suitable for use with the Google Authenticator. 275 | * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format 276 | */ 277 | 278 | exports.url = function (options) { 279 | 280 | // unpack options 281 | var secret = options.secret; 282 | var label = options.label; 283 | var issuer = options.issuer; 284 | var type = (options.type || "totp").toLowerCase(); 285 | var counter = options.counter; 286 | var algorithm = options.algorithm; 287 | var digits = options.digits; 288 | var period = options.period; 289 | var encoding = options.encoding; 290 | 291 | // validate type 292 | switch (type) { 293 | case "totp": 294 | case "hotp": 295 | break; 296 | default: 297 | throw new Error("invalid type `" + type + "`"); 298 | } 299 | 300 | // validate required options 301 | if (!secret) throw new Error("missing secret"); 302 | if (!label) throw new Error("missing label"); 303 | 304 | // require counter for HOTP 305 | if (type == "hotp" && counter == null) { 306 | throw new Error("missing counter value for HOTP"); 307 | } 308 | 309 | // build query while validating 310 | var query = {secret: secret}; 311 | if (options.issuer) query.issuer = options.issuer; 312 | 313 | // validate algorithm 314 | if (algorithm != null) { 315 | switch (algorithm.toUpperCase()) { 316 | case "SHA1": 317 | case "SHA256": 318 | case "SHA512": 319 | break; 320 | default: 321 | throw new Error("invalid algorithm `" + algorithm + "`"); 322 | } 323 | query.algorithm = algorithm.toUpperCase(); 324 | } 325 | 326 | // validate digits 327 | if (digits != null) { 328 | switch (parseInt(digits, 10)) { 329 | case 6: 330 | case 8: 331 | break; 332 | default: 333 | throw new Error("invalid digits `" + digits + "`"); 334 | } 335 | query.digits = digits; 336 | } 337 | 338 | // validate period 339 | if (period != null) { 340 | if (~~period != period) { 341 | throw new Error("invalid period `" + period + "`"); 342 | } 343 | query.period = period; 344 | } 345 | 346 | // convert secret to base32 347 | if (encoding != "base32") secret = new Buffer(secret, encoding); 348 | if (Buffer.isBuffer(secret)) secret = base32.encode(secret); 349 | 350 | // return url 351 | return url.format({ 352 | protocol: "otpauth", 353 | slashes: true, 354 | hostname: type, 355 | pathname: label, 356 | query: query 357 | }); 358 | }; 359 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": [ 4 | "index.js", 5 | "package.json", 6 | "README.md" 7 | ] 8 | }, 9 | "plugins": ["plugins/markdown"], 10 | "templates": { 11 | "applicationName": "Passcode", 12 | "meta": { 13 | "title": "Passcode", 14 | "description": "Passcode - One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", 15 | "keyword": "one-time passcode hotp totp google authenticator" 16 | }, 17 | "default": { 18 | "outputSourceFiles": true 19 | }, 20 | "linenums": true 21 | }, 22 | "opts": { 23 | "destination": "docs" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passcode", 3 | "description": "One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", 4 | "version": "1.0.2", 5 | "author": { 6 | "name": "Michael Phan-Ba", 7 | "email": "michael@mikepb.com" 8 | }, 9 | "contributors": [{ 10 | "name": "Mark Bao", 11 | "email": "mark@markbao.com", 12 | "url": "http://markbao.com" 13 | }, { 14 | "name": "Guy Halford-Thompson", 15 | "email": "guy@cach.me" 16 | }], 17 | "homepage": "http://github.com/mikepb/passcode", 18 | "keywords": [ 19 | "authentication", 20 | "google authenticator", 21 | "hmac", 22 | "hotp", 23 | "multi-factor", 24 | "one-time password", 25 | "passwords", 26 | "totp", 27 | "two-factor" 28 | ], 29 | "license": "MIT", 30 | "repository": { 31 | "type": "git", 32 | "url": "git://github.com/mikepb/passcode.git" 33 | }, 34 | "main": "index.js", 35 | "engines": { 36 | "node": ">= 0.10.0" 37 | }, 38 | "dependencies": { 39 | "base32.js": "0.0.1" 40 | }, 41 | "keywords": [ 42 | "two-factor", 43 | "authentication", 44 | "hotp", 45 | "totp", 46 | "multi-factor", 47 | "hmac", 48 | "one-time password", 49 | "passwords" 50 | ], 51 | "devDependencies": { 52 | "jsdoc": "^3.3.1", 53 | "mocha": "^2.2.5" 54 | }, 55 | "scripts": { 56 | "test": "mocha", 57 | "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/passcode/*/*.html" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/hotp_test.js: -------------------------------------------------------------------------------- 1 | "use scrict"; 2 | 3 | var assert = require('assert'); 4 | var speakeasy = require('..'); 5 | 6 | // These tests use the information from RFC 4226's Appendix D: Test Values. 7 | // http://tools.ietf.org/html/rfc4226#appendix-D 8 | 9 | describe('HOTP Counter-Based Algorithm Test', function () { 10 | 11 | describe('normal operation with secret = \'12345678901234567890\' at counter 3', function () { 12 | it('should return correct one-time password', function() { 13 | var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 3}); 14 | assert.equal(topic, '969429'); 15 | }); 16 | }); 17 | 18 | describe('another counter normal operation with secret = \'12345678901234567890\' at counter 7', function () { 19 | it('should return correct one-time password', function() { 20 | var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 7}); 21 | assert.equal(topic, '162583'); 22 | }); 23 | }); 24 | 25 | describe('digits override with secret = \'12345678901234567890\' at counter 4 and digits = 8', function () { 26 | it('should return correct one-time password', function() { 27 | var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 4, digits: 8}); 28 | assert.equal(topic, '40338314'); 29 | }); 30 | }); 31 | 32 | describe('hexadecimal encoding with secret = \'3132333435363738393031323334353637383930\' as hexadecimal at counter 4', function () { 33 | it('should return correct one-time password', function() { 34 | var topic = speakeasy.hotp({secret: '3132333435363738393031323334353637383930', encoding: 'hex', counter: 4}); 35 | assert.equal(topic, '338314'); 36 | }); 37 | }); 38 | 39 | describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at counter 4', function () { 40 | it('should return correct one-time password', function() { 41 | var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', counter: 4}); 42 | assert.equal(topic, '338314'); 43 | }); 44 | }); 45 | 46 | describe('base32 encoding with secret = \'12345678901234567890\' at counter 3', function () { 47 | it('should return correct one-time password', function() { 48 | var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 3}); 49 | assert.equal(topic, '969429'); 50 | }); 51 | }); 52 | 53 | describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA\' as base32 at counter 1, digits = 8 and algorithm as \'sha256\'', function () { 54 | it('should return correct one-time password', function() { 55 | var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha256'}); 56 | assert.equal(topic, '46119246'); 57 | }); 58 | }); 59 | 60 | describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA\' as base32 at counter 1, digits = 8 and algorithm as \'sha512\'', function () { 61 | it('should return correct one-time password', function() { 62 | var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha512'}); 63 | assert.equal(topic, '90693936'); 64 | }); 65 | }); 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /test/notp_test.js: -------------------------------------------------------------------------------- 1 | "use scrict"; 2 | 3 | var assert = require('assert'); 4 | var speakeasy = require('..'); 5 | 6 | /* 7 | * Test HOTtoken. Uses test values from RFcounter 4226 8 | * 9 | * 10 | * The following test data uses the AScounterII string 11 | * "12345678901234567890" for the secret: 12 | * 13 | * Secret = 0x3132333435363738393031323334353637383930 14 | * 15 | * Table 1 details for each count, the intermediate HMAcounter value. 16 | * 17 | * counterount Hexadecimal HMAcounter-SHA-1(secret, count) 18 | * 0 cc93cf18508d94934c64b65d8ba7667fb7cde4b0 19 | * 1 75a48a19d4cbe100644e8ac1397eea747a2d33ab 20 | * 2 0bacb7fa082fef30782211938bc1c5e70416ff44 21 | * 3 66c28227d03a2d5529262ff016a1e6ef76557ece 22 | * 4 a904c900a64b35909874b33e61c5938a8e15ed1c 23 | * 5 a37e783d7b7233c083d4f62926c7a25f238d0316 24 | * 6 bc9cd28561042c83f219324d3c607256c03272ae 25 | * 7 a4fb960c0bc06e1eabb804e5b397cdc4b45596fa 26 | * 8 1b3c89f65e6c9e883012052823443f048b4332db 27 | * 9 1637409809a679dc698207310c8c7fc07290d9e5 28 | * 29 | * Table 2 details for each count the truncated values (both in 30 | * hexadecimal and decimal) and then the HOTtoken value. 31 | * 32 | * Truncated 33 | * counterount Hexadecimal Decimal HOTtoken 34 | * 0 4c93cf18 1284755224 755224 35 | * 1 41397eea 1094287082 287082 36 | * 2 82fef30 137359152 359152 37 | * 3 66ef7655 1726969429 969429 38 | * 4 61c5938a 1640338314 338314 39 | * 5 33c083d4 868254676 254676 40 | * 6 7256c032 1918287922 287922 41 | * 7 4e5b397 82162583 162583 42 | * 8 2823443f 673399871 399871 43 | * 9 2679dc69 645520489 520489 44 | * 45 | * 46 | * see http://tools.ietf.org/html/rfc4226 47 | */ 48 | 49 | it("HOTP", function() { 50 | var options = { 51 | secret: '12345678901234567890', 52 | window: 0 53 | }; 54 | var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']; 55 | 56 | // make sure we can not pass in opt 57 | options.token = 'WILL NOT PASS'; 58 | speakeasy.hotp.verify(options); 59 | 60 | // counterheck for failure 61 | options.counter = 0; 62 | assert.ok(!speakeasy.hotp.verify(options), 'Should not pass'); 63 | 64 | // counterheck for passes 65 | for(i=0;i= 9 142 | options.window = 8; 143 | assert.ok(speakeasy.hotp.verify(options), 'Should pass for value of window >= 9'); 144 | 145 | // counterheck that test should pass for negative counter values 146 | token = '755224'; 147 | options.counter = 7 148 | options.window = 8; 149 | assert.ok(speakeasy.hotp.verify(options), 'Should pass for negative counter values'); 150 | }); 151 | 152 | 153 | /* 154 | * counterheck for codes that are out of sync 155 | * windowe are going to use a value of T = 1999999909 (91s behind 2000000000) 156 | */ 157 | 158 | it("TOTPOutOfSync", function() { 159 | 160 | var options = { 161 | secret: '12345678901234567890', 162 | token: '279037', 163 | time: 1999999909000 164 | }; 165 | 166 | // counterheck that the test should fail for window < 2 167 | options.window = 2; 168 | assert.ok(!speakeasy.totp.verify(options), 'Should not pass for value of window < 3'); 169 | 170 | // counterheck that the test should pass for window >= 3 171 | options.window = 3; 172 | assert.ok(speakeasy.totp.verify(options), 'Should pass for value of window >= 3'); 173 | }); 174 | 175 | 176 | it("hotp_gen", function() { 177 | var options = { 178 | secret: '12345678901234567890', 179 | window: 0 180 | }; 181 | 182 | var HOTP = ['755224', '287082','359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']; 183 | 184 | // make sure we can not pass in opt 185 | speakeasy.hotp(options); 186 | 187 | // counterheck for passes 188 | for(i=0;i nbytes) { 184 | key = key.slice(0, nbytes); 185 | } else { 186 | i = ~~(nbytes / key.length); 187 | key = [key]; 188 | while (i--) key.push(key[0]); 189 | key = Buffer.concat(key).slice(0, nbytes); 190 | } 191 | 192 | it("should calculate counter value for time " + subject.time, function () { 193 | var counter = passcode._counter({ 194 | time: subject.time 195 | }); 196 | assert.equal(counter, subject.counter); 197 | }); 198 | 199 | it("should calculate counter value for date " + subject.date, function () { 200 | var counter = passcode._counter({ 201 | time: subject.date 202 | }); 203 | assert.equal(counter, subject.counter); 204 | }); 205 | 206 | it("should generate TOTP code for time " + subject.time + " and algorithm " + subject.algorithm, function () { 207 | var counter = passcode.totp({ 208 | secret: key, 209 | time: subject.time, 210 | algorithm: subject.algorithm, 211 | digits: 8 212 | }); 213 | assert.equal(counter, subject.code); 214 | }); 215 | 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /test/totp_test.js: -------------------------------------------------------------------------------- 1 | "use scrict"; 2 | 3 | var assert = require('assert'); 4 | var speakeasy = require('..'); 5 | 6 | // These tests use the test vectors from RFC 6238's Appendix B: Test Vectors 7 | // http://tools.ietf.org/html/rfc6238#appendix-B 8 | // They use an ASCII string of 12345678901234567890 and a time step of 30. 9 | 10 | describe('TOTP Time-Based Algorithm Test', function () { 11 | 12 | describe('normal operation with secret = \'12345678901234567890\' at time = 59000', function () { 13 | it('should return correct one-time password', function() { 14 | var topic = speakeasy.totp({secret: '12345678901234567890', time: 59000}); 15 | assert.equal(topic, '287082'); 16 | }); 17 | }); 18 | 19 | describe('a different time normal operation with secret = \'12345678901234567890\' at time = 1111111109000', function () { 20 | it('should return correct one-time password', function () { 21 | var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000}); 22 | assert.equal(topic, '081804'); 23 | }); 24 | }); 25 | 26 | describe('digits parameter with secret = \'12345678901234567890\' at time = 1111111109000 and digits = 8', function () { 27 | it('should return correct one-time password', function () { 28 | var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, digits: 8}); 29 | assert.equal(topic, '07081804'); 30 | }); 31 | }); 32 | 33 | describe('hexadecimal encoding with secret = \'3132333435363738393031323334353637383930\' as hexadecimal at time 1111111109', function () { 34 | it('should return correct one-time password', function () { 35 | var topic = speakeasy.totp({secret: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109000}); 36 | assert.equal(topic, '081804'); 37 | }); 38 | }); 39 | 40 | describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at time 1111111109', function () { 41 | it('should return correct one-time password', function () { 42 | var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109000}); 43 | assert.equal(topic, '081804'); 44 | }); 45 | }); 46 | 47 | describe('a custom step with secret = \'12345678901234567890\' at time = 1111111109000 with step = 60', function () { 48 | it('should return correct one-time password', function () { 49 | var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, step: 60}); 50 | assert.equal(topic, '360094'); 51 | }); 52 | }); 53 | 54 | describe('initial time with secret = \'12345678901234567890\' at time = 1111111109000 and epoch = 1111111100000', function () { 55 | it('should return correct one-time password', function () { 56 | var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, epoch: 1111111100000}); 57 | assert.equal(topic, '755224'); 58 | }); 59 | }); 60 | 61 | describe('base32 encoding with secret = \'1234567890\' at time = 1111111109000', function () { 62 | it('should return correct one-time password', function () { 63 | var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000}); 64 | assert.equal(topic, '081804'); 65 | }); 66 | }); 67 | 68 | describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha256\'', function () { 69 | it('should return correct one-time password', function () { 70 | var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha256'}); 71 | assert.equal(topic, '68084774'); 72 | }); 73 | }); 74 | 75 | describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha512\'', function () { 76 | it('should return correct one-time password', function () { 77 | var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha512'}); 78 | assert.equal(topic, '25091201'); 79 | }); 80 | }); 81 | 82 | }); 83 | -------------------------------------------------------------------------------- /test/url_test.js: -------------------------------------------------------------------------------- 1 | "use scrict"; 2 | 3 | var assert = require('assert'); 4 | var speakeasy = require('..'); 5 | var url = require("url"); 6 | 7 | describe("#url", function () { 8 | 9 | it("should require options", function () { 10 | assert.throws(function () { 11 | speakeasy.url(); 12 | }); 13 | }); 14 | 15 | it("should validate type", function () { 16 | assert.throws(function () { 17 | speakeasy.url({ 18 | type: "haha", 19 | secret: "hello", 20 | label: "that", 21 | }, /invalid type `haha`/); 22 | }); 23 | }); 24 | 25 | it("should require secret", function () { 26 | assert.throws(function () { 27 | speakeasy.url({ 28 | label: "that" 29 | }, /missing secret/); 30 | }); 31 | }); 32 | 33 | it("should require label", function () { 34 | assert.throws(function () { 35 | speakeasy.url({ 36 | secret: "hello" 37 | }, /missing label/); 38 | }); 39 | }); 40 | 41 | it("should require counter for HOTP", function () { 42 | assert.throws(function () { 43 | speakeasy.url({ 44 | type: "hotp", 45 | secret: "hello", 46 | label: "that" 47 | }, /missing counter/); 48 | }); 49 | assert.ok(speakeasy.url({ 50 | type: "hotp", 51 | secret: "hello", 52 | label: "that", 53 | counter: 0 54 | })); 55 | assert.ok(speakeasy.url({ 56 | type: "hotp", 57 | secret: "hello", 58 | label: "that", 59 | counter: 199 60 | })); 61 | }); 62 | 63 | it("should validate algorithm", function () { 64 | assert.throws(function () { 65 | speakeasy.url({ 66 | secret: "hello", 67 | label: "that", 68 | algorithm: "hello" 69 | }, /invalid algorithm `hello`/); 70 | }); 71 | assert.ok(speakeasy.url({ 72 | secret: "hello", 73 | label: "that", 74 | algorithm: "sha1" 75 | })); 76 | assert.ok(speakeasy.url({ 77 | secret: "hello", 78 | label: "that", 79 | algorithm: "sha256" 80 | })); 81 | assert.ok(speakeasy.url({ 82 | secret: "hello", 83 | label: "that", 84 | algorithm: "sha512" 85 | })); 86 | }); 87 | 88 | it("should validate digits", function () { 89 | assert.throws(function () { 90 | speakeasy.url({ 91 | secret: "hello", 92 | label: "that", 93 | digits: "hello" 94 | }, /invalid digits `hello`/); 95 | }); 96 | assert.throws(function () { 97 | speakeasy.url({ 98 | secret: "hello", 99 | label: "that", 100 | digits: 12 101 | }, /invalid digits `12`/); 102 | }); 103 | assert.throws(function () { 104 | speakeasy.url({ 105 | secret: "hello", 106 | label: "that", 107 | digits: "7" 108 | }, /invalid digits `7`/); 109 | }); 110 | assert.ok(speakeasy.url({ 111 | secret: "hello", 112 | label: "that", 113 | digits: 6 114 | })); 115 | assert.ok(speakeasy.url({ 116 | secret: "hello", 117 | label: "that", 118 | digits: 8 119 | })); 120 | assert.ok(speakeasy.url({ 121 | secret: "hello", 122 | label: "that", 123 | digits: "6" 124 | })); 125 | assert.ok(speakeasy.url({ 126 | secret: "hello", 127 | label: "that", 128 | digits: "8" 129 | })); 130 | }); 131 | 132 | it("should validate period", function () { 133 | assert.throws(function () { 134 | speakeasy.url({ 135 | secret: "hello", 136 | label: "that", 137 | period: "hello" 138 | }, /invalid period `hello`/); 139 | }); 140 | assert.ok(speakeasy.url({ 141 | secret: "hello", 142 | label: "that", 143 | period: 60 144 | })); 145 | assert.ok(speakeasy.url({ 146 | secret: "hello", 147 | label: "that", 148 | period: 121 149 | })); 150 | assert.ok(speakeasy.url({ 151 | secret: "hello", 152 | label: "that", 153 | period: "60" 154 | })); 155 | assert.ok(speakeasy.url({ 156 | secret: "hello", 157 | label: "that", 158 | period: "121" 159 | })); 160 | }); 161 | 162 | it("should generate an URL compatible with the Google Authenticator app", function () { 163 | var answer = speakeasy.url({ 164 | secret: "JBSWY3DPEHPK3PXP", 165 | label: "Example:alice@google.com", 166 | issuer: "Example", 167 | encoding: "base32" 168 | }); 169 | var expect = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"; 170 | assert.deepEqual( 171 | url.parse(answer), 172 | url.parse(expect) 173 | ); 174 | }); 175 | 176 | }); 177 | --------------------------------------------------------------------------------