When cryptography is outlawed, bayl bhgynjf jvyy unir cevinpl.
Kevin McCurley
Asymmetric Encryption is a cryptographic system that uses two keys - a public key known to everyone and a private or secret key known only to the recipient of the message. When Alice wants to send a secure message to Bob, she uses Bob's public key to encrypt the message. Bob then uses his private key to decrypt it.
An important element to the public key system is that the public and private keys are related in such a way that only the public key can be used to encrypt messages and only the corresponding private key can be used to decrypt them. Moreover, it is virtually impossible to deduce the private key even if you know the public key.
For an overview on what encryption is I heartily recommend you read a primer. It's a broad topic and to cover it in depth would be way out of scope of this series.
Getting Started
We will be using the RSA Crypto Service Provider to demonstrate encryption with a public key and decryption using the related private key.
Generating Keys
The first step to using RSA encryption is to actually create a keypair. We can do this using the Strong Name Tool which ships with the .Net SDK. We will generate a key pair file and then extract the public key.
sn -k keypair.snk
sn -p keypair.snk public.snk
Common Methods
Encrypting or Decrypting with RSA have a few commonalities. Whichever we choose we start by creating an RSACryptoServiceProvider and configure it with the parameters of our RSA keys. To make this easier we can create some common functions that will load an RSACryptoServiceProvider from the SNK files that we created earlier.
This is a lot of boilerplate code. Click to toggle view.
/// <summary>Index of the private key header</summary>
private const int magic_priv_idx = 0x08;
/// <summary>Index of the public key header</summary>
private const int magic_pub_idx = 0x14;
/// <summary>Header size</summary>
private const int magic_size = 4;
public static RSACryptoServiceProvider GetRSAFromSnkFile(string path)
{
if (path == null)
throw new ArgumentNullException("path");
byte[] snkBytes = GetFileBytes(path);
if (snkBytes == null)
throw new ArgumentException("Invalid SNK file.");
RSACryptoServiceProvider rsa = GetRSAFromSnkBytes(snkBytes);
return rsa;
}
public static RSACryptoServiceProvider
GetPublicKeyFromAssembly(Assembly assembly)
{
if (assembly == null)
throw new ArgumentNullException("assembly",
"Assembly may not be null");
byte[] pubkey = assembly.GetName().GetPublicKey();
if (pubkey.Length == 0)
throw new ArgumentException("No public key in assembly.");
RSAParameters rsaParams = EncryptionUtils.GetRSAParameters(pubkey);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParams);
return rsa;
}
/// <summary>
/// Returns RSAParameters from byte[]. Example to get rsa public key from
/// assembly:
/// byte[] pubkey = Assembly.GetExecutingAssembly().GetName().GetPublicKey();
/// RSAParameters p = SnkUtil.GetRSAParameters(pubkey);
/// </summary>
/// <param name="keypair">keypair loaded as a byte array</param>
/// <returns>Parameters for initialising an RSA Crypto Provider with
/// the passed key information</returns>
public static RSAParameters GetRSAParameters(byte[] keyBytes)
{
RSAParameters ret = new RSAParameters();
if ((keyBytes == null) || (keyBytes.Length < 1))
throw new ArgumentNullException("keyBytes");
bool pubonly = SnkBufIsPubLength(keyBytes);
if ((pubonly) && (!CheckRSA1(keyBytes)))
return ret;
if ((!pubonly) && (!CheckRSA2(keyBytes)))
return ret;
int magic_idx = pubonly ? magic_pub_idx : magic_priv_idx;
// Bitlen is stored here, but note this class is
// only set up for 1024 bit length keys
int bitlen_idx = magic_idx + magic_size;
int bitlen_size = 4; // DWORD
// Exponent, In read file, will usually be { 1, 0, 1, 0 } or 65537
int exp_idx = bitlen_idx + bitlen_size;
int exp_size = 4;
//BYTE modulus[rsapubkey.bitlen/8]; == MOD; Size 128
int mod_idx = exp_idx + exp_size;
int mod_size = 128;
//BYTE prime1[rsapubkey.bitlen/16]; == P; Size 64
int p_idx = mod_idx + mod_size;
int p_size = 64;
//BYTE prime2[rsapubkey.bitlen/16]; == Q; Size 64
int q_idx = p_idx + p_size;
int q_size = 64;
//BYTE exponent1[rsapubkey.bitlen/16]; == DP; Size 64
int dp_idx = q_idx + q_size;
int dp_size = 64;
//BYTE exponent2[rsapubkey.bitlen/16]; == DQ; Size 64
int dq_idx = dp_idx + dp_size;
int dq_size = 64;
//BYTE coefficient[rsapubkey.bitlen/16]; == InverseQ; Size 64
int invq_idx = dq_idx + dq_size;
int invq_size = 64;
//BYTE privateExponent[rsapubkey.bitlen/8]; == D; Size 128
int d_idx = invq_idx + invq_size;
int d_size = 128;
// Figure public params, Must reverse order (little vs. big endian)
ret.Exponent = BlockCopy(keyBytes, exp_idx, exp_size);
Array.Reverse(ret.Exponent);
ret.Modulus = BlockCopy(keyBytes, mod_idx, mod_size);
Array.Reverse(ret.Modulus);
if (pubonly) return ret;
// Figure private params
// Must reverse order (little vs. big endian issue)
ret.P = BlockCopy(keyBytes, p_idx, p_size);
Array.Reverse(ret.P);
ret.Q = BlockCopy(keyBytes, q_idx, q_size);
Array.Reverse(ret.Q);
ret.DP = BlockCopy(keyBytes, dp_idx, dp_size);
Array.Reverse(ret.DP);
ret.DQ = BlockCopy(keyBytes, dq_idx, dq_size);
Array.Reverse(ret.DQ);
ret.InverseQ = BlockCopy(keyBytes, invq_idx, invq_size);
Array.Reverse(ret.InverseQ);
ret.D = BlockCopy(keyBytes, d_idx, d_size);
Array.Reverse(ret.D);
return ret;
}
public static byte[] BlockCopy(byte[] source, int startAt, int size)
{
if ((source == null) || (source.Length < (startAt + size)))
return null;
byte[] ret = new byte[size];
Buffer.BlockCopy(source, startAt, ret, 0, size);
return ret;
}
private static byte[] GetFileBytes(string path)
{
using (FileStream fs = new FileStream(path, FileMode.Open,
FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs)) {
byte[] bytes = br.ReadBytes((int)fs.Length);
return bytes;
}
}
/// <summary>
/// Reads a file as an array of bytes
/// </summary>
/// <param name="path">Path to the file to read</param>
/// <returns>File contents as an array of bytes</returns>
private static byte[] GetFileBytes(string path)
{
using (FileStream fs = new FileStream(path, FileMode.Open,
FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs)) {
byte[] bytes = br.ReadBytes((int)fs.Length);
return bytes;
}
}
/// <summary>
/// Creates an RSA Provider using the passed keypair byte array
/// </summary>
/// <param name="snkBytes">Keypair as byte array</param>
/// <returns>RSA Provider initialised with the passed keypair</returns>
private static RSACryptoServiceProvider GetRSAFromSnkBytes(byte[] snkBytes)
{
if (snkBytes == null)
throw new ArgumentNullException("snkBytes");
RSAParameters param = GetRSAParameters(snkBytes);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(param);
return rsa;
}
#region Snk Buffer Is Public Length
/// <summary>
/// Returns true if buffer length is public key size.
/// </summary>
/// <param name="keypair">Keypair as bytes</param>
/// <returns>True if the buffer length is public key size,
/// otherwise false</returns>
private static bool SnkBufIsPubLength(byte[] keypair)
{
if (keypair == null)
return false;
return (keypair.Length == 160);
}
/// <summary>
/// Check that RSA1 is in header (public key only).
/// </summary>
/// <param name="keypair">Keypair to check</param>
/// <returns>True if the header contains RSA1, otherwise false</returns>
private static bool CheckRSA1(byte[] pubkey)
{
// Check that RSA1 is in header.
// R S A 1
byte[] check = new byte[] { 0x52, 0x53, 0x41, 0x31 };
return CheckMagic(pubkey, check, magic_pub_idx);
}
/// <summary>
/// Check that RSA2 is in header (public and private key).
/// </summary>
/// <param name="keypair">Keypair to check</param>
/// <returns>True if RSA2 is in the header,
/// otherwise, false</returns>
private static bool CheckRSA2(byte[] pubkey)
{
// Check that RSA2 is in header.
// R S A 2
byte[] check = new byte[] { 0x52, 0x53, 0x41, 0x32 };
return CheckMagic(pubkey, check, magic_priv_idx);
}
/// <summary>
/// Checks the "Magic" bytes that form the header for RSA definition
/// </summary>
/// <param name="keypair">Keypair to check</param>
/// <param name="check">Bytes that we are trying to match</param>
/// <param name="idx">Index of the header</param>
/// <returns>True if the keypair contains the check bytes,
/// otherwise false</returns>
private static bool CheckMagic(byte[] keypair, byte[] check, int idx)
{
byte[] magic = BlockCopy(keypair, idx, magic_size);
if (magic == null)
return false;
for (int i = 0; i < magic_size; i++) {
if (check[i] != magic[i])
return false;
}
return true;
}
Encrypting
Encrypting involves creating an RSA Crypto Service provider from a public key source, then streaaming data through a cipher into a memory stream. We ensure that we work in chunks that match our block size otherwise our data would get junked. In our example we pull the public key from a signed assembly but you can easily use GetRSAFromSnkFile instead.
public static byte[] AsymmetricEncrypt(Assembly assembly,
byte[] dataToEncrypt)
{
if (assembly == null || dataToEncrypt == null)
return null;
//Load the keys from the assembly
RSACryptoServiceProvider cipher =
EncryptionUtils.GetPublicKeyFromAssembly(assembly);
//Prepare variables used in encryption
int blockSize = EncryptionUtils.BLOCK_SIZE,
index = 0,
bytesLeft = dataToEncrypt.Length,
totalBytes = 0;
//Ensure that the total bytes is a multiple of 128, the final
// encrypted content block will be padded to this length in bytes
if ((bytesLeft * 2) % 128 != 0)
totalBytes = (bytesLeft * 2) + (128 - ((bytesLeft * 2) % 128));
else
totalBytes = bytesLeft * 2;
//Create the buffer for encrypted data
using (MemoryStream memEncryptedTextbuffer =
new MemoryStream(totalBytes))
{
byte[] block = null;
byte[] encryptedBlock = null;
//Split the serialized data into smaller blocks for encryption
while (bytesLeft > 0) {
//If the blocksize is too large, set to the required amount
if (bytesLeft < blockSize)
blockSize = bytesLeft;
//Get a block and encrypt it
block = EncryptionUtils.BlockCopy(dataToEncrypt,
index, blockSize);
encryptedBlock = cipher.Encrypt(block, false);
memEncryptedTextbuffer.Write(encryptedBlock, 0,
encryptedBlock.Length);
//Update position and size tracking
index += blockSize;
bytesLeft -= blockSize;
block = null;
encryptedBlock = null;
}
cipher.Clear();
//Dump the encrypted data to the caller
memEncryptedTextbuffer.Position = 0;
return memEncryptedTextbuffer.ToArray();
}
}
Decryption
Decryption is essentially the encryption routine in reverse. This time we load the private keyfile and reverse the process we did for encryption.
public static byte[] AsymmetricDecrypt(byte[] encryptedData,
byte[] snkFileContent)
{
RSACryptoServiceProvider cipher =
EncryptionUtils.GetRSAFromSnkBytes(snkFileContent);
return DoAsymmetricDecrypt(cipher, encryptedData);
}
private static byte[] DoAsymmetricDecrypt(
RSACryptoServiceProvider cipher,
byte[] encryptedData)
{
//Prepare block detail
int blockSize = EncryptionUtils.BLOCK_SIZE * 2,
index = 0,
bytesLeft = encryptedData.Length;
using (MemoryStream memDecryptedTextbuffer =
new MemoryStream(bytesLeft / 2))
{
byte[] block = null;
byte[] decryptedBlock = null;
//Split the serialized data into smaller blocks for processing
while (bytesLeft > 0) {
//If the blocksize is too large, set to the required amount
if (bytesLeft < blockSize)
blockSize = bytesLeft;
//Get a block from the encrypted data
block = EncryptionUtils.BlockCopy(encryptedData,
index, blockSize);
decryptedBlock = cipher.Decrypt(block, false);
memDecryptedTextbuffer.Write(decryptedBlock, 0,
decryptedBlock.Length);
//Update position and size tracking
index += blockSize;
bytesLeft -= blockSize;
block = null;
decryptedBlock = null;
}
cipher.Clear();
//Dump the encrypted data to the caller
memDecryptedTextbuffer.Position = 0;
return memDecryptedTextbuffer.ToArray();
}
}
Putting it all together
You can aquire a full library of the encryption functions that we have discussed from our Open Source Archive.
Other Posts
Symmetric Encryption with AES
C# ,
Encryption
Comments are Locked for this Post