I'm looking to create a class that uses the .NET libraries that is compatible with OpenSSL. I'm aware there is an OpenSSL.Net wrapper, but I would prefer to avoid referencing 3rd party\unmanaged code. I'm not looking for a discussion of whether this is the right choice, but there are reasons for it.
Currently I have the following, which I believe should be compatible with OpenSSL - it effectively does what I believe OpenSSL does from the OpenSSL documentation. However even when just using this class to do both the encryption and decryption, I'm getting the following error:
[CryptographicException] Padding is invalid and cannot be removed.
I have stepped through the code and verified that the salt\key\iv are all the same during the encryption and decryption process.
See below for sample class and call to do encrypt decrypt. Any ideas or pointers would be welcome.
public class Protection
{
public string OpenSSLEncrypt(string plainText, string passphrase)
{
// generate salt
byte[] key, iv;
byte[] salt = new byte[8];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetNonZeroBytes(salt);
DeriveKeyAndIV(passphrase, salt, out key, out iv);
// encrypt bytes
byte[] encryptedBytes = EncryptStringToBytesAes(plainText, key, iv);
// add salt as first 8 bytes
byte[] encryptedBytesWithSalt = new byte[salt.Length + encryptedBytes.Length];
Buffer.BlockCopy(salt, 0, encryptedBytesWithSalt, 0, salt.Length);
Buffer.BlockCopy(encryptedBytes, 0, encryptedBytesWithSalt, salt.Length, encryptedBytes.Length);
// base64 encode
return Convert.ToBase64String(encryptedBytesWithSalt);
}
public string OpenSSLDecrypt(string encrypted, string passphrase)
{
// base 64 decode
byte[] encryptedBytesWithSalt = Convert.FromBase64String(encrypted);
// extract salt (first 8 bytes of encrypted)
byte[] salt = new byte[8];
byte[] encryptedBytes = new byte[encryptedBytesWithSalt.Length - salt.Length];
Buffer.BlockCopy(encryptedBytesWithSalt, 0, salt, 0, salt.Length);
Buffer.BlockCopy(encryptedBytesWithSalt, salt.Length, encryptedBytes, 0, encryptedBytes.Length);
// get key and iv
byte[] key, iv;
DeriveKeyAndIV(passphrase, salt, out key, out iv);
return DecryptStringFromBytesAes(encryptedBytes, key, iv);
}
private static void DeriveKeyAndIV(string passphrase, byte[] salt, out byte[] key, out byte[] iv)
{
// generate key and iv
List<byte> concatenatedHashes = new List<byte>(48);
byte[] password = Encoding.UTF8.GetBytes(passphrase);
byte[] currentHash = new byte[0];
MD5 md5 = MD5.Create();
bool enoughBytesForKey = false;
// See http://www.openssl.org/docs/crypto/EVP_BytesToKey.html#KEY_DERIVATION_ALGORITHM
while (!enoughBytesForKey)
{
int preHashLength = currentHash.Length + password.Length + salt.Length;
byte[] preHash = new byte[preHashLength];
Buffer.BlockCopy(currentHash, 0, preHash, 0, currentHash.Length);
Buffer.BlockCopy(password, 0, preHash, currentHash.Length, password.Length);
Buffer.BlockCopy(salt, 0, preHash, currentHash.Length + password.Length, salt.Length);
currentHash = md5.ComputeHash(preHash);
concatenatedHashes.AddRange(currentHash);
if (concatenatedHashes.Count >= 48)
enoughBytesForKey = true;
}
key = new byte[32];
iv = new byte[16];
concatenatedHashes.CopyTo(0, key, 0, 32);
concatenatedHashes.CopyTo(32, iv, 0, 16);
md5.Clear();
md5 = null;
}
static byte[] EncryptStringToBytesAes(string plainText, byte[] key, byte[] iv)
{
// Check arguments.
if (plainText == null || plainText.Length <= 0)
throw new ArgumentNullException("plainText");
if (key == null || key.Length <= 0)
throw new ArgumentNullException("key");
if (iv == null || iv.Length <= 0)
throw new ArgumentNullException("iv");
// Declare the stream used to encrypt to an in memory
// array of bytes.
MemoryStream msEncrypt;
// Declare the RijndaelManaged object
// used to encrypt the data.
RijndaelManaged aesAlg = null;
try
{
// Create a RijndaelManaged object
// with the specified key and IV.
aesAlg = new RijndaelManaged { Key = key, IV = iv, Mode = CipherMode.CBC, KeySize = 256, BlockSize = 256 };
// Create an encryptor to perform the stream transform.
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for encryption.
msEncrypt = new MemoryStream();
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
//Write all data to the stream.
swEncrypt.Write(plainText);
swEncrypt.Flush();
swEncrypt.Close();
}
}
}
finally
{
// Clear the RijndaelManaged object.
if (aesAlg != null)
aesAlg.Clear();
}
// Return the encrypted bytes from the memory stream.
return msEncrypt.ToArray();
}
static string DecryptStringFromBytesAes(byte[] cipherText, byte[] key, byte[] iv)
{
// Check arguments.
if (cipherText == null || cipherText.Length <= 0)
throw new ArgumentNullException("cipherText");
if (key == null || key.Length <= 0)
throw new ArgumentNullException("key");
if (iv == null || iv.Length <= 0)
throw new ArgumentNullException("iv");
// Declare the RijndaelManaged object
// used to decrypt the data.
RijndaelManaged aesAlg = null;
// Declare the string used to hold
// the decrypted text.
string plaintext;
try
{
// Create a RijndaelManaged object
// with the specified key and IV.
aesAlg = new RijndaelManaged { Key = key, IV = iv, Mode = CipherMode.CBC, KeySize = 256, BlockSize = 256};
// Create a decrytor to perform the stream transform.
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for decryption.
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
// Read the decrypted bytes from the decrypting stream
// and place them in a string.
plaintext = srDecrypt.ReadToEnd();
srDecrypt.Close();
}
}
}
}
finally
{
// Clear the RijndaelManaged object.
if (aesAlg != null)
aesAlg.Clear();
}
return plaintext;
}
}
I then call this to test it:
Protection protection = new Protection();
const string passphrase = "<passphrase>";
string encrypted = protection.OpenSSLEncrypt(jobid, passphrase);
string decrypted = protection.OpenSSLDecrypt(encrypted, passphrase);
Finally figured this one out. In the event someone needs to integrate openssl and .NET without using the openssl wrappers, I'll share the results here.
1) The main issue with my original code (as in the question) is that you must initialize the BlockSize and KeySize on your RijndaelManaged instance BEFORE setting the key or IV.
2) I also had BlockSize set to 256 when it should only be 128
3) The remainder of my issue came to the fact that openssl puts and expects "Salted__" onto the front of the salt before appending the encrypted string and then base64 encoding it. (I saw this initially in the openssl documentation with respect to file encryption but didn't think it did it when doing it directly through commandline - Apparently I was wrong!! Note also the capitalization of the S in Salted!)
With that all in mind, here is my "fixed" code:
public class Protection
{
public string OpenSSLEncrypt(string plainText, string passphrase)
{
// generate salt
byte[] key, iv;
byte[] salt = new byte[8];
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetNonZeroBytes(salt);
DeriveKeyAndIV(passphrase, salt, out key, out iv);
// encrypt bytes
byte[] encryptedBytes = EncryptStringToBytesAes(plainText, key, iv);
// add salt as first 8 bytes
byte[] encryptedBytesWithSalt = new byte[salt.Length + encryptedBytes.Length + 8];
Buffer.BlockCopy(Encoding.ASCII.GetBytes("Salted__"), 0, encryptedBytesWithSalt, 0, 8);
Buffer.BlockCopy(salt, 0, encryptedBytesWithSalt, 8, salt.Length);
Buffer.BlockCopy(encryptedBytes, 0, encryptedBytesWithSalt, salt.Length + 8, encryptedBytes.Length);
// base64 encode
return Convert.ToBase64String(encryptedBytesWithSalt);
}
public string OpenSSLDecrypt(string encrypted, string passphrase)
{
// base 64 decode
byte[] encryptedBytesWithSalt = Convert.FromBase64String(encrypted);
// extract salt (first 8 bytes of encrypted)
byte[] salt = new byte[8];
byte[] encryptedBytes = new byte[encryptedBytesWithSalt.Length - salt.Length - 8];
Buffer.BlockCopy(encryptedBytesWithSalt, 8, salt, 0, salt.Length);
Buffer.BlockCopy(encryptedBytesWithSalt, salt.Length + 8, encryptedBytes, 0, encryptedBytes.Length);
// get key and iv
byte[] key, iv;
DeriveKeyAndIV(passphrase, salt, out key, out iv);
return DecryptStringFromBytesAes(encryptedBytes, key, iv);
}
private static void DeriveKeyAndIV(string passphrase, byte[] salt, out byte[] key, out byte[] iv)
{
// generate key and iv
List<byte> concatenatedHashes = new List<byte>(48);
byte[] password = Encoding.UTF8.GetBytes(passphrase);
byte[] currentHash = new byte[0];
MD5 md5 = MD5.Create();
bool enoughBytesForKey = false;
// See http://www.openssl.org/docs/crypto/EVP_BytesToKey.html#KEY_DERIVATION_ALGORITHM
while (!enoughBytesForKey)
{
int preHashLength = currentHash.Length + password.Length + salt.Length;
byte[] preHash = new byte[preHashLength];
Buffer.BlockCopy(currentHash, 0, preHash, 0, currentHash.Length);
Buffer.BlockCopy(password, 0, preHash, currentHash.Length, password.Length);
Buffer.BlockCopy(salt, 0, preHash, currentHash.Length + password.Length, salt.Length);
currentHash = md5.ComputeHash(preHash);
concatenatedHashes.AddRange(currentHash);
if (concatenatedHashes.Count >= 48)
enoughBytesForKey = true;
}
key = new byte[32];
iv = new byte[16];
concatenatedHashes.CopyTo(0, key, 0, 32);
concatenatedHashes.CopyTo(32, iv, 0, 16);
md5.Clear();
md5 = null;
}
static byte[] EncryptStringToBytesAes(string plainText, byte[] key, byte[] iv)
{
// Check arguments.
if (plainText == null || plainText.Length <= 0)
throw new ArgumentNullException("plainText");
if (key == null || key.Length <= 0)
throw new ArgumentNullException("key");
if (iv == null || iv.Length <= 0)
throw new ArgumentNullException("iv");
// Declare the stream used to encrypt to an in memory
// array of bytes.
MemoryStream msEncrypt;
// Declare the RijndaelManaged object
// used to encrypt the data.
RijndaelManaged aesAlg = null;
try
{
// Create a RijndaelManaged object
// with the specified key and IV.
aesAlg = new RijndaelManaged { Mode = CipherMode.CBC, KeySize = 256, BlockSize = 128, Key = key, IV = iv };
// Create an encryptor to perform the stream transform.
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for encryption.
msEncrypt = new MemoryStream();
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
//Write all data to the stream.
swEncrypt.Write(plainText);
swEncrypt.Flush();
swEncrypt.Close();
}
}
}
finally
{
// Clear the RijndaelManaged object.
if (aesAlg != null)
aesAlg.Clear();
}
// Return the encrypted bytes from the memory stream.
return msEncrypt.ToArray();
}
static string DecryptStringFromBytesAes(byte[] cipherText, byte[] key, byte[] iv)
{
// Check arguments.
if (cipherText == null || cipherText.Length <= 0)
throw new ArgumentNullException("cipherText");
if (key == null || key.Length <= 0)
throw new ArgumentNullException("key");
if (iv == null || iv.Length <= 0)
throw new ArgumentNullException("iv");
// Declare the RijndaelManaged object
// used to decrypt the data.
RijndaelManaged aesAlg = null;
// Declare the string used to hold
// the decrypted text.
string plaintext;
try
{
// Create a RijndaelManaged object
// with the specified key and IV.
aesAlg = new RijndaelManaged {Mode = CipherMode.CBC, KeySize = 256, BlockSize = 128, Key = key, IV = iv};
// Create a decrytor to perform the stream transform.
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for decryption.
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
// Read the decrypted bytes from the decrypting stream
// and place them in a string.
plaintext = srDecrypt.ReadToEnd();
srDecrypt.Close();
}
}
}
}
finally
{
// Clear the RijndaelManaged object.
if (aesAlg != null)
aesAlg.Clear();
}
return plaintext;
}
}