Table of Contents

Bodu.Security.Cryptography - Getting started

Install

dotnet add package Bodu.Security.Cryptography

Targets net8.0. Depends on Bodu.Core and the BCL System.Security.Cryptography.

Minimal samples - one per subfamily

Standard block cipher - Camellia in CBC mode

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

byte[] plaintext = Encoding.UTF8.GetBytes("the quick brown fox");

using var cipher = new Camellia();
cipher.GenerateKey();
cipher.GenerateIV();
cipher.BlockMode    = CipherModeKind.CBC;
cipher.BlockPadding = PaddingModeKind.PKCS7;

byte[] ciphertext = cipher.Encrypt(plaintext);
byte[] roundtrip  = cipher.Decrypt(ciphertext);

Swap Camellia for Twofish, Serpent128, Blowfish, or Skipjack - the lifecycle is identical.

Tweakable block cipher - Threefish-512 with a per-record tweak

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

using var cipher = new Threefish512();
cipher.GenerateKey();
cipher.GenerateIV();
cipher.GenerateTweak();              // 128-bit public domain separator
cipher.BlockMode = CipherModeKind.CBC;
cipher.Padding   = PaddingMode.PKCS7;

byte[] ciphertext = cipher.Encrypt(plaintext);
byte[] roundtrip  = cipher.Decrypt(ciphertext);

Encrypting the same plaintext under the same key with a different tweak yields an entirely independent ciphertext - useful for disk encryption (sector number as tweak) or per-record encryption.

Stream cipher - ChaCha20

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

using var cipher = new ChaCha20();
cipher.GenerateKey();               // 32-byte key
cipher.GenerateNonce();                // 12-byte nonce - unique per message

byte[] ciphertext = cipher.Encrypt(plaintext);
byte[] roundtrip  = cipher.Decrypt(ciphertext);   // self-inverse

Swap ChaCha20 for XChaCha20, Salsa20, XSalsa20, Rabbit, or Hc128 - the lifecycle is identical (no block mode or padding). These are raw, confidentiality-only ciphers: never reuse a (key, nonce) pair, and pair with a MAC such as Poly1305 or prefer an AEAD construction when you need integrity.

AEAD - AES-GCM via AesBlockCipher + GcmModeTransform

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

byte[] key   = RandomNumberGenerator.GetBytes(32); // AES-256
byte[] nonce = RandomNumberGenerator.GetBytes(12);
byte[] aad   = "header"u8.ToArray();
byte[] data  = "the quick brown fox"u8.ToArray();

using var aes = new AesBlockCipher(key);
using var gcm = new GcmModeTransform(aes, nonce);

byte[] ciphertextWithTag = gcm.Encrypt(data, associatedData: aad);

using var verify = new GcmModeTransform(aes, nonce);   // fresh transform per message
byte[] recovered = verify.Decrypt(ciphertextWithTag, associatedData: aad);

Swap GcmModeTransform for CcmModeTransform, OcbModeTransform, EaxModeTransform, SivModeTransform, or GcmSivModeTransform. AEAD transforms are single-use per message - construct a fresh transform on the encrypt side and another on the decrypt side.

AEAD - ASCON-AEAD128 (no separate cipher)

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

byte[] key   = RandomNumberGenerator.GetBytes(16);
byte[] nonce = RandomNumberGenerator.GetBytes(16);
byte[] aad   = "header"u8.ToArray();
byte[] data  = "the quick brown fox"u8.ToArray();

using var aead = new AsconAead128(key, nonce);
byte[] ciphertextWithTag = aead.Encrypt(data, associatedData: aad);

using var verify = new AsconAead128(key, nonce);
byte[] recovered = verify.Decrypt(ciphertextWithTag, associatedData: aad);

Keyed hash (MAC) - SipHash-64

using System.Security.Cryptography;
using System.Text;
using Bodu.Security.Cryptography;

byte[] data = Encoding.UTF8.GetBytes("the quick brown fox");
byte[] key  = RandomNumberGenerator.GetBytes(16);

using var sip = new SipHash64 { Key = key };
ulong digest  = BitConverter.ToUInt64(sip.ComputeHash(data));

Cryptographic digest - Tiger-192

using System.Text;
using Bodu.Security.Cryptography;

byte[] data = Encoding.UTF8.GetBytes("the quick brown fox");

using var tiger = new Tiger();
byte[] digest   = tiger.ComputeHash(data);

Swap Tiger for CubeHash, Blake2b, Whirlpool, Skein512, or AsconHash256.

Cryptographic digest - variable length (XOF) ASCON-XOF-128

using Bodu.Security.Cryptography;

// The ASCON XOFs are sponges, not HashAlgorithms: absorb, then squeeze as many bytes as you need.
using var xof = new AsconXof128();
xof.Absorb(data);
byte[] output = xof.GetHash(outputLength: 64);      // squeeze 64 bytes and finalize

