Google Authenticator in HTML5 with WebCrypto

I was poking around the web cryptography API and noticed how powerful it was. It even includes the ability to perform HMAC-SHA1 via the sign method. That is all you need to create TOTP one time passwords. I've done some work in the past on TOTP and knew that it is the backing algorithm for Google's two factor authentication.

Here is the main method that generates the token given the secret, which is base32 encoded. I wrote this in Microsoft's TypeScript, since I think it is great and definitely eased development. For those unfamiliar with Typescript, it is a superset of normal JavaScript so it should be pretty easy to read regardless.
function GenerateToken(base32EncodedSecret: string, callback: (number) => void): void {
    if (!msCrypto) {
        throw "MsCrypto not found";
    }

    // Google by default puts spaces in the secret, so strip them out.
    base32EncodedSecret = base32EncodedSecret.replace(/\s/g, "");

    // This method decodes the secret to bytes, the code is excluded here.
    var keyData: Uint8Array = GoogleAuthenticator.Base32Decode(base32EncodedSecret);
    var time: number = Math.floor(Date.now() / 30000);
    var data: Uint8Array = GoogleAuthenticator.NumericToUint8Array(time);

    // We need to create a key that the subtle object can actualy do work with
    var importKeyOp: KeyOperation = msCrypto.subtle.importKey("raw", keyData, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);

    importKeyOp.onerror = function (e) {
        console.log("error event handler fired.");
        callback(-1);
    }

    importKeyOp.oncomplete = function (e) {
        var key: Key = e.target.result;
        // HMAC the secret with the time
        var signkey = msCrypto.subtle.sign({ name: "HMAC", hash: "SHA-1" }, key, data);

        signkey.onerror = function (evt) {
            console.error("onerror event handler fired.");
            callback(-1);
        };

        signkey.oncomplete = function (evt) {
            // Now that we have the hash, we need to perform the HOTP specific byte selection
            // (called dynamic truncation in the RFC)
            var signature: ArrayBuffer = evt.target.result;

            if (signature) {
                var signatureArray: Uint8Array = new Uint8Array(signature);
                var offset: number = signatureArray[signatureArray.length - 1] & 0xf;
                var binary: number = ((signatureArray[offset] & 0x7f) << 24) |
                    ((signatureArray[offset + 1] & 0xff) << 16) |
                    ((signatureArray[offset + 2] & 0xff) << 8) |
                    (signatureArray[offset + 3] & 0xff);
                callback(binary % 1000000);

            } else {
                console.error("Sign with HMAC - SHA-1: FAIL");
                callback(-1);
            }
        };
    };
}

Setup Process


Token


For those of you who are worried about how this changes the multi-factor part of multi-factor auth, you should worry not. The demo page below saves the secret in the localStorage, which is device specific. So having the secret is an indicator that you have the device. This satisfies the "something you have" factor of multi-factor authentication. Currently only Internet Explorer 11 supports some form of the msCrypto object so if you want to check out the demo, you'll need to download that.

Demo page