解密用 PHP openssl_encrypt 加密的 C# 中的字符串

Posted

技术标签:

【中文标题】解密用 PHP openssl_encrypt 加密的 C# 中的字符串【英文标题】:Decrypt string in C# that was encrypted with PHP openssl_encrypt 【发布时间】:2013-11-12 05:06:47 【问题描述】:

我有一个客户使用以下代码在 php 中加密一个字符串:

    $password = 'Ty63rs4aVqcnh2vUqRJTbNT26caRZJ';
    $method = 'AES-256-CBC';
    texteACrypter = 'Whether you think you can, or you think you can\'t--you\'re right. - Henry Ford';

    $encrypted = openssl_encrypt($texteACrypter, $method, $password);

导致此加密输出:MzVWX4tH4yZWc/w75zUagUMEsP34ywSYISsIIS9fj0W3Q/lR0hBrHmdvMOt106PlKhN/1zXFBPbyKmI6nWC5BN54GuGFSjkxfuansJkfoi0=

当我尝试在 C# 中解密该字符串时,它给了我一堆像这样的垃圾:Z�o�'*2��I4y�J6S�� ��xz���9^�ED�fF ���گs�)�Q���i��$)�

我尝试更改填充,使用 AesManaged 而不是 RijndaelManaged,更改密钥大小,使用不同的密钥等。所有这些都会导致不同的垃圾字符串或各种异常。我必须在这里遗漏一些非常基本的东西,但我不确定此时还可以尝试什么。

这是我的解密代码(我无耻地从另一个 *** 问题中复制:openssl using only .NET classes)

class Program

    //https://***.com/questions/5452422/openssl-using-only-net-classes
    static void Main(string[] args)
    
        var secret = "Ty63rs4aVqcnh2vUqRJTbNT26caRZJ";
        var encrypted = "MzVWX4tH4yZWc/w75zUagUMEsP34ywSYISsIIS9fj0W3Q/lR0hBrHmdvMOt106PlKhN/1zXFBPbyKmI6nWC5BN54GuGFSjkxfuansJkfoi0=";

        var yeah = OpenSSLDecrypt(encrypted, secret);
        Console.WriteLine(yeah);
        Console.ReadKey();
    

    public static 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();
    

    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;

        // Create a RijndaelManaged object
        // with the specified key and IV.
        aesAlg = new RijndaelManaged  Mode = CipherMode.CBC, Padding = PaddingMode.None, 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();
                
            
        

        return plaintext;
    

【问题讨论】:

【参考方案1】:

这很有趣,需要跳入 PHP 源代码并获得一些有趣的结果。首先,PHP 甚至不使用密钥派生算法 just takes the bytes of the passphrase and pads it out with zero's 到所需的长度。这意味着不需要整个 DeriveKeyAndIV 方法。

由于上述原因,这意味着正在使用的 IV 是一个包含零的 16 字节数组。

最后,您的代码唯一的其他问题是您从中复制它的源在他们的加密实现中使用了盐,然后必须将其删除,PHP 也没有您这样做,因此删除盐字节是不正确的。

所以所有这些放在一起意味着您需要将OpenSSLDecrypt 方法更改为此。

public static string OpenSSLDecrypt(string encrypted, string passphrase)

    //get the key bytes (not sure if UTF8 or ASCII should be used here doesn't matter if no extended chars in passphrase)
    var key = Encoding.UTF8.GetBytes(passphrase);

    //pad key out to 32 bytes (256bits) if its too short
    if (key.Length < 32)
    
        var paddedkey = new byte[32];
        Buffer.BlockCopy(key, 0, paddedkey, 0, key.Length);
        key = paddedkey;
    

    //setup an empty iv
    var iv = new byte[16];

    //get the encrypted data and decrypt
    byte[] encryptedBytes = Convert.FromBase64String(encrypted);
    return DecryptStringFromBytesAes(encryptedBytes, key, iv);

最后,结果字符串的末尾有一些额外的字符,即一组 3 个ETX char,但这些应该很容易过滤掉。我实际上无法弄清楚这些是从哪里来的。

感谢@neubert 指出填充是标准PKCS 填充的一部分,如果您希望框架将其删除,只需在实例化RijndaelManaged 对象时将其指定为填充模式。

new RijndaelManaged  Padding = PaddingMode.PKCS7 ;

【讨论】:

ETX 是 chr(3)。最后出现的 3 个 ETX 字符正是 PKCS 填充将导致的结果:en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 谢谢你。我无法弄清楚为什么 .NET 和 PHP 使用 16 字节密钥获得不同的 AES-256 密文。我(错误地)假设 .NET 和 PHP 会使用相同的 KDF,但从没想过 PHP 只会对密钥进行零填充。 我的数据被部分解密了?如何解决? @AlexandrSargsyan 我会检查您使用的 PHP 版本如何派生密钥,因为这可能已更改。 @DavidEwen 我修好了。问题出在 iv 键中。 PHP 用 iv 加密它。在 C# 中,您必须使用 iv 而不是空字节

以上是关于解密用 PHP openssl_encrypt 加密的 C# 中的字符串的主要内容,如果未能解决你的问题,请参考以下文章

PHP笔记-AES加解密(PHP7)

PHP版DES算法加密数据(3DES)另附openssl_encrypt版本

AES-256-CBC用PHP加密并用Java解密

不能 AES_DECRYPT (MySQL) 使用 openssl_encrypt (PHP) 加密的字符串

PHP如何使用AES加密和解密

openssl_encrypt到底几个参数?