-
Notifications
You must be signed in to change notification settings - Fork 0
XChaChaStream
Parameter | Value |
---|---|
Encryption Algorithm | XChaCha20 |
Authentication Algorithm | Poly1305-MAC |
Key Size | 32 bytes |
Nonce Size | 24 bytes |
Max Message Size | 2^64-1 bytes |
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
The following example demonstrates how to encrypt/decrypt a pair of messages:
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
).
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
.
The following examples demonstrate how to encrypt/decrypt a file in 128KB blocks:
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);
}
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);
}
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();
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.
var additionalData = BitConverter.GetBytes(DateTime.Now.ToBinary());
encryptionStream.Write(buffer, additionalData);
...
var additionalDataFinal = BitConverter.GetBytes(DateTime.Now.ToBinary());
encryptionStream.WriteFinal(buffer, additionalDataFinal);
var additionalData = BitConverter.GetBytes(DateTime.Now.ToBinary());
decryptionStream.Read(buffer, additionalData);