jsAuthenticator (script/userscript)



  • I've been planning on posting this for about 2 days now, but I kept on tweaking it, adding little things to it...

    (this can also be run by pasting it directly into the console, as an alternative to using it as a userscript)

    // ==UserScript==
    // @name           jsAuthenticator
    // @namespace      custom
    // @description    Generates randomly changing 6-digit codes commonly used for two-step verification. This is a Javascript implementation of the Time-based One-Time Password (TOTP) algorithm, and is compatible with Google Authenticator and similar apps.
    // @include        http://*/*
    // @include        https://*/*
    // @grant          unsafeWindow
    // ==/UserScript==
    
    (function jsAuth_init(r) {
      if (typeof unsafeWindow != 'undefined' && !r) {
        unsafeWindow.eval('(' + jsAuth_init + ')')(1);
    
      } else {
        console.log('Initializing jsAuthenticator...');
    
        var codes = [
          // If the page's host matches an item in this list, it'll automatically load its settings
          // The 'host' and 'key' parameters are required; all of the other parameters are optional
          // If the 'hide' parameter is true, the secret key will be hidden from outside scripts
          // Changing the key will make the new key visible by default, unless specified otherwise
          {host: /^(.*\.)?example\.com$/, issuer: 'Example', account: 'user@example.com', key: '2NEGV2ITNZ4FNPCCEERYL2TZOCKEOWAC', hide: false},
        ];
    
        var jsAuthenticator = {}, issuer, account, key, clockAdjust = 0, keyIsHidden = false;
    
        Object.defineProperty(jsAuthenticator, 'issuer', {
          get: function get() { return issuer; },
          set: function set(n) { issuer = String(n); }
        });
    
        Object.defineProperty(jsAuthenticator, 'account', {
          get: function get() { return account; },
          set: function set(u) { account = String(u); }
        });
    
        Object.defineProperty(jsAuthenticator, 'key', {
          get: function get() { return keyIsHidden ? '*'.repeat(32) : key; },
          set: function set(s) {
            if (typeof s == 'undefined') {
              key = undefined;
            } else {
              s = String(s).toUpperCase().replace(/\s/g, '');
              if (/[^A-Z2-7]/.test(s))
                throw new TypeError('Invalid base32 key');
              key = s;
              keyIsHidden = false;
            }
          }
        });
    
        // If the system clock is wrong, setting clockAdjust will bring codes back into sync with reality
        // Positive adjust values will adjust the clock forward; negative values will adjust it back
        // You can also adjust this in increments of 30000 (30 seconds) to get codes from the future/past
        // E.g. you could get a sneak peek at the next code by setting it to 30000, then calling getCode
        Object.defineProperty(jsAuthenticator, 'clockAdjust', {
          enumerable: false,
          get: function get() { return clockAdjust; },
          set: function set(ms) {
            ms = Number(ms);
            if (!isFinite(ms) || isNaN(ms))
              throw new TypeError('Invalid clockAdjust value');
            clockAdjust = ms;
          }
        });
    
        // Calling this function will hide the key from outside scripts
        jsAuthenticator.hideKey = function hideKey() {
          keyIsHidden = true;
        };
    
        // If getCode is called with a key argument, the key will be used and stored for subsequent calls
        // By default, the new key will not be hidden; to hide it, specify 'true' as the second argument
        jsAuthenticator.getCode = function getCode(k, hide) {
          if (typeof k != 'undefined') {
            jsAuthenticator.key = k;
            keyIsHidden = !!hide;
          }
          if (typeof key != 'undefined') k = key;
          else throw new ReferenceError('key is not defined');
    
          var period = 30; // seconds (default is 30)
          var adjust = jsAuthenticator.clockAdjust;
          var time = Math.floor(Math.round((new Date().getTime() + adjust) / 1000) / period);
          var expires = new Date((time + 1) * period * 1000);
    
          time = ('0'.repeat(16) + time.toString(16)).slice(-16);
    
          var message = time.replace(/../g, function (h) { return String.fromCharCode(parseInt(h, 16)); });
          var hmac = HMAC(base32decode(k), message, SHA1);
          hmac = hmac.replace(/./g, function (c) { return ('0' + c.charCodeAt(0).toString(16)).slice(-2); });
    
          var offset = parseInt(hmac.slice(-1), 16) * 2;
          var code = ('00000' + (parseInt(hmac.substr(offset, 8), 16) & 0x7fffffff)).slice(-6);
    
          return {code: code, expires: expires};
        };
    
        // Generate your own secret keys! Impress your friends!
        // The new key will be stored for subsequent use, and returned in base32 format
        // The stored key not be hidden, but you could hide it if you want by calling hideKey
        jsAuthenticator.generateKey = function generateKey(password) {
          if (typeof password == 'undefined') { // if a password isn't supplied, generate a random one
            password = '';
            for (var i = 0; i < 20; ++ i) {
              password += String.fromCharCode(Math.floor(Math.random() * 256));
            }
          } else {
            password = String(password);
          }
    
          var key = base32encode(SHA1(password));
          jsAuthenticator.key = key;
          return key;
        };
    
        // Everybody loves QR codes.
        jsAuthenticator.getQRCode = function getQRCode() {
          var url;
          if (keyIsHidden) {
            console.log('Unable to create QR code -- secret key is hidden.');
    
          } else {
            var key = jsAuthenticator.key;
            if (typeof key == 'undefined') throw new ReferenceError('key is not defined');
    
            var label = '', parameters = '';
            var issuer = jsAuthenticator.issuer, account = jsAuthenticator.account;
            if (typeof account != 'undefined') {
              account = encodeURIComponent(account);
    
              if (typeof issuer != 'undefined') {
                issuer = encodeURIComponent(issuer);
                label = issuer + ':';
                parameters = '&issuer=' + issuer;
              }
    
              label += account;
            }
    
            url = 'https://chart.googleapis.com/chart?chs=287x287&chld=L|4&cht=qr&chl='
              + encodeURIComponent('otpauth://totp/' + label + '?secret=' + key + parameters);
          }
    
          return url;
        };
    
        // This attempts to calculate a clock adjust factor to sync the system clock with the server time
        // (assuming the server clock is correct, of course... if it's not, we might probably have problems.)
        jsAuthenticator.syncClock = function syncClock() {
          console.log('Syncing clock...');
    
          // 10 attempts gives a fairly decent sync... there's network latency, of course, and the time header
          // only has a resolution of 1 second; the attempts are staggered by 100ms to hone in better on it
          for (var c = 0, t = 0, n = 0, offsets = [], i = 0; i < 10; ++ i) {
            ++ c;
            setTimeout(function () {
              var d = new Date;
              with (new XMLHttpRequest) {
                open('HEAD', '/'); // poor man's time API
                setRequestHeader('Cache-Control', 'no-cache');
                addEventListener('load', () => {
                  var sysDate = new Date, ping = sysDate - d;
                  // Adding half the ping should account for the latency, but for some reason the end result is
                  // consistently around 0.5 seconds slow. Adding 500 ms to realDate gives very good results.
                  var realDate = new Date(getResponseHeader('date')).getTime() + ping / 2 + 500;
                  var offset = Math.floor(realDate - sysDate);
    
                  t += offset;
                  ++ n;
                  jsAuthenticator.clockAdjust = Math.floor(t / n);
    
                  if (-- c <= 0) console.log('Done.');
                });
                send();
              }
            }, i * 1100);
          }
        };
    
        Object.freeze(jsAuthenticator);
        window.jsAuthenticator = jsAuthenticator;
    
        for (var c of codes) {
          if (c.host.test(location.host)) {
            jsAuthenticator.issuer = c.issuer;
            jsAuthenticator.account = c.account;
            jsAuthenticator.key = c.key;
            if (c.hide) jsAuthenticator.hideKey();
          }
        }
    
        console.log('jsAuthenticator: Done.');
        return;
    
    
        // extra functions
        function base32decode(key) { // converts from base32 to byte encoding
          key = key.toUpperCase();
          for (var k = '0'.repeat((4 - key.length % 4) % 4), i = 0; i < key.length; ++ i)
            k += '0123456789abcdefghijklmnopqrstuv'.charAt('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.indexOf(key.charAt(i)));
          for (var o = '', i = 0; i < k.length; i += 4)
            o += ('0000' + parseInt(k.substr(i, 4), 32).toString(16)).slice(-5);
          return ('0'.repeat((2 - o.length % 2) % 2) + o).replace(/../g, function (c) { return String.fromCharCode(parseInt(c, 16)); });
        }
        function base32encode(key) { // converts from byte encoding to base32
          key = '\0'.repeat((5 - (key.length % 5)) % 5) + key;
          for (var hex = '', i = 0; i < key.length; ++ i)
            hex += ('0' + key.charCodeAt(i).toString(16)).slice(-2);
          for (var base32 = '', i = 0; i < hex.length; i += 5)
            base32 += ('000' + parseInt(hex.substr(i, 5), 16).toString(32)).slice(-4);
          base32 = base32.toLowerCase();
          for (var result = '', i = 0; i < base32.length; ++ i)
            result += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.charAt('0123456789abcdefghijklmnopqrstuv'.indexOf(base32.charAt(i)));
          return result;
        }
        function SHA1(message) {
          var leftRotate = function (n, i) { return (n << i) | (n >>> (32 - i)); };
          var messageLength = message.length * 8, blockSize = 64;
    
          // pad the message to a multiple of 512 bits (64 bytes)
          message += '\u0080'; // add a '1' bit to the end of the message (since the message is bytes, add byte '1000000')
          message += '\0'.repeat(blockSize - ((message.length + 8) % blockSize)); // pad with '0' bits, leaving 64 bits for length
          for (var ml = ('0'.repeat(16) + messageLength.toString(16)).slice(-16), j = 0; j < 8; ++ j)
            message += String.fromCharCode(parseInt(ml.substr(j * 2, 2), 16)); // append the original length in bits as a 64-bit number
    
          // main loop - process each 64-byte block of the message
          var h = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
          for (var blockOffset = 0; blockOffset < message.length; blockOffset += blockSize) {
            // SHA-1 uses an 80-word message schedule
            for (var w = [], t = 0; t < 80; ++ t) {
              if (t < 16) {
                // for the first 16 words, split the 512-bit block into 32 bit words
                w[t] = 0;
                for (var i = 0; i < 4; ++ i) {
                  w[t] = w[t] << 8 | message.charCodeAt(blockOffset + t * 4 + i);
                }
              } else {
                // the remaining words are derived by smashing and rotating several of the previous words together
                w[t] = leftRotate(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1);
              }
            }
    
            // loop to process each word from the 80-word message schedule
            var a = h[0], b = h[1], c = h[2], d = h[3], e = h[4];
            for (var t = 0, temp, f, k; t < 80; ++ t) {
              if (t < 20) {
                f = (b & c) | (~b & d);
                k = 0x5a827999;
              } else if (t < 40) {
                f = b ^ c ^ d;
                k = 0x6ed9eba1;
              } else if (t < 60) {
                f = (b & c) | (b & d) | (c & d);
                k = 0x8f1bbcdc;
              } else {
                f = b ^ c ^ d;
                k = 0xca62c1d6;
              }
    
              temp = leftRotate(a, 5) + f + e + k + w[t];
              e = d;
              d = c;
              c = leftRotate(b, 30);
              b = a;
              a = temp;
            }
    
            h[0] = h[0] + a;
            h[1] = h[1] + b;
            h[2] = h[2] + c;
            h[3] = h[3] + d;
            h[4] = h[4] + e;
          }
    
          for (var hex = '', i = 0; i < 5; ++ i)
            hex += ('0'.repeat(8) + (h[i] >>> 0).toString(16)).slice(-8);
          for (var bytes = '', i = 0; i < hex.length; i += 2)
            bytes += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
          return bytes;
        }
        function HMAC(key, message, hashFn) {
          var blockSize = 64;
    
          if (key.length > blockSize) key = hashFn(key);
          if (key.length < blockSize) key += '\0'.repeat(blockSize - key.length);
    
          var xor = function xor(str, num) {
            for (var out = '', i = 0; i < str.length; ++ i)
              out += String.fromCharCode(str.charCodeAt(i) ^ num);
            return out;
          };
    
          var pad1 = xor(key, 0x5c), pad2 = xor(key, 0x36);
          return hashFn(pad1 + hashFn(pad2 + message));
        }
      }
    })();
    

    Anyway, you probably want to see what it does. Okay, here's what it does:

    0_1512575661022_ab3ee083-05bc-424e-a586-4d05556224d4-image.png

    The jsAuthenticator global is available to scripts on the page... I'm probably going to incorporate it into a userscript that automatically fills in the code on a particular website that requests a code, but in the meantime, I'll probably just put together a demo page...



  • In other news, I've updated the code to contain an ugly hack of a time sync function (which works surprisingly well, considering -- since there's no good/official internet time API, I had to improvise).

    Mostly because my computer's clock is wrong. As you can see:

    The code that rendered the date/time overlay isn't in the script... for that, I used this:

    (function () {
      s = document.body.appendChild(document.createElement('span'));
      s.setAttribute('style','position:absolute;top:10px;color:#fff;font-size:28px;left:0;font-family:Helvetica,Arial,Sans-Serif;font-weight:400;z-index:999;text-shadow:-1px -1px #000,0 -1px #000,1px -1px #000,-1px 0 #000,1px 0 #000,-1px 1px #000,0 1px #000,1px 1px #000,1px 2px #000,2px 1px #000,2px 2px #000;width:100%;text-align:center');
      s = s.appendChild(document.createTextNode(''));
      (function clock() {
        var d = new Date((new Date).getTime() + jsAuthenticator.clockAdjust);
        var t = new Date(Math.round(d.getTime() / 1000) * 1000);
        s.data = location.host + '’s time is currently: ' + t.toUTCString();
        setTimeout(clock, t.getTime() + 1000 - d);
      })();
      jsAuthenticator.syncClock();
    })();
    


  • @anotherusername said in jsAuthenticator (script/userscript):

    Anyway, you probably want to see what it does. Okay, here's what it does:

    What does it do? In words, please.



  • @blakeyrat It looks like it generates numeric codes for 2-factor authentication, like Google Authenticator, except as a userscript instead of a phone app.



  • @blakeyrat the same thing as Google Authenticator.

    "Generates randomly changing 6-digit codes commonly used for two-step verification. This is a Javascript implementation of the Time-based One-Time Password (TOTP) algorithm, and is compatible with Google Authenticator and similar apps."

    It'll be clearer once I get a demo working instead of running stuff from the console.


  • Considered Harmful

    @anotherusername I definitely know your computer's something is wrong.



  • @pie_flavor What browser are you using and what OS? That video plays fine for me.



  • @pie_flavor yeah I seem to remember you complaining about the last time I posted a video too. Your codecs are still screwed up. Download the video and use VLC and it'll work fine.


  • area_can

    The beauty of adding video support to html is that videos will finally work across all browsers:

    0_1512604471396_Screenshot_20171206-185423.png



  • Here's the demo. It generates a random key each time it starts up (displayed under the QR code), so F5 will get you a different one.

    jsAuthenticator demo.html (right click, download, then open -- the scripts won't run here)


  • Considered Harmful

    @anotherusername And I can view this video just fine. :wtf:



  • @anotherusername said in jsAuthenticator (script/userscript):

    the same thing as Google Authenticator.

    @anotherusername said in jsAuthenticator (script/userscript):

    the same thing as Google Authenticator.

    I don't really know that that is, so... good description?

    But now that I've Googled it, it looks like one of those RSA SecurID widgets we all used to carry, but in Android instead of a specialized piece of hardware.


  • Considered Harmful

    @blakeyrat said in jsAuthenticator (script/userscript):

    @anotherusername said in jsAuthenticator (script/userscript):

    the same thing as Google Authenticator.

    @anotherusername said in jsAuthenticator (script/userscript):

    the same thing as Google Authenticator.

    I don't really know that that is, so... good description?

    But now that I've Googled it, it looks like one of those RSA SecurID widgets we all used to carry, but in Android instead of a specialized piece of hardware.

    You have effectively summarized it.
    And 'used to'? My dad still uses one daily.



  • @blakeyrat well there's lots of apps that have 2 factor authentication support. Google Authenticator is just one of the most popular ones that screws you over if your phone dies or gets lost. Authy is another and it syncs your 2FA tokens between devices, encrypted with a password of your choosing. There's also LastPass Authenticator which recently got the ability to backup and restore 2FA tokens.

    I hope you're using 2FA or U2F on most of your accounts.


  • :belt_onion:

    @pie_flavor said in jsAuthenticator (script/userscript):

    And 'used to'? My dad still uses one daily.

    I have 3. :/

    (Though entertainingly one is for RSA Support and even they stopped using it a couple months after it was issued. They are definitely on their way out, to be replaced by the software solutions.)



  • @anotherusername said in jsAuthenticator (script/userscript):

    Here's the demo. It generates a random key each time it starts up (displayed under the QR code), so F5 will get you a different one.

    jsAuthenticator demo.html (right click, download, then open -- the scripts won't run here)

    Do you have a video of how you took this video?



  • @lb_ said in jsAuthenticator (script/userscript):

    Google Authenticator is just one of the most popular ones that screws you over if your phone dies or gets lost.

    As long as you have the key you'll be fine. Either re-import it into Google Authenticator or use some other software implementing the algorithm (like the userscript in this topic!)



  • @heterodox in writing this I discovered that the software code generator is actually fairly simple. It's based on a Hash-based Message Authentication Code (HMAC).

    It takes two components: one, a secret key, which can be of any length, and two, a message. It also takes a hash function. The key is first right-padded with nulls, or hashed (if too large) then right-padded with nulls, as required to make it the same size as the hash function's data blocks (64 bytes for SHA-1). Two padding values are generated, one by XORing every byte of the key with 0x5c, and the second by XORing every byte of the key with 0x36. The second padding value is left concatenated to the message and hashed; the result of that is left-padded again with the first padding value and hashed one more time.

    For TOTP, the key is usually* 10 or 20 bytes, and is given as a 16- or 32-digit base32 number using the digits [A-Z2-7] (unfortunately, since toString(32) and parseInt(n, 32) use the digits [0-9A-V], translation is required). The message is the total number of intervals (typically** 30 seconds) that have passed since the epoch, given as a big-endian 64-bit number. Both the key and the message are byte-encoded (base-256), and the HMAC is obtained, typically** using a SHA-1 hash which produces an 160-bit result. The last 4 bits are used as an index, and a 32-bit substring is extracted, starting from the byte offset of that index; the highest bit of this substring is dropped to make it a 31-bit value (or a positive signed 32-bit number). It is then converted to decimal, and the leading digits are dropped, to finally result in a (typically**) 6-digit code.

    *My code doesn't care, so you can use either.
    **I hard-coded these default values, but they could be trivially changed (you'd need to replace the hash function if you wanted to use a different one, though, because I only wrote a SHA-1 hash function).



  • @hungrier said in jsAuthenticator (script/userscript):

    @anotherusername said in jsAuthenticator (script/userscript):

    Here's the demo. It generates a random key each time it starts up (displayed under the QR code), so F5 will get you a different one.

    jsAuthenticator demo.html (right click, download, then open -- the scripts won't run here)

    Do you have a video of how you took this video?

    Yes, but it's not stored on a device from which I could easily digitize it using current technology. And it probably contains PII, so I wouldn't want to share it anyway.

    edit: or you could get CSI to enhance the reflection in the video that I posted.



  • @hungrier said in jsAuthenticator (script/userscript):

    @lb_ said in jsAuthenticator (script/userscript):

    Google Authenticator is just one of the most popular ones that screws you over if your phone dies or gets lost.

    As long as you have the key you'll be fine. Either re-import it into Google Authenticator or use some other software implementing the algorithm (like the userscript in this topic!)

    Google Authenticator doesn't let you export keys, so you'd need to have it saved separately. Either the text key or the QR code should be fine; both is better, but you can get the one from the other and vice versa.

    In fact, if you have the text key, you could easily generate a QR code using my script... just set the issuer, account, and key, and then call getQRCode. (Google Authenticator didn't like it when I tried to scan QR codes that only contained a key, even though the TOTP URI spec seemed to indicate that they'd be valid with/without specifying issuer and account.)



  • @anotherusername said in jsAuthenticator (script/userscript):

    Google Authenticator doesn't let you export keys, so you'd need to have it saved separately

    Well yeah, and for good reason, since that would allow anyone with momentary access to your phone to pwn you forever. But when you first setup whatever code with Authenticator, that's when you, being diligent and forward-thinking, copy the key to some secure location (I didn't)



  • @hungrier said in jsAuthenticator (script/userscript):

    (I didn't)

    Also, I just now realized that some services that I apparently haven't used since I bought my current phone, which I setup 2FA on, are probably gone forever since I don't have them in Authenticator now.



  • @hungrier at least those services properly planned for that scenario by forcing you to print out or save the backup codes. Right?





  • @hungrier the backup codes are separate and unrelated to the 2FA key/QRcode that you did not back up.


  • Notification Spam Recipient

    @pie_flavor said in jsAuthenticator (script/userscript):

    @anotherusername I definitely know your computer's something is wrong.

    Wow, what happened to you renderer? I can't even repro that on my slowest computer...


  • Considered Harmful

    @tsaukpaetra Magic.