Updated on
Bouncy Castle is a cryptography library for .NET that ships the algorithms and formats System.Security.Cryptography does not, such as Ed25519, Blowfish, IDEA, Camellia and OpenPGP.
Where the two overlap, the built-in types do the same job with fewer moving parts. So this article covers when we still need Bouncy Castle, and then how to use it.
Let’s get started!
What Is Bouncy Castle in C#?
Bouncy Castle is a free, open-source cryptography library for .NET, published on NuGet as BouncyCastle.Cryptography and maintained by the Legion of the Bouncy Castle, an Australian charity.
It covers far more ground than the framework’s own System.Security.Cryptography. Hashes, symmetric and asymmetric ciphers, digital signatures, certificate generation, OpenPGP, CMS and TLS all live in the one package, a single BouncyCastle.Cryptography.dll that depends on no other Bouncy Castle package.
The API has the same shape for every cipher and signer, so what we learn for one carries over to the next. We ask a factory for an object, initialise it with Init() and a key, feed it data, and call a finishing method.
CipherUtilities.GetCipher() returns ciphers, SignerUtilities.GetSigner() returns signers, and digest classes such as Sha256Digest are constructed directly. Once we have written one encryption method, moving from AES to Camellia is a change to one string.
For the concepts behind these algorithms, see our guide to cryptography implementations in .NET, and the project’s own documentation and downloads for the parts this article does not cover.
When Do We Still Need Bouncy Castle on .NET 10?
Most of what this article demonstrates no longer needs a third-party package.
System.Security.Cryptography ships SHA-2 and SHA-3, AES in CBC, ECB and CFB modes, the authenticated ciphers AesGcm and ChaCha20Poly1305, RSA, ECDSA, HKDF and PBKDF2. .NET 10 adds the post-quantum algorithms as well: MLKem, MLDsa and SlhDsa.
So the rule is narrow. We reach for Bouncy Castle when the platform does not have the algorithm at all, or does not expose the format we need.
Three main cases still qualify. Ed25519 signing has no type in System.Security.Cryptography, and the approved API proposal is not yet scheduled for a release. The older block ciphers, Blowfish, IDEA, CAST-5, Camellia and SEED, are not there either, and neither is OpenPGP.
CMS signatures and RFC 3161 timestamps do not qualify: Microsoft’s System.Security.Cryptography.Pkcs package covers them, and CertificateRequest can issue a certificate signed by our own CA. Where both libraries have what we need, the built-in types take fewer lines and, CMS aside, one less dependency.
Here is the same comparison by type name:
| What we need | In .NET 10 | In Bouncy Castle |
|---|---|---|
| SHA-2 and SHA-3 hashing | SHA256, SHA3_256 | Sha256Digest, Sha3Digest |
| MD5 (legacy interop only) | MD5 | MD5Digest |
| AES in CBC, ECB and CFB modes | Aes | GetCipher() with "AES/CBC/PKCS7Padding" |
| Authenticated encryption | AesGcm, AesCcm, ChaCha20Poly1305 | GcmBlockCipher, ChaCha20Poly1305 |
| RSA encryption and signing | RSA | RsaEngine with OaepEncoding or Pkcs1Encoding, RsaDigestSigner |
| ECDSA signing | ECDsa | ECDsaSigner |
| Post-quantum KEM and signatures | MLKem, MLDsa, SlhDsa | MLKemKeyPairGenerator, MLDsaSigner, SlhDsaSigner |
| Password-based key derivation | Rfc2898DeriveBytes.Pbkdf2 (PBKDF2) | Pkcs5S2ParametersGenerator, SCrypt, Argon2BytesGenerator |
| Building an X.509 certificate in code | CertificateRequest (self-signed, CA-signed or as a CSR) | X509V3CertificateGenerator |
| CMS signatures and RFC 3161 timestamps | SignedCms, Rfc3161TimestampRequest (System.Security.Cryptography.Pkcs package) | CmsSignedDataGenerator, TimeStampRequestGenerator |
| Ed25519 signing | Not available | Ed25519Signer |
| Blowfish, IDEA, CAST-5, Camellia, SEED | Not available | GetCipher() with "Blowfish/CBC/PKCS7Padding" |
| OpenPGP | Not available | PgpEncryptedDataGenerator, PgpSecretKeyRingBundle |
The post-quantum types need platform support underneath them: OpenSSL 3.5 or newer, or Windows CNG with PQC. Each type exposes a static IsSupported property to check at runtime, and on a machine without that support it returns false. SHA3_256 and ChaCha20Poly1305 have the same check, because they depend on the operating system too: both return false on the Windows 10 machine we tested this article on, where Bouncy Castle’s Sha3Digest and ChaCha20Poly1305 still run. MLKem and MLDsa are ordinary API on .NET 10, though some of their members, mostly key import and export ones, are still marked [Experimental] under SYSLIB5006. SlhDsa is experimental in its entirety, so using it at all needs that diagnostic suppressed.
If AES is all we need, encrypting and decrypting a string in C# with the built-in Aes class takes no package at all.
How Do We Install Bouncy Castle?
We can install Bouncy Castle via the .NET command line:
dotnet add package BouncyCastle.Cryptography
If an older project references BouncyCastle, that package is marked deprecated on NuGet and points at BouncyCastle.Cryptography as its replacement. Portable.BouncyCastle and BouncyCastle.NetCore are not flagged, but neither has had a release since 2021 and 2023 respectively. BouncyCastle.Cryptography is the maintained package.
In the examples that follow, we put the calling lines at the top of each code block in Program.cs, one algorithm at a time. Everything from the first public or private member down goes in a public static class named as the call names it, such as Md5Hasher, in its own file that starts with these usings:
using System.Text; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Encodings; using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security;
How Do We Hash Data With Bouncy Castle?
A hash function maps input of any length to a fixed-size byte array, and the same input always produces the same bytes. The mapping runs one way only, so the digest can be published without revealing what it was computed from. That is what makes it useful for checking that a downloaded file matches what the publisher released.
In Bouncy Castle, each algorithm is a digest class. We construct it, allocate a byte[] of GetDigestSize() bytes, push the data through BlockUpdate(), and call DoFinal() to write the result into that array.
MD5 is broken and belongs only in code that has to interoperate with something old, so Sha256Digest is the safe default here.
Hashing a password with a bare digest is a separate mistake, whichever algorithm we pick, because a raw hash is fast and that is exactly what an attacker needs.
For passwords specifically, see hashing and salting passwords in C# and securing passwords with BCrypt.
MD5 Hashing
Do not use MD5 to protect anything. It has been broken since 2004, and new code should use SHA-256. It stays in this article because plenty of existing systems still publish MD5 checksums that we have to verify, and Bouncy Castle’s digest API is the same for every algorithm.
Let’s begin with performing an MD5 hash with Bouncy Castle:
var secretValue = "This is my password! Dont read me!";
var hash = Md5Hasher.Md5Hash(secretValue);
public static byte[] Md5Hash(string secret)
{
var hash = new MD5Digest();
var result = new byte[hash.GetDigestSize()];
var data = Encoding.UTF8.GetBytes(secret);
hash.BlockUpdate(data, 0, data.Length);
hash.DoFinal(result, 0);
return result;
}
To perform an MD5 hash, first, we create an MD5Digest object that we will use to create the hash. Next, we create a byte array, result, the size of the digest. We then convert the secret value to UTF-8 encoded byte array. Next, we update the hash digest object with a byte array. We do this by calling BlockUpdate() with blocks of bytes the size of our byte array. Finally, we call DoFinal() on the digest object to produce the final hash.
This will produce a hash of our secret, shown here in Base64:
l5aMGrOOPOxm6i6cRK/sqA==
SHA Hashing
Let’s move on to SHA hashing.
We can use Bouncy Castle to perform a SHA256 hash:
var secretValue = "This is my password! Dont read me!";
var hash = ShaHasher.ShaHash(secretValue);
public static byte[] ShaHash(string secret)
{
var hash = new Sha256Digest();
var result = new byte[hash.GetDigestSize()];
var data = Encoding.UTF8.GetBytes(secret);
hash.BlockUpdate(data, 0, data.Length);
hash.DoFinal(result, 0);
return result;
}
As we are using Bouncy Castle the code is very similar to creating an MD5 hash although they are different algorithms altogether. In this case, we create a Sha256Digest object to perform the hashing. Similarly, we convert the secret into a byte array. We then load the byte array in the hash block. Lastly, like before, we call DoFinal() on the digest object to produce our final hash.
This is an example of what a SHA256 hashed value would look like in Base64:
SSNYFZG3vQSGtXqZVaSZry4QBm4uueLwegd/gd6H8ns=
How Do We Encrypt With Symmetric Algorithms in Bouncy Castle?
Symmetric encryption, also known as secret key encryption, is a class of encryption algorithms that use a single secret key to perform encryption and decryption of a data set. This means the party that wants to decrypt data can only do so if they have obtained this key from whoever encrypted the data. Bouncy Castle allows us to easily use various symmetric encryption algorithms to protect data.
In the following sections we will discuss how to perform specific symmetric encryption algorithms, but let’s discuss what options Bouncy Castle offers. Symmetric encryption always begins by creating a cipher object. We use the CipherUtilities.GetCipher("AlgorithmName/OperationMode/PaddingStyle") method to make this cipher object. This method accepts a string with three sections that are delimited by a forward slash. The first section describes the algorithm the cipher should use. The second section describes the operation mode for the cipher. Last, the final section describes which padding style the cipher should use.
The string our AES example uses, AES/CBC/PKCS7Padding, splits into those three sections like this:

