Creating a layered design in an application is a fundamental element of modern software architecture. The goal is to promote the Separation of Concerns (SoC) design principle. Separation of Concerns The ideas behind SoC date back to Dijkstra's 1974 paper "On the role of scientific thought" . In computer science, separation of concerns (sometimes abbreviated as SoC) is a design principle for separating a computer program into distinct sections. Each section addresses a separate concern, a set of information that affects the code of a computer program. A concern can be as general as "the details of the hardware for an application", or as specific as "the name of which class to instantiate". A program that embodies SoC well is called a modular program. Modularity, and hence separation of concerns, is achieved by encapsulating information inside a section of code that has a well-defined interface. - Wikipedia SoC is a broad design principal th...
Encrypting data exchanged between applications is a very common scenario. Oftentimes, this is done transparently by the communication protocol (HTTPS, for instance). However, when manual encryption is needed, .NET makes this easy via the RSACryptoServiceProvider class. While it's a simple class, there is a small learning curve.
To use the RSACryptoServiceProvider, first you must create an RSAParameters object. This object contains the information that will be used to encrypt/decrypt strings (public and private keys, for instance). In this example, for simplicity, we'll just create a new RSAParameters object every time a Web API application starts and expose it via Dependency Injection. More than likely, an application will create a public/private key combination and store them somewhere secure to maintain consistency when restarting the application. The application may even pull the keys from an SSL certificate equipped with the RSA algorithm, which the RSACryptoServiceProvider supports.
Configuration
Every time the Web API starts, let's create a new RSAParameters object by placing the following code in Program.cs.
RSAParameters _rsaParameters;
using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider())
{
_rsaParameters = provider.ExportParameters(true);
}
// make the RSAParameters available throughout the application
builder.Services.AddSingleton(_rsaParameters.GetType(), _rsaParameters);
The code above creates a new RSAParameters object and stores that object as a Singleton in the ServiceCollection. Any class in our application that needs to use RSA encryption can now access the same RSAParameters object. Keep in mind, this example is geared towards simplicity, and the keys will change every time the application restarts.
Usage
Now lets create a simple Controller class to test out the encryption. This is a highly simplified controller that doesn't follow Web API best practices; the purpose is to clearly illustrate how the RSACryptoServiceProvider works.
In the code below, we instantiate a new RSACryptoServiceProvider as needed and initialize it with the global RSAParameters.
[ApiController]
[Route("[controller]")]
public class EncryptedDataController : ControllerBase
{
private readonly ILogger<EncryptedDataController> _logger;
private readonly RSAParameters _rsaParameters;
public EncryptedDataController(ILogger<EncryptedDataController> logger, RSAParameters rsaParameters)
{
_logger = logger;
_rsaParameters = rsaParameters;
}
/// <summary>
/// Get the public key from that is in the current RSAParameters.
/// This can be used by a client to encrypt strings.
/// </summary>
/// <returns>The PEM encoded public key</returns>
[HttpGet(nameof(GetPublicRSAKey), Name = "GetPublicRSAKey")]
public string GetPublicRSAKey()
{
using (var provider = new RSACryptoServiceProvider())
{
// initialize the provider with the global RSAParameters (public/private key)
provider.ImportParameters(_rsaParameters);
// return the PEM encoded Public Key
return provider.ExportRSAPublicKeyPem();
}
}
/// <summary>
/// Returns an encrypted version of the string that is passed in.
/// The string can be decrypted using the private key that is in the current RSAParameters.
/// </summary>
/// <param name="unencryptedString">string to be encrypted</param>
/// <returns>encrypted string</returns>
[HttpGet(nameof(EncryptString), Name = "EncryptString")]
public string EncryptString(string unencryptedString)
{
using (var provider = new RSACryptoServiceProvider())
{
// initialize the provider with the public key to illustrate how a client will encrypt data
provider.ImportFromPem(GetPublicRSAKey());
// convert the string to a byte[], encrypt it, then convert it to a base64 string
return Convert.ToBase64String(provider.Encrypt(Encoding.Unicode.GetBytes(unencryptedString), true));
}
}
/// <summary>
/// Decrypts the passed in string.
/// The string will be decrypted using the private key that is in the current RSAParameters.
/// If the application has been restarted since encryption occurred, this will fail.
/// </summary>
/// <param name="encryptedString">base64 string to be decrypted</param>
/// <returns>decrypted string</returns>
[HttpPost(nameof(DecryptString), Name = "DecryptString")]
public string DecryptString(string encryptedString)
{
using (var provider = new RSACryptoServiceProvider())
{
// initialize the provider with the global RSAParameters (public/private key)
provider.ImportParameters(_rsaParameters);
// convert the string from a base64 string, decrypt it, then convert the return byte[] to a string
return Encoding.Unicode.GetString(provider.Decrypt(Convert.FromBase64String(encryptedString), true));
}
}
}
We can test the controller using curl or SwaggerUI as below (if SwaggerUI is enabled in the application's Program.cs).
Get the Public Key
The call to GetPublicRSAKey returns the PEM encoded public key.
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAL6jvt9KyGJd6sfju/j6+sX4NaWKU0sd8SInRAb/10IFlvhQ+Bd7F/AF
VnVvJuL4rG7bPKMS2LJkCtj+u/q4fKJBALzFdossviJ4ncfMv4TGdvfRaxdISHGz
g00RIK6Ce2pd0ZjWGvQOJx65s3Pjc47t0JLf9qz4ehqOPMs6SzMFAgMBAAE=
-----END RSA PUBLIC KEY-----
SwaggerUI Screenshot of GetPublicRSAKey
Encrypt a String
When we pass "test1234" to EncryptString, it returns: