简体   繁体   中英

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

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

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, ie 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, ie 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).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM