如何将 IV 和密钥派生到 crypto.createCipheriv 以进行解密?

Posted

技术标签:

【中文标题】如何将 IV 和密钥派生到 crypto.createCipheriv 以进行解密?【英文标题】:How to derive IV and key to crypto.createCipheriv for decryption? 【发布时间】:2018-08-07 20:06:20 【问题描述】:

我看到了其他关于为encryption 创建初始化向量 (IV) 的问题,似乎使用随机值是一种选择。但是,我需要生成 IV 进行解密,因此我必须使用基于某种盐对数据进行加密的相同 IV。

node.js 加密函数createDecipher 说:

crypto.createDecipher() 的实现使用 OpenSSL 函数 EVP_BytesToKey,摘要算法设置为 MD5, 一次迭代,没有盐。

为了向后兼容由其他软件加密的资产,我需要不同的迭代次数和我指定的盐。

继续阅读文档,它进一步说:

根据 OpenSSL 的建议,使用 PBKDF2 代替 EVP_BytesToKey 建议开发者派生出一个密钥和IV 自己使用 crypto.pbkdf2() 并使用 crypto.createDecipheriv() 创建 Decipher 对象。

好的,听起来不错。我需要解密的数据是使用 EVP_BytesToKey 加密以获取密钥和 IV,所以我需要与之兼容。

无论如何,crypto.pbkdf2 function 似乎接受了我需要的所有参数,但问题是,它似乎没有创建初始化向量。

需要兼容的解密对应的C代码如下:

// parameters to function:
// unsigned char *decrypt_salt
// int nrounds
// unsigned char *decrypt_key_data  <- the password
//  int decrypt_key_data_len <- password length

// the following is not initialized before the call to EVP_BytesToKey
unsigned char decrypt_key[32], decrypt_iv[32];

EVP_BytesToKey(EVP_aes_256_cbc(), EVP_md5(), decrypt_salt, decrypt_key_data,
                   decrypt_key_data_len, nrounds, decrypt_key, decrypt_iv);

我尝试使用crypto.pbkdf2 来复制这种行为:

crypto.pbkdf2(password, salt, nrounds, 32, "md5", (err, derivedKey) => 
    if (err) throw err
    console.log(derivedKey.toString("hex"))
)

derivedKey 也与上面的 C 代码生成的密钥不匹配。我不确定这是否是预期的!我还尝试了 48 和 64 的密钥长度,但它们也没有生成与预期密钥和 IV 相似的任何内容。

给定正确的密码、盐和散列轮次,我如何生成相同的密钥和 IV 来解密?

【问题讨论】:

【参考方案1】:

首先,你没有得到你想要的结果的原因是你的 C 代码确实使用了EVP_BytesToKey,而你的 NodeJS 代码使用了 PBKDF2。我想你可能误解了 OpenSSL 的推荐。他们推荐 PBKDF2,不是作为产生相同结果的更好方法,而是作为解决问题的更好方法。 PBKDF2 只是一个更好的密钥派生函数,但它不会产生与EVP_BytesToKey 相同的结果。

此外,您最初处理 IV 代的方式非常糟糕。使用 KDF 生成密钥非常棒,做得很好。坦率地说,使用 KDF 生成 IV 是一个很糟糕的主意。您发现随机生成 IV 是一个好主意的初始读数是正确的。 所有 IV/nonce 应随机生成。总是。这里要记住的重要一点是静脉注射不是秘密。您可以公开传递。

大多数实现会随机生成一个 IV,然后将其作为密文的前缀。然后,在解密时,您可以简单地删除前 128 位 (AES) 的字节并将其用作 IV。这涵盖了您的所有基础,这意味着您不必从与关键材料相同的位置获取 IV(这很糟糕)。

有关详细信息,请参阅this GitHub repository 中的示例。我在下面包含了一个 NodeJS,这是 NodeJS 中最佳实践现代加密的一个示例:

const crypto = require("crypto");

const ALGORITHM_NAME = "aes-128-gcm";
const ALGORITHM_NONCE_SIZE = 12;
const ALGORITHM_TAG_SIZE = 16;
const ALGORITHM_KEY_SIZE = 16;
const PBKDF2_NAME = "sha256";
const PBKDF2_SALT_SIZE = 16;
const PBKDF2_ITERATIONS = 32767;

