相当于 C# 的 JS 中的 AES 加密

AES encryption in JS equivalent of C#

我需要使用 AES 加密来加密字符串。这种加密早先在 C# 中进行过,但需要将其转换为 JavaScript(在浏览器上将是 运行)。

C#中用于加密的当前代码如下 -

public static string EncryptString(string plainText, string encryptionKey)
{
    byte[] clearBytes = Encoding.Unicode.GetBytes(plainText);

    using (Aes encryptor = Aes.Create())

    {
        Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(encryptionKey, new byte[] { 0x49, 0x76, 0x61, 0x6e, 0x20, 0x4d, 0x65, 0x64, 0x76, 0x65, 0x64, 0x65, 0x76 });
        encryptor.Key = pdb.GetBytes(32);
        encryptor.IV = pdb.GetBytes(16);
        using (MemoryStream ms = new MemoryStream())
        {
            using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write))

            {
                cs.Write(clearBytes, 0, clearBytes.Length);
                cs.Close();
            }
            plainText = Convert.ToBase64String(ms.ToArray());
        }
    }
    return plainText;
}

我曾尝试使用 CryptoJS 来复制相同的功能,但它没有给我等效的加密 base64 字符串。这是我的 CryptoJS 代码 -

function encryptString(encryptString, secretKey) {
    var iv = CryptoJS.enc.Hex.parse('Ivan Medvedev');
    var key = CryptoJS.PBKDF2(secretKey, iv, { keySize: 256 / 32, iterations: 500 });

    var encrypted = CryptoJS.AES.encrypt(encryptString, key,{iv:iv);
    return encrypted;
}

必须将加密的字符串发送到能够对其进行解密的服务器。服务器可以解密C#代码生成的加密字符串,但不能解密JS代码生成的加密字符串。我尝试比较这两种代码生成的加密字符串,发现 C# 代码生成的加密字符串更长。例如,将 'Example String' 保留为纯文本,将 'Example Key' 保留为键,我得到以下结果 -

C# - eAQO+odxOdGlNRB81SHR2XzJhyWtz6XmQDko9HyDe0w=
JS - 9ex5i2g+8iUCwdwN92SF+A==

JS加密字符串的长度总是比C#的短。我做错了什么吗?我只需要将 C# 代码复制到 JS 代码中即可。

更新:
我在 之后的当前代码是这样的 -

function encryptString(encryptString, secretKey) {
    var keyBytes = CryptoJS.PBKDF2(secretKey, 'Ivan Medvedev', { keySize: 48 / 4, iterations: 1000 });
    console.log(keyBytes.toString());

    // take first 32 bytes as key (like in C# code)
    var key = new CryptoJS.lib.WordArray.init(keyBytes.words, 32);
    // skip first 32 bytes and take next 16 bytes as IV
    var iv = new CryptoJS.lib.WordArray.init(keyBytes.words.splice(32 / 4), 16);

    console.log(key.toString());
    console.log(iv.toString());

    var encrypted = CryptoJS.AES.encrypt(encryptString, key, { iv: iv });
    return encrypted;
}

如 his/her 答案所示,如果 C# 代码使用 ASCII 而不是 Unicode 将纯文本转换为字节,则 C# 和 JS 代码都将产生准确的结果。但由于我无法修改解密代码,我必须将代码转换为等效于使用 Unicode 的原始 C# 代码。

所以,我试着看看 C# 中 ASCII 和 Unicode 字节转换之间的字节数组有什么区别。这是我发现的 -

ASCII Byte Array: [69,120,97,109,112,108,101,32,83,116, 114, 105, 110, 103]
Unicode Byte Array: [69,0,120,0,97,0,109,0,112,0,108,0,101,0,32,0,83,0,116,0, 114,0, 105,0, 110,0, 103,0]

因此 C# 中的每个字符都有一些额外的字节可用(因此 Unicode 为每个字符分配的字节数是 ASCII 的两倍)。

这是 Unicode 和 ASCII 转换的区别 -

ASCII
clearBytes: [69,120,97,109,112,108,101,32,83,116,114,105,110,103,]
encryptor.Key: [123,213,18,82,141,249,182,218,247,31,246,83,80,77,195,134,230,92,0,125,232,210,135,115,145,193,140,239,228,225,183,13,]
encryptor.IV: [101,74,46,177,46,233,68,252,83,169,211,13,249,61,118,167,]
Result: eQus9GLPKULh9vhRWOJjog==

Unicode:
clearBytes: [69,0,120,0,97,0,109,0,112,0,108,0,101,0,32,0,83,0,116,0,114,0,105,0,110,0,103,0,]
encryptor.Key: [123,213,18,82,141,249,182,218,247,31,246,83,80,77,195,134,230,92,0,125,232,210,135,115,145,193,140,239,228,225,183,13,]
encryptor.IV: [101,74,46,177,46,233,68,252,83,169,211,13,249,61,118,167,]
Result: eAQO+odxOdGlNRB81SHR2XzJhyWtz6XmQDko9HyDe0w=

因此,由于生成的密钥和 iv 在 Unicode 和 ASCII 方法中都具有完全相同的字节数组,因此它不应该生成不同的输出,但不知何故它正在这样做。我认为这是因为 clearBytes 的长度,因为它使用它的长度写入 CryptoStream。

我试图查看 JS 代码中生成的字节的输出是什么,发现它使用需要使用 toString() 方法转换为字符串的单词。

keyBytes: 7bd512528df9b6daf71ff653504dc386e65c007de8d2877391c18cefe4e1b70d654a2eb12ee944fc53a9d30df93d76a7
key: 7bd512528df9b6daf71ff653504dc386e65c007de8d2877391c18cefe4e1b70d
iv: 654a2eb12ee944fc53a9d30df93d76a7

因为我无法在JS代码中影响生成的加密字符串的长度(无法直接访问写入流),所以仍然卡在这里。

这是如何在 C#CryptoJS 之间重现相同密文的示例:

static void Main(string[] args)
{
    byte[] plainText = Encoding.Unicode.GetBytes("Example String"); // this is UTF-16 LE
    string cipherText;
    using (Aes encryptor = Aes.Create())
    {
        var pdb = new Rfc2898DeriveBytes("Example Key", Encoding.ASCII.GetBytes("Ivan Medvedev"));
        encryptor.Key = pdb.GetBytes(32);
        encryptor.IV = pdb.GetBytes(16);
        using (MemoryStream ms = new MemoryStream())
        {
            using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write))
            {
                cs.Write(plainText, 0, plainText.Length);
                cs.Close();
            }
            cipherText = Convert.ToBase64String(ms.ToArray());
        }
    }

    Console.WriteLine(cipherText);
}

