スキップしてメイン コンテンツに移動

C++ and C#/.NET Interoperability for RSA Public-key Cryptography andAES Symmetric Cipher

When you have to write a secure network application, cryptography is one of the topics you can't escape from. In most cases there are high-level packages such as SSL available, but it's not always like that and you may have to go lower-level. Besides, even if you don't program a custom security solution by yourself, it's not a bad idea to know how these secure protocols actually work as it helps you to choose a right solution for your problem. This article provides a basic idea of secure communication by illustrating C++ and C# code examples. Also this article will be useful for those who writes a custom secure protocol between a C++ application and a C# application. (Disclaimer: but don't use the example explained here as is in your mission-critical application! This article is only for the education purpose. Realworld secure communication libraries implement countermeasures against many kinds of known cryptographic attacks while this sample unfortunately doesn't.)

Regarding the sample code of this article, the C# side is based on .NET Framework 1.1 and the C++ side is based on Crypto++ Library 5.2.1. .NET Framework is an obvious solution for a .NET application and it has necessary tools for security albeit lacks a bit of variety. On the other hand Crypto++ Library is a full of variety and even supports many obscure crypto algorithms in the Open-Source form. Though the Platform SDK provided by Microsoft also offers a security framework and related utilities, I prefer Crypto++ Library because it's cross-platform and Open Source with a lenient license. It is updated rather quickly considering it's maintained by a single person. The only point in Crypto++ Library that may not be easy for a newcomer is its relatively complicated framework based on C++ template. In the C++ example of this article I'll show you minimal but effective use of it.

The basic flow and interaction between these C# and C++ programs is like this:

1. The C# program generates a 1024-bit RSA public and private key pair (pubkey.txt and prvkey.txt respectively)
2. The C++ program receives the public key (pubkey.txt). It generates a 128-bit key and a 128-bit initialization vector (IV) for 128-bit AES symmetric cipher and encrypts them using the public key sent from the C# program, producing a ciphertext (cipher.txt). The key and the IV in a plaintext are saved locally (aeskey.txt) while the ciphertext is sent to the C# program.
3. The C# program decrypts the ciphertext (cipher.txt) with the RSA private key. With the obtained key and IV, it encrypts a message and sends the resulted ciphertext (aescipher.txt) to the C++ program.
4. The C++ program receives the ciphertext and decrypts it with the AES key (aeskey.txt).

The step 1 and 2 are the preparation for establishing a secure channel. The 3 and 4 are the parts that does actual message exchange. As you notice, establishing a secure channel equals secure key exchange. It exchanges an AES key under the protection of RSA public-key crypto.

To enable secure key exchange, a public-key cryptographic algorithm is essential. Anyone can use a public key to encrypt a message, but only the one with its correspondent private key can decrypt it. As the US patent for the RSA algorithm expired some years ago, it's the most popular public-key crypto algorithm. No wonder the .NET framework supports it. However, you have to understand the key format to exchange a public key between a .NET application and a C++ application as there's no agreement on which format to use. In the example code I'll show you the most straightforward way to exchange an RSA public key, which in fact doesn't encode it in a certain format and passes it in its raw form. Subsequent messages after key exchange are encrypted by the AES symmetric cipher because public-key encryption eats relatively large CPU resources. Symmetric cipher (dubbed "block cipher" due to the encryption method) has various operation modes and the one that uses an IV is more secure than the most basic mode.

Let's take a look at the C# code for a console application RSATestCSharp.exe. When it's executed without an argument, it shows the usage help. Executing it with the "gen" argument does the step 1 in the explanation above. Giving the "dec" argument results in the step 3. You have to manually copy ciphertext files between the C# program and the C++ program.


using System;
using System.Security.Cryptography;
using System.IO;
using System.Text;

namespace RSATestCSharp
{
/// <summary>
/// Summary description for Class1.
/// </summary>
class Class1
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: Add code to start application here
//

if (args.Length == 0)
{
Console.WriteLine(@"Usage:
To generate an RSA private/public key pair: type ""RSATestCSharp gen""
To decrypt an RSA ciphertext: type ""RSATestCSharp dec""
");
return;
}

try
{
RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(1024);

if (args[0] == "gen")
{
RSAParameters rp = RSA.ExportParameters(false);
Console.WriteLine("PubKey Modulus size: {0:D}", rp.Modulus.Length);
Console.WriteLine("PubKey Exponent size: {0:D}", rp.Exponent.Length);

// exports a modulus and a public exponent
using (FileStream fs = new FileStream("pubkey.txt", FileMode.Create))
{
BinaryWriter sw = new BinaryWriter(fs);
sw.Write(rp.Modulus);
sw.Write(rp.Exponent);
}

// exports a modulus and a private exponent, in XML
using (StreamWriter sw = new StreamWriter("prvkey.txt"))
{
sw.Write(RSA.ToXmlString(true));
}

Console.WriteLine("Key generation is done.");
return;
}

using (StreamReader sr = new StreamReader("prvkey.txt"))
{
string line;
string x = "";
while ((line = sr.ReadLine()) != null)
{
x += line;
}
try
{
RSA.FromXmlString(x);
}
catch (CryptographicException e)
{
Console.WriteLine(e.ToString());
}
}

byte[] enc;
using (FileStream fs = new FileStream("cipher.txt", FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[8192];
using (MemoryStream ms = new MemoryStream())
{
while (true)
{
int read = fs.Read(buffer, 0, buffer.Length);
if (read <= 0)
{
enc = ms.ToArray();
break;
}
ms.Write (buffer, 0, read);
}
}
}

Console.WriteLine("RSA ciphertext size: {0:D} bytes", enc.Length);

RSA.ImportParameters(RSA.ExportParameters(true));
byte[] dec = RSA.Decrypt(enc, true);

if (dec.Length != 32)
{
Console.WriteLine("Invalid plaintext size");
return;
}

// this plaintext is the key + the IV
Console.WriteLine("Plaintext size: {0:D} bytes", dec.Length);

RijndaelManaged myRijndael = new RijndaelManaged();

myRijndael.KeySize = 128;

Console.WriteLine("Rijndael KeySize: {0:D} bits", myRijndael.KeySize);
Console.WriteLine("Rijndael BlockSize: {0:D} bits", myRijndael.BlockSize);
Console.WriteLine("Rijndael Padding: {0}", myRijndael.Padding.ToString());
Console.WriteLine("Rijndael Mode: {0}", myRijndael.Mode.ToString());

byte[] key = new byte[16];
byte[] IV = new byte[16];
Array.Copy(dec, 0, key, 0, 16);
Array.Copy(dec, 16, IV, 0, 16);

ICryptoTransform encryptor = myRijndael.CreateEncryptor(key, IV);

MemoryStream msEnc = new MemoryStream();
CryptoStream cs = new CryptoStream(msEnc, encryptor, CryptoStreamMode.Write);

byte[] plaintext = {(byte)'t', (byte)'e', (byte)'s', (byte)'t'};
Console.WriteLine("Rijndael original size: {0:D} bytes", plaintext.Length);

cs.Write(plaintext, 0, plaintext.Length);
cs.FlushFinalBlock();

byte[] ciphertext = msEnc.ToArray();
Console.WriteLine("Rijndael encrypted Size: {0:D} bytes", ciphertext.Length);

using (FileStream fs = new FileStream("aescipher.txt", FileMode.Create))
{
BinaryWriter sw = new BinaryWriter(fs);
sw.Write(ciphertext);
}

Console.WriteLine("Done");
}
catch(Exception e)
{
Console.WriteLine("{0}: {1}", e.StackTrace, e.Message);
}
}
}
}


An RSA public key is composed of 2 parts, a modulus and a public exponent. A modulus is a product of 2 random prime numbers. For 1024-bit RSA, its modulus becomes 1024-bit (128 bytes). A public exponent is an arbitrary integer. To pass a public key to the C++ program, this program exports a modulus and a public key in a binary format. A private key can be easily exported to the XML format by using the ToXmlString method of the RSACryptoServiceProvider class. When decrypting a message encrypted by a public key it deserializes a private key from an XML file by FromXmlString.

The real name of the algorithm used in the AES symmetric cipher is called Rijndael. In .NET Framework 1.1 only a Managed implementation of the algorithm is provided and the performance may suffer compared to a native implementation, but it won't matter for most use cases. When you execute it with the "dec" argument after it received a cipher text, you'll see the default padding method for the Rijndael implementation in .NET is PKCS7 as shown by myRijndael.Padding.ToString(), and the symmetric cipher mode is CBC (cipher-block chaining) as shown by myRijndael.Mode.ToString(). The CBC mode uses an IV. The C++ program has to use PKCS#7 and CBC to communicate with the C# program. RSA also does padding for a message block and in this case the method called OAEP (Optimal Asymmetric Encryption Padding) is used. The second parameter of RSA.Decrypt is set true to make it aware of OAEP. As the C++ program chooses a 128-bit block for AES, it assumes the size of key and IV are both 16 bytes. The plaintext message is 4-bytes length and has no terminating null character.

The code for the C++ side (RSATestCPP) is as follows. When it's called with the "rsa" it executes the step 2 in the flow, and with "aes" the step 4. You have to build Crypto++ Library and put its include path and library path in your project to build this code.


#include <string>

#include <iostream>
#include <fstream>

// CryptoPP
#include "sha.h"
#include "rsa.h"
#include "hex.h"
#include "osrng.h"
#include "secblock.h"
#include "aes.h"
#include "modes.h"

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
if (argc == 1)
{
cout << "Usage:" << endl
<< "To encrypt an RSA ciphertext: type \"RSATestCPP rsa\"" << endl
<< "To decrypt an AES ciphertext: type \"RSATestCPP aes\"" << endl;
return 0;
}

