如何导出 IV 和 crypto.createCipheriv 的密钥进行解密?

How to derive IV and key to crypto.createCipheriv for decryption?

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

node.js 加密函数 createDecipher 说:

The implementation of crypto.createDecipher() derives keys using the OpenSSL function EVP_BytesToKey with the digest algorithm set to MD5, one iteration, and no salt.

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

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

In line with OpenSSL's recommendation to use PBKDF2 instead of EVP_BytesToKey it is recommended that developers derive a key and IV on their own using crypto.pbkdf2() and to use crypto.createDecipheriv() to create the Decipher object.

好的,听起来不错。我需要解密的数据已使用 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 的东西。

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

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

相同的结果

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

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

有关详细信息,请参阅 this GitHub repository 中的示例。我在下面包含了 NodeJS,这是 NodeJS 中 best-practice 现代加密的示例:

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() ]);
}