Table of Contents

Using stream ciphers

A stream cipher generates a key- and nonce-dependent keystream and XORs it with the plaintext. Unlike the block ciphers elsewhere in this library, there is no cipher block, no block mode, and no padding: any byte length is encrypted directly. All of Bodu's stream ciphers derive from SymmetricStreamAlgorithm - a standalone IDisposable base (not a SymmetricAlgorithm) with Key, Nonce, NonceSize, GenerateKey() / GenerateNonce(), and CreateTransform() - so they still flow through CreateEncryptor() / CreateDecryptor(), a CryptoStream, and the Encrypt / Decrypt extension methods in SymmetricStreamAlgorithmExtensions, much like the block ciphers do.

Because the keystream is XORed in, every stream cipher here is self-inverse: encryption and decryption are the same operation, and CreateEncryptor() and CreateDecryptor() are interchangeable. These ciphers derive from SymmetricStreamAlgorithm, not SymmetricAlgorithm, so the per-message nonce is supplied through the Nonce property (generated with GenerateNonce()) rather than a block-cipher IV.

Warning

These are raw, confidentiality-only ciphers - they provide no authentication. A given (key, nonce) pair must encrypt at most one message: reusing it XORs two keystreams together and reveals the XOR of the plaintexts. For most applications, prefer an AEAD construction (see the AEAD modes guide and ASCON AEAD) so that tampering is detected; if you use a raw stream cipher, pair it with a MAC such as Poly1305 (encrypt-then-MAC).

The family at a glance

Cipher Key Nonce / IV Counter Lineage
ChaCha20 256 bits (32 B) 96 bits (12 B) 32-bit Bernstein; RFC 8439
XChaCha20 256 bits (32 B) 192 bits (24 B) 32-bit Extended-nonce ChaCha20 (HChaCha20 subkey)
Salsa20 128 or 256 bits 64 bits (8 B) 64-bit Bernstein; eSTREAM
XSalsa20 256 bits (32 B) 192 bits (24 B) 64-bit Extended-nonce Salsa20 (HSalsa20 subkey); NaCl
Rabbit 128 bits (16 B) 64 bits (8 B) - (evolving state) RFC 4503; eSTREAM
Hc128 128 bits (16 B) 128 bits (16 B) - (evolving state) Wu; eSTREAM

Which one? For new work prefer ChaCha20 (the de-facto modern standard, RFC 8439) or, when nonces are chosen at random rather than from a counter, XChaCha20 / XSalsa20 - their 192-bit nonces are large enough to pick randomly without meaningful collision risk. Salsa20, Rabbit, and HC-128 are provided for interoperability and for completeness of the eSTREAM portfolio.

Note

A 64-bit nonce (Salsa20, Rabbit) is too short to choose randomly without collision risk. Use a strict counter, or prefer an extended-nonce cipher.

Nonce vs IV - and how to manage it

A stream cipher takes a Nonce, not a block-cipher IV. The two play the same role - both randomise the keystream so the same key encrypts different messages safely - but the requirements differ. A CBC IV must be unpredictable; a stream-cipher nonce only has to be unique under the key. The size dictates how you should generate it:

Nonce width Safe to pick at random? Recommended source
96-bit (ChaCha20) Borderline - a counter is safer A monotonic 96-bit counter, or a sequence number per session
64-bit (Salsa20, Rabbit) No - collisions at ~2³² messages A strict counter; never RandomNumberGenerator
192-bit (XChaCha20, XSalsa20) Yes - collision risk negligible GenerateNonce() / RandomNumberGenerator

The birthday bound is why width matters: random 96-bit nonces reach a ~50 % collision probability near 2⁴⁸ messages, and 64-bit nonces near 2³². A single collision under a fixed key XORs two keystreams together and exposes the XOR of the two plaintexts - there is no recovery. When you cannot guarantee a counter never repeats (distributed senders, restarts that lose state), prefer an extended-nonce cipher and choose the nonce at random, or move to an AEAD that is misuse-resistant.

