Skip to content

Commit

Permalink
Fix Base64 decoding when whitespaces occur (#571)
Browse files Browse the repository at this point in the history
* Fix Base64 decoding when whitespaces occur

* Fix JWK validation of the field 'x5c'. This field is b64-encoded and not b64-url encoded.
  • Loading branch information
ycrumeyrolle authored Aug 16, 2021
1 parent b899a2f commit 13ec96e
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 79 deletions.
92 changes: 55 additions & 37 deletions src/JsonWebToken/Base64.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,28 +79,46 @@ public static int Decode(ReadOnlySpan<byte> base64, Span<byte> data)
public static OperationStatus Decode(ReadOnlySpan<byte> base64, Span<byte> data, out int bytesConsumed, out int bytesWritten)
{
int lastWhitespace = base64.LastIndexOfAny(WhiteSpace);
if (lastWhitespace != -1)
if (lastWhitespace == -1)
{
return gfoidl.Base64.Base64.Default.Decode(base64, data, out bytesConsumed, out bytesWritten);
}
else
{
byte[]? utf8ArrayToReturn = null;
Span<byte> utf8Data = base64.Length > Constants.MaxStackallocBytes
? (utf8ArrayToReturn = ArrayPool<byte>.Shared.Rent(base64.Length))
: stackalloc byte[Constants.MaxStackallocBytes];
try
{
int firstWhitespace = base64.IndexOfAny(WhiteSpace);
int length = 0;
int i = 0;
for (; i <= lastWhitespace; i++)
Span<byte> buffer = utf8Data;
if (firstWhitespace != lastWhitespace)
{
var current = base64[i];
if (!IsWhiteSpace(current))
while (firstWhitespace != -1)
{
utf8Data[length++] = current;
base64.Slice(0, firstWhitespace).CopyTo(buffer);
buffer = buffer.Slice(firstWhitespace);
length += firstWhitespace;

// Skip whitespaces
int i = firstWhitespace;
while (++i < base64.Length && IsWhiteSpace(base64[i])) ;

base64 = base64.Slice(i);
firstWhitespace = base64.IndexOfAny(WhiteSpace);
}
}

for (; i < base64.Length; i++)
//// Copy the remaining
base64.CopyTo(buffer);
length += base64.Length;
}
else
{
utf8Data[length++] = base64[i];
base64.Slice(0, firstWhitespace).CopyTo(buffer);
base64.Slice(firstWhitespace + 1).CopyTo(buffer.Slice(firstWhitespace));
length = base64.Length - 1;
}

return gfoidl.Base64.Base64.Default.Decode(utf8Data.Slice(0, length), data, out bytesConsumed, out bytesWritten);
Expand All @@ -113,16 +131,13 @@ public static OperationStatus Decode(ReadOnlySpan<byte> base64, Span<byte> data,
}
}
}
else
{
return gfoidl.Base64.Base64.Default.Decode(base64, data, out bytesConsumed, out bytesWritten);
}
}

private static bool IsWhiteSpace(byte c)
=> c == ' ' || (c >= '\t' && c <= '\r');

private static ReadOnlySpan<byte> WhiteSpace => new byte[] { (byte)' ', (byte)'\t', (byte)'\r', (byte)'\n', (byte)'\v', (byte)'\f' };
private static ReadOnlySpan<byte> WhiteSpace
=> new byte[] { (byte)' ', (byte)'\t', (byte)'\n', (byte)'\v', (byte)'\f', (byte)'\r' };

/// <summary>Encodes a span of UTF-8 text into a span of bytes.</summary>
/// <returns>The number of the bytes written to <paramref name="base64"/>.</returns>
Expand Down Expand Up @@ -225,33 +240,36 @@ internal static unsafe bool IsBase64String(ReadOnlySpan<char> value)

static bool IsValidBase64Char(char value)
{
if (value > byte.MaxValue)
bool result = false;
if (value <= byte.MaxValue)
{
return false;
}

byte byteValue = (byte)value;
byte byteValue = (byte)value;

// 0-9
if (byteValue >= (byte)'0' && byteValue <= (byte)'9')
{
return true;
}

// + or /
if (byteValue == (byte)'+' || byteValue == (byte)'/')
{
return true;
}

// a-z or A-Z
byteValue |= 0x20;
if (byteValue >= (byte)'a' && byteValue <= (byte)'z')
{
return true;
// 0-9
if (byteValue >= (byte)'0' && byteValue <= (byte)'9')
{
result = true;
}
else
{
// a-z or A-Z
byte letter = (byte)(byteValue | 0x20);
if (letter >= (byte)'a' && letter <= (byte)'z')
{
result = true;
}
else
{
// + or / or whitespaces
if (byteValue == (byte)'+' || byteValue == (byte)'/' || IsWhiteSpace(byteValue))
{
result = true;
}
}
}
}

return false;
return result;
}
}
}
Expand Down
51 changes: 27 additions & 24 deletions src/JsonWebToken/Base64Url.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,33 +210,36 @@ internal static unsafe bool IsBase64UrlString(ReadOnlySpan<char> value)

