nodejs crypto是否有任何加密和解密文本,应该每次都给出不同的加密

时间:2018-05-12 14:25:24

标签: node.js encryption aes cryptojs

我知道带有缓冲区的AES 256 CBC可以提供不同的加密,但其长度 66 。 这是我的代码

  const crypto = require('crypto');
  const ENCRYPTION_KEY = 'Must256bytes(32characters)secret';
  const IV_LENGTH = 16;
  function encrypt(text) {
    let iv = crypto.randomBytes(IV_LENGTH);
    let cipher = crypto.createCipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
    let encrypted = cipher.update(text.toString());
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return iv.toString('hex') + 'XX' + encrypted.toString('hex');
  }
  function decrypt(text) {
    let textParts = text.split('XX');
    let iv = new Buffer(textParts.shift(), 'hex');
    let encryptedText = new Buffer(textParts.join('XX'), 'hex');
    let decipher = crypto.createDecipheriv('aes-256-cbc', new Buffer(ENCRYPTION_KEY), iv);
    let decrypted = decipher.update(encryptedText);
    try{
      decrypted = Buffer.concat([decrypted, decipher.final()]);
      return decrypted.toString();
    }catch(Err){
      return 'NULL';
    }
  }

问题是加密数据长度 66 即使文本是1
那么有没有任何加密和解密方法应该每次都提供不同的加密数据,文本的数量少于 10 ,这是1(根据我的例子)

谢谢

3 个答案:

答案 0 :(得分:3)

是。您可以获得5个字节(或10个十六进制字符)或更少的密文。但是有这么短的密文接种。

基本上有两种方法。我将从更简单的开始。

计数器模式(CTR)

您可以使用带有随机数的CTR(计数器)模式。使用CTR模式加密X字节可以为您提供精确的X字节。 CTR模式不需要将明文填充为块大小的N倍。

此随机数(使用次数为一次)必须是唯一编号,或您的明文有直接暴露的危险。你不能只依赖一个随机数;由于生日界限,4字节随机数具有较高的重复概率。

因此,您需要单独存储随机数或在密文中包含4字节的随机数。但是,如果你设法重用nonce,那么你就搞砸了。对于如此少的字节数,这意味着您基本上必须保留一个4字节计数器,这意味着必须存储状态

通常,CTR模式加密例程要求您提供IV而非nonce。这只是初始计数器值。你必须通过获取现时来构造这个值,然后右填充它使用零值字节,直到你得到16字节,块大小为AES。

您可以找到示例代码here,不要忘记upvote。 Rob还提供了一些示例代码in his answer

格式保留加密(FPE)

使用格式保留加密时,加密输出使用精确的位数或偶数值作为输入的可能值。听起来不错,但FPE由一堆相对复杂的算法组成。基本上你必须在JavaScript中找到一个加密库来实现它。

请注意,对于FPE,相同的消息将始终加密到相同的密文。根据应用程序,这可能是也可能不是问题。

注意:

  • 您使用的是十六进制编码,这不是最密集的编码,您可以使用base 64甚至ASCII 85编码。
  • 首先将IV和密文连接为字节然后执行编码会更有效。如果IV的静态大小,则只需使用常量而不是分隔符。
  • 您的密钥应包含随机字节,而不是文本字符串。密码需要使用密码散列进行转换。

答案 1 :(得分:1)

你的代码在我的结尾处工作得很好,并且长整数的十六进制字符对我来说似乎很正常。

您的密文长度始终为66个字符(十六进制),因为您存储的是32个字符的IV,2个字符的分隔符(“AP”)和32个字符(256位)的密文。你的代码使用填充,因此,密文将始终是明文长度的16个字节(32个十六进制字符)的下一个倍数,这就是为什么你得到一个很长的十六进制字符串作为明文的原因字符串,只有1个字符。

不幸的是,AES需要填充(至少在你正在使用的模式下),否则它将无法正常工作。

答案 2 :(得分:1)

Maarten covers我想提出的大多数要点,所以这只是Node中的一些细节和示例。

