Skip to content

Base64Url Encoding & Decoding

Aleksandar Gyonov edited this page Feb 16, 2024 · 6 revisions

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.

Helper implementation in current package

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.

Methods references

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);

Sample usage

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.

Interesting sample with large files and pipes

Well, interesting sample with large files and detached signatures.

You see, with JSON Web Signatures and JAdES you basically (on several places) have to:

  1. Base64Url encode the data
  2. 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 :).

  1. No duplication of data, no copy of the whole file into the memory. Read portions of the big file in modest memory buffers.
  2. No sequential processing. You basically Base64Url encode and calculate HASH in parallel.

But you do need this additional async overloads in Base64Url helper class!