# Implementing RSA in .NET Core

WARNING

This guide assumes you have created a barebones .NET Core Console Project and have configured it to use .NET Core 2.2. I've called my console project RSACore so you'll see this namespace used throughout the code samples below.

WARNING

This code is NOT suitable for production. It skips over basic exception handling and bounds checking. I'm assuming that if you plan on implementing RSA in your project, you know enough C# and enough about crypto (such as the limits on message sizes) to expand on the below to fit your own requirements.

# Prerequisites

We'll be reading and writing RSA keys in PEM format, so to make this easier we need to include the PemUtils NuGet package by Wouter Huysentruit. Install it using the NuGet Package Manager or from GitHub.

As we're creating a console app and we'll be using an appsettings.json file, we don't have the benefit of ASP.NET Core's Dependency Injection to just plug a configuration object into our project, so we'll need a few additional NuGet packages. Add in Microsoft.Extensions.Configuration, Microsoft.Extensions.Configuration.FileExtensions and Microsoft.Extensions.Configuration.Json from NuGet.

# Overview

Our goals are to:

  1. Generate an RSA key and save it to disk.
  2. Use the key to encrypt some data and save it to disk.
  3. Read the key from disk.
  4. Use the key to decrypt the data from step 2.

# The Basic Class

Add a new file to your project and call it RSAImpl.cs. We'll need a couple of empty stub functions for encrypting and decrypting data. We'll also include some using statements we'll be relying on later.

Go ahead and add an appsettings.json file to your project if one doesn't exist. Edit its properties and ensure "Copy to Output Directory" is set to "Copy if newer" (otherwise our config builder won't be able to find the file!)

Edit your appsettings.json file to look like this:

{
  "Settings": {
    "KeySize":  4096
  }
}
1
2
3
4
5

We now need to add in some code to build our configuration from the appsettings.json file. We'll build the config in our constructor. As a general rule of thumb in production code, I consider this bad practice (don't do anything complicated in your constructor that could cause instantiating an object to fail - a constructor should always construct) but for the purposes of this example, we'll let it slide.

The final code and barebones class should look like this:

using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using Microsoft.Extensions.Configuration;
using PemUtils;

namespace RSACore
{
    public class RSAImpl
    {
        private readonly IConfiguration _config;

        public RSAImpl()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

            _config = builder.Build();
        }

        public void EncryptAndSave(string plainText, string fileName, string keyFileName)
        {
        }

