I have written an asymmetric encryption wrapper as follows:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
namespace MyEncryptionLibrary
{
public static class AsymmetricCipher
{
//Options for serialising the data. This will make it as small as possible:
private static JsonSerializerOptions jsonSerialiserOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
public static string Encrypt<T>(T value, RSACryptoServiceProvider destinationPublicKeyRsa)
{
if (value == null)
{
return null;
}
try
{
var json = JsonSerializer.Serialize(value, jsonSerialiserOptions);
using (var aesAlg = Aes.Create())
{
using (var encryptor = aesAlg.CreateEncryptor())
{
using (var msEncrypt = new MemoryStream())
{
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (var swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(json);
}
//Encrypt the key asymmetrically:
var encryptedKey = destinationPublicKeyRsa.Encrypt(aesAlg.Key, true);
var decryptedContent = msEncrypt.ToArray();
using (var ms = new MemoryStream())
using (var bW = new BinaryWriter(ms))
{
bW.Write(encryptedKey.Length);
bW.Write(encryptedKey);
bW.Write(aesAlg.IV.Length);
bW.Write(aesAlg.IV);
bW.Write(decryptedContent.Length);
bW.Write(decryptedContent);
return Convert.ToBase64String(ms.ToArray());
}
}
}
}
}
catch
{
return null;
}
}
public static T Decrypt<T>(string value, RSACryptoServiceProvider receiverPrivateKeyRsa)
{
T result = default(T);
if (string.IsNullOrEmpty(value))
{
return result;
}
try
{
var fullCipher = Convert.FromBase64String(value);
using (var ms = new MemoryStream(fullCipher))
using (var bR = new BinaryReader(ms))
{
var encryptedKeyLength = bR.ReadInt32();
var encryptedKey = bR.ReadBytes(encryptedKeyLength);
var ivLength = bR.ReadInt32();
var iv = bR.ReadBytes(ivLength);
var cipherLength = bR.ReadInt32();
var cipher = bR.ReadBytes(cipherLength);
//Decrypt the asymmetrically encrypted key:
var key = receiverPrivateKeyRsa.Decrypt(encryptedKey, true);
using (var aesAlg = Aes.Create())
{
using (var decryptor = aesAlg.CreateDecryptor(key, iv))
{
using (var msDecrypt = new MemoryStream(cipher))
{
using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (var srDecrypt = new StreamReader(csDecrypt))
{
var json = srDecrypt.ReadToEnd();
result = JsonSerializer.Deserialize<T>(json);
}
}
}
}
}
}
}
catch { }
return result;
}
}
}
For my testing, I generated some keys as follows:
public class KeyGenerator
{
private RSA rsa;
public KeyGenerator()
{
rsa = RSA.Create();
}
public string GetPrivateKeyContainer()
{
return rsa.ToXmlString(true);
}
public string GetPublicKeyContainer()
{
return rsa.ToXmlString(false);
}
}
And then used them in the test via:
[TestInitialize]
public void Initialise()
{
keyGenerator = new KeyGenerator();
var publicKey = keyGenerator.GetPublicKeyContainer();
var privateKey = keyGenerator.GetPrivateKeyContainer();
publicKeyRsa = new RSACryptoServiceProvider();
publicKeyRsa.FromXmlString(publicKey);
privateKeyRsa = new RSACryptoServiceProvider();
privateKeyRsa.FromXmlString(privateKey);
}
I've written some unit tests for it, and it seems to work well for most of them. However, the following test is confusing me:
[TestMethod]
public void ItShouldWork_006()
{
//Here the private key is used in both directions:
var text = "I wrote this. It should not matter how long it is as I may be sending a large string, or a smaller string, it's hard to say.";
var encrpyted = AsymmetricCipher.Encrypt(text, privateKeyRsa);
var decrypted = AsymmetricCipher.Decrypt<string>(encrpyted, privateKeyRsa);
Assert.AreNotEqual(text, decrypted);
}
This test fails because the decrypted text matches the input text. I would have expected that something encrpted with the private key could not also be decrypted with the private key, but it seems that it can. Is this because the private key container contains both the private and public keys? Or am I doing something wrong?
Any advice is much appreciated!
CodePudding user response:
The documentation for the RSA.GetXmlString(bool)
method is fairly clear on this:
The ToXmlString method creates an XML string that contains either the public and private key of the current RSA object or contains only the public key of the current RSA object.
In fact all of the methods on the RSA
object that extract the private key (ExportRSAPrivateKey
, TryExportEncryptedPkcs8PrivateKey
and so on) do so as a public/private key pair. Encryption is always done with the public key of the pair, which is why both ends of a properly secured connection have their own key pairs. (There are mountains of writings on why, only a tiny portion of which I actually understand - encryption is weird.)
In principle it's possible to separate the public and private keys, but not with the standard RSA class.