From c38768772a60c5880d71b84add78c8b62f2b7b02 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Fri, 23 Oct 2015 00:11:29 -0700 Subject: [PATCH] working test --- authenticator.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ test.html | 66 +++++++++++++++++++++++++++++++++++++ test.js | 71 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 authenticator.js create mode 100644 test.html create mode 100644 test.js diff --git a/authenticator.js b/authenticator.js new file mode 100644 index 0000000..e60fc53 --- /dev/null +++ b/authenticator.js @@ -0,0 +1,86 @@ +(function (exports) { +'use strict'; + +var Authenticator = exports.Authenticator || exports; +var Unibabel = window.Unibabel || require('unibabel'); +console.log('window.totp', window.totp); +var totp = window.totp || require('notp').totp; + +if (!window.crypto) { + document.addEventListener('mousemove', function (event) { + var ev = event || window.event; + + window.forge.random.collectInt(ev.pageX, 16); + window.forge.random.collectInt(ev.pageY, 16); + }); +} + +// Generate a key +function generateOtpKey() { + // 20 cryptographically random binary bytes (160-bit key) + if (false && window.crypto) { + var key = window.crypto.getRandomValues(new Uint8Array(20)); + + return Promise.resolve(key); + } else { + return new Promise(function (resolve, reject) { + window.forge.random.getBytes(20, function (err, bytes) { + if (err) { + reject(err); + return; + } + + resolve(Unibabel.binaryStringToBuffer(bytes)); + }); + }); + } +} + +// 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 = Unibabel.bufferToBase32(bin).replace(/=/g, ''); + + // lowercase with a space every 4 characters + var key = base32.toLowerCase().replace(/(\w{4})/g, "$1 ").trim(); + + return key; +} + +function generateGoogleAuthKey() { + return generateOtpKey().then(encodeGoogleAuthKey); +} + +// 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 = Unibabel.base32ToBuffer(unformatted); + + return bin; +} + +// Generate a Google Auth Token +function generateGoogleAuthToken(key) { + var bin = decodeGoogleAuthKey(key); + + return totp.gen(bin); +} + +// Verify a Google Auth Token +function verifyGoogleAuthToken(key, token) { + var bin = decodeGoogleAuthKey(key); + + token = token.replace(/\W+/g, ''); + + // window is +/- 1 period of 30 seconds + return totp.verify(token, bin, { window: 1, time: 30 }); +} + +Authenticator.generateKey = generateGoogleAuthKey; +Authenticator.generateToken = generateGoogleAuthToken; +Authenticator.verifyToken = verifyGoogleAuthToken; + +}( + 'undefined' !== typeof window ? (window.Authenticator = {}) : module.exports +)); diff --git a/test.html b/test.html new file mode 100644 index 0000000..ebb3057 --- /dev/null +++ b/test.html @@ -0,0 +1,66 @@ + + + + BOTP Test + + + +

Authenticator Test

+ +
+ Test with the Authy App. +
+ +
+
+ qrcode +

20-byte (160-bit) Base32 Key:

+

xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx

+
+ +
+
+ + + + + + + +
+ +
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test.js b/test.js new file mode 100644 index 0000000..1630283 --- /dev/null +++ b/test.js @@ -0,0 +1,71 @@ +// forgive the suckiness, but whatever +(function (exports) { +'use strict'; + +var key; +var Authenticator = exports.Authenticator; +var $ = function (x) { + return document.querySelector(x); +}; + +function generate(ke) { + Authenticator.generateKey().then(function (k) { + key = ke || k; + + var companyName = $('.js-company-name').value; + var userAccount = $('.js-user-account').value; + + // obviously don't use this in production, but it's not so bad for the demo + var src = 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + + encodeURIComponent( + 'otpauth://totp/' + + encodeURIComponent(companyName) + + ':' + + encodeURIComponent(userAccount) + + '?secret=' + + key.replace(/\s+/g, '').toUpperCase() + ); + + $('.js-key').innerHTML = key; // safe to inject because I created it + $('img.js-qrcode').src = src; + + Authenticator.generateToken(key).then(function (token) { + console.log('token', token); + + Authenticator.verifyToken(key, token).then(function (result) { + console.log('verify', result); + + Authenticator.verifyToken(key, '000 000').then(function (result) { + console.log('reject', result); + }); + }); + }); + }); +} + +$('.js-verify').addEventListener('click', function () { + var token = $('.js-token').value; + + Authenticator.verifyToken(key, token).then(function (result) { + var msg; + if (result) { + msg = 'Correct!'; + } else { + msg = 'FAIL!'; + } + + window.alert(msg); + }); +}); + +$('.js-generate').addEventListener('click', function () { + generate(null); +}); + +$('.js-company-name').value = 'ACME Co'; +$('.js-user-account').value = 'john@example.com'; +generate('hxdm vjec jjws rb3h wizr 4ifu gftm xboz'); + +}( + 'undefined' !== typeof window ? window : module.exports +));