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/.npmignore b/.npmignore new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +test diff --git a/.travis.yml b/.travis.yml index 6064ca0..61da3b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: node_js node_js: - - "0.10" + - "5.3" + - "5.0" + - "4.1" + - "4.0" - "0.12" - - "iojs" +after_script: 'istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..de2cf6b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,100 @@ +# Change Log +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/) and the [Keep a Changelog template](https://github.com/olivierlacan/keep-a-changelog/blob/master/CHANGELOG.md). + +## [2.0.0] - 2016-01-27 + +Speakeasy 2.0.0 is a major update based on a Speakeasy fork, [Passcode](https://github.com/mikepb/passcode), by [Michael Phan-Ba](https://github.com/mikepb), which also incorporates code from another Node.js HOTP/TOTP module, [notp](https://github.com/guyht/notp), by [Guy Halford-Thompson](https://github.com/guyht), with additional functionality and API compatibility changes made by [Mark Bao](https://github.com/markbao). Speakeasy is now also moving to its own GitHub organization. + +Speakeasy 2.0.0 is API-compatible with Speakeasy 1.x.x, but a number of functions are renamed and deprecated for consistency. See below. Future versions of Speakeasy 2.x.x may not be API-compatible with Speakeasy 1.x.x. Deprecation notices have been added. + +### Added + +- Added support for SHA256 and SHA512 hashing algorithms, and general support for other hashing algorithms. Thanks, JHTWebAdmin. +- Added `verify` functions from notp, adding verification window functionality which allows for the verification of tokens across a window (e.g. in HOTP, x tokens ahead, or in TOTP, x tokens ahead or behind). +- Added `verifyDelta` functions which calculate a delta between a given token and where it was found within the window. +- Added `verify` functions which wrap `verifyDelta` to return a boolean. +- Added tests for key generator. +- Added many more tests from Passcode and notp. All the above thanks to work from mikepb, guyht, and markbao. +- Added `issuer`, `counter`, and `type` to Google Authenticator otpauth:// URL. Thanks, Vincent Lombard. +- Added the output of a Google Authenticator–compatible otpauth:// URL to the key generator. +- Added a new function, `otpuathURL()`, to output an otpauth:// URL. +- Added a new demo and a guide for how to use Speakeasy to implement two-factor authentication. +- Added code coverage testing with Istanbul. +- Now conforms to JavaScript Semistandard code style. + +### API Changes + +v2.0.0 does not introduce any breaking changes, but deprecates a number of functions and parameters. Backwards compatibility is maintained for v2.0.0 but may not be maintained for future versions. While we highly recommend updating to 2.x.x, please make sure to update your `package.json` to use Speakeasy at versions `^1.0.5` if you'd like to use the 1.x.x API. + +- `generate_key()` is now `generateSecret()`. `generate_key()` deprecated. +- `generate_key_ascii()` is now `generateSecretASCII()`. `generate_key_ascii()` deprecated. +- `totp()` and `hotp()` now take the `key` parameter as `secret` (`key` deprecated). +- `totp()` and `hotp()` now take the `length` parameter as `digits` (`length` deprecated). +- `totp()` now takes the `initial_time` parameter as `epoch` (`initial_time` deprecated). +- `generateSecret()` no longer supports returning URLs to QR codes using `qr_codes` and `google_auth_qr` since passing the secret to a third party may be a security risk. Implement QR code generation on your own instead, such as by using a QR module like `qr-image` or `node-qrcode`. + +### Changed + +- Now uses native Node.js buffers for converting encodings. +- Now uses `base32.js` Node package for base32 conversions. +- Moved location of main file to `index.js`. +- Moved digesting into a separate function. +- Documentation now uses JSDoc. + + +### Fixed + +- Double-escape otpauth:// parameters for Google Authenticator otpauth:// URL. Thanks, cgarvey. + +## [1.0.5] - 2016-01-23 + +### Fixed + +- Fixed key generator random selector overflow introduced in 1.0.4. Thanks, cmaster11. + +## [1.0.4] - 2016-01-08 + +### Changed + +- Removed ezcrypto in favor of native Node crypto. Thanks, connor4312. +- Move to a more secure key generator using `crypto.randomBytes`. Thanks, connor4312. +- Allow `generate_key` to be called with no options. Thanks, PeteJodo. + +### Fixed + +- Fixed zero-padding bug in hotp. Thanks, haarvardw. + +## [1.0.3] - 2013-02-05 + +### Changed + +- Add vows to devDependencies and support `npm test` in package.json. Thanks, freewill! + +## [1.0.2] - 2012-10-21 + +### Fixed + +- Remove global leaks. Thanks for the fix, mashihua. + +## [1.0.1] - 2012-09-10 + +### Fixed + +- Fixes issue where Google Chart API was being called at a deprecated URL. Thanks for the fix, sakkaku. +- Fixes issue where `generate_key`'s `symbols` option was not working, and was also causing pollution with global var. Thanks for reporting the bug, ARAtlas. + +## [1.0.0] - 2011-11-03 + +### Added + +- Initial release. + +[2.0.0]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.5...v2.0.0 +[1.0.5]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.4...v1.0.5 +[1.0.4]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.3...v1.0.4 +[1.0.3]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.2...v1.0.3 +[1.0.2]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.1...v1.0.2 +[1.0.1]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.0...v1.0.1 +[1.0.1]: https://github.com/speakeasyjs/speakeasy/compare/v1.0.0...v1.0.1 +[1.0.0]: https://github.com/speakeasyjs/speakeasy/compare/3de0a0f887d5146f0e90176263e8984c20ee2478...v1.0.0 \ No newline at end of file diff --git a/History.md b/History.md deleted file mode 100644 index 6abdc5b..0000000 --- a/History.md +++ /dev/null @@ -1,30 +0,0 @@ -Speakeasy - -Version History - -1.0.3 -===== - -Convenience release. Sabaidee from Luang Prabang, Laos. - -- Add vows to devDependencies and support `npm test` in package.json. Thanks, freewill! - -1.0.2 -===== - -Bugfix release. - -- Remove global leaks. Thanks for the fix, mashihua. - -1.0.1 -===== - -Bugfix release. Ciao from Florence, Italy. - -- Fixes issue where Google Chart API was being called at a deprecated URL. Thanks for the fix, sakkaku. -- Fixes issue where `generate_key`'s `symbols` option was not working, and was also causing pollution with global var. Thanks for reporting the bug, ARAtlas. - -1.0.0 -===== - -Initial release. diff --git a/LICENSE b/LICENSE index f3afb8f..49d1fee 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,8 @@ The MIT License (MIT) -Copyright (c) 2012-2013 Mark Bao +Copyright (c) 2012-2016 Mark Bao +Copyright (c) 2015 Michael Phan-Ba +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 diff --git a/README.md b/README.md index 5e2b218..f539db6 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,731 @@ -# speakeasy + -## Easy two-factor authentication for node.js. Calculate time-based or counter-based one-time passwords. Supports the Google Authenticator mobile app. +[![Build Status](https://travis-ci.org/speakeasyjs/speakeasy.svg?branch=v2)](https://travis-ci.org/speakeasyjs/speakeasy) +[![NPM downloads](https://img.shields.io/npm/dt/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) +[![Coverage Status](https://coveralls.io/repos/github/speakeasyjs/speakeasy/badge.svg?branch=v2)](https://coveralls.io/github/speakeasyjs/speakeasy?branch=v2) +[![NPM version](https://img.shields.io/npm/v/speakeasy.svg)](https://www.npmjs.com/package/speakeasy) -Uses the HMAC One-Time Password algorithms, supporting counter-based and time-based moving factors (HOTP and TOTP). +--- -## An Introduction +**Jump to** — [Install](#install) · [Demo](#demo) · [Two-Factor Usage](#two-factor) · [General Usage](#general-usage) · [Documentation](#documentation) · [Contributing](#contributing) · [License](#license) -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. +Speakeasy is a one-time passcode generator, ideal for use in two-factor +authentication, that supports Google Authenticator and other two-factor devices. -This module was written to follow the RFC memos on HOTP and TOTP: +It is well-tested and includes robust support for custom token lengths, +authentication windows, hash algorithms like SHA256 and SHA512, and other +features, and includes helpers like a secret key generator. -* 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 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. This project incorporates code from [passcode][], originally a +fork of Speakeasy, and [notp][]. -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. + +## Install + +```sh +npm install --save speakeasy +``` -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. + +## Demo -## Install +This demo uses the `generateSecret` method of Speakeasy to generate a secret key, +displays a Google Authenticator–compatible QR code which you can scan into your +phone's two-factor app, and shows the token, which you can verify with your +phone. Includes sample code. https://sedemo-mktb.rhcloud.com/ + + + + +## Two-Factor Usage + +Let's say you have a user that wants to enable two-factor authentication, and you intend to do two-factor authentication using an app like Google Authenticator, Duo Security, Authy, etc. This is a three-step process: +1. Generate a secret +2. Show a QR code for the user to scan in +3. Authenticate the token for the first time + +### Generating a key + +Use Speakeasy's key generator to get a key. + +```js +var secret = speakeasy.generateSecret(); +// Returns an object with secret.ascii, secret.hex, and secret.base32. +// Also returns secret.otpauth_url, which we'll use later. ``` -npm install speakeasy + +This will generate a secret key of length 32, which will be the secret key for the user. + +Now, we want to make sure that this secret works by validating the token that the user gets from it for the first time. In other words, we don't want to set this as the user's secret key just yet – we first want to verify their token for the first time. We need to persist the secret so that we can use it for token validation later. + +So, store one of the encodings for the secret, preferably `secret.base32`, somewhere temporary, since we'll use that in the future to authenticate the user's first token. + +```js +// Example for storing the secret key somewhere (varies by implementation): +user.two_factor_temp_secret = secret.base32; ``` -## Example (with Google Authenticator) +### Displaying a QR code + +Next, we'll want to display a QR code to the user so they can scan in the secret into their app. Google Authenticator and similar apps take in a QR code that holds a URL with the protocol `otpauth://`, which you get automatically from `secret.otpauth_url`. + +Use a QR code module to generate a QR code that stores the data in `secret.otpauth_url`, and then display the QR code to the user. This is one simple way to do it, which generates a PNG data URL which you can put into an `` tag on a webpage: -```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& tag + // Example: + write(''); +}); ``` -You'll get this QR code. If you don't already have it, get [Google Authenticator](http://www.google.com/support/accounts/bin/answer.py?answer=1066447). +Ask the user to scan this QR code into their authenticator app. + +### Verifying the token -![](http://i.imgur.com/INZnk.png) +Finally, we want to make sure that the token on the server side and the token on the client side match. The best practice is to do a token check before fully enabling two-factor authenticaton for the user. This code applies to the first and subsequent token checks. +After the user scans the QR code, ask the user to enter in the token that they see in their app. Then, verify it against the secret. + +```js +// Let's say the user says that the token they have is 132890 +var userToken = '132890'; + +// Let's say we stored the user's temporary secret in a user object like above: +// (This is specific to your implementation) +var base32secret = user.two_factor_temp_secret; ``` -// specify a length and encoding (ascii, hex, or base32). -speakeasy.time({key: 'KY7TSZRWFZBXCMJGHRED6PDOPBSS4WCK', encoding: 'base32'}); // see the base32 result above -// => try this in your REPL and it should match the number on your phone +```js +// Use verify() to check the token against the secret +var verified = speakeasy.totp.verify({ secret: base32secret, + encoding: 'base32', + token: userToken }); ``` -## Manual +`verified` will be true if the token is successfully verified, false if not. + +If successfully verified, you can now save the secret to the user's account and use the same process above whenever you need to use two-factor to authenticate the user, like during login. -### speakeasy.hotp(options) | speakeasy.counter(options) +```js +// Example for saving user's token (varies by implementation): +user.two_factor_secret = user.two_factor_temp_secret; +user.two_factor_enabled = true +``` -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. +Now you're done implementing two-factor authentication! -Written to follow [RFC 4226](http://tools.ietf.org/html/rfc4226). Calculated with: `HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` + +## General Usage -#### Options +```js +var speakeasy = require("speakeasy"); +``` -* `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. +#### Generating a key -#### Example +```js +// Generate a secret key. +var secret = speakeasy.generateSecret({length: 20}); +// Access using secret.ascii, secret.hex, or secret.base32. +``` -```javascript -// normal use. -speakeasy.hotp({key: 'secret', counter: 582}); -// => 246642 +#### Getting a time-based token for the current time -// use a custom length. -speakeasy.hotp({key: 'secret', counter: 582, length: 8}); -// => 67246642 +```js +// Generate a time-based token based on the base-32 key. +// HOTP (counter-based tokens) can also be used if `totp` is replaced by +// `hotp` (i.e. speakeasy.hotp()) and a `counter` is given in the options. +var token = speakeasy.totp({ + secret: secret.base32, + encoding: 'base32' +}); -// use a custom encoding. -speakeasy.hotp({key: 'AJFIEJGEHIFIU7148SF', counter: 147, encoding: 'base32'}); -// => 974955 +// Returns token for the secret at the current time +// Compare this to user input ``` -### speakeasy.totp(options) | speakeasy.time(options) +#### Verifying a token + +```js +// Verify a given token +var tokenValidates = speakeasy.totp.verify({ + secret: secret.base32, + encoding: 'base32', + token: '123456', + window: 6 +}); +// Returns true if the token matches +``` + +#### Verifying a token and calculating a delta + +A TOTP is incremented every `step` time-step seconds. By default, the time-step +is 30 seconds. You may change the time-step using the `step` option, with units +in seconds. + +```js +// Verify a given token is within 3 time-steps (+/- 2 minutes) from the server +// time-step. +var tokenDelta = speakeasy.totp.verifyDelta({ + secret: secret.base32, + encoding: 'base32', + token: '123456', + window: 2, + step: 60 +}); +// Returns {delta: 0} where the delta is the time step difference +// between the given token and the current time +``` + +#### Getting a time-based token for a custom time + +```js +var token = speakeasy.totp({ + secret: secret.base32, + encoding: 'base32', + time: 1453667708 // specified in seconds +}); +``` + +#### Calculating a counter-based token + +```js +// Get a counter-based token +var token = speakeasy.hotp({ + secret: secret.base32, + encoding: 'base32', + counter: 123 +}); + +// Verify a counter-based token +var tokenValidates = speakeasy.hotp.verify({ + secret: secret.base32, + encoding: 'base32', + token: '123456', + counter: 123 +}); +``` + +#### Using other encodings + +The default encoding (when `encoding` is not specified) is `ascii`. + +```js +// Specifying an ASCII token for TOTP +// (encoding is 'ascii' by default) +var token = speakeasy.totp({ + secret: secret.ascii +}); +``` -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. +```js +// Specifying a hex token for TOTP +var token = speakeasy.totp({ + secret: secret.hex, + encoding: 'hex' +}); +``` + + +#### Using other hash algorithms + +The default hash algorithm is SHA1. + +```js +// Specifying SHA256 +var token = speakeasy.totp({ + secret: secret.ascii, + algorithm: 'sha256' +}); +``` -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))` +```js +// Specifying SHA512 +var token = speakeasy.totp({ + secret: secret.ascii, + algorithm: 'sha512' +}); +``` -#### Options +#### Getting an otpauth:// URL and QR code for non-SHA1 hash algorithms -* `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. +```js +// Generate a secret, if needed +var secret = speakeasy.generateSecret(); +// By default, generateSecret() returns an otpauth_url for SHA1 -#### Example +// Use otpauthURL() to get a custom authentication URL for SHA512 +var url = speakeasy.otpauthURL({ secret: secret.ascii, label: 'Name of Secret', algorithm: 'sha512' }); -```javascript -// normal use. -speakeasy.totp({key: 'secret'}); +// Pass URL into a QR code generator +``` -// use a custom time step. -speakeasy.totp({key: 'secret', step: 60}); +#### Specifying a window for verifying HOTP and TOTP -// use a custom time. -speakeasy.totp({key: 'secret', time: 159183717}); -// => 558014 +Verify a HOTP token with counter value 42 and a window of 10. HOTP has a one-sided window, so this will check counter values from 42 to 52, inclusive, and return a `{ delta: n }` where `n` is the difference between the given counter value and the counter position at which the token was found, or `undefined` if it was not found within the window. See the `hotp․verifyDelta(options)` documentation for more info. -// use a initial time. -speakeasy.totp({key: 'secret', initial_time: 4182881485}); -// => 670417 +```js +var token = speakeasy.hotp.verifyDelta({ + secret: secret.ascii, + counter: 42, + token: '123456', + window: 10 +}); ``` -#### Techniques +How this works: + +```js +// Set ASCII secret +var secret = 'rNONHRni6BAk7y2TiKrv'; -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)`) +// Get HOTP counter token at counter = 42 +var counter42 = speakeasy.hotp({ secret: secret, counter: 42 }); +// => '566646' -### speakeasy.generate_key(options) +// Get HOTP counter token at counter = 45 +var counter45 = speakeasy.hotp({ secret: secret, counter: 45 }); +// => '323238' -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. +// Verify the secret at counter 42 with the actual value and a window of 10 +// This will check all counter values from 42 to 52, inclusive +speakeasy.hotp.verifyDelta({ secret: secret, counter: 42, token: counter42, window: 10 }); +// => { delta: 0 } because the given token at counter 42 is 0 steps away from the given counter 42 -#### Options +// Verify the secret at counter 45, but give a counter of 42 and a window of 10 +// This will check all counter values from 42 to 52, inclusive +speakeasy.hotp.verifyDelta({ secret: secret, counter: 42, token: counter45, window: 10 }); +// => { delta: 3 } because the given token at counter 45 is 0 steps away from given counter 42 -* `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. +// Not in window: specify a window of 1, which only tests counters 42 and 43, not 45 +speakeasy.hotp.verifyDelta({ secret: secret, counter: 42, token: counter45, window: 1 }); +// => undefined -#### Examples +// Shortcut to use verify() to simply return whether it is verified as within the window +speakeasy.hotp.verify({ secret: secret, counter: 42, token: counter45, window: 10 }); +// => true -```javascript -// generate a key -speakeasy.generate_key({length: 20, symbols: true}); -// => { ascii: 'km^A?n&sOPJW.iCKPHKU', hex: '6b6d5e413f6e26734f504a572e69434b50484b55', base32: 'NNWV4QJ7NYTHGT2QJJLS42KDJNIEQS2V' } +// Not in window: specify a window of 1, which only tests counters 42 and 43, not 45 +speakeasy.hotp.verify({ secret: secret, counter: 42, token: counter45, window: 1 }); +// => false +``` -// 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' } +Verify a TOTP token at the current time with a window of 2. Since the default time step is 30 seconds, and TOTP has a two-sided window, this will check tokens between [current time minus two tokens before] and [current time plus two tokens after]. In other words, with a time step of 30 seconds, it will check the token at the current time, plus the tokens at the current time minus 30 seconds, minus 60 seconds, plus 30 seconds, and plus 60 seconds – basically, it will check tokens between a minute ago and a minute from now. It will return a `{ delta: n }` where `n` is the difference between the current time step and the counter position at which the token was found, or `undefined` if it was not found within the window. See the `totp․verifyDelta(options)` documentation for more info. -// 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& { delta: -2 } -## Issues and patches +// This signifies that the given token, token1, is -2 steps away from +// the given time, which means that it is the token for the value at +// (-2 * time step) = (-2 * 30 seconds) = 60 seconds ago. +``` -If you're having an issue, I'm quite sorry that you came across it. Please submit issues to the [GitHub Issues page](https://github.com/markbao/speakeasy/issues). +As shown previously, you can also change `verifyDelta()` to `verify()` to simply return a boolean if the given token is within the given window. + + +## Documentation + +Full API documentation (in JSDoc format) is available below and at http://speakeasyjs.github.io/speakeasy/ + + + +### Functions + +
+
digest(options)Buffer
+