        public void LoadAndDecrypt(string fileName, string keyFileName)
        {
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# RSACryptoServiceProvider and .NET Core

If you've worked with RSACryptoServiceProvider previously, you'll probably have serialized parameters using ToXmlString() and FromXmlString(). That doesn't work in .NET Core (as of writing, anyway). You should avoid using RSACryptoServiceProvider as it is tightly bound to the Windows platform. Instead, we'll be using the RSA base class, which will return a platform-specific RSA implementation:

  • On Windows: an RSA object implemented from CNG (Crypto API: Next Generation).
  • On Linux and MacOS: an RSA object implemented from OpenSSL.

So we're going to need something that will hand us back an RSA object. Let's add a private method to do so. We'll be using our configuration object to set the key size to the value we added in appsettings.json earlier. Add the following method to the RSAImpl class.

private RSA GetRSACryptoProvider()
{
    try
    {
        var rsa = RSA.Create();
        rsa.KeySize = Convert.ToInt32(_config["Settings:KeySize"]);
        return rsa;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Exception in GetRSACryptoProvider(): {ex}");
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# The Encryption Method

Next we'll expand on our encryption method. We want this to accept some plaintext (whatever it is we want to encrypt) and some parameters indicating where we want to save the encrypted text and the RSA private key to. We're not doing any sense checks for whether we're overwriting files or have permission to write to the destination in fileName and keyFileName. You should ensure that the keyfile is stored somewhere safe if you use this code in a production environment.

The finished encryption method should look like this:

public void EncryptAndSave(string plainText, string fileName, string keyFileName)
{
    using (var rsa = GetRSACryptoProvider())
    {
        var plainTextBytes = Encoding.Unicode.GetBytes(plainText);
        var cipherTextBytes = rsa.Encrypt(plainTextBytes, RSAEncryptionPadding.Pkcs1);
        var cipherText = Convert.ToBase64String(cipherTextBytes);

        // Save our encrypted text
        File.WriteAllText(fileName, cipherText);

        // Export the RSA private key (put this somewhere safe!)
        using (var fs = File.Create(keyFileName))
        {
            using (var pem = new PemWriter(fs))
            {
                pem.WritePrivateKey(rsa);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# The Decryption Method

Now we're going to need a method to read a private key and ciphertext from a file and decrypt the ciphertext. The first thing we're going to do is expand on the GetRSACryptoProvider method and optionally pass in a path to a keyfile. If this parameter isn't null, we'll try and load the key from the file instead of creating a new one. We'll use PemUtils to read the keyfile and import its parameters into the newly-created RSA object. Our GetRSACryptoProvider method now looks like this:

private RSA GetRSACryptoProvider(string keyFileName = null)
{
    var rsa = RSA.Create();

    try
    {
        if (string.IsNullOrEmpty(keyFileName))
        {
            rsa.KeySize = Convert.ToInt32(_config["Settings:KeySize"]);
        }
        else
        {
            using (var privateKey = File.OpenRead(keyFileName))
            {
                using (var pem = new PemReader(privateKey))
                {
                    var rsaParameters = pem.ReadRsaKey();
                    rsa.ImportParameters(rsaParameters);
                }
            }
        }
        return rsa;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Exception in GetRSACryptoProvider(): {ex}");
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Now we add some code to our decryption method, to load the keyfile from disk and return our plaintext as a string:

public string LoadAndDecrypt(string fileName, string keyFileName)
{
    var plainText = string.Empty;

    using (var rsa = GetRSACryptoProvider(keyFileName))
    {
        var cipherText = File.ReadAllText(fileName);
        var cipherTextBytes = Convert.FromBase64String(cipherText);
        var plainTextBytes = rsa.Decrypt(cipherTextBytes, RSAEncryptionPadding.Pkcs1);
        plainText = Encoding.Unicode.GetString(plainTextBytes);
    }

    return plainText;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Finishing Up

That should be all we need. We can test this by plugging our new class into Program.cs as follows:

private static void Main(string[] args)
{
    var rsa = new RSAImpl();
    rsa.EncryptAndSave("Hello from 4142.io!", @"C:\Temp\crypt.txt", @"C:\Temp\crypt.key");
    Console.WriteLine(rsa.LoadAndDecrypt(@"C:\Temp\crypt.txt", @"C:\Temp\crypt.key"));
    Console.ReadKey();
}
1
2
3
4
5
6
7

The final RSAImpl class should look like this:

using Microsoft.Extensions.Configuration;
using PemUtils;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace RSACore
{
    public class RSAImpl
    {
        private readonly IConfiguration _config;

        public RSAImpl()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

            _config = builder.Build();
        }

        public void EncryptAndSave(string plainText, string fileName, string keyFileName)
        {
            using (var rsa = GetRSACryptoProvider())
            {
                var plainTextBytes = Encoding.Unicode.GetBytes(plainText);
                var cipherTextBytes = rsa.Encrypt(plainTextBytes, RSAEncryptionPadding.Pkcs1);
                var cipherText = Convert.ToBase64String(cipherTextBytes);

                // Save our encrypted text
                File.WriteAllText(fileName, cipherText);

                // Export the RSA private key (put this somewhere safe!)
                using (var fs = File.Create(keyFileName))
                {
                    using (var pem = new PemWriter(fs))
                    {
                        pem.WritePrivateKey(rsa);
                    }
                }
            }
        }

        public string LoadAndDecrypt(string fileName, string keyFileName)
        {
            var plainText = string.Empty;

            using (var rsa = GetRSACryptoProvider(keyFileName))
            {
                var cipherText = File.ReadAllText(fileName);
                var cipherTextBytes = Convert.FromBase64String(cipherText);
                var plainTextBytes = rsa.Decrypt(cipherTextBytes, RSAEncryptionPadding.Pkcs1);
                plainText = Encoding.Unicode.GetString(plainTextBytes);
            }

            return plainText;
        }

        private RSA GetRSACryptoProvider(string keyFileName = null)
        {
            var rsa = RSA.Create();

            try
            {
                if (string.IsNullOrEmpty(keyFileName))
                {
                    rsa.KeySize = Convert.ToInt32(_config["Settings:KeySize"]);
                }
                else
                {
                    using (var privateKey = File.OpenRead(keyFileName))
                    {
                        using (var pem = new PemReader(privateKey))
                        {
                            var rsaParameters = pem.ReadRsaKey();
                            rsa.ImportParameters(rsaParameters);
                        }
                    }
                }
                return rsa;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Exception in GetRSACryptoProvider(): {ex}");
                return null;
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

I hope this serves as a brief introduction as to how to implement asymmetric RSA crypto using .NET Core. If you have any questions, feel free to poke me on Mastodon or send me an email.

Copyright © 2018 Andy Belfield. All rights reserved. Some portions may be CC-BY-NC where noted. Code examples on this web site are for educational purposes only. Don't use them in a production environment. Seriously.