Let’s take a look at the options we can choose from for each of these sections:
Algorithm:
- AES:
"AES" - DES:
"DES" - Triple DES (DESede):
"DESede","TDEA" - Blowfish:
"Blowfish" - CAST-5:
"CAST5" - IDEA:
"IDEA" - Camellia:
"Camellia" - SEED:
"SEED"
Mode of Operation:
- Electronic Codebook (ECB):
"ECB" - Cipher Block Chaining (CBC):
"CBC" - Cipher Feedback (CFB):
"CFB" - Output Feedback (OFB):
"OFB" - Counter (CTR):
"CTR" - Galois/Counter Mode (GCM):
"GCM"
Padding Style:
- NoPadding:
"NoPadding" - PKCS5Padding:
"PKCS5Padding" - PKCS7Padding:
"PKCS7Padding" - ISO10126Padding:
"ISO10126Padding" - ISO7816d4Padding:
"ISO7816-4Padding" - TBCPadding:
"TBCPadding" - X923Padding:
"X9.23Padding" - ZeroBytePadding:
"ZeroBytePadding"
AES Encryption
First, let’s take a look at a code example of how to perform AES encryption:
string input = "Hello, Bouncy Castle!";
var encryptedData = AesEncryptor.AesEncrypt(input, out byte[] iv, out byte[] key);
public static byte[] AesEncrypt(string input, out byte[] iv, out byte[] key)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
// Generate a random 128-bit key
key = new byte[16];
var random = new SecureRandom();
random.NextBytes(key);
// Generate a random 128-bit initialization vector (IV)
iv = new byte[16];
random.NextBytes(iv);
// Create AES cipher with CBC mode
var cipher = CipherUtilities.GetCipher("AES/CBC/PKCS7Padding");
cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv));
return cipher.DoFinal(inputBytes);
}
We begin by transforming our input data set into a byte array using UTF8.GetBytes(). Next, we create two randomly generated byte arrays for the key and initialization vector (IV). Now that we have a key and IV, we generate a cipher object and initialize it with our key and IV. The first parameter of Init() is a boolean that indicates if we are encrypting. Finally, we call the DoFinal() method on the cipher object to produce the encrypted data.
Outside Bouncy Castle, .NET’s own RandomNumberGenerator plays the part of SecureRandom, and we cover it in RandomNumberGenerator in C#: Secure Random Numbers.
Now that we have encrypted data, let’s take a look at how we can perform decryption using Bouncy Castle to recover the original data:
public static string AesDecrypt(byte[] key, byte[] iv, byte[] encryptedBytes)
{
var cipher = CipherUtilities.GetCipher("AES/CBC/PKCS7Padding");
cipher.Init(false, new ParametersWithIV(new KeyParameter(key), iv));
// Decrypt the encrypted data
var decryptedBytes = cipher.DoFinal(encryptedBytes);
return Encoding.UTF8.GetString(decryptedBytes);
}
We start by creating a cipher object with the same algorithm and mode used to encrypt the data. This is very important because if the cipher is not the same the decryption would not work. Next, we initialize the cipher with the same IV and key as the encryption. Additionally, we provide a false value in the Init() method to indicate we will be decrypting.
Blowfish Encryption
Moving on, let’s take a look at how we can encrypt data using Blowfish:
var input = "Hello, Bouncy Castle!";
var password = "mysecretpassword";
var encryptedBytes = BlowfishEncryptor.BlowfishEncrypt(input, password, out byte[] salt, out byte[] iv);
private const int Iterations = 600_000;
public static byte[] BlowfishEncrypt(string input, string password, out byte[] salt, out byte[] iv)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var random = new SecureRandom();
salt = new byte[16];
random.NextBytes(salt);
iv = new byte[8]; // Blowfish uses an 8-byte (64-bit) IV
random.NextBytes(iv);
var cipher = CipherUtilities.GetCipher("Blowfish/CBC/PKCS7Padding");
var keyParam = DeriveKey(password, salt);
cipher.Init(true, new ParametersWithIV(keyParam, iv));
return cipher.DoFinal(inputBytes);
}
private static KeyParameter DeriveKey(string password, byte[] salt)
{
// PBKDF2 with HMAC-SHA256 stretches the password and salt into a 128-bit key
var generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
generator.Init(Encoding.UTF8.GetBytes(password), salt, Iterations);
return (KeyParameter)generator.GenerateDerivedParameters("Blowfish", 128);
}
A password is not a key, so we do not pass its bytes straight to KeyParameter. Those bytes would carry only as much randomness as the password itself, and Blowfish throws an ArgumentException for any key shorter than 4 bytes or longer than 56. Instead, we generate a random salt and a random IV, and DeriveKey() runs the password and the salt through PBKDF2 with HMAC-SHA256 to produce a 128-bit key. The 600,000 iterations follow the OWASP Password Storage Cheat Sheet, which recommends that count for PBKDF2 with HMAC-SHA256.
Next, we create a cipher object using GetCipher() and we specify we want to use Blowfish with CBC mode and PKCS7 padding. Again, we initialize the cipher set to encryption. Finally, we produce the final encryption by calling DoFinal().
Bouncy Castle’s BlowfishEngine works on 64-bit blocks over 16 rounds, and it reads each 8-byte block as two big-endian 32-bit words before encrypting, writing them back big-endian afterwards. The first four bytes of a block become the left word, which is why the engine reproduces the published Blowfish test vectors byte for byte.
Lastly, let’s take a look at how we can decrypt a data set we encrypted using Blowfish:
public static string BlowfishDecrypt(byte[] encryptedBytes, string password, byte[] salt, byte[] iv)
{
var cipher = CipherUtilities.GetCipher("Blowfish/CBC/PKCS7Padding");
var keyParam = DeriveKey(password, salt);
cipher.Init(false, new ParametersWithIV(keyParam, iv));
var plainBytes = cipher.DoFinal(encryptedBytes);
return Encoding.UTF8.GetString(plainBytes);
}
To perform the decryption, we initialize a new cipher with the same settings as we did when it was encrypted and set it to decryption mode. Next, we derive the same key again from the password and the salt we generated during encryption, so the salt has to be stored next to the ciphertext and the IV. Finally, we call DoFinal() to receive our original data back.
How Do We Encrypt With Asymmetric Algorithms in Bouncy Castle?
Asymmetric encryption is also known as public key encryption. They are a class of encryption algorithms that use a public key to encrypt data and a private key to perform decryption of a data set. The public and private keys are mathematically related. Only a private key related to the public key can decrypt the data set. Bouncy Castle enables us to perform asymmetric encryptions. In the following section, we will cover two asymmetric algorithms: RSA, and DSA. It should be noted that DSA is typically used to digitally sign files as a security measure to prove files have not been tampered with while exchanging between two parties.
RSA Encryption
Here is how we can perform RSA encryption using Bouncy Castle:
string input = "Hello, Bouncy Castle!";
// Generate RSA key pair
var keyPair = RsaEncryptor.GenerateRsaKeyPair();
var encryptedBytes = RsaEncryptor.RsaEncrypt(input, keyPair.Public);
public static AsymmetricCipherKeyPair GenerateRsaKeyPair()
{
var rsaKeyPairGen = new RsaKeyPairGenerator();
rsaKeyPairGen.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
return rsaKeyPairGen.GenerateKeyPair();
}
public static byte[] RsaEncrypt(string input, AsymmetricKeyParameter publicKey)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var cipher = new OaepEncoding(new RsaEngine(), new Sha256Digest());
cipher.Init(true, publicKey);
return cipher.ProcessBlock(inputBytes, 0, inputBytes.Length);
}
Here we see the pattern for asymmetric encryption is different from symmetric encryption. First, we generate a key pair by creating a RsaKeyPairGenerator object. We initialize it with a random number generator of type SecureRandom and an int indicating the strength of the encryption using a bit length number. Lastly, we call GenerateKeyPair() to produce the private and public keys.
Now that we have a key pair, we create a cipher object by providing an RsaEngine object and a Sha256Digest to an OaepEncoding object. OAEP is the padding to use for new code: RFC 8017, the PKCS #1 standard, keeps the older PKCS #1 v1.5 encryption padding “only for compatibility with existing applications”. We pass the digest because the constructor without it falls back to SHA-1. Next, we initialize the cipher with the encryption mode as true and the public key we generated. Finally, we call ProcessBlock() with our input byte array to encrypt, an offset value for where to start reading our input, and the length of the input. This will produce an encrypted byte array representation of our input data.
Now, let’s discuss how we can recover our data through decryption:
string decryptedString = RsaEncryptor.RsaDecrypt(encryptedBytes, keyPair.Private);
public static string RsaDecrypt(byte[] encryptedBytes, AsymmetricKeyParameter privateKey)
{
var cipher = new OaepEncoding(new RsaEngine(), new Sha256Digest());
// Initialize the cipher for decryption with the private key
cipher.Init(false, privateKey);
// Decrypt the encrypted data
var decryptedBytes = cipher.ProcessBlock(encryptedBytes, 0, encryptedBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
Again, we create a cipher with the same algorithm engine object and padding style as used in encryption. Next, we initialize the cipher in decryption mode and pass in the private key this time. Now, we call the ProcessBlock() method again to produce the original byte array data.
Creating Digital Signatures
Another application of asymmetric cryptography is digital signatures used for secure file transfers. In this section, we will use the DSA algorithm to achieve this goal.
Let’s start by looking at an example of how we can digitally sign a byte array:
string input = "Hello, Bouncy Castle!";
var keyPair = DsaEncryptor.GenerateDsaKeyPair();
var signature = DsaEncryptor.DsaSign(input, keyPair.Private);
public static AsymmetricCipherKeyPair GenerateDsaKeyPair()
{
// Create DSA parameters
var dsaParamsGenerator = new DsaParametersGenerator(new Sha256Digest());
var random = new SecureRandom();
dsaParamsGenerator.Init(new DsaParameterGenerationParameters(2048, 256, 80, random)); // key size: 2048 bits, q size: 256 bits, certainty: 80
// Generate DSA key pair
var dsaParams = dsaParamsGenerator.GenerateParameters();
var dsaKeyParams = new DsaKeyGenerationParameters(random, dsaParams);
var dsaKeyPairGen = new DsaKeyPairGenerator();
dsaKeyPairGen.Init(dsaKeyParams);
return dsaKeyPairGen.GenerateKeyPair();
}
public static byte[] DsaSign(string message, AsymmetricKeyParameter privateKey)
{
var messageBytes = Encoding.UTF8.GetBytes(message);
var signer = SignerUtilities.GetSigner("SHA256withDSA");
signer.Init(true, privateKey);
signer.BlockUpdate(messageBytes, 0, messageBytes.Length);
return signer.GenerateSignature();
}
In this example, we are signing a byte array produced from a string, but in a practical example, we would read in a file as a byte array and perform all the same operations on it.
Firstly, we start by creating a key pair we can use for signing. Create a DsaParametersGenerator object with a Sha256Digest and initialize it with a DsaParameterGenerationParameters object that carries the key size, the 256-bit size of the subgroup, a certainty value and a secure random number generator. The shorter Init(size, certainty, random) overload only accepts sizes up to 1024 bits, and GenerateParameters() then throws unless the digest is SHA-1, so a 2048-bit key with SHA-256 needs the parameters object. Next, we generate the parameters by calling GenerateParameters() and pass them into the constructor of DsaKeyGenerationParameters. We follow this up by creating a DsaKeyPairGenerator object which we initialize with the previously created DsaKeyGenerationParameters. Lastly, we generate the keys by calling GenerateKeyPair().
Now that we have the DSA keys we can begin the signing process. Starting off, we create an ISigner implementation by calling SignerUtilities.GetSigner("SHA256withDSA"). We initialize the signer by calling Init() and indicating we are signing with the true value and our private key. Once we have initialized the signer, we call BlockUpdate() to pass in our data, an offset value, and the data array length. We produce the signature by calling GenerateSignature(). Signing does not obscure the original data.
DSA is a poor choice for new signatures, though. FIPS 186-5, the Digital Signature Standard NIST published in 2023, “no longer approves the DSA for digital signature generation”. New systems should sign with ECDSA or Ed25519, both of which the same standard approves. We keep DSA in this example because Bouncy Castle’s signing API looks the same whichever algorithm we pick.
Validating Digital Signatures
We can also verify the signature on the data using Bouncy Castle:
var isSignatureValid = DsaEncryptor.DsaVerify(input, signature, keyPair.Public);
public static bool DsaVerify(string message, byte[] signature, AsymmetricKeyParameter publicKey)
{
var messageBytes = Encoding.UTF8.GetBytes(message);
var signer = SignerUtilities.GetSigner("SHA256withDSA");
signer.Init(false, publicKey);
signer.BlockUpdate(messageBytes, 0, messageBytes.Length);
return signer.VerifySignature(signature);
}
Begin by creating another ISigner implementation with the same parameters as the one used to produce the signature. We initialize it with a false value and pass in the public key we generated before. We call BlockUpdate() on the signer passing in the input data, an offset, and the data array length. Lastly, we verify the signature by calling VerifySignature() and passing in the signature we produced.
Conclusion
Bouncy Castle allows us to perform a variety of cryptographic operations with an easy-to-use and consistent syntax. Data security remains an ever-important aspect of development. This is a great library to enable data security in applications!
Tested with .NET 10.0.10 and BouncyCastle.Cryptography 2.7.0.
