-
Notifications
You must be signed in to change notification settings - Fork 2
Base64Url Encoding & Decoding
The JSON Web Signatures RFC 7515 heavily relay on a so called Base64Url encoding / decoding of binary data.
Base64Url is based upon standard Base64, but with character changes and without = padding. For details you may have a look at Appendix C, RFC 7515 of the specification.
In the core library there is an implementation of the Base64Url - the class:
CryptoEx.Utils.Base64UrlEncoder
It provides basically 2 (two) methods:
- Encode
- Decode
Also, it provides async
version of the above methods and some helpful overloads. The implementation is intended to be as performance optimized as author's abilities to optimize it :).
Especially important is to use 'async' version when encoding / decoding large external files, for signing / verifying in detached mode, using piped streams! Check one of the examples below.
Here is a list of methods:
/// <summary>
/// Converts the specified string, base-64-url encoded to bytes.</summary>
/// <param name="str">base64Url encoded string.</param>
/// <returns>UTF8 bytes.</returns>
public static byte[] Decode(ReadOnlySpan<char> str);
/// <summary>
/// Converts the specified string, base-64-url encoded to bytes.</summary>
/// <param name="str">TexReader with base64Url encoded string.</param>
/// <param name="result">The Stream to write the bytes to. Result stream - can be piped!</param>
public static void Decode(TextReader str, Stream result);
/// <summary>
/// Converts the specified string, base-64-url encoded to bytes.</summary>
/// <param name="str">TextReader with base64Url encoded string.</param>
/// <param name="result">The Stream to write the bytes to. Result stream - can be piped!</param>
public async static Task DecodeAsync(TextReader str, Stream result, CancellationToken ct = default);
/// <summary>
/// The following functions perform base64url encoding .
/// </summary>
/// <param name="arg">The byte array to encode</param>
/// <returns>Base64Url encoding as string</returns>
public static string Encode(ReadOnlySpan<byte> arg);
/// <summary>
/// The following functions perform base64url encoding .
/// </summary>
/// <param name="arg">The Stream to encode</param>
/// <param name="result">The Writer to write the Base64Url to</param>
public static void Encode(Stream arg, TextWriter result);
/// <summary>
/// The following functions perform base64url encoding, as ASCII bytes.
/// </summary>
/// <param name="arg">The Stream to encode</param>
/// <param name="result">The Stream to write the Base64Url to, as ASCII bytes. Result stream - can be piped!</param>
public static void Encode(Stream arg, Stream result);
/// <summary>
/// The following functions perform base64url encoding.
/// </summary>
/// <param name="arg">The Stream to encode</param>
/// <param name="result">The Writer to write the Base64Url to</param>
public async static Task EncodeAsync(Stream arg, TextWriter result, CancellationToken ct = default);
/// <summary>
/// The following functions perform base64url encoding, as ASCII bytes.
/// </summary>
/// <param name="arg">The Stream to encode</param>
/// <param name="result">The Stream to write the Base64Url to, as ASCII bytes. Result stream - can be piped!</param>
public async static Task EncodeAsync(Stream arg, Stream result, CancellationToken ct = default);
Basic usage in general case is quite straightforward (An exempt from one of the test methods):
// Encode
string result = Base64UrlEncoder.Encode(data);
// Decode
byte[] data = Base64UrlEncoder.Decode(base64urlEncoded);
For examples with full list of methods you can look at the accompanying test project CryptoEx.Tests, class CryptoEx.Tests.TestBase64Url.
Well, interesting sample with large files and detached signatures.
You see, with JSON Web Signatures and JAdES you basically (on several places) have to:
- Base64Url encode the data
- Calculate HASH on Base64Url encoded data
and if you have a large file (several gigabytes or even just dozens megabytes) it is not smart to do it in-memory :). So, there shall be streams. You obviously can use your local disk, but then on Step 1 you will duplicate the size of the file on disk and additionally you will perform the above calculation in sequence.
Well, with .NET there is another option - System.IO.Pipes. Check it out:
// Hash attachemnt
using (HashAlgorithm hAlg = SHA512.Create())
using (AnonymousPipeServerStream apss = new(PipeDirection.In))
using (AnonymousPipeClientStream apcs = new(PipeDirection.Out, apss.GetClientHandleAsString())) {
_ = Task.Run(async () =>
{
try {
// Encode
await Base64UrlEncoder.EncodeAsync(attachement, apcs);
} finally {
// Close the pipe
apcs.Close(); // To avoid blocking of the pipe.
}
});
hashedData = await hAlg.ComputeHashAsync(apss); // Read from the pipe. Blocks until the pipe is closed (Upper Task ends).
}
Nice :).
- No duplication of data, no copy of the whole file into the memory. Read portions of the big file in modest memory buffers.
- No sequential processing. You basically Base64Url encode and calculate HASH in parallel.
But you do need this additional async
overloads in Base64Url helper class!
CryptoEx