Table of Contents

Interoperating with System.Security.Cryptography

Bodu.Security.Cryptography is built on the BCL's own abstractions rather than beside them: every hash is a System.Security.Cryptography.HashAlgorithm, every block-cipher wrapper is a SymmetricAlgorithm (through ExtendedSymmetricAlgorithm or TweakableSymmetricAlgorithm), the raw-key asymmetric types derive from AsymmetricAlgorithm, and the KDF and OTP surfaces take HashAlgorithmName or an equivalent enum. This page shows, with verified round trips, exactly where a Bodu type drops into a BCL-shaped call site - and the two places where it deliberately does not.

Note

Bodu.Security.Cryptography is not independently audited and offers best-effort, not guaranteed, side-channel resistance. Where the BCL implements the same algorithm, prefer the BCL; see Choosing a primitive.

Pattern 1 - one-shot versus incremental hashing

A Bodu hash is a HashAlgorithm, so the BCL lifecycle applies unchanged: ComputeHash for one shot, TransformBlock … TransformFinalBlock then Hash for incremental use. HashAlgorithmExtensions adds a span-friendly AppendData that forwards to TransformBlock, so you can mix the two freely.

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

byte[] part1 = "The quick brown fox "u8.ToArray();
byte[] part2 = "jumps over the lazy dog"u8.ToArray();

// One shot: ComputeHash resets, hashes, and finalizes.
using var oneShot = new Blake2b(256);
byte[] whole = oneShot.ComputeHash([.. part1, .. part2]);

// Incremental, the BCL way.
using var incremental = new Blake2b(256);
incremental.TransformBlock(part1, 0, part1.Length, null, 0);
incremental.TransformFinalBlock(part2, 0, part2.Length);
byte[] streamed = incremental.Hash!;

// Incremental, the Bodu way: AppendData over spans, then an empty final block.
using var appended = new Blake2b(256);
appended.AppendData(part1);
appended.AppendData(part2);
appended.TransformFinalBlock([], 0, 0);
byte[] appendedHash = appended.Hash!;

// All three: 01718CEC35CD3D796DD00020E0BFECB473AD23457D063B75EFF29C0FFA2E58A9

HashAlgorithm.ComputeHash(Stream) and ComputeHashAsync(Stream, CancellationToken) work too - they are inherited, not reimplemented. See Streams and async for the buffer sizes and the verify helpers.

Pattern 2 - IncrementalHash has no Bodu equivalent, and does not need one

System.Security.Cryptography.IncrementalHash only accepts algorithms a HashAlgorithmName can name, so it cannot host BLAKE2, Skein, or Tiger. The equivalent shape on a Bodu hash is AppendData followed by an empty TransformFinalBlock, and Initialize() plays the part of GetHashAndReset's reset:

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

byte[] data = "abc"u8.ToArray();

using IncrementalHash bcl = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
bcl.AppendData(data);
byte[] sha = bcl.GetHashAndReset();          // BA7816BF…F20015AD

using var bodu = new Blake3();
bodu.AppendData(data);
bodu.TransformFinalBlock([], 0, 0);
byte[] blake3 = bodu.Hash!;                  // 6437B3AC…BD9D85
bodu.Initialize();                           // ready for the next message

Do not look for GetHashAndReset on a Bodu hash - it does not exist on any public type.

Pattern 3 - CryptoStream over a Bodu cipher

CreateEncryptor() / CreateDecryptor() on any wrapper return an ICryptoTransform (a BlockCipherTransform, or a stream-cipher transform), so CryptoStream composes as it would with Aes:

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

byte[] plaintext = "CryptoStream over a Bodu block cipher"u8.ToArray();

using var camellia = new Camellia { BlockMode = CipherModeKind.CBC, Padding = PaddingMode.PKCS7 };
camellia.Key = Convert.FromHexString("000102030405060708090a0b0c0d0e0f");
camellia.IV  = Convert.FromHexString("101112131415161718191a1b1c1d1e1f");

byte[] ciphertext;
using (var output = new MemoryStream())
{
    using (var crypto = new CryptoStream(output, camellia.CreateEncryptor(), CryptoStreamMode.Write))
        crypto.Write(plaintext);
    ciphertext = output.ToArray();           // 48 bytes: 6265D259…D92B2B2A
}

byte[] recovered;
using (var input = new MemoryStream(ciphertext))
using (var crypto = new CryptoStream(input, camellia.CreateDecryptor(), CryptoStreamMode.Read))
using (var sink = new MemoryStream())
{
    crypto.CopyTo(sink);
    recovered = sink.ToArray();
}

Set the mode through BlockMode (CipherModeKind) rather than the inherited Mode, because only BlockMode can name CTR; the two properties stay in sync for the values both enums share. See Modes, transforms, and factories.

Pattern 4 - BCL one-shots that do not work

The BCL SymmetricAlgorithm one-shot helpers (EncryptCbc, EncryptEcb, and the CFB variants) call the protected System.Security.Cryptography.SymmetricAlgorithm.TryEncryptCbcCore family, which the Bodu wrappers do not override - those calls throw NotSupportedException ("Method not supported. Derived class must override."). Use CreateEncryptor with CryptoStream, or the SymmetricAlgorithmExtensions one-shots (alg.Encrypt(byte[]), alg.Encrypt(Stream, Stream)), which route through CreateEncryptor.

Pattern 5 - AES-GCM is wire-compatible with AesGcm

GcmModeTransform over AesBlockCipher implements the same 96-bit-nonce, 128-bit-tag profile of NIST SP 800-38D that System.Security.Cryptography.AesGcm does. The only difference is layout: Bodu returns ciphertext ‖ tag in one array, the BCL takes the ciphertext and tag as separate buffers. Split or concatenate at the boundary and the bytes match in both directions - this run uses the SP 800-38D test-case-4 material:

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

byte[] key   = Convert.FromHexString("feffe9928665731c6d6a8f9467308308");
byte[] nonce = Convert.FromHexString("cafebabefacedbaddecaf888");
byte[] plaintext = Convert.FromHexString(
    "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72" +
    "1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255");
byte[] aad = Convert.FromHexString("feedfacedeadbeeffeedfacedeadbeefabaddad2");

// Bodu encrypts: ciphertext ‖ tag.
byte[] boduSealed;
using (var cipher = new AesBlockCipher(key))
    boduSealed = new GcmModeTransform(cipher, nonce).Encrypt(plaintext, associatedData: aad);

// The BCL decrypts the same bytes.
byte[] ciphertext = boduSealed[..^16];
byte[] tag        = boduSealed[^16..];          // DA80CE830CFDA02DA2A218A1744F4C76
byte[] bclPlain   = new byte[ciphertext.Length];
using (var bcl = new AesGcm(key, tagSizeInBytes: 16))
    bcl.Decrypt(nonce, ciphertext, tag, bclPlain, aad);

// And the other way round: BCL encrypts, Bodu decrypts.
byte[] bclCiphertext = new byte[plaintext.Length];
byte[] bclTag = new byte[16];
using (var bcl = new AesGcm(key, tagSizeInBytes: 16))
    bcl.Encrypt(nonce, plaintext, bclCiphertext, bclTag, aad);     // same tag: DA80CE83…744F4C76

byte[] boduPlain;
using (var cipher = new AesBlockCipher(key))
    boduPlain = new GcmModeTransform(cipher, nonce).Decrypt([.. bclCiphertext, .. bclTag], associatedData: aad);

Both bclPlain and boduPlain equal plaintext. The BCL type supports 12- to 16-byte tags; the Bodu transform always produces 16, so pass tagSizeInBytes: 16 on the BCL side.

Warning

Write Encrypt(plaintext, associatedData: aad), naming the argument. With two positional byte[] arguments the public span overload Encrypt(plaintext, output) wins, the AAD array becomes the output buffer, and the call returns an int.

Pattern 6 - XChaCha20Poly1305 is not ChaCha20Poly1305