和 JS:

var keyBytes = CryptoJS.PBKDF2('Example Key', 'Ivan Medvedev', { keySize: 48 / 4, iterations: 1000 });
// take first 32 bytes as key (like in C# code)
var key = new CryptoJS.lib.WordArray.init(keyBytes.words, 32);
// skip first 32 bytes and take next 16 bytes as IV
var iv = new CryptoJS.lib.WordArray.init(keyBytes.words.splice(32 / 4), 16);
// use the same encoding as in C# code, to convert string into bytes
var data = CryptoJS.enc.Utf16LE.parse("Example String");
var encrypted = CryptoJS.AES.encrypt(data, key, { iv: iv });
console.log(encrypted.toString());

两个代码return:eAQO+odxOdGlNRB81SHR2XzJhyWtz6XmQDko9HyDe0w=

TL;DR 最终代码如下所示 -

function encryptString(encryptString, secretKey) {
    encryptString = addExtraByteToChars(encryptString);
    var keyBytes = CryptoJS.PBKDF2(secretKey, 'Ivan Medvedev', { keySize: 48 / 4, iterations: 1000 });
    console.log(keyBytes.toString());
    var key = new CryptoJS.lib.WordArray.init(keyBytes.words, 32);
    var iv = new CryptoJS.lib.WordArray.init(keyBytes.words.splice(32 / 4), 16);
    var encrypted = CryptoJS.AES.encrypt(encryptString, key, { iv: iv, });
    return encrypted;
}

function addExtraByteToChars(str) {
    let strResult = '';
    for (var i = 0; i < str.length; ++i) {
        strResult += str.charAt(i) + String.fromCharCode(0);
    }
    return strResult;
}

解释:

中的 C# 代码(感谢 him/her)使用 ASCII 将纯文本转换为字节,而我的 C# 代码使用的是 Unicode。 Unicode 正在为生成的字节数组中的每个字符分配额外的字节,这不会影响密钥和 iv 字节的生成,但会影响结果,因为加密字符串的长度取决于从纯文本生成的字节的长度。
如以下为每个字节生成的字节所示,分别使用 "Example String" 和 "Example Key" 作为 plainText 和 secretKey -

ASCII
clearBytes: [69,120,97,109,112,108,101,32,83,116,114,105,110,103,]
encryptor.Key: [123,213,18,82,141,249,182,218,247,31,246,83,80,77,195,134,230,92,0,125,232,210,135,115,145,193,140,239,228,225,183,13,]
encryptor.IV: [101,74,46,177,46,233,68,252,83,169,211,13,249,61,118,167,]
Result: eQus9GLPKULh9vhRWOJjog==

Unicode:
clearBytes: [69,0,120,0,97,0,109,0,112,0,108,0,101,0,32,0,83,0,116,0,114,0,105,0,110,0,103,0,]
encryptor.Key: [123,213,18,82,141,249,182,218,247,31,246,83,80,77,195,134,230,92,0,125,232,210,135,115,145,193,140,239,228,225,183,13,]
encryptor.IV: [101,74,46,177,46,233,68,252,83,169,211,13,249,61,118,167,]
Result: eAQO+odxOdGlNRB81SHR2XzJhyWtz6XmQDko9HyDe0w=

JS 结果也类似,这证实它使用的是 ASCII 字节转换 -

keyBytes: 7bd512528df9b6daf71ff653504dc386e65c007de8d2877391c18cefe4e1b70d654a2eb12ee944fc53a9d30df93d76a7
key: 7bd512528df9b6daf71ff653504dc386e65c007de8d2877391c18cefe4e1b70d
iv: 654a2eb12ee944fc53a9d30df93d76a7  

因此我只需要增加纯文本的长度,使其使用 Unicode 等效字节生成(抱歉,不熟悉该术语)。由于 Unicode 为 byteArray 中的每个字符分配 2 space,将第二个 space 保持为 0,我基本上在纯文本的字符中创建了间隙,并使用 ASCII 值为 0 的字符填充了该间隙 addExtraByteToChars()函数。它让一切变得不同。

这当然是一种解决方法,但开始适用于我的方案。我想这可能对其他人有用,也可能没有用,因此可以分享这些发现。如果有人可以建议更好地实现 addExtraByteToChars() 函数(可能是这种转换的某个术语,而不是 ASCII 到 Unicode 或更好、更高效、更简单的方法),请提出建议。