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/History.md b/History.md index 6abdc5b..e6742be 100644 --- a/History.md +++ b/History.md @@ -1,30 +1,16 @@ -Speakeasy +1.0.2 / 2015-07-13 +================== -Version History + * [Fixed] Don't repeat the secret key generating the digest. -1.0.3 -===== -Convenience release. Sabaidee from Luang Prabang, Laos. +1.0.1 / 2015-07-13 +================== -- Add vows to devDependencies and support `npm test` in package.json. Thanks, freewill! + * [Fixed] Ignore case on algorithm option. -1.0.2 -===== -Bugfix release. +1.0.0 / 2015-07-12 +================== -- Remove global leaks. Thanks for the fix, mashihua. - -1.0.1 -===== - -Bugfix release. Ciao from Florence, Italy. - -- Fixes issue where Google Chart API was being called at a deprecated URL. Thanks for the fix, sakkaku. -- Fixes issue where `generate_key`'s `symbols` option was not working, and was also causing pollution with global var. Thanks for reporting the bug, ARAtlas. - -1.0.0 -===== - -Initial release. + * Initial release based on speakeasy and notp. 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..7d33f28 100644 --- a/README.md +++ b/README.md @@ -1,160 +1,466 @@ -# 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) · [Usage](#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, suitable for use in two-factor +authentication, that supports Google Authenticator and other two-factor apps. -This module was written to follow the RFC memos on HOTP and TOTP: +It includes robust support for custom token lengths, authentication windows, +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's key generator allows you to generate keys, and get them back in their ASCII, hexadecimal, and base32 representations. In addition, it also can automatically generate QR codes for you. - -A useful integration is that it fully supports the popular Google Authenticator app, the virtual multi-factor authentication app available for iPhone and iOS, Android, and BlackBerry. This module's key generator can also generate a link to the specialized QR code you can use to scan in the Google Authenticator mobile app. - -An overarching goal of this module, other than to make it very easy to implement the HOTP and TOTP algorithms, is to be extensively documented. Indeed, it is well-documented, with clear functions and parameter explanations. +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. + ## Install -``` -npm install speakeasy +```sh +npm install --save speakeasy ``` -## Example (with Google Authenticator) + +## Demo -```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& -![](http://i.imgur.com/INZnk.png) + +## Usage +```js +var speakeasy = require("speakeasy"); ``` -// 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 -``` - -## Manual - -### speakeasy.hotp(options) | speakeasy.counter(options) -Calculate the one-time password using the counter-based algorithm, HOTP. Specify the key and counter, and receive the one-time password for that counter position. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. +#### Generating a key -Written to follow [RFC 4226](http://tools.ietf.org/html/rfc4226). Calculated with: `HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` - -#### Options - -* `key`: the secret key in ASCII, hexadecimal, or base32 format. `K` in the algorithm. -* `counter`: the counter position (moving factor). `C` in the algorithm. -* `length` (default `6`): the length of the resulting one-time password. -* `encoding` (default `ascii`): the encoding of the `key`. Can be `'ascii'`, `'hex'`, or `'base32'`. The key will automatically be converted to ASCII. - -#### Example - -```javascript -// normal use. -speakeasy.hotp({key: 'secret', counter: 582}); -// => 246642 - -// use a custom length. -speakeasy.hotp({key: 'secret', counter: 582, length: 8}); -// => 67246642 - -// use a custom encoding. -speakeasy.hotp({key: 'AJFIEJGEHIFIU7148SF', counter: 147, encoding: 'base32'}); -// => 974955 +```js +// Generate a secret key. +var secret = speakeasy.generate_key({length: 20}); +// Access using secret.ascii, secret.hex, or secret.base32. ``` -### speakeasy.totp(options) | speakeasy.time(options) - -Calculate the one-time password using the time-based algorithm, TOTP. Specify the key, and receive the one-time password for that time. By default, the time step is 30 seconds, so there is a new password every 30 seconds. However, you may override the time step. You may also override the time you want to calculate the time from. You can also specify a password length, as well as the encoding (ASCII, hexadecimal, or base32) for convenience. Returns the one-time password as a string. - -Written to follow [RFC 6238](http://tools.ietf.org/html/rfc6238). Calculated with: `C = ((T - T0) / X); HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))` +#### Getting a time-based token for the current time -#### Options +```js +// Generate a time-based token based on the base-32 key. +// HOTP (counter-based tokens) can also be used if `totp` is replaced by +// `hotp` (i.e. speakeasy.hotp()) and a `counter` is given in the options. +var token = speakeasy.totp({ + secret: base32secret +}); -* `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. - -#### Example - -```javascript -// normal use. -speakeasy.totp({key: 'secret'}); - -// use a custom time step. -speakeasy.totp({key: 'secret', step: 60}); - -// use a custom time. -speakeasy.totp({key: 'secret', time: 159183717}); -// => 558014 - -// use a initial time. -speakeasy.totp({key: 'secret', initial_time: 4182881485}); -// => 670417 +// Returns token for the secret at the current time +// Compare this to user input ``` -#### Techniques - -You can implement a double-authentication scheme, where you ask the user to input the one-time password once, wait until the next 30-second refresh, and then input the one-time password again. In this case, you can calculate the second (later) input by calculating TOTP as usual, then also verify the first (earlier) input by taking the current epoch time in seconds and subtracting 30 seconds to get to the previous step (for example: `time1 = (parseInt(new Date()/1000) - 30)`) - -### speakeasy.generate_key(options) - -Generate a random secret key. It will return the key in ASCII, hexadecimal, and base32 formats. You can specify the length, whether or not to use symbols, and ask it (nicely) to generate URLs for QR codes. Returns an object with the ASCII, hex, and base32 representations of the secret key, plus any QR codes you can optionally ask for. +#### Verifying a token -#### Options - -* `length` (default `32`): the length of the generated secret key. -* `symbols` (default `true`): include symbols in the key? if not, the key will be alphanumeric, {A-Z, a-z, 0-9} -* `qr_codes` (default `false`): generate links to QR codes for each encoding (ASCII, hexadecimal, and base32). It uses the Google Charts API and they are served over HTTPS. A future version might allow for QR code generation client-side for security. -* `google_auth_qr` (default `false`): generate a link to a QR code that you can scan using the Google Authenticator app. The contents of the QR code are in this format: `otpauth://totp/[KEY NAME]?secret=[KEY SECRET, BASE 32]`. -* `name` (optional): specify a name when you are using `google_auth_qr`, which will show up as the label after scanning. `[KEY NAME]` in the previous line. - -#### Examples - -```javascript -// generate a key -speakeasy.generate_key({length: 20, symbols: true}); -// => { ascii: 'km^A?n&sOPJW.iCKPHKU', hex: '6b6d5e413f6e26734f504a572e69434b50484b55', base32: 'NNWV4QJ7NYTHGT2QJJLS42KDJNIEQS2V' } - -// generate a key and request QR code links -speakeasy.generate_key({length: 20, qr_codes: true}); -// => { ascii: 'eV:JQ1NedJkKn&]6^i>s', ... (truncated) -// qr_code_ascii: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=eV%3AJQ1NedJkKn%26%5D6%5Ei%3Es', -// qr_code_hex: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=65563a4a51314e65644a6b4b6e265d365e693e73', -// qr_code_base32: 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=MVLDUSSRGFHGKZCKNNFW4JS5GZPGSPTT' } - -// generate a key and get a QR code you can scan with the Google Authenticator app -speakeasy.generate_key({length: 20, google_auth_qr: true}); -// => { ascii: 'V?9f6.Cq1& +## 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. +By default, it verifies the token at the given counter value, with no leeway +(no look-ahead or look-behind). A token validated at the current counter value +will have a delta of 0.