Digest the one-time passcode options.

+
+
hotp(options)String
+

Generate a counter-based one-time token.

+
+
hotp․verifyDelta(options)Object
+

Verify a counter-based one-time token against the secret and return the delta.

+
+
hotp․verify(options)Boolean
+

Verify a counter-based one-time token against the secret and return true if it +verifies.

+
+
totp(options)String
+

Generate a time-based one-time token.

+
+
totp․verifyDelta(options)Object
+

Verify a time-based one-time token against the secret and return the delta.

+
+
totp․verify(options)Boolean
+

Verify a time-based one-time token against the secret and return true if it +verifies. +

+
generateSecret(options)Object | GeneratedSecret
+

Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length +(default 32).

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

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

+
+
otpauthURL(options)String
+

Generate an URL for use with the Google Authenticator app.

+
+
+ +### Typedefs + +
+
GeneratedSecret : Object
+
+
+ + +### digest(options) ⇒ Buffer +Digest the one-time passcode options. + +**Kind**: function + +**Returns**: Buffer - The one-time passcode as a buffer. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.counter | Integer | | Counter value | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| [options.key] | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | + + +### hotp(options) ⇒ String + +Generate a counter-based one-time token. Specify the key and counter, and +receive the one-time password for that counter position as a string. You can +also specify a token length, as well as the encoding (ASCII, hexadecimal, or +base32) and the hashing algorithm to use (SHA1, SHA256, SHA512). + +**Kind**: function + +**Returns**: String - The one-time passcode. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.counter | Integer | | Counter value | +| [options.digest] | Buffer | | Digest, automatically generated by default | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| [options.key] | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | +| [options.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. | + + +### hotp․verifyDelta(options) ⇒ Object +Verify a counter-based one-time token against the secret and return the delta. +By default, it verifies the token at the given counter value, with no leeway +(no look-ahead or look-behind). A token validated at the current counter value +will have a delta of 0. + +You can specify a window to add more leeway to the verification process. +Setting the window param will check for the token at the given counter value +as well as `window` tokens ahead (one-sided window). See param for more info. + +`verifyDelta()` will return the delta between the counter value of the token +and the given counter value. For example, if given a counter 5 and a window +10, `verifyDelta()` will look at tokens from 5 to 15, inclusive. If it finds +it at counter position 7, it will return `{ delta: 2 }`. + +**Kind**: function + +**Returns**: Object - On success, returns an object with the counter + difference between the client and the server as the `delta` property (i.e. + `{ delta: 0 }`). + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| options.counter | Integer | | Counter value. This should be stored by the application and must be incremented for each request. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future against the provided passcode, e.g. if W = 10, and C = 5, this function will check the passcode against all One Time Passcodes between 5 and 15, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### hotp․verify(options) ⇒ Boolean +Verify a counter-based one-time token against the secret and return true if it +verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean +instead of an object. For more on how to use a window with this, see +hotp.verifyDelta. + +**Kind**: function + +**Returns**: Boolean - Returns true if the token matches within the given + window, false otherwise. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| options.counter | Integer | | Counter value. This should be stored by the application and must be incremented for each request. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future against the provided passcode, e.g. if W = 10, and C = 5, this function will check the passcode against all One Time Passcodes between 5 and 15, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### totp(options) ⇒ String + +Generate a time-based one-time token. Specify the key, and receive the +one-time password for that time as a string. By default, it uses the current +time and a time step of 30 seconds, so there is a new token every 30 seconds. +You may override the time step and epoch for custom timing. You can also +specify a token length, as well as the encoding (ASCII, hexadecimal, or +base32) and the hashing algorithm to use (SHA1, SHA256, SHA512). + +Under the hood, TOTP calculates the counter value by finding how many time +steps have passed since the epoch, and calls HOTP with that counter value. + +**Kind**: function + +**Returns**: String - The one-time passcode. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| [options.time] | Integer | | Time in seconds with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| [options.key] | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | +| [options.initial_time] | Integer | 0 | (DEPRECATED. Use `epoch` instead.) Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. | + + +### totp․verifyDelta(options) ⇒ Object +Verify a time-based one-time token against the secret and return the delta. +By default, it verifies the token at the current time window, with no leeway +(no look-ahead or look-behind). A token validated at the current time window +will have a delta of 0. + +You can specify a window to add more leeway to the verification process. +Setting the window param will check for the token at the given counter value +as well as `window` tokens ahead and `window` tokens behind (two-sided +window). See param for more info. + +`verifyDelta()` will return the delta between the counter value of the token +and the given counter value. For example, if given a time at counter 1000 and +a window of 5, `verifyDelta()` will look at tokens from 995 to 1005, +inclusive. In other words, if the time-step is 30 seconds, it will look at +tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive. +If it finds it at counter position 1002, it will return `{ delta: 2 }`. +If it finds it at counter position 997, it will return `{ delta: -3 }`. + +**Kind**: function + +**Returns**: Object - On success, returns an object with the time step + difference between the client and the server as the `delta` property (e.g. + `{ delta: 0 }`). + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| [options.time] | Integer | | Time in seconds with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future and the past against the provided passcode, e.g. if W = 5, and C = 1000, this function will check the passcode against all One Time Passcodes between 995 and 1005, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### totp․verify(options) ⇒ Boolean +Verify a time-based one-time token against the secret and return true if it +verifies. Helper function for verifyDelta() that returns a boolean instead of +an object. For more on how to use a window with this, see totp.verifyDelta. + +**Kind**: function + +**Returns**: Boolean - Returns true if the token matches within the given + window, false otherwise. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| [options.time] | Integer | | Time in seconds with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future and the past against the provided passcode, e.g. if W = 5, and C = 1000, this function will check the passcode against all One Time Passcodes between 995 and 1005, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### generateSecret(options) ⇒ Object | [GeneratedSecret](#GeneratedSecret) +Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length +(default 32). Returns the secret key in ASCII, hexadecimal, and base32 format, +along with the URL used for the QR code for Google Authenticator (an otpauth +URL). Use a QR code library to generate a QR code based on the Google +Authenticator URL to obtain a QR code you can scan into the app. + +**Kind**: function + +**Returns**: A [`GeneratedSecret`](#GeneratedSecret) object + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| [options.length] | Integer | 32 | Length of the secret | +| [options.symbols] | Boolean | false | Whether to include symbols | +| [options.otpauth_url] | Boolean | true | Whether to output a Google Authenticator-compatible otpauth:// URL (only returns otpauth:// URL, no QR code) | +| [options.name] | String | | The name to use with Google Authenticator. | +| [options.qr_codes] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output QR code URLs for the token. | +| [options.google_auth_qr] | Boolean | false | (DEPRECATED. Do not use to prevent leaking of secret to a third party. Use your own QR code implementation.) Output a Google Authenticator otpauth:// QR code URL. | + + +### generateSecretASCII([length], [symbols]) ⇒ String +Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and +symbols (if requested). + +**Kind**: function + +**Returns**: String - The generated key. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [length] | Integer | 32 | The length of the key. | +| [symbols] | Boolean | false | Whether to include symbols in the key. | + + +### otpauthURL(options) ⇒ String +Generate a Google Authenticator-compatible otpauth:// URL for passing the +secret to a mobile device to install the secret. + +Authenticator considers TOTP codes valid for 30 seconds. Additionally, +the app presents 6 digits codes to the user. According to the +documentation, the period and number of digits are currently ignored by +the app. + +To generate a suitable QR Code, pass the generated URL to a QR Code +generator, such as the `qr-image` module. + +**Kind**: function + +**Throws**: Error if secret or label is missing, or if hotp is used and a + counter is missing, if the type is not one of `hotp` or `totp`, if the + number of digits is non-numeric, or an invalid period is used. Warns if + the number of digits is not either 6 or 8 (though 6 is the only one + supported by Google Authenticator), and if the hashihng algorithm is + not one of the supported SHA1, SHA256, or SHA512. + +**Returns**: String - A URL suitable for use with the Google Authenticator. + +**See**: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.label | String | | Used to identify the account with which the secret key is associated, e.g. the user's email address. | +| [options.type] | String | "totp" | Either "hotp" or "totp". | +| [options.counter] | Integer | | The initial counter value, required for HOTP. | +| [options.issuer] | String | | The provider or service with which the secret key is associated. | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. Currently ignored by Google Authenticator. | +| [options.period] | Integer | 30 | The length of time for which a TOTP code will be valid, in seconds. Currently ignored by Google Authenticator. | +| [options.encoding] | String | | Key encoding (ascii, hex, base32, base64). If the key is not encoded in Base-32, it will be reencoded. | + + +### GeneratedSecret : Object + +**Kind**: global typedef + +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| ascii | String | ASCII representation of the secret | +| hex | String | Hex representation of the secret | +| base32 | String | Base32 representation of the secret | +| qr_code_ascii | String | URL for the QR code for the ASCII secret. | +| qr_code_hex | String | URL for the QR code for the hex secret. | +| qr_code_base32 | String | URL for the QR code for the base32 secret. | +| google_auth_qr | String | URL for the Google Authenticator otpauth URL's QR code. | +| otpauth_url | String | Google Authenticator-compatible otpauth URL. | + + +## 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]) -To submit a patch, please first make sure the tests pass, and then make a pull request detailing your changes. Thank you! +## License + +This project incorporates code from [passcode][], which was originally a +fork of speakeasy, and [notp][], both of which are licensed under MIT. +Please see the [LICENSE](LICENSE) file for the full combined license. + +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 diff --git a/docs/docco.css b/docs/docco.css deleted file mode 100644 index 5aa0a8d..0000000 --- a/docs/docco.css +++ /dev/null @@ -1,186 +0,0 @@ -/*--------------------- Layout and Typography ----------------------------*/ -body { - font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; - font-size: 15px; - line-height: 22px; - color: #252519; - margin: 0; padding: 0; -} -a { - color: #261a3b; -} - a:visited { - color: #261a3b; - } -p { - margin: 0 0 15px 0; -} -h1, h2, h3, h4, h5, h6 { - margin: 0px 0 15px 0; -} - h1 { - margin-top: 40px; - } -#container { - position: relative; -} -#background { - position: fixed; - top: 0; left: 525px; right: 0; bottom: 0; - background: #f5f5ff; - border-left: 1px solid #e5e5ee; - z-index: -1; -} -#jump_to, #jump_page { - background: white; - -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; - -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; - font: 10px Arial; - text-transform: uppercase; - cursor: pointer; - text-align: right; -} -#jump_to, #jump_wrapper { - position: fixed; - right: 0; top: 0; - padding: 5px 10px; -} - #jump_wrapper { - padding: 0; - display: none; - } - #jump_to:hover #jump_wrapper { - display: block; - } - #jump_page { - padding: 5px 0 3px; - margin: 0 0 25px 25px; - } - #jump_page .source { - display: block; - padding: 5px 10px; - text-decoration: none; - border-top: 1px solid #eee; - } - #jump_page .source:hover { - background: #f5f5ff; - } - #jump_page .source:first-child { - } -table td { - border: 0; - outline: 0; -} - td.docs, th.docs { - max-width: 450px; - min-width: 450px; - min-height: 5px; - padding: 10px 25px 1px 50px; - overflow-x: hidden; - vertical-align: top; - text-align: left; - } - .docs pre { - margin: 15px 0 15px; - padding-left: 15px; - } - .docs p tt, .docs p code { - background: #f8f8ff; - border: 1px solid #dedede; - font-size: 12px; - padding: 0 0.2em; - } - .pilwrap { - position: relative; - } - .pilcrow { - font: 12px Arial; - text-decoration: none; - color: #454545; - position: absolute; - top: 3px; left: -20px; - padding: 1px 2px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - } - td.docs:hover .pilcrow { - opacity: 1; - } - td.code, th.code { - padding: 14px 15px 16px 25px; - width: 100%; - vertical-align: top; - background: #f5f5ff; - border-left: 1px solid #e5e5ee; - } - pre, tt, code { - font-size: 12px; line-height: 18px; - font-family: Monaco, Consolas, "Lucida Console", monospace; - margin: 0; padding: 0; - } - - -/*---------------------- Syntax Highlighting -----------------------------*/ -td.linenos { background-color: #f0f0f0; padding-right: 10px; } -span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } -body .hll { background-color: #ffffcc } -body .c { color: #408080; font-style: italic } /* Comment */ -body .err { border: 1px solid #FF0000 } /* Error */ -body .k { color: #954121 } /* Keyword */ -body .o { color: #666666 } /* Operator */ -body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -body .cp { color: #BC7A00 } /* Comment.Preproc */ -body .c1 { color: #408080; font-style: italic } /* Comment.Single */ -body .cs { color: #408080; font-style: italic } /* Comment.Special */ -body .gd { color: #A00000 } /* Generic.Deleted */ -body .ge { font-style: italic } /* Generic.Emph */ -body .gr { color: #FF0000 } /* Generic.Error */ -body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -body .gi { color: #00A000 } /* Generic.Inserted */ -body .go { color: #808080 } /* Generic.Output */ -body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -body .gs { font-weight: bold } /* Generic.Strong */ -body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -body .gt { color: #0040D0 } /* Generic.Traceback */ -body .kc { color: #954121 } /* Keyword.Constant */ -body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ -body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ -body .kp { color: #954121 } /* Keyword.Pseudo */ -body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ -body .kt { color: #B00040 } /* Keyword.Type */ -body .m { color: #666666 } /* Literal.Number */ -body .s { color: #219161 } /* Literal.String */ -body .na { color: #7D9029 } /* Name.Attribute */ -body .nb { color: #954121 } /* Name.Builtin */ -body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -body .no { color: #880000 } /* Name.Constant */ -body .nd { color: #AA22FF } /* Name.Decorator */ -body .ni { color: #999999; font-weight: bold } /* Name.Entity */ -body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -body .nf { color: #0000FF } /* Name.Function */ -body .nl { color: #A0A000 } /* Name.Label */ -body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -body .nt { color: #954121; font-weight: bold } /* Name.Tag */ -body .nv { color: #19469D } /* Name.Variable */ -body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -body .w { color: #bbbbbb } /* Text.Whitespace */ -body .mf { color: #666666 } /* Literal.Number.Float */ -body .mh { color: #666666 } /* Literal.Number.Hex */ -body .mi { color: #666666 } /* Literal.Number.Integer */ -body .mo { color: #666666 } /* Literal.Number.Oct */ -body .sb { color: #219161 } /* Literal.String.Backtick */ -body .sc { color: #219161 } /* Literal.String.Char */ -body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ -body .s2 { color: #219161 } /* Literal.String.Double */ -body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -body .sh { color: #219161 } /* Literal.String.Heredoc */ -body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -body .sx { color: #954121 } /* Literal.String.Other */ -body .sr { color: #BB6688 } /* Literal.String.Regex */ -body .s1 { color: #219161 } /* Literal.String.Single */ -body .ss { color: #19469D } /* Literal.String.Symbol */ -body .bp { color: #954121 } /* Name.Builtin.Pseudo */ -body .vc { color: #19469D } /* Name.Variable.Class */ -body .vg { color: #19469D } /* Name.Variable.Global */ -body .vi { color: #19469D } /* Name.Variable.Instance */ -body .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/speakeasy.html b/docs/speakeasy.html deleted file mode 100644 index fb0ae24..0000000 --- a/docs/speakeasy.html +++ /dev/null @@ -1,164 +0,0 @@ - speakeasy.js

