C# .NET AES 与 JavaScript 的互操作性

C# .NET AES interoperability with JavaScript

我一直在尝试以可互操作的方式使用 AES 和 C# 加密和解密字符串。我的客户端应用程序是一个节点服务器,它与供应商的 API 通信是在点 NET 中。

供应商使用这些方法来加密和解密字符串:

public static string Encrypt(string data, string key)
{
  string IV = key.Substring(0, 16);
  byte[] iv = Encoding.UTF8.GetBytes(IV);
  byte[] array;
  using(Aes aes = Aes.Create())
  {
    aes.Key = Encoding.UTF8.GetBytes(key);
    aes.IV = iv;
    ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
    using(MemoryStream memoryStream = new MemoryStream())
    {
      using(CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, encryptor, CryptoStreamMode.Write))
      {
        using(StreamWriter streamWriter = new StreamWriter((Stream)cryptoStream))
        {
          streamWriter.Write(data);
        }
        array = memoryStream.ToArray();
      }
    }
  }
  return Convert.ToBase64String(array);
}



public static string Decrypt(string data, string key)
{
  string IV = key.Substring(0, 16);
  byte[] iv = Encoding.UTF8.GetBytes(IV);
  byte[] buffer = Convert.FromBase64String(data);
  using(Aes aes = Aes.Create())
  {
    aes.Key = Encoding.UTF8.GetBytes(key);
    aes.IV = iv;
    ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
    using(MemoryStream memoryStream = new MemoryStream(buffer))
    {
      using(CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, decryptor, CryptoStreamMode.Read))
      {
        using(StreamReader streamReader = new StreamReader((Stream)cryptoStream))
        {
          return streamReader.ReadToEnd();
        }
      }
    }
  }
}

我试过 crypto-js 来解密字符串,但我无法让它工作:

const encryptedText = CryptoJS.enc.Base64.parse(base64Value)
const encrypted2 = encryptedText.toString(CryptoJS.enc.Base64);
const decrypt2 = CryptoJS.AES.decrypt(encrypted2, key, {
 mode: CryptoJS.mode.ECB,
 padding: CryptoJS.pad.Pkcs7
});

console.log(decrypt2.toString(CryptoJS.enc.Utf8)) // (also tried various other encodings, Utf16, Utf16LE and others)

以下 Node.js 代码应该可以与您的 .NET 代码一起正常工作。

我们正在使用算法 aes-256-cbc 来匹配 C# 示例中使用的模式。

const crypto = require("crypto");
const Algorithm = "aes-256-cbc";

function encrypt(plainText, key, iv, outputEncoding = "base64") {
    const cipher = crypto.createCipheriv(Algorithm, key, iv);
    const output = Buffer.concat([cipher.update(plainText), cipher.final()]).toString(outputEncoding);
    return output.replace('+', '-').replace('/', '_').replace('=', '');
}

function decrypt(cipherText, key, iv, outputEncoding = "utf8") {
    cipherText = Buffer.from(cipherText, "base64");
    const cipher = crypto.createDecipheriv(Algorithm, key, iv);
    return Buffer.concat([cipher.update(cipherText), cipher.final()]).toString(outputEncoding);
}

const KEY = 'KFmnMAPzP!g@6Dy5HD?JSgYC9obE&m@m';
const IV = KEY.slice(0,16);

// Taking the output from our C# sample...
const encrypted = 'SORoNS48u0KniiANU3Y9Mw==';
console.log("Encrypted (base64):", encrypted);
const decrypted = decrypt(encrypted, KEY, IV)
console.log("Decrypted:", decrypted);

等效的 C# 代码如下:

using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;
                    
public class Program
{
    public static void Main()
    {
        var str = Encrypt("test", "KFmnMAPzP!g@6Dy5HD?JSgYC9obE&m@m");
        Console.WriteLine("Encrypted: " + str);
        Console.WriteLine("Decrypted: " + Decrypt(str, "KFmnMAPzP!g@6Dy5HD?JSgYC9obE&m@m"));
    }
    
    public static string Encrypt(string data, string key)
    {
      string IV = key.Substring(0, 16);
      byte[] iv = Encoding.UTF8.GetBytes(IV);
      byte[] array;
      using(Aes aes = Aes.Create())
      {
        aes.Key = Encoding.UTF8.GetBytes(key);
        aes.IV = iv;
        ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
        using(MemoryStream memoryStream = new MemoryStream())
        {
          using(CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, encryptor, CryptoStreamMode.Write))
          {
            using(StreamWriter streamWriter = new StreamWriter((Stream)cryptoStream))
            {
              streamWriter.Write(data);
            }
            array = memoryStream.ToArray();
          }
        }
      }
      return Convert.ToBase64String(array);
    }



    public static string Decrypt(string data, string key)
    {
      string IV = key.Substring(0, 16);
      byte[] iv = Encoding.UTF8.GetBytes(IV);
      byte[] buffer = Convert.FromBase64String(data);
      using(Aes aes = Aes.Create())
      {
        aes.Key = Encoding.UTF8.GetBytes(key);
        aes.IV = iv;
        ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
        using(MemoryStream memoryStream = new MemoryStream(buffer))
        {
          using(CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, decryptor, CryptoStreamMode.Read))
          {
            using(StreamReader streamReader = new StreamReader((Stream)cryptoStream))
            {
              return streamReader.ReadToEnd();
            }
          }
        }
      }
    }
}

C# 代码的输出是:

 Encrypted: SORoNS48u0KniiANU3Y9Mw==
 Decrypted: test

然后 Node.js 代码对此进行解密(使用相同的密钥和 IV):

 Encrypted (base64): SORoNS48u0KniiANU3Y9Mw=
 Decrypted: test

由于您使用的是 NodeJS,因此使用 NodeJS 的加密模块而不是 CryptoJS 是有意义的(请参阅 )。 CryptoJS当然也是可以的。那么必须考虑以下几点:

在JavaScript代码中,必须使用CBC模式,并且key和IV必须作为WordArray传递给CryptoJS.AES.decrypt()。密文可以通过 Base64 编码传递,CryptoJS 将其隐式转换为 CipherParams 对象。

CryptoJS 默认应用 CBC 和 PKCS#7 填充,因此无需明确指定(但当然可以)。

下面例子中的密文是用C#代码生成的,可以用下面的CryptoJS代码解密:

const ciphertext = 'yKiV9TBw3eNt2QvK1kdXaw=='; 
const keyStr = "01234567890123456789012345678901"; // 32 bytes -> AES-256
const key = CryptoJS.enc.Utf8.parse(keyStr);
const IV = CryptoJS.enc.Utf8.parse(keyStr.substr(0, 16));
const decrypted = CryptoJS.AES.decrypt(ciphertext, key, {iv: IV}); // apply default: CBC and PKCS#7 padding

console.log(decrypted.toString(CryptoJS.enc.Utf8)); // Hello world!
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>

关于安全:
将密钥(或其中的一部分)用作 IV 是不安全的。相反,应该为每次加密生成一个随机 IV。这不是秘密,与密文一起传递,通常是串联的。
此外,从文本生成密钥会降低安全性(即使大小正确,值范围也会缩小)。如果涉及 text/passphrase,则更安全的方法是使用可靠的密钥派生函数,如 PBKDF2。