+

You can specify a window to add more leeway to the verification process. +verifyDelta() will then return the delta between the given token and the +given counter value.

+
+
hotp․verify(options)Boolean
+

Verify a time-based one-time token against the secret and return true if it +verifies. Helper function for verifyDelta() that returns a boolean instead of +an object.

+
+
totp(options)String
+

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

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

+
+
totp․verify(options)Boolean
+

Verify a time-based one-time token against the secret and return true if it +verifies. Helper function for verifyDelta() that returns a boolean instead of +an object.

+
+
generate_key(options)Object | GeneratedSecret
+

Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length +(default 32). Returns the secret key in ASCII, hexadecimal, and base32 format, +along with the URL used for the QR code for Google Authenticator (an otpauth +URL).

+

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

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

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

+
+
google_auth_url(options)String
+

Generate an URL for use with the Google Authenticator app.

+

Authenticator considers TOTP codes valid for 30 seconds. Additionally, +the app presents 6 digits codes to the user. According to the +documentation, the period and number of digits are currently ignored by +the app.

+

To generate a suitable QR Code, pass the generated URL to a QR Code +generator, such as the qr-image module.

+
+
+ +### Typedefs + +
+
GeneratedSecret : Object
+
+
+ + +### digest(options) ⇒ Buffer +Digest the one-time passcode options. + +**Kind**: global function +**Returns**: Buffer - The one-time passcode as a buffer. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.counter | Integer | | Counter value | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| options.key | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | + + +### hotp(options) ⇒ String +Generate a counter-based one-time token. + +**Kind**: global function +**Returns**: String - The one-time passcode. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.counter | Integer | | Counter value | +| [options.digest] | Buffer | | Digest, automatically generated by default | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| options.key | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | +| [options.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. | + + +### hotp․verifyDelta(options) ⇒ Object +Verify a counter-based one-time token against the secret and return the delta. +By default, it verifies the token at the given counter value, with no leeway +(no look-ahead or look-behind). A token validated at the current counter value +will have a delta of 0. + +You can specify a window to add more leeway to the verification process. +`verifyDelta()` will then return the delta between the given token and the +given counter value. + +**Kind**: global function +**Returns**: Object - On success, returns an object with the counter + difference between the client and the server as the `delta` property (i.e. + `{ delta: 0 }`). + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| options.counter | Integer | | Counter value. This should be stored by the application and must be incremented for each request. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future against the provided passcode, e.g. if W = 10, and C = 5, this function will check the passcode against all One Time Passcodes between 5 and 15, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### hotp․verify(options) ⇒ Boolean +Verify a time-based one-time token against the secret and return true if it +verifies. Helper function for verifyDelta() that returns a boolean instead of +an object. + +**Kind**: global function +**Returns**: Boolean - Returns true if the token matches within the given + window, false otherwise. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| options.counter | Integer | | Counter value. This should be stored by the application and must be incremented for each request. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future against the provided passcode, e.g. if W = 10, and C = 5, this function will check the passcode against all One Time Passcodes between 5 and 15, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### totp(options) ⇒ String +Generate a time-based one-time token. By default, it returns the token for +the current time. + +**Kind**: global function +**Returns**: String - The one-time passcode. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| [options.time] | Integer | | Time with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| [options.key] | String | | (DEPRECATED. Use `secret` instead.) Shared secret key | +| [options.initial_time] | Integer | 0 | (DEPRECATED. Use `epoch` instead.) Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.length] | Integer | 6 | (DEPRECATED. Use `digits` instead.) The number of digits for the one-time passcode. | + + +### totp․verifyDelta(options) ⇒ Object +Verify a time-based one-time token against the secret and return the delta. +By default, it verifies the token at the current time window, with no leeway +(no look-ahead or look-behind). A token validated at the current time window +will have a delta of 0. + +You can specify a window to add more leeway to the verification process. +`verifyDelta()` will then return the delta between the given token and the +current time in time steps. + +**Kind**: global function +**Returns**: Object - On success, returns an object with the time step + difference between the client and the server as the `delta` property (e.g. + `{ delta: 0 }`). + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| [options.time] | Integer | | Time with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future and the past against the provided passcode, e.g. if W = 5, and C = 1000, this function will check the passcode against all One Time Passcodes between 995 and 1005, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### totp․verify(options) ⇒ Boolean +Verify a time-based one-time token against the secret and return true if it +verifies. Helper function for verifyDelta() that returns a boolean instead of +an object. + +**Kind**: global function +**Returns**: Boolean - Returns true if the token matches within the given + window, false otherwise. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.token | String | | Passcode to validate | +| [options.time] | Integer | | Time with which to calculate counter value. Defaults to `Date.now()`. | +| [options.step] | Integer | 30 | Time step in seconds | +| [options.epoch] | Integer | 0 | Initial time since the UNIX epoch from which to calculate the counter value. Defaults to 0 (no offset). | +| [options.counter] | Integer | | Counter value, calculated by default. | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. | +| [options.window] | Integer | 0 | The allowable margin for the counter. The function will check "W" codes in the future and the past against the provided passcode, e.g. if W = 5, and C = 1000, this function will check the passcode against all One Time Passcodes between 995 and 1005, inclusive. | +| [options.encoding] | String | "ascii" | Key encoding (ascii, hex, base32, base64). | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | + + +### generate_key(options) ⇒ Object | [GeneratedSecret](#GeneratedSecret) +Generates a random secret with the set A-Z a-z 0-9 and symbols, of any length +(default 32). Returns the secret key in ASCII, hexadecimal, and base32 format, +along with the URL used for the QR code for Google Authenticator (an otpauth +URL). + +Can also optionally return QR codes for the secret and for the Google +Authenticator URL. + +**Kind**: global function + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| [options.length] | Integer | 32 | Length of the secret | +| [options.symbols] | Boolean | false | Whether to include symbols | +| [options.qr_codes] | Boolean | false | Whether to output QR code URLs | +| [options.google_auth_qr] | Boolean | false | Whether to output a Google Authenticator otpauth:// QR code URL (returns the URL to the QR code) | +| [options.google_auth_url] | Boolean | true | Whether to output a Google Authenticator otpauth:// URL (only returns otpauth:// URL, no QR code) | +| [options.name] | String | | The name to use with Google Authenticator. | + + +### generate_key_ascii([length], [symbols]) ⇒ String +Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and +symbols (if requested). + +**Kind**: global function +**Returns**: String - The generated key. + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| [length] | Integer | 32 | The length of the key. | +| [symbols] | Boolean | false | Whether to include symbols in the key. | + + +### google_auth_url(options) ⇒ String +Generate an URL for use with the Google Authenticator app. + +Authenticator considers TOTP codes valid for 30 seconds. Additionally, +the app presents 6 digits codes to the user. According to the +documentation, the period and number of digits are currently ignored by +the app. + +To generate a suitable QR Code, pass the generated URL to a QR Code +generator, such as the `qr-image` module. + +**Kind**: global function +**Returns**: String - A URL suitable for use with the Google Authenticator. +**See**: https://github.com/google/google-authenticator/wiki/Key-Uri-Format + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| options | Object | | | +| options.secret | String | | Shared secret key | +| options.label | String | | Used to identify the account with which the secret key is associated, e.g. the user's email address. | +| [options.type] | String | "totp" | Either "hotp" or "totp". | +| [options.counter] | Integer | | The initial counter value, required for HOTP. | +| [options.issuer] | String | | The provider or service with which the secret key is associated. | +| [options.algorithm] | String | "sha1" | Hash algorithm (sha1, sha256, sha512). | +| [options.digits] | Integer | 6 | The number of digits for the one-time passcode. Currently ignored by Google Authenticator. | +| [options.period] | Integer | 30 | The length of time for which a TOTP code will be valid, in seconds. Currently ignored by Google Authenticator. | +| [options.encoding] | String | | Key encoding (ascii, hex, base32, base64). If the key is not encoded in Base-32, it will be reencoded. | + + +### GeneratedSecret : Object +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| ascii | String | ASCII representation of the secret | +| hex | String | Hex representation of the secret | +| base32 | String | Base32 representation of the secret | +| qr_code_ascii | String | URL for the QR code for the ASCII secret. | +| qr_code_hex | String | URL for the QR code for the hex secret. | +| qr_code_base32 | String | URL for the QR code for the base32 secret. | +| google_auth_qr | String | URL for the Google Authenticator otpauth URL's QR code. | +| google_auth_url | String | Google Authenticator 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]) + +## 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 \ No newline at end of file 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..a1bb161 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,566 @@ -// i has a cheezburger -module.exports = require('./lib/speakeasy'); +"use strict"; + +var base32 = require("base32.js"); +var crypto = require("crypto"); +var url = require("url"); + +/** + * 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 key = options.secret; + var counter = options.counter; + var encoding = options.encoding || "ascii"; + var algorithm = (options.algorithm || "sha1").toLowerCase(); + + // Backwards compatibility - deprecated + if (options.key) key = options.key; + + // convert key to buffer + if (!Buffer.isBuffer(key)) { + key = encoding == "base32" ? base32.decode(key) + : new Buffer(key, encoding); + } + + // create an buffer from the counter + var buf = new Buffer(8); + var tmp = counter; + for (i = 0; i < 8; i++) { + + // mask 0xff over number to get last 8 + buf[7 - i] = tmp & 0xff; + + // 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, key); + + // update hmac with the counter + hmac.update(buf); + + // return the digest + return hmac.digest(); +}; + +/** + * Generate a counter-based one-time token. + * + * @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; + + // 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); +}; + +/** + * Verify a counter-based one-time token against the secret and return the delta. + * By default, it verifies the token at the given counter value, with no leeway + * (no look-ahead or look-behind). A token validated at the current counter value + * will have a delta of 0. + * + * You can specify a window to add more leeway to the verification process. + * `verifyDelta()` will then return the delta between the given token and the + * given counter value. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @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 = options.token; + var window = parseInt(options.window || 0, 10); + var counter = parseInt(options.counter || 0, 10); + + // loop from C to C + W + for (i = counter; i <= counter + window; ++i) { + options.counter = i; + if (exports.hotp(options) == token) { + // found a matching code, return delta + return {delta: i - counter}; + } + } + + // no codes have matched +}; + +/** + * Verify a time-based one-time token against the secret and return true if it + * verifies. Helper function for verifyDelta() that returns a boolean instead of + * an object. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @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. + * + * @param {Object} options + * @param {Integer} [options.time] Time with which to calculate counter value. + * Defaults to `Date.now()`. + * @param {Integer} [options.step=30] Time step in seconds + * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from + * which to calculate the counter value. Defaults to 0 (no offset). + * @param {Integer} [options.initial_time=0] (DEPRECATED. Use `epoch` instead.) + * Initial time since the UNIX epoch from which to calculate the counter + * value. Defaults to 0 (no offset). + * @return {Integer} The calculated counter value + * @private + */ + +exports._counter = function _counter (options) { + var step = options.step || 30; + var time = options.time != null ? options.time : Date.now(); + + // also accepts 'initial_time', but deprecated + var epoch = (options.epoch != null ? options.epoch : options.initial_time) || 0; + + return Math.floor((time - epoch) / step / 1000); +}; + +/** + * Generate a time-based one-time token. By default, it returns the token for + * the current time. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {Integer} [options.time] Time with which to calculate counter value. + * Defaults to `Date.now()`. + * @param {Integer} [options.step=30] Time step in seconds + * @param {Integer} [options.epoch=0] Initial time since the UNIX epoch from + * which to calculate the counter value. Defaults to 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 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); +}; + +/** + * Verify a time-based one-time token against the secret and return the delta. + * By default, it verifies the token at the current time window, with no leeway + * (no look-ahead or look-behind). A token validated at the current time window + * will have a delta of 0. + * + * You can specify a window to add more leeway to the verification process. + * `verifyDelta()` will then return the delta between the given token and the + * current time in time steps. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} [options.time] Time with which to calculate counter value. + * Defaults to `Date.now()`. + * @param {Integer} [options.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.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 || 0, 10); + + // 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 + return exports.hotp.verifyDelta(options); +}; + +/** + * Verify a time-based one-time token against the secret and return true if it + * verifies. Helper function for verifyDelta() that returns a boolean instead of + * an object. + * + * @param {Object} options + * @param {String} options.secret Shared secret key + * @param {String} options.token Passcode to validate + * @param {Integer} [options.time] Time with which to calculate counter value. + * Defaults to `Date.now()`. + * @param {Integer} [options.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.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} google_auth_url Google Authenticator 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). + * + * Can also optionally return QR codes for the secret and for the Google + * Authenticator URL. + * + * @param {Object} options + * @param {Integer} [options.length=32] Length of the secret + * @param {Boolean} [options.symbols=false] Whether to include symbols + * @param {Boolean} [options.qr_codes=false] Whether to output QR code URLs + * @param {Boolean} [options.google_auth_qr=false] Whether to output a Google + * Authenticator otpauth:// QR code URL (returns the URL to the QR code) + * @param {Boolean} [options.google_auth_url=true] Whether to output a Google + * Authenticator otpauth:// URL (only returns otpauth:// URL, no QR code) + * @param {String} [options.name] The name to use with Google Authenticator. + * @return {Object} + * @return {GeneratedSecret} The generated secret key. + */ +exports.generate_key = function generateKey (options) { + // options + if(!options) options = {}; + var length = options.length || 32; + var name = options.name || "SecretKey"; + var qr_codes = options.qr_codes || false; + var google_auth_qr = options.google_auth_qr || false; + var google_auth_url = options.google_auth_url != null ? options.google_auth_url : true; + var 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 = Buffer(key, 'ascii').toString('hex'); + SecretKey.base32 = base32.encode(Buffer(key)).toString().replace(/=/g,''); + + // generate some qr codes if requested + if (qr_codes) { + SecretKey.qr_code_ascii = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.ascii); + SecretKey.qr_code_hex = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.hex); + SecretKey.qr_code_base32 = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(SecretKey.base32); + } + + if (google_auth_url) { + SecretKey.google_auth_url = exports.google_auth_url({ + secret: SecretKey.hex, + label: name + }); + } + + // generate a QR code for use in Google Authenticator if requested + // (Google Authenticator has a special style and requires base32) + if (google_auth_qr) { + // first, make sure that the name doesn't have spaces, since Google Authenticator doesn't like them + name = name.replace(/ /g,''); + SecretKey.google_auth_qr = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://totp/' + encodeURIComponent(name) + '%3Fsecret=' + encodeURIComponent(SecretKey.base32); + } + + return SecretKey; +}; + +/** + * Generates a key of a certain length (default 32) from A-Z, a-z, 0-9, and + * symbols (if requested). + * + * @param {Integer} [length=32] The length of the key. + * @param {Boolean} [symbols=false] Whether to include symbols in the key. + * @return {String} The generated key. + */ +exports.generate_key_ascii = function(length, symbols) { + var bytes = crypto.randomBytes(length || 32); + var set = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'; + if (symbols) { + set += '!@#$%^&*()<>?/[]{},.:;'; + } + + var output = ''; + for (var i = 0, l = bytes.length; i < l; i++) { + output += set[Math.floor(bytes[i] / 255.0 * (set.length-1))]; + } + return output; +}; + +/** + * Generate an URL for use with the Google Authenticator app. + * + * Authenticator considers TOTP codes valid for 30 seconds. Additionally, + * the app presents 6 digits codes to the user. According to the + * documentation, the period and number of digits are currently ignored by + * the app. + * + * To generate a suitable QR Code, pass the generated URL to a QR Code + * generator, such as the `qr-image` module. + * + * @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. + * @see https://github.com/google/google-authenticator/wiki/Key-Uri-Format + */ + +exports.google_auth_url = function (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; + + // validate type + switch (type) { + case "totp": + case "hotp": + break; + default: + throw new Error("invalid type `" + type + "`"); + } + + // validate required options + if (!secret) throw new Error("missing secret"); + if (!label) throw new Error("missing label"); + + // require counter for HOTP + if (type == "hotp" && counter == null) { + throw new Error("missing counter value for HOTP"); + } + + // build query while validating + var query = {secret: secret}; + if (options.issuer) query.issuer = options.issuer; + + // validate algorithm + if (algorithm != null) { + switch (algorithm.toUpperCase()) { + case "SHA1": + case "SHA256": + case "SHA512": + break; + default: + throw new Error("invalid algorithm `" + algorithm + "`"); + } + query.algorithm = algorithm.toUpperCase(); + } + + // validate digits + if (digits != null) { + switch (parseInt(digits, 10)) { + case 6: + case 8: + break; + default: + throw new Error("invalid digits `" + digits + "`"); + } + query.digits = digits; + } + + // validate period + if (period != null) { + if (~~period != period) { + throw new Error("invalid period `" + period + "`"); + } + query.period = period; + } + + // convert secret to base32 + if (encoding != "base32") secret = new Buffer(secret, encoding); + if (Buffer.isBuffer(secret)) secret = base32.encode(secret); + + // return url + return url.format({ + protocol: "otpauth", + slashes: true, + hostname: type, + pathname: label, + query: query + }); +}; diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..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..4067135 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,41 @@ { - "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. One-time passcode generator (HOTP/TOTP) with URL generation 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", + "keywords": [ + "authentication", + "google authenticator", + "hmac", + "hotp", + "multi-factor", + "one-time password", + "passwords", + "totp", + "two-factor" + ], + "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", @@ -26,9 +48,18 @@ "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..0ced736 --- /dev/null +++ b/test/generate.js @@ -0,0 +1,70 @@ +"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.generate_key(); + assert.equal(secret.ascii.length, 32, 'Should return the correct length'); + + // check returned fields + assert.isDefined(secret.google_auth_url, 'Google Auth URL should be returned') + assert.isUndefined(secret.qr_code_ascii, 'QR Code ASCII should not be returned') + assert.isUndefined(secret.qr_code_hex, 'QR Code Hex should not be returned') + assert.isUndefined(secret.qr_code_base32, 'QR Code Base 32 should not be returned') + assert.isUndefined(secret.google_auth_qr, 'Google Auth QR should not be returned') + + // check encodings + assert.equal(Buffer(secret.hex, 'hex').toString('ascii'), secret.ascii, 'Should have encoded correct hex string'); + assert.equal(base32.decode(secret.base32).toString('ascii'), secret.ascii, 'Should have encoded correct base32 string'); + }); + + it('Generation with custom key length', function () { + var secret = speakeasy.generate_key({length: 50}); + assert.equal(secret.ascii.length, 50, 'Should return the correct length'); + }); + + it('Generation with symbols disabled', function () { + var secret = speakeasy.generate_key({symbols: false}); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Generation with QR URL output enabled', function () { + var secret = speakeasy.generate_key({qr_codes: true}); + assert.isDefined(secret.qr_code_ascii, 'QR Code ASCII should be returned') + assert.isDefined(secret.qr_code_hex, 'QR Code Hex should be returned') + assert.isDefined(secret.qr_code_base32, 'QR Code Base 32 should be returned') + }); + + it('Generation with Google Auth URL output disabled', function () { + var secret = speakeasy.generate_key({google_auth_url: false}); + assert.isUndefined(secret.google_auth_url, 'Google Auth URL should not be returned') + }); + + it('Generation with Google Auth QR URL output enabled', function () { + var secret = speakeasy.generate_key({google_auth_qr: true}); + assert.isDefined(secret.google_auth_qr, 'Google Auth QR should be returned') + }); + + it('Testing generate_key_ascii with defaults', function () { + var secret = speakeasy.generate_key_ascii(); + assert.equal(secret.length, 32, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + + it('Testing generate_key_ascii with custom length', function () { + var secret = speakeasy.generate_key_ascii(20); + assert.equal(secret.length, 20, 'Should return the correct length'); + assert.ok(/^[a-z0-9]+$/i.test(secret.ascii), 'Should return an alphanumeric key'); + }); + +}); diff --git a/test/hotp_test.js b/test/hotp_test.js new file mode 100644 index 0000000..73eb4d0 --- /dev/null +++ b/test/hotp_test.js @@ -0,0 +1,70 @@ +"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'); + }); + }); + + 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..7da78ad --- /dev/null +++ b/test/notp_test.js @@ -0,0 +1,260 @@ +"use strict"; + +/* global describe, it */ + +var chai = require('chai'); +var assert = chai.assert; +var speakeasy = require('..'); + +/* + * Test HOTtoken. Uses test values from RFcounter 4226 + * + * + * The following test data uses the AScounterII string + * "12345678901234567890" for the secret: + * + * Secret = 0x3132333435363738393031323334353637383930 + * + * Table 1 details for each count, the intermediate HMAcounter value. + * + * counterount Hexadecimal HMAcounter-SHA-1(secret, count) + * 0 cc93cf18508d94934c64b65d8ba7667fb7cde4b0 + * 1 75a48a19d4cbe100644e8ac1397eea747a2d33ab + * 2 0bacb7fa082fef30782211938bc1c5e70416ff44 + * 3 66c28227d03a2d5529262ff016a1e6ef76557ece + * 4 a904c900a64b35909874b33e61c5938a8e15ed1c + * 5 a37e783d7b7233c083d4f62926c7a25f238d0316 + * 6 bc9cd28561042c83f219324d3c607256c03272ae + * 7 a4fb960c0bc06e1eabb804e5b397cdc4b45596fa + * 8 1b3c89f65e6c9e883012052823443f048b4332db + * 9 1637409809a679dc698207310c8c7fc07290d9e5 + * + * Table 2 details for each count the truncated values (both in + * hexadecimal and decimal) and then the HOTtoken value. + * + * Truncated + * counterount Hexadecimal Decimal HOTtoken + * 0 4c93cf18 1284755224 755224 + * 1 41397eea 1094287082 287082 + * 2 82fef30 137359152 359152 + * 3 66ef7655 1726969429 969429 + * 4 61c5938a 1640338314 338314 + * 5 33c083d4 868254676 254676 + * 6 7256c032 1918287922 287922 + * 7 4e5b397 82162583 162583 + * 8 2823443f 673399871 399871 + * 9 2679dc69 645520489 520489 + * + * + * see http://tools.ietf.org/html/rfc4226 + */ + +it("HOTP", function() { + var options = { + 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); + + // countercheck for failure + options.counter = 0; + assert.ok(!speakeasy.hotp.verify(options), 'Should not pass'); + + // countercheck for passes + for(var i=0;i= 9 + options.window = 8; + assert.ok(speakeasy.hotp.verify(options), 'Should pass for value of window >= 9'); + + // countercheck that test should pass for negative counter values + // token = '755224'; + options.counter = 7 + options.window = 8; + assert.ok(speakeasy.hotp.verify(options), 'Should pass for negative counter values'); +}); + + +/* + * 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: 1999999909000 + }; + + // 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 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: subject.date + }); + 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..3288898 --- /dev/null +++ b/test/totp_test.js @@ -0,0 +1,106 @@ +"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 = 59000', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 59000}); + assert.equal(topic, '287082'); + }); + }); + + describe('normal operation with secret = \'12345678901234567890\' at time = 59000 using key (deprecated)', function () { + it('should return correct one-time password', function() { + var topic = speakeasy.totp({key: '12345678901234567890', time: 59000}); + assert.equal(topic, '287082'); + }); + }); + + describe('a different time normal operation with secret = \'12345678901234567890\' at time = 1111111109000', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('digits parameter with secret = \'12345678901234567890\' at time = 1111111109000 and digits = 8', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, digits: 8}); + assert.equal(topic, '07081804'); + }); + }); + + describe('hexadecimal encoding with secret = \'3132333435363738393031323334353637383930\' as hexadecimal at time 1111111109', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '3132333435363738393031323334353637383930', encoding: 'hex', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ\' as base32 at time 1111111109', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', encoding: 'base32', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('a custom step with secret = \'12345678901234567890\' at time = 1111111109000 with step = 60', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, step: 60}); + assert.equal(topic, '360094'); + }); + }); + + describe('initial time with secret = \'12345678901234567890\' at time = 1111111109000 and epoch = 1111111100000', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000, epoch: 1111111100000}); + assert.equal(topic, '755224'); + }); + }); + + describe('base32 encoding with secret = \'1234567890\' at time = 1111111109000', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: '12345678901234567890', time: 1111111109000}); + assert.equal(topic, '081804'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha256\'', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA', encoding: 'base32', time: 1111111109000, digits: 8, algorithm: 'sha256'}); + assert.equal(topic, '68084774'); + }); + }); + + describe('base32 encoding with secret = \'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA\' as base32 at time = 1111111109000, digits = 8 and algorithm as \'sha512\'', function () { + it('should return correct one-time password', function () { + var topic = speakeasy.totp({secret: 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA', encoding: 'base32', time: 1111111109000, 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'); + }); + }); + +}); diff --git a/test/url_test.js b/test/url_test.js new file mode 100644 index 0000000..f1133ba --- /dev/null +++ b/test/url_test.js @@ -0,0 +1,179 @@ +"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.google_auth_url(); + }); + }); + + it("should validate type", function () { + assert.throws(function () { + speakeasy.google_auth_url({ + type: "haha", + secret: "hello", + label: "that", + }, /invalid type `haha`/); + }); + }); + + it("should require secret", function () { + assert.throws(function () { + speakeasy.google_auth_url({ + label: "that" + }, /missing secret/); + }); + }); + + it("should require label", function () { + assert.throws(function () { + speakeasy.google_auth_url({ + secret: "hello" + }, /missing label/); + }); + }); + + it("should require counter for HOTP", function () { + assert.throws(function () { + speakeasy.google_auth_url({ + type: "hotp", + secret: "hello", + label: "that" + }, /missing counter/); + }); + assert.ok(speakeasy.google_auth_url({ + type: "hotp", + secret: "hello", + label: "that", + counter: 0 + })); + assert.ok(speakeasy.google_auth_url({ + type: "hotp", + secret: "hello", + label: "that", + counter: 199 + })); + }); + + it("should validate algorithm", function () { + assert.throws(function () { + speakeasy.google_auth_url({ + secret: "hello", + label: "that", + algorithm: "hello" + }, /invalid algorithm `hello`/); + }); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + algorithm: "sha1" + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + algorithm: "sha256" + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + algorithm: "sha512" + })); + }); + + it("should validate digits", function () { + assert.throws(function () { + speakeasy.google_auth_url({ + secret: "hello", + label: "that", + digits: "hello" + }, /invalid digits `hello`/); + }); + assert.throws(function () { + speakeasy.google_auth_url({ + secret: "hello", + label: "that", + digits: 12 + }, /invalid digits `12`/); + }); + assert.throws(function () { + speakeasy.google_auth_url({ + secret: "hello", + label: "that", + digits: "7" + }, /invalid digits `7`/); + }); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + digits: 6 + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + digits: 8 + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + digits: "6" + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + digits: "8" + })); + }); + + it("should validate period", function () { + assert.throws(function () { + speakeasy.google_auth_url({ + secret: "hello", + label: "that", + period: "hello" + }, /invalid period `hello`/); + }); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + period: 60 + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + period: 121 + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + period: "60" + })); + assert.ok(speakeasy.google_auth_url({ + secret: "hello", + label: "that", + period: "121" + })); + }); + + it("should generate an URL compatible with the Google Authenticator app", function () { + var answer = speakeasy.google_auth_url({ + secret: "JBSWY3DPEHPK3PXP", + label: "Example:alice@google.com", + issuer: "Example", + encoding: "base32" + }); + var expect = "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"; + assert.deepEqual( + url.parse(answer), + url.parse(expect) + ); + }); + +});