I have seen other questions which ask about creating the initialization vector (IV) for encryption and it seems using a random value is one option. However, I need to generate the IV for decryption, so I have to use the same one that the data was encrypted with based on some salt.
The node.js crypto function createDecipher says:
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.
For backwards compatibility with assets encrypted by other software, I need a different number of iterations, and a salt that I specify.
Continuing to read the documentation, it further says:
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.
Ok, that sounds good. The data I need to decrypt was encrypted using EVP_BytesToKey to get the key and IV, so I need to be compatible with that, though.
Anyway, the crypto.pbkdf2 function appears to take all the parameters I need it to, but the problem is, it does not appear to create an initialization vector.
The corresponding C code which did the decryption which this needs to be compatible with looks like this:
// 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);
My attempt to use crypto.pbkdf2
to replicate this behavior:
crypto.pbkdf2(password, salt, nrounds, 32, "md5", (err, derivedKey) => {
if (err) throw err
console.log(derivedKey.toString("hex"))
})
The derivedKey
also does not match the key produced by the C code above. I'm not sure if that's even expected! I also tried key lengths of 48 and 64 but those didn't generate anything similar to the expected key and IV either.
Given the correct password, salt, and hashing rounds, how do I generate the same key and IV to decrypt with?
createCipheriv() method will first create and then return the cipher object as per the algorithm passed for the given key and authorization factor (iv).
You need to use the same IV for encryption and decryption.
You don't need to keep the IV secret, but it must be random and unique. The IV should also be protected against modification. If you authenticate the ciphertext (e.g. with a HMAC) but fail to authenticate the IV, an attacker can abuse the malleability of CBC to arbitrarily modify the first block of plaintext.
Definition of crypto. createDecipheriv() method is an inherent application programming interface (API) of the crypto module, which is used to create and return a Decipher object with the stated algorithm, key, and initialization vector.
To start, the reason you are not getting your desired result is because the C code you have does use EVP_BytesToKey
, whereas your NodeJS code uses PBKDF2. I think you may have misunderstood the recommendation of OpenSSL. They recommend PBKDF2, not as a better way to produce the same result, but as a better way to solve the problem. PBKDF2 is simply a better key derivation function, but it will not produce the same result as EVP_BytesToKey
.
Further, the way you are handling your IV generation in the first place is quite poor. Using a KDF to generate your key is excellent, well done. Using a KDF to generate an IV is, frankly, quite a poor idea. Your initial readings, where you found that generating an IV randomly is a good idea, are correct. All IVs/nonces should be generated randomly. Always. The important thing to keep in mind here is that an IV is not a secret. You can pass it publicly.
Most implementations will randomly generate an IV and then prefix it to the ciphertext. Then, when it comes to decrypting, you can simply remove the first 128-bits (AES) worth of bytes and use that as the IV. This covers all your bases and means you don't have to derive your IV from the same place as the key material (which is yucky).
For further information, see the examples in this GitHub repository. I have included the NodeJS one below, which is an example of best-practice modern encryption in 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() ]);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With