Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for explanatory text on PEM decoding #586

Merged
merged 1 commit into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading