NodeJS 中的无效密钥长度,但 C# 中的有效密钥长度
Invalid Key Length in NodeJS, but Valid Key Length in C#
我正在将 Rijndael 解密从 C# 转换为 NodeJS。
使用的密钥(或密码)长度为 13 个字符。使用的 IV 长度为 17 个字符。
注意:我无法控制长度选择
下面是C#中的Rijndael解密
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class Program
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector)
: this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3)
{
}
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Decrypt(string cipherText) {
return this.Decrypt(Convert.FromBase64String(cipherText));
}
public string Decrypt(byte[] cipherTextBytes) {
return Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
}
public byte[] DecryptToBytes(string cipherText) {
return this.DecryptToBytes(Convert.FromBase64String(cipherText));
}
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
}
public static void Main()
{
string Key = "";
string IV = "";
string encryptedUserData = "u7uENpFfpQhMXiTThL/ajA==";
string decryptedUserData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
decryptedUserData = crypto.Decrypt(encryptedUserData.Trim());
Console.WriteLine(decryptedUserData);
}
}
出于某种原因,我可以解密 dotnetfiddle 中的字符串,但不能解密 Visual Studio 中的字符串(因为它 returns 是 'Specified initialization vector (IV) does not match the block size for this algorithm. (Parameter 'rgbIV')'
[=24= 的错误]
下面是我尝试使用 rijndael-js 库在 NodeJS 中进行转换:
const Rijndael = require("rijndael-js");
const key = "";
const iv = "";
const cipher = new Rijndael(key, "cbc");
const ciphertext = "u7uENpFfpQhMXiTThL/ajA==";
const plaintext = Buffer.from(cipher.decrypt(ciphertext, 256, iv));
其中 returns 错误 Unsupported key size: 104 bit
所有错误都指向同一件事:Key/IV 长度无效。
是否有变通方法可以强制 NodeJS 接受 Key 和 IV 作为有效长度?有什么我遗漏、做错或配置错误的吗?
编辑:
我找到了 PasswordDeriveBytes
implementation for NodeJS 并比较了 C# 的结果,它们是相等的。
我更新了我的 NodeJS 实现(参见 sandbox)并注意到一些事情:
- 所有生成的密文都是一样的。我猜这源于盐。
- 我尝试解密从 C# 生成的密文,但结果值的左侧似乎有几个字符。
示例:C# 加密字符串:
zAqv5w/gwT0sFYXZEx+Awg==
,NodeJS 解密字符串:���&��4423
- 当我尝试在 C# 中解密 NodeJS 生成的密文时,C# 编译器 returns 出现
System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
错误
编辑:
C# 代码(可使用 .NET Framework 4.7.2 执行):
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace ProgramEncrypt
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector) : this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3) { }
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Encrypt(string plainText) => this.Encrypt(Encoding.UTF8.GetBytes(plainText));
public string Encrypt(byte[] plainTextBytes) => Convert.ToBase64String(this.EncryptToBytes(plainTextBytes));
public byte[] EncryptToBytes(string plainText) => this.EncryptToBytes(Encoding.UTF8.GetBytes(plainText));
public byte[] EncryptToBytes(byte[] plainTextBytes)
{
byte[] buffer = this.AddSalt(plainTextBytes);
MemoryStream memoryStream = new MemoryStream();
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.encryptor, CryptoStreamMode.Write);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
byte[] array = memoryStream.ToArray();
memoryStream.Close();
cryptoStream.Close();
return array;
}
}
public string Decrypt(string cipherText) => this.Decrypt(Convert.FromBase64String(cipherText));
public string Decrypt(byte[] cipherTextBytes) => Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
public byte[] DecryptToBytes(string cipherText) => this.DecryptToBytes(Convert.FromBase64String(cipherText));
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
private byte[] AddSalt(byte[] plainTextBytes)
{
if (this.maxSaltLen == 0 || this.maxSaltLen < this.minSaltLen)
return plainTextBytes;
byte[] salt = this.GenerateSalt();
byte[] destinationArray = new byte[plainTextBytes.Length + salt.Length];
Array.Copy((Array)salt, (Array)destinationArray, salt.Length);
Array.Copy((Array)plainTextBytes, 0, (Array)destinationArray, salt.Length, plainTextBytes.Length);
return destinationArray;
}
private byte[] GenerateSalt()
{
int length = this.minSaltLen != this.maxSaltLen ? this.GenerateRandomNumber(this.minSaltLen, this.maxSaltLen) : this.minSaltLen;
byte[] data = new byte[length];
new RNGCryptoServiceProvider().GetNonZeroBytes(data);
data[0] = (byte)((int)data[0] & 252 | length & 3);
data[1] = (byte)((int)data[1] & 243 | length & 12);
data[2] = (byte)((int)data[2] & 207 | length & 48);
data[3] = (byte)((int)data[3] & 63 | length & 192);
return data;
}
private int GenerateRandomNumber(int minValue, int maxValue)
{
byte[] data = new byte[4];
new RNGCryptoServiceProvider().GetBytes(data);
return new Random(((int)data[0] & (int)sbyte.MaxValue) << 24 | (int)data[1] << 16 | (int)data[2] << 8 | (int)data[3]).Next(minValue, maxValue + 1);
}
public static void Main()
{
string Key = "HelL!oWoRL3ds";
string IV = "HElL!o@wOrld!#@%$";
string toEncrypt = "1234";
string encryptedData, decryptedData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
encryptedData = crypto.Encrypt(toEncrypt.Trim());
decryptedData = crypto.Decrypt(encryptedData.Trim());
Console.WriteLine("ENCRYPTED: " + encryptedData);
Console.WriteLine("DECRYPTED: " + decryptedData);
}
}
}
NodeJS 代码(codesandbox.io):
import { deriveBytesFromPassword } from "./deriveBytesFromPassword";
const Rijndael = require("rijndael-js");
const dataToEncrypt = "1234";
const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
const SECRET_IV = "HElL!o@wOrld!#@%$"; // 17 chars
const keySize = 256;
const hashAlgorithm = "SHA512";
// Use only the first 16 bytes of the IV
const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16); // @ref
const rgbSalt = Buffer.from([]);
const derivedPasswordBytes = deriveBytesFromPassword(
SECRET_KEY,
rgbSalt,
3,
hashAlgorithm,
keySize / 8
);
const dataToEncryptInBytes = Buffer.from(dataToEncrypt, "utf8");
const cipher = new Rijndael(derivedPasswordBytes, "cbc");
const encrypted = Buffer.from(cipher.encrypt(dataToEncryptInBytes, 16, rgbIV));
console.log(encrypted.toString("base64"));
// Use this if you only have the Base64 string
// Note: The Base64 string in Line 34 is from C#
// const decrypted = Buffer.from(
// cipher.decrypt(Buffer.from("zAqv5w/gwT0sFYXZEx+Awg==", "base64"), 16, rgbIV)
// );
const decrypted = Buffer.from(cipher.decrypt(encrypted, 16, rgbIV));
console.log(decrypted.toString());
基于沙箱代码并与 C# 代码兼容的可能的 NodeJS 实现是:
const crypto = require("crypto");
const Rijndael = require("rijndael-js");
const pkcs7 = require('pkcs7-padding');
const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
const SECRET_IV = "HElL!o@wOrld!#@%$"; // 17 chars
const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16);
const rgbSalt = Buffer.from([]);
const keySize = 256;
const hashAlgorithm = "SHA512";
const minSaltLen = 4;
const maxSaltLen = 8;
function encrypt(plaintextStr) {
var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);
var cipher = new Rijndael(derivedPasswordBytes, "cbc");
var plaintext = Buffer.from(plaintextStr, "utf8");
var salt = generateSalt();
var saltPlaintext = Buffer.concat([salt, plaintext])
var saltPlaintextPadded = pkcs7.pad(saltPlaintext, 16)
var ciphertext = Buffer.from(cipher.encrypt(saltPlaintextPadded, 128, rgbIV));
return ciphertext.toString("base64");
}
function decrypt(ciphertextB64) {
var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);
var cipher = new Rijndael(derivedPasswordBytes, "cbc");
var ciphertext = Buffer.from(ciphertextB64, 'base64');
var saltPlaintextPadded = Buffer.from(cipher.decrypt(ciphertext, 128, rgbIV));
var sourceIndex = saltPlaintextPadded[0] & 3 | saltPlaintextPadded[1] & 12 | saltPlaintextPadded[2] & 48 | saltPlaintextPadded[3] & 192
var plaintextPadded = saltPlaintextPadded.subarray(sourceIndex)
var plaintext = pkcs7.unpad(plaintextPadded)
return plaintext;
}
function generateSalt() {
var length = minSaltLen != maxSaltLen ? crypto.randomInt(minSaltLen, maxSaltLen + 1) : minSaltLen;
var data = crypto.randomBytes(length);
data[0] = data[0] & 252 | length & 3;
data[1] = data[1] & 243 | length & 12;
data[2] = data[2] & 207 | length & 48;
data[3] = data[3] & 63 | length & 192;
return data;
}
var plaintext = "1234";
var ciphertextB64 = encrypt(plaintext);
var plaintext = decrypt(ciphertextB64);
console.log(ciphertextB64);
console.log(plaintext.toString('hex'))
使用从 linked post 派生的密钥。
用此代码生成的密文可以用C#代码解密,反之,用C#代码生成的密文也可以用此代码解密。
解释:
- 链接的 C# 代码可以在 .NET Framework 下处理 17 字节的 IV(针对 4.7.2 测试)。但是,只考虑前 16 个字节。添加
rijndaelManaged.IV = rgbIV
(如在 MS 示例中)会引发异常。在 .NET Core(针对 3.0+ 进行测试)下,始终会抛出异常。这表明在 .NET Framework 中处理过大的 IV 更可能是错误。无论如何,在 NodeJS 代码中也只需要考虑 IV 的前 16 个字节。
- C# 代码使用专有密钥推导
PasswordDeriveBytes
。必须在 NodeJS 代码中应用相同的密钥派生。在上面的代码中,使用了OP链接的实现。
- 涉及的图书馆 rijndael-js applies Zero padding, but the C# code uses PKCS#7 padding. Therefore, in the NodeJS code, the plaintext (or concatenation of salt and plaintext) must be padded with PKCS#7 before encryption (this satisfies the length criterion and Zero padding is no longer applied). Accordingly, the padding must be removed after decryption. A possible library is pkcs7-padding。或者,可以使用另一个默认应用 PKCS#7 填充的库来代替 rijndael-js。
- C# 代码使用了两种盐:一种是 empty (!)
rgbSalt
,用于密钥推导。另一个是 second salt,它是在加密过程中随机生成的长度和内容,被添加到明文中,并包含有关 salt 长度的信息,该信息在加密过程中确定解密。必须在 NodeJS 代码中实现此逻辑才能使两种代码兼容。
- 无法移植
GenerateRandomNumber()
方法,因为其结果取决于应用 Random()
implementation (which, by the way, is not a CSPRNG). The method is supposed to generate a random integer. For this purpose crypto.randomInt()
is used. For RNGCryptoServiceProvider#GetNonZeroBytes()
create.RandomBytes()
的内部细节。此 NodeJS 函数还允许 0x00 字节,如果需要可以对其进行优化。
安全性:
- 专有密钥派生
PasswordDeriveBytes
已弃用且不安全。相反,NodeJS 代码中的 Rfc2898DeriveBytes
should be used in the C# code and PBKDF2。
- 密钥派生中缺少盐是不安全的,并且允许攻击,例如通过彩虹表。相反,应该为每次加密随机生成足够大小(至少 8 个字节)的盐。此盐不是秘密的,因此通常与密文连接在一起。
- C# 实现使用静态 IV,这也是不安全的。尽管随机 second salt 为相同的明文和相同的 IV 提供了不同的密文,但应该应用最佳实践而不是用户定义的构造。一种行之有效的方法是随机生成 IV,类似于用于密钥派生的盐(为每次加密随机生成,与密文连接)。
我正在将 Rijndael 解密从 C# 转换为 NodeJS。
使用的密钥(或密码)长度为 13 个字符。使用的 IV 长度为 17 个字符。
注意:我无法控制长度选择
下面是C#中的Rijndael解密
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class Program
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector)
: this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3)
{
}
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Decrypt(string cipherText) {
return this.Decrypt(Convert.FromBase64String(cipherText));
}
public string Decrypt(byte[] cipherTextBytes) {
return Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
}
public byte[] DecryptToBytes(string cipherText) {
return this.DecryptToBytes(Convert.FromBase64String(cipherText));
}
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
}
public static void Main()
{
string Key = "";
string IV = "";
string encryptedUserData = "u7uENpFfpQhMXiTThL/ajA==";
string decryptedUserData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
decryptedUserData = crypto.Decrypt(encryptedUserData.Trim());
Console.WriteLine(decryptedUserData);
}
}
出于某种原因,我可以解密 dotnetfiddle 中的字符串,但不能解密 Visual Studio 中的字符串(因为它 returns 是 'Specified initialization vector (IV) does not match the block size for this algorithm. (Parameter 'rgbIV')'
[=24= 的错误]
下面是我尝试使用 rijndael-js 库在 NodeJS 中进行转换:
const Rijndael = require("rijndael-js");
const key = "";
const iv = "";
const cipher = new Rijndael(key, "cbc");
const ciphertext = "u7uENpFfpQhMXiTThL/ajA==";
const plaintext = Buffer.from(cipher.decrypt(ciphertext, 256, iv));
其中 returns 错误 Unsupported key size: 104 bit
所有错误都指向同一件事:Key/IV 长度无效。
是否有变通方法可以强制 NodeJS 接受 Key 和 IV 作为有效长度?有什么我遗漏、做错或配置错误的吗?
编辑:
我找到了 PasswordDeriveBytes
implementation for NodeJS 并比较了 C# 的结果,它们是相等的。
我更新了我的 NodeJS 实现(参见 sandbox)并注意到一些事情:
- 所有生成的密文都是一样的。我猜这源于盐。
- 我尝试解密从 C# 生成的密文,但结果值的左侧似乎有几个字符。
示例:C# 加密字符串:
zAqv5w/gwT0sFYXZEx+Awg==
,NodeJS 解密字符串:���&��4423
- 当我尝试在 C# 中解密 NodeJS 生成的密文时,C# 编译器 returns 出现
System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
错误
编辑:
C# 代码(可使用 .NET Framework 4.7.2 执行):
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace ProgramEncrypt
{
public class CryptoProvider
{
private ICryptoTransform encryptor = (ICryptoTransform)null;
private ICryptoTransform decryptor = (ICryptoTransform)null;
private int minSaltLen = -1;
private int maxSaltLen = -1;
public CryptoProvider(string passPhrase, string initVector) : this(passPhrase, initVector, -1, -1, -1, (string)null, (string)null, 3) { }
public CryptoProvider(
string passPhrase,
string initVector,
int minSaltLen,
int maxSaltLen,
int keySize,
string hashAlgorithm,
string saltValue,
int passwordIterations)
{
this.minSaltLen = 4;
this.maxSaltLen = 8;
keySize = 256;
hashAlgorithm = "SHA512";
byte[] rgbIV = Encoding.ASCII.GetBytes(initVector);
byte[] rgbSalt = new byte[0];
byte[] bytes = new PasswordDeriveBytes(passPhrase, rgbSalt, hashAlgorithm, passwordIterations).GetBytes(keySize / 8);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
if (rgbIV.Length == 0)
rijndaelManaged.Mode = CipherMode.ECB;
else
rijndaelManaged.Mode = CipherMode.CBC;
this.encryptor = rijndaelManaged.CreateEncryptor(bytes, rgbIV);
this.decryptor = rijndaelManaged.CreateDecryptor(bytes, rgbIV);
}
public string Encrypt(string plainText) => this.Encrypt(Encoding.UTF8.GetBytes(plainText));
public string Encrypt(byte[] plainTextBytes) => Convert.ToBase64String(this.EncryptToBytes(plainTextBytes));
public byte[] EncryptToBytes(string plainText) => this.EncryptToBytes(Encoding.UTF8.GetBytes(plainText));
public byte[] EncryptToBytes(byte[] plainTextBytes)
{
byte[] buffer = this.AddSalt(plainTextBytes);
MemoryStream memoryStream = new MemoryStream();
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.encryptor, CryptoStreamMode.Write);
cryptoStream.Write(buffer, 0, buffer.Length);
cryptoStream.FlushFinalBlock();
byte[] array = memoryStream.ToArray();
memoryStream.Close();
cryptoStream.Close();
return array;
}
}
public string Decrypt(string cipherText) => this.Decrypt(Convert.FromBase64String(cipherText));
public string Decrypt(byte[] cipherTextBytes) => Encoding.UTF8.GetString(this.DecryptToBytes(cipherTextBytes));
public byte[] DecryptToBytes(string cipherText) => this.DecryptToBytes(Convert.FromBase64String(cipherText));
public byte[] DecryptToBytes(byte[] cipherTextBytes)
{
int num = 0;
int sourceIndex = 0;
MemoryStream memoryStream = new MemoryStream(cipherTextBytes);
byte[] numArray = new byte[cipherTextBytes.Length];
lock (this)
{
CryptoStream cryptoStream = new CryptoStream((Stream)memoryStream, this.decryptor, CryptoStreamMode.Read);
num = cryptoStream.Read(numArray, 0, numArray.Length);
memoryStream.Close();
cryptoStream.Close();
}
if (this.maxSaltLen > 0 && this.maxSaltLen >= this.minSaltLen)
sourceIndex = (int)numArray[0] & 3 | (int)numArray[1] & 12 | (int)numArray[2] & 48 | (int)numArray[3] & 192;
byte[] destinationArray = new byte[num - sourceIndex];
Array.Copy((Array)numArray, sourceIndex, (Array)destinationArray, 0, num - sourceIndex);
return destinationArray;
}
private byte[] AddSalt(byte[] plainTextBytes)
{
if (this.maxSaltLen == 0 || this.maxSaltLen < this.minSaltLen)
return plainTextBytes;
byte[] salt = this.GenerateSalt();
byte[] destinationArray = new byte[plainTextBytes.Length + salt.Length];
Array.Copy((Array)salt, (Array)destinationArray, salt.Length);
Array.Copy((Array)plainTextBytes, 0, (Array)destinationArray, salt.Length, plainTextBytes.Length);
return destinationArray;
}
private byte[] GenerateSalt()
{
int length = this.minSaltLen != this.maxSaltLen ? this.GenerateRandomNumber(this.minSaltLen, this.maxSaltLen) : this.minSaltLen;
byte[] data = new byte[length];
new RNGCryptoServiceProvider().GetNonZeroBytes(data);
data[0] = (byte)((int)data[0] & 252 | length & 3);
data[1] = (byte)((int)data[1] & 243 | length & 12);
data[2] = (byte)((int)data[2] & 207 | length & 48);
data[3] = (byte)((int)data[3] & 63 | length & 192);
return data;
}
private int GenerateRandomNumber(int minValue, int maxValue)
{
byte[] data = new byte[4];
new RNGCryptoServiceProvider().GetBytes(data);
return new Random(((int)data[0] & (int)sbyte.MaxValue) << 24 | (int)data[1] << 16 | (int)data[2] << 8 | (int)data[3]).Next(minValue, maxValue + 1);
}
public static void Main()
{
string Key = "HelL!oWoRL3ds";
string IV = "HElL!o@wOrld!#@%$";
string toEncrypt = "1234";
string encryptedData, decryptedData;
CryptoProvider crypto = new CryptoProvider(Key, IV);
encryptedData = crypto.Encrypt(toEncrypt.Trim());
decryptedData = crypto.Decrypt(encryptedData.Trim());
Console.WriteLine("ENCRYPTED: " + encryptedData);
Console.WriteLine("DECRYPTED: " + decryptedData);
}
}
}
NodeJS 代码(codesandbox.io):
import { deriveBytesFromPassword } from "./deriveBytesFromPassword";
const Rijndael = require("rijndael-js");
const dataToEncrypt = "1234";
const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
const SECRET_IV = "HElL!o@wOrld!#@%$"; // 17 chars
const keySize = 256;
const hashAlgorithm = "SHA512";
// Use only the first 16 bytes of the IV
const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16); // @ref
const rgbSalt = Buffer.from([]);
const derivedPasswordBytes = deriveBytesFromPassword(
SECRET_KEY,
rgbSalt,
3,
hashAlgorithm,
keySize / 8
);
const dataToEncryptInBytes = Buffer.from(dataToEncrypt, "utf8");
const cipher = new Rijndael(derivedPasswordBytes, "cbc");
const encrypted = Buffer.from(cipher.encrypt(dataToEncryptInBytes, 16, rgbIV));
console.log(encrypted.toString("base64"));
// Use this if you only have the Base64 string
// Note: The Base64 string in Line 34 is from C#
// const decrypted = Buffer.from(
// cipher.decrypt(Buffer.from("zAqv5w/gwT0sFYXZEx+Awg==", "base64"), 16, rgbIV)
// );
const decrypted = Buffer.from(cipher.decrypt(encrypted, 16, rgbIV));
console.log(decrypted.toString());
基于沙箱代码并与 C# 代码兼容的可能的 NodeJS 实现是:
const crypto = require("crypto");
const Rijndael = require("rijndael-js");
const pkcs7 = require('pkcs7-padding');
const SECRET_KEY = "HelL!oWoRL3ds"; // 13 chars
const SECRET_IV = "HElL!o@wOrld!#@%$"; // 17 chars
const rgbIV = Buffer.from(SECRET_IV, "ascii").slice(0, 16);
const rgbSalt = Buffer.from([]);
const keySize = 256;
const hashAlgorithm = "SHA512";
const minSaltLen = 4;
const maxSaltLen = 8;
function encrypt(plaintextStr) {
var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);
var cipher = new Rijndael(derivedPasswordBytes, "cbc");
var plaintext = Buffer.from(plaintextStr, "utf8");
var salt = generateSalt();
var saltPlaintext = Buffer.concat([salt, plaintext])
var saltPlaintextPadded = pkcs7.pad(saltPlaintext, 16)
var ciphertext = Buffer.from(cipher.encrypt(saltPlaintextPadded, 128, rgbIV));
return ciphertext.toString("base64");
}
function decrypt(ciphertextB64) {
var derivedPasswordBytes = deriveBytesFromPassword(SECRET_KEY, rgbSalt, 3, hashAlgorithm, keySize/8);
var cipher = new Rijndael(derivedPasswordBytes, "cbc");
var ciphertext = Buffer.from(ciphertextB64, 'base64');
var saltPlaintextPadded = Buffer.from(cipher.decrypt(ciphertext, 128, rgbIV));
var sourceIndex = saltPlaintextPadded[0] & 3 | saltPlaintextPadded[1] & 12 | saltPlaintextPadded[2] & 48 | saltPlaintextPadded[3] & 192
var plaintextPadded = saltPlaintextPadded.subarray(sourceIndex)
var plaintext = pkcs7.unpad(plaintextPadded)
return plaintext;
}
function generateSalt() {
var length = minSaltLen != maxSaltLen ? crypto.randomInt(minSaltLen, maxSaltLen + 1) : minSaltLen;
var data = crypto.randomBytes(length);
data[0] = data[0] & 252 | length & 3;
data[1] = data[1] & 243 | length & 12;
data[2] = data[2] & 207 | length & 48;
data[3] = data[3] & 63 | length & 192;
return data;
}
var plaintext = "1234";
var ciphertextB64 = encrypt(plaintext);
var plaintext = decrypt(ciphertextB64);
console.log(ciphertextB64);
console.log(plaintext.toString('hex'))
使用从 linked post 派生的密钥。
用此代码生成的密文可以用C#代码解密,反之,用C#代码生成的密文也可以用此代码解密。
解释:
- 链接的 C# 代码可以在 .NET Framework 下处理 17 字节的 IV(针对 4.7.2 测试)。但是,只考虑前 16 个字节。添加
rijndaelManaged.IV = rgbIV
(如在 MS 示例中)会引发异常。在 .NET Core(针对 3.0+ 进行测试)下,始终会抛出异常。这表明在 .NET Framework 中处理过大的 IV 更可能是错误。无论如何,在 NodeJS 代码中也只需要考虑 IV 的前 16 个字节。 - C# 代码使用专有密钥推导
PasswordDeriveBytes
。必须在 NodeJS 代码中应用相同的密钥派生。在上面的代码中,使用了OP链接的实现。 - 涉及的图书馆 rijndael-js applies Zero padding, but the C# code uses PKCS#7 padding. Therefore, in the NodeJS code, the plaintext (or concatenation of salt and plaintext) must be padded with PKCS#7 before encryption (this satisfies the length criterion and Zero padding is no longer applied). Accordingly, the padding must be removed after decryption. A possible library is pkcs7-padding。或者,可以使用另一个默认应用 PKCS#7 填充的库来代替 rijndael-js。
- C# 代码使用了两种盐:一种是 empty (!)
rgbSalt
,用于密钥推导。另一个是 second salt,它是在加密过程中随机生成的长度和内容,被添加到明文中,并包含有关 salt 长度的信息,该信息在加密过程中确定解密。必须在 NodeJS 代码中实现此逻辑才能使两种代码兼容。 - 无法移植
GenerateRandomNumber()
方法,因为其结果取决于应用Random()
implementation (which, by the way, is not a CSPRNG). The method is supposed to generate a random integer. For this purposecrypto.randomInt()
is used. ForRNGCryptoServiceProvider#GetNonZeroBytes()
create.RandomBytes()
的内部细节。此 NodeJS 函数还允许 0x00 字节,如果需要可以对其进行优化。
安全性:
- 专有密钥派生
PasswordDeriveBytes
已弃用且不安全。相反,NodeJS 代码中的Rfc2898DeriveBytes
should be used in the C# code and PBKDF2。 - 密钥派生中缺少盐是不安全的,并且允许攻击,例如通过彩虹表。相反,应该为每次加密随机生成足够大小(至少 8 个字节)的盐。此盐不是秘密的,因此通常与密文连接在一起。
- C# 实现使用静态 IV,这也是不安全的。尽管随机 second salt 为相同的明文和相同的 IV 提供了不同的密文,但应该应用最佳实践而不是用户定义的构造。一种行之有效的方法是随机生成 IV,类似于用于密钥派生的盐(为每次加密随机生成,与密文连接)。