// One-shot equivalent.
byte[] same = AsconXof128.HashData(data, outputLength: 64);

Squeeze(Span<byte>) writes directly into a caller buffer, and Initialize() resets the sponge for the next message.

Merkle tree - an RFC 6962 root over fixed-size blocks, with a proof

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

// The tree creates and disposes its own inner digests, so it takes a factory rather than an instance.
var tree = new MerkleTree(SHA256.Create);          // immutable; share it across threads

byte[] root = tree.ComputeRootOfBlocks(largeFile, blockSize: 1024);   // Stream, ReadOnlySpan<byte>, or byte[]

// Keep the leaf hashes when a proof will be needed later.
MerkleBlockComputation blocks = tree.ComputeBlocked(largeFile, blockSize: 1024);
byte[][] path = tree.AuthenticationPath(blocks.LeafHashes, leafIndex: 3);

bool included = tree.VerifyInclusionOfLeafHash(
    blocks.Root, treeSize: blocks.LeafHashes.Count, leafIndex: 3,
    leafHash: blocks.LeafHashes[3],
    path: path.Select(step => (ReadOnlyMemory<byte>)step).ToArray());

The root is RFC 6962's Merkle Tree Hash over the blocks, whichever way the bytes arrive: ComputeRootOfBlocks folds as it reads, CreateBlockAccumulator builds the same root from incremental Append calls, and new MerkleTree(SHA256.Create, maxDegreeOfParallelism: -1) hashes leaves across cores without changing the tree.

Digital signature - Ed25519

using System.Text;
using Bodu.Security.Cryptography;

byte[] message = Encoding.UTF8.GetBytes("transfer 100 to account 42");

using var signer = Ed25519.Create();
signer.GenerateKey();

byte[] signature = signer.SignData(message);          // 64 bytes
bool valid       = signer.VerifyData(message, signature);   // true

Swap Ed25519 for MLDsa65 for a post-quantum (FIPS 204) signature - the lifecycle is identical, only the key and signature sizes differ.

Key agreement - X25519

using Bodu.Security.Cryptography;

using var alice = X25519.Create();
using var bob   = X25519.Create();
alice.GenerateKey();
bob.GenerateKey();

// Each side publishes its public key, then derives the secret from the peer's.
byte[] aliceShared = alice.DeriveSharedSecret(bob.ExportPublicKey());
byte[] bobShared   = bob.DeriveSharedSecret(alice.ExportPublicKey());

// aliceShared and bobShared are identical (32 bytes each).

Swap X25519 for MLKem768 when you need post-quantum (FIPS 203) key establishment - the receiver publishes an encapsulation key and the sender calls Encapsulate().

Hybrid public-key encryption - HPKE

using Bodu.Security.Cryptography;

using var recipient = X25519.Create();
recipient.GenerateKey();
byte[] recipientPublicKey = recipient.ExportPublicKey();   // published, 32 bytes

HpkeSuite suite = HpkeSuite.X25519_HkdfSha256_Aes128Gcm;
byte[] info = "myapp v1"u8.ToArray();   // binds the exchange to a context
byte[] aad  = "headers"u8.ToArray();    // authenticated, not encrypted

// Sender - needs only the recipient's public key.
var (enc, ciphertext) = Hpke.Seal(suite, recipientPublicKey, info, aad, "secret message"u8);

// Recipient - needs its private key, the encapsulated key, and the same info / aad.
byte[] plaintext = Hpke.Open(suite, recipient, enc, info, aad, ciphertext);

Password hashing - Argon2id

using System.Security.Cryptography;
using System.Text;
using Bodu.Security.Cryptography;

byte[] password = Encoding.UTF8.GetBytes("correct horse battery staple");
byte[] salt     = RandomNumberGenerator.GetBytes(16);

var parameters = new Argon2Parameters
{
    MemoryKiB   = 65536,   // 64 MiB
    Iterations  = 3,
    Parallelism = 4,
    TagLength   = 32,
};

byte[] key = Argon2id.DeriveKey(password, salt, parameters);   // 32 bytes

Swap Argon2id for Scrypt (Scrypt.DeriveKey(password, salt, costN: 16384, blockSizeR: 8, parallelization: 1, length: 32)) for RFC 7914 interoperability.

Key derivation - HKDF

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

byte[] sharedSecret = GetSharedSecret();   // high-entropy, but not uniform

byte[] sessionKey = Hkdf.DeriveKey(
    HashAlgorithmName.SHA256,
    inputKeyingMaterial: sharedSecret,
    outputLength: 32,
    salt: salt,                            // optional, non-secret; binds the derivation
    info: "myapp v1 traffic key"u8);       // optional context / label

CryptographicOperations.ZeroMemory(sharedSecret);   // wipe the raw secret once stretched

HKDF is for high-entropy inputs only - for passwords reach for Argon2id or scrypt above.

Where to go next