Search code examples
c#.net-coreopensslaes

It is possible to decrypt AES password protected file in C# / dotNet 6 encrypted by openssl enc -k?


I need to decrypt a file coming from an linux box, password protected with Openssl and AES. The encryption is done with

openssl enc -aes-256-cbc -k <pwd>

Currently, I get it properly decrypted with the following script on Windows:

"openssl.exe" enc -d -aes-256-cbc -k <pwd> -in <inputFile> -out <output>

So far, I included the openssl exe and 2 dll with my project to do so.

However, I would like to get rid of those dependencies and decode it directly in C#.

What is the C# equivalent of the openssl enc -d as above?

It is anyway possible? I read from https://security.stackexchange.com/questions/20628/where-is-the-salt-on-the-openssl-aes-encryption that openssl enc is kind of non standard and is using a random salt from the given password.

Inspired by a few other similar topics, my current method always get the "padding invalid" issue as e.g. this other question AES-256-CBC Decrypt Error Stating Padding is invalid and cannot be removed

This 10-years old thread OpenSSL encryption using .NET classes proposed a solution, even more complex to retrieve the salt and IV, but this is not working anymore. I also get the "padding invalid" issue.

(original code with Rfc2898DeriveBytes object for the pwd removed, openssl does not use this Rfc2898DeriveBytes stuff). See working code in the accepted answer.


Solution

  • The code from the 10 year old question you linked actully still works with minor modifications. First note that by default OpenSSL now uses SHA256 as a hash function and not MD5, we can easily fix that. Then, that answer assumes you provide "-base64" option to openssl and get result in base64 and not strange format used by OpenSSL by default, but that's also easy to fix. Just read target file as bytes, then strip ascii-encoded "SALTED__" string from its beginning:

    var input = File.ReadAllBytes(@"your encrypted file");
    input = input.Skip(Encoding.ASCII.GetBytes("SALTED__").Length).ToArray();
    

    Now adjust how it extracts salt and encrypted data from there, and use PKCS7 padding, and it'll work. Full code copied from the answer above with mentioned fixes:

    public class Protection
    {
        public string OpenSSLDecrypt(byte[] encryptedBytesWithSalt, string passphrase)
        {
            // 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];
            var md5 = SHA256.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 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, Padding = PaddingMode.PKCS7};
    
                // 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;
        }
    }
    

    Then just:

    var input = File.ReadAllBytes(@"path to your encrypted file");
    input = input.Skip(Encoding.ASCII.GetBytes("SALTED__").Length).ToArray();
    var decrypted= new Protection().OpenSSLDecrypt(input, "123123");
    

    If you decrypt non-string data, change DecryptStringFromBytesAes like that:

    static byte[] 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;
    
        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, Padding = PaddingMode.PKCS7 };
    
            // 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 (var output = new MemoryStream()) {
                        csDecrypt.CopyTo(output);
                        return output.ToArray();
                    }
                }
            }
        }
        finally {
            // Clear the RijndaelManaged object.
            if (aesAlg != null)
                aesAlg.Clear();
        }
    }