Overview of asymmetric encryption in .NET

Introduction

Asymmetric encryption is based on a pair of cryptographic keys. One of the keys is public, i.e. anyone can have access to it. The other key is private which should be kept secret. The keys are complementary which means that they go hand in hand, they are not independent of each other. If a value is calculated as the public key then the private key cannot be calculated independently otherwise the encryption process will fail. Normally the public key is used to encrypt a message and the private key is there for the decryption process but they can be used in the opposite direction as well. Asymmetric algorithms are also called Public Key Cryptography.

The most important advantage of asymmetric over symmetric encryption is that we don’t need to worry about distributing the public key. The key used in symmetric encryption must be known to all parties taking part in the encryption/decryption process which increases the chances of the key landing in the wrong hands. With asymmetric encryption we only need to worry about storing the private key, the public key can be freely distributed. For a hacker it is not practical to attempt to calculate the private key based on the public key, that is close to impossible to achieve.

However, asymmetric encryption is a very complex mathematical process which is a lot slower than symmetric encryption. Also, storing the private key can still be problematic.

RSA

The most widely used asymmetric encryption algorithm is called RSA which stands for the last names of its inventors: Rivest, Shamir and Adleman. Its most frequent key sizes are 1024, 2048 and 4096 bits though both lower and higher key sizes are available as we’ll see later in the demo. The algorithm is implemented by the RSACryptoServiceProvider object in .NET. It can not only perform the encryption and decryption process but helps us create the public-private key-pair. The default key size is 1024 bits but it’s recommended to take a higher value, either 2048 or 4096 bits.

Objects for code demo

Let’s start with the objects we’ll use in the demo. The various return types of the asymmetric encryption service will will derive from from an abstract base class to hold the outcome and any exception message:

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

The first task of the asymmetric service is to generate a key pair. The keys can be exported with or without the private key as XML. The following object will hold both:

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

Here’s the key generation function of AsymmetricEncryptionService:

public class AsymmetricEncryptionService
	{
		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;
		}
}

We instantiate the RSACryptoServiceProvider class with the given key size. The ToXmlString function accepts a boolean where false means we only want to export the public key. If a cryptographic exception is thrown then we provide the valid key sizes along with the exception message.

The encryption and decryption results are also stored in separate objects:

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

}

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

Here’s the function of AsymmetricEncryptionService that performs the encryption using the public key:

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

We initialise a RSACryptoServiceProvider object and let the public key XML populate its properties using the FromXmlString method. The extra boolean in the Encrypt function means that we want to use OAEP padding for extra randomness in the encryption process.

The decryption function is almost identical:

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

It’s important that we send in the full key otherwise we’ll get an exception.

Demo code

Here’s a function that uses all of the objects above:

private void TestAsymmetricEncryptDecrypt()
{
	string NL = Environment.NewLine;
	AsymmetricEncryptionService asymmetricEncryptionService = new AsymmetricEncryptionService();
	int keySizeBits = 2048;
	AsymmetricKeyPairGenerationResult keyPairGenerationResult = asymmetricEncryptionService.GenerateKeysAsXml(keySizeBits);
	if (keyPairGenerationResult.Success)
	{				
		XDocument publicKeyXdoc = XDocument.Parse(keyPairGenerationResult.PublicKeyXml);
		XDocument fullKeyXdoc = XDocument.Parse(keyPairGenerationResult.PublicPrivateKeyPairXml);
		Console.WriteLine($"Asymmetric key-pair generation success.{NL}Public key:{NL}{NL}{publicKeyXdoc.ToString()}{NL}{NL}Full key:{NL}{NL}{fullKeyXdoc.ToString()}{NL}{NL}");
		string originalMessage = "This is an extremely secret message.";
		AsymmetricEncryptionResult asymmetricEncryptionResult = asymmetricEncryptionService.EncryptWithPublicKeyXml(originalMessage, keyPairGenerationResult.PublicKeyXml);
		if (asymmetricEncryptionResult.Success)
		{
			Console.WriteLine("Encryption success.");
			Console.WriteLine($"Cipher text: {NL}{asymmetricEncryptionResult.EncryptedAsBase64}{NL}");
			AsymmetricDecryptionResult asymmetricDecryptionResult = 
				asymmetricEncryptionService.DecryptWithFullKeyXml
				(asymmetricEncryptionResult.EncryptedAsBytes, keyPairGenerationResult.PublicPrivateKeyPairXml);
			if (asymmetricDecryptionResult.Success)
			{
				Console.WriteLine("Decryption success.");
				Console.WriteLine($"Deciphered text: {NL}{asymmetricDecryptionResult.DecryptedMessage}");
			}
			else
			{
				Console.WriteLine($"Decryption failed.{NL}{asymmetricDecryptionResult.ExceptionMessage}");
			}
		}
		else
		{
			Console.WriteLine($"Encryption failed.{NL}{asymmetricEncryptionResult.ExceptionMessage}");
		}
	}
	else
	{
		Console.WriteLine($"Asymmetric key-pair generation failed.{NL}{keyPairGenerationResult.ExceptionMessage}");
	}
}

I used XDocument to parse the public and full keys for better formatting only.

If we provide an invalid key size, like 10 for the keySizeBits field then we’ll get the following output:

Asymmetric key-pair generation failed.
Cryptographic exception when generating a key-pair of size 10. Exception: Invalid flags specified.

Make sure you provide a valid key size. Here are the valid key size boundaries:
Min: 384
Max: 16384
Step: 8

There we have the min and max key sizes with the step size. This means that the minimum key size is 384, the next valid key size is 384 + 8 = 394 and so on.

If the key size is valid, like 2048 bits then we’ll get some RSA keys similar to the following. Here’s a possible public key:

<RSAKeyValue>     <Modulus>ovoY6npFhZiEXLZBmVbW05Cj5DWlFAW379xvouly87pv8C6zM0w/JInnvacb9U9ZlW4/mR2NEJD0PS6ZKqMiwJAduC48eFr6eecXZedFtwSC6xKxmFQami6E0wbGq7awA9wJUGfg0/4hglXv3H8f8bmeLUQ3lz+cAVXzs8AmQOpTY8xfZ51eL/J1imCOIs3R2Ml6BqLpBQ7sGzN+HdBUwSdqyjOPCYrM77Ynua0ziVHjb1B17OVKuYJd0uKdeI4wdxYPJg11G3kuRMLtuxywA80Rc2J3DM2nqzCgheoz+xWL5bCBLZHy5urfqRckO4DM8p1J+9Ib9Ytlw3jRAtaPrw==</Modulus>
  <Exponent>AQAB</Exponent>
</RSAKeyValue>

…and here’s a full key:

