Producing an AES Cipher To Match OpenSSL in C#

--

I received a strange question over the weekend:

“Why is my code always producing the same cipher, no matter the passphrase and salt?” I use C#, and it is not producing the same output as OpenSSL.

Well, the solution was clear. It is all about how OpenSSL does its formating and key generation. The ciphertext was actually changing, but the first part of it was staying the same.

Let’s encrypt some plaintext with 256-bit CBC with a salt value (in hex) and a passphrase. This then generate the required 256-bit key and IV (Initialisation Vector). In OpenSSL we use the EVP method to generate the key and IV:

First we take text of “Hello” and of “hello”, and a passphrase of “qwerty” and a hex salt value of “241fa86763b85341”:

$ echo -n Hello | openssl enc -aes-256-cbc  -pass pass:"qwerty" -e -base64 -S 241fa86763b85341
U2FsdGVkX18kH6hnY7hTQZAGxV2faF01w6uhO+X6+9Q=
$ echo -n hello | openssl enc -aes-256-cbc -pass pass:"qwerty" -e -base64 -S 241fa86763b85341
U2FsdGVkX18kH6hnY7hTQQvZbphR8WXJbOkwYSCKfY0=

Let’s examine this. When we have an output of “U2FsdGVkX18kH6hnY7hTQZ … F01w6uhO+X6+9Q=”. The hex output is:

53616C7465645F5F 241FA86763B85341 9006C55D9F685D35C3ABA13BE5FAFBD4

Then the first part of it is “Salted__$” and which is “U2FsdGVkX18k” in Base64". This then followed by the salt value, and then the cipher. Thus when we are creating our cipher output, we need to add the “Salted__$” string and then the hex value of the salt, and then the cipher.

Let’s now create some code to do this. A sample run proves these [here]:

Text:       Hello
Pass phrase: qwerty
------------------------
IV: 6BE952EBC17EED10411EAA9892F19124
Key: 33A5820536F9EEB709D88AF3B40FDBB100C04327C71B5ACCF48424C8EB40C3F9
------------------------
Cipher: U2FsdGVkX18kH6hnY7hTQZAGxV2faF01w6uhO+X6+9Q=
Decrypt: Hello

and:

Text:       hello
Pass phrase: qwerty
------------------------
IV: 6BE952EBC17EED10411EAA9892F19124
Key: 33A5820536F9EEB709D88AF3B40FDBB100C04327C71B5ACCF48424C8EB40C3F9
------------------------
Cipher: U2FsdGVkX18kH6hnY7hTQQvZbphR8WXJbOkwYSCKfY0=
Decrypt: hello

The code used is [here]:

using System.Security.Cryptography;
using System.IO;
using System;
using System.Collections.Generic;
using System.Text;
// Code derived from https://stackoverflow.com/questions/5452422/openssl-encryption-using-net-classes/5454692
namespace Aes
{
class Program
{
static void Main(string[] args)
{
string cipher,dec;
string text,passphrase,salt;

text="Hello";
passphrase="qwerty";
salt="241fa86763b85341";

if (args.Length>0) text = args[0];
if (args.Length>1) passphrase = args[1];
if (args.Length>2) salt = args[2];
Console.WriteLine("Text:\t"+text);
Console.WriteLine("Pass phrase:\t"+passphrase);
Console.WriteLine("------------------------\n");
cipher=OpenSSLEncrypt(text,passphrase,salt);
dec=OpenSSLDecrypt(cipher,passphrase,salt);
Console.WriteLine("------------------------\n");
Console.WriteLine("Cipher:\t"+cipher);
Console.WriteLine("Decrypt:\t"+dec);
}

public static string OpenSSLEncrypt(string plainText, string passphrase, string s)
{
// generate salt
byte[] key, iv;
byte[] salt = new byte[8];
salt=StringToByteArray(s);
DeriveKeyAndIV(passphrase, salt, out key, out iv);
// encrypt bytes
Console.WriteLine("IV:"+ByteArrayToHexString(iv));
Console.WriteLine("Key: "+ByteArrayToHexString(key));
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 static string OpenSSLDecrypt(string encrypted, string passphrase, string s)
{
// base 64 decode
byte[] encryptedBytesWithSalt = Convert.FromBase64String(encrypted);
// extract salt (first 8 bytes of encrypted)
byte[] salt = new byte[8];
salt=StringToByteArray(s);
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 concatenatedHashes = new List(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;
}

public static byte[] StringToByteArray(String hex)
{
int NumberChars = hex.Length;
byte[] bytes = new byte[NumberChars / 2];
for (int i = 0; i < NumberChars; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
public static string ByteArrayToHexString(byte[] Bytes)
{
StringBuilder Result = new StringBuilder(Bytes.Length * 2);
string HexAlphabet = "0123456789ABCDEF";
foreach (byte B in Bytes)
{
Result.Append(HexAlphabet[(int)(B >> 4)]);
Result.Append(HexAlphabet[(int)(B & 0xF)]);
}
return Result.ToString();
}

}
}

Conclusions

OpenSSL is the benchmark for crypto, and it’s important that you test your code against it.

--

--

Prof Bill Buchanan OBE FRSE
ASecuritySite: When Bob Met Alice

Professor of Cryptography. Serial innovator. Believer in fairness, justice & freedom. Based in Edinburgh. Old World Breaker. New World Creator. Building trust.