Cryptography in .NET

 
 
  • Gérald Barré

Cryptography is a very important thing for information security. Information security is composed of 4 parts:

  • Integrity: ensure a document is not altered
  • Confidentiality: ensure only authorized people can read a document
  • Authentication: ensure the document was written by an identified person
  • Non-Repudiation: prove who/where a document came from as well as the authenticity of that message, so the sender cannot deny they have sent it

Let's see what .NET provides for each part!

#Generating random numbers

Random number generation is a common way to generate cryptographic keys. These keys have to be as random as possible so that it is infeasible to predict. Don't use System.Random for generating cryptographic numbers. Instead, use System.Security.Cryptography.RNGCryptoServiceProvider.

C#
byte[] GetRandomData(int length)
{
    using (var rngCsp = new System.Security.Cryptography.RNGCryptoServiceProvider())
    {
        var randomData = new byte[length];
        rngCsp.GetBytes(randomData);
        return randomData;
    }
}

// .NET Core 2.1+
byte[] GetRandomData(int length)
{
    var randomData = new byte[length];
    RandomNumberGenerator.Fill(randomData);
    return randomData;
}

// .NET 6
byte[] GetRandomData(int length)
{
    return RandomNumberGenerator.GetBytes(length);
}

#Generating a cryptographic key from a password

A password has a variable length and is often composed only of what the user can type with their keyboard. In this form, you cannot use it to cipher data. Indeed, if you want to cipher a block of data using AES 128, you need a fixed-length key of 128 bits. Instead, you need to create a cryptographic key of the desired size from the user's password. This is what "Password-Based Key Derivation Function 2" (PBKDF2) does. In .NET, the class that implements this algorithm is Rfc2898DeriveBytes.

C#
string password = "foobar";

byte[] salt;
using (var derivedBytes = new System.Security.Cryptography.Rfc2898DeriveBytes(password, saltSize: 16, iterations: 50000, HashAlgorithmName.SHA256))
{
    salt = derivedBytes.Salt;
    byte[] key = derivedBytes.GetBytes(16); // 128 bits key
    Console.WriteLine(Convert.ToBase64String(key)); // Qs117rioEMXqseslxc5X4A==
}

You can recreate the same key by using the same password, the same number of iterations, the same salt, and the same algorithm. This means you need to store these information (except the password!) somewhere to be able to reuse them later.

C#
using (var derivedBytes = new Rfc2898DeriveBytes(password, salt, iterations: 50000, HashAlgorithmName.SHA256))
{
    byte[] key = derivedBytes.GetBytes(16); // Same as the first generated key
    Console.WriteLine(Convert.ToBase64String(key)); // Qs117rioEMXqseslxc5X4A==
}

// .NET 6+
Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations: 50000, HashAlgorithmName.SHA256, outputLength: 16);
Console.WriteLine(Convert.ToBase64String(key)); // Qs117rioEMXqseslxc5X4A==

If you change one of the parameters, you should get another key:

C#
// Number of iteration changed (49999 instead of 50000)
using (var derivedBytes = new Rfc2898DeriveBytes(password, salt, iterations: 49999, HashAlgorithmName.SHA256))
{
    byte[] key = derivedBytes.GetBytes(16);
    Console.WriteLine(Convert.ToBase64String(key)); // N3Zi7D9jcf4WMV26YAKmDg==
}

You can also use this algorithm to validate a password, but this is for another post.

#Hash algorithms

Hash algorithms convert a binary message of an arbitrary length to a smaller binary value of a fixed length. The same input must output the same hash. If you change a single byte of the input, the hash should be different. If the hash is cryptographically strong, its value will change significantly. A hash algorithm should have the following characteristics:

  • It should be infeasible to generate a specific hash
  • It should be infeasible to modify a message without changing the hash
  • It should be infeasible to find 2 different inputs with identical hashes

When someone wants to send data to someone else, they can send the data and the hash of the data. The receiver can compute the hash of the received data and compare it to the value of the hash sent by the emitter. This way the receiver can validate the data haven't been altered during the transmission. So, you can validate the integrity of the data.