The BCL's ChaCha20Poly1305 is RFC 8439 with a 96-bit nonce. Bodu's XChaCha20Poly1305 is the extended-nonce construction (draft-irtf-cfrg-xchacha, libsodium crypto_aead_xchacha20poly1305_ietf): it derives a subkey with HChaCha20 from the first 16 nonce bytes and runs ChaCha20 on the remaining 8. They are different constructions and are not wire-compatible, even under the same key. Bodu ships no plain ChaCha20-Poly1305 because the BCL already does.

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

byte[] key = Convert.FromHexString("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");
byte[] plaintext = "same key, different constructions"u8.ToArray();

using var xchacha = new XChaCha20Poly1305(key, Convert.FromHexString("404142434445464748494a4b4c4d4e4f5051525354555657"));
byte[] boduSealed = xchacha.Encrypt(plaintext);            // 24-byte nonce

byte[] bclCiphertext = new byte[plaintext.Length];
byte[] bclTag = new byte[16];
using var bcl = new ChaCha20Poly1305(key);                 // check ChaCha20Poly1305.IsSupported first
bcl.Encrypt(Convert.FromHexString("070000004041424344454647"), plaintext, bclCiphertext, bclTag);

Choose by the nonce discipline: a counter you can guarantee never repeats → the BCL type; nonces drawn at random → XChaCha20Poly1305. Both accept associated data; see Authenticated stream ciphers.

Pattern 7 - PEM, PKCS#8, and SPKI for X25519 and Ed25519

X25519 and Ed25519 override the AsymmetricAlgorithm import/export core (ImportPkcs8PrivateKey, ImportSubjectPublicKeyInfo, TryExportPkcs8PrivateKey, TryExportSubjectPublicKeyInfo) with the RFC 8410 encodings, so the inherited convenience members work unchanged: ExportPkcs8PrivateKey(), ExportSubjectPublicKeyInfo(), ExportPkcs8PrivateKeyPem(), ExportSubjectPublicKeyInfoPem(), and ImportFromPem(string).

using Bodu.Security.Cryptography;

using var signer = new Ed25519();
signer.ImportPrivateKey(Convert.FromHexString("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"));

string privatePem = signer.ExportPkcs8PrivateKeyPem();       // -----BEGIN PRIVATE KEY-----  (48-byte DER)
string publicPem  = signer.ExportSubjectPublicKeyInfoPem();  // -----BEGIN PUBLIC KEY-----   (44-byte DER)
byte[] spki       = signer.ExportSubjectPublicKeyInfo();     // 302A300506032B6570032100 D75A9801…F707511A

using var verifier = new Ed25519();
verifier.ImportFromPem(publicPem);                           // dispatches to ImportSubjectPublicKeyInfo

byte[] message = "pem round trip"u8.ToArray();
bool ok = verifier.VerifyData(message, signer.SignData(message));     // true

using var restored = new Ed25519();
restored.ImportFromPem(privatePem);                          // dispatches to ImportPkcs8PrivateKey
bool same = restored.SignData(message).AsSpan().SequenceEqual(signer.SignData(message));   // true

The same members work on X25519 (-----BEGIN PUBLIC KEY----- with the id-X25519 OID), which is how peers exchange agreement keys in PEM. What does not work, by design - each throws NotSupportedException:

Member X25519 / Ed25519 ML-KEM / ML-DSA
ImportPkcs8PrivateKey, ImportSubjectPublicKeyInfo, ImportFromPem Supported NotSupportedException
ExportPkcs8PrivateKey[Pem], ExportSubjectPublicKeyInfo[Pem] Supported NotSupportedException
ImportEncryptedPkcs8PrivateKey, ExportEncryptedPkcs8PrivateKey[Pem] NotSupportedException NotSupportedException
ToXmlString, FromXmlString NotSupportedException NotSupportedException

The post-quantum types expose raw encodings only (ExportEncapsulationKey, ExportDecapsulationKey, ImportPrivateSeed, and their ML-DSA equivalents); see ML-KEM and ML-DSA.

Pattern 8 - HashAlgorithmName in HKDF, an enum in OTP

