首页 新闻 搜索 专区 学院

遭遇奇怪的 .NET 在 Linux 上的 AES 加密问题

0
悬赏园豆:200 [已解决问题] 解决于 2021-11-06 21:03

遇到一个很奇怪的问题,用 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);
dudu的主页 dudu | 高人七级 | 园豆:36310
提问于:2021-11-05 10:51

刚发现乱码的是前面16个字符(正好128位),key与iv都是16个字符

dudu 10个月前

相关源码 [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#L235

dudu 10个月前

linux 上加密调用就是 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 10个月前

接下来准备用 openssl 命令加密试试

dudu 10个月前
< >
分享
最佳答案
2

在 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除。

dudu | 高人七级 |园豆:36310 | 2021-11-05 17:13

改进后的 C# 解密代码

cipherTextBytes = Convert.FromBase64String(cipherText);
var result = aes.DecryptCbc(cipherTextBytes, ivBytes);
var decryptText = Encoding.UTF8.GetString(result[16..]);
dudu | 园豆:36310 (高人七级) | 2021-11-05 17:21
其他回答(4)
0

dudu 你应该附上的是解密代码

收获园豆:60
小小咸鱼YwY | 园豆:3191 (老鸟四级) | 2021-11-05 10:57

已附

支持(0) 反对(0) dudu | 园豆:36310 (高人七级) | 2021-11-05 11:05

@dudu: 少了一步填充,我C#用的模块不一样,但是你代码里面应该没有填充这一步骤
byte[] plainText = transform.TransformFinalBlock(encryptedData, 0, encryptedData.Length);
return Encoding.UTF8.GetString(plainText);

支持(0) 反对(0) 小小咸鱼YwY | 园豆:3191 (老鸟四级) | 2021-11-05 11:18

@小小咸鱼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);
支持(0) 反对(0) dudu | 园豆:36310 (高人七级) | 2021-11-05 12:11

@dudu: 要是没法解决,那就强制字符串替换吧,反正代码已莫名其妙的方式跑起来就好了,可能是linux编码吧,可我ios我逆向用oc注入时候打印出来的没有这个东西,会不会是终端打印的效果,本质不一定有,保存文件看下,然后拉下来

支持(0) 反对(0) 小小咸鱼YwY | 园豆:3191 (老鸟四级) | 2021-11-05 16:02
0

记得之前用公司的轮子, AES 除了密钥 还有补齐的动作;可以再看看源码包的介绍

收获园豆:60
〆灬丶 | 园豆:1214 (小虾三级) | 2021-11-05 11:33
0

看起来很像字符编码的问题,加密的时候为什么不用Encoding.UTF8

Encoding.ASCII.GetBytes(plainText);

收获园豆:60
Laggage | 园豆:824 (小虾三级) | 2021-11-05 12:26

UTF8 与 ASCII 都试过了

支持(0) 反对(0) dudu | 园豆:36310 (高人七级) | 2021-11-05 12:49

的确奇怪,我用下面的代码再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);
    }
}

支持(0) 反对(0) Laggage | 园豆:824 (小虾三级) | 2021-11-05 13:01

@Laggage: 统一系统环境应该没问题,我测试的是跨环境, focal、windows、不知道是什么系统的 php 环境

支持(0) 反对(0) dudu | 园豆:36310 (高人七级) | 2021-11-05 16:28
0

遇事不决重启程序,再不决重启电脑,再不决重装系统

收获园豆:20
dodogit | 园豆:273 (菜鸟二级) | 2021-11-05 15:13
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册