Skip to content

XChaChaStream

Tom Auger edited this page Jun 25, 2018 · 2 revisions

Technical Details

Parameter Value
Encryption Algorithm XChaCha20
Authentication Algorithm Poly1305-MAC
Key Size 32 bytes
Nonce Size 24 bytes
Max Message Size 2^64-1 bytes

Description

The XChaChaStream class wraps the crypto_secretstream_xchacha20poly1305_* functions provided in libsodium. The following description is taken from the libsodium documentation [2018-05-30]:

This high-level API encrypts a sequence of messages, or a single message split into an arbitrary number of chunks, using a secret key, with the following properties:

  • Messages cannot be truncated, removed, reordered, duplicated or modified without this being detected by the decryption functions.
  • The same sequence encrypted twice will produce different ciphertexts.
  • An authentication tag is added to each encrypted message: stream corruption will be detected early, without having to read the stream until the end.
  • Each message can include additional data (ex: timestamp, protocol version) in the computation of the authentication tag.
  • Messages can have different sizes.
  • There are no practical limits to the total length of the stream, or to the total number of individual messages.

It [the API] transparently generates nonces and automatically handles key rotation.

Note: manual key ratcheting has not been implemented

Example - Encrypt/Decrypt a sequence of messages

The following example demonstrates how to encrypt/decrypt a pair of messages:

Encryption

using (var ciphertextStream = new MemoryStream())
using (var key = XChaChaKey.Generate())
{
    var block1 = Encoding.UTF8.GetBytes("The quick brown fox");
    var block2 = Encoding.UTF8.GetBytes("jumps over the lazy dog");

    using (var encryptionStream = new XChaChaStream(ciphertextStream, key, EncryptionMode.Encrypt))
    {
        encryptionStream.Write(block1);
        encryptionStream.WriteFinal(block2);
    }

    var ciphertext = ciphertextStream.ToArray();
}

If multiple blocks will be written to the stream then Write should be used for all the initial blocks and WriteFinal for the last block. If only a single block is being written, then just use WriteFinal (but then consider using a non stream cipher such as XChaChaAeadCipher).

Decryption

var ciphertext = Convert.FromBase64String("...");
var keyBytes = Convert.FromBase64String("v7ymK2liU1EX64LE/6lJYEA8uW9bjcbe3Y/TdjWQYQk=");
using (var ciphertextStream = new MemoryStream(ciphertext))
using (var key = new XChaChaKey(keyBytes))
{
    var block1 = new byte[19];
    var block2 = new byte[23];

    using (var decryptionStream = new XChaChaStream(ciphertextStream, key, EncryptionMode.Decrypt))
    {
        decryptionStream.Read(block1);
        decryptionStream.Read(block2);
    }
}

The plaintext must be read out of the decryption stream in the same way that it was written to the encryption stream. In this example the first block ("The quick brown fox") has length 19 bytes and the second block ("jumps over the lazy dog") has length 23 bytes. Therefore when decrypting we need to read out a block of length 19 bytes and then a block of length 23 bytes. If you need to encrypt variable length data and don't want to deal with remembering the block sizes, or splitting it into equal sized blocks, consider using XChaChaBufferedStream.

Example - Encrypt/Decrypt a file

The following examples demonstrate how to encrypt/decrypt a file in 128KB blocks:

Encryption

using (var inFileStream = File.OpenRead(@"C:\input.txt"))
using (var outFileStream = File.OpenWrite(@"C:\output.enc"))
using (var key = XChaChaKey.Generate())
using (var encryptionStream = new XChaChaStream(outFileStream, key, EncryptionMode.Encrypt))
{
    var buffer = new byte[128 * 1024];
    bool eof;

    do
    {
        var bytesRead = inFileStream.Read(buffer);
        eof = inFileStream.Position == inFileStream.Length;
        if (!eof)
        {
            encryptionStream.Write(buffer);
        }
        else
        {
            encryptionStream.WriteFinal(buffer, 0, bytesRead);
        }
    } while (!eof);
}

Decryption

var keyBytes = ...; // load the raw key
using (var inFileStream = File.OpenRead(@"C:\input.enc"))
using (var outFileStream = File.OpenWrite(@"C:\output.txt"))
using (var key = new XChaChaKey(keyBytes))
using (var decryptionStream = new XChaChaStream(inFileStream, key, EncryptionMode.Decrypt))
{
    var buffer = new byte[128 * 1024];
    do
    {
        var bytesRead = decryptionStream.Read(buffer);
        outFileStream.Write(buffer, 0, bytesRead);
    } while (inFileStream.Position != inFileStream.Length);
}

Additional Methods

VerifyEndOfMessage

During encryption the final block is appended with a special tag, marking the end of the message. During decryption this method will return whether the most recently decrypted block was the last one in the message, i.e. that the message has been fully decrypted.

bool endOfMessage = decryptionStream.VerifyEndOfMessage();

Additional Associated Data

There are overloads of Read, Write, and WriteFinal that take additional associated data. This will be used when computing/verifying the authentication tag when encrypting/decrypting that block.

Encryption

var additionalData = BitConverter.GetBytes(DateTime.Now.ToBinary());
encryptionStream.Write(buffer, additionalData);
...
var additionalDataFinal = BitConverter.GetBytes(DateTime.Now.ToBinary());
encryptionStream.WriteFinal(buffer, additionalDataFinal);

Decryption

var additionalData = BitConverter.GetBytes(DateTime.Now.ToBinary());
decryptionStream.Read(buffer, additionalData);