Mixing asymmetric and symmetric encryption in .NET part II

Introduction

In the previous post we started working on a mixed encryption demo project. The goal is to show how the benefits of symmetric and asymmetric encryption can be used in a single encryption-decryption flow. Symmetric encryption is fast but key distribution is problematic. Asymmetric encryption solves the key distribution problem but is on the other hand slow. Fortunately we can use both at the same time for increased security.

Previously we built the encryption service components: the interfaces and their implementations. Now it’s time to connect them.

The secret message receiver

The Receiver will receiver a secret message and must be able to decipher it. It will also need to be able to generate an asymmetric key and send it to the requesting party, i.e. the Sender that we’ll see soon.

The Receiver will need both a symmetric and an asymmetric encryption service and we’ll use the IXmlBasedAsymmetricEncryptionService and ISymmetricEncryptionService as required parameters through its constructor. The key-pairs will be stored in memory in a dictionary to keep the repository simple.

The generated one-time public keys will be stored in the following container object:

public class AsymmetricPublicKey
{
	public AsymmetricPublicKey(Guid publicKeyId, XDocument publicKeyXml)
	{
		PublicKeyId = publicKeyId;
		PublicKeyXml = publicKeyXml;
	}
		
	public Guid PublicKeyId { get; }
	public XDocument PublicKeyXml { get; }
}

We store the public key in an XML document and associate it with an ID for an easy lookup.

The secret message will also need an object where the Receiver has all the information in order to run the decryption process. The EncryptedMessage object fulfils this purpose:

public class EncryptedMessage
{
	public EncryptedMessage(string symmetricKeyEncryptedBase64, string initializationVectorBase64
		, string cipherTextBase64, Guid asymmetricKeyId)
	{
		SymmetricKeyEncryptedBase64 = symmetricKeyEncryptedBase64;
		InitializationVectorBase64 = initializationVectorBase64;
		CipherTextBase64 = cipherTextBase64;
		AsymmetricKeyId = asymmetricKeyId;
	}

	public string SymmetricKeyEncryptedBase64 { get; }
	public string InitializationVectorBase64 { get; }		
	public string CipherTextBase64 { get; }
	public Guid AsymmetricKeyId { get; }
}

Here’s the full SecretMessageReceiver class:

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

namespace HybridEncryption
{
	public class SecretMessageReceiver
	{
		private readonly IXmlBasedAsymmetricEncryptionService _xmlBasedAsymmetricEncryptionService;
		private readonly ISymmetricEncryptionService _symmetricEncryptionService;
		private readonly Dictionary<Guid, AsymmetricKeyPairGenerationResult> _successfulKeyPairResults;

		public SecretMessageReceiver(IXmlBasedAsymmetricEncryptionService xmlBasedAsymmetricEncryptionService
			, ISymmetricEncryptionService symmetricEncryptionService)
		{
			if (xmlBasedAsymmetricEncryptionService == null) throw new ArgumentNullException("XmlBasedAsymmetricEncryptionService");
			if (symmetricEncryptionService == null) throw new ArgumentNullException("SymmetricEncryptionService");
			_xmlBasedAsymmetricEncryptionService = xmlBasedAsymmetricEncryptionService;
			_symmetricEncryptionService = symmetricEncryptionService;
			_successfulKeyPairResults = new Dictionary<Guid, AsymmetricKeyPairGenerationResult>();
		}

		public AsymmetricPublicKey GenerateOneTimeAsymmetricPublicKey()
		{
			int defaultAsymmetricKeySize = 2048;
			AsymmetricKeyPairGenerationResult asymmKeyPairGenerationResult = 
				_xmlBasedAsymmetricEncryptionService.GenerateKeysAsXml(defaultAsymmetricKeySize);
			if (asymmKeyPairGenerationResult.Success)
			{
				Guid guid = Guid.NewGuid();
				_successfulKeyPairResults[guid] = asymmKeyPairGenerationResult;
				return new AsymmetricPublicKey(guid, XDocument.Parse(asymmKeyPairGenerationResult.PublicKeyXml));
			}
			throw new CryptographicException(asymmKeyPairGenerationResult.ExceptionMessage);
		}

		public void ProcessIncomingMessage(EncryptedMessage encryptedMessage)
		{
			if (_successfulKeyPairResults.ContainsKey(encryptedMessage.AsymmetricKeyId))
			{
				byte[] encryptedSymmetricKey = Convert.FromBase64String(encryptedMessage.SymmetricKeyEncryptedBase64);
				AsymmetricDecryptionResult decryptSymmetricKey = _xmlBasedAsymmetricEncryptionService.DecryptWithFullKeyXml
					(encryptedSymmetricKey,
					_successfulKeyPairResults[encryptedMessage.AsymmetricKeyId].PublicPrivateKeyPairXml);
				if (decryptSymmetricKey.Success)
				{
					string symmetricKeyBase64 = decryptSymmetricKey.DecryptedMessage;
					byte[] cipherText = Convert.FromBase64String(encryptedMessage.CipherTextBase64);
					byte[] iv = Convert.FromBase64String(encryptedMessage.InitializationVectorBase64);
					byte[] symmetricKey = Convert.FromBase64String(symmetricKeyBase64);
					string secretMessage = _symmetricEncryptionService.Decrypt(cipherText
						, symmetricKey, iv);
					Console.WriteLine($"Secret message receiver got the following message: {secretMessage}");
					_successfulKeyPairResults.Remove(encryptedMessage.AsymmetricKeyId);
				}
				else
				{
					throw new CryptographicException(decryptSymmetricKey.ExceptionMessage);
				}
			}
			else
			{
				throw new ArgumentException("No such key id found");
			}
		}
	}
}

