Home > database >  Implement PHP method openssl_encrypt "AES-256-CBC" OPENSSL_RAW_DATA in C#
Implement PHP method openssl_encrypt "AES-256-CBC" OPENSSL_RAW_DATA in C#

Time:01-18

I'm implementing a third party API that asks me to encrypt the payload of a POST request "as done in this PHP example":

class RESTfulAPI {
        
    function __construct($DomainOrIP, $Key, $Secret) {
        $this->BaseUri = $DomainOrIP ."/api/v2/";
        $this->ApiKey = $Key;
        $this->ApiSecret = $Secret;
        // Encription vector initialization
        $this->SecretIV = substr(hash("SHA256", $this->ApiKey, true), 0, 16);
    }
    
    function base64Url_Encode($data) {
        return rtrim(strtr(base64_encode($data), ' /', '-_'), '=');
    }
    
    // Encription for properties
    function APIEncryptData($Data) {
        $output = openssl_encrypt(
            $Data, 
            "AES-256-CBC", 
            md5($this->ApiSecret),
            OPENSSL_RAW_DATA, 
            $this->SecretIV);
        return $this->base64Url_Encode($output);
    }

}

I can't implement it in PHP, but in C#. It seems very difficult to get the same result in the .NET world.


What I've tried

These two methods are battle tested with PHP equivalence. I tried them many times to compare C# version results and PHP version results and they are the same (I tried the PHP version here)

private static byte[] Sha256(string input)
{
    using (SHA256 sha = SHA256.Create())
    {
        return sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
    }
}

private static byte[] Md5(string input)
{
    using (MD5 md5 = MD5.Create())
    {
        return md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(input));
    }
}

private string Base64UrlEncode(byte[] data)
{
    var base64 = Convert.ToBase64String(data);
    base64 = base64.Replace(" ", "-").Replace("/", "_").TrimEnd('=');
    return base64;
}

The method I can't get to work is the APIEncryptData. In this C# I've tried:

_secretIV = Sha256(Key).Take(16).ToArray();
_hashedApiKey = Md5(Secret);

private string APIEncryptDataV1(string data)
{
    using (var aes = Aes.Create())
    {
        aes.Key = _hashedApiKey;
        aes.IV = _secretIV;
        aes.Mode = CipherMode.CBC;

        using (var encryptor = aes.CreateEncryptor())
        {
            using (var msEncrypt = new MemoryStream())
            {
                using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    using (var swEncrypt = new StreamWriter(csEncrypt))
                    {
                        swEncrypt.Write(data);
                    }
                    return Base64UrlEncode(msEncrypt.ToArray());
                }
            }
        }
    }
}

// Using BouncyCastle
public string APIEncryptDataV2(string data)
{
    var dataBytes = Encoding.UTF8.GetBytes(data);
    
    // Create AES Engine
    AesEngine engine = new AesEngine();

    // Create CBC Mode
    CbcBlockCipher blockCipher = new CbcBlockCipher(engine);

    // Create Padding
    Pkcs7Padding padding = new Pkcs7Padding();

    // Create BufferedBlockCipher
    BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(blockCipher, padding);

    // Create Key Parameter
    KeyParameter keyParam = new KeyParameter(_hashedApiKey);

    // Create IV Parameter
    ParametersWithIV ivParam = new ParametersWithIV(keyParam, _secretIV);

    // Init Cipher
    cipher.Init(true, ivParam);

    // Encrypt Data
    var output = new byte[cipher.GetOutputSize(dataBytes.Length)];
    int outputLength = cipher.ProcessBytes(dataBytes, output, 0);
    cipher.DoFinal(output, outputLength);
    
    return Base64UrlEncode(output);
}

...and many other methods I found here and in other parts of the web. I don't have experience with cryptography algorithms, so I'm in trouble.

What Am I doing wrong?


Sample inputs and outputs:

Key: Q8ZHQ8P5RH5V1RVYS29S3XHDV4PTS7XX
Secret: 527L1MDDQ7WDNDZ13ZHWLNY2D7JV5LXX
Data: Test123

PHP APIEncryptData result: xwMzbdEVqer8Py-c9hapFQ
C# APIEncryptData result: fcklnK82vuNT3DlLJF8h1A
C# APIEncryptDataV2 result: fcklnK82vuNT3DlLJF8h1A

CodePudding user response:

The C# code generates the same ciphertext as the PHP code when _hashedApiKey is derived as follows:

byte[] _hashedApiKey = Encoding.UTF8.GetBytes(Convert.ToHexString(Md5(Secret)).ToLower()); 

The reason is that by default, md5() in the PHP code returns the result as a hexadecimal encoded string in lowercase.

The porting flaw in the C# code is a direct result of the PHP code's key and IV derivation vulnerabilities:

  • A key should be a random byte sequence, i.e. 32 bytes for AES-256. If a hexdecimal encoded 16 bytes value is used for this (which corresponds to 32 hexdigits or 32 bytes), each byte has a reduced value range of only 16 instead of 256 values, which means a reduction in security.
    In addition, some libraries apply lowercase letters, some uppercase letters, which can lead to incompatibility (as in this case).
    Another weakness is the use of a fast hash function like MD5 as key derivation function (KDF), which is made worse by the fact that MD5 itself is considered insecure.
    The correct way is to apply a dedicated key derivation function such as Argon2, scrypt or at least PBKDF2 in conjunction with a random salt, directly generating a key of the required length, i.e. 32 bytes.
  • The use of a static IV is also a vulnerability, since it leads to reuse of key/IV pairs (for a fixed key). Instead, a random IV should be applied for each encryption, which is passed along with the ciphertext to the decrypting side, usually concatenated (note that the IV is not secret).
  • Related