在NodeJS中无效的密钥长度,但在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中解密(因为它返回'Specified initialization vector (IV) does not match the block size for this algorithm. (Parameter 'rgbIV')'

的错误)下面是我使用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));

返回错误Unsupported key size: 104 bit

所有的错误都指向同一件事:无效的密钥/IV长度。

是否有一个解决方案,我可以强迫NodeJS接受密钥和IV作为有效长度?是否有我遗漏的东西,做得不正确,或错误配置?

编辑:

我能够找到NodeJS的PasswordDeriveBytes实现,并比较了c#的结果,它们是相等的。

我更新了我的NodeJS实现(见sandbox),并注意到一些事情:

  1. 所有产生的密文都相同。我猜这源于盐。
  2. 我尝试解密从c#生成的密文,但结果值的左边似乎有几个字符。示例:c#加密字符串:zAqv5w/gwT0sFYXZEx+Awg==, NodeJS解密字符串:���&��4423
  3. 当我试图解密c#中NodeJS中生成的密文时,c#编译器返回System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.
  4. 的错误

编辑:

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 https://stackoverflow.com/a/57147116/12278028
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'))

使用链接帖子的密钥派生。

用该代码生成的密文可以用c#代码解密,反之亦然,用c#代码生成的密文可以用该代码解密。


解释:

  • 链接的c#代码可以在。net框架下处理一个17字节的IV(测试于4.7.2)。但是,只考虑前16个字节。添加rijndaelManaged.IV = rgbIV后(如MS示例中)会抛出异常。在。net Core(3.0+测试)下,总是会抛出异常。这表明在。net框架中处理太大的IV更有可能是一个bug。无论如何,在NodeJS代码也只有前16个字节的IV必须考虑。
  • c#代码使用专有密钥派生PasswordDeriveBytes。必须在NodeJS代码中应用相同的键派生。在上面的代码中,使用了OP链接的实现。
  • rijndael-js使用零填充,但c#代码使用pkcs# 7填充。因此,在NodeJS代码中,明文(或salt和明文的连接)必须在加密之前用pkcs# 7填充(这满足长度标准,并且不再应用Zero填充)。因此,在解密后必须删除填充。一个可能的库是pkcs7-padding。或者,可以使用另一个库代替rijndael-js,它默认应用pkcs# 7填充。
  • c#代码使用了两个盐:一个是(!)rgbSalt,用于密钥派生。另一个是第二个盐,它是在加密过程中根据长度和内容随机生成的,放在明文之前,包含在解密过程中确定的盐长度信息。这个逻辑必须在NodeJS代码中实现,以便两个代码兼容。
  • GenerateRandomNumber()方法不能被移植,因为它的结果依赖于Random()实现的内部细节(顺便说一下,它不是CSPRNG)。该方法应该生成一个随机整数。为此,使用crypto.randomInt()。对于RNGCryptoServiceProvider#GetNonZeroBytes(),应用create.RandomBytes()。这个NodeJS函数也允许0x00字节,如果需要可以优化。

安全:

  • 专有密钥派生PasswordDeriveBytes已弃用且不安全。相反,Rfc2898DeriveBytes应该在c#代码中使用,PBKDF2应该在NodeJS代码中使用。
  • 密钥派生中缺失的盐是不安全的,允许攻击,例如通过彩虹表。相反,应该为每个加密随机生成一个足够大的盐(至少8字节)。此盐不是秘密的,因此通常与密文连接。
  • c#实现使用静态IV,这也是不安全的。尽管随机的第二个salt为相同的明文和相同的iv提供了不同的密文,但应该应用最佳实践而不是用户定义的构造。一种已证明的方法是随机生成IV,类似于用于密钥派生的盐(为每个加密随机生成,与密文连接)。

相关内容

最新更新