Crypto.decipher.final 对于 'aes-256-cbc' 具有无效密钥的算法因错误解密而失败
Crypto.decipher.final for 'aes-256-cbc' algorithm with invalid key fails with bad decrypt
我可以使用 node.js 加密模块使用 Cipher 和 Decipher 类 和 'aes-256-cbc' 算法加密和解密消息,如下所示:
var crypto = require('crypto');
var cipherKey = crypto.randomBytes(32); // aes-256 => key length is 256 bits => 32 bytes
var cipherIV = crypto.randomBytes(16); // aes block size = initialization vector size = 128 bits => 16 bytes
var cipher = crypto.createCipheriv('aes-256-cbc', cipherKey, cipherIV);
var message = 'Hello world';
var encrypted = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
console.log('Encrypted \'' + message + '\' as \'' + encrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Encrypted 'Hello world' as '2b8559ce4227c3c3c200ea126cb50957' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'
var decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, cipherIV);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
console.log('Decrypted \'' + encrypted + '\' as \'' + decrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Decrypted '2b8559ce4227c3c3c200ea126cb50957' as 'Hello world' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'
然而,当我尝试使用错误的密钥解密消息时,也许天真地证明攻击者将无法解密消息,除非知道密钥,我得到 Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt at Decipheriv.final (internal/crypto/cipher.js:164:28)
:
var differentCipherKey = crypto.randomBytes(32);
var decipherDifferentKey = crypto.createDecipheriv('aes-256-cbc', differentCipherKey, cipherIV);
decrypted = decipherDifferentKey.update(encrypted, 'hex', 'utf8') + decipherDifferentKey.final('utf8');
我希望得到的是难以理解的文字。 bad decrypt
在其他 SO 问题中有特色,要么是关于加密和解密之间的 openssl 版本不匹配,要么是同一情况下的初始化向量太短,但我相信我的情况是不同的情况。 AES 是否以某种方式知道加密文本是使用不同的密钥生成的?
在 Windows 10 上的节点 v12.13.0 以及 repl.it 运行 v10.16.0.
中进行了测试
编辑:
正如答案中所建议的那样,问题出在默认填充上,为了看到无法理解的输出,需要禁用密码和解密器上的自动填充并手动填充:
var requirePadding = 16 - Buffer.byteLength(message, 'utf8');
var paddedMessage = Buffer.alloc(requirePadding, 0).toString('utf8') + message;
cipher.setAutoPadding(false)
CBC模式需要padding,你没有定义,库默认给你加了一个。默认值为 PKCS7Padding,它支持从 1 到最多 256 字节的块大小。
每个填充都有特定的格式,因此可以从解密的文本中唯一地删除它而不会产生歧义。例如,如果明文缺少两个字符以匹配块大小,在 AES 中为 16 字节,则 PKCS7 填充添加 0202
(十六进制)表示添加了 2 个字符,每个字符的值作为添加的数量人物。如果少了5个0505050505
,等等下面的xy
是一个字节。
xyxyxyxyxyxyxyxyxyxyxyxyxyxyxy01
xyxyxyxyxyxyxyxyxyxyxyxyxyxy0202
xyxyxyxyxyxyxyxyxyxyxyxyxy030303
...
xyxy0E0E0E0E0E0E0E0E0E0E0E0E0E0E
xy0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F
如果最后一个块是一个完整的块,一个完全填充的新块
xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxy 10101010101010101010101010101010
解密后,首先检查填充。如果填充的格式不符合 rfc 2315 中指定的正确格式,则可以说存在填充错误。
在这种情况下,解密库时会检查填充并就此向您发出警告。为了防止填充 oracle 攻击,您不会收到错误的填充警告。你得到一个错误的解密。
库知道密钥结果是否具有有效填充,仅此而已。即使在完整性有帮助的情况下,即使概率可以忽略不计,也可能有不止一个密钥产生有效填充。
在现代密码学中,我们不再使用 CBC 模式。我们更喜欢 AES-GCM 或 ChaCha20-Poly1305 等认证加密 (AE) 模式。 AE 模式在捆绑包中提供机密性、完整性和身份验证。
伽罗瓦计数器模式 (GCM),内部使用 CTR 模式,其中没有填充,因此它们不受填充 oracle 攻击。
已正确地将问题识别为填充问题。我可能会这样总结这个问题:
- 块密码只能对长度为密码块大小的倍数的数据进行操作。 (AES 的块大小为 128 位。)
- 为了使各种大小的输入符合块大小,库添加了填充。此填充具有特定格式(例如,当添加长度为 N 的填充时,对输入的最后 N 个字节重复值
N
。)
- 解密时,库会检查是否存在正确的填充。由于您的严重解密数据是任意噪声,因此不太可能有有效的填充。
您可以通过使用 setAutoPadding
的 decipher.setAutoPadding(false)
before you do update
. However, note that this will include the padding in your decrypted output. Here is a modified repl.it instance 关闭此检查。
我可以使用 node.js 加密模块使用 Cipher 和 Decipher 类 和 'aes-256-cbc' 算法加密和解密消息,如下所示:
var crypto = require('crypto');
var cipherKey = crypto.randomBytes(32); // aes-256 => key length is 256 bits => 32 bytes
var cipherIV = crypto.randomBytes(16); // aes block size = initialization vector size = 128 bits => 16 bytes
var cipher = crypto.createCipheriv('aes-256-cbc', cipherKey, cipherIV);
var message = 'Hello world';
var encrypted = cipher.update(message, 'utf8', 'hex') + cipher.final('hex');
console.log('Encrypted \'' + message + '\' as \'' + encrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Encrypted 'Hello world' as '2b8559ce4227c3c3c200ea126cb50957' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'
var decipher = crypto.createDecipheriv('aes-256-cbc', cipherKey, cipherIV);
var decrypted = decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
console.log('Decrypted \'' + encrypted + '\' as \'' + decrypted + '\' with key \''+ cipherKey.toString('hex') + '\' and IV \'' + cipherIV.toString('hex') + '\'');
// Outputs: Decrypted '2b8559ce4227c3c3c200ea126cb50957' as 'Hello world' with key '50f7a656cfa3c4f90796a972b2f6eedf41b589da705fdec95b9d25c180c16cf0' and IV '6b28c13d63af14cf05059a2a2caf370c'
然而,当我尝试使用错误的密钥解密消息时,也许天真地证明攻击者将无法解密消息,除非知道密钥,我得到 Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt at Decipheriv.final (internal/crypto/cipher.js:164:28)
:
var differentCipherKey = crypto.randomBytes(32);
var decipherDifferentKey = crypto.createDecipheriv('aes-256-cbc', differentCipherKey, cipherIV);
decrypted = decipherDifferentKey.update(encrypted, 'hex', 'utf8') + decipherDifferentKey.final('utf8');
我希望得到的是难以理解的文字。 bad decrypt
在其他 SO 问题中有特色,要么是关于加密和解密之间的 openssl 版本不匹配,要么是同一情况下的初始化向量太短,但我相信我的情况是不同的情况。 AES 是否以某种方式知道加密文本是使用不同的密钥生成的?
在 Windows 10 上的节点 v12.13.0 以及 repl.it 运行 v10.16.0.
中进行了测试编辑: 正如答案中所建议的那样,问题出在默认填充上,为了看到无法理解的输出,需要禁用密码和解密器上的自动填充并手动填充:
var requirePadding = 16 - Buffer.byteLength(message, 'utf8');
var paddedMessage = Buffer.alloc(requirePadding, 0).toString('utf8') + message;
cipher.setAutoPadding(false)
CBC模式需要padding,你没有定义,库默认给你加了一个。默认值为 PKCS7Padding,它支持从 1 到最多 256 字节的块大小。
每个填充都有特定的格式,因此可以从解密的文本中唯一地删除它而不会产生歧义。例如,如果明文缺少两个字符以匹配块大小,在 AES 中为 16 字节,则 PKCS7 填充添加 0202
(十六进制)表示添加了 2 个字符,每个字符的值作为添加的数量人物。如果少了5个0505050505
,等等下面的xy
是一个字节。
xyxyxyxyxyxyxyxyxyxyxyxyxyxyxy01
xyxyxyxyxyxyxyxyxyxyxyxyxyxy0202
xyxyxyxyxyxyxyxyxyxyxyxyxy030303
...
xyxy0E0E0E0E0E0E0E0E0E0E0E0E0E0E
xy0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F
如果最后一个块是一个完整的块,一个完全填充的新块
xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxy 10101010101010101010101010101010
解密后,首先检查填充。如果填充的格式不符合 rfc 2315 中指定的正确格式,则可以说存在填充错误。
在这种情况下,解密库时会检查填充并就此向您发出警告。为了防止填充 oracle 攻击,您不会收到错误的填充警告。你得到一个错误的解密。
库知道密钥结果是否具有有效填充,仅此而已。即使在完整性有帮助的情况下,即使概率可以忽略不计,也可能有不止一个密钥产生有效填充。
在现代密码学中,我们不再使用 CBC 模式。我们更喜欢 AES-GCM 或 ChaCha20-Poly1305 等认证加密 (AE) 模式。 AE 模式在捆绑包中提供机密性、完整性和身份验证。
伽罗瓦计数器模式 (GCM),内部使用 CTR 模式,其中没有填充,因此它们不受填充 oracle 攻击。
- 块密码只能对长度为密码块大小的倍数的数据进行操作。 (AES 的块大小为 128 位。)
- 为了使各种大小的输入符合块大小,库添加了填充。此填充具有特定格式(例如,当添加长度为 N 的填充时,对输入的最后 N 个字节重复值
N
。) - 解密时,库会检查是否存在正确的填充。由于您的严重解密数据是任意噪声,因此不太可能有有效的填充。
您可以通过使用 setAutoPadding
的 decipher.setAutoPadding(false)
before you do update
. However, note that this will include the padding in your decrypted output. Here is a modified repl.it instance 关闭此检查。