Mixing asymmetric and symmetric encryption in .NET part I

Introduction

In this post we briefly went through symmetric encryption in .NET. We know that symmetric encryption requires a single cryptographic key for both encryption and decryption. The AES standard is the most widely used symmetric encryption and generally it’s very difficult to guess the right key for an attacker. Symmetric encryption is fast but key distribution is problematic since all parties involved in the encryption process must have access to it. If it is compromised then it can be difficult to revoke it and let all legitimate parties that things have gone wrong.

This post on the other hand discussed asymmetric encryption. With asymmetric encryption we don’t have a single key but a key-pair: a public and a private key that belong together. This means that they depend on each other. However, the private key cannot be derived from the public key. The public key can be distributed to anyone who wants to send us an encrypted message. We then decrypt the cipher text with our private key. The private key must stay with us. It can be stored as an XML string in a file or a database. Alternatively we can store it in the Windows key store. The most common implementation is the RSA standard. Therefore asymmetric encryption solves the key distribution problem. On the other hand asymmetric encryption is slow as it involves some very complex mathematical computations. Therefore it is not really a good option if long strings need to be encrypted or if data encryption is heavily used by an application even for short strings.

This is where mixed or hybrid encryption enters the picture which brings together the best of both worlds: the speed of symmetric encryption and increased security of asymmetric encryption. This is the topic of the present and the next post.

The flow

Mixing symmetric and asymmetric encryption is nothing new and the following flow is quite standard:

  • John wants to send a secret message to Sarah
  • John requests Sarah’s public key of her asymmetric key-pair
  • To increase security Sarah generates a one-time asymmetric key that can be used for a single message and sends the public key to John
  • John generates a symmetric key and an initialization vector (IV) and encrypts the secret message with them
  • John encrypts the symmetric key with Sarah’s public key
  • John sends the ciphertext of the symmetric key and the secret message to Sarah along with the IV
  • Sarah decrypts the symmetric key using her private key
  • Sarah then decrypts the message with the symmetric key and the IV
  • Finally Sarah deletes the asymmetric key-pair from the repository so that it cannot be used again

Note that the use of a one-off asymmetric key-pair is optional for the flow but it can increase the security as opposed to using the same key-pair over and over again. One-off asymmetric key-pairs are also called session keys. Symmetric keys can also be reused for the flow to work but it’s obviously a better option to generate one for each new message.

Service interfaces

We’ll be using many elements from the posts on symmetric and asymmetric encryption referred to above in the first paragraph. I won’t repeat every single detail about the implementation. If you’re not sure about some code snippet then make sure you read the relevant posts for details.

Some return objects from the service classes will inherit from the OperationResult class:

public abstract class OperationResult
{
	public bool Success { get; set; }
	public string ExceptionMessage { get; set; }
}

Let’s hide the symmetric and asymmetric encryption services behind interfaces. Recall that asymmetric key-pairs can be stored in XML strings in memory. We’ll go for that option for the demo.

Here’s the asymmetric encryption service interface:

public interface IXmlBasedAsymmetricEncryptionService
{
	AsymmetricKeyPairGenerationResult GenerateKeysAsXml(int keySizeBits);
	AsymmetricEncryptionResult EncryptWithPublicKeyXml(string message, string publicKeyAsXml);
	AsymmetricDecryptionResult DecryptWithFullKeyXml(byte[] cipherBytes, string fullKeyPairXml);
}

…where the return types are the following:

public class AsymmetricKeyPairGenerationResult : OperationResult
{
	public string PublicKeyXml { get; set; }
	public string PublicPrivateKeyPairXml { get; set; }
}

public class AsymmetricEncryptionResult : OperationResult
{
	public byte[] EncryptedAsBytes { get; set; }
	public string EncryptedAsBase64 { get; set; }
}

public class AsymmetricDecryptionResult : OperationResult
{
	public string DecryptedMessage { get; set; }
}

They are simple container objects for multiple return types.

Here’s the symmetric encryption service interface:

public interface ISymmetricEncryptionService
{
	SymmetricEncryptionResult Encrypt(string messageToEncrypt, int symmetricKeyLengthBits);
	string Decrypt(byte[] cipherTextBytes, byte[] key, byte[] iv);
}

…where SymmetricEncryptionResult looks as follows:

public class SymmetricEncryptionResult : OperationResult
{
	public byte[] Cipher { get; set; }
	public string CipherBase64 { get; set; }
	public byte[] IV { get; set; }
	public byte[] SymmetricKey { get; set; }		
}

