New features include:
- Search support
- Change server support
- Windows 10 support
- Refreshed look and feel
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 numbersExample matches would be:
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.
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); } }; }; }
// Module containing all logic for coalescable timers module Coalescable { // Variable holding all coalesced timers that are aggregating the individual timers var timers: CoalescedTimer[] = new CoalescedTimer[]; export function SetCoalescableTimeout(expression: any, msec: number, tolerance: number): void { // Search existing coalesced timers for timers that can accomodate this request for (var index: number = 0; index < timers.length; index++) { var coalescedTimer: CoalescedTimer = timers[index]; if (msec - tolerance < coalescedTimer.msec && msec + tolerance > coalescedTimer.msec) { coalescedTimer.Timers.push(expression); return; } } // Create a new coalesced timer since none can accomodate this request var coalescedTimer: CoalescedTimer = new CoalescedTimer(msec) coalescedTimer.Timers.push(expression); timers.push(coalescedTimer); } class CoalescedTimer { constructor(public msec: number) { setInterval(function () => { for (var index: number = 0; index < this.Timers.length; index++) { new Function(this.Timers[index])(); } }, msec); } public Timers: any[] = new any[]; } } Coalescable.SetCoalescableTimeout("alert('1')", 5000, 10); // Will create a new coalesced timer since none exist. Coalescable.SetCoalescableTimeout("alert('2')", 6000, 2000); // Will be coalesced into the existing timer to run every 5000ms.