I'm new to Crypto and working with RSA keys in general. I'm working with a Clover payment API and I have a working Java example that I'm running in VSCode. From a Base64 public key, it parses the modulus and exponent, generates a public key and then encrypts some card data. I'm trying to duplicate the functionality in C# without success.
taPublicKey = "vyYRQA3cS4wV9yk+6bFzA7KLDmE+D/SOP+Q5bNOPG9nUDkAPalRBz12KA5SDxTw2vO1BIeSFUQlYTpzEDb/XkfNNm5e6nqf12M4kdHP1F2EXW4WArilUZegAVw/Y7FvAkA8PQFbfgmBirSa5GS/fuAHjemqEf0DxIgq552IDeFw3nB0vccK6ePue5sVB9Sm2vWpKm/lj2UE4P6z2ngZr5V31cSAVN08USxHvz+MEnoUBKt6aKvfRUAp4iFyIpxlp4eylxY8zizPekS29lcRMsI9hGug2CoKFhhUJ1gD8G280zIoWCxysNvl40k/l8OTtPKrnlhAzQcIyy/RB0lwb6QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVTU="
final byte[] key = DatatypeConverter.parseBase64Binary(taPublicKey);
// BigIntegers in java are signed, we prefix by a zero byte to make sure we get unsigned.
// Not a problem for the exponent since it's heavily left-padded with zeros already
final byte[] unsignedPrefix = {0x00};
final BigInteger modulus1 = new BigInteger(ArrayUtils.addAll(unsignedPrefix, Arrays.copyOfRange(key, 0, 256)));
final BigInteger exponent1 = new BigInteger(Arrays.copyOfRange(key, 256, 512));
final KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(new RSAPublicKeySpec(modulus1, exponent1))
...
I understand that a byte is different in Java and C# and I may be having trouble with the endianness of byte arrays but again, I'm new to this and after a ton of searches and looking at different posts, I can't seem to find a working solution for encryption that is correct and accepted by the payment API. Apparently, from the Java and PHP examples I've seen, the first 256 bytes are the modulus and the second 256 bytes are the exponent.
byte[] publicKey = Convert.FromBase64String(taPublicKey);
byte[] modulus1 = new byte[256];
Array.Copy(publicKey, modulus1, 256);
byte[] exponent1 = new byte[256];
Array.Copy(publicKey, 256, exponent1, 0, 256);
// This may need to be unsigned, adding 0 to end??
BigInteger mod1 = new BigInteger(modulus1.Concat(new byte[] { 0 }).ToArray());
// Exponent is left padded with a bunch of 0's
// Hardcoded for now with Java results, RSACrptoServiceProvider can't import Exponent > 4 bytes
// BigInteger exp1 = new BigInteger(exponent1);
BigInteger exp1 = new BigInteger(415029);
RSAParameters rSAParameters = new RSAParameters
{
Modulus = mod1.ToByteArray(),
Exponent = exp1.ToByteArray()
};
//Add prefix to credit card number, using Clover test card number for sandbox per instructions
string cardToEncrypt = "00000000" + "6011361000006668";
string base64EncryptedCardNumber = null;
using (var rsa = new RSACryptoServiceProvider(2048))
{
try
{
rsa.PersistKeyInCsp = false;
rsa.ImportParameters(rSAParameters);
var encryptedUtf8CardNumber = rsa.Encrypt(Encoding.UTF8.GetBytes(cardToEncrypt), true);
// convert encrypted card to base64 per instructions
base64EncryptedCardNumber = Convert.ToBase64String(encryptedUtf8CardNumber);
}
catch (Exception ex)
{
// Handle this
}
finally
{
rsa.PersistKeyInCsp = false;
}
}
return base64EncryptedCardNumber;
...
Encryption seems to work but something is not correct, API responds with Invalid Request as well.
Any help is appreciated!
Java is predominantly big endian, and so is RSA if you use it according to PKCS#1.
The Java code is not very elegant as they could have used BigInteger(int signum, byte[] magnitude)
constructor to indicate a positive integer (RSA only works with positive integers).
C# is predominantly little endian, although it is officially platform dependent for the base types (which is a very stupid design decision). However, the default constructor that takes a byte array is explicitly little endian.
Now you could of course first reverse the array and then concatenate the zero byte to the end. However, it would be much clearer if you'd use the more elaborate constructor that takes booleans (yuck) for the sign and endianness.
That would give you:
BigInteger mod1 = new BigInteger(new ReadOnlySpan(modulus1), true, true);
instead of:
BigInteger mod1 = new BigInteger(modulus1.Concat(new byte[] { 0 }).ToArray());
I'd strongly recommend to use constants UNSIGNED
and BIG_ENDIAN
set to true
so that the constructor makes more sense when reading the code.
Just for completeness I'd also do the same thing for the public exponent. The public exponent is usually 65537 nowadays, but other values are absolutely valid, including ones that have as many bits as the modulus (and would therefore be considered a negative value if the most significant bit is considered a sign bit).
However, this is all not required as it seems that you could directly use the byte arrays for RSAParameters
without changing the order of the bytes or prefixing with 0x00
.