Cryptography in .NET

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.

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

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.

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.

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==
}

If you chance one of the parameter, you should get another key:

// Number of iteration changed (49999 insteal 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. Note that SHA3 is not yet implemented by .NET.

byte[] ComputeHash(byte[] data)
{
    using (var sha256 = SHA256.Create())
    {
        return sha256.ComputeHash(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. If you can validate the hash it means the one that generates it is in possession of the key. 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.

byte[] ComputeHmac256(byte[] key, byte[] data)
{
    using (var hmac = new HMACSHA256(key))
    {
        return hmac.ComputeHash(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 the 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 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
  • DES: DESCryptoServiceProvider
  • RC2: RC2CryptoServiceProvider
  • Rijndael: RijndaelManaged
  • TripleDES: TripleDESCng, TripleDESCryptoServiceProvider

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 use 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 own usage. This Stack Overflow answer describes very well each mode. Note that some of them are not yet implemented in .NET.

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())
    {
        aes.Mode = CipherMode.CBC; // Should ajust depending on what you want to encrypt
        aes.Key = key;
        aes.GenerateIV(); // Ensure we use a new IV for each encryption

        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);
        }
    }
}

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.

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 a symmetric and an 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 in a similar way 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.

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 document. 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.

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://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.xml.reference.uri
        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:

<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

The .NET Framework 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.

Follow me:
Enjoy this blog? Buy Me A Coffee Donate with PayPal

Comments

Daniel Fisher (Lennybacon) -

The RNG should ONLY be used from ONE static instance. Otherwise it’s pretty easy to eat up all memory available if called multiple times.

Best Daniel

Gérald Barré -

If you need to generate lots of random data, you can create a unique instance of RNGCryptoServiceProvider. Note that is object is very light and it will require lots of instantiation before you run out of memory. Depending of your usage it would also be useful to use an ArrayPool to avoid some allocations.

Leave a reply