I am trying to use RSA 2048 to have encryption between my C# and C programs. To accomplish this, I am imbedding the private key inside my C program and imbedding the public key inside the C# program. Then my C# program encrypts a string and sends it to my C program for decrypting. The issue is I am receiving a 0xc000000d error when calling the BCryptDecrypt function inside my C program when I use any padding option other than BCRYPT_PAD_NONE. The issue with this is that C#'s System.Security.Cryptography library only has options to use PKCS1 and OAEP which don't seem compatible with BCrypt's version (or my public key is wrong doubtful though). I determined this because setting var encryptedData = rsa.Encrypt(dataToEncryptBytes, true); to true means it uses OAEP while false means it uses PKCS1 and when I made these changes, I also modified both BCryptDecrypt function calls inside my DecryptRsaData function where it always results in a 0xc000000d error aka INVALID_PARAMETER. Then when I changed to using BCRYPT_PAD_NONE it did not result in an error (since it is not even checking the data anymore) but the resulting PUCHAR after decrypting is gibberish and does not contain my original string.
I originally generated the keys inside my C# program and then copied their keys into each program but that was bad since debugging the public key would be far easier since its only two parameters. So, I instead decided to generate the hex associated with both the private and public keys inside my C program and then copy the private key into an unsigned char array and copy the public key into a C# string for further processing. The following is how I did that.
static void PrintKeyInHex(PUCHAR key, ULONG keySize) {
for (ULONG i = 0; i < keySize; ++i) {
DebugMessage("%02X", key[i]); // %02X formats the output as two-digit hexadecimal
if (i + 1 < keySize) {
DebugMessage(":");
}
}
DebugMessage("\n");
}
NTSTATUS GenerateAndPrintKeys() {
BCRYPT_ALG_HANDLE hAlgorithm = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status = STATUS_UNSUCCESSFUL;
PUCHAR pbPublicKey = NULL, pbPrivateKey = NULL;
ULONG cbPublicKey = 0, cbPrivateKey = 0, cbData = 0;
// Open an algorithm provider for RSA.
status = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_RSA_ALGORITHM, NULL, 0);
if (!NT_SUCCESS(status)) {
DebugMessage("Failed to open algorithm provider.");
goto Cleanup;
}
// Generate the key pair.
status = BCryptGenerateKeyPair(hAlgorithm, &hKey, 2048, 0);
if (!NT_SUCCESS(status)) {
DebugMessage("Failed to generate key pair.");
goto Cleanup;
}
// Finalize the key (make it usable).
status = BCryptFinalizeKeyPair(hKey, 0);
if (!NT_SUCCESS(status)) {
DebugMessage("Failed to finalize key pair.");
goto Cleanup;
}
// Export the public key.
status = BCryptExportKey(hKey, NULL, BCRYPT_RSAPUBLIC_BLOB, NULL, 0, &cbPublicKey, 0);
if (!NT_SUCCESS(status)) {
DebugMessage("Failed to get public key size.");
goto Cleanup;
}
pbPublicKey = (PUCHAR)ExAllocatePoolWithTag(NonPagedPool, cbPublicKey, '1Tag');
if (!pbPublicKey) {
status = STATUS_NO_MEMORY;
goto Cleanup;
}
status = BCryptExportKey(hKey, NULL, BCRYPT_RSAPUBLIC_BLOB, pbPublicKey, cbPublicKey, &cbData, 0);
if (!NT_SUCCESS(status)) {
DebugMessage("Failed to export public key.");
goto Cleanup;
}
// Export the private key
status = BCryptExportKey(hKey, NULL, BCRYPT_RSAPRIVATE_BLOB, NULL, 0, &cbPrivateKey, 0);
if (!NT_SUCCESS(status)) {
DebugMessage("Failed to get private key size.");
goto Cleanup;
}
pbPrivateKey = (PUCHAR)ExAllocatePoolWithTag(NonPagedPool, cbPrivateKey, '2Tag');
if (!pbPrivateKey) {
status = STATUS_NO_MEMORY;
goto Cleanup;
}
status = BCryptExportKey(hKey, NULL, BCRYPT_RSAPRIVATE_BLOB, pbPrivateKey, cbPrivateKey, &cbData, 0);
if (!NT_SUCCESS(status)) {
DebugMessage("Failed to export private key.");
goto Cleanup;
}
DebugMessage("Public Key: ");
PrintKeyInHex(pbPublicKey, cbPublicKey);
DebugMessage("\n \n");
DebugMessage("Private Key: ");
PrintKeyInHex(pbPrivateKey, cbPrivateKey);
Cleanup:
if (pbPublicKey) ExFreePoolWithTag(pbPublicKey, '1Tag');
if (pbPrivateKey) ExFreePoolWithTag(pbPrivateKey, '2Tag');
if (hKey) BCryptDestroyKey(hKey);
if (hAlgorithm) BCryptCloseAlgorithmProvider(hAlgorithm, 0);
return status;
}
This ensured that my private key was correct which just means I needed to make the public key work with C#'s System.Security.Cryptography RSA algorithm. But since I knew the public key ended with the modulus (256 bytes) then the exponent (3 bytes) at the end it was fairly easy. Then since I was not sure on the endianness (also just to cover all the bases) I decided to brute force it by trying all 4 endian combinations and sending it to my C program. By that I mean I would call the following C# EncryptDataWithPublicKey method 4 times with an i from 0 - 3 ensuring any errors was not due to incorrect endianness. Even so I still received the same issues outlined in the first paragraph all 4 times.
public static byte[] EncryptDataWithPublicKey(string dataToEncrypt, int i)
{
RSAParameters rsaParams = ConvertHexToRSAParameters(hexPublicKey, i);
using (var rsa = new RSACryptoServiceProvider())
{
// Load the RSA public key
rsa.ImportParameters(rsaParams);
// Convert the data to encrypt to a byte array
var dataToEncryptBytes = Encoding.UTF8.GetBytes(dataToEncrypt);
// Encrypt the data. OAEP padding is recommended for new applications.
var encryptedData = rsa.Encrypt(dataToEncryptBytes, false); // Set to true to use OAEP padding
return encryptedData;
}
}
private static RSAParameters ConvertHexToRSAParameters(string hexString, int i)
{
byte[] bytes = HexStringToByteArray(hexString);
Console.WriteLine("BytesSize:" + bytes.Length);
bytes.ToList().ForEach(i => Console.Write(i.ToString() + " "));
RSAParameters rsaParams = new RSAParameters();
// Extract the modulus and exponent from the blob
byte[] exponent = ExtractExponent(bytes, 3); // Assuming 3 bytes for exponent
byte[] modulus = ExtractModulus(bytes, 256);
if(i == 1)
{
Array.Reverse(exponent);
Array.Reverse(modulus);
}
else if(i == 2)
{
Array.Reverse(exponent);
}
else if(i == 3)
{
Array.Reverse(modulus);
}
rsaParams.Exponent = exponent;
rsaParams.Modulus = modulus;
return rsaParams;
}
Anyways once my C program tries to decrypt the original string, I get a 0xc000000d status when using BCRYPT_PAD_PKCS1 or BCRYPT_PAD_OAEP while BCRYPT_PAD_NONE results in no status issues but unreadable data. My assumptions here are either the C library aka BCrypt's BCRYPT_PAD_OAEP and BCRYPT_PAD_PKCS1 are not compatible with C#'s version or I am creating the public keys wrong. I concluded this since I know the private key is 100% valid since its generated by the BCrypt algorithm and I know the public key's Hex is also correct, but I am not certain if I am converting the public keys hex to the correct format. Also, one last note based on earlier testing the BCryptImportKeyPair function would fail when I formatted the public key incorrectly inside my C# application, but I am fairly sure that is just a length check which tells me my field lengths are correct but then again, I am also trying every endianness possibility so I am really at a lost to what the issue could be. That being said the following is my C code that I use to decrypt the actual data.
unsigned char g_privateKeyBlob[] = { 0x52, 0x53, 0x41, 0x32, ... }; // I cut off the rest since its 539 bytes long
static void DebugPrintEncryptedData(UCHAR* data, ULONG length) {
for (ULONG i = 0; i < length; ++i) {
DebugMessage("%02X ", data[i]);
if ((i + 1) % 16 == 0) {
DebugMessage("\n"); // New line every 16 bytes for readability
}
}
DebugMessage("\n"); // New line at the end
}
// This code then runs which calls the bellow function and then formats the data in a readable format with the above function
ULONG privateKeyBlobLength = sizeof(g_privateKeyBlob); // Get the size of your private key blob
PUCHAR decryptedData = NULL;
ULONG decryptedDataLength = 0;
NTSTATUS stat = DecryptRsaData(handshakeData->EncryptedData, 256, g_privateKeyBlob, privateKeyBlobLength, &decryptedData, &decryptedDataLength);
DebugMessage("FirstDebug Status:%d \n", stat);
DebugMessage("Status:0x%x \n", stat);
DebugPrintEncryptedData(decryptedData, decryptedDataLength);
DebugMessage("Length:%lu \n", decryptedDataLength);
DebugPrintEncryptedData(handshakeData->EncryptedData, 256);
DebugMessage("PrivateKeyLength:%lu \n \n", privateKeyBlobLength);
NTSTATUS DecryptRsaData(
PUCHAR encryptedData, ULONG encryptedDataLength,
PUCHAR privateKeyBlob, ULONG privateKeyBlobLength,
PUCHAR* decryptedData, PULONG decryptedDataLength) {
BCRYPT_ALG_HANDLE algHandle = NULL;
BCRYPT_KEY_HANDLE keyHandle = NULL;
NTSTATUS status = STATUS_UNSUCCESSFUL;
DebugMessage("DecryptRsaData 1 \n");
// Open an algorithm provider for RSA.
status = BCryptOpenAlgorithmProvider(&algHandle, BCRYPT_RSA_ALGORITHM, NULL, 0);
if (!NT_SUCCESS(status)) {
return status;
}
DebugMessage("DecryptRsaData 2 \n");
// Import the private key.
status = BCryptImportKeyPair(algHandle, NULL, BCRYPT_RSAPRIVATE_BLOB, &keyHandle, privateKeyBlob, privateKeyBlobLength, 0);
if (!NT_SUCCESS(status)) {
BCryptCloseAlgorithmProvider(algHandle, 0);
return status;
}
DebugMessage("DecryptRsaData 3 \n");
// Get the size needed for the decrypted data buffer.
status = BCryptDecrypt(keyHandle, encryptedData, encryptedDataLength, NULL, NULL, 0, NULL, 0, decryptedDataLength, BCRYPT_PAD_PKCS1); // We are failing here
if (!NT_SUCCESS(status)) {
BCryptDestroyKey(keyHandle);
BCryptCloseAlgorithmProvider(algHandle, 0);
return status;
}
DebugMessage("DecryptRsaData 4 \n");
// Allocate the buffer for the decrypted data.
*decryptedData = (PUCHAR)ExAllocatePoolWithTag(NonPagedPool, *decryptedDataLength, 'decR');
if (*decryptedData == NULL) {
BCryptDestroyKey(keyHandle);
BCryptCloseAlgorithmProvider(algHandle, 0);
return STATUS_INSUFFICIENT_RESOURCES;
}
DebugMessage("DecryptRsaData 5 \n");
// Perform the decryption.
status = BCryptDecrypt(keyHandle, encryptedData, encryptedDataLength, NULL, NULL, 0, *decryptedData, *decryptedDataLength, decryptedDataLength, BCRYPT_PAD_PKCS1);
if (!NT_SUCCESS(status)) {
ExFreePoolWithTag(*decryptedData, 'decR');
*decryptedData = NULL;
*decryptedDataLength = 0;
}
DebugMessage("DecryptRsaData 6 \n");
BCryptDestroyKey(keyHandle);
BCryptCloseAlgorithmProvider(algHandle, 0);
return status;
}
Edit: Ok I made some advancements here are the keys for reference. I also included the public key layout in BCrypt.
typedef struct _BCRYPT_RSAKEY_BLOB {
ULONG Magic; // A magic number identifying the blob type, for public key, this is BCRYPT_RSAPUBLIC_MAGIC.
ULONG BitLength; // The length of the key, in bits.
ULONG cbPublicExp; // The length of the public exponent, in bytes.
ULONG cbModulus; // The length of the modulus, in bytes.
// Following this header in memory would be:
// - The public exponent (of length cbPublicExp)
// - The modulus (of length cbModulus)
} BCRYPT_RSAKEY_BLOB;
Public Key: 52:53:41:31:00:08:00:00:03:00:00:00:00:01:00:00:00:00:00:00:00:00:00:00:01:00:01:B6:49:39:26:7C:58:59:09:F3:BE:14:10:A8:A2:E1:7A:1B:1E:32:6F:43:65:B5:56:1A:30:24:7D:DB:AA:6A:58:C9:40:AA:B4:91:5C:D3:9B:1C:9D:DD:97:0E:BF:D8:D2:40:1F:C4:27:E4:1E:74:3A:A1:9E:F7:60:B8:C1:47:86:F5:C4:73:F3:A7:DD:AA:F6:6F:81:FA:FD:FE:4E:00:D4:5A:04:2D:24:16:89:D5:03:30:72:36:A5:FE:D2:8E:96:76:EB:04:BC:92:29:3B:77:37:C8:C3:09:27:A9:CD:52:B9:DD:7C:20:AE:60:F8:50:B9:DA:CD:E2:4A:4C:18:7A:66:FC:8E:F7:84:E8:D1:B6:CB:6B:DD:8E:56:F5:99:80:B3:27:44:60:29:2C:3F:1C:72:72:0A:37:FB:FE:37:FA:61:37:2E:A1:EC:35:DE:FA:91:23:A0:91:C4:AC:E4:2F:C9:6D:85:84:94:5A:8A:F3:47:FD:FB:6C:58:7A:62:49:C7:2C:8E:05:E9:4D:79:5B:61:51:CF:2E:1A:4C:22:A5:15:C6:CB:70:8B:D3:E6:0E:1D:C9:62:37:D2:0E:5F:DD:50:B3:05:E1:5B:CE:1A:F5:22:9B:5F:6E:9C:7B:DF:9A:98:19:93:1B:00:93:27:F3:69:43:CD:08:6D:E6:D8:55
Private Key: 52:53:41:32:00:08:00:00:03:00:00:00:00:01:00:00:80:00:00:00:80:00:00:00:01:00:01:B6:49:39:26:7C:58:59:09:F3:BE:14:10:A8:A2:E1:7A:1B:1E:32:6F:43:65:B5:56:1A:30:24:7D:DB:AA:6A:58:C9:40:AA:B4:91:5C:D3:9B:1C:9D:DD:97:0E:BF:D8:D2:40:1F:C4:27:E4:1E:74:3A:A1:9E:F7:60:B8:C1:47:86:F5:C4:73:F3:A7:DD:AA:F6:6F:81:FA:FD:FE:4E:00:D4:5A:04:2D:24:16:89:D5:03:30:72:36:A5:FE:D2:8E:96:76:EB:04:BC:92:29:3B:77:37:C8:C3:09:27:A9:CD:52:B9:DD:7C:20:AE:60:F8:50:B9:DA:CD:E2:4A:4C:18:7A:66:FC:8E:F7:84:E8:D1:B6:CB:6B:DD:8E:56:F5:99:80:B3:27:44:60:29:2C:3F:1C:72:72:0A:37:FB:FE:37:FA:61:37:2E:A1:EC:35:DE:FA:91:23:A0:91:C4:AC:E4:2F:C9:6D:85:84:94:5A:8A:F3:47:FD:FB:6C:58:7A:62:49:C7:2C:8E:05:E9:4D:79:5B:61:51:CF:2E:1A:4C:22:A5:15:C6:CB:70:8B:D3:E6:0E:1D:C9:62:37:D2:0E:5F:DD:50:B3:05:E1:5B:CE:1A:F5:22:9B:5F:6E:9C:7B:DF:9A:98:19:93:1B:00:93:27:F3:69:43:CD:08:6D:E6:D8:55:E0:13:02:06:E2:A0:BB:63:B7:5E:3C:27:8C:75:E4:EA:92:E4:75:A3:2E:9E:AF:C0:8A:87:A0:50:DC:77:1E:B7:D8:3F:7F:9E:42:4D:06:AB:07:3C:77:F5:00:64:8F:C1:F2:86:89:18:B9:4E:B7:FA:26:CE:58:BA:08:B3:B7:86:D3:53:A5:D5:4E:66:D9:CF:3B:18:C2:FB:D0:DE:B5:A2:6F:72:8F:71:D3:D4:0A:36:97:14:9C:ED:AE:03:DC:BC:4E:B4:CF:1F:DC:F5:C7:35:F6:AC:BB:DD:22:AC:CF:B1:BB:83:ED:86:58:E0:72:18:54:EE:A9:8D:76:23:14:03:D0:42:02:F9:4F:E4:FE:00:2A:76:97:6D:1D:F5:6D:F3:27:4C:9D:4D:C6:C2:6A:B9:30:C3:D0:E8:99:F3:C0:6C:ED:25:85:6F:71:CF:D2:0F:F2:18:E5:FC:EA:16:9C:4B:81:C9:3B:4A:DF:84:BF:CD:C4:1F:04:52:2E:77:D7:51:2B:4B:1F:25:14:7C:E4:1B:A2:7B:57:A1:95:49:A0:4B:0A:C6:6B:9E:99:00:29:E9:B0:07:F1:7D:2C:ED:8F:38:20:3A:EA:36:67:53:C5:29:35:2C:63:7A:50:60:3E:70:8D:24:5C:B0:1A:1E:E1:1C:42:1E:D0:C4:6E:03:6E:C7
Above we can see that theres 4, 4-byte hex values at the start in Little-Endian. Magic and BitLength dont matter to us but the next two do. Here we get confirmation that the Exponent is 3 bytes and Modulus is 256 bytes.
byte[] bytes = HexStringToByteArray(publicKey);
int bitLength = BitConverter.ToInt32(bytes, 4);
int cbPublicExp = BitConverter.ToInt32(bytes, 8);
int cbModulus = BitConverter.ToInt32(bytes, 12);
Console.WriteLine(" \n Exp:");
byte[] exponent = ExtractExponent(bytes, cbPublicExp, cbModulus);
exponent.ToList().ForEach(i => Console.Write(i.ToString() + " "));
Console.WriteLine(" \n Mod:");
byte[] modulus = ExtractModulus(bytes, cbModulus);
modulus.ToList().ForEach(i => Console.Write(i.ToString() + " "));
But the far more interesting thing we notice is that the exponent actually comes before the Modulus which means I was reading it wrong. Also, another thing to point out here is that so far, we have 16 bytes for the defined fields + 3 bytes for the Exponent and 256 bytes for the Modulus which only gives us 275 bytes while our public key is 283 bytes long meaning we have 8 bytes extra. Based on common sense we can see that there are 8 bytes worth of zeros till we get to the last 259 bytes where the first 3 bytes of that is 01 00 01 also known as 65,537 in decimal which is a common Exponent for RSA.
private static byte[] ExtractModulus(byte[] keyBlob, int modulusLength)
{
int startIndex = keyBlob.Length - modulusLength;
return keyBlob.Skip(startIndex).Take(modulusLength).ToArray();
}
private static byte[] ExtractExponent(byte[] keyBlob, int exponentLength, int modulusLength)
{
int startIndex = keyBlob.Length - exponentLength - modulusLength;
return keyBlob.Skip(startIndex).Take(exponentLength).ToArray();
}
So that's that right? Nope, I now know what every byte in the public key represents with 100% accuracy but even after making these changes I still have the exact same issue outlined in the first paragraph. Anyone have any ideas?
The mistake is revealed if you look closely at the definitions of
BCRYPT_RSAKEY_BLOBandBCRYPT_RSAPUBLIC_BLOB:BCRYPT_RSAKEY_BLOBis defined here and looks as follows:The public key you posted is a
BCRYPT_RSAPUBLIC_BLOBstructure, which is defined here and looks like this:Note that all of the numbers following the
BCRYPT_RSAKEY_BLOBstructure are in big-endian order.In your C# code, however, you are inverting the exponent and modulus, which is wrong and the main cause of the problem.
There is also a typo in the
ExtractExponent()call (the third parameter is missing).Fix:
Test: Using the public key you posted and the plaintext The quick brown fox jumps over the lazy dog, the fixed C# code returns e.g. the following ciphertext:
which can be successfully decrypted with your C code and the posted private key.
A note on ...meaning we have 8 bytes extra. Based on common sense we can see that there are 8 bytes worth of zeros...: These 0x00 sequences are due to
cbPrime1andcbPrime2, which are not included in the case of a public key, i.e. have the length 0 (which due toULONGleads to 4 bytes each consisting of 0x00 values)