.NET中AES解密无法实现PHP中openssl_decrypt的效果
我帮你排查了C#和PHP代码之间的差异,找到了导致解密失败的几个核心问题,下面一步步给你解释并提供修正后的代码:
关键问题分析
你的C#代码和PHP代码在三个核心环节上处理不一致:
Key的生成逻辑完全不同
PHP里hash('sha256', $secret_key)返回的是64位小写十六进制字符串,openssl_decrypt会把这个字符串当作明文key,用UTF-8编码成64字节的数组,再通过OpenSSL内置的EVP_BytesToKey算法自动转换成AES-256需要的32字节key。而你的C#代码直接用了SHA256哈希后的原始32字节数组作为key,这和PHP生成的key完全不是一回事,这是最主要的问题。Padding模式不匹配
PHP的openssl_decrypt默认使用PKCS7填充(当options为0时),但你在C#里设置了PaddingMode.None,这会导致解密时无法正确识别加密数据末尾的填充字节,要么抛出"Input data is not a complete block"错误,要么得到乱码。编码细节的潜在问题
你用Encoding.ASCII转换IV字符串,虽然十六进制字符的ASCII和UTF-8编码结果一致,但PHP默认用UTF-8,统一用UTF-8能避免其他场景下的编码问题。
修正后的C#代码实现
首先,生成和PHP一致的SHA256十六进制字符串:
public static string GenerateSha256HexString(string input) { using (var sha256 = SHA256.Create()) { byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); var builder = new StringBuilder(); foreach (byte b in hashBytes) { builder.Append(b.ToString("x2")); // 生成小写十六进制,和PHP的hash()输出完全一致 } return builder.ToString(); } }
然后,模拟OpenSSL的EVP_BytesToKey算法生成正确的key(PHP会自动执行这一步):
private static byte[] DeriveKeyUsingEvpBytesToKey(byte[] passwordBytes, int keyLength) { var key = new List<byte>(); byte[] currentHash = Array.Empty<byte>(); using (var md5 = MD5.Create()) { while (key.Count < keyLength) { // 拼接当前哈希和密码字节,生成新的哈希 var input = new byte[currentHash.Length + passwordBytes.Length]; Buffer.BlockCopy(currentHash, 0, input, 0, currentHash.Length); Buffer.BlockCopy(passwordBytes, 0, input, currentHash.Length, passwordBytes.Length); currentHash = md5.ComputeHash(input); key.AddRange(currentHash); } } return key.Take(keyLength).ToArray(); }
接下来,修正AES解密方法,匹配PHP的填充和编码逻辑:
public static string AesDecrypt(byte[] encryptedData, string keyString, string ivString) { using (var aes = Aes.Create()) { aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; // 和PHP默认填充一致 aes.KeySize = 256; // 把key字符串转成UTF-8字节,再用EVP_BytesToKey生成32字节的AES key byte[] keyBytes = Encoding.UTF8.GetBytes(keyString); aes.Key = DeriveKeyUsingEvpBytesToKey(keyBytes, aes.KeySize / 8); // IV直接用UTF-8编码转换 aes.IV = Encoding.UTF8.GetBytes(ivString); using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) using (var memoryStream = new MemoryStream(encryptedData)) using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) using (var streamReader = new StreamReader(cryptoStream, Encoding.UTF8)) { return streamReader.ReadToEnd(); } } }
最后,调整调用代码,完全对齐PHP的逻辑:
var secretKey = "t-3zafRa"; var secretIv = "St@cE4eZ"; var encryptedTxt = "RmVpUjU4VjJlcXp2dkN5UlhUODQzUT09"; // 生成和PHP一致的key字符串 var keyString = GenerateSha256HexString(secretKey); // 生成IV:SHA256十六进制字符串取前16位 var ivString = GenerateSha256HexString(secretIv).Substring(0, 16); // 解码Base64加密数据 var encryptedData = Convert.FromBase64String(encryptedTxt); // 解密并输出结果 var decryptedText = AesDecrypt(encryptedData, keyString, ivString); Console.WriteLine(decryptedText);
为什么要模拟EVP_BytesToKey?
当你给PHP的openssl_decrypt传入的key长度不符合AES要求时(比如AES-256需要32字节,但我们传入的是64字节的UTF-8数组),OpenSSL会自动用EVP_BytesToKey算法生成符合要求的key。这个算法默认用MD5哈希、无salt、迭代1次,所以我们必须在C#里模拟这个逻辑,才能得到和PHP完全一致的key。
现在运行这段代码,应该就能得到和PHP解密一样的结果了。
内容的提问来源于stack exchange,提问作者Badhon Jain