In .NET you can use any class that inherits from HashAlgorithm. However, some algorithms, such as MD5 or SHA1, are considered as vulnerable because they do not respect the previous characteristics. You can use any algorithms derived from SHA2 such as SHA256, SHA384, or SHA512. SHA3 algorithms are supported on .NET 8.

C#
byte[] ComputeHash(byte[] data)
{
    using (var sha256 = SHA256.Create())
    {
        return sha256.ComputeHash(data);
    }
}

// .NET 5.0+
byte[] ComputeHash(byte[] data) => Sha256.HashData(data);

#Authenticated Hashing (HMAC)

Hash-based message authentication code (HMAC) may be used to verify both the data integrity and the authentication of a message. It works like a classic hash algorithm except you need a secret key to generate and validate the hash: HMAC = Hash + Cryptographic key. Only someone with the secret key can generate and validate the hash. As long as the key is secret, you know who has generated the hash.

HMAC is very useful when you send a message to someone and you expect them to send it back. Then, you can validate the message was effectively generated by you and hasn't been altered. For instance, ASP.NET WebForms send the ViewState and its HMAC to the client. When the client sends a POST request to the server, it sends back the value of the ViewState. The server can validate that the HMAC of the ViewState to ensure it has been generated by the server and hasn't been altered by the client.

C#
byte[] ComputeHmac256(byte[] key, byte[] data)
{
    using (var hmac = new HMACSHA256(key))
    {
        return hmac.ComputeHash(data);
    }
}

// .NET 6
byte[] ComputeHmac256(byte[] key, byte[] data)
{
    return HMACSHA256.HashData(key, data);
}

#Encryption

Encryption scrambles the content of a file so only authorized people can read it. There are 2 ways to encrypt data. The first one is known as symmetric encryption. In this kind of algorithms, the key to encrypt and decrypt data is the same. This means you need a secure way to transmit the key to other people. The other way is asymmetric encryption. The keys to encrypt and decrypt data are different. We often use the terms public key and private key. The public key allows you to encrypt data, so you can share it with anyone. The private key allows you to decrypt data, so you must key it secret.

The main symmetric algorithm is AES, and RSA is the most widely used asymmetric algorithm. AES is very fast and can be used with data of any length. RSA is far much slower and cannot encrypt data that is longer than the size of the key.

##Symmetric encryption

.NET provides multiple symmetric algorithms and multiple implementations for some algorithms:

  • AES: AESManaged, AesCng, AesCryptoServiceProvider
  • AES-GCM: AesGcm
  • DES: DESCryptoServiceProvider
  • RC2: RC2CryptoServiceProvider
  • Rijndael: RijndaelManaged
  • TripleDES: TripleDESCng, TripleDESCryptoServiceProvider

###AES

If you don't know which one to use, you may want to start with AES. AESManaged is fully implemented in .NET, however, the implementation is not FIPS compliant. AESCryptoServiceProvider uses the Windows implementation (CryptoAPI) which is FIPS compliant. Or you can create an instance of the best available provider using AES.Create().

There are multiple modes of AES, each has its usage. This Stack Overflow answer describes very well each mode. Note that some of them are not yet implemented in .NET.

C#
static void Main()
{
    var key = GetRandomData(256 / 8);

    var data = new byte[] { 1, 2, 3 };
    var encryptedData = Encrypt(data, key, out var iv);
    var decryptedData = Decrypt(encryptedData, key, iv);
}

static byte[] Encrypt(byte[] data, byte[] key, out byte[] iv)
{
    using var aes = Aes.Create();

    // You should adjust the mode depending on what you want to encrypt.
    // However, some mode may be weak or require additional security steps such as CBC: https://learn.microsoft.com/en-us/dotnet/standard/security/vulnerabilities-cbc-mode?WT.mc_id=DT-MVP-5003978
    aes.Mode = CipherMode.CBC;

    aes.Key = key;
    aes.GenerateIV(); // You must use a new IV for each encryption for security purpose

    using (var cryptoTransform = aes.CreateEncryptor())
    {
        iv = aes.IV;
        return cryptoTransform.TransformFinalBlock(data, 0, data.Length);
    }
}

