-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
init module with basic functionality
- Loading branch information
0 parents
commit 6dd15f7
Showing
4 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# 2FA | ||
|
||
Module for generating and verifying 2FA codes (specifically TOTP and HOTP). | ||
|
||
Also contains utilities for handing 2FA logic, such as generating Google QR codes (without going via Google Charts) and generating backup codes. | ||
|
||
## Install | ||
``` | ||
npm install --save 2fa | ||
``` | ||
|
||
## Usage | ||
|
||
```js | ||
var TFA = require('2fa'); | ||
var tfa = new TFA(); | ||
|
||
// lets generate a new key for a user | ||
// tfa.generateKey(length (optional), cb) | ||
tfa.generateKey(32, function(err, key) { | ||
// crypto secure hex key with 32 characters | ||
|
||
// generate crypto-secure backups codes in a user-friendly pattern | ||
// tfa.generateBackupCodes(num, pattern (optional), cb) | ||
tfa.generateBackupCodes(8, 'xxxx-xxxx-xxxx', function(err, codes) { | ||
// [ '7818-b7b8-c928', '3526-dc04-d3f2', 'be3c-5d9f-cb68', ... ] | ||
|
||
// these should be sent to the user, stored and checked when we get a 2fa code | ||
}); | ||
|
||
// generate a google QR code so the user can save their new key | ||
// tfa.generateGoogleQR(name, accountname, secretkey, cb) | ||
tfa.generateGoogleQR('Company', 'email@gmail.com', key, function(err, qr) { | ||
// data URL png image for google authenticator | ||
}); | ||
|
||
var opts = { | ||
// the number of counters to check before what we're given | ||
// default: 0 | ||
beforeDrift: 2, | ||
// and the number to check after | ||
// default: 0 | ||
afterDrift: 2, | ||
// if before and after drift aren't specified, | ||
// before + after drift are set to drift / 2 | ||
// default: 0 | ||
drift: 4, | ||
// the step for the TOTP counter in seconds | ||
// default: 30 | ||
step: 30 | ||
}; | ||
|
||
// calculate the counter for the HOTP (pretending it's actually TOTP) | ||
var counter = Math.floor(Date.now() / 1000 / opts.step); | ||
|
||
// generate a valid code (in real-life this will be user-input) | ||
var code = tfa.generateCode(key, counter); | ||
|
||
// verify it as a HOTP | ||
var validHOTP = tfa.verifyHOTP(key, code, counter, opts); | ||
// true | ||
|
||
// for TOTP, the counter is calculated internally using Date.now(); | ||
var validTOTP = tfa.verifyTOTP(key, code, opts); | ||
// true | ||
|
||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
var crypto = require('crypto'); | ||
var base32 = require('thirty-two'); | ||
var QRCode = require('qrcode'); | ||
var async = require('async'); | ||
|
||
var TFA = module.exports = function(opts) { | ||
var self = this; | ||
|
||
// unused at the moment | ||
self.opts = opts; | ||
}; | ||
|
||
// basically a crypto.randomBytes helper | ||
TFA.prototype.generateKey = function(length, cb) { | ||
var self = this; | ||
|
||
if (!cb && typeof length === 'function') { | ||
cb = opts; | ||
length = 32; | ||
} | ||
|
||
crypto.randomBytes(length / 2, function(err, buf) { | ||
if (err) return cb(err); | ||
|
||
cb(err, buf.toString('hex')); | ||
}); | ||
}; | ||
|
||
TFA.prototype.verifyHOTP = function(key, code, counter, opts) { | ||
var self = this; | ||
opts = opts || {}; | ||
|
||
var drift = (opts.drift || 0) / 2; | ||
|
||
// allow drift X counters before | ||
var before = opts.beforeDrift || drift; | ||
|
||
// allow drift X counters after | ||
var after = opts.afterDrift || drift; | ||
|
||
for (var i = counter - before; i <= counter + after; i++) { | ||
if (self.generateCode(key, i, opts) === code) return true; | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
TFA.prototype.verifyTOTP = function(key, code, opts) { | ||
var self = this; | ||
opts = opts || {}; | ||
|
||
var step = opts.step || 30; | ||
|
||
var counter = Math.floor(Date.now() / 1000 / step); | ||
|
||
return self.verifyHOTP(key, code, counter, opts); | ||
}; | ||
|
||
TFA.prototype.generateCode = function(key, counter, opts) { | ||
var self = this; | ||
opts = opts || {}; | ||
var length = opts.length || 6; | ||
|
||
var hmac = crypto.createHmac('sha1', key); | ||
|
||
// get the counter as bytes | ||
var counterBytes = new Array(8); | ||
for (var i = counterBytes.length - 1; i >= 0; i--) { | ||
counterBytes[i] = counter & 0xff; | ||
counter = counter >> 8; | ||
} | ||
|
||
var token = hmac.update(new Buffer(counterBytes)).digest('hex'); | ||
|
||
// get the token as bytes | ||
var tokenBytes = []; | ||
for (var i = 0; i < token.length; i += 2) { | ||
tokenBytes.push(parseInt(token.substr(i, 2), 16)); | ||
} | ||
|
||
// truncate to 4 bytes | ||
var offset = tokenBytes[19] & 0xf; | ||
var ourCode = | ||
(tokenBytes[offset++] & 0x7f) << 24 | | ||
(tokenBytes[offset++] & 0xff) << 16 | | ||
(tokenBytes[offset++] & 0xff) << 8 | | ||
(tokenBytes[offset++] & 0xff); | ||
|
||
// we want strings! | ||
ourCode += ''; | ||
|
||
return ourCode.substr(ourCode.length - length); | ||
}; | ||
|
||
|
||
TFA.prototype.generateGoogleQR = function(name, account, key, opts, cb) { | ||
if (!cb && typeof opts === 'function') { | ||
cb = opts; | ||
opts = {}; | ||
} | ||
|
||
var data = 'otpauth://totp/' + encodeURIComponent(account); | ||
data += '?issuer=' + encodeURIComponent(name); | ||
data += '&secret=' + base32.encode(key).toString().replace(/=/g, ''); | ||
QRCode.toDataURL(data, opts, cb); | ||
}; | ||
|
||
TFA.prototype.generateBackupCodes = function(count, pattern, cb) { | ||
if (!cb && typeof pattern === 'function') { | ||
cb = pattern; | ||
pattern = 'xxxx-xxxx-xxxx'; | ||
} | ||
|
||
// how many crypto bytes do we need? | ||
var patternLength = Math.ceil((pattern.split('x').length) - 1 / 2); | ||
|
||
async.times(count, function(t, done) { | ||
crypto.randomBytes(patternLength, function(err, buf) { | ||
if (err) return done(err); | ||
var chars = buf.toString('hex'); | ||
var code = ''; | ||
|
||
// number of crypto characters that we've used | ||
var xs = 0; | ||
for (var i = 0; i < pattern.length; i++) { | ||
code += pattern[i] === 'x' ? chars[xs++] : pattern[i]; | ||
} | ||
done(err, code); | ||
}); | ||
}, cb); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "2fa", | ||
"version": "0.0.1", | ||
"description": "TOTP + HOTP library, with nice utilities for handing 2FA", | ||
"main": "lib/2FA.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/simontabor/2fa.git" | ||
}, | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"keywords": [ | ||
"auth", | ||
"two-factor", | ||
"2fa" | ||
], | ||
"author": "Simon Tabor", | ||
"license": "MIT", | ||
"dependencies": { | ||
"async": "^0.9.0", | ||
"qrcode": "^0.2.12", | ||
"thirty-two": "0.0.2" | ||
} | ||
} |