static bool IsValidBase64UrlChar(char value)
{
if (value > byte.MaxValue)
bool result = false;
if (value <= byte.MaxValue)
{
return false;
}

byte byteValue = (byte)value;

// 0-9
if (byteValue >= (byte)'0' && byteValue <= (byte)'9')
{
return true;
}

// - or _
if (byteValue == (byte)'-' || byteValue == (byte)'_')
{
return true;
}

// a-z or A-Z
byteValue |= 0x20;
if (byteValue >= (byte)'a' && byteValue <= (byte)'z')
{
return true;
byte byteValue = (byte)value;

// 0-9
if (byteValue >= (byte)'0' && byteValue <= (byte)'9')
{
result = true;
}
else
{
// a-z or A-Z
byte letter = (byte)(byteValue | 0x20);
if (letter >= (byte)'a' && letter <= (byte)'z')
{
result = true;
}
else
{
// - or _
if (byteValue == (byte)'-' || byteValue == (byte)'_')
{
result = true;
}
}
}
}

return false;
return result;
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/JsonWebToken/Jwk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,7 +1096,7 @@ public static void Validate(string json)
throw new JwkValidationException($"Invalid '{JwkParameterNames.X5c}' item. Must be of type 'String'. Value '{item.GetRawText()}' is of type '{item.ValueKind}'.");
}