<RSAKeyValue>
  <Modulus>ovoY6npFhZiEXLZBmVbW05Cj5DWlFAW379xvouly87pv8C6zM0w/JInnvacb9U9ZlW4/mR2NEJD0PS6ZKqMiwJAduC48eFr6eecXZedFtwSC6xKxmFQami6E0wbGq7awA9wJUGfg0/4hglXv3H8f8bmeLUQ3lz+cAVXzs8AmQOpTY8xfZ51eL/J1imCOIs3R2Ml6BqLpBQ7sGzN+HdBUwSdqyjOPCYrM77Ynua0ziVHjb1B17OVKuYJd0uKdeI4wdxYPJg11G3kuRMLtuxywA80Rc2J3DM2nqzCgheoz+xWL5bCBLZHy5urfqRckO4DM8p1J+9Ib9Ytlw3jRAtaPrw==</Modulus>
  <Exponent>AQAB</Exponent>
  <P>wPeuuTtMNyHMcde12Mc+NmWf4/kgDMM0ljU1eEq4iuwWRvL4zIOmxQ2M0WT3axOoZbqGKO4nInEi9KAIR3M8rUSru7km4LaGiSR0u4fNxhe6K+NjoVYm25TJoazN/1B4tQtfZWBK87+VVfiK2v/pv2+ojhZLyMRPnm8LsR7kz3s=</P>
  <Q>2DaLoFLo85THV9Ly7S0EW9jEAwL5Jz+jaE2k3gqNMZaGFsGox8+KxyEpIx9sJaZDggbW4JKbznV76C4xljTbaT0/oHE94lDYw2P0qB+nqwzMsEMf85wSb4MZi95i/Nssk4emGhhTc9tYoIn7Fssm/gNeWicNqFzDa6+Qxx5mkF0=</Q>
  <DP>swpcVEM/kPvMHGifsfYTtBcQhk5UvWK1PRU7elQh68vnU4cG74LLTpQm7vX2VqRTn6ez6PAm4V8FpuGBLQIv7zgC+1dsSh0wsLhhksoLU9waAbSmcUBlJ2Fiv559N4HrrVdS+NWiVYyRu8Wy2EWQFO49/y+Su0Hh+cdgmsNmW1c=</DP>
  <DQ>dHitrqf7JXw4Bm90vZ9Kgc+7h4PKhAIcHMv2zEYS2WukwA2CxmBe+fY7GtiKgZ2XMCxPBJr1o7pWDTUVMT04FPERnXRjSc8Tde4alZa308UJXspKJAknRTryQA6EdIH8+VxRdi00h2zZyWGLCTRWxO87nIT9Ln0KKLBi5WDTD2E=</DQ>
  <InverseQ>vrnDh+imePONIIZozoX6MvjLmrmDha/tBkVqt7gxlP+NK8p7r08xutC18hRL7FX2VmRHDk+VZ5yxrD97w9VMonqgyR3RGees6jCBsUr7c+amzJmHH7GK1LTg7Wor6QmLdgDa0a62rXn/hrraYx7exd5mXWdlXsJhGHb3UedLLCM=</InverseQ>
  <D>UJppjTn4vUh/mgzHp8tThyH/dIP1h1AGtvnqOoPDxvf7mam3FhVNG0ibFOibgrkCYM28ZYFAfaN7L/EmbnhtXRhJoog3fvzb6UNaBpuZlpkEGLfPnrtlxsBgD7BHvLAnIFmIP7yIbMBAmWHgNcinp1j6en/FEKg0g1Kbq1hKcwrG4Wy9ONd4sbuZPJV7y00yIS3ASdNuctBwtMrPSrcE8dW6nGLswJem120+ZknrXDCfh1ofUuGwVAURj5jX+QUX9HFwQNefbyKXypvsFqk+BpYGEWUgJS4gFPgWeBr3l7/5g96wgup6h5PnSZxA+mvptlvNLrMmJdv60y/pNZYPjQ==</D>
</RSAKeyValue>

As you can see an RSA key is not a single string or byte array but consists of many components. I won’t even pretend that I understand how these various components like D, P etc. work and developers normally don’t need to be concerned with them. The important point here is that two components are necessary for the public key: Modulus and Exponent. The others are used for building the private key.

Here’s the demo output:
================================
Asymmetric key-pair generation success.
Public key:

–ignored, see above

Full key:

–ignored, see above

Encryption success.
Cipher text:
nd7Mlpa0PFRu3P/svS48tccjiRw9kV3/tAXN7GkRLs0EMZF+IZfkbJRLn0gRDOxsCkj7xc54xu7tqsKw/atXMIk3N/N6BrUd5KspBB4havt954odeiSaP37FUSEP9rgPZNwkKAmikcvkSXvSlqhElGlSMm++Hcdl1aNZMX4ni2FlMpWxuL7MGXWBBHLHiBI73F+57RaCfY81EN3GiiUl/CKYxZDuiwmpjXL65UwKeRcCQ5FX4tbe+KXUm+YuNXD7VKvfgW0lW+rONk03MFCOew0JxM6KoViOeimTDhzM8qqtSVn7sIfNDYhZaKmVUBWys0vo6/XOLUgLtrItblg/pg==

Decryption success.
Deciphered text:
This is an extremely secret message.
================================

The above example showed one way to store the keys. The XML keys can be saved in a file or a database.

Another option is to store the key-pair in a key store on the computer. We’ll see how it works in an upcoming 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 Overview of asymmetric encryption in .NET

  1. Bax says:

    wonderful

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.