Hkdf takes a HashAlgorithmName and accepts SHA-1, SHA-256, SHA-384, and SHA-512 - anything else throws ArgumentException (hashAlgorithm). Its output is identical to the BCL HKDF for the same inputs, so the two are interchangeable (this is RFC 5869 test case 1):

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

byte[] ikm  = Convert.FromHexString("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
byte[] salt = Convert.FromHexString("000102030405060708090a0b0c");
byte[] info = Convert.FromHexString("f0f1f2f3f4f5f6f7f8f9");

byte[] bodu = Hkdf.DeriveKey(HashAlgorithmName.SHA256, ikm, outputLength: 42, salt: salt, info: info);
byte[] bcl  = HKDF.DeriveKey(HashAlgorithmName.SHA256, ikm, outputLength: 42, salt: salt, info: info);
// both: 3CB25F25FAACD57A90434F64D0362F2A2D2D0A90CF1A5A4C5DB02D56ECC4C5BF34007208D5B887185865

Hotp and Totp do not take a HashAlgorithmName; they take OtpHashAlgorithm (Sha1, Sha256, Sha512), which maps onto the BCL HMACs internally:

using Bodu.Security.Cryptography;

byte[] secret = "12345678901234567890"u8.ToArray();                 // the RFC 4226 / 6238 test secret
string hotp   = Hotp.GenerateCode(secret, counter: 0);              // "755224" (SHA-1, 6 digits)
string hotp256 = Hotp.GenerateCode(secret, counter: 0, digits: 6, algorithm: OtpHashAlgorithm.Sha256);
string totp   = Totp.GenerateCode(secret, DateTimeOffset.FromUnixTimeSeconds(59), digits: 8);   // "94287082"

Pattern 9 - constant-time comparison

System.Security.Cryptography.CryptographicOperations.FixedTimeEquals is what the library itself uses. The value types wrap it: HashValue, AuthenticationTag, and SignatureValue compare in constant time through Equals, ==, and an explicit FixedTimeEquals; the VerifyHash / TryVerifyHash extensions do the same over a freshly computed digest.

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

byte[] expected = Convert.FromHexString("ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923");
using var blake = new Blake2b();                           // 512-bit default
byte[] actual = blake.ComputeHash("abc"u8.ToArray());

bool a = CryptographicOperations.FixedTimeEquals(actual, expected);
bool b = HashValue.FromBytes(actual).FixedTimeEquals(HashValue.FromBytes(expected));
bool c = HashValue.FromBytes(actual) == HashValue.ParseHex(Convert.ToHexString(expected));
bool d = blake.VerifyHash("abc"u8.ToArray(), expected);
// all true

See Nonces, salts, tags, and secrets for the full value-type surface.

API summary

BCL surface Bodu types that plug in Notes
HashAlgorithm (ComputeHash, TransformBlock, ComputeHashAsync, CryptoStream) every hash: Blake2b, Skein*, Tiger, Whirlpool, SipHash*, Poly1305, AsconHash*, Shake, … plus AppendData / AppendDataAsync / VerifyHash* from HashAlgorithmExtensions
IncrementalHash - use AppendData + empty TransformFinalBlock
SymmetricAlgorithm (CreateEncryptor, CryptoStream) Blowfish, Skipjack, Camellia, Twofish, Serpent128, Threefish*, Serpent256/512/1024 BCL one-shots (EncryptCbc …) throw NotSupportedException
AesGcm GcmModeTransform + AesBlockCipher byte-identical; split ciphertext ‖ tag
ChaCha20Poly1305 - XChaCha20Poly1305 is a different construction
AsymmetricAlgorithm PEM / PKCS#8 / SPKI X25519, Ed25519 encrypted PKCS#8 and XML throw; ML-KEM / ML-DSA are raw-only
HashAlgorithmName Hkdf (SHA-1/256/384/512) OTP uses OtpHashAlgorithm
CryptographicOperations.FixedTimeEquals HashValue, AuthenticationTag, SignatureValue, SecretBytes, VerifyHash*

Where to go next