static byte[] Decrypt(byte[] data, byte[] key, byte[] iv)
{
    using var aes = Aes.Create();
    aes.Key = key;
    aes.IV = iv;
    aes.Mode = CipherMode.CBC; // same as for encryption

    using (var cryptoTransform = aes.CreateDecryptor())
    {
        return cryptoTransform.TransformFinalBlock(data, 0, data.Length);
    }
}

###AES-GCM (AEAD)

AES-GCM provides confidentiality and integrity. Like other AES modes, it encrypts the data using a key (confidentiality). When decrypting the data, it ensures the message has not been altered (integrity) which is not done with other modes. You can consider it as a safe way to combine AES and HMAC.

C#
static void Main()
{
    var key = GetRandomData(256 / 8);

    var data = new byte[] { 1, 2, 3 };
    byte[]? associatedData = null; // Optional. If possible, provide something associated to the current context such as the current user id https://crypto.stackexchange.com/a/6716/16630
    var encryptedAesGcmData = EncryptAesGcm(data, key, associatedData, out var tag, out var nonce);
    var decryptedAesGcmData = DecryptAesGcm(encryptedAesGcmData, key, associatedData, tag, nonce);
}

public static byte[] EncryptAesGcm(byte[] data, byte[] key, byte[]? associatedData, out byte[] tag, out byte[] nonce)
{
    tag = new byte[AesGcm.TagByteSizes.MaxSize];
    nonce = new byte[AesGcm.NonceByteSizes.MaxSize];
    RandomNumberGenerator.Fill(nonce);

    byte[] cipherText = new byte[data.Length];
    using var cipher = new AesGcm(key);
    cipher.Encrypt(nonce, data, cipherText, tag, associatedData);
    return cipherText;
}

public static byte[] DecryptAesGcm(byte[] data, byte[] key, byte[]? associatedData, byte[] tag, byte[] nonce)
{
    byte[] decryptedData = new byte[data.Length];
    using var cipher = new AesGcm(key);
    cipher.Decrypt(nonce, data, tag, decryptedData, associatedData);
    return decryptedData;
}

###ChaCha20Poly1305 (AEAD)

ChaCha20Poly1305 (RFC 8439) is an Authenticated Encryption with Associated Data (AEAD) cipher amenable to fast, constant-time implementations in software, based on the ChaCha20 stream cipher and Poly1305 universal hash function. ChaCha20Poly1305 is a good alternative to AES-GCM when the machine doesn't have hardware AES support. Indeed, AES is vulnerable to timing based side channels if done in software. Cloudflare wrote two good posts about ChaCha20Poly1305: It takes two to ChaCha (Poly), Do the ChaCha: better mobile performance with cryptography.

C#
static void Main()
{
    var key = GetRandomData(32);

    var data = new byte[] { 1, 2, 3 };
    byte[]? associatedData = null; // Optional. If possible, provide something associated to the current context such as the current user id https://crypto.stackexchange.com/a/6716/16630
    var encryptedChaCha20Poly1305 = EncryptChaCha20Poly1305(data, key, associatedData, out var tag, out var nonce);
    var decryptedChaCha20Poly1305 = DecryptChaCha20Poly1305(encryptedChaCha20Poly1305, key, associatedData, tag, nonce);
}

public static byte[] EncryptChaCha20Poly1305(byte[] data, byte[] key, byte[]? associatedData, out byte[] tag, out byte[] nonce)
{
    tag = new byte[16];
    nonce = GetRandomData(12);
    byte[] cipherText = new byte[data.Length];
    using var cipher = new  ChaCha20Poly1305(key);
    cipher.Encrypt(nonce, data, cipherText, tag, associatedData);
    return cipherText;
}