Encrypt and decrypt - ChaCha20

using System.Diagnostics;
using System.Linq;
using System.Text;
using Bodu.Security.Cryptography;
using Bodu.Security.Cryptography.Extensions;

byte[] plaintext = Encoding.UTF8.GetBytes("message under ChaCha20");

byte[] key, nonce, ciphertext;
using (var alg = new ChaCha20())
{
    alg.GenerateKey();    // 32 bytes (256-bit)
    alg.GenerateNonce();     // 12 bytes (96-bit nonce) - unique per message

    key   = alg.Key;
    nonce = alg.Nonce;

    ciphertext = alg.Encrypt(plaintext);
}

byte[] recovered;
using (var alg = new ChaCha20 { Key = key, Nonce = nonce })
{
    recovered = alg.Decrypt(ciphertext);   // self-inverse - Encrypt would work too
}

Debug.Assert(plaintext.SequenceEqual(recovered));

Streaming with CryptoStream

Because a stream cipher imposes no block alignment, it composes naturally with CryptoStream for arbitrary-length, chunked data:

using System.IO;
using System.Security.Cryptography;
using Bodu.Security.Cryptography;

using var alg = new XChaCha20();
alg.GenerateKey();
alg.GenerateNonce();                       // 24-byte nonce - safe to choose at random

using var output = new MemoryStream();
using (var crypto = new CryptoStream(output, alg.CreateEncryptor(alg.Key, alg.Nonce), CryptoStreamMode.Write))
{
    crypto.Write(firstChunk);
    crypto.Write(secondChunk);          // any lengths; the keystream carries across calls
}

byte[] ciphertext = output.ToArray();

Salsa20 with a 128-bit key

Salsa20 is the only cipher in this family that accepts more than one key size. Set KeySize to 128 before generating or assigning the key:

using Bodu.Security.Cryptography;
using Bodu.Security.Cryptography.Extensions;

using var alg = new Salsa20 { KeySize = 128 };   // default is 256
alg.GenerateKey();                               // 16 bytes
alg.GenerateNonce();                                // 8-byte nonce

byte[] ciphertext = alg.Encrypt(plaintext);

Choosing the keystream start - InitialCounter

ChaCha20, XChaCha20, Salsa20, and XSalsa20 expose an InitialCounter that sets the block counter for the first keystream block. The default is 0; set it to match an external convention (for example, some protocols reserve block 0 for a one-time MAC key and start the message at block 1):

using var alg = new ChaCha20 { InitialCounter = 1 };
alg.GenerateKey();
alg.GenerateNonce();

byte[] ciphertext = alg.Encrypt(plaintext);

Rabbit and HC-128 have no seekable counter - their keystream comes from an evolving internal state, so a message is always encrypted as one forward sequence.

A note on initialization cost

HC-128 has a comparatively expensive setup: it warms up two 512-word tables before releasing any keystream. Rabbit's key/IV setup is lighter but still non-trivial. As with Blowfish's key schedule, do not build a fresh instance per message under the same key - cache the instance and call CreateEncryptor() / CreateDecryptor() per message instead.

Authenticated stream ciphers - Poly1305 AEAD

A raw stream cipher gives you confidentiality but not integrity. The library pairs the extended-nonce ciphers with Poly1305 in three ready-made AEAD constructions - XChaCha20Poly1305, the NaCl secretbox XSalsa20Poly1305 (with converters for libsodium's tag ‖ ciphertext layout), and the Bodu-defined XSalsa20Poly1305Aead - all built on Poly1305AeadTransform, all taking a 256-bit key and a 192-bit nonce, all emitting ciphertext ‖ tag, and all single-use per message. They have their own guide now: Authenticated stream ciphers covers the draft-vector round trip, tamper and reuse behaviour, the secretbox layout, the IStreamAeadTransform interface, in-place operation, and detached tags.

Where to go next