speakeasy.js

speakeasy

- -

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

- -

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

- -

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

- -

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

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

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

- -

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

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

speakeasy.hotp(options)

- -

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

- -

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

speakeasy.hotp = function(options) {

set vars

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

preprocessing: convert to ascii if it's not

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

init hmac with the key

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

create an octet array from the counter

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

mask 255 over number to get last 8

    octet_array[i_from_right] = counter_temp & 255;

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

    counter_temp = counter_temp >> 8;
-  }

create a buffer from the octet array

  var counter_buffer = new Buffer(octet_array);

update hmac with the counter

  hmac.update(counter_buffer);

get the digest in hex format

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

convert the result to an array of bytes

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

compute HOTP -get offset

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

calculate bin_code (RFC4226 5.4)

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

get the chars at position bin_code - length through length chars

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

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

  return(code);
-}

speakeasy.totp(options)

- -

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

- -

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

speakeasy.totp = function(options) {

set vars

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

get current time in seconds since unix epoch

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

are we forcing a specific time?

  if (options.time_now) {

override the time

    time_now = options.time_now;
-  }

calculate counter value

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

pass to hotp

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

return the code

  return(code);
-}

speakeasy.hextoascii(key)

- -

helper function to convert a hex key to ascii.

speakeasy.hex_to_ascii = function(str) {

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

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

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

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

speakeasy.asciitohex(key)

- -

helper function to convert an ascii key to hex.

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

speakeasy.generate_key(options)

- -

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

- -

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

speakeasy.generate_key = function(options) {

options

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

turn off symbols only when explicity told to

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

generate an ascii key

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

return a SecretKey with ascii, hex, and base32

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

generate some qr codes if requested

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

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

  if (google_auth_qr) {

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

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

speakeasy.generatekeyascii(length, symbols)

- -

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

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

alias, not the TV show

speakeasy.counter = speakeasy.hotp;
-speakeasy.time = speakeasy.totp;
-
-module.exports = speakeasy;
-
-
\ No newline at end of file diff --git a/index.js b/index.js index 8854ed0..f6e6cd0 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,647 @@ -// i has a cheezburger -module.exports = require('./lib/speakeasy'); +'use strict'; + +var base32 = require('base32.js'); +var crypto = require('crypto'); +var url = require('url'); +var util = require('util'); + +/** + * Digest the one-time passcode options. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {Integer} options.counter Counter value + * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, + * base32, base64). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @param {String} [options.key] (DEPRECATED. Use `secret` instead.) + * Shared secret key + * @return {Buffer} The one-time passcode as a buffer. + */ + +exports.digest = function digest (options) { + var i; + + // unpack options + var secret = options.secret; + var counter = options.counter; + var encoding = options.encoding || 'ascii'; + var algorithm = (options.algorithm || 'sha1').toLowerCase(); + + // Backwards compatibility - deprecated + if (options.key != null) { + console.warn('Speakeasy - Deprecation Notice - Specifying the secret using `key` is no longer supported. Use `secret` instead.'); + secret = options.key; + } + + // convert secret to buffer + if (!Buffer.isBuffer(secret)) { + secret = encoding === 'base32' ? base32.decode(secret) + : new Buffer(secret, encoding); + } + + // create an buffer from the counter + var buf = new Buffer(8); + var tmp = counter; + for (i = 0; i < 8; i++) { + // mask 0xff over number to get last 8 + buf[7 - i] = tmp & 0xff; + + // shift 8 and get ready to loop over the next batch of 8 + tmp = tmp >> 8; + } + + // init hmac with the key + var hmac = crypto.createHmac(algorithm, secret); + + // update hmac with the counter + hmac.update(buf); + + // return the digest + return hmac.digest(); +}; + +/** + * Generate a counter-based one-time token. Specify the key and counter, and + * receive the one-time password for that counter position as a string. You can + * also specify a token length, as well as the encoding (ASCII, hexadecimal, or + * base32) and the hashing algorithm to use (SHA1, SHA256, SHA512). + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {Integer} options.counter Counter value + * @param {Buffer} [options.digest] Digest, automatically generated by default + * @param {Integer} [options.digits=6] The number of digits for the one-time + * passcode. + * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, + * base32, base64). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @param {String} [options.key] (DEPRECATED. Use `secret` instead.) + * Shared secret key + * @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The + * number of digits for the one-time passcode. + * @return {String} The one-time passcode. + */ + +exports.hotp = function hotpGenerate (options) { + // unpack digits + // backward compatibility: `length` is also accepted here, but deprecated + var digits = (options.digits != null ? options.digits : options.length) || 6; + if (options.length != null) console.warn('Speakeasy - Deprecation Notice - Specifying token digits using `length` is no longer supported. Use `digits` instead.'); + + // digest the options + var digest = options.digest || exports.digest(options); + + // compute HOTP offset + var offset = digest[digest.length - 1] & 0xf; + + // calculate binary code (RFC4226 5.4) + var code = (digest[offset] & 0x7f) << 24 | + (digest[offset + 1] & 0xff) << 16 | + (digest[offset + 2] & 0xff) << 8 | + (digest[offset + 3] & 0xff); + + // left-pad code + code = new Array(digits + 1).join('0') + code.toString(10); + + // return length number off digits + return code.substr(-digits); +}; + +// Alias counter() for hotp() +exports.counter = exports.hotp; + +/** + * Verify a counter-based one-time token against the secret and return the delta. + * By default, it verifies the token at the given counter value, with no leeway + * (no look-ahead or look-behind). A token validated at the current counter value + * will have a delta of 0. + * + * You can specify a window to add more leeway to the verification process. + * Setting the window param will check for the token at the given counter value + * as well as `window` tokens ahead (one-sided window). See param for more info. + * + * `verifyDelta()` will return the delta between the counter value of the token + * and the given counter value. For example, if given a counter 5 and a window + * 10, `verifyDelta()` will look at tokens from 5 to 15, inclusive. If it finds + * it at counter position 7, it will return `{ delta: 2 }`. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} options.counter Counter value. This should be stored by + * the application and must be incremented for each request. + * @param {Integer} [options.digits=6] The number of digits for the one-time + * passcode. + * @param {Integer} [options.window=0] The allowable margin for the counter. + * The function will check "W" codes in the future against the provided + * passcode, e.g. if W = 10, and C = 5, this function will check the + * passcode against all One Time Passcodes between 5 and 15, inclusive. + * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, + * base32, base64). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @return {Object} On success, returns an object with the counter + * difference between the client and the server as the `delta` property (i.e. + * `{ delta: 0 }`). + * @method hotp․verifyDelta + * @global + */ + +exports.hotp.verifyDelta = function hotpVerifyDelta (options) { + var i; + + // shadow options + options = Object.create(options); + + // unpack options + var token = String(options.token); + var digits = parseInt(options.digits, 10) || 6; + var window = parseInt(options.window, 10) || 0; + var counter = parseInt(options.counter, 10) || 0; + + // fail if token is not of correct length + if (token.length !== digits) { + return; + } + + // parse token to integer + token = parseInt(token, 10); + + // fail if token is NA + if (isNaN(token)) { + return; + } + + // loop from C to C + W inclusive + for (i = counter; i <= counter + window; ++i) { + options.counter = i; + // domain-specific constant-time comparison for integer codes + if (parseInt(exports.hotp(options), 10) === token) { + // found a matching code, return delta + return {delta: i - counter}; + } + } + + // no codes have matched +}; + +/** + * Verify a counter-based one-time token against the secret and return true if + * it verifies. Helper function for `hotp.verifyDelta()`` that returns a boolean + * instead of an object. For more on how to use a window with this, see + * {@link hotp.verifyDelta}. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} options.counter Counter value. This should be stored by + * the application and must be incremented for each request. + * @param {Integer} [options.digits=6] The number of digits for the one-time + * passcode. + * @param {Integer} [options.window=0] The allowable margin for the counter. + * The function will check "W" codes in the future against the provided + * passcode, e.g. if W = 10, and C = 5, this function will check the + * passcode against all One Time Passcodes between 5 and 15, inclusive. + * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, + * base32, base64). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @return {Boolean} Returns true if the token matches within the given + * window, false otherwise. + * @method hotp․verify + * @global + */ +exports.hotp.verify = function hotpVerify (options) { + return exports.hotp.verifyDelta(options) != null; +}; + +/** + * Calculate counter value based on given options. A counter value converts a + * TOTP time into a counter value by finding the number of time steps that have + * passed since the epoch to the current time. + * + * @param {Object} options + * @param {Integer} [options.time] Time in seconds with which to calculate + * counter value. Defaults to `Date.now()`. + * @param {Integer} [options.step=30] Time step in seconds + * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from + * which to calculate the counter value. Defaults to 0 (no offset). + * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.) + * Initial time in seconds since the UNIX epoch from which to calculate the + * counter value. Defaults to 0 (no offset). + * @return {Integer} The calculated counter value. + * @private + */ + +exports._counter = function _counter (options) { + var step = options.step || 30; + var time = options.time != null ? (options.time * 1000) : Date.now(); + + // also accepts 'initial_time', but deprecated + var epoch = (options.epoch != null ? (options.epoch * 1000) : (options.initial_time * 1000)) || 0; + if (options.initial_time != null) console.warn('Speakeasy - Deprecation Notice - Specifying the epoch using `initial_time` is no longer supported. Use `epoch` instead.'); + + return Math.floor((time - epoch) / step / 1000); +}; + +/** + * Generate a time-based one-time token. Specify the key, and receive the + * one-time password for that time as a string. By default, it uses the current + * time and a time step of 30 seconds, so there is a new token every 30 seconds. + * You may override the time step and epoch for custom timing. You can also + * specify a token length, as well as the encoding (ASCII, hexadecimal, or + * base32) and the hashing algorithm to use (SHA1, SHA256, SHA512). + * + * Under the hood, TOTP calculates the counter value by finding how many time + * steps have passed since the epoch, and calls HOTP with that counter value. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {Integer} [options.time] Time in seconds with which to calculate + * counter value. Defaults to `Date.now()`. + * @param {Integer} [options.step=30] Time step in seconds + * @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX + * epoch from which to calculate the counter value. Defaults to 0 (no offset). + * @param {Integer} [options.counter] Counter value, calculated by default. + * @param {Integer} [options.digits=6] The number of digits for the one-time + * passcode. + * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, + * base32, base64). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @param {String} [options.key] (DEPRECATED. Use `secret` instead.) + * Shared secret key + * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.) + * Initial time in seconds since the UNIX epoch from which to calculate the + * counter value. Defaults to 0 (no offset). + * @param {Integer} [options.length=6] (DEPRECATED. Use `digits` instead.) The + * number of digits for the one-time passcode. + * @return {String} The one-time passcode. + */ + +exports.totp = function totpGenerate (options) { + // shadow options + options = Object.create(options); + + // calculate default counter value + if (options.counter == null) options.counter = exports._counter(options); + + // pass to hotp + return this.hotp(options); +}; + +// Alias time() for totp() +exports.time = exports.totp; + +/** + * Verify a time-based one-time token against the secret and return the delta. + * By default, it verifies the token at the current time window, with no leeway + * (no look-ahead or look-behind). A token validated at the current time window + * will have a delta of 0. + * + * You can specify a window to add more leeway to the verification process. + * Setting the window param will check for the token at the given counter value + * as well as `window` tokens ahead and `window` tokens behind (two-sided + * window). See param for more info. + * + * `verifyDelta()` will return the delta between the counter value of the token + * and the given counter value. For example, if given a time at counter 1000 and + * a window of 5, `verifyDelta()` will look at tokens from 995 to 1005, + * inclusive. In other words, if the time-step is 30 seconds, it will look at + * tokens from 2.5 minutes ago to 2.5 minutes in the future, inclusive. + * If it finds it at counter position 1002, it will return `{ delta: 2 }`. + * If it finds it at counter position 997, it will return `{ delta: -3 }`. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} [options.time] Time in seconds with which to calculate + * counter value. Defaults to `Date.now()`. + * @param {Integer} [options.step=30] Time step in seconds + * @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX + * epoch from which to calculate the counter value. Defaults to 0 (no offset). + * @param {Integer} [options.counter] Counter value, calculated by default. + * @param {Integer} [options.digits=6] The number of digits for the one-time + * passcode. + * @param {Integer} [options.window=0] The allowable margin for the counter. + * The function will check "W" codes in the future and the past against the + * provided passcode, e.g. if W = 5, and C = 1000, this function will check + * the passcode against all One Time Passcodes between 995 and 1005, + * inclusive. + * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, + * base32, base64). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @return {Object} On success, returns an object with the time step + * difference between the client and the server as the `delta` property (e.g. + * `{ delta: 0 }`). + * @method totp․verifyDelta + * @global + */ + +exports.totp.verifyDelta = function totpVerifyDelta (options) { + // shadow options + options = Object.create(options); + + // unpack options + var window = parseInt(options.window, 10) || 0; + + // calculate default counter value + if (options.counter == null) options.counter = exports._counter(options); + + // adjust for two-sided window + options.counter -= window; + options.window += window; + + // pass to hotp.verifyDelta + var delta = exports.hotp.verifyDelta(options); + + // adjust for two-sided window + if (delta) { + delta.delta -= window; + } + + return delta; +}; + +/** + * Verify a time-based one-time token against the secret and return true if it + * verifies. Helper function for verifyDelta() that returns a boolean instead of + * an object. For more on how to use a window with this, see + * {@link totp.verifyDelta}. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} [options.time] Time in seconds with which to calculate + * counter value. Defaults to `Date.now()`. + * @param {Integer} [options.step=30] Time step in seconds + * @param {Integer} [options.epoch=0] Initial time in seconds since the UNIX + * epoch from which to calculate the counter value. Defaults to 0 (no offset). + * @param {Integer} [options.counter] Counter value, calculated by default. + * @param {Integer} [options.digits=6] The number of digits for the one-time + * passcode. + * @param {Integer} [options.window=0] The allowable margin for the counter. + * The function will check "W" codes in the future and the past against the + * provided passcode, e.g. if W = 5, and C = 1000, this function will check + * the passcode against all One Time Passcodes between 995 and 1005, + * inclusive. + * @param {String} [options.encoding="ascii"] Key encoding (ascii, hex, + * base32, base64). + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @return {Boolean} Returns true if the token matches within the given + * window, false otherwise. + * @method totp․verify + * @global + */ +exports.totp.verify = function totpVerify (options) { + return exports.totp.verifyDelta(options) != null; +}; + +/** + * @typedef GeneratedSecret + * @type Object + * @property {String} ascii ASCII representation of the secret + * @property {String} hex Hex representation of the secret + * @property {String} base32 Base32 representation of the secret + * @property {String} qr_code_ascii URL for the QR code for the ASCII secret. + * @property {String} qr_code_hex URL for the QR code for the hex secret. + * @property {String} qr_code_base32 URL for the QR code for the base32 secret. + * @property {String} google_auth_qr URL for the Google Authenticator otpauth + * URL's QR code. + * @property {String} otpauth_url Google Authenticator-compatible otpauth URL. + */ + +/** + * Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length + * (default 32). Returns the secret key in ASCII, hexadecimal, and base32 format, + * along with the URL used for the QR code for Google Authenticator (an otpauth + * URL). Use a QR code library to generate a QR code based on the Google + * Authenticator URL to obtain a QR code you can scan into the app. + * + * @param {Object} options + * @param {Integer} [options.length=32] Length of the secret + * @param {Boolean} [options.symbols=false] Whether to include symbols + * @param {Boolean} [options.otpauth_url=true] Whether to output a Google + * Authenticator-compatible otpauth:// URL (only returns otpauth:// URL, no + * QR code) + * @param {String} [options.name] The name to use with Google Authenticator. + * @param {Boolean} [options.qr_codes=false] (DEPRECATED. Do not use to prevent + * leaking of secret to a third party. Use your own QR code implementation.) + * Output QR code URLs for the token. + * @param {Boolean} [options.google_auth_qr=false] (DEPRECATED. Do not use to + * prevent leaking of secret to a third party. Use your own QR code + * implementation.) Output a Google Authenticator otpauth:// QR code URL. + * @return {Object} + * @return {GeneratedSecret} The generated secret key. + */ +exports.generateSecret = function generateSecret (options) { + // options + if (!options) options = {}; + var length = options.length || 32; + var name = encodeURIComponent(options.name || 'SecretKey'); + var qr_codes = options.qr_codes || false; + var google_auth_qr = options.google_auth_qr || false; + var otpauth_url = options.otpauth_url != null ? options.otpauth_url : true; + var symbols = true; + + // turn off symbols only when explicity told to + if (options.symbols !== undefined && options.symbols === false) { + symbols = false; + } + + // generate an ascii key + var key = this.generateSecretASCII(length, symbols); + + // return a SecretKey with ascii, hex, and base32 + var SecretKey = {}; + SecretKey.ascii = key; + SecretKey.hex = Buffer(key, 'ascii').toString('hex'); + SecretKey.base32 = base32.encode(Buffer(key)).toString().replace(/=/g, ''); + + // generate some qr codes if requested + if (qr_codes) { + console.warn('Speakeasy - Deprecation Notice - generateSecret() QR codes are deprecated and no longer supported. Please use your own QR code implementation.'); + SecretKey.qr_code_ascii = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.ascii); + SecretKey.qr_code_hex = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.hex); + SecretKey.qr_code_base32 = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.base32); + } + + // add in the Google Authenticator-compatible otpauth URL + if (otpauth_url) { + SecretKey.otpauth_url = exports.otpauthURL({ + secret: SecretKey.ascii, + label: name + }); + } + + // generate a QR code for use in Google Authenticator if requested + if (google_auth_qr) { + console.warn('Speakeasy - Deprecation Notice - generateSecret() Google Auth QR code is deprecated and no longer supported. Please use your own QR code implementation.'); + SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(exports.otpauthURL({ secret: SecretKey.base32, label: name })); + } + + return SecretKey; +}; + +// Backwards compatibility - generate_key is deprecated +exports.generate_key = util.deprecate(function (options) { + return exports.generateSecret(options); +}, 'Speakeasy - Deprecation Notice - `generate_key()` is depreciated, please use `generateSecret()` instead.'); + +/** + * Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and + * symbols (if requested). + * + * @param {Integer} [length=32] The length of the key. + * @param {Boolean} [symbols=false] Whether to include symbols in the key. + * @return {String} The generated key. + */ +exports.generateSecretASCII = function generateSecretASCII (length, symbols) { + var bytes = crypto.randomBytes(length || 32); + var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'; + if (symbols) { + set += '!@#$%^&*()<>?/[]{},.:;'; + } + + var output = ''; + for (var i = 0, l = bytes.length; i < l; i++) { + output += set[Math.floor(bytes[i] / 255.0 * (set.length - 1))]; + } + return output; +}; + +// Backwards compatibility - generate_key_ascii is deprecated +exports.generate_key_ascii = util.deprecate(function (length, symbols) { + return exports.generateSecretASCII(length, symbols); +}, 'Speakeasy - Deprecation Notice - `generate_key_ascii()` is depreciated, please use `generateSecretASCII()` instead.'); + +/** + * Generate a Google Authenticator-compatible otpauth:// URL for passing the + * secret to a mobile device to install the secret. + * + * Authenticator considers TOTP codes valid for 30 seconds. Additionally, + * the app presents 6 digits codes to the user. According to the + * documentation, the period and number of digits are currently ignored by + * the app. + * + * To generate a suitable QR Code, pass the generated URL to a QR Code + * generator, such as the `qr-image` module. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.label Used to identify the account with which + * the secret key is associated, e.g. the user's email address. + * @param {String} [options.type="totp"] Either "hotp" or "totp". + * @param {Integer} [options.counter] The initial counter value, required + * for HOTP. + * @param {String} [options.issuer] The provider or service with which the + * secret key is associated. + * @param {String} [options.algorithm="sha1"] Hash algorithm (sha1, sha256, + * sha512). + * @param {Integer} [options.digits=6] The number of digits for the one-time + * passcode. Currently ignored by Google Authenticator. + * @param {Integer} [options.period=30] The length of time for which a TOTP + * code will be valid, in seconds. Currently ignored by Google + * Authenticator. + * @param {String} [options.encoding] Key encoding (ascii, hex, base32, + * base64). If the key is not encoded in Base-32, it will be reencoded. + * @return {String} A URL suitable for use with the Google Authenticator. + * @throws Error if secret or label is missing, or if hotp is used and a + counter is missing, if the type is not one of `hotp` or `totp`, if the + number of digits is non-numeric, or an invalid period is used. Warns if + the number of digits is not either 6 or 8 (though 6 is the only one + supported by Google Authenticator), and if the hashihng algorithm is + not one of the supported SHA1, SHA256, or SHA512. + * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format + */ + +exports.otpauthURL = function otpauthURL (options) { + // unpack options + var secret = options.secret; + var label = options.label; + var issuer = options.issuer; + var type = (options.type || 'totp').toLowerCase(); + var counter = options.counter; + var algorithm = options.algorithm; + var digits = options.digits; + var period = options.period; + var encoding = options.encoding || 'ascii'; + + // validate type + switch (type) { + case 'totp': + case 'hotp': + break; + default: + throw new Error('Speakeasy - otpauthURL - Invalid type `' + type + '`; must be `hotp` or `totp`'); + } + + // validate required options + if (!secret) throw new Error('Speakeasy - otpauthURL - Missing secret'); + if (!label) throw new Error('Speakeasy - otpauthURL - Missing label'); + + // require counter for HOTP + if (type === 'hotp' && (counter === null || typeof counter === 'undefined')) { + throw new Error('Speakeasy - otpauthURL - Missing counter value for HOTP'); + } + + // convert secret to base32 + if (encoding !== 'base32') secret = new Buffer(secret, encoding); + if (Buffer.isBuffer(secret)) secret = base32.encode(secret); + + // build query while validating + var query = {secret: secret}; + if (issuer) query.issuer = issuer; + + // validate algorithm + if (algorithm != null) { + switch (algorithm.toUpperCase()) { + case 'SHA1': + case 'SHA256': + case 'SHA512': + break; + default: + console.warn('Speakeasy - otpauthURL - Warning - Algorithm generally should be SHA1, SHA256, or SHA512'); + } + query.algorithm = algorithm.toUpperCase(); + } + + // validate digits + if (digits != null) { + if (isNaN(digits)) { + throw new Error('Speakeasy - otpauthURL - Invalid digits `' + digits + '`'); + } else { + switch (parseInt(digits, 10)) { + case 6: + case 8: + break; + default: + console.warn('Speakeasy - otpauthURL - Warning - Digits generally should be either 6 or 8'); + } + } + query.digits = digits; + } + + // validate period + if (period != null) { + period = parseInt(period, 10); + if (~~period !== period) { + throw new Error('Speakeasy - otpauthURL - Invalid period `' + period + '`'); + } + query.period = period; + } + + // return url + return url.format({ + protocol: 'otpauth', + slashes: true, + hostname: type, + pathname: label, + query: query + }); +}; diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..6a92809 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,25 @@ +{ + "source": { + "include": [ + "index.js", + "package.json", + "README.md" + ] + }, + "plugins": ["plugins/markdown"], + "templates": { + "applicationName": "Speakeasy", + "meta": { + "title": "Speakeasy", + "description": "Speakeasy - Two-factor authentication for Node.js. One-time passcode generator (HOTP/TOTP) with URL generation for Google Authenticator", + "keyword": "one-time passcode hotp totp google authenticator" + }, + "default": { + "outputSourceFiles": true + }, + "linenums": true + }, + "opts": { + "destination": "docs" + } +} diff --git a/lib/speakeasy.js b/lib/speakeasy.js deleted file mode 100644 index 98a6c7a..0000000 --- a/lib/speakeasy.js +++ /dev/null @@ -1,288 +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('thirty-two'); - -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) -// -speakeasy.hotp = function(options) { - // set vars - var key = options.key; - var counter = options.counter; - var length = options.length || 6; - var encoding = options.encoding || 'ascii'; - - // preprocessing: convert to ascii if it's not - if (encoding === 'hex') { - key = speakeasy.hex_to_ascii(key); - } else if (encoding === 'base32') { - key = base32.decode(key); - } - - // init hmac with the key - var hmac = crypto.createHmac('sha1', 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 = speakeasy.hexToBytes(digest); - - // compute HOTP - // get offset - var offset = digest_bytes[19] & 0xf; - - // calculate bin_code (RFC4226 5.4) - var bin_code = (digest_bytes[offset] & 0x7f) << 24 - |(digest_bytes[offset+1] & 0xff) << 16 - |(digest_bytes[offset+2] & 0xff) << 8 - |(digest_bytes[offset+3] & 0xff); - - var code = speakeasy.bin_to_string(bin_code, length); - - 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 -// -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 - - // 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}); - - // return the code - return(code); -}; - -// speakeasy.bin_to_string(bin, length) -// -// helper function to convert a number to a string of given length. -// -speakeasy.bin_to_string = function(bin, length) { - var bin_str = bin.toString(); - - if (bin_str.length < length) { - // pad with 0's - var pad = ''; - var padLength = length - bin_str.length; - for (var j = 0; j < padLength; ++j) { - pad += '0'; - } - - return pad + bin_str; - } else { - // get the chars at position bin_code - length through length chars - var sub_start = bin_str.length - length; - var code = bin_str.substr(sub_start, length); - - 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 = speakeasy.hexToBytes(str); - - // 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 -// (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 -// -speakeasy.generate_key = function(options) { - // options - if(!options) options = {}; - var length = options.length || 32; - var name = options.name || "Secret Key"; - var qr_codes = options.qr_codes || false; - var google_auth_qr = options.google_auth_qr || false; - var symbols = true; - - // turn off symbols only when explicity told to - if (options.symbols !== undefined && options.symbols === false) { - symbols = false; - } - - // generate an ascii key - var key = this.generate_key_ascii(length, symbols); - - // return a SecretKey with ascii, hex, and base32 - var SecretKey = {}; - SecretKey.ascii = key; - SecretKey.hex = this.ascii_to_hex(key); - SecretKey.base32 = base32.encode(key).toString().replace(/=/g,''); - - // generate some qr codes if requested - if (qr_codes) { - SecretKey.qr_code_ascii = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.ascii); - SecretKey.qr_code_hex = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.hex); - SecretKey.qr_code_base32 = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.base32); - } - - // generate a QR code for use in Google Authenticator if requested - // (Google Authenticator has a special style and requires base32) - if (google_auth_qr) { - // first, make sure that the name doesn't have spaces, since Google Authenticator doesn't like them - name = name.replace(/ /g,''); - SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://totp/' + encodeURIComponent(name) + '%3Fsecret=' + encodeURIComponent(SecretKey.base32); - } - - return SecretKey; -}; - -// 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[Math.floor(bytes[i] / 255.0 * (set.length-1))]; - } - 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; - -module.exports = speakeasy; diff --git a/package.json b/package.json index 27c4e0d..a829d3d 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,58 @@ { - "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.5", - "homepage": "http://github.com/markbao/speakeasy", + "description": "Two-factor authentication for Node.js. One-time passcode generator (HOTP/TOTP) with support for Google Authenticator.", + "version": "2.0.0", + "author": { + "name": "Mark Bao & Speakeasy Contributors", + "email": "mark@markbao.com" + }, + "contributors": [{ + "name": "Michael Phan-Ba", + "email": "michael@mikepb.com" + }, { + "name": "Guy Halford-Thompson", + "email": "guy@cach.me" + }], + "homepage": "http://github.com/speakeasyjs/speakeasy", + "bugs": "https://github.com/speakeasyjs/speakeasy/issues", + "keywords": [ + "authentication", + "google authenticator", + "hmac", + "hotp", + "multi-factor", + "one-time password", + "passwords", + "totp", + "two factor", + "two-factor", + "two-factor authentication" + ], + "license": "MIT", "repository": { "type": "git", - "url": "git://github.com/markbao/speakeasy.git" + "url": "git://github.com/speakeasyjs/speakeasy.git" }, "main": "index.js", "engines": { - "node": ">= 0.3.0" + "node": ">= 0.10.0" }, "dependencies": { - "thirty-two": "0.0.2" + "base32.js": "0.0.1" }, - "keywords": [ - "two-factor", - "authentication", - "hotp", - "totp", - "multi-factor", - "hmac", - "one-time password", - "passwords" - ], "devDependencies": { - "vows": "*" + "chai": "^3.4.1", + "coveralls": "^2.11.6", + "istanbul": "^0.4.2", + "jsdoc": "^3.3.1", + "mocha": "^2.2.5", + "semistandard": "^7.0.5", + "snazzy": "^2.0.1" }, "scripts": { - "test": "vows --spec test/*" + "test": "mocha", + "doc": "jsdoc -c jsdoc.json && sed -i '' -e 's/․/./g' docs/speakeasy/*/*.html", + "cover": "istanbul cover _mocha -- test/* -R spec", + "lint": "semistandard --verbose | snazzy" } } diff --git a/test/generate.js b/test/generate.js new file mode 100644 index 0000000..b24cae8 --- /dev/null +++ b/test/generate.js @@ -0,0 +1,80 @@ +'use strict'; + +/* global describe, it */ + +var chai = require('chai'); +var assert = chai.assert; +var base32 = require('base32.js'); +var speakeasy = require('..'); + +// These tests use the information from RFC 4226's Appendix D: Test Values. +// http://tools.ietf.org/html/rfc4226#appendix-D + +describe('Generator tests', function () { + it('Normal generation with defaults', function () { + var secret = speakeasy.generateSecret(); + assert.equal(secret.ascii.length, 32, 'Should return the correct length'); + + // check returned fields + assert.isDefined(secret.otpauth_url, 'otpauth:// URL should be returned'); + assert.isUndefined(secret.qr_code_ascii, 'QR Code ASCII should not be returned'); + assert.isUndefined(secret.qr_code_hex, 'QR Code Hex should not be returned'); + assert.isUndefined(secret.qr_code_base32, 'QR Code Base 32 should not be returned'); + assert.isUndefined(secret.google_auth_qr, 'Google Auth QR should not be returned'); + + // check encodings + assert.equal(Buffer(secret.hex, 'hex').toString('ascii'), secret.ascii, 'Should have encoded correct hex string'); + assert.equal(base32.decode(secret.base32).toString('ascii'), secret.ascii, 'Should have encoded correct base32 string'); + }); + + it('Generation with custom key length', function () { + var secret = speakeasy.generateSecret({length: 50}); + assert.equal(secret.ascii.length, 50, 'Should return the correct length'); + }); + + it('Generation with symbols disabled', function () { + var secret = speakeasy.generateSecret({symbols: false}); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Generation with QR URL output enabled', function () { + var secret = speakeasy.generateSecret({qr_codes: true}); + assert.isDefined(secret.qr_code_ascii, 'QR Code ASCII should be returned'); + assert.isDefined(secret.qr_code_hex, 'QR Code Hex should be returned'); + assert.isDefined(secret.qr_code_base32, 'QR Code Base 32 should be returned'); + }); + + it('Generation with otpath:// URL output disabled', function () { + var secret = speakeasy.generateSecret({otpauth_url: false}); + assert.isUndefined(secret.otpauth_url, 'Google Auth URL should not be returned'); + }); + + it('Generation with Google Auth QR URL output enabled', function () { + var secret = speakeasy.generateSecret({google_auth_qr: true}); + assert.isDefined(secret.google_auth_qr, 'Google Auth QR should be returned'); + }); + + it('Testing generateSecretASCII with defaults', function () { + var secret = speakeasy.generateSecretASCII(); + assert.equal(secret.length, 32, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Testing generateSecretASCII with custom length', function () { + var secret = speakeasy.generateSecretASCII(20); + assert.equal(secret.length, 20, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Testing backward compatibility (deprecated function) generate_key', function () { + var secret = speakeasy.generate_key(); + assert.ok(secret, 'Should return a secret'); + assert.equal(secret.ascii.length, 32, 'Should return default secret length'); + }); + + it('Testing backward compatibility (deprecated function) generate_key_ascii', function () { + var secret = speakeasy.generate_key_ascii(20); + assert.ok(secret, 'Should return a secret'); + assert.equal(secret.length, 20, 'Should return the correct length'); + }); +}); diff --git a/test/hotp_test.js b/test/hotp_test.js new file mode 100644 index 0000000..373fbdb --- /dev/null +++ b/test/hotp_test.js @@ -0,0 +1,76 @@ +'use strict'; + +/* global describe, it */ + +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); + +// These tests use the information from RFC 4226's Appendix D: Test Values. +// http://tools.ietf.org/html/rfc4226#appendix-D + +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'); + }); + }); + + // Backwards compatibility - deprecated + describe("digits override with secret = '12345678901234567890' at counter 4 and length = 8", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 4, length: 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 = '12345678901234567890' at counter 3", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.hotp({secret: '12345678901234567890', counter: 3}); + assert.equal(topic, '969429'); + }); + }); + + describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA' as base32 at counter 1, digits = 8 and algorithm as 'sha256'", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha256'}); + assert.equal(topic, '46119246'); + }); + }); + + describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA' as base32 at counter 1, digits = 8 and algorithm as 'sha512'", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.hotp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', counter: 1, digits: 8, algorithm: 'sha512'}); + assert.equal(topic, '90693936'); + }); + }); +}); diff --git a/test/notp_test.js b/test/notp_test.js new file mode 100644 index 0000000..c300c16 --- /dev/null +++ b/test/notp_test.js @@ -0,0 +1,269 @@ +'use strict'; + +/* global it */ + +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); + +/* + * Tests originally from the notp module with specific changes and bugfixes for + * Speakeasy: https://github.com/guyht/notp + * + * Test HOTtoken. Uses test values from RFcounter 4226 + * + * + * 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 = { + secret: '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); + + // check for invalid token value in verifyDelta + options.token = 'NOPASS'; + assert.ok(!speakeasy.hotp.verifyDelta(options), 'Should not pass'); + + // countercheck for failure + options.counter = 0; + assert.ok(!speakeasy.hotp.verify(options), 'Should not pass'); + + // countercheck for passes + for (var i = 0; i < HOTP.length; i++) { + options.counter = i; + options.token = HOTP[i]; + + var res = speakeasy.hotp.verifyDelta(options); + + assert.ok(res, 'Should pass'); + assert.equal(res.delta, 0, 'Should be in sync'); + + res = speakeasy.hotp.verify(options); + + assert.ok(res, 'Should pass'); + } +}); + +/* + * Test TOTtoken using test vectors from TOTtoken RFcounter. + * + * see http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.txt + */ + +it('TOTtoken', function () { + var options = { + secret: '12345678901234567890', + window: 0 + }; + + // countercheck for failure + options.time = 0; + options.token = 'windowILLNOTtokenASS'; + assert.ok(!speakeasy.totp.verify(options), 'Should not pass'); + + // countercheck for failure + options.time = 0; + options.token = 'windowILLNOTtokenASS'; + assert.ok(!speakeasy.totp.verifyDelta(options), 'Should not pass'); + + // countercheck for test vector at 59s with verifyDelta + options.time = 59; + options.token = '287082'; + var res = speakeasy.totp.verifyDelta(options); + assert.ok(res, 'Should pass'); + assert.equal(res.delta, 0, 'Should be in sync'); + + // countercheck for test vector at 59s with verify + res = speakeasy.totp.verify(options); + assert.ok(res, 'Should pass'); + + // countercheck for test vector at 1234567890 with delta + options.time = 1234567890; + options.token = '005924'; + res = speakeasy.totp.verifyDelta(options); + assert.ok(res, 'Should pass'); + assert.equal(res.delta, 0, 'Should be in sync'); + + // countercheck for test vector at 1234567890 with verify + res = speakeasy.totp.verify(options); + assert.ok(res, 'Should pass'); + + // countercheck for test vector at 1111111109 with delta + options.time = 1111111109; + options.token = '081804'; + res = speakeasy.totp.verifyDelta(options); + assert.ok(res, 'Should pass'); + assert.equal(res.delta, 0, 'Should be in sync'); + + // countercheck for test vector at 1111111109 with verify + res = speakeasy.totp.verify(options); + assert.ok(res, 'Should pass'); + + // countercheck for test vector at 2000000000 with delta + options.time = 2000000000; + options.token = '279037'; + res = speakeasy.totp.verifyDelta(options); + assert.ok(res, 'Should pass'); + assert.equal(res.delta, 0, 'Should be in sync'); + + // countercheck for test vector at 2000000000 with verify + res = speakeasy.totp.verify(options); + assert.ok(res, 'Should pass'); + + // countercheck for test vector at 1234567890 with custom counter with delta + options.token = '005924'; + options.counter = 41152263; + res = speakeasy.totp.verifyDelta(options); + assert.ok(res, 'Should pass'); + assert.equal(res.delta, 0, 'Should be in sync'); + + // countercheck for test vector at 1234567890 with verify + res = speakeasy.totp.verify(options); + assert.ok(res, 'Should pass'); +}); + +/* + * countercheck for codes that are out of sync + * window are going to use a value of counter = 1 and test against + * a code for counter = 9 + */ + +it('HOTPOutOfSync', function () { + /* + * for secret 12345678901234567890: + * 755224 = counter 0 + * 287082 = counter 1 + * 520489 = counter 8 + */ + + var options = { + secret: '12345678901234567890', + token: '520489', + counter: 1 + }; + + // countercheck that the test should fail for window < 8 + options.window = 7; + assert.ok(!speakeasy.hotp.verify(options), 'Should not pass for value of window < 8'); + + // countercheck that the test should pass for window >= 9 + options.window = 8; + assert.ok(speakeasy.hotp.verify(options), 'Should pass for value of window >= 9'); + + // countercheck that test should not pass for tokens behind the current counter + // 755224 is counter 0, and unlike notp (which has a two-sided window), + // Speakeasy will only allow a one-sided window, so counter = 7 and window = 8 + // will not look at counter 0. + options.token = '755224'; + options.counter = 7; + options.window = 8; + assert.notOk(speakeasy.hotp.verify(options), 'Should not pass for tokens behind the current counter'); +}); + +/* + * countercheck 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 = { + secret: '12345678901234567890', + token: '279037', + time: 1999999909 + }; + + // countercheck that the test should fail for window < 2 + options.window = 2; + assert.ok(!speakeasy.totp.verify(options), 'Should not pass for value of window < 3'); + + // countercheck that the test should pass for window >= 3 + options.window = 3; + assert.ok(speakeasy.totp.verify(options), 'Should pass for value of window >= 3'); +}); + +it('hotp_gen', function () { + var options = { + secret: '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); + + // countercheck for passes + for (var i = 0; i < HOTP.length; i++) { + options.counter = i; + assert.equal(speakeasy.hotp(options), HOTP[i], 'HOTP value should be correct'); + } +}); + +it('totp_gen', function () { + var options = { + secret: '12345678901234567890', + window: 0 + }; + + // make sure we can not pass in opt + speakeasy.totp(options); + + // countercheck for test vector at 59s + options.time = 59; + assert.equal(speakeasy.totp(options), '287082', 'TOTtoken values should match'); + + // countercheck for test vector at 1234567890 + options.time = 1234567890; + assert.equal(speakeasy.totp(options), '005924', 'TOTtoken values should match'); + + // countercheck for test vector at 1111111109 + options.time = 1111111109; + assert.equal(speakeasy.totp(options), '081804', 'TOTtoken values should match'); + + // countercheck for test vector at 2000000000 + options.time = 2000000000; + assert.equal(speakeasy.totp(options), '279037', 'TOTtoken values should match'); +}); diff --git a/test/rfc4226_test.js b/test/rfc4226_test.js new file mode 100644 index 0000000..6220de2 --- /dev/null +++ b/test/rfc4226_test.js @@ -0,0 +1,94 @@ +'use strict'; + +/* global describe, it */ + +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); + +/* + + The following test data uses the ASCII string + "12345678901234567890" for the secret: + + Secret = 0x3132333435363738393031323334353637383930 + + Table 1 details for each count, the intermediate HMAC value. + + Count Hexadecimal HMAC-SHA-1(secret, count) + 0 cc93cf18508d94934c64b65d8ba7667fb7cde4b0 + 1 75a48a19d4cbe100644e8ac1397eea747a2d33ab + 2 0bacb7fa082fef30782211938bc1c5e70416ff44 + 3 66c28227d03a2d5529262ff016a1e6ef76557ece + 4 a904c900a64b35909874b33e61c5938a8e15ed1c + 5 a37e783d7b7233c083d4f62926c7a25f238d0316 + 6 bc9cd28561042c83f219324d3c607256c03272ae + 7 a4fb960c0bc06e1eabb804e5b397cdc4b45596fa + 8 1b3c89f65e6c9e883012052823443f048b4332db + 9 1637409809a679dc698207310c8c7fc07290d9e5 + + Table 2 details for each count the truncated values (both in + hexadecimal and decimal) and then the HOTP value. + + Truncated + Count Hexadecimal Decimal HOTP + 0 4c93cf18 1284755224 755224 + 1 41397eea 1094287082 287082 + 2 82fef30 137359152 359152 + 3 66ef7655 1726969429 969429 + 4 61c5938a 1640338314 338314 + 5 33c083d4 868254676 254676 + 6 7256c032 1918287922 287922 + 7 4e5b397 82162583 162583 + 8 2823443f 673399871 399871 + 9 2679dc69 645520489 520489 + +*/ + +describe('RFC 4226 test values', function () { + describe('intermediate HMAC values', function () { + [ + 'cc93cf18508d94934c64b65d8ba7667fb7cde4b0', + '75a48a19d4cbe100644e8ac1397eea747a2d33ab', + '0bacb7fa082fef30782211938bc1c5e70416ff44', + '66c28227d03a2d5529262ff016a1e6ef76557ece', + 'a904c900a64b35909874b33e61c5938a8e15ed1c', + 'a37e783d7b7233c083d4f62926c7a25f238d0316', + 'bc9cd28561042c83f219324d3c607256c03272ae', + 'a4fb960c0bc06e1eabb804e5b397cdc4b45596fa', + '1b3c89f65e6c9e883012052823443f048b4332db', + '1637409809a679dc698207310c8c7fc07290d9e5' + ].forEach(function (expect, count) { + it('should match for counter = ' + count, function () { + var hash = speakeasy.digest({ + secret: '12345678901234567890', + counter: count + }).toString('hex'); + assert.equal(hash, expect); + }); + }); + }); + + describe('HOTP values', function () { + [ + '755224', + '287082', + '359152', + '969429', + '338314', + '254676', + '287922', + '162583', + '399871', + '520489' + ].forEach(function (expect, count) { + it('should match for count = ' + count, function () { + var code = speakeasy.hotp({ + secret: '12345678901234567890', + counter: count + }); + assert.equal(code, expect); + }); + }); + }); +}); diff --git a/test/rfc6238_test.js b/test/rfc6238_test.js new file mode 100644 index 0000000..0905f82 --- /dev/null +++ b/test/rfc6238_test.js @@ -0,0 +1,221 @@ +'use strict'; + +/* global describe, it */ + +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); + +/* + + This section provides test values that can be used for the HOTP time- + based variant algorithm interoperability test. + + The test token shared secret uses the ASCII string value + "12345678901234567890". With Time Step X = 30, and the Unix epoch as + the initial value to count time steps, where T0 = 0, the TOTP + algorithm will display the following values for specified modes and + timestamps. + + +-------------+--------------+------------------+----------+--------+ + | Time (sec) | UTC Time | Value of T (hex) | TOTP | Mode | + +-------------+--------------+------------------+----------+--------+ + | 59 | 1970-01-01 | 0000000000000001 | 94287082 | SHA1 | + | | 00:00:59 | | | | + | 59 | 1970-01-01 | 0000000000000001 | 46119246 | SHA256 | + | | 00:00:59 | | | | + | 59 | 1970-01-01 | 0000000000000001 | 90693936 | SHA512 | + | | 00:00:59 | | | | + | 1111111109 | 2005-03-18 | 00000000023523EC | 07081804 | SHA1 | + | | 01:58:29 | | | | + | 1111111109 | 2005-03-18 | 00000000023523EC | 68084774 | SHA256 | + | | 01:58:29 | | | | + | 1111111109 | 2005-03-18 | 00000000023523EC | 25091201 | SHA512 | + | | 01:58:29 | | | | + | 1111111111 | 2005-03-18 | 00000000023523ED | 14050471 | SHA1 | + | | 01:58:31 | | | | + | 1111111111 | 2005-03-18 | 00000000023523ED | 67062674 | SHA256 | + | | 01:58:31 | | | | + | 1111111111 | 2005-03-18 | 00000000023523ED | 99943326 | SHA512 | + | | 01:58:31 | | | | + | 1234567890 | 2009-02-13 | 000000000273EF07 | 89005924 | SHA1 | + | | 23:31:30 | | | | + | 1234567890 | 2009-02-13 | 000000000273EF07 | 91819424 | SHA256 | + | | 23:31:30 | | | | + | 1234567890 | 2009-02-13 | 000000000273EF07 | 93441116 | SHA512 | + | | 23:31:30 | | | | + | 2000000000 | 2033-05-18 | 0000000003F940AA | 69279037 | SHA1 | + | | 03:33:20 | | | | + | 2000000000 | 2033-05-18 | 0000000003F940AA | 90698825 | SHA256 | + | | 03:33:20 | | | | + | 2000000000 | 2033-05-18 | 0000000003F940AA | 38618901 | SHA512 | + | | 03:33:20 | | | | + | 20000000000 | 2603-10-11 | 0000000027BC86AA | 65353130 | SHA1 | + | | 11:33:20 | | | | + | 20000000000 | 2603-10-11 | 0000000027BC86AA | 77737706 | SHA256 | + | | 11:33:20 | | | | + | 20000000000 | 2603-10-11 | 0000000027BC86AA | 47863826 | SHA512 | + | | 11:33:20 | | | | + +-------------+--------------+------------------+----------+--------+ + + Table 1: TOTP Table + +*/ + +describe('RFC 6238 test vector', function () { + [{ + time: 59, + date: new Date('1970-01-01T00:00:59Z'), + counter: 0x01, + code: '94287082', + algorithm: 'SHA1' + }, { + time: 59, + date: new Date('1970-01-01T00:00:59Z'), + counter: 0x01, + code: '46119246', + algorithm: 'SHA256' + }, { + time: 59, + date: new Date('1970-01-01T00:00:59Z'), + counter: 0x01, + code: '90693936', + algorithm: 'SHA512' + }, { + time: 1111111109, + date: new Date('2005-03-18T01:58:29Z'), + counter: 0x023523EC, + code: '07081804', + algorithm: 'SHA1' + }, { + time: 1111111109, + date: new Date('2005-03-18T01:58:29Z'), + counter: 0x023523EC, + code: '68084774', + algorithm: 'SHA256' + }, { + time: 1111111109, + date: new Date('2005-03-18T01:58:29Z'), + counter: 0x023523EC, + code: '25091201', + algorithm: 'SHA512' + }, { + time: 1111111111, + date: new Date('2005-03-18T01:58:31Z'), + counter: 0x023523ED, + code: '14050471', + algorithm: 'SHA1' + }, { + time: 1111111111, + date: new Date('2005-03-18T01:58:31Z'), + counter: 0x023523ED, + code: '67062674', + algorithm: 'SHA256' + }, { + time: 1111111111, + date: new Date('2005-03-18T01:58:31Z'), + counter: 0x023523ED, + code: '99943326', + algorithm: 'SHA512' + }, { + time: 1234567890, + date: new Date('2009-02-13T23:31:30Z'), + counter: 0x0273EF07, + code: '89005924', + algorithm: 'SHA1' + }, { + time: 1234567890, + date: new Date('2009-02-13T23:31:30Z'), + counter: 0x0273EF07, + code: '91819424', + algorithm: 'SHA256' + }, { + time: 1234567890, + date: new Date('2009-02-13T23:31:30Z'), + counter: 0x0273EF07, + code: '93441116', + algorithm: 'SHA512' + }, { + time: 2000000000, + date: new Date('2033-05-18T03:33:20Z'), + counter: 0x03F940AA, + code: '69279037', + algorithm: 'SHA1' + }, { + time: 2000000000, + date: new Date('2033-05-18T03:33:20Z'), + counter: 0x03F940AA, + code: '90698825', + algorithm: 'SHA256' + }, { + time: 2000000000, + date: new Date('2033-05-18T03:33:20Z'), + counter: 0x03F940AA, + code: '38618901', + algorithm: 'SHA512' + }, { + time: 20000000000, + date: new Date('2603-10-11T11:33:20Z'), + counter: 0x27BC86AA, + code: '65353130', + algorithm: 'SHA1' + }, { + time: 20000000000, + date: new Date('2603-10-11T11:33:20Z'), + counter: 0x27BC86AA, + code: '77737706', + algorithm: 'SHA256' + }, { + time: 20000000000, + date: new Date('2603-10-11T11:33:20Z'), + counter: 0x27BC86AA, + code: '47863826', + algorithm: 'SHA512' + }].forEach(function (subject) { + var key = new Buffer('12345678901234567890'); + var nbytes, i; + + // set hash size based on algorithm + switch (subject.algorithm) { + case 'SHA256': nbytes = 32; + break; + case 'SHA512': nbytes = 64; + break; + default: nbytes = 20; + } + + // repeat the key to the minimum length + if (key.length > nbytes) { + key = key.slice(0, nbytes); + } else { + i = ~~(nbytes / key.length); + key = [key]; + while (i--) key.push(key[0]); + key = Buffer.concat(key).slice(0, nbytes); + } + + it('should calculate counter value for time ' + subject.time, function () { + var counter = speakeasy._counter({ + time: subject.time + }); + assert.equal(counter, subject.counter); + }); + + it('should calculate counter value for date ' + subject.date, function () { + var counter = speakeasy._counter({ + time: Math.floor(subject.date / 1000) + }); + assert.equal(counter, subject.counter); + }); + + it('should generate TOTP code for time ' + subject.time + ' and algorithm ' + subject.algorithm, function () { + var counter = speakeasy.totp({ + secret: key, + time: subject.time, + algorithm: subject.algorithm, + digits: 8 + }); + assert.equal(counter, subject.code); + }); + }); +}); diff --git a/test/test_hotp.js b/test/test_hotp.js deleted file mode 100644 index 1f3a379..0000000 --- a/test/test_hotp.js +++ /dev/null @@ -1,70 +0,0 @@ -var vows = require('vows'), - assert = require('assert'); - -var speakeasy = require('../lib/speakeasy'); - -// 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}); - }, - - 'correct one-time password returned': function(topic) { - 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) { - 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) { - 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) { - 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) { - assert.equal(topic, '338314'); - } - }, - - 'Test 0-padding encoding with key = \'h/,Iv]ET34!].kfNUU^Nf!I#gp1bNT1C\' at counter 3 and length = 8': { - topic: function() { - return speakeasy.hotp({key: 'h/,Iv]ET34!].kfNUU^Nf!I#gp1bNT1C', length: 8, counter: 3}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '05314231'); - } - }, - -}).exportTo(module); diff --git a/test/test_totp.js b/test/test_totp.js deleted file mode 100644 index ae0e53a..0000000 --- a/test/test_totp.js +++ /dev/null @@ -1,80 +0,0 @@ -var vows = require('vows'), - assert = require('assert'); - -var speakeasy = require('../lib/speakeasy'); - -// 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. - -vows.describe('TOTP Time-Based Algorithm Test').addBatch({ - 'Test normal operation with key = \'12345678901234567890\' at time = 59': { - topic: function() { - return speakeasy.totp({key: '12345678901234567890', time: 59}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '287082'); - } - }, - - 'Test a different time normal operation with key = \'12345678901234567890\' at time = 1111111109': { - topic: function() { - return speakeasy.totp({key: '12345678901234567890', time: 1111111109}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '081804'); - } - }, - - 'Test length parameter with key = \'12345678901234567890\' at time = 1111111109 and length = 8': { - topic: function() { - return speakeasy.totp({key: '12345678901234567890', time: 1111111109, length: 8}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '07081804'); - } - }, - - 'Test hexadecimal encoding with key = \'3132333435363738393031323334353637383930\' as hexadecimal at time 1111111109': { - topic: function() { - return speakeasy.totp({key: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '081804'); - } - }, - - 'Test base32 encoding with key = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at time 1111111109': { - topic: function() { - return speakeasy.totp({key: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '081804'); - } - }, - - 'Test a custom step with key = \'12345678901234567890\' at time = 1111111109 with step = 60': { - topic: function() { - return speakeasy.totp({key: '12345678901234567890', time: 1111111109, step: 60}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '360094'); - } - }, - - 'Test initial time with key = \'12345678901234567890\' at time = 1111111109 and initial time = 1111111100': { - topic: function() { - return speakeasy.totp({key: '12345678901234567890', time: 1111111109, initial_time: 1111111100}); - }, - - 'correct one-time password returned': function(topic) { - assert.equal(topic, '755224'); - } - }, -}).exportTo(module); diff --git a/test/totp_test.js b/test/totp_test.js new file mode 100644 index 0000000..9e4a2c0 --- /dev/null +++ b/test/totp_test.js @@ -0,0 +1,176 @@ +'use strict'; + +/* global describe, it */ + +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); + +// These tests use the test vectors from RFC 6238's Appendix B: Test Vectors +// 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 = 59", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 59}); + assert.equal(topic, '287082'); + }); + }); + + describe("normal operation with secret = '12345678901234567890' at time = 59 using key (deprecated)", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({key: '12345678901234567890', time: 59}); + assert.equal(topic, '287082'); + }); + }); + + describe("a different time normal operation with secret = '12345678901234567890' at time = 1111111109", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109}); + assert.equal(topic, '081804'); + }); + }); + + describe("digits parameter with secret = '12345678901234567890' at time = 1111111109 and digits = 8", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109, digits: 8}); + assert.equal(topic, '07081804'); + }); + }); + + describe("hexadecimal encoding with secret = '3132333435363738393031323334353637383930' as hexadecimal at time 1111111109", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109}); + assert.equal(topic, '081804'); + }); + }); + + describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ' as base32 at time 1111111109", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109}); + assert.equal(topic, '081804'); + }); + }); + + describe("a custom step with secret = '12345678901234567890' at time = 1111111109 with step = 60", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109, step: 60}); + assert.equal(topic, '360094'); + }); + }); + + describe("initial time with secret = '12345678901234567890' at time = 1111111109 and epoch = 1111111100", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109, epoch: 1111111100}); + assert.equal(topic, '755224'); + }); + }); + + // Backwards compatibility - deprecated + describe("initial time with secret = '12345678901234567890' at time = 1111111109 and initial_time = 1111111100", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109, initial_time: 1111111100}); + assert.equal(topic, '755224'); + }); + }); + + describe("base32 encoding with secret = '1234567890' at time = 1111111109", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109}); + assert.equal(topic, '081804'); + }); + }); + + describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA' as base32 at time = 1111111109, digits = 8 and algorithm as 'sha256'", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', time: 1111111109, digits: 8, algorithm: 'sha256'}); + assert.equal(topic, '68084774'); + }); + }); + + describe("base32 encoding with secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA' as base32 at time = 1111111109, digits = 8 and algorithm as 'sha512'", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', time: 1111111109, digits: 8, algorithm: 'sha512'}); + assert.equal(topic, '25091201'); + }); + }); + + describe("normal operation with secret = '12345678901234567890' with overridden counter 3", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', counter: 3}); + assert.equal(topic, '969429'); + }); + }); + + describe("normal operation with secret = '12345678901234567890' with overridden counter 3", function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', counter: 3}); + assert.equal(topic, '969429'); + }); + }); + + describe('totp.verifyDelta() window tests', function () { + var secret = 'rNONHRni6BAk7y2TiKrv'; + it('should get current TOTP value', function () { + this.token = speakeasy.totp({secret: secret, counter: 1}); + assert.equal(this.token, '314097'); + }); + + it('should get TOTP value at counter 3', function () { + this.token = speakeasy.totp({secret: secret, counter: 3}); + assert.equal(this.token, '663640'); + }); + + it('should get delta with varying window lengths', function () { + var delta; + + delta = speakeasy.totp.verifyDelta({ + secret: secret, token: '314097', counter: 1, window: 0 + }); + assert.isObject(delta); assert.strictEqual(delta.delta, 0); + + delta = speakeasy.totp.verifyDelta({ + secret: secret, token: '314097', counter: 1, window: 2 + }); + assert.isObject(delta); assert.strictEqual(delta.delta, 0); + + delta = speakeasy.totp.verifyDelta({ + secret: secret, token: '314097', counter: 1, window: 3 + }); + assert.isObject(delta); assert.strictEqual(delta.delta, 0); + }); + + it('should get delta when the item is not at specified counter but within window', function () { + // Use token at counter 3, initial counter 1, and a window of 2 + var delta = speakeasy.totp.verifyDelta({ + secret: secret, token: '663640', counter: 1, window: 2 + }); + assert.isObject(delta); assert.strictEqual(delta.delta, 2); + }); + + it('should not get delta when the item is not at specified counter and not within window', function () { + // Use token at counter 3, initial counter 1, and a window of 1 + var delta = speakeasy.totp.verifyDelta({ + secret: secret, token: '663640', counter: 1, window: 1 + }); + assert.isUndefined(delta); + }); + + it('should support negative delta values when token is on the negative side of the window', function () { + // Use token at counter 1, initial counter 3, and a window of 2 + var delta = speakeasy.totp.verifyDelta({ + secret: secret, token: '314097', counter: 3, window: 2 + }); + assert.isObject(delta); assert.strictEqual(delta.delta, -2); + }); + + it('should support negative delta values when token is on the negative side of the window using time input', function () { + // Use token at counter 1, initial counter 3, and a window of 2 + var delta = speakeasy.totp.verifyDelta({ + secret: secret, token: '625175', time: 1453854005, window: 2 + }); + assert.isObject(delta); assert.strictEqual(delta.delta, -2); + }); + }); +}); diff --git a/test/url_test.js b/test/url_test.js new file mode 100644 index 0000000..480a1e8 --- /dev/null +++ b/test/url_test.js @@ -0,0 +1,191 @@ +'use strict'; + +/* global describe, it */ + +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); +var url = require('url'); + +describe('#url', function () { + it('should require options', function () { + assert.throws(function () { + speakeasy.otpauthURL(); + }); + }); + + it('should validate type', function () { + assert.throws(function () { + speakeasy.otpauthURL({ + type: 'haha', + secret: 'hello', + label: 'that' + }, /invalid type `haha`/); + }); + }); + + it('should require secret', function () { + assert.throws(function () { + speakeasy.otpauthURL({ + label: 'that' + }, /missing secret/); + }); + }); + + it('should require label', function () { + assert.throws(function () { + speakeasy.otpauthURL({ + secret: 'hello' + }, /missing label/); + }); + }); + + it('should require counter for HOTP', function () { + assert.throws(function () { + speakeasy.otpauthURL({ + type: 'hotp', + secret: 'hello', + label: 'that' + }, /missing counter/); + }); + assert.ok(speakeasy.otpauthURL({ + type: 'hotp', + secret: 'hello', + label: 'that', + counter: 0 + })); + assert.ok(speakeasy.otpauthURL({ + type: 'hotp', + secret: 'hello', + label: 'that', + counter: 199 + })); + }); + + it('should validate algorithm', function () { + assert.doesNotThrow(function () { + speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + algorithm: 'hello' + }, /invalid algorithm `hello`/); + }); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + algorithm: 'sha1' + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + algorithm: 'sha256' + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + algorithm: 'sha512' + })); + }); + + it('should validate digits', function () { + assert.throws(function () { + speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + digits: 'hello' + }, /invalid digits `hello`/); + }); + // Non-6 and non-8 digits should not throw, but should have a warn message + assert.doesNotThrow(function () { + speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + digits: 12 + }, /invalid digits `12`/); + }); + assert.doesNotThrow(function () { + speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + digits: '7' + }, /invalid digits `7`/); + }); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + digits: 6 + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + digits: 8 + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + digits: '6' + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + digits: '8' + })); + }); + + it('should validate period', function () { + assert.throws(function () { + speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + period: 'hello' + }, /invalid period `hello`/); + }); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + period: 60 + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + period: 121 + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + period: '60' + })); + assert.ok(speakeasy.otpauthURL({ + secret: 'hello', + label: 'that', + period: '121' + })); + }); + + it('should generate an URL compatible with the Google Authenticator app', function () { + var answer = speakeasy.otpauthURL({ + 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) + ); + }); + + it('should generate an URL compatible with the Google Authenticator app and convert an ASCII-encoded string', function () { + var answer = speakeasy.otpauthURL({ + secret: 'MKiNHTvmfQ', + label: 'Example:alice@google.com', + issuer: 'Example' + }); + var expect = 'otpauth://totp/Example:alice@google.com?secret=JVFWSTSIKR3G2ZSR&issuer=Example'; + assert.deepEqual( + url.parse(answer), + url.parse(expect) + ); + }); +});