While creating a website using Web Optimization to handle bundling, I became curious as to what the {version} pattern matched. I couldn't find documentation for this besides what it outlined here:
The bundling framework follows several common conventions such as:
•Selecting “.min” file for release when “FileX.min.js” and “FileX.js” exist.
•Selecting the non “.min” version for debug.
•Ignoring “-vsdoc” files (such as jquery-1.7.1-vsdoc.js), which are used only by IntelliSense.
Digging into the source code here, I was able to find exactly what {version} matches, the C# regex @"(\d+(\s*\.\s*\d+){1,3})(-[a-z][0-9a-z-]*)?". This means it matches:
1 or more digits followed by
0 or more whitespace followed by
the '.' character followed by
0 or more whitespace followed by
1 or more digits followed by
the preceding group at least 1 times but no more than 3 times optionally followed by
the '-' character followed by
any a-z character followed by
0 or more a-z characters or numbers
Example matches would be:
1.23.23-a9
1.23.23
2.34
2.2-min
2
I hope that helps clears things up for some people.
When working on the Google Authenticator web page, I realized that I needed to base32 decode the secret given from Google to get to the raw bytes. For those unaware, Base32 encoding is a mechanism to represent arbitrary binary data (1s and 0s) into an alphanumeric representation that is more convenient for transport (typing, etc.). Base32 encoding is exactly like hexadecimal or octal, where binary data is represented using different characters (0-9, A-F in hex, 0-7 in octal) except Base32 encoding uses even more alphanumeric characters (2-7, A-Z) to reduce the space required to represent the same number of bits.
Looking around I couldn't find a well tested implementation of this, so I decided to write my own. I used this opportunity to explore unit testing a JavaScript method using QUnit.
Let's start with gathering the test cases. The RFC has some test vectors, so obviously those will be included:
test("Test Vectors", function () {
// Base32Decode should correctly decode the test vectors from the RFC
strictEqual(Base32Decode("").length, 0, "Base32Decode should return an empty array for the empty string");
ok(compareUint8ArrayToString(Base32Decode("MY======"), "f"), "Base32Decode should return 'f' for 'MY======'");
ok(compareUint8ArrayToString(Base32Decode("MZXQ===="), "fo"), "Base32Decode should return 'f' for 'MZXQ===='");
ok(compareUint8ArrayToString(Base32Decode("MZXW6YQ="), "foob"), "Base32Decode should return 'foob' for 'MZXW6YQ='");
ok(compareUint8ArrayToString(Base32Decode("MZXW6YTB"), "fooba"), "Base32Decode should return 'fooba' for 'MZXW6YTB'");
ok(compareUint8ArrayToString(Base32Decode("MZXW6YTBOI======"), "foobar"), "Base32Decode should return 'foobar' for 'MZXW6YTBOI======'");
});
Obviously these tests won't pass until we have a working Base32 decoder. The decoder is fairly straight-forward for inputs that don't have padding (i.e., the number of bytes are multiples of 40). In that case, you simple map the bits per the RFC:
The RFC goes into detail about what cases are possible with padding, etc. but I'll leave that as an exercise to the reader. I could have made the code smaller, but I wanted to be clear and follow the RFC as closely as possible. Here is the implementation:
var Base32Decode = function (base32EncodedString) {
/// Decodes a base32 encoded string into a Uin8Array, note padding is not supported
/// The base32 encoded string to be decoded
/// The Unit8Array representation of the data that was encoded in base32EncodedString
if (!base32EncodedString && base32EncodedString !== "") {
throw "base32EncodedString cannot be null or undefined";
}
if (base32EncodedString.length * 5 % 8 !== 0) {
throw "base32EncodedString is not of the proper length. Please verify padding.";
}
base32EncodedString = base32EncodedString.toLowerCase();
var alphabet = "abcdefghijklmnopqrstuvwxyz234567";
var returnArray = new Array(base32EncodedString.length * 5 / 8);
var currentByte = 0;
var bitsRemaining = 8;
var mask = 0;
var arrayIndex = 0;
for (var count = 0; count < base32EncodedString.length; count++) {
var currentIndexValue = alphabet.indexOf(base32EncodedString[count]);
if (-1 === currentIndexValue) {
if ("=" === base32EncodedString[count]) {
var paddingCount = 0;
for (count = count; count < base32EncodedString.length; count++) {
if ("=" !== base32EncodedString[count]) {
throw "Invalid '=' in encoded string";
} else {
paddingCount++;
}
}
switch (paddingCount) {
case 6:
returnArray = returnArray.slice(0, returnArray.length - 4);
break;
case 4:
returnArray = returnArray.slice(0, returnArray.length - 3);
break;
case 3:
returnArray = returnArray.slice(0, returnArray.length - 2);
break;
case 1:
returnArray = returnArray.slice(0, returnArray.length - 1);
break;
default:
throw "Incorrect padding";
}
} else {
throw "base32EncodedString contains invalid characters or invalid padding.";
}
} else {
if (bitsRemaining > 5) {
mask = currentIndexValue << (bitsRemaining - 5);
currentByte = currentByte | mask;
bitsRemaining -= 5;
} else {
mask = currentIndexValue >> (5 - bitsRemaining);
currentByte = currentByte | mask;
returnArray[arrayIndex++] = currentByte;
currentByte = currentIndexValue << (3 + bitsRemaining);
bitsRemaining += 3;
}
}
}
return new Uint8Array(returnArray);
};
I've added more tests around padding and other specifics you can find at the source below, but enjoy a live demo converting base32 encoded strings to hexadecimal:
You can find the source for both the tests and the actual decode on github here: Base32Decode in JavaScript. Also, if you'd like, you can run the tests directly from your browser via this link.
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.