Streams and async
Everything in Bodu.Security.Cryptography that touches a Stream lives in Bodu.Security.Cryptography.Extensions, layered over the BCL abstractions the types already implement: SymmetricAlgorithmExtensions and SymmetricStreamAlgorithmExtensions for ciphers, ICryptoTransformExtensions for the transforms underneath them, and HashAlgorithmExtensions for hashes - plus the MerkleTree block and root members and HashAlgorithmHelper. This page lists every stream and Task member, the buffer sizes they default to, how they react to cancellation, and what they do with memory.
Note
Bodu.Security.Cryptography is not independently audited and offers best-effort, not guaranteed, side-channel resistance.
Pattern 1 - encrypt a stream with a block cipher
Encrypt(Stream, Stream) / Decrypt(Stream, Stream) on any SymmetricAlgorithm create a transform with CreateEncryptor() / CreateDecryptor(), pump the source through it in bufferSize-byte reads, and return the number of bytes read. Neither stream is disposed. The default buffer is SymmetricAlgorithmExtensions.DefaultBufferSize = 81920 bytes (80 KiB); the overload with bufferSize overrides it and rejects values ≤ 0 with ArgumentOutOfRangeException.
using System.Security.Cryptography;
using Bodu.Security.Cryptography;
using Bodu.Security.Cryptography.Extensions;
byte[] payload = new byte[300_000];
for (int i = 0; i < payload.Length; i++) payload[i] = (byte)(i * 31);
using var alg = new Camellia { BlockMode = CipherModeKind.CBC, Padding = PaddingMode.PKCS7 };
alg.Key = Convert.FromHexString("000102030405060708090a0b0c0d0e0f");
alg.IV = Convert.FromHexString("101112131415161718191a1b1c1d1e1f");
using var source = new MemoryStream(payload);
using var encrypted = new MemoryStream();
int consumed = alg.Encrypt(source, encrypted, bufferSize: 64 * 1024); // 300000 bytes read; 300016 written (PKCS7)
encrypted.Position = 0;
using var decrypted = new MemoryStream();
int ciphertextConsumed = alg.Decrypt(encrypted, decrypted); // default 80 KiB buffer
Because the transform is created from the algorithm's current Key / IV / BlockMode / Padding, set those first. The same overloads exist on SymmetricStreamAlgorithmExtensions for the stream ciphers (ChaCha20, XChaCha20, Salsa20, XSalsa20, Rabbit, Hc128).
Pattern 2 - the same, awaitable
EncryptAsync / DecryptAsync have the same signatures plus a CancellationToken, and delegate to ICryptoTransformExtensions.TransformAsync. Cancellation is honoured between reads and before the final block is flushed: a token that fires after the last read but before finalization throws and leaves the target stream partial. An already-cancelled token surfaces as TaskCanceledException (an OperationCanceledException).
using System.Security.Cryptography;
using Bodu.Security.Cryptography;
using Bodu.Security.Cryptography.Extensions;
byte[] payload = new byte[300_000];
using var alg = new Camellia();
alg.Key = Convert.FromHexString("000102030405060708090a0b0c0d0e0f");
alg.IV = Convert.FromHexString("101112131415161718191a1b1c1d1e1f");
using var source = new MemoryStream(payload);
using var encrypted = new MemoryStream();
await alg.EncryptAsync(source, encrypted, bufferSize: 32 * 1024, cancellationToken: CancellationToken.None);
encrypted.Position = 0;
using var decrypted = new MemoryStream();
await alg.DecryptAsync(encrypted, decrypted); // default buffer, no token
using var cts = new CancellationTokenSource();
cts.Cancel();
try { await alg.EncryptAsync(new MemoryStream(payload), new MemoryStream(), cts.Token); }
catch (OperationCanceledException) { /* nothing was finalized */ }
The stream-cipher extension class has no *Async members. For an awaitable stream-cipher copy, drop one level to the transform (Pattern 3).
Pattern 3 - ICryptoTransform.TransformAsync
TransformAsync(sourceStream, targetStream, bufferSize, cancellationToken) is the engine behind the cipher overloads and works on any ICryptoTransform - a Bodu BlockCipherTransform, a stream-cipher transform, or a BCL one. It rents its read buffer from ArrayPool<byte>.Shared and returns it cleared on every exit path, wraps the target in a CryptoStream with leaveOpen: true, and disposes neither stream.
using System.Security.Cryptography;
using Bodu.Security.Cryptography;
using Bodu.Security.Cryptography.Extensions;
byte[] payload = new byte[300_000];
using var alg = new ChaCha20();
alg.Key = Convert.FromHexString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
alg.Nonce = Convert.FromHexString("000000000000004a00000000");
using var source = new MemoryStream(payload);
using var encrypted = new MemoryStream();
int consumed = alg.Encrypt(source, encrypted, bufferSize: 16 * 1024); // synchronous stream-cipher overload
// The parameterless CreateDecryptor() would throw: SymmetricStreamAlgorithm allows one transform per nonce.
// Pass the key and nonce explicitly for the second transform.
encrypted.Position = 0;
using var decrypted = new MemoryStream();
using ICryptoTransform decryptor = alg.CreateDecryptor(alg.Key, alg.Nonce);
await decryptor.TransformAsync(encrypted, decrypted, bufferSize: 16 * 1024);
A second call to the parameterless CreateEncryptor() / CreateDecryptor() under the same nonce throws CryptographicException - the library refuses to hand out two keystreams for one (key, nonce). The (key, nonce) overloads are the documented way to build the matching decryptor.
Pattern 4 - hashing a stream, synchronously and asynchronously
Every Bodu hash is a HashAlgorithm, so the BCL's ComputeHash(Stream) and ComputeHashAsync(Stream, CancellationToken) already work. The extension class adds:
| Member | Buffer | Finalizes? | Returns |
|---|---|---|---|
AppendData(ReadOnlySpan<byte>) |
pooled copy of the span, cleared on return | No | - |
AppendDataAsync(Stream, bufferSize = 4096, CancellationToken) |
pooled, cleared on every exit | No | Task |
VerifyHash(…) / VerifyHashAsync(Stream, byte[] \| string \| ReadOnlyMemory<byte>, CancellationToken) |
via ComputeHash / ComputeHashAsync |
Yes | bool - constant-time compare |
TryVerifyHash(…) / TryVerifyHashAsync(…) |
as above | Yes | bool; false for null arguments, malformed hex, or any exception |
using Bodu.Security.Cryptography;
using Bodu.Security.Cryptography.Extensions;
byte[] payload = new byte[300_000];
for (int i = 0; i < payload.Length; i++) payload[i] = (byte)(i * 31);
using var blake = new Blake2b(256);
byte[] expected = blake.ComputeHash(payload); // 7C453235…255798F6
// BCL member, inherited: reads to end, then finalizes.
using var s1 = new MemoryStream(payload);
byte[] viaBcl = await blake.ComputeHashAsync(s1);
// Bodu: feed TransformBlock from a stream, finalize when you decide to.
using var s2 = new MemoryStream(payload);
using var incremental = new Blake2b(256);
await incremental.AppendDataAsync(s2, bufferSize: 8192);
incremental.TransformFinalBlock([], 0, 0);
byte[] viaAppend = incremental.Hash!;
// Verify helpers.
using var s3 = new MemoryStream(payload);
bool ok = await blake.VerifyHashAsync(s3, expected); // true
using var s4 = new MemoryStream(payload);
bool okHex = await blake.TryVerifyHashAsync(s4, Convert.ToHexString(expected)); // true; hex is case-insensitive
bool empty = await blake.TryVerifyHashAsync(Stream.Null, expected); // false, no exception
VerifyHashAsync checks the token before any I/O and throws OperationCanceledException directly; TryVerifyHashAsync swallows that into false. Both compare with CryptographicOperations.FixedTimeEquals.
Pattern 5 - Merkle roots over a stream, in parallel
MerkleTree covers both the synchronous and the awaitable path. ComputeRootOfBlocks(Stream, int blockSize, MerkleTreeDiagnostics? = null, CancellationToken = default) folds a stream block by block; ComputeRootOfBlocksAsync(…) is the same computation over ReadAsync. Parallelism is a property of the instance rather than a separate type: maxDegreeOfParallelism is 1 (the calling thread) by default, -1 for unbounded, or an explicit worker count. The tree shape never changes with the setting, so a parallel instance and a sequential one produce the same root and the same proofs.
blockSize is a per-call argument - the same tree can fold different inputs at different block sizes - and fanOut defaults to 2, RFC 6962's binary tree.
using Bodu.Security.Cryptography;
byte[] payload = new byte[300_000];
for (int i = 0; i < payload.Length; i++) payload[i] = (byte)(i * 31);
var sequential = new MerkleTree(() => new Blake2b(256));
byte[] expected = sequential.ComputeRootOfBlocks(payload, blockSize: 4096);
var parallel = new MerkleTree(() => new Blake2b(256), maxDegreeOfParallelism: -1);
using var source = new MemoryStream(payload);
byte[] root = await parallel.ComputeRootOfBlocksAsync(source, blockSize: 4096);
// root == expected: 15BB6566…297F7C4F
The factory delegate must return a fresh HashAlgorithm per call - workers never share an instance, and a factory handing back one shared instance cannot serve a parallel tree at all. An empty input is not an error: it yields the empty tree's root, H() over zero bytes, which for Blake2b(256) is 0E5751C0…F12FE3A8.
Use ComputeBlocked / ComputeBlockedAsync instead when you want the leaf hashes and block arithmetic alongside the root; they return a MerkleBlockComputation carrying Root, InputLength, BlockSize, BlockCount, and the retained LeafHashes.
Pattern 6 - factory-driven one-shots
HashAlgorithmHelper hashes through an IHashAlgorithmFactory<T>, creating and disposing the algorithm per call: HashData(factory, ReadOnlySpan<byte>), HashData(factory, Stream), HashDataAsync(factory, Stream, CancellationToken) (an 8 KiB pooled buffer), and TryHashData(factory, input, destination, out bytesWritten).
using Bodu.Security.Cryptography;
byte[] payload = new byte[300_000];
using var source = new MemoryStream(payload);
byte[] digest = await HashAlgorithmHelper.HashDataAsync(HashAlgorithmFactory.From(() => new Blake2b(256)), source);
Buffers, cancellation, and memory - the rules
- Buffer sizes. Cipher stream overloads default to 80 KiB;
AppendDataAsyncto 4 KiB;HashAlgorithmHelperto 8 KiB;MerkleTreereads oneblockSizeblock at a time, batching them per worker when run in parallel. EverybufferSizeparameter must be > 0. - Pooled buffers are cleared.
TransformAsync,AppendData,AppendDataAsync, theMerkleTreeblock and leaf loops, and the helper all rent fromArrayPool<byte>.Sharedand return withclearArray: true, on success, cancellation, and exception paths alike, so plaintext never lingers in a pool. - Streams are never disposed by these members; the caller owns both ends.
- Cancellation is cooperative. It is checked per read and, for
TransformAsync, before the final block; a partially written target is the caller's to discard. Already-cancelled tokens fail fast before any I/O. - Instances are not thread-safe - with one exception. Hashes and transforms are single-caller objects.
MerkleTreeis immutable and safe to share across threads, provided its factory returns a freshHashAlgorithmon each call; a parallel instance spreads leaf hashing across workers inside one call and folds them in order on the calling thread.
API summary
| Class | Stream / async members |
|---|---|
| SymmetricAlgorithmExtensions | Encrypt(Stream, Stream[, int]), Decrypt(…), EncryptAsync(Stream, Stream[, int], CancellationToken), DecryptAsync(…); DefaultBufferSize = 81920 |
| SymmetricStreamAlgorithmExtensions | Encrypt(Stream, Stream[, int]), Decrypt(…) - synchronous only |
| ICryptoTransformExtensions | Transform(Stream, Stream, int), TransformAsync(Stream, Stream, int, CancellationToken) |
| HashAlgorithmExtensions | AppendData, AppendDataAsync, VerifyHash, VerifyHashAsync, TryVerifyHash, TryVerifyHashAsync |
System.Security.Cryptography.HashAlgorithm (inherited) |
ComputeHash(Stream), ComputeHashAsync(Stream, CancellationToken) |
| MerkleTree | ComputeRootOfBlocks(Stream, int, MerkleTreeDiagnostics?, CancellationToken), ComputeRootOfBlocksAsync(…), ComputeBlocked(…), ComputeBlockedAsync(…) |
| HashAlgorithmHelper | HashData(factory, Stream), HashDataAsync(factory, Stream, CancellationToken) |
Where to go next
- Interoperating with System.Security.Cryptography -
CryptoStreamand theHashAlgorithmlifecycle. - Runnable samples -
Bodu.Security.Cryptography.Samples.StreamingPipelinesruns these members end to end: stream-to-stream and async encryption, download verification, and an encrypt-then-MAC sealed file. - Using Merkle trees - fan-out, Tiger-Tree hashes, and diagnostics.
- Streaming, async, and resumable hashing - the same story for the non-cryptographic checksums, including
HashingStream. - Hashing & Cryptography guides - every guide in this topic, across Bodu.IO.Hashing and Bodu.Security.Cryptography.