public static byte[] DecryptChaCha20Poly1305(byte[] data, byte[] key, byte[]? associatedData, byte[] tag, byte[] nonce)
{
    byte[] decryptedData = new byte[data.Length];
    using var cipher = new ChaCha20Poly1305(key);
    cipher.Decrypt(nonce, data, tag, decryptedData, associatedData);
    return decryptedData;
}

##Asymmetric encryption

.NET provides multiple asymmetric algorithms and multiple implementations for some algorithms:

  • DSA: DSACng, DSACryptoServiceProvider, DSAOpenSsl
  • ECDiffieHellman: ECDiffieHellmanCng, ECDiffieHellmanOpenSsl
  • ECDsa: ECDsaCng, ECDsaOpenSsl
  • RSA: RSACng, RSACryptoServiceProvider, RSAOpenSsl

To encrypt data, you can use RSA. For the key length, you should use at least 2048 (NSA recommends 3072 or larger ref). Note that RSA can only encrypt data up to the key length (including padding). So, with a key of 2048 bits, you cannot encrypt a blob of 4096 bits. You'll need to split it into 2 blobs and encrypt them separately.

C#
static void Main(string[] args)
{
    var data = new byte[] { 1, 2, 3 };
    var (publicKey, privateKey) = GenerateKeys(2048);

    var encryptedData = Encrypt(data, publicKey);
    var decryptedData = Decrypt(encryptedData, privateKey);
}

static (RSAParameters publicKey, RSAParameters privateKey) GenerateKeys(int keyLength)
{
    using (var rsa = RSA.Create())
    {
        rsa.KeySize = keyLength;
        return (
            publicKey: rsa.ExportParameters(includePrivateParameters: false),
            privateKey: rsa.ExportParameters(includePrivateParameters: true)
        );
    }
}

static byte[] Encrypt(byte[] data, RSAParameters publicKey)
{
    using (var rsa = RSA.Create())
    {
        rsa.ImportParameters(publicKey);

        var result = rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256);
        return result;
    }
}

static byte[] Decrypt(byte[] data, RSAParameters privateKey)
{
    using (var rsa = RSA.Create())
    {
        rsa.ImportParameters(privateKey);
        return rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256);
    }
}

##Hybrid encryption

Hybrid encryption is a combination of symmetric and asymmetric encryption algorithms. You'll often generate a symmetric key and encrypt it using an asymmetric algorithm. This way you take advantage of the asymmetric algorithm to exchange the key, and you can use the speed of AES compared to RSA to encrypt the data. Plus, the symmetric key is small enough to be encrypted using RSA. Note that you can also HMAC the encrypted data using the symmetric key to validate the integrity of the data.

#Digital signature

A digital signature allows verifying the authenticity of a document. You know who has signed the document and you can validate the document has not been altered after the signature. A signature works similarly as asymmetric encryption. You'll sign the document (or a hash of the document) using your private key. And anyone will be able to validate the signature using your public key. If the public key is associated with a certificate, you'll be able to validate the identity of the person who signed the document.

C#
static void Main(string[] args)
{
    var (publicKey, privateKey) = GenerateKeys(2048);

    var data = new byte[] { 1, 2, 3 };
    var signedData = Sign(data, privateKey);
    var isValid = VerifySignature(data, signedData, publicKey);
}

static (RSAParameters publicKey, RSAParameters privateKey) GenerateKeys(int keyLength)
{
    using (var rsa = RSA.Create())
    {
        rsa.KeySize = keyLength;
        return (
            publicKey: rsa.ExportParameters(includePrivateParameters: false),
            privateKey: rsa.ExportParameters(includePrivateParameters: true));
    }
}

static byte[] Sign(byte[] data, RSAParameters privateKey)
{
    using (var rsa = RSA.Create())
    {
        rsa.ImportParameters(privateKey);

        // the hash to sign
        byte[] hash;
        using (var sha256 = SHA256.Create())
        {
            hash = sha256.ComputeHash(data);
        }

        var rsaFormatter = new RSAPKCS1SignatureFormatter(rsa);
        rsaFormatter.SetHashAlgorithm("SHA256");
        return rsaFormatter.CreateSignature(hash);
    }
}

