From c16ce07792b9681e16c0bf4c975bfce7c586e496 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 7 Oct 2015 12:56:17 -0700 Subject: [PATCH] v1.0.0 --- README.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++- authenticator.js | 59 +++++++++++++++++++++++++++ example.js | 24 +++++++++++ package.json | 36 ++++++++++++++++ 4 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 authenticator.js create mode 100644 example.js create mode 100644 package.json diff --git a/README.md b/README.md index 4ad4aca..aa5dc57 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,102 @@ -# node-authenticator -Two- / Multi- Factor Authenication (2FA / MFA) for node.js +Node.js Authenticator +===================== + +Two- and Multi- Factor Authenication (2FA / MFA) for node.js + +There are a number of apps that various websites use to give you 6-digit codes to increase security when you log in: + +* Authy [iPhone](https://itunes.apple.com/us/app/authy/id494168017?mt=8) [Android](https://play.google.com/store/apps/details?id=com.authy.authy&hl=en) [Chrome](https://chrome.google.com/webstore/detail/authy/gaedmjdfmmahhbjefcbgaolhhanlaolb?hl=en) [Linux](https://www.authy.com/personal/) [OS X](https://www.authy.com/personal/) [BlackBerry](https://appworld.blackberry.com/webstore/content/38831914/?countrycode=US&lang=en) +* Google Authenticator [iPhone](https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8) [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en) +* Microsoft Authenticator [Windows Phone](https://www.microsoft.com/en-us/store/apps/authenticator/9wzdncrfj3rj) [Android](https://play.google.com/store/apps/details?id=com.microsoft.msa.authenticator) [iPhone]() +* GAuth [FxOS](https://marketplace.firefox.com/app/gauth/) + +There are many [Services that Support MFA](http://lifehacker.com/5938565/heres-everywhere-you-should-enable-two-factor-authentication-right-now), +including Google, Microsoft, Facebook, Digital Ocean, for starters. + +This module uses [`notp`](https://github.com/guyht/notp) which implements `TOTP` [(RFC 6238)](https://www.ietf.org/rfc/rfc6238.txt) +(the *Authenticator* standard), which is based on `HOTP` [(RFC 4226)](https://www.ietf.org/rfc/rfc4226.txt) +to provide codes that are exactly compatible with all other *Authenticator* apps and services that use them. + +Usage +===== + +```bash +npm install authenticator --save +``` + +```javascript +'use strict'; + +var authenticator = require('authenticator'); + +var formattedKey = authenticator.generateKey(); +// "acqo ua72 d3yf a4e5 uorx ztkh j2xl 3wiz" + +var formattedToken = authenticator.generateToken(formattedKey); +// "957 124" + +authenticator.verifyToken(formattedKey, formattedToken); +// { delta: 0 } + +authenticator.verifyToken(formattedKey, '000 000'); +// null +``` + +QRCode +------ + +See + +```javascript +'use strict'; + +var el = document.querySelector('.js-qrcode-canvas'); +var link = "otpauth://totp/{{NAME}}?secret={{KEY}}"; +var name = "Your Service"; + // remove spaces, hyphens, equals, whatever +var key = "acqo ua72 d3yf a4e5 uorx ztkh j2xl 3wiz".replace(/\W/g, '').toLowerCase(); + +var qr = new QRCode(el, { + text: link.replace(/{{NAME}}/g, name).replace(/{{KEY}}/g, key) +}); +``` + +Formatting +---------- + +All non-alphanumeric characters are ignored, so you could just as well use hyphens +or periods or whatever suites your use case. + +These are just as valid: + +* "acqo ua72 d3yf a4e5 - uorx ztkh j2xl 3wiz" +* "98.24.63" + +0, 1, 8, and 9 also not used (so that base32). +To further avoid confusion with O, o, L, l, I, B, and g +you may wish to display lowercase instead of uppercase. + +TODO: should this library replace 0 with o, 1 with l (or I?), 8 with b, 9 with g, and so on? + +90-second Window +---------------- + +The window is set to +/- 1, meaning each token is valid for a total of 90 seconds +(-30 seconds, +0 seconds, and +30 seconds) +to account for time drift (which should be very rare for mobile devices) +and humans who are handicapped or otherwise struggle with quick fine motor skills (like my grandma). + + +Why not SpeakEasy? +------------------ + +I took a look at the code and I didn't feel comfortable using it. + +For any module related to security I want to see that the code is clean, +well-maintained, and that any security-related bugs are addressed. + +The author was obviously not well-versed in JavaScript at the time +that he wrote it and it hasn't been cleaned up since. +Also, the author hasn't been responsive to issues and pull requests. + +The notp author has been responsive, but notp doesn't do everything I would like. diff --git a/authenticator.js b/authenticator.js new file mode 100644 index 0000000..d17e75b --- /dev/null +++ b/authenticator.js @@ -0,0 +1,59 @@ +'use strict'; + +var crypto = require('crypto'); +var b32 = require('thirty-two'); + +// Generate a key +function generateOtpKey() { + // 20 cryptographically random binary bytes (160-bit key) + var key = crypto.randomBytes(20); + + return key; +} + +// Text-encode the key as base32 (in the style of Google Authenticator - same as Facebook, Microsoft, etc) +function encodeGoogleAuthKey(bin) { + // 32 ascii characters without trailing '='s + var base32 = b32.encode(bin).toString('utf8').replace(/=/g, ''); + + // lowercase with a space every 4 characters + var key = base32.toLowerCase().replace(/(\w{4})/g, "$1 ").trim(); + + return key; +} + +function generateGoogleAuthKey() { + return encodeGoogleAuthKey(generateOtpKey()); +} + +// Binary-decode the key from base32 (Google Authenticator, FB, M$, etc) +function decodeGoogleAuthKey(key) { + // decode base32 google auth key to binary + var unformatted = key.replace(/\W+/g, '').toUpperCase(); + var bin = b32.decode(unformatted); + + return bin; +} + +// Generate a Google Auth Token +function generateGoogleAuthToken(key) { + var bin = decodeGoogleAuthKey(key); + var notp = require('notp'); + + return notp.totp.gen(bin); +} + +// Verify a Google Auth Token +function verifyGoogleAuthToken(key, token) { + var bin = decodeGoogleAuthKey(key); + var notp = require('notp'); + + token = token.replace(/\W+/g, ''); + + // window is +/- 1 period of 30 seconds + return notp.totp.verify(token, bin, { window: 1, time: 30 }); +} + +module.exports.generateKey = generateGoogleAuthKey; +module.exports.generateToken = generateGoogleAuthToken; +module.exports.verifyToken = verifyGoogleAuthToken; diff --git a/example.js b/example.js new file mode 100644 index 0000000..27042e1 --- /dev/null +++ b/example.js @@ -0,0 +1,24 @@ +'use strict'; + +var authenticator = require('./authenticator'); + +var formattedKey = authenticator.generateKey(); +console.log(formattedKey); +// "acqo ua72 d3yf a4e5 uorx ztkh j2xl 3wiz" + +var formattedToken = authenticator.generateToken(formattedKey); +console.log(formattedToken); +// "957 124" + +var result = authenticator.verifyToken(formattedKey, formattedToken); +console.log(result); +// { delta: 0 } + +result = authenticator.verifyToken(formattedKey, '000-000'); +console.log(result); +// null + +// result will allways be one of +// (failure) null +// (success) { delta: -1 }, { delta: 0 }, or { delta: 1 } +// delta lets you know which way time drift is occurring diff --git a/package.json b/package.json new file mode 100644 index 0000000..f732aaf --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "authenticator", + "version": "1.0.0", + "description": "Two- / Multi- Factor Authenication (2FA / MFA) for node.js", + "main": "authenticator.js", + "scripts": { + "test": "node example.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Daplie/node-authenticator.git" + }, + "keywords": [ + "authenticator", + "2fa", + "mfa", + "token", + "key", + "base32", + "code", + "generator", + "authy", + "google", + "microsoft" + ], + "author": "AJ ONeal (http://coolaj86.com/)", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/Daplie/node-authenticator/issues" + }, + "homepage": "https://github.com/Daplie/node-authenticator#readme", + "dependencies": { + "notp": "^2.0.3", + "thirty-two": "0.0.2" + } +}