Skip to content

Commit

Permalink
Add support for explanatory text on PEM decoding (#586)
Browse files Browse the repository at this point in the history
  • Loading branch information
ycrumeyrolle authored Jul 21, 2024
1 parent 89fa29b commit 516d0df
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 138 deletions.
145 changes: 64 additions & 81 deletions src/JsonWebToken/Base64.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace JsonWebToken
Expand All @@ -12,12 +11,12 @@ namespace JsonWebToken
public static class Base64
{
/// <summary>Decodes a span of UTF-8 base64-encoded text.</summary>
/// <remarks>This method allocate an array of bytes. Use <see cref="Decode(ReadOnlySpan{byte}, Span{byte})"/> when possible.</remarks>
public static byte[] Decode(ReadOnlySpan<byte> base64)
/// <remarks>This method allocate an array of bytes. Use <see cref="Decode(ReadOnlySpan{byte}, Span{byte}, bool)"/> when possible.</remarks>
public static byte[] Decode(ReadOnlySpan<byte> base64, bool stripWhitespace = false)
{
var dataLength = GetArraySizeRequiredToDecode(base64.Length);
var data = new byte[dataLength];
int length = Decode(base64, data);
int length = Decode(base64, data, stripWhitespace);
if (length != dataLength)
{
data = data.AsSpan(0, length).ToArray();
Expand All @@ -29,29 +28,29 @@ public static byte[] Decode(ReadOnlySpan<byte> base64)
#if NETSTANDARD2_0
/// <summary>Decodes a string of UTF-8 base64-encoded text into a span of bytes.</summary>
/// <returns>The number of the bytes written to <paramref name="data"/>.</returns>
public static int Decode(string base64, Span<byte> data)
public static int Decode(string base64, Span<byte> data, bool stripWhitespace = false)
{
if (base64 is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.base64url);
}

return Decode(base64.AsSpan(), data);
return Decode(base64.AsSpan(), data, stripWhitespace);
}
#endif

/// <summary>Decodes a span of UTF-8 base64-encoded text into a span of bytes.</summary>
/// <returns>The number of the bytes written to <paramref name="data"/>.</returns>
public static int Decode(ReadOnlySpan<char> base64, Span<byte> data)
public static int Decode(ReadOnlySpan<char> base64, Span<byte> data, bool stripWhitespace = false)
{
byte[]? arrayToReturn = null;
var buffer = base64.Length > Constants.MaxStackallocBytes
? (arrayToReturn = ArrayPool<byte>.Shared.Rent(base64.Length))
: stackalloc byte[Constants.MaxStackallocBytes];
: stackalloc byte[base64.Length];
try
{
int length = Utf8.GetBytes(base64, buffer);
return Decode(buffer.Slice(0, length), data);
return Decode(buffer.Slice(0, length), data, stripWhitespace);
}
finally
{
Expand All @@ -64,9 +63,9 @@ public static int Decode(ReadOnlySpan<char> base64, Span<byte> data)

/// <summary>Decodes the span of UTF-8 base64-encoded text into a span of bytes.</summary>
/// <returns>The number of the bytes written to <paramref name="data"/>.</returns>
public static int Decode(ReadOnlySpan<byte> base64, Span<byte> data)
public static int Decode(ReadOnlySpan<byte> base64, Span<byte> data, bool stripWhitespace = false)
{
var status = Decode(base64, data, out _, out int bytesWritten);
var status = Decode(base64, data, out _, out int bytesWritten, stripWhitespace);
if (status != OperationStatus.Done)
{
ThrowHelper.ThrowOperationNotDoneException(status);
Expand All @@ -76,68 +75,55 @@ public static int Decode(ReadOnlySpan<byte> base64, Span<byte> data)
}

/// <summary>Decodes the span of UTF-8 base64-encoded text into binary data.</summary>
public static OperationStatus Decode(ReadOnlySpan<byte> base64, Span<byte> data, out int bytesConsumed, out int bytesWritten)
public static OperationStatus Decode(ReadOnlySpan<byte> base64, Span<byte> data, out int bytesConsumed, out int bytesWritten, bool stripWhitespace = false)
{
int lastWhitespace = base64.LastIndexOfAny(WhiteSpace);
if (lastWhitespace == -1)
if (stripWhitespace)
{
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 lastWhitespace = base64.LastIndexOfAny(WhiteSpace);
if (lastWhitespace != -1)
{
int firstWhitespace = base64.IndexOfAny(WhiteSpace);
int length = 0;
Span<byte> buffer = utf8Data;
if (firstWhitespace != lastWhitespace)
byte[]? utf8ArrayToReturn = null;
Span<byte> utf8Data = base64.Length > Constants.MaxStackallocBytes
? (utf8ArrayToReturn = ArrayPool<byte>.Shared.Rent(base64.Length))
: stackalloc byte[base64.Length];
try
{
while (firstWhitespace != -1)
int length = 0;
int i = 0;
for (; i <= lastWhitespace; i++)
{
base64.Slice(0, firstWhitespace).CopyTo(buffer);
buffer = buffer.Slice(firstWhitespace);
length += firstWhitespace;

// Skip whitespaces
int i = firstWhitespace;
while (++i < base64.Length && IsWhiteSpace(base64[i])) ;
var current = base64[i];
if (!IsWhiteSpace(current))
{
utf8Data[length++] = current;
}
}

base64 = base64.Slice(i);
firstWhitespace = base64.IndexOfAny(WhiteSpace);
for (; i < base64.Length; i++)
{
utf8Data[length++] = base64[i];
}

//// Copy the remaining
base64.CopyTo(buffer);
length += base64.Length;
return gfoidl.Base64.Base64.Default.Decode(utf8Data.Slice(0, length), data, out bytesConsumed, out bytesWritten);
}
else
finally
{
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);
}
finally
{
if (utf8ArrayToReturn != null)
{
ArrayPool<byte>.Shared.Return(utf8ArrayToReturn);
if (utf8ArrayToReturn != null)
{
ArrayPool<byte>.Shared.Return(utf8ArrayToReturn);
}
}
}
}

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)'\n', (byte)'\v', (byte)'\f', (byte)'\r' };
=> new byte[] { (byte)' ', (byte)'\t', (byte)'\r', (byte)'\n', (byte)'\v', (byte)'\f' };

/// <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 @@ -189,7 +175,7 @@ public static byte[] Encode(ReadOnlySpan<char> data)
int length = Utf8.GetMaxByteCount(data.Length);
var utf8Data = length > Constants.MaxStackallocBytes
? (utf8ArrayToReturn = ArrayPool<byte>.Shared.Rent(length))
: stackalloc byte[Constants.MaxStackallocBytes];
: stackalloc byte[length];

int written = Utf8.GetBytes(data, utf8Data);
return Encode(utf8Data.Slice(0, written));
Expand Down Expand Up @@ -240,36 +226,33 @@ internal static unsafe bool IsBase64String(ReadOnlySpan<char> value)

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

// 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;
}
}
}
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;
}

return result;
return false;
}
}
}
Expand Down
62 changes: 41 additions & 21 deletions src/JsonWebToken/Cryptography/PemParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ namespace JsonWebToken.Cryptography
{
internal static class PemParser
{
internal static ReadOnlySpan<char> BeginPrefix => new[] { '-', '-', '-', '-', '-', 'B', 'E', 'G', 'I', 'N', ' ' };
internal static ReadOnlySpan<char> EndSuffix => new[] { '-', '-', '-', '-', '-', 'E', 'N', 'D', ' ' };

public static AsymmetricJwk Read(string key)
{
if (key is null)
Expand All @@ -12,29 +15,46 @@ public static AsymmetricJwk Read(string key)
}

var data = key.AsSpan().Trim();
if (data.StartsWith(Pkcs8.PrivateKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs8.PrivateKeySuffix, StringComparison.Ordinal))
{
return Pkcs8.ReadPrivateKey(data);
}
else if (data.StartsWith(Pkcs8.PublicKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs8.PublicKeySuffix, StringComparison.Ordinal))
{
return Pkcs8.ReadPublicKey(data);
}
if (data.StartsWith(Pkcs1.PrivateRsaKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs1.PrivatRsaKeySuffix, StringComparison.Ordinal))
{
return Pkcs1.ReadRsaPrivateKey(data);
}
else if (data.StartsWith(Pkcs1.PublicRsaKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs1.PublicRsaKeySuffix, StringComparison.Ordinal))
int startOffset = data.IndexOf(BeginPrefix);
int endOffset = data.IndexOf(EndSuffix);

if (startOffset != -1 && endOffset != -1)
{
return Pkcs1.ReadRsaPublicKey(data);
}
if (data.Slice(startOffset + BeginPrefix.Length, Pkcs8.PrivateKeyPrefix.Length).SequenceEqual(Pkcs8.PrivateKeyPrefix) &&
data.Slice(endOffset + EndSuffix.Length, Pkcs8.PrivateKeySuffix.Length).SequenceEqual(Pkcs8.PrivateKeySuffix))
{
return Pkcs8.ReadPrivateKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs8.PrivateKeyPrefix.Length, endOffset - startOffset - Pkcs8.PrivateKeySuffix.Length - BeginPrefix.Length));
}
else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs8.PublicKeyPrefix.Length).SequenceEqual(Pkcs8.PublicKeyPrefix) &&
data.Slice(endOffset + EndSuffix.Length, Pkcs8.PublicKeySuffix.Length).SequenceEqual(Pkcs8.PublicKeySuffix))
{
return Pkcs8.ReadPublicKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs8.PublicKeyPrefix.Length, endOffset - startOffset - Pkcs8.PublicKeySuffix.Length - BeginPrefix.Length));
}
else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs1.PrivateRsaKeyLabel.Length).SequenceEqual(Pkcs1.PrivateRsaKeyLabel) &&
data.Slice(endOffset + EndSuffix.Length, Pkcs1.PrivateRsaKeyLabel.Length).SequenceEqual(Pkcs1.PrivateRsaKeyLabel))
{
return Pkcs1.ReadRsaPrivateKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs1.PrivateRsaKeyLabel.Length, endOffset - startOffset - Pkcs1.PrivateRsaKeyLabel.Length - BeginPrefix.Length));
}
else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs1.PublicRsaKeyLabel.Length).SequenceEqual(Pkcs1.PublicRsaKeyLabel) &&
data.Slice(endOffset + EndSuffix.Length, Pkcs1.PublicRsaKeyLabel.Length).SequenceEqual(Pkcs1.PublicRsaKeyLabel))
{
return Pkcs1.ReadRsaPublicKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs1.PublicRsaKeyLabel.Length, endOffset - startOffset - Pkcs1.PublicRsaKeyLabel.Length - BeginPrefix.Length));
}
#if SUPPORT_ELLIPTIC_CURVE
if (data.StartsWith(Pkcs1.PrivateECKeyPrefix, StringComparison.Ordinal) && data.EndsWith(Pkcs1.PrivateECKeySuffix, StringComparison.Ordinal))
{
return Pkcs1.ReadECPrivateKey(data);
}
else if (data.Slice(startOffset + BeginPrefix.Length, Pkcs1.PrivateECKeyLabel.Length).SequenceEqual(Pkcs1.PrivateECKeyLabel) &&
data.Slice(endOffset + EndSuffix.Length, Pkcs1.PrivateECKeyLabel.Length).SequenceEqual(Pkcs1.PrivateECKeyLabel))
{
return Pkcs1.ReadECPrivateKey(data.Slice(startOffset + BeginPrefix.Length + Pkcs1.PrivateECKeyLabel.Length, endOffset - startOffset - Pkcs1.PrivateECKeyLabel.Length - BeginPrefix.Length));
}
#endif
throw new ArgumentException("PEM-encoded key be contained within valid prefix and suffix.", nameof(key));
else
{
throw new ArgumentException($"PEM-encoded key of type {data.Slice(endOffset + EndSuffix.Length, data.Length - endOffset - EndSuffix.Length - 5).ToString()} is not supported.", nameof(key));
}
}


throw new ArgumentException("PEM-encoded key must be contained within valid prefix and suffix.", nameof(key));
}
}
}
}
Loading

0 comments on commit 516d0df

Please sign in to comment.