Encrypting texts for an Identity in ASP.NET Core Razor Pages using AES and RSA

The article shows how encrypted texts can be created for specific users in an ASP.NET Core Razor page application. Symmetric encryption is used to encrypt the text or the payload. Asymmetric encryption is used to encrypt the AES key and the IV of the symmetric encryptions. Each ASP.NET Core Identity has an associated X509Certificate2 with a private key and a public key. The public key, which is saved in a Microsoft SQL database in the PEM format, is used to encrypt the key used for the AES encryption. Only the owner of the private key can then decrypt this.

Code: https://github.com/damienbod/SendingEncryptedData

Blogs in this series

Registering users using ASP.NET Core Identity

A standard ASP.NET Core application was created using ASP.NET Core Identity. The Razor pages from Identity were scaffolded into the application and a new class ApplicationUser was created which inherits from the IdentityUser class. This was then used everywhere instead of the IdentityUser class. The ApplicationUser has two extra properties, PemPrivateKey and PemPublicKey. This is used to save the RSA certificate with a 3072 key size and saved to the database using the PEM format.

public class ApplicationUser : IdentityUser
{
	public string PemPrivateKey { get; set; }

	public string PemPublicKey { get; set; }
}

The Register Razor Page is changed from the default scaffolded page to create a new RSA certificate for each new Identity. The X509Certificate2 was created using the CertificateManager Nuget package.

private readonly CreateCertificates _createCertificates;
private readonly ImportExportCertificate _importExportCertificate;
		
public RegisterModel(
	UserManager<ApplicationUser> userManager,
	SignInManager<ApplicationUser> signInManager,
	ILogger<RegisterModel> logger,
	IEmailSender emailSender,
	CreateCertificates createCertificates,
	ImportExportCertificate importExportCertificate)
{
	_userManager = userManager;
	_signInManager = signInManager;
	_logger = logger;
	_emailSender = emailSender;
	_createCertificates = createCertificates;
	_importExportCertificate = importExportCertificate;
}

The certificate is exported to a public key certificate in the PEM format and a PEM private key. This is then saved to the database for each new Identity. This PEM strings will be used when encrypting or decrypting texts.

 public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
	returnUrl = returnUrl ?? Url.Content("~/");
	ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
	if (ModelState.IsValid)
	{
		var identityRsaCert3072 = CreateRsaCertificates.CreateRsaCertificate(_createCertificates, 3072);
		var publicKeyPem = _importExportCertificate.PemExportPublicKeyCertificate(identityRsaCert3072);
		var privateKeyPem = _importExportCertificate.PemExportRsaPrivateKey(identityRsaCert3072);

		var user = new ApplicationUser { 
			UserName = Input.Email, 
			Email = Input.Email,
			PemPrivateKey = privateKeyPem,
			PemPublicKey = publicKeyPem
		};

		var result = await _userManager.CreateAsync(user, Input.Password);
		if (result.Succeeded)
		{
			_logger.LogInformation("User created a new account with password.");

			var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
			code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
			var callbackUrl = Url.Page(
				"/Account/ConfirmEmail",
				pageHandler: null,
				values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl },
				protocol: Request.Scheme);

			await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
				$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

			if (_userManager.Options.SignIn.RequireConfirmedAccount)
			{
				return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
			}
			else
			{
				await _signInManager.SignInAsync(user, isPersistent: false);
				return LocalRedirect(returnUrl);
			}
		}
		foreach (var error in result.Errors)
		{
			ModelState.AddModelError(string.Empty, error.Description);
		}
	}

	// If we got this far, something failed, redisplay form
	return Page();
}

Now all Identities have a certificate saved in the PEM format in two fields which can be used to encrypt or decrypt the texts.

Creating Encrypted texts for an Identity

A new Razor page was created to provide the UI to encrypt the texts. After you login, you can select any Identity which exists in the database. This is the target person to receive the encrypted message. A text can be pasted into the text area and the encrypt button submits the form as a HTTP Post.

@page
@model ExchangeSecureTexts.Pages.EncryptTextModel
@{
}

<form asp-page="EncryptTextModel" method="post">

    <div class="form-group">
        <label for="TargetUserEmail">Encrypted Text intended for Identity with Email address </label>
        <select name="TargetUserEmail" asp-items="Model.Users" class="form-control"></select>
    </div>

    <div class="form-group">
        <label for="Message">Message:</label>
        <textarea class="form-control" rows="5" id="Message" name="Message">@Model.Message</textarea>
        <span asp-validation-for="Message" style="color:red"></span>
    </div>

    <button type="submit" class="btn btn-primary" style="width:100%">Encrypt</button>

</form>

<br />
<br />
<textarea class="form-control" rows="5" id="EncryptedMessage" name="EncryptedMessage" readonly>@Model.EncryptedMessage</textarea>

The code behind the view of the Razor page requires properties to bind to. The SymmetricEncryptDecrypt, AsymmetricEncryptDecrypt, ApplicationDbContext , ImportExportCertificate services which were added to the DI in the startup class and are added in the constructor. The SymmetricEncryptDecrypt and the AsymmetricEncryptDecrypt services are used to encrypt and decrypt the texts. The ApplicationDbContext is used to find the Identities and select the public PEM string for the target user. The ImportExportCertificate is used to import the PEM string and create a X509Certificate2 to encrypt the key and the IV used for the AES encryption.

public class EncryptTextModel : PageModel
{
	private readonly SymmetricEncryptDecrypt _symmetricEncryptDecrypt;
	private readonly AsymmetricEncryptDecrypt _asymmetricEncryptDecrypt;
	private readonly ApplicationDbContext _applicationDbContext;
	private readonly ImportExportCertificate _importExportCertificate;

	[BindProperty]
	[Required]
	public string TargetUserEmail { get; set; }

	[BindProperty]
	[Required]
	public string Message { get; set; }

	[BindProperty]
	public string EncryptedMessage { get; set; }

	public List<SelectListItem> Users { get; set; }

	public EncryptTextModel(SymmetricEncryptDecrypt symmetricEncryptDecrypt,
		AsymmetricEncryptDecrypt asymmetricEncryptDecrypt,
		ApplicationDbContext applicationDbContext,
		ImportExportCertificate importExportCertificate)
	{
		_symmetricEncryptDecrypt = symmetricEncryptDecrypt;
		_asymmetricEncryptDecrypt = asymmetricEncryptDecrypt;
		_applicationDbContext = applicationDbContext;
		_importExportCertificate = importExportCertificate;
	}

	public IActionResult OnGet()
	{
		// not good if you have a lot of users
		Users = _applicationDbContext.Users.Select(a =>
							 new SelectListItem
							 {
								 Value = a.Email.ToString(),
								 Text = a.Email
							 }).ToList();

		return Page();
	}
}

The HTTP Post creates the encrypted data and returns this to the UI. Like in the previous post, the symmetric encryption creates a new AES key and IV in a base 64 format. The target Identity email is used to get the public Key PEM string and a X509Certificate2 certificate is created from this. The key and the IV are then encrypted using this RSA asymmetric encryption. The data is then serialized in a Json string format and returned to the UI.

public IActionResult OnPost()
{
	if (!ModelState.IsValid)
	{
		// Something failed. Redisplay the form.
		return OnGet();
	}

	var (Key, IVBase64) = _symmetricEncryptDecrypt.InitSymmetricEncryptionKeyIV();

	var encryptedText = _symmetricEncryptDecrypt.Encrypt(Message, IVBase64, Key);

	var targetUserPublicCertificate = GetCertificateWithPublicKeyForIdentity(TargetUserEmail);

	var encryptedKey = _asymmetricEncryptDecrypt.Encrypt(Key,
		Utils.CreateRsaPublicKey(targetUserPublicCertificate));

	var encryptedIV = _asymmetricEncryptDecrypt.Encrypt(IVBase64,
		Utils.CreateRsaPublicKey(targetUserPublicCertificate));

	var encryptedDto = new EncryptedDto
	{
		EncryptedText = encryptedText,
		Key = encryptedKey,
		IV = encryptedIV
	};

	string jsonString = JsonSerializer.Serialize(encryptedDto);

	EncryptedMessage = $"{jsonString}";

	// Redisplay the form.
	return OnGet();

}

private X509Certificate2 GetCertificateWithPublicKeyForIdentity(string email)
{
	var user = _applicationDbContext.Users.First(user => user.Email == email);
	var cert = _importExportCertificate.PemImportCertificate(user.PemPublicKey);
	return cert;
}

The data can now be copy pasted and sent as an email over an insecure channel or whatever. The Decrypt Razor Page can be used to read the data.

Decrypting texts

The Decrypt Razor Page takes the encrypted Json string and submits a HTTP POST request. The original text is returned if the Identity trying to read the data has the correct PEM private key to encrypt the key and the IV.

@page
@model ExchangeSecureTexts.Pages.DecryptTextModel
@{
}

<form asp-page="DecryptTextModel" method="post">


    <div class="form-group">
        <label for="Message">EncryptedMessage:</label>
        <textarea class="form-control" rows="5" id="Message" name="EncryptedMessage">@Model.EncryptedMessage</textarea>
        <span asp-validation-for="EncryptedMessage" style="color:red"></span>
    </div>

    <button type="submit" class="btn btn-primary" style="width:100%">Decrypt</button>

</form>

<br />
<br />
<textarea class="form-control" rows="5" id="Message" name="Message" readonly>@Model.Message</textarea>

The Post method gets the RSA certificate for the Identity using the public and the private PEM strings in the database. The key and the IV are then decrypted using the RSA private key. The key and the IV are used to decrypt the AES encryption.

public IActionResult OnPost()
{
	if (!ModelState.IsValid)
	{
		// Something failed. Redisplay the form.
		return OnGet();
	}

	var cert = GetCertificateWithPrivateKeyForIdentity();

	var encryptedDto = JsonSerializer.Deserialize<EncryptedDto>(EncryptedMessage);

	var key = _asymmetricEncryptDecrypt.Decrypt(encryptedDto.Key,
		Utils.CreateRsaPrivateKey(cert));

	var IV = _asymmetricEncryptDecrypt.Decrypt(encryptedDto.IV,
		Utils.CreateRsaPrivateKey(cert));

	var text = _symmetricEncryptDecrypt.Decrypt(encryptedDto.EncryptedText, IV, key);

	Message = $"{text}";

	// Redisplay the form.
	return OnGet();

}

private X509Certificate2 GetCertificateWithPrivateKeyForIdentity()
{
	var user = _applicationDbContext.Users.First(user => user.Email == User.Identity.Name);

	var certWithPublicKey = _importExportCertificate.PemImportCertificate(user.PemPublicKey);
	var privateKey = _importExportCertificate.PemImportPrivateKey(user.PemPrivateKey);

	var cert = _importExportCertificate.CreateCertificateWithPrivateKey(
		certWithPublicKey, privateKey);

	return cert;
}

The encrypted text can now be read again.

By using asymmetric encryption in this way together with symmetric encryption, a text can be sent over an insecure channel in a safe way, like for example in an email. The sender knows that only the owner of the private key can read the message. The receiver of the message does not know who sent the message. This can be solved using Hashes and Digital Signatures. This is also required when using CBC mode in AES. The private key is saved in the database in a plain PEM format. This is not good, because if the database gets lost, or made public, then all messages can be read. This needs protection. The users in the encrypt Razor page select is just returned as a list. A search function would need to be implemented here or paging. Error handling would also be required for a real application.

Links:

https://docs.microsoft.com/en-us/dotnet/standard/security/encrypting-data

https://docs.microsoft.com/en-us/dotnet/standard/security/decrypting-data

https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview

https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata.unprotect

https://docs.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection

https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.aes?view=netcore-3.1

https://docs.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography

https://docs.microsoft.com/en-us/dotnet/standard/security/vulnerabilities-cbc-mode

https://edi.wang/post/2019/1/15/caveats-in-aspnet-core-data-protection

https://dev.to/stratiteq/cryptography-with-practical-examples-in-net-core-1mc4

https://www.tpeczek.com/2020/08/supporting-encrypted-content-encoding.html

https://cryptobook.nakov.com/

https://www.meziantou.net/cryptography-in-dotnet.htm

4 comments

  1. Interesting… I had to do something similar a few years back.. I used libsodium (https://github.com/tabrath/libsodium-core/), it is an implementation of NaCl… great stuff.

    Coupled with javascript’s implementation of libsodium, the encrypted text is encrypted at the client’s computer… never reaches the server in plaintext, so even the server admin or db admin won’t be able to see it. Only someone with the correct private key will be able to decrypt it the original text.

    No trusted parties required.

    1. Hi Rosdi thanks for the link, I must try this out.

      Greetings Damien

  2. […] Encrypting texts for an Identity in ASP.NET Core Razor Pages using AES and RSA (Damien Bowden) […]

  3. […] Azure Functions using API Keys, Symmetric and Asymmetric Encryption in .NET Core & Encrypting texts for an Identity in ASP.NET Core Razor Pages using AES and RSA – Damien […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.