Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Commit 0f2b9b3

Browse files
Add Base64UrlEncode / Base64UrlDecode.
1 parent 0d27849 commit 0f2b9b3

File tree

3 files changed

+273
-11
lines changed

3 files changed

+273
-11
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Diagnostics;
6+
7+
namespace Microsoft.AspNet.WebUtilities
8+
{
9+
/// <summary>
10+
/// Contains utility APIs to assist with common encoding and decoding operations.
11+
/// </summary>
12+
public static class WebEncoders
13+
{
14+
/// <summary>
15+
/// Decodes a base64url-encoded string.
16+
/// </summary>
17+
/// <param name="input">The base64url-encoded input to decode.</param>
18+
/// <returns>The base64url-decoded form of the input.</returns>
19+
/// <remarks>
20+
/// The input must not contain any whitespace or padding characters.
21+
/// Throws FormatException if the input is malformed.
22+
/// </remarks>
23+
public static byte[] Base64UrlDecode([NotNull] string input)
24+
{
25+
return Base64UrlDecode(input, 0, input.Length);
26+
}
27+
28+
/// <summary>
29+
/// Decodes a base64url-encoded substring of a given string.
30+
/// </summary>
31+
/// <param name="input">A string containing the base64url-encoded input to decode.</param>
32+
/// <param name="offset">The position in <paramref name="input"/> at which decoding should begin.</param>
33+
/// <param name="count">The number of characters in <paramref name="input"/> to decode.</param>
34+
/// <returns>The base64url-decoded form of the input.</returns>
35+
/// <remarks>
36+
/// The input must not contain any whitespace or padding characters.
37+
/// Throws FormatException if the input is malformed.
38+
/// </remarks>
39+
public static byte[] Base64UrlDecode([NotNull] string input, int offset, int count)
40+
{
41+
ValidateParameters(input.Length, offset, count);
42+
43+
// Assumption: input is base64url encoded without padding and contains no whitespace.
44+
45+
// First, we need to add the padding characters back.
46+
int numPaddingCharsToAdd = GetNumBase64PaddingCharsToAddForDecode(count);
47+
char[] completeBase64Array = new char[checked(count + numPaddingCharsToAdd)];
48+
Debug.Assert(completeBase64Array.Length % 4 == 0, "Invariant: Array length must be a multiple of 4.");
49+
input.CopyTo(offset, completeBase64Array, 0, count);
50+
for (int i = 1; i <= numPaddingCharsToAdd; i++)
51+
{
52+
completeBase64Array[completeBase64Array.Length - i] = '=';
53+
}
54+
55+
// Next, fix up '-' -> '+' and '_' -> '/'
56+
for (int i = 0; i < completeBase64Array.Length; i++)
57+
{
58+
char c = completeBase64Array[i];
59+
if (c == '-')
60+
{
61+
completeBase64Array[i] = '+';
62+
}
63+
else if (c == '_')
64+
{
65+
completeBase64Array[i] = '/';
66+
}
67+
}
68+
69+
// Finally, decode.
70+
// If the caller provided invalid base64 chars, they'll be caught here.
71+
return Convert.FromBase64CharArray(completeBase64Array, 0, completeBase64Array.Length);
72+
}
73+
74+
/// <summary>
75+
/// Encodes an input using base64url encoding.
76+
/// </summary>
77+
/// <param name="input">The binary input to encode.</param>
78+
/// <returns>The base64url-encoded form of the input.</returns>
79+
public static string Base64UrlEncode([NotNull] byte[] input)
80+
{
81+
return Base64UrlEncode(input, 0, input.Length);
82+
}
83+
84+
/// <summary>
85+
/// Encodes an input using base64url encoding.
86+
/// </summary>
87+
/// <param name="input">The binary input to encode.</param>
88+
/// <param name="offset">The offset into <paramref name="input"/> at which to begin encoding.</param>
89+
/// <param name="count">The number of bytes of <paramref name="input"/> to encode.</param>
90+
/// <returns>The base64url-encoded form of the input.</returns>
91+
public static string Base64UrlEncode([NotNull] byte[] input, int offset, int count)
92+
{
93+
ValidateParameters(input.Length, offset, count);
94+
95+
// Special-case empty input
96+
if (count == 0)
97+
{
98+
return String.Empty;
99+
}
100+
101+
// We're going to use base64url encoding with no padding characters.
102+
// See RFC 4648, Sec. 5.
103+
char[] buffer = new char[GetNumBase64CharsRequiredForInput(count)];
104+
int numBase64Chars = Convert.ToBase64CharArray(input, offset, count, buffer, 0);
105+
106+
// Fix up '+' -> '-' and '/' -> '_'
107+
for (int i = 0; i < numBase64Chars; i++)
108+
{
109+
char ch = buffer[i];
110+
if (ch == '+')
111+
{
112+
buffer[i] = '-';
113+
}
114+
else if (ch == '/')
115+
{
116+
buffer[i] = '_';
117+
}
118+
else if (ch == '=')
119+
{
120+
// We've reached a padding character: truncate the string from this point
121+
return new String(buffer, 0, i);
122+
}
123+
}
124+
125+
// If we got this far, the buffer didn't contain any padding chars, so turn
126+
// it directly into a string.
127+
return new String(buffer, 0, numBase64Chars);
128+
}
129+
130+
private static int GetNumBase64CharsRequiredForInput(int inputLength)
131+
{
132+
int numWholeOrPartialInputBlocks = checked(inputLength + 2) / 3;
133+
return checked(numWholeOrPartialInputBlocks * 4);
134+
}
135+
136+
private static int GetNumBase64PaddingCharsInString(string str)
137+
{
138+
// Assumption: input contains a well-formed base64 string with no whitespace.
139+
140+
// base64 guaranteed have 0 - 2 padding characters.
141+
if (str[str.Length - 1] == '=')
142+
{
143+
if (str[str.Length - 2] == '=')
144+
{
145+
return 2;
146+
}
147+
return 1;
148+
}
149+
return 0;
150+
}
151+
152+
private static int GetNumBase64PaddingCharsToAddForDecode(int inputLength)
153+
{
154+
switch (inputLength % 4)
155+
{
156+
case 0:
157+
return 0;
158+
case 2:
159+
return 2;
160+
case 3:
161+
return 1;
162+
default:
163+
throw new FormatException("TODO: Malformed input.");
164+
}
165+
}
166+
167+
private static void ValidateParameters(int bufferLength, int offset, int count)
168+
{
169+
if (offset < 0)
170+
{
171+
throw new ArgumentOutOfRangeException("offset");
172+
}
173+
if (count < 0)
174+
{
175+
throw new ArgumentOutOfRangeException("count");
176+
}
177+
if (bufferLength - offset < count)
178+
{
179+
throw new ArgumentException("Invalid offset / length.");
180+
}
181+
}
182+
}
183+
}
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
{
2-
"version": "1.0.0-*",
3-
"dependencies": {
4-
"Microsoft.AspNet.Http": "1.0.0-*"
5-
},
6-
"frameworks": {
7-
"aspnet50": {},
8-
"aspnetcore50": {
9-
"dependencies": {
10-
"System.Runtime": "4.0.20-beta-*"
11-
}
2+
"version": "1.0.0-*",
3+
"dependencies": {
4+
"Microsoft.AspNet.Http": "1.0.0-*"
5+
},
6+
"frameworks": {
7+
"aspnet50": { },
8+
"aspnetcore50": {
9+
"dependencies": {
10+
"System.Diagnostics.Debug": "4.0.10-beta-*",
11+
"System.Runtime": "4.0.20-beta-*"
12+
}
13+
}
1214
}
13-
}
1415
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using Xunit;
7+
8+
namespace Microsoft.AspNet.WebUtilities
9+
{
10+
public class WebEncodersTests
11+
{
12+
[Theory]
13+
[InlineData("", 1, 0)]
14+
[InlineData("", 0, 1)]
15+
[InlineData("0123456789", 9, 2)]
16+
[InlineData("0123456789", Int32.MaxValue, 2)]
17+
[InlineData("0123456789", 9, -1)]
18+
public void Base64UrlDecode_BadOffsets(string input, int offset, int count)
19+
{
20+
// Act & assert
21+
Assert.ThrowsAny<ArgumentException>(() =>
22+
{
23+
var retVal = WebEncoders.Base64UrlDecode(input, offset, count);
24+
});
25+
}
26+
27+
[Theory]
28+
[InlineData("x")]
29+
[InlineData("(x)")]
30+
public void Base64UrlDecode_MalformedInput(string input)
31+
{
32+
// Act & assert
33+
Assert.Throws<FormatException>(() =>
34+
{
35+
var retVal = WebEncoders.Base64UrlDecode(input);
36+
});
37+
}
38+
39+
[Theory]
40+
[InlineData("", "")]
41+
[InlineData("123456qwerty++//X+/x", "123456qwerty--__X-_x")]
42+
[InlineData("123456qwerty++//X+/xxw==", "123456qwerty--__X-_xxw")]
43+
[InlineData("123456qwerty++//X+/xxw0=", "123456qwerty--__X-_xxw0")]
44+
public void Base64UrlEncode_And_Decode(string base64Input, string expectedBase64Url)
45+
{
46+
// Arrange
47+
byte[] input = new byte[3].Concat(Convert.FromBase64String(base64Input)).Concat(new byte[2]).ToArray();
48+
49+
// Act & assert - 1
50+
string actualBase64Url = WebEncoders.Base64UrlEncode(input, 3, input.Length - 5); // also helps test offsets
51+
Assert.Equal(expectedBase64Url, actualBase64Url);
52+
53+
// Act & assert - 2
54+
// Verify that values round-trip
55+
byte[] roundTripped = WebEncoders.Base64UrlDecode("xx" + actualBase64Url + "yyy", 2, actualBase64Url.Length); // also helps test offsets
56+
string roundTrippedAsBase64 = Convert.ToBase64String(roundTripped);
57+
Assert.Equal(roundTrippedAsBase64, base64Input);
58+
}
59+
60+
[Theory]
61+
[InlineData(0, 1, 0)]
62+
[InlineData(0, 0, 1)]
63+
[InlineData(10, 9, 2)]
64+
[InlineData(10, Int32.MaxValue, 2)]
65+
[InlineData(10, 9, -1)]
66+
public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count)
67+
{
68+
// Arrange
69+
byte[] input = new byte[inputLength];
70+
71+
// Act & assert
72+
Assert.ThrowsAny<ArgumentException>(() =>
73+
{
74+
var retVal = WebEncoders.Base64UrlEncode(input, offset, count);
75+
});
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)