Named Keys in AES on Windows: Cryptography Next Generation and C#

So, what’s the method that has contributed the most to cybersecurity? Well, some might say it is the Diffie-Hellman key exchange method…

Named Keys in AES on Windows: Cryptography Next Generation and C#

So, what’s the method that has contributed the most to cybersecurity? Well, some might say it is the Diffie-Hellman key exchange method, but the workhorse of the industry is perhaps AES (Advanced Encryption Standard), and where it is used almost every time we use an HTTPs connection. It thus protects us from spying and Eve-in-the-middle attacks. In this article, we will create a named encryption key on a Microsoft Windows system, and which is then persistent so that it can be called up by its name in other applications or at a future time

With symmetric key encryption, we use the same key to encrypt and to decrypt:

The most popular symmetric key methods are AES and ChaCha20. In its purest form, AES is a block cipher, with 128-bit block sizes. ChaCha20 is a stream cipher. With a block cipher, we need padding, and there is no padding required for a stream cipher. The main modes that we get with symmetric key are ECB (Electronic Code Book), CBC (Cipher Block Chain), CTR (Counter) and GCM (Galois Counter Mode), and with either a 128-bit, a 192-bit or a 256-bit encryption key.

In this case, we will use CNG (Cryptography Next Generation) to create a named symmetric key and then use the default AES mode of CBC and a 128-bit key. Overall, CNG creates a named key, and which is stored securely on the system, and can be recalled at a future time. With CNG, we can only implement on a Windows system, and so it will not run on Mac OS and Linux (even though Microsoft .NET runs on these systems, too).

First we create a folder named “dotnet_aes”, and then go into that folder.We can create a Dotnet console project for .NET 8.0 with:

dotnet new console --framework net8.0

This produces a Csproject file of:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

First we can convert our message to a byte array [here]:

var msg=Encoding.ASCII.GetBytes(message);

Next we can create our key storage provider [here]:

CngProvider keyStorageProvider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
CngKeyCreationParameters keyCreationParameters = new CngKeyCreationParameters();

In this case, the key will be protected from being viewed and copied. If we want to view and copy it, we can set these properties [here]:

keyCreationParameters.ExportPolicy =  CngExportPolicies.AllowPlaintextExport;
keyCreationParameters.Provider = keyStorageProvider;

Each key has a name on the system. If the key name does not exist, it will be created with [here]:

if (CngKey.Exists("SymKey1").Equals(false)) { CngKey.Create(new CngAlgorithm("AES"), "SymKey1", keyCreationParameters); }

In this case, the name of the key in this case will be “SymKey1”. Next, we can encrypt our message [here]:

Aes aes = new AesCng("SymKey1", keyStorageProvider);
aes.IV = nonce;

var encryptor = aes.CreateEncryptor();
byte[] ciphertextBytes = encryptor.TransformFinalBlock(msg, 0, msg.Length);

To decrypt, we can then use the TransformFinalBlock() method [here]:

var decryptor = aes.CreateDecryptor();
byte[] plain = decryptor.TransformFinalBlock(ciphertextBytes, 0, ciphertextBytes.Length);

The following is the coding [here]:

namespace CngRSA
{
using System.Security.Cryptography;
using System.Text;
class Program
{

static void Main(string[] args)

{

var message="Hello";
var iv="00112233445566770011223344556677";
try {
var padding = RSASignaturePadding.Pkcs1;
if (args.Length >0) message=args[0];
if (args.Length >1) iv=args[1];
byte[] nonce = new byte[iv.Length/2];
Array.Copy(Convert.FromHexString(iv), nonce, iv.Length/2);

var msg=Encoding.ASCII.GetBytes(message);

CngProvider keyStorageProvider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
CngKeyCreationParameters keyCreationParameters = new CngKeyCreationParameters();

keyCreationParameters.ExportPolicy = CngExportPolicies.AllowPlaintextExport;
keyCreationParameters.Provider = keyStorageProvider;


if (CngKey.Exists("SymKey1").Equals(false)) { CngKey.Create(new CngAlgorithm("AES"), "SymKey1", keyCreationParameters); }


Aes aes = new AesCng("SymKey1", keyStorageProvider);
aes.IV = nonce;

var encryptor = aes.CreateEncryptor();
byte[] ciphertextBytes = encryptor.TransformFinalBlock(msg, 0, msg.Length);
var decryptor = aes.CreateDecryptor();

byte[] plain = decryptor.TransformFinalBlock(ciphertextBytes, 0, ciphertextBytes.Length);

Console.WriteLine("Message:\t\t{0}",message);
Console.WriteLine("IV:\t\t\t{0}",Convert.ToHexString(nonce));
Console.WriteLine("Cipher:\t\t\t{0}",Convert.ToHexString(ciphertextBytes));

Console.WriteLine("Decrypted:\t\t{0}",Encoding.UTF8.GetString(plain));
Console.WriteLine("\nBlocksize:\t\t{0}",aes.BlockSize);
Console.WriteLine("Key:\t\t\t{0}",Convert.ToHexString(aes.Key));
Console.WriteLine("IV:\t\t\t{0}",Convert.ToHexString(aes.IV));
Console.WriteLine("Padding:\t\t{0}",aes.Padding);
aes.Dispose();
} catch (Exception e) {
Console.WriteLine("Error: {0}",e.Message);

}
}
}
}

A sample run is [here]:

Message:		Hello 12345
IV: 00112233445566770011223344556677
Cipher: 9D0DAA414A9D45168D2724982D418D9A
Decrypted: Hello 12345

Blocksize: 128
Key: 5A87C97BCED0B0CFFA98098C70AE88D7AA0F0B5BD7DC9F38E71118FA370FB328
IV: 00112233445566770011223344556677
Padding: PKCS7
Mode: CBC

We can see that the key is “5A87C97BCED0B0CFFA98098C70AE88D7AA0F0B5BD7DC9F38E71118FA370FB328”, and with a salt value of “00112233445566770011223344556677”. In production, we could use a random salt value, and which we can generate with the aes.GenerateIV() method. For a given data input, a fixed symmetric key and a fixed IV value, we will always get the same ciphertext.

The main advantage of using the persitent key is that — once created — we can recall it for other applications, and just have to use its name.