As part of my work with ScottBrady.IdentityModel, I’ve had the chance to play with XChaCha20-Poly1305. Despite sounding a bit silly and being a pain to type, XChaCha20-Poly1305 is a useful symmetric encryption algorithm that offers an alternative to the AES we know and love.
In this article, I am going to give a high-level overview of ChaCha20, Poly1305, and XChaCha20-Poly1305. This will include some code samples using a libsodium implementation in .NET, and a silly “rolling your own” implementation to help demonstrate the differences between ChaCha20-Poly1305 and XChaCha20-Poly1305.
XChaCha20-Poly1305
XChaCha20-Poly1305 is made up of a few different moving parts, combined to provide an effective encryption algorithm. Let’s take a quick look at their properties and their reason for existing.
💃 ChaCha20
Defined in RFC 8439, ChaCha20 was created as an alternative to AES and is a refinement of Salsa20. While AES may be super-fast thanks to dedicated hardware, ChaCha20 was designed to be a high-speed stream cipher that is faster than AES on software-only implementations. ChaCha20 also acts as a “standby cipher”, offering a viable, widely supported alternative to AES that is ready to use if a weakness in AES is discovered.
ChaCha20 uses a 256-bit (32-byte) key and a 96-bit (12-byte) nonce (aka Initialization Vector). Other variants exist, but that’s the IETF definition.
The ChaCha20 cipher does not use lookup tables (think of the S-box used by AES), is not sensitive to timing attacks, and is designed to provide 256-bit security.
🦜 Poly1305
Again defined in RFC 8439, Poly1305 is a one-time authenticator that takes a 256-bit (32-byte), one-time key, and creates a 128-bit (16-byte) tag for a message (a Message Authentication Code (MAC)).
You can use any keyed function to pseudorandomly generate the one-time key used by Poly1305, including ChaCha20 or AES, by using the key and the nonce to generate the one-time key. The procedure to do this is again defined in RFC 8439.
Unlike HMAC, Poly1305 can only use a key once, meaning the two are not interchangeable.
💃🦜 ChaCha20-Poly1305
Combine the two, and you get the ChaCha20-Poly1305 Authenticated Encryption with Associated Data (AEAD) construct. This construct produces a ciphertext that is the same length as the original plaintext, plus a 128-bit tag.
ChaCha20-Poly1305 is fairly popular; you’ll find implementations in most crypto libraries, such as libsodium and Bouncy Castle, and in use as a TLS ciphersuite.
❎💃🦜 XChaCha20-Poly1305
Unfortunately, ChaCha20-Poly1305’s 96-bit nonce doesn’t lend itself too well to accidental nonce reuse when you start encrypting a large number of messages with the same long-lived key.
eXtended-nonce ChaCha (XChaCha) solves this by taking the existing ChaCha20 cipher suite and extends the nonce from 96-bits (12-bytes) to 192-bits (24-bytes), while also defining an algorithm for calculating a subkey from the nonce and key using HChaCha20, further reducing the security implications of nonce reuse.
By upping the nonce size to 192-bits, the specification author estimates that 2^80 messages can safely be sent using a single key. This reduces the chance of nonce reuse while not sacrificing speed.
While XChaCha20-Poly1305 implementations already exist, it is currently still being standardized. At the time of writing, this draft has expired, so I’m not sure about the status of this specification, or the expired PASETO draft that was waiting for this algorithm to be standardized.
However, using XChaCha20-Poly1305 is perfectly acceptable, and you can see XChaCha20-Poly1305 in use with popular JWT alternatives such as Branca and PASETO in my IdentityModel library.
XChaCha20-Poly1305 in .NET using libsodium
Let’s see how to do this in .NET using libsodium. Libsodium is a popular cryptography library written in C, with wrappers available in many programming languages.
From what I can tell, the most up to date wrapper that works with .NET Core and .NET is libsodium-core. So, let’s install that now.
dotnet add package Sodium.Core
First, you’ll need a 32-byte key. You can bring your own, but for now, let’s new one up using .NET’s cryptographic random number generator:
var key = new byte[32];
RandomNumberGenerator.Create().GetBytes(key);
Next, you’ll need a 24-byte nonce.
Again, you can generate this using .NET’s cryptographic random number generator.
Libsodium does have its own random number, which in this instance, is available to us through SecretAeadXChaCha20Poly1305.GenerateNonce()
.
As long you use one of those, I don’t see an issue (XChaCha20 is designed to account for nonce misuse).
var nonce = new byte[24];
RandomNumberGenerator.Create().GetBytes(nonce);
For the final input, you’ll need your plaintext message that you want to encrypt. I’ll use some MF DOOM lyrics.
var message = Encoding.UTF8.GetBytes("Got more soul than a sock with a hole");
And now, you can create the ciphertext.
var ciphertext = SecretAeadXChaCha20Poly1305.Encrypt(plaintext, nonce, key);
This will return a ciphertext with the same length as your plaintext, plus 16-bytes at the end for the tag.
Under the hood, this calls libsodium’s crypto_aead_xchacha20poly1305_ietf_encrypt
function, which you can read more about in the libsodium documentation.
Decryption is much the same, where we take the ciphertext, the nonce, and the key and call into libsodium’s crypto_aead_xchacha20poly1305_ietf_decrypt
function.
var plaintext = SecretAeadXChaCha20Poly1305.Decrypt(ciphertext, nonce, key);
Put it all together in a console app, base64 encode it and you’ll end up with something like this:
Key: hOgkbL66iBftqIRgkVnL9vB7zmxvvbtT2KeW9snWe5k=
Nonce (IV): TC4etSvNDA/8QSEEwjJNAmUd4oVd4f9h
Libsodium Ouput: PTruPyd0yCJza8mdcZS56RHrSmlclCl4GWW9TsO4RoDSbwd4amxgHbsfdU7Kbr4MtJltqW8=
Libsodium Decrypted message: Got more soul than a sock with a hole
Full .NET Implementation of ChaCha20
This example used libsodium; however, there is an implementation of ChaCh20 written fully in .NET called NaCl.Core. I found this library super useful when learning this algorithm, and the testing on it isn’t too bad at all. Not only does it use the test vectors from the various IETF specifications, but it also uses tests from Google’s Project Wycheproof.
It’s worth checking this library when considering an XChaCha20-Poly1305 implementation or if you are curious about how the ChaCha20 internals look in C#.
“Rolling your own crypto” with XChaCha20-Poly1305 in .NET
Now that you’ve seen how to use a production-ready implementation let’s see how to “roll your own crypto” by taking an existing implementation of ChaCha20-Poly1305 and creating XChaCha20-Poly1305.
This will implement the following pseudo code from the XChaCha20-Poly1305 draft specification:
xchacha20_encrypt(key, nonce, plaintext, blk_ctr = 0):
subkey = hchacha20(key, nonce[0:15])
chacha20_nonce = "\x00\x00\x00\x00" + nonce[16:23]
return chacha20_encrypt(subkey, chacha20_nonce, plaintext, blk_ctr)
So, to implement XChaCha20-Poly1305 on top of ChaCha20-Poly1305, you will need to implement HChaCha20, which in turn includes implementing the ChaCha20 block function. For our ChaCha20-Poly1305 implementation, we will use Bouncy Castle.
I’m including this section because I found it useful when understanding some of the internals of ChaCha20, but either way, this isn’t really rolling your own crypto. You’re implementing a specification and using test vectors, rather than creating a new algorithm.
HChaCha20 in .NET
First, you will need to implement HChaCha20 so that you can generate a subkey from your key and nonce. By creating a subkey from the key and a part of the nonce, you reduce the effect nonce reuse has on the security of the algorithm.
HChaCha20 has three steps: creating the initial state, performing the 20 ChaCha rounds, and parsing the subkey.
Initial State
The initial state involves setting up a ChaCha20 state that consists of some spec-defined values, your key, and the first 32-bits of the nonce.
public static uint[] CreateInitialState(byte[] key, byte[] nonce)
{
var state = new uint[16];
// set HChaCha20 constant
var constant = new uint[] {0x61707865, 0x3320646E, 0x79622D32, 0x6B206574};
Array.Copy(constant, state, constant.Length);
// set key
var keyState = ToUint32LittleEndian(key, 8);
Array.Copy(keyState, 0, state, 4, keyState.Length);
// set nonce
var nonceState = ToUint32LittleEndian(nonce, 4);
Array.Copy(nonceState, 0, state, state.Length - 4, nonceState.Length);
return state;
}
private static uint[] ToUint32LittleEndian(byte[] bytes, int outputLength)
{
var pos = 0;
var output = new uint[outputLength];
using (var ms = new MemoryStream(bytes))
{
while (pos != outputLength)
{
var temp = new byte[4];
ms.Read(temp, 0, 4);
output[pos] = BinaryPrimitives.ReadUInt32LittleEndian(temp);
pos += 1;
}
}
return output;
}
ChaCha20 Quarter Rounds
Next up, you’ll need to run your initial state through the ChaCha20 block function. This means you’ll need to go back to RFC 8439 and implement the ChaCha quarter round and the ChaCha20 block function. I relied upon the existing C# implementation from David De Smet for this step.
public static void PerformRounds(uint[] state)
{
for (var i = 0; i < 10; i++)
{
ChaCha20.QuarterRound(ref state[0], ref state[4], ref state[8], ref state[12]);
ChaCha20.QuarterRound(ref state[1], ref state[5], ref state[9], ref state[13]);
ChaCha20.QuarterRound(ref state[2], ref state[6], ref state[10], ref state[14]);
ChaCha20.QuarterRound(ref state[3], ref state[7], ref state[11], ref state[15]);
ChaCha20.QuarterRound(ref state[0], ref state[5], ref state[10], ref state[15]);
ChaCha20.QuarterRound(ref state[1], ref state[6], ref state[11], ref state[12]);
ChaCha20.QuarterRound(ref state[2], ref state[7], ref state[8], ref state[13]);
ChaCha20.QuarterRound(ref state[3], ref state[4], ref state[9], ref state[14]);
}
}
// Adapted from https://github.com/daviddesmet/NaCl.Core
public static class ChaCha20
{
public static void QuarterRound(ref uint a, ref uint b, ref uint c, ref uint d)
{
a += b; d = RotateLeft(d ^ a, 16);
c += d; b = RotateLeft(b ^ c, 12);
a += b; d = RotateLeft(d ^ a, 8);
c += d; b = RotateLeft(b ^ c, 7);
}
private static uint RotateLeft(uint value, int offset)
{
return (value << offset) | (value >> (32 - offset));
}
}
Parsing the Subkey
The HChaCha20 subkey itself is only the first 128-bits (16-bytes) and the last 128-bits. So, the final step is to use the previous code, parse the subkey, and return the subkey in a format .NET APIs are a bit more comfortable with.
public static class HChaCha20
{
public static byte[] CreateSubkey(byte[] key, byte[] nonce)
{
var state = CreateInitialState(key, nonce);
PerformRounds(state);
return FromUint32LittleEndian(new[]
{
state[0], state[1], state[2], state[3],
state[12], state[13], state[14], state[15],
}, 32);
}
private static byte[] FromUint32LittleEndian(uint[] input, int outputLength)
{
var output = new byte[outputLength];
for (var i = 0; i < input.Length; i++)
{
var u = input[i];
var temp = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(temp, u);
Array.Copy(temp, 0, output, i * 4, temp.Length);
}
return output;
}
}
XChaCha20-Poly1305 in .NET using Bouncy Castle
For the implementation of ChaCha20-Poly1305, let’s use Bouncy Castle.
This is a bit more involved than libsodium (which would be the one-liner of SecretAeadChaCha20Poly1305IETF.Encrypt(plaintext, chaChaNonce, subkey, aad)
), but I think it’s worthwhile seeing how to this using both of the major C# crypto libraries.
So, here’s the final implementation. The nonce passed into the ChaCha20-Poly1305 implementation is 4 null bytes, followed by the final 18 bytes of your original nonce (the bytes you didn’t use for generating the subkey).
For Bouncy Castle to handle Additional Authenticated Data (AAD), you’ll need to call ProcessAadBytes
after initializing the cipher, but before you start processing the message.
public static byte[] Encrypt(byte[] key, byte[] nonce, byte[] plaintext, byte[] aad = null)
{
if (key.Length != 32) throw new ArgumentException("Key must be 32 bytes", nameof(key));
if (nonce.Length != 24) throw new ArgumentException("Nonce must be 24 bytes", nameof(nonce));
// subkey (hchacha20(key, nonce[0:15]))
var subkey = HChaCha20.CreateSubkey(key, nonce);
// nonce (chacha20_nonce = "\x00\x00\x00\x00" + nonce[16:23])
var chaChaNonce = new byte[12];
Array.Copy(new byte[] {0, 0, 0, 0}, chaChaNonce, 4);
Array.Copy(nonce, 16, chaChaNonce, 4, 8);
// chacha20_encrypt(subkey, chacha20_nonce, plaintext, blk_ctr)
var ciphertext = new byte[plaintext.Length + 16];
var keyMaterial = new KeyParameter(subkey);
var parameters = new ParametersWithIV(keyMaterial, chaChaNonce);
var chaCha20Poly1305 = new ChaCha20Poly1305();
chaCha20Poly1305.Init(true, parameters);
// if aditional data present
if (aad != null)
{
chaCha20Poly1305.ProcessAadBytes(aad, 0, aad.Length);
}
var len = chaCha20Poly1305.ProcessBytes(plaintext, 0, plaintext.Length, ciphertext, 0);
chaCha20Poly1305.DoFinal(ciphertext, len);
return ciphertext;
}
For decryption, it’s almost exactly the same code.
The decryption function is the same pseudocode you saw before; however, since you are using Bouncy Castle, you’ll need to account for the difference in length between the ciphertext and the plaintext (the ciphertext we returned above included the tag).
In my sample code, I ended up copying Bouncy Castle and using an isEncryption
flag to make the code reusable while accounting for this minor difference.
If you run both this and the previous libsodium eample using the same key and nonce, then you should see the same ciphertext and tag.
Key: hOgkbL66iBftqIRgkVnL9vB7zmxvvbtT2KeW9snWe5k=
Nonce (IV): TC4etSvNDA/8QSEEwjJNAmUd4oVd4f9h
Libsodium Ouput: PTruPyd0yCJza8mdcZS56RHrSmlclCl4GWW9TsO4RoDSbwd4amxgHbsfdU7Kbr4MtJltqW8=
BouncyDancing Ouput: PTruPyd0yCJza8mdcZS56RHrSmlclCl4GWW9TsO4RoDSbwd4amxgHbsfdU7Kbr4MtJltqW8=
Source Code
You can find a simple console app for all of the above code on my GitHub samples repo. This app shows XChaCha20-Poly1305 in action with both libsodium and my implementation, which I have dubbed “Bouncy Dancing”.