Skip to content

XChaChaBufferedStream

Tom Auger edited this page Jun 25, 2018 · 3 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 XChaChaBufferedStream 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

The stream encrypts/decrypts data in fixed sized blocks, but allows you to read and write data without worrying about chunking the data manually. The default block size is 128KB. This is particularly useful if you are compressing data before encrypting it, as you can compose the compression stream with the cipher stream without needing to break the compressed data into chunks.

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 XChaChaBufferedStream(ciphertextStream, key, EncryptionMode.Encrypt))
    {
        encryptionStream.Write(block1);
        encryptionStream.Write(block2);
    }

    var ciphertext = ciphertextStream.ToArray();
}

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 plaintext = new byte[42];

    using (var decryptionStream = new XChaChaBufferedStream(ciphertextStream, key, EncryptionMode.Decrypt))
    {
        decryptionStream.Read(plaintext);
    }
}

Any amount of plaintext can be read out of the stream. It doesn't have to be read out in fixed sized blocks.

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 XChaChaBufferedStream(outFileStream, key, EncryptionMode.Encrypt))
{
    var buffer = new byte[128 * 1024];
    int bytesRead;

    do
    {
        bytesRead = inFileStream.Read(buffer);
        encryptionStream.Write(buffer);
    } while (bytesRead != 0);
}

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 XChaChaBufferedStream(inFileStream, key, EncryptionMode.Decrypt))
{
    var buffer = new byte[128 * 1024];
    int bytesRead;

    do
    {
        bytesRead = decryptionStream.Read(buffer);
        outFileStream.Write(buffer, 0, bytesRead);
    } while (bytesRead != 0);
}

Example - Encrypt/Decrypt a file with compression

The following examples demonstrate how to encrypt/decrypt a file while simultaneously compressing/decompressing the data:

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 XChaChaBufferedStream(outFileStream, key, EncryptionMode.Encrypt))
using (var compressionStream = new BrotliStream(encryptionStream, CompressionMode.Compress))
{
    var buffer = new byte[8192];
    int bytesRead;

    do
    {
        bytesRead = inFileStream.Read(buffer);
        compressionStream.Write(buffer, 0, bytesRead);
    } while (bytesRead != 0);
}

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 XChaChaBufferedStream(inFileStream, key, EncryptionMode.Decrypt))
using (var decompressionStream = new BrotliStream(decryptionStream, CompressionMode.Decompress))
{
    var buffer = new byte[128 * 1024];
    int bytesRead;

    do
    {
        bytesRead = decompressionStream.Read(buffer);
        outFileStream.Write(buffer, 0, bytesRead);
    } while (bytesRead != 0);
}

By default XChaChaBufferedStream encrypts in blocks of 128KB. An overload of the constructor exists to provide a custom block size. Note that if you use a custom block size during encryption, the same block size must be used during decryption.

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