function encryptString(plaintext, password) 
    // Generate a 128-bit salt using a CSPRNG.
    let salt = crypto.randomBytes(PBKDF2_SALT_SIZE);

    // Derive a key using PBKDF2.
    let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);

    // Encrypt and prepend salt.
    let ciphertextAndNonceAndSalt = Buffer.concat([ salt, encrypt(new Buffer(plaintext, "utf8"), key) ]);

    // Return as base64 string.
    return ciphertextAndNonceAndSalt.toString("base64");


function decryptString(base64CiphertextAndNonceAndSalt, password) 
    // Decode the base64.
    let ciphertextAndNonceAndSalt = new Buffer(base64CiphertextAndNonceAndSalt, "base64");

    // Create buffers of salt and ciphertextAndNonce.
    let salt = ciphertextAndNonceAndSalt.slice(0, PBKDF2_SALT_SIZE);
    let ciphertextAndNonce = ciphertextAndNonceAndSalt.slice(PBKDF2_SALT_SIZE);

    // Derive the key using PBKDF2.
    let key = crypto.pbkdf2Sync(new Buffer(password, "utf8"), salt, PBKDF2_ITERATIONS, ALGORITHM_KEY_SIZE, PBKDF2_NAME);

    // Decrypt and return result.
    return decrypt(ciphertextAndNonce, key).toString("utf8");


function encrypt(plaintext, key) 
    // Generate a 96-bit nonce using a CSPRNG.
    let nonce = crypto.randomBytes(ALGORITHM_NONCE_SIZE);

    // Create the cipher instance.
    let cipher = crypto.createCipheriv(ALGORITHM_NAME, key, nonce);

    // Encrypt and prepend nonce.
    let ciphertext = Buffer.concat([ cipher.update(plaintext), cipher.final() ]);

    return Buffer.concat([ nonce, ciphertext, cipher.getAuthTag() ]);


function decrypt(ciphertextAndNonce, key) 
    // Create buffers of nonce, ciphertext and tag.
    let nonce = ciphertextAndNonce.slice(0, ALGORITHM_NONCE_SIZE);
    let ciphertext = ciphertextAndNonce.slice(ALGORITHM_NONCE_SIZE, ciphertextAndNonce.length - ALGORITHM_TAG_SIZE);
    let tag = ciphertextAndNonce.slice(ciphertext.length + ALGORITHM_NONCE_SIZE);

    // Create the cipher instance.
    let cipher = crypto.createDecipheriv(ALGORITHM_NAME, key, nonce);

    // Decrypt and return result.
    cipher.setAuthTag(tag);
    return Buffer.concat([ cipher.update(ciphertext), cipher.final() ]);

【讨论】:

嘿卢克,只是想澄清你的陈述“所有 IV/nonce 都应该随机生成。总是。”你的意思是总是在加密步骤,或者?我们不能用随机生成的 IV/nonce 解密某些东西,对吗?所以这就是为什么我们应该用密文发送盐和 IV/nonce 的原因。提前致谢! 嗨@Artiom,是的,这是正确的。用于加密的 IV/nonce 应始终随机生成。正如您正确指出的那样,在 解密 期间使用的 IV/nonce 应该与在加密期间使用的相同。

以上是关于如何将 IV 和密钥派生到 crypto.createCipheriv 以进行解密?的主要内容,如果未能解决你的问题,请参考以下文章

Node.js 加密密钥和 iv 匹配 java SecretKeySpec / IvParameterSpec

如何使用 BouncyCastle 解密,使用 GCM Tag 的字符串,IV 字符串和密钥字符串,所有这些都是十六进制的?

使用 PBKDF2 和 AES256 进行加密和解密 - 需要实际示例 - 如何获取派生密钥

如何安全地处理 AES “Key” 和 “IV” 值

javascript 从密码短语计算密钥+ IV(createCipher到createCipheriv迁移脚本)

什么是 openssl iv,为什么我需要密钥和 iv?