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)
Based on comments from Klaus Gütte and Topaco, I'm satisfied with the response!
The problem is caused by the size of the input string, the limit of the input so is the size of the input according to the question linked by Klaus, and this size depends on the size configured for the encryption key as mentioned by Topaco!
Link to cause clarification: System.Security.Cryptography.CryptographicException : Bad length in RSACryptoserviceProvider
Link to clarify input size limit: https://crypto.stackexchange.com/questions/42097/what-is-the-maximum-size-of-the-plaintext-message-for-rsa-oaep/42100# 42100
More specifically: https://www.rfc-editor.org/rfc/rfc8017#section-7.1.1
Print RSAES-OAEP-ENCRYPT standars
In my specific case, the maximum length of strings accepted by the encrypt/decrypt methods was 214 (Length). Test with 100k random strings and it worked perfectly! However, passing a string size >= 215 already generates the "Bad length" exception.
Success with length <= 214
Fail with 215
Thanks a lot for the help guys!!