nodejs crypto 是否有加密和解密文本,应该每次都给不同的加密
nodejs crypto Is there any encrypt and decrypt text, should give different encryption at every time
我知道带有缓冲区的 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(根据我的例子)
谢谢
你的代码在我这边运行良好,一长串十六进制字符对我来说似乎很正常。
您的密文始终为 66 个字符长(十六进制),因为您要存储 32 个字符的 IV、2 个字符的定界符 ("AP") 和 32 个字符(256 位)的密文。您的代码使用填充,因此,密文将始终是明文长度的下一个 16 字节(32 个十六进制字符)的倍数,这就是为什么您得到这么长的明文十六进制字符串只有 1 个字符的字符串。
不幸的是,AES 需要填充(至少在您使用的模式下),否则它将无法运行。
是的。可以获得 5 个字节(或 10 个十六进制字符)或更少的密文。但是这么短的密文也有陷阱。
基本上有两种方法。我先从简单的开始。
计数器模式(点击率)
您可以使用带有随机数的 CTR(计数器)模式。使用 CTR 模式加密 X 个字节,结果正好是 X 个字节。 CTR 模式不需要将明文填充到块大小的 N 倍。
这个随机数(number-used-once)必须是一个唯一的数字,否则你的明文有被暴露的直接危险 .您不能只依赖随机数;由于生日界限.
,一个 4 字节的随机数有很高的重复概率
所以你要么需要单独存储随机数,要么在你的密文中包含一个 4 字节的随机数。但是,如果您设法重用随机数,那么您就完蛋了。对于如此低的字节数,这意味着您基本上必须保留一个 4 字节的计数器,这意味着必须存储 state.
通常 CTR 模式加密例程要求您提供 IV 而不是随机数。这只是初始计数器值。您必须通过获取随机数来构造此值,然后右填充它具有零值字节,直到达到 16 字节,即块大小AES.
您可以找到示例代码 , don't forget to upvote. Rob also has provided some sample code 。
格式保留加密 (FPE)
使用保留格式的加密,加密的输出使用确切的位数或偶数作为输入的可能值。这听起来不错,但 FPE 由一堆相对复杂的算法组成。基本上你必须在 JavaScript 中找到一个加密库来实现它。
请注意,使用 FPE,相同的消息将始终加密为相同的密文。这可能是问题,也可能不是问题,具体取决于应用程序。
备注:
- 您正在使用十六进制编码,这不是最密集的编码,您可以使用 base 64 甚至 ASCII 85 编码。
- 首先将IV和密文连接为字节然后进行编码会更高效。如果您有 IV 的静态大小,您可以简单地使用常量而不是分隔符。
- 您的密钥应由随机字节组成,而不是文本字符串。需要使用密码哈希转换密码。
我想表达的大部分要点,所以这只是一些阐述和 Node 中的示例。
您的代码所做的更改是:
- 使用 Base64 而不是 Hex 进行编码。这 space 效率更高。只使用缓冲区而不创建字符串会更好;那么我们可以有一个 9 字节的随机数而不是 5 字节的随机数。
- 去掉IV/nonce和密文之间的分隔符。我们知道 IV/nonce 有多长;我们不需要分隔符。
- 使用CTR模式而不是CBC模式。这使得输出长度等于输入长度。
- 使用随机数而不是 IV。从 2^40 space 中随机选择随机数。 (随机选择 CTR 随机数通常是非常危险的。请参阅下文了解为什么它 可能 在您的用例中是可以接受的;它仍然不被推荐。)
- 通过添加 PBKDF2 修复密码生成问题(您也可以只使用 randomBytes)。您的密码非常不安全。它是一个 ASCII 字符串,这意味着它代表 AES-256 密钥的一小部分space。在密钥的 0.000000000002% 的数量级 space。这就是它比 AES-256 安全得多。
正如 Maarten 所说,CTR 模式复制 Key+Nonce 对是非常危险的。如果有人这样做,他们就可以学习两个原始消息的 XOR。这样,他们就有很好的机会解密这两条消息。例如,如果您在两条消息上复制了您的密钥+随机数,并且攻击者使用它来发现它们的 XOR 是 3 并且知道加密文本是大写字母,那么他们就会知道这两条消息必须是其中之一:
[('A', 'B'), ('D', 'G'), ('E', 'F'), ('H', 'K'), ('I', 'J'), ('L', 'O'),
('M', 'N'), ('P', 'S'), ('Q', 'R'), ('T', 'W'), ('U', 'V')]
这种信息对于人类语言或计算机协议等结构化数据来说是毁灭性的。它可以很快用于解密整个消息。 Key+nonce 复用就是这样WEP was broken。 (当您手动执行此操作时,它基本上与解决您在报纸上找到的密码谜题相同。)加密数据的随机性越大,它提供的上下文越少,它的功能就越弱。
使用随机的 5 字节随机数,在大约 130 万次加密后发生冲突的可能性为 50%。使用随机的 8 字节随机数,在大约 5.3B 加密后发生冲突的可能性为 50%。 sqrt(pi/2 * 2^bits)
用密码学术语来说,这是一个完全破解的过程。它可能足以满足您的目的,也可能不足以满足您的目的。要正确地做到这一点(我在下面没有这样做),正如 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)
我知道带有缓冲区的 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(根据我的例子)
谢谢
你的代码在我这边运行良好,一长串十六进制字符对我来说似乎很正常。
您的密文始终为 66 个字符长(十六进制),因为您要存储 32 个字符的 IV、2 个字符的定界符 ("AP") 和 32 个字符(256 位)的密文。您的代码使用填充,因此,密文将始终是明文长度的下一个 16 字节(32 个十六进制字符)的倍数,这就是为什么您得到这么长的明文十六进制字符串只有 1 个字符的字符串。
不幸的是,AES 需要填充(至少在您使用的模式下),否则它将无法运行。
是的。可以获得 5 个字节(或 10 个十六进制字符)或更少的密文。但是这么短的密文也有陷阱。
基本上有两种方法。我先从简单的开始。
计数器模式(点击率)
您可以使用带有随机数的 CTR(计数器)模式。使用 CTR 模式加密 X 个字节,结果正好是 X 个字节。 CTR 模式不需要将明文填充到块大小的 N 倍。
这个随机数(number-used-once)必须是一个唯一的数字,否则你的明文有被暴露的直接危险 .您不能只依赖随机数;由于生日界限.
,一个 4 字节的随机数有很高的重复概率所以你要么需要单独存储随机数,要么在你的密文中包含一个 4 字节的随机数。但是,如果您设法重用随机数,那么您就完蛋了。对于如此低的字节数,这意味着您基本上必须保留一个 4 字节的计数器,这意味着必须存储 state.
通常 CTR 模式加密例程要求您提供 IV 而不是随机数。这只是初始计数器值。您必须通过获取随机数来构造此值,然后右填充它具有零值字节,直到达到 16 字节,即块大小AES.
您可以找到示例代码
格式保留加密 (FPE)
使用保留格式的加密,加密的输出使用确切的位数或偶数作为输入的可能值。这听起来不错,但 FPE 由一堆相对复杂的算法组成。基本上你必须在 JavaScript 中找到一个加密库来实现它。
请注意,使用 FPE,相同的消息将始终加密为相同的密文。这可能是问题,也可能不是问题,具体取决于应用程序。
备注:
- 您正在使用十六进制编码,这不是最密集的编码,您可以使用 base 64 甚至 ASCII 85 编码。
- 首先将IV和密文连接为字节然后进行编码会更高效。如果您有 IV 的静态大小,您可以简单地使用常量而不是分隔符。
- 您的密钥应由随机字节组成,而不是文本字符串。需要使用密码哈希转换密码。
您的代码所做的更改是:
- 使用 Base64 而不是 Hex 进行编码。这 space 效率更高。只使用缓冲区而不创建字符串会更好;那么我们可以有一个 9 字节的随机数而不是 5 字节的随机数。
- 去掉IV/nonce和密文之间的分隔符。我们知道 IV/nonce 有多长;我们不需要分隔符。
- 使用CTR模式而不是CBC模式。这使得输出长度等于输入长度。
- 使用随机数而不是 IV。从 2^40 space 中随机选择随机数。 (随机选择 CTR 随机数通常是非常危险的。请参阅下文了解为什么它 可能 在您的用例中是可以接受的;它仍然不被推荐。)
- 通过添加 PBKDF2 修复密码生成问题(您也可以只使用 randomBytes)。您的密码非常不安全。它是一个 ASCII 字符串,这意味着它代表 AES-256 密钥的一小部分space。在密钥的 0.000000000002% 的数量级 space。这就是它比 AES-256 安全得多。
正如 Maarten 所说,CTR 模式复制 Key+Nonce 对是非常危险的。如果有人这样做,他们就可以学习两个原始消息的 XOR。这样,他们就有很好的机会解密这两条消息。例如,如果您在两条消息上复制了您的密钥+随机数,并且攻击者使用它来发现它们的 XOR 是 3 并且知道加密文本是大写字母,那么他们就会知道这两条消息必须是其中之一:
[('A', 'B'), ('D', 'G'), ('E', 'F'), ('H', 'K'), ('I', 'J'), ('L', 'O'),
('M', 'N'), ('P', 'S'), ('Q', 'R'), ('T', 'W'), ('U', 'V')]
这种信息对于人类语言或计算机协议等结构化数据来说是毁灭性的。它可以很快用于解密整个消息。 Key+nonce 复用就是这样WEP was broken。 (当您手动执行此操作时,它基本上与解决您在报纸上找到的密码谜题相同。)加密数据的随机性越大,它提供的上下文越少,它的功能就越弱。
使用随机的 5 字节随机数,在大约 130 万次加密后发生冲突的可能性为 50%。使用随机的 8 字节随机数,在大约 5.3B 加密后发生冲突的可能性为 50%。 sqrt(pi/2 * 2^bits)
用密码学术语来说,这是一个完全破解的过程。它可能足以满足您的目的,也可能不足以满足您的目的。要正确地做到这一点(我在下面没有这样做),正如 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)