The concrete encryption services

SymmetricEncryptionService implements ISymmetricEncryptionService as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using RandomNumberGenerator;
using System.IO;

namespace SymmetricEnryption
{
    public class SymmetricEncryptionService : ISymmetricEncryptionService
	{
		private readonly SymmetricAlgorithm _symmetricAlgorithm;

		public SymmetricEncryptionService(SymmetricAlgorithm symmetricAlgorithm)
		{
			if (symmetricAlgorithm == null) throw new ArgumentNullException("SymmetricAlgorithm");
			_symmetricAlgorithm = symmetricAlgorithm;
		}

		public SymmetricEncryptionResult Encrypt(string messageToEncrypt, int symmetricKeyLengthBits)
		{
			SymmetricEncryptionResult encryptionResult = new SymmetricEncryptionResult();
			try
			{
				//first test if bit length is valid
				if (_symmetricAlgorithm.ValidKeySize(symmetricKeyLengthBits))
				{
					_symmetricAlgorithm.KeySize = symmetricKeyLengthBits;
					using (MemoryStream mem = new MemoryStream())
					{
						CryptoStream crypto = new CryptoStream(mem, _symmetricAlgorithm.CreateEncryptor(), CryptoStreamMode.Write);
						byte[] bytesToEncrypt = Encoding.UTF8.GetBytes(messageToEncrypt);
						crypto.Write(bytesToEncrypt, 0, bytesToEncrypt.Length);
						crypto.FlushFinalBlock();
						byte[] encryptedBytes = mem.ToArray();
						string encryptedBytesBase64 = Convert.ToBase64String(encryptedBytes);
						encryptionResult.Success = true;
						encryptionResult.Cipher = encryptedBytes;
						encryptionResult.CipherBase64 = encryptedBytesBase64;
						encryptionResult.IV = _symmetricAlgorithm.IV;
						encryptionResult.SymmetricKey = _symmetricAlgorithm.Key;
					}
				}
				else
				{
					string NL = Environment.NewLine;
					StringBuilder exceptionMessageBuilder = new StringBuilder();
					exceptionMessageBuilder.Append("The provided key size - ")
						.Append(symmetricKeyLengthBits).Append(" bits - is not valid for this algorithm.");
					exceptionMessageBuilder.Append(NL)
						.Append("Valid key sizes: ").Append(NL);
					KeySizes[] validKeySizes = _symmetricAlgorithm.LegalKeySizes;
					foreach (KeySizes keySizes in validKeySizes)
					{
						exceptionMessageBuilder.Append("Min: ")
							.Append(keySizes.MinSize).Append(NL)
							.Append("Max: ").Append(keySizes.MaxSize).Append(NL)
							.Append("Step: ").Append(keySizes.SkipSize);
					}
					throw new CryptographicException(exceptionMessageBuilder.ToString());
				}
			}
			catch (Exception ex)
			{
				encryptionResult.Success = false;
				encryptionResult.ExceptionMessage = ex.Message;
			}

			return encryptionResult;
		}

		public string Decrypt(byte[] cipherTextBytes, byte[] key, byte[] iv)
		{
			_symmetricAlgorithm.IV = iv;
			_symmetricAlgorithm.Key = key;
			using (MemoryStream mem = new MemoryStream())
			{
				CryptoStream crypto = new CryptoStream(mem, _symmetricAlgorithm.CreateDecryptor(), CryptoStreamMode.Write);
				crypto.Write(cipherTextBytes, 0, cipherTextBytes.Length);
				crypto.FlushFinalBlock();
				return Encoding.UTF8.GetString(mem.ToArray());
			}
		}
    }
}

This is identical to what we saw in the post about symmetric encryption so you can read through it for details if needed. The only difference is that SymmetricEncryptionService accepts a SymmetricAlgorithm class in its constructor whereas the original example had it as parameters in the Encrypt and Decrypt functions.

IXmlBasedAsymmetricEncryptionService is implemented by the RsaXmlBasedAsymmetricEncryptionService class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace AsymmetricEncryption
{
	public class RsaXmlBasedAsymmetricEncryptionService : IXmlBasedAsymmetricEncryptionService
	{
		public AsymmetricKeyPairGenerationResult GenerateKeysAsXml(int keySizeBits)
		{
			AsymmetricKeyPairGenerationResult asymmetricKeyPairGenerationResult = new AsymmetricKeyPairGenerationResult();
			RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider(keySizeBits);
			try
			{
				asymmetricKeyPairGenerationResult.PublicKeyXml = rsaProvider.ToXmlString(false);
				asymmetricKeyPairGenerationResult.PublicPrivateKeyPairXml = rsaProvider.ToXmlString(true);
				asymmetricKeyPairGenerationResult.Success = true;
			}
			catch (CryptographicException cex)
			{
				string NL = Environment.NewLine;
				StringBuilder validKeySizeBuilder = new StringBuilder();
				KeySizes[] validKeySizes = rsaProvider.LegalKeySizes;
				foreach (KeySizes keySizes in validKeySizes)
				{
					validKeySizeBuilder.Append("Min: ")
						.Append(keySizes.MinSize).Append(NL)
						.Append("Max: ").Append(keySizes.MaxSize).Append(NL)
						.Append("Step: ").Append(keySizes.SkipSize);
				}
				asymmetricKeyPairGenerationResult.ExceptionMessage =
					$"Cryptographic exception when generating a key-pair of size {keySizeBits}. Exception: {cex.Message}{NL}Make sure you provide a valid key size. Here are the valid key size boundaries:{NL}{validKeySizeBuilder.ToString()}";
			}
			catch (Exception otherEx)
			{
				asymmetricKeyPairGenerationResult.ExceptionMessage =
					$"Other exception caught while generating the key pair: {otherEx.Message}";
			}
			return asymmetricKeyPairGenerationResult;
		}

		public AsymmetricEncryptionResult EncryptWithPublicKeyXml(string message, string publicKeyAsXml)
		{
			AsymmetricEncryptionResult asymmetricEncryptionResult = new AsymmetricEncryptionResult();
			try
			{
				RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider();
				rsaProvider.FromXmlString(publicKeyAsXml);
				byte[] encryptedAsBytes = rsaProvider.Encrypt(Encoding.UTF8.GetBytes(message), true);
				string encryptedAsBase64 = Convert.ToBase64String(encryptedAsBytes);
				asymmetricEncryptionResult.EncryptedAsBase64 = encryptedAsBase64;
				asymmetricEncryptionResult.EncryptedAsBytes = encryptedAsBytes;
				asymmetricEncryptionResult.Success = true;
			}
			catch (Exception ex)
			{
				asymmetricEncryptionResult.ExceptionMessage =
					$"Exception caught while encrypting the message: {ex.Message}";
			}
			return asymmetricEncryptionResult;
		}

		public AsymmetricDecryptionResult DecryptWithFullKeyXml(byte[] cipherBytes, string fullKeyPairXml)
		{
			AsymmetricDecryptionResult asymmetricDecryptionResult = new AsymmetricDecryptionResult();
			try
			{
				RSACryptoServiceProvider rsaProvider = new RSACryptoServiceProvider();
				rsaProvider.FromXmlString(fullKeyPairXml);
				byte[] decryptBytes = rsaProvider.Decrypt(cipherBytes, true);
				asymmetricDecryptionResult.DecryptedMessage = Encoding.UTF8.GetString(decryptBytes);
				asymmetricDecryptionResult.Success = true;
			}
			catch (Exception ex)
			{
				asymmetricDecryptionResult.ExceptionMessage =
					$"Exception caught while decrypting the cipher: {ex.Message}";
			}
			return asymmetricDecryptionResult;
		}
	}
}

Again this is nothing new, we saw all that in the post about asymmetric encryption.

We’ll connect the above objects in the sender and receiver classes in the next post.

You can view the list of posts on Security and Cryptography here.

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

One Response to Mixing asymmetric and symmetric encryption in .NET part I

  1. Sathish says:

    Hi Andras,
    I love your posts. Can you please help me solve the below
    I am developing a small Windows Desktop application in .Net and want to provide licensing as below 30 Days Trial. Post that user has to enter key and activate the app

    I have developed a rest API from which will be called from the app as soon as it is installed and the date is saved in DB. By this every time the app is opened, I will make a call and check for the installation date, based on the delta, I will show the remaining time. I know there are challenges when the user is not connected to the internet.

    To call the API, If I have to pass the key and secret, where do we store this info?

    Concern: User can reverse engineer my code and can see the API endpoints and call it from the postman and hack this

Leave a comment

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.