In GenerateOneTimeAsymmetricPublicKey we construct a key of size 2048 bits and associate it with an ID. The key-pair is stored in a dictionary of GUID and key-pair generation result.

Then in ProcessIncomingMessage the Receiver first decrypts the symmetric key using its asymmetric private key that is associated with the incoming ID. If this step was successful then the Receiver uses the decrypted symmetric key and the IV to decrypt the cipher text of the secret message. The message is printed in the console window and the key ID is deleted from the in-memory “key store” dictionary.

The secret message sender

The Sender will also need a symmetric and asymmetric encryption service. It will also need a Receiver to communicate with.

Here’s the full SecretMessageSender class:

using AsymmetricEncryption;
using SymmetricEnryption;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace HybridEncryption
{
    public class SecretMessageSender
    {
		private readonly SecretMessageReceiver _secretMessageReceiver;
		private readonly ISymmetricEncryptionService _symmetricEncryptionService;
		private readonly IXmlBasedAsymmetricEncryptionService _xmlBasedAsymmetricEncryptionService;
		private readonly string _extremelyConfidentialMessage;

		public SecretMessageSender(SecretMessageReceiver secretMessageReceiver, 
			ISymmetricEncryptionService symmetricEncryptionService, 
			IXmlBasedAsymmetricEncryptionService xmlBasedAsymmetricEncryptionService)
		{
			if (secretMessageReceiver == null) throw new ArgumentNullException("SecretMessageReceiver");
			if (symmetricEncryptionService == null) throw new ArgumentNullException("SymmetricEncryptionService");
			if (xmlBasedAsymmetricEncryptionService == null) throw new ArgumentNullException("XmlBasedAsymmetricEncryptionService");
			_secretMessageReceiver = secretMessageReceiver;
			_symmetricEncryptionService = symmetricEncryptionService;
			_xmlBasedAsymmetricEncryptionService = xmlBasedAsymmetricEncryptionService;
			_extremelyConfidentialMessage = "My new invention will save the world.";
		}

		public void Start()
		{
			int defaultSymmetricKeySize = 256;
			AsymmetricPublicKey oneTimeAsymmetricPublicKey = _secretMessageReceiver.GenerateOneTimeAsymmetricPublicKey();
			SymmetricEncryptionResult symmetricEncryptionOfSecretMessage = 
				_symmetricEncryptionService.Encrypt(_extremelyConfidentialMessage, defaultSymmetricKeySize);
			string symmetricKeyBase64 = Convert.ToBase64String(symmetricEncryptionOfSecretMessage.SymmetricKey);
			string ivBase64 = Convert.ToBase64String(symmetricEncryptionOfSecretMessage.IV);
			AsymmetricEncryptionResult asymmetricallyEncryptedSymmetricKeyResult =
				_xmlBasedAsymmetricEncryptionService.EncryptWithPublicKeyXml(symmetricKeyBase64, oneTimeAsymmetricPublicKey.PublicKeyXml.ToString());
			EncryptedMessage encryptedMessage = new EncryptedMessage(asymmetricallyEncryptedSymmetricKeyResult.EncryptedAsBase64
				, ivBase64, symmetricEncryptionOfSecretMessage.CipherBase64, oneTimeAsymmetricPublicKey.PublicKeyId);
			_secretMessageReceiver.ProcessIncomingMessage(encryptedMessage);
		}
	}
}

In the Start method the Sender asks the Receiver to provide an asymmetric public key. It then encrypts the secret message using the symmetric encryption service. The generated symmetric key is then encrypted using the one-time asymmetric public key. All the necessary components are stored in an EncryptedMessage object and relayed to the Receiver.

Obviously the above setup is very limited in the sense that the Sender sends one message to the Receiver and that’s it. Normally there should be a two-way continuation back and forth. However, the main point here was to demonstrate the steps involved in a mixed encryption strategy.

Pulling it all together

The following short method demonstrates how to use the Sender and Receiver objects:

private void TestHybridEncryption()
{
	IXmlBasedAsymmetricEncryptionService asymmetricEncryptionService = new RsaXmlBasedAsymmetricEncryptionService();
	ISymmetricEncryptionService symmetricEncryptionService = new SymmetricEncryptionService(new AesCryptoServiceProvider());
	SecretMessageReceiver secretMessageReceiver = new SecretMessageReceiver(asymmetricEncryptionService, symmetricEncryptionService);
	SecretMessageSender secretMessageSender = new SecretMessageSender(secretMessageReceiver, symmetricEncryptionService, asymmetricEncryptionService);
	secretMessageSender.Start();
}

For easier understanding it’s a good idea to set breakpoints at various places in the demo code. If everything goes fine then the secret message will be deciphered by the Receiver and will print the following message to the console window:

Secret message receiver got the following message: My new invention will save the world.

This is not the end of the story though. We can add some more spice to the demo to increase security even further by message authentication like we saw in this post. We’ll do that in a separate 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.

2 Responses to Mixing asymmetric and symmetric encryption in .NET part II

  1. Okan DUnay says:

    Hello Andras, I can’t find the AsymmetricEncryption class you applied. Can you help me?

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.