private static bool VerifySignature(byte[] data, byte[] signature, RSAParameters publicKey)
{
    using (var rsa = RSA.Create())
    {
        rsa.ImportParameters(publicKey);

        // the hash to sign
        byte[] hash;
        using (var sha256 = SHA256.Create())
        {
            hash = sha256.ComputeHash(data);
        }

        var rsaFormatter = new RSAPKCS1SignatureDeformatter(rsa);
        rsaFormatter.SetHashAlgorithm("SHA256");
        return rsaFormatter.VerifySignature(hash, signature);
    }
}

##XML document signature

There is a standard for signing XML documents. It works mostly the same as the general signature algorithm, except the signature can be included in the document. You can also sign only a subset of the document if needed.

C#
static void Main(string[] args)
{
    var (publicKey, privateKey) = GenerateKeys(2048);

    var document = new XmlDocument();
    document.LoadXml("<Root><Author>Meziantou</Author></Root>");

    SignXml(document, privateKey);
    var isValidXmlSignature = VerifyXmlSignature(document, publicKey);
}

public static void SignXml(XmlDocument xmlDocument, RSAParameters privateKey)
{
    using (var rsa = RSA.Create())
    {
        rsa.ImportParameters(privateKey);

        var signedXml = new SignedXml(xmlDocument);
        signedXml.SigningKey = rsa;

        // Create a reference to be signed
        var reference = new Reference(""); // "" means entire document, https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.xml.reference.uri?WT.mc_id=DT-MVP-5003978&view=dotnet-plat-ext-6.0
        var env = new XmlDsigEnvelopedSignatureTransform();
        reference.AddTransform(env);

        // Add the reference to the SignedXml object and compute the signature
        signedXml.AddReference(reference);
        signedXml.ComputeSignature();

        // Get the XML representation of the signature and add it to the document
        XmlElement xmlDigitalSignature = signedXml.GetXml();
        xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(xmlDigitalSignature, deep: true));
    }
}

public static bool VerifyXmlSignature(XmlDocument xmlDocument, RSAParameters publicKey)
{
    using (var rsa = RSA.Create())
    {
        rsa.ImportParameters(publicKey);

        var signatureElement = xmlDocument.GetElementsByTagName("Signature").OfType<XmlElement>().FirstOrDefault();
        var signedXml = new SignedXml(xmlDocument);
        signedXml.LoadXml(signatureElement);

        return signedXml.CheckSignature(rsa);
    }
}

Here's the signed xml document:

XML
<Root>
  <Author>Meziantou</Author>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
        <DigestValue>/nqzc97wNrLZ03VLo8ycnAFEOduEHAyUeP4nnPaiWU8=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>MdtKVRg+esWEDv8+TqAt0XLWd7kzgWvluBk6i0IyirUMPnUKifnkA7DfRRKifXQagP+1jZ4LZ9dLNCzul1Y8w8ZZeE7dy40pdgYcppHl+1dq4qRnymz2yDwU3bg50ZoAAyLVcW3fn7AuN1QS4eOj5fhird9epIQQdVZz8f8hZMGHgpAhR+c2MFPW6EmzeAQ7XBrhMtc9GhIrwMCUczkSYFOHYp+jaTYPb8hfVvW2ACmApsKw5/a3uxQS/9+n4CTy4y5mdksjKZLRMOtLRlzStg4CSUnsYYsJK+1y3yfyQlIQuglTVi+K8yEX8ZI+C4jz8rTV3U5hbilNRZ3LMlVusA==</SignatureValue>
  </Signature>
</Root>

#Conclusion

.NET contains all the classes you need to use the main cryptographic algorithms. Most of the algorithms require configuration. Be sure to check the documentation to know what are the recommended values. If you need to use an algorithm that is not provided by the framework, be sure to use an external library that is very well-tested and maintained.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub