Following is my implementation of Rsa encryption and decryption methods, to do it I based myself on microsoft documentation.
public string Encrypt(string plainText)
{
rsaCsp.ImportParameters(publicKey);
byte[] data = Encoding.UTF8.GetBytes(plainText);
byte[] cypher = rsaCsp.Encrypt(data, fOAEP: true);
string cypherText = Convert.ToBase64String(cypher);
return cypherText;
}
public string Decrypt(string cypherText)
{
rsaCsp.ImportParameters(privateKey);
byte[] data = Convert.FromBase64String(cypherText);
byte[] decrypted = rsaCsp.Decrypt(data, fOAEP: true);
string plainText = Encoding.UTF8.GetString(decrypted);
return plainText;
}
I am injecting the keys and the provider so that I can reuse specific keys, so that I can even decrypt strings that were encrypted in other executions of the program. (Example: I encrypt in a test and save in a database and in another test I search in the database the encrypted string and decrypt). If I allowed public and private keys to be generated automatically this behavior would not be possible.
private readonly RSAParameters privateKey;
private readonly RSAParameters publicKey;
private readonly RSACryptoServiceProvider rsaCsp;
public RsaCryptoService(
RSACryptoServiceProvider rsaCsp,
RSAParameters privateKey,
RSAParameters publicKey)
{
this.rsaCsp = rsaCsp;
this.privateKey = privateKey;
this.publicKey = publicKey;
}
Below is the construction of the dependencies:
public static void InjectRsaCryptoService(this IServiceCollection services, string userName = "default", int keySize = 2048)
{
Services = services;
RsaKeyPair rsaKeyPair = SecuritySettings.RsaKeys.SingleOrDefault(p => p.UserName == userName);
if (rsaKeyPair == null)
throw new lsilvpin_securityException(
$"O usuário {userName} não está autorizado a utilizar as ferramentas de segurança.");
RSAParameters privateKey = rsaKeyPair.PrivateKey.AsParameter();
RSAParameters publicKey = rsaKeyPair.PublicKey.AsParameter();
RSACryptoServiceProvider rsaCsp = new RSACryptoServiceProvider(keySize);
rsaCsp.ImportParameters(publicKey);
rsaCsp.ImportParameters(privateKey);
Services.AddTransient(sp =>
{
IRsaCryptoService rsa = new RsaCryptoService(rsaCsp, privateKey, publicKey);
return rsa;
});
}
At first I thought it was working fine, but to be safe, I decided to do a performance test, sending random strings to be encrypted and decrypted.
Here's my performance test:
[Fact]
public void TestRsaCryptionPerformance()
{
for (int i = 0; i < 1000; i++)
{
var plainText = RandomWordGenerator.Next(new RandomWordParameters(WordMaxLength: 495)) + "@eA8";
IRsaCryptoService rsa = Services
.BuildServiceProvider()
.GetRequiredService<IRsaCryptoService>();
string publicKey = rsa.GetPublicKey();
string cypherText = rsa.Encrypt(plainText);
string decrypted = rsa.Decrypt(cypherText);
Assert.True(!string.IsNullOrWhiteSpace(publicKey));
Assert.True(!string.IsNullOrWhiteSpace(cypherText));
Assert.True(!string.IsNullOrWhiteSpace(decrypted));
Assert.True(plainText.Equals(decrypted));
Debug.WriteLine($"PublicKey: {publicKey}");
Debug.WriteLine($"CypherText: {cypherText}");
Debug.WriteLine($"Decrypted: {decrypted}");
}
}
I'm generating random words of size between 100 and 499 with the following method:
public static string Next(RandomWordParameters? randomWordParameters = null)
{
randomWordParameters ??= new RandomWordParameters();
if (randomWordParameters.CharLowerBound <= 0 || randomWordParameters.CharUpperBound <= 0)
throw new RandomGeneratorException("Os limites inferior e superior devem ser positivos.");
if (randomWordParameters.CharLowerBound >= randomWordParameters.CharUpperBound)
throw new RandomGeneratorException("O limite inferior deve ser menor do que o limite superior.");
var rd_char = new Random();
var rd_length = new Random();
var wordLength = rd_length.Next(randomWordParameters.WordMinLength, randomWordParameters.WordMaxLength);
var sb = new StringBuilder();
int sourceNumber;
for (int i = 0; i < wordLength; i++)
{
sourceNumber = rd_char.Next(randomWordParameters.CharLowerBound, randomWordParameters.CharUpperBound);
sb.Append(Convert.ToChar(sourceNumber));
}
var word = sb.ToString();
return word;
}
Note: I use characters obtained by passing integers between 33 and 126 to the Convert.ToChar(int) method.
When I run a loop of 1000 random words, I always find one that throws the exception below:
Internal.Cryptography.CryptoThrowHelper.WindowsCryptographicException: 'Comprimento inválido.' (Invalid length)
String under test: a_BdH[(4/6-9m>,9a_J/^t2GCsxo{{W#j*!R![h;TMi/42Yw7Z0yWOb3f15&P:NEI7!vTFm8W.5Q1,d?I9DR>u{M0,$YxP1Cd ]MC(gnd$){x[`@L9C7E>z()PS,>w:|?<|j3!KCkC%usV']958^}a2P(SHun'=VR?^qLcj_1nu"UUR|Bu{UlP =mEJ0RIJP#9O$a^g7&I|Q^]_pG0!h}RjiEu_Df8*(@eA8
I want to use RSA encryption on a system I'm implementing, but this test made me unsure. Does anyone see where this problem comes from?
Note: The encrypt/decrypt methods worked perfectly with smaller and simpler strings. (Password simulations up to about 20 in length, from weak to very strong passwords)