您的代码更改为:

  • 在Base64而不是Hex中编码。这样更节省空间。最好只使用Buffers而不是创建一个字符串;然后我们可以有一个9字节的随机数而不是一个5字节的随机数。
  • 删除IV / nonce和密文之间的分隔符。我们知道IV / nonce有多长;我们不需要分隔符。
  • 使用点击率模式而不是CBC模式。这使输出长度等于输入长度。
  • 使用nonce而不是IV。从2 ^ 40空间中随机选择随机数。 (随机选择一个CTR随机数一般来说非常危险。请参阅下文,了解可能在您的用例中可接受的原因;但仍然不推荐。)
  • 通过添加PBKDF2来修复密码生成(您也可以使用randomBytes)。您的密码非常不安全。它是一个ASCII字符串,这意味着它代表了AES-256键空间的一小部分。按键空间的0.000000000002%的顺序。这比AES-256安全得多。

正如Maarten所说,CTR模式复制Key + Nonce对非常危险。如果有人这样做,他们可以学习两条原始消息的异或。有了它,他们很有可能解密这两个消息。例如,如果您在两条消息上复制了密钥+ nonce,并且攻击者使用它来发现它们的XOR为3并且知道加密文本是大写字母,那么他们就会知道这两条消息必须是以下消息之一:

[('A', 'B'), ('D', 'G'), ('E', 'F'), ('H', 'K'), ('I', 'J'), ('L', 'O'),
 ('M', 'N'), ('P', 'S'), ('Q', 'R'), ('T', 'W'), ('U', 'V')]

这种信息对于人类语言或计算机协议等结构化数据来说是毁灭性的。它可以非常快速地用于解密整个消息。密钥+随机数重用是WEP was broken的方式。 (当你手动执行此操作时,它与解决你在报纸上找到的密码谜题基本相同。)加密数据随机性越强,功能越少,提供的上下文越少。

使用随机的5字节随机数,在大约1.3M加密后发生冲突的可能性为50%。对于随机的8字节随机数,在大约5.3B加密之后存在50%的冲突可能性。 sqrt(pi / 2 * 2 ^位)

在加密术语中,这是完全破碎的。它可能或可能不足以满足您的目的。要正确地做到这一点(我不在下面做),正如Maarten所说,你应该跟踪你的计数器并为每次加密增加它而不是使用随机加密。经过2 ^ 40次加密(~1T)后,您可以更改密钥。

假设泄漏有关每百万条消息的信息是可以接受的,这就是你实现这一点的方法。

const crypto = require('crypto');

const ENCRYPTION_KEY = 'Must256bytes(32characters)secret';
const SALT = 'somethingrandom';
const IV_LENGTH = 16;

const NONCE_LENGTH = 5; // Gives us 8-character Base64 output. The higher this number, the better

function encrypt(key, text) {
  let nonce = crypto.randomBytes(NONCE_LENGTH);
  let iv = Buffer.alloc(IV_LENGTH)
  nonce.copy(iv)

  let cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
  let encrypted = cipher.update(text.toString());
  message = Buffer.concat([nonce, encrypted, cipher.final()]);
  return message.toString('base64')
}

function decrypt(key, text) {
  let message = Buffer.from(text, 'base64')
  let iv = Buffer.alloc(IV_LENGTH)
  message.copy(iv, 0, 0, NONCE_LENGTH)
  let encryptedText = message.slice(NONCE_LENGTH)
  let decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
  let decrypted = decipher.update(encryptedText);
  try{
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
  }catch(Err){
    return 'NULL';
  }
}

// You could do this one time and record the result. Or you could just
// generate a random 32-byte key and record that. But you should never
// pass an ASCII string to the encryption function.
let key = crypto.pbkdf2Sync(ENCRYPTION_KEY, SALT, 10000, 32, 'sha512')

let encrypted = encrypt(key, "X")
console.log(encrypted + " : " + encrypted.length)

let decrypted = decrypt(key, encrypted)
console.log(decrypted)