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