if (argc != 2)
{
cout << "Unknown option" << endl;
return 0;
}

if (string(argv[1]) == "rsa")
{
string strPublicKey;
ifstream fi("pubkey.txt", ios::in | ios::binary);
if (!fi)
{
cout << "Can't open pubkey.txt" << endl;
return 0;
}

char buf[8192];
while (fi)
{
fi.read(buf, 8192);
strPublicKey.append(buf, fi.gcount());
}

if (strPublicKey.size() <= 128)
{
cout << "Invalid public key size" << endl;
return 1;
}

CryptoPP::RSAES_OAEP_SHA_Encryptor pub;

CryptoPP::Integer nModulus((const byte*)(strPublicKey.data()), 128);
CryptoPP::RSAFunction&amp; r = pub.AccessKey();
r.SetModulus(nModulus);

CryptoPP::Integer nExponent((const byte*)(strPublicKey.data()) + 128, strPublicKey.size() - 128);
r.SetPublicExponent(nExponent);

byte key[16];
byte IV[16];

CryptoPP::OS_GenerateRandomBlock(false, key, 16);
CryptoPP::OS_GenerateRandomBlock(false, IV, 16);

string strPlainText((char*)key, 16);
strPlainText.append((char*)IV, 16);

CryptoPP::AutoSeededRandomPool randPool;
string strCipherText;
CryptoPP::StringSource(
strPlainText,
true,
new CryptoPP::PK_EncryptorFilter(
randPool,
pub,
new CryptoPP::StringSink(strCipherText)
)
);

ofstream out("cipher.txt", ios::out | ios::binary);
if (!out)
{
cout << "Can't open cipher.txt" << endl;
return 0;
}
out.write(strCipherText.data(), strCipherText.size());

ofstream out2("aeskey.txt", ios::out | ios::binary);
if (!out2)
{
cout << "Can't open aeskey.txt" << endl;
return 0;
}
out2.write(strPlainText.data(), strPlainText.size());

cout << "Done" << endl;

return 0;
}

string strAES;
ifstream fi("aescipher.txt", ios::in | ios::binary);
if (!fi)
{
cout << "Can't open aescipher.txt" << endl;
return 0;
}

char buf[8192];
while (fi)
{
fi.read(buf, 8192);
strAES.append(buf, fi.gcount());
}

string strTmp;
ifstream fi2("aeskey.txt", ios::in | ios::binary);
if (!fi2)
{
cout << "Can't open aeskey.txt" << endl;
return 0;
}
while (fi2)
{
fi2.read(buf, 8192);
strTmp.append(buf, fi2.gcount());
}

if (strTmp.size() != 16 + 16)
{
cout << "Invalid key/IV size" << endl;
return 1;
}

byte pDst[16];

CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption d;
d.SetKeyWithIV((const byte*)strTmp.data(), 16, (const byte*)(strTmp.data() + 16));
d.ProcessData((byte*)pDst, (const byte*)(strAES.data()), strAES.size());

// Stripping PKCS#7 padding bytes
unsigned int dwDstSize = strAES.size() - (unsigned int)(pDst[strAES.size() - 1]);

if (dwDstSize >= 16)
{
cout << "Invalid plaintext size: " << dwDstSize << " bytes" << endl;
return 1;
}

string strPlainText((char*)pDst, dwDstSize);

cout << "plaintext: " << strPlainText << " (" << dwDstSize << " bytes)" << endl;

return 0;
}


Conveniently Crypto++ Library predefines CryptoPP::RSAES_OAEP_SHA_Encryptor as one of the RSA encryption schemes in PKCS #1 v2.0. It can interact with the C# counterpart with OAEP without a hassle. CryptoPP::Integer is a CryptoPP class that can hold an integer with arbitrary figures. Cryptography often uses these big integers such as a public key modulus for RSA which is 1024-bit (128 bytes) in this sample code. It imports a modulus and an exponent from the binary file and sets them in the properties of the RSA encryptor object that is an instance of the CryptoPP::RSAES_OAEP_SHA_Encryptor class. The usage of StringSource and StringSink in Crypto++ may be a bit complicated for those who are not familiar with the crypto library, but the basic idea is you set source data in StringSource with a transformation filter that has a destination data in its own parameter. A destination data for a filter must be wrapped with StringSink. These StringSource / StringSink classes can handle the standard C++ classes such as basic_string fairly well.

For an AES key and an IV, it sets 128-bit (16 bytes) random data in them. Each message block for 128-bit AES is 128-bit too. The last block in a message that is smaller than 16 bytes needs padding. After setting a key and IV by SetKeyWithIV the AES decryptor decrypts the ciphertext with ProcessData. As you know the plaintext is 4-bytes length, so this block has paddings. In PKCS#7, if actual padding is 3 bytes the last 3 bytes are filled with 0x03. This sample code manually strips PKCS#7 padding. After the padding is removed, the plaintext will be recovered.

コメント