diff --git a/src/Microsoft.AspNet.WebUtilities/WebEncoders.cs b/src/Microsoft.AspNet.WebUtilities/WebEncoders.cs new file mode 100644 index 0000000000..f73c3c249f --- /dev/null +++ b/src/Microsoft.AspNet.WebUtilities/WebEncoders.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; + +namespace Microsoft.AspNet.WebUtilities +{ + /// + /// Contains utility APIs to assist with common encoding and decoding operations. + /// + public static class WebEncoders + { + /// + /// Decodes a base64url-encoded string. + /// + /// The base64url-encoded input to decode. + /// The base64url-decoded form of the input. + /// + /// The input must not contain any whitespace or padding characters. + /// Throws FormatException if the input is malformed. + /// + public static byte[] Base64UrlDecode([NotNull] string input) + { + return Base64UrlDecode(input, 0, input.Length); + } + + /// + /// Decodes a base64url-encoded substring of a given string. + /// + /// A string containing the base64url-encoded input to decode. + /// The position in at which decoding should begin. + /// The number of characters in to decode. + /// The base64url-decoded form of the input. + /// + /// The input must not contain any whitespace or padding characters. + /// Throws FormatException if the input is malformed. + /// + public static byte[] Base64UrlDecode([NotNull] string input, int offset, int count) + { + ValidateParameters(input.Length, offset, count); + + // Assumption: input is base64url encoded without padding and contains no whitespace. + + // First, we need to add the padding characters back. + int numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count); + char[] completeBase64Array = new char[checked(count + numPaddingCharsToAdd)]; + Debug.Assert(completeBase64Array.Length % 4 == 0, "Invariant: Array length must be a multiple of 4."); + input.CopyTo(offset, completeBase64Array, 0, count); + for (int i = 1; i <= numPaddingCharsToAdd; i++) + { + completeBase64Array[completeBase64Array.Length - i] = '='; + } + + // Next, fix up '-' -> '+' and '_' -> '/' + for (int i = 0; i < completeBase64Array.Length; i++) + { + char c = completeBase64Array[i]; + if (c == '-') + { + completeBase64Array[i] = '+'; + } + else if (c == '_') + { + completeBase64Array[i] = '/'; + } + } + + // Finally, decode. + // If the caller provided invalid base64 chars, they'll be caught here. + return Convert.FromBase64CharArray(completeBase64Array, 0, completeBase64Array.Length); + } + + /// + /// Encodes an input using base64url encoding. + /// + /// The binary input to encode. + /// The base64url-encoded form of the input. + public static string Base64UrlEncode([NotNull] byte[] input) + { + return Base64UrlEncode(input, 0, input.Length); + } + + /// + /// Encodes an input using base64url encoding. + /// + /// The binary input to encode. + /// The offset into at which to begin encoding. + /// The number of bytes of to encode. + /// The base64url-encoded form of the input. + public static string Base64UrlEncode([NotNull] byte[] input, int offset, int count) + { + ValidateParameters(input.Length, offset, count); + + // Special-case empty input + if (count == 0) + { + return String.Empty; + } + + // We're going to use base64url encoding with no padding characters. + // See RFC 4648, Sec. 5. + char[] buffer = new char[GetNumBase64CharsRequiredForInput(count)]; + int numBase64Chars = Convert.ToBase64CharArray(input, offset, count, buffer, 0); + + // Fix up '+' -> '-' and '/' -> '_' + for (int i = 0; i < numBase64Chars; i++) + { + char ch = buffer[i]; + if (ch == '+') + { + buffer[i] = '-'; + } + else if (ch == '/') + { + buffer[i] = '_'; + } + else if (ch == '=') + { + // We've reached a padding character: truncate the string from this point + return new String(buffer, 0, i); + } + } + + // If we got this far, the buffer didn't contain any padding chars, so turn + // it directly into a string. + return new String(buffer, 0, numBase64Chars); + } + + private static int GetNumBase64CharsRequiredForInput(int inputLength) + { + int numWholeOrPartialInputBlocks = checked(inputLength + 2) / 3; + return checked(numWholeOrPartialInputBlocks * 4); + } + + private static int GetNumBase64PaddingCharsInString(string str) + { + // Assumption: input contains a well-formed base64 string with no whitespace. + + // base64 guaranteed have 0 - 2 padding characters. + if (str[str.Length - 1] == '=') + { + if (str[str.Length - 2] == '=') + { + return 2; + } + return 1; + } + return 0; + } + + private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength) + { + switch (inputLength % 4) + { + case 0: + return 0; + case 2: + return 2; + case 3: + return 1; + default: + throw new FormatException("TODO: Malformed input."); + } + } + + private static void ValidateParameters(int bufferLength, int offset, int count) + { + if (offset < 0) + { + throw new ArgumentOutOfRangeException("offset"); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException("count"); + } + if (bufferLength - offset < count) + { + throw new ArgumentException("Invalid offset / length."); + } + } + } +} diff --git a/src/Microsoft.AspNet.WebUtilities/project.json b/src/Microsoft.AspNet.WebUtilities/project.json index fcb9fea61d..74757f774a 100644 --- a/src/Microsoft.AspNet.WebUtilities/project.json +++ b/src/Microsoft.AspNet.WebUtilities/project.json @@ -1,14 +1,15 @@ { - "version": "1.0.0-*", - "dependencies": { - "Microsoft.AspNet.Http": "1.0.0-*" - }, - "frameworks": { - "aspnet50": {}, - "aspnetcore50": { - "dependencies": { - "System.Runtime": "4.0.20-beta-*" - } + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Http": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { + "dependencies": { + "System.Diagnostics.Debug": "4.0.10-beta-*", + "System.Runtime": "4.0.20-beta-*" + } + } } - } } diff --git a/test/Microsoft.AspNet.WebUtilities.Tests/WebEncodersTests.cs b/test/Microsoft.AspNet.WebUtilities.Tests/WebEncodersTests.cs new file mode 100644 index 0000000000..9813a780c3 --- /dev/null +++ b/test/Microsoft.AspNet.WebUtilities.Tests/WebEncodersTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Xunit; + +namespace Microsoft.AspNet.WebUtilities +{ + public class WebEncodersTests + { + [Theory] + [InlineData("", 1, 0)] + [InlineData("", 0, 1)] + [InlineData("0123456789", 9, 2)] + [InlineData("0123456789", Int32.MaxValue, 2)] + [InlineData("0123456789", 9, -1)] + public void Base64UrlDecode_BadOffsets(string input, int offset, int count) + { + // Act & assert + Assert.ThrowsAny(() => + { + var retVal = WebEncoders.Base64UrlDecode(input, offset, count); + }); + } + + [Theory] + [InlineData("x")] + [InlineData("(x)")] + public void Base64UrlDecode_MalformedInput(string input) + { + // Act & assert + Assert.Throws(() => + { + var retVal = WebEncoders.Base64UrlDecode(input); + }); + } + + [Theory] + [InlineData("", "")] + [InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")] + [InlineData("123456qwerty++//X+/xxw==", "123456qwerty--__X-_xxw")] + [InlineData("123456qwerty++//X+/xxw0=", "123456qwerty--__X-_xxw0")] + public void Base64UrlEncode_And_Decode(string base64Input, string expectedBase64Url) + { + // Arrange + byte[] input = new byte[3].Concat(Convert.FromBase64String(base64Input)).Concat(new byte[2]).ToArray(); + + // Act & assert - 1 + string actualBase64Url = WebEncoders.Base64UrlEncode(input, 3, input.Length - 5); // also helps test offsets + Assert.Equal(expectedBase64Url, actualBase64Url); + + // Act & assert - 2 + // Verify that values round-trip + byte[] roundTripped = WebEncoders.Base64UrlDecode("xx" + actualBase64Url + "yyy", 2, actualBase64Url.Length); // also helps test offsets + string roundTrippedAsBase64 = Convert.ToBase64String(roundTripped); + Assert.Equal(roundTrippedAsBase64, base64Input); + } + + [Theory] + [InlineData(0, 1, 0)] + [InlineData(0, 0, 1)] + [InlineData(10, 9, 2)] + [InlineData(10, Int32.MaxValue, 2)] + [InlineData(10, 9, -1)] + public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count) + { + // Arrange + byte[] input = new byte[inputLength]; + + // Act & assert + Assert.ThrowsAny(() => + { + var retVal = WebEncoders.Base64UrlEncode(input, offset, count); + }); + } + } +}