if (Base64Url.IsBase64UrlString(item.GetString()!))
if (!Base64.IsBase64String(item.GetString()!))
{
throw new JwkValidationException($"Invalid '{JwkParameterNames.X5c}' value '{item.GetString()}'. Must be a valid base64 encoded string.");
}
Expand All @@ -1106,7 +1106,6 @@ public static void Validate(string json)
CheckOptionalBase64UrlMember(document, JwkParameterNames.X5t, 160);
CheckOptionalBase64UrlMember(document, JwkParameterNames.X5tS256, 256);
CheckOptionalStringMember(document, JwkParameterNames.X5u);

}
catch (JsonException e)
{
Expand All @@ -1125,7 +1124,7 @@ static void CheckRequiredBase64UrlMember(JsonDocument document, JsonEncodedText
throw new JwkValidationException($"Invalid '{memberName}' member. Must be of type 'String'. Value '{value.GetRawText()}' is of type '{value.ValueKind}'.");
}

if (!Base64.IsBase64String(value.GetString()!))
if (!Base64Url.IsBase64UrlString(value.GetString()!))
{
throw new JwkValidationException($"Invalid '{memberName}' member. Must be a base64-URL encoded string.");
}
Expand Down
28 changes: 14 additions & 14 deletions test/JsonWebToken.Tests.Common/Tokens.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,42 +60,42 @@ private static IDictionary<string, Dictionary<string, object>> CreatePayloads()
{
{ "jti", "756E69717565206964656E746966696572"},
{ "iss", "https://idp.example.com/"},
{ "iat", 1508184845},
{ "iat", EpochTime.UtcNow },
{ "aud", "636C69656E745F6964"},
{ "exp", 1628184845},
{ "nbf", 1508184845}
{ "exp", EpochTime.UtcNow + EpochTime.OneDay },
{ "nbf", EpochTime.UtcNow }
}
},
{
"multiAud", new Dictionary<string, object>
{
{ "jti", "756E69717565206964656E746966696572"},
{ "iss", "https://idp.example.com/"},
{ "iat", 1508184845},
{ "iat", EpochTime.UtcNow },
{ "aud", new JArray("636C69656E745F6964", "X", "Y" ) },
{ "exp", 1628184845},
{ "nbf", 1508184845}
{ "exp", EpochTime.UtcNow + EpochTime.OneDay},
{ "nbf", EpochTime.UtcNow }
}
},
{
"nd-nbf", new Dictionary<string, object>
{
{ "jti", "756E69717565206964656E746966696572"},
{ "iss", "https://idp.example.com/"},
{ "iat", 1508184845},
{ "iat", EpochTime.UtcNow },
{ "aud", "636C69656E745F6964"},
{ "exp", 1628184845}
{ "exp", EpochTime.UtcNow + EpochTime.OneDay}
}
},
{
"medium", new Dictionary<string, object>
{
{ "jti", "756E69717565206964656E746966696572"},
{ "iss", "https://idp.example.com/"},
{ "iat", 1508184845},
{ "iat", EpochTime.UtcNow },
{ "aud", "636C69656E745F6964"},
{ "exp", 1628184845},
{ "nbf", 1508184845},
{ "exp", EpochTime.UtcNow + EpochTime.OneDay},
{ "nbf", EpochTime.UtcNow },
{ "claim1", "value1ABCDEFGH" },
{ "claim2", "value1ABCDEFGH" },
{ "claim3", "value1ABCDEFGH" },
Expand All @@ -119,10 +119,10 @@ private static IDictionary<string, Dictionary<string, object>> CreatePayloads()
{
{ "jti", "756E69717565206964656E746966696572" },
{ "iss", "https://idp.example.com/" },
{ "iat", 1508184845 },
{ "iat", EpochTime.UtcNow },
{ "aud", "636C69656E745F6964" },
{ "exp", 1628184845 },
{ "nbf", 1508184845},
{ "exp", EpochTime.UtcNow + EpochTime.OneDay },
{ "nbf", EpochTime.UtcNow },
{ "big_claim", Convert.ToBase64String(bigData) }
}
},
Expand Down
44 changes: 44 additions & 0 deletions test/JsonWebToken.Tests/Base64Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Text;
using Xunit;

namespace JsonWebToken.Tests
{
public class Base64Tests
{
[Theory]
[InlineData("", "")]
[InlineData("SGVsbG8=", "Hello")]
[InlineData("SGVsbG8gV29ybGQ=", "Hello World")]
[InlineData("SGVsbG8\tgV29ybGQ=", "Hello World")]
[InlineData("SGVsbG8\rgV29ybGQ=", "Hello World")]
[InlineData("SGVsbG8\ngV29ybGQ=", "Hello World")]
[InlineData("SGVsbG8\vgV29ybGQ=", "Hello World")]
[InlineData("SGVsbG8\fgV29ybGQ=", "Hello World")]
[InlineData("SGVsbG8 gV29ybGQ=", "Hello World")]
[InlineData(" SGVsbG8gV29ybGQ=", "Hello World")]
[InlineData("SG Vsb G8gV29ybGQ=", "Hello World")]
[InlineData("S G V s b G 8 g V 2 9 y b G Q =", "Hello World")]
[InlineData("S G V s b G8gV29ybGQ=", "Hello World")]
[InlineData(" S G V s b G8gV29ybGQ= ", "Hello World")]
[InlineData("SGV+bG8=", "He~lo")]
[InlineData("SGV/bG8=", "He\u007flo")]
public void Decode_Valid(string value, string expected)
{
var result = Base64.Decode(Encoding.UTF8.GetBytes(value));
Assert.NotNull(result);
Assert.Equal(Encoding.UTF8.GetBytes(expected), result);
}

[Theory]
[InlineData("SGVsbG8")]
[InlineData("SGVsbG8&")]
[InlineData("SGVsbG=8")]
[InlineData("S-/sbG8=")]
[InlineData("S+_sbG8=")]
public void Decode_Invalid(string value)
{
Assert.Throws<FormatException>(() => Base64.Decode(Encoding.UTF8.GetBytes(value)));
}
}
}
38 changes: 38 additions & 0 deletions test/JsonWebToken.Tests/Base64UrlTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Text;
using Xunit;

namespace JsonWebToken.Tests
{
public class Base64UrlTests
{
[Theory]
[InlineData("", "")]
[InlineData("SGVsbG8", "Hello")]
[InlineData("SGVsbG8gV29ybGQ", "Hello World")]
[InlineData("SGV-bG8", "He~lo")]
[InlineData("SGV_bG8", "He\u007flo")]
public void Decode_Valid(string value, string expected)
{
var result = Base64Url.Decode(Encoding.UTF8.GetBytes(value));
Assert.NotNull(result);
Assert.Equal(Encoding.UTF8.GetBytes(expected), result);
}

[Theory]
[InlineData("SGVsbG8=")]
[InlineData("SGVsbG8 ")]
[InlineData(" SGVsbG8")]
[InlineData("SGV sbG8")]
public void Decode_Invalid(string value)
{
Assert.Throws<FormatException>(() => Base64Url.Decode(Encoding.UTF8.GetBytes(value)));
}

[Fact]
public void Decode_Null()
{
Assert.Throws<ArgumentNullException>(() => Base64Url.Decode((string)null));
}
}
}
Loading

0 comments on commit 13ec96e

Please sign in to comment.