遇到一个很奇怪的问题,用 C# 写的 AES CBC 128 位加密代码,在 linux 上的加密结果,不管用 C# 还是 PHP,解密出来的开头几个字符总是会乱码
.ug6'<'3;71dek`7667463c836a535b40d6b839","user_year":13,"ad":"vth"}
而同样的代码在 windows 上运行得到的加密结果,不管用 C# 还是 PHP,不管是在 windows 还是 linux 上都能正常解密
{"uid":"096e3e957667463c836a535b40d6b839","user_year":13,"ad":"vth"}
.NET 5.0 与 .NET 6 都是同样的问题。
.NET 6 的 C# 加密代码:
var aes = Aes.Create();
aes.Key = Encoding.ASCII.GetBytes(KEY);
ReadOnlySpan<byte> ivBytes = Encoding.ASCII.GetBytes(IV);
ReadOnlySpan<byte> plainTextBytes = Encoding.ASCII.GetBytes(plainText);
var cipherTextBytes = aes.EncryptCbc(plainTextBytes, ivBytes);
return Convert.ToBase64String(cipherTextBytes);
.NET 5.0 的 C# 加密代码:
var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Key = Encoding.ASCII.GetBytes(KEY);
aes.IV = Encoding.ASCII.GetBytes(IV);
using var memoryStream = new MemoryStream();
using var aesEncryptor = aes.CreateEncryptor();
CryptoStream cryptoStream = new(memoryStream, aesEncryptor, CryptoStreamMode.Write);
byte[] plainBytes = Encoding.ASCII.GetBytes(plainText);
cryptoStream.Write(plainBytes, 0, plainBytes.Length);
cryptoStream.FlushFinalBlock();
byte[] cipherBytes = memoryStream.ToArray();
return Convert.ToBase64String(cipherBytes);
PHP 解密代码
$method = 'aes-128-cbc';
$decrypted = openssl_decrypt(base64_decode($encrypted), $method, $key, OPENSSL_RAW_DATA, $iv);
C# 解密代码
using var aes = Aes.Create();
aes.Key = Encoding.ASCII.GetBytes(KEY);
ReadOnlySpan<byte> ivBytes = Encoding.ASCII.GetBytes(IV);
var decryptedBytes = aes.DecryptCbc(cipherTextBytes, ivBytes);
var decryptedText = Encoding.UTF8.GetString(decryptedBytes);
在 stackoverflow 的这个回答中找到了关键线索:
The key/IV pair is used to encrypt the plaintext with AES-256 in CBC mode and PKCS7 padding, s. here. The result is returned in OpenSSL format, which starts with the 8 bytes ASCII encoding of Salted__, followed by the 8 bytes salt and the actual ciphertext, all Base64 encoded. The salt is needed for decryption, so that key and IV can be reconstructed.
经过分析和验证,得出一个推断。
正因为上面提到的 OpenSSL format —— 加密结果的开头是8+8=16个字节的加盐字符串"Salted__xxxxxxxx",所以 aes-128-cbc 在解密时会跳过密文的前16个字节,这16个没有解密的字节读取出来理所当然就是乱码。
有了这个推断,解决方法就油然而生,给密文开头塞进16个字节,iv 正好16字节,就塞它吧。
改进后的 C# 加密代码如下:
var aes = Aes.Create();
aes.Key = Encoding.ASCII.GetBytes(KEY);
var ivBytes = Encoding.ASCII.GetBytes(IV);
ReadOnlySpan<byte> plainTextBytes = Encoding.ASCII.GetBytes(plainText);
var cipherTextBytes = aes.EncryptCbc(plainTextBytes, ivBytes);
return Convert.ToBase64String(ivBytes.Concat(cipherTextBytes).ToArray());
PHP 解密代码也稍作改进,解密后跳过前16个字符获取文本
$method = 'aes-128-cbc';
$decrypted = openssl_decrypt($cypherBytes, $method, $key, OPENSSL_RAW_DATA, $iv);
$decrypted = substr($decrypted, 16);
经过实际验证,该解决方法药到bug除。
改进后的 C# 解密代码
cipherTextBytes = Convert.FromBase64String(cipherText);
var result = aes.DecryptCbc(cipherTextBytes, ivBytes);
var decryptText = Encoding.UTF8.GetString(result[16..]);
dudu 你应该附上的是解密代码
已附
@dudu: 少了一步填充,我C#用的模块不一样,但是你代码里面应该没有填充这一步骤
byte[] plainText = transform.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
return Encoding.UTF8.GetString(plainText);
@小小咸鱼YwY: 用 TransformFinalBlock 问题依旧
var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.Key = Encoding.ASCII.GetBytes(KEY);
aes.IV = Encoding.ASCII.GetBytes(IV);
using var aesEncryptor = aes.CreateEncryptor();
byte[] plainBytes = Encoding.ASCII.GetBytes(plainText);
var cipherBytes = aesEncryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
return Convert.ToBase64String(cipherBytes);
@dudu: 要是没法解决,那就强制字符串替换吧,反正代码已莫名其妙的方式跑起来就好了,可能是linux编码吧,可我ios我逆向用oc注入时候打印出来的没有这个东西,会不会是终端打印的效果,本质不一定有,保存文件看下,然后拉下来
记得之前用公司的轮子, AES 除了密钥 还有补齐的动作;可以再看看源码包的介绍
看起来很像字符编码的问题,加密的时候为什么不用Encoding.UTF8
Encoding.ASCII.GetBytes(plainText);
UTF8 与 ASCII 都试过了
的确奇怪,我用下面的代码再dotnet/ sdk:6.0-focal的容器中测试是正常的
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class Program
{
private static string KEY = "";
static string IV = "";
[STAThread]
public static void Main()
{
string plainText = "{\"uid\":\"中午呢096e3e957667463c836a535b40d6b839\",\"user_year\":13,\"ad\":\"vth\"}";
var p = new Program();
string encryptedBase64 = p.Encrypt(plainText);
string decryptText = p.Decrypt(encryptedBase64);
Console.WriteLine($"plain: {plainText}");
Console.WriteLine($"encrypt: {encryptedBase64}");
Console.WriteLine($"decrypt: {decryptText}");
}
internal string Encrypt(string text)
{
var aes = Aes.Create();
aes.Mode = CipherMode.CBC;
aes.GenerateIV();
aes.GenerateKey();
KEY = Convert.ToBase64String(aes.Key);
IV = Convert.ToBase64String(aes.IV);
using var memoryStream = new MemoryStream();
using var aesEncryptor = aes.CreateEncryptor();
CryptoStream cryptoStream = new (memoryStream, aesEncryptor, CryptoStreamMode.Write);
byte[] plainBytes = Encoding.UTF8.GetBytes(text);
cryptoStream.Write(plainBytes, 0, plainBytes.Length);
cryptoStream.FlushFinalBlock();
byte[] cipherBytes = memoryStream.ToArray();
return Convert.ToBase64String(cipherBytes);
}
internal string Decrypt(string base64Text)
{
using var aes = Aes.Create();
aes.Key = Convert.FromBase64String(KEY);
ReadOnlySpan<byte> ivBytes = Convert.FromBase64String(IV);
byte[] decryptedBytes = aes.DecryptCbc(Convert.FromBase64String(base64Text), ivBytes);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
@Laggage: 统一系统环境应该没问题,我测试的是跨环境, focal、windows、不知道是什么系统的 php 环境
遇事不决重启程序,再不决重启电脑,再不决重装系统
刚发现乱码的是前面16个字符(正好128位),key与iv都是16个字符
– dudu 3年前相关 .NET 6 源码
– dudu 3年前(128, CipherMode.CBC) => Interop.Crypto.EvpAes128Cbc()
https://github.com/dotnet/runtime/blob/release/6.0/src/libraries/System.Security.Cryptography.Algorithms/src/Internal/Cryptography/AesImplementation.Unix.cs相关源码
– dudu 3年前[DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes128Cbc")]
https://github.com/dotnet/runtime/blob/release/6.0/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Cipher.cs#L235linux 上加密调用就是 openssl 库 libSystem.Security.Cryptography.Native.OpenSsl https://github.com/dotnet/runtime/blob/release/6.0/src/libraries/Common/src/Interop/Unix/Interop.Libraries.cs
– dudu 3年前接下来准备用 openssl 命令加密试试
– dudu 3年前