EVP_DecryptFinal_ex:bad decrypt when using Node.js
Asked Answered
H

4

17

Using the following node js:

var crypto = require('crypto');
var encrypt = function (input, password, callback) {
    var m = crypto.createHash('md5');
    m.update(password);
    var key = m.digest('hex');

    m = crypto.createHash('md5');
    m.update(password + key);
    var iv = m.digest('hex');
    console.log(iv);

    var data = new Buffer(input, 'utf8').toString('binary');

    var cipher = crypto.createCipheriv('aes-256-cbc', key, iv.slice(0,16));
    var encrypted = cipher.update(data, 'binary') + cipher.final('binary');
    var encoded = new Buffer(encrypted, 'binary').toString('base64');
    callback(encoded);
};

var decrypt = function (input, password, callback) {
    // Convert urlsafe base64 to normal base64
    input = input.replace(/\-/g, '+').replace(/_/g, '/');
    // Convert from base64 to binary string
    var edata = new Buffer(input, 'base64').toString('binary');

    // Create key from password
    var m = crypto.createHash('md5');
    m.update(password);
    var key = m.digest('hex');

    // Create iv from password and key
    m = crypto.createHash('md5');
    m.update(password + key);
    var iv = m.digest('hex');

    // Decipher encrypted data
    var decipher = crypto.createDecipheriv('aes-256-cbc', key, iv.slice(0,16));
    var decrypted = decipher.update(edata, 'binary') + decipher.final('binary');
    var plaintext = new Buffer(decrypted, 'binary').toString('utf8');

    callback(plaintext);
};

To execute I ran this:

encrypt("uWeShxRrCKyK4pcs", "secret", function (encoded) {
    console.log(encoded);
    decrypt(encoded, "secret", function (output) {
        console.log(output);
    });
});

Encrypting seems to work fine, but when I try to decrypt, I receive the following error:

Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt at Error (native) at Decipheriv.Cipher.final (crypto.js:202:26)

I am pretty new to cryptography, so don't really know why I am receiving this error. I just need to get it fixed for now.

Hammer answered 23/6, 2016 at 16:42 Comment(5)
1. MD5 shoukld not be used, it is not secure nor is simple hashing of a password sufficiently secure. 2. Iterate over an HMAC with a random salt for about a 100ms duration (the salt needs to be saved with the hash). Use functions such as password_hash, PBKDF2, Bcrypt or similar functions. The point is to make the attacker spend a lot of time finding passwords by brute force. See OWASP (Open Web Application Security Project) Password Storage Cheat Sheet.Cheekpiece
Thanks, but I have no idea how to change my code to do that. I will try to look at bcrypt.Hammer
Time to debug. Hex dump the key and iv on both encryption and decryption and verify that they are correct and the correct length. The AES key should be 32-bytes and the iv 16-bytes, exactly. Hex dump the encrypted immediately after encryption and again immediately prior to decryption, verify that they are the same.Cheekpiece
If this is for a serious app with many users you need to get a domain expert to at least review the code and how the encryption is used. Encryption security is very hard to get correct even for veteran developers.Cheekpiece
Everything seems to be correct. What I noticed is that if I use a shorter password than 16 then everything works. It seems to be related to this issue: #33586560 however my code already has that the suggested changes in the answer.Hammer
E
11

You mixed up two different encodings. See

cipher.update(data[, input_encoding][, output_encoding])

and

cipher.final([output_encoding])

and now look at

var encrypted = cipher.update(data, 'binary') + cipher.final('binary');

Here the square brackets denote an optional function input. If you pass more values than are required then the additional values will be matched to the optional inputs from the left.

It should be

var encrypted = cipher.update(data, 'binary', 'binary') + cipher.final('binary');

The issue is that cipher.update(data, 'binary') outputs a buffer which automatically stringifies to a Hex-encoded string instead of a "binary"-string.


Anyway, there is so much wrong with this code that you should start over and simply use an existing library that is highly opinionated.

  • You must have a random IV which is prepended to the ciphertext in order to reach semantic security.

  • A password has low entropy and cannot be used as a key. A single MD5 invocation doesn't change that fact. Key derivation from a password is supposed to be slow, so use a known scheme such as PBKDF2, bcrypt, scrypt or Argon2 (increasing security) with a high iteration count/cost factor. Don't forget the salt.

  • Authenticate your ciphertext with a message authentication code such as HMAC-SHA256 in an encrypt-then-MAC scheme. Otherwise, an attacker may manipulate ciphertexts and you won't even be able to detect changes. First step to losing data with a padding oracle attack.

Ericaericaceous answered 27/6, 2016 at 18:31 Comment(0)
K
6

As this question is the first one to appear on Google, here is another solution.

lifesaver bad decrypt

In case the link goes down, on my case, I had to update the decrypt function to add

function decrypt(text) {
    let iv = Buffer.from((text.split(':')[1]).split('=')[0], 'hex')//will return iv;
    let enKey = Buffer.from(text.split('=')[1], 'hex')//will return key;
    let encryptedText = Buffer.from(text.split(':')[0], 'hex');//returns encrypted Data
    let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(enKey), iv);
// Added this line here
    decipher.setAutoPadding(false);
    let decrypted = decipher.update(encryptedText);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
    //returns decryptedData
}
Koodoo answered 14/6, 2021 at 7:29 Comment(2)
Although it removed the error, it didn't solve the root cause. Now I don't get the expected result from decrypted.toString()Remuneration
Link is broken/removed.Rsfsr
B
0

a most recent working example. be careful that keep the key same . my mistake was that was generating key on every call.

var express = require("express");
var router = express.Router();
var crypto = require("crypto");
/* GET users listing. */

const algorithm = "aes-256-cbc";
const key = "loC3dzXsvgCips3Q6jHa6hjmeaUr4Eak";

router.post("/encrypt", function (req, res, next) {
  var mystr = encrypt(`${req.body.message}`);
  res.json({ encrypted_message: mystr });
});

router.post("/decrypt", function (req, res, next) {
  var mystr = req.body;
  let data = decrypt(mystr);
  res.json({ plan_text: data });
});
module.exports = router;

function encrypt(text) {
  const iv = crypto.randomBytes(16);
  let cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv);
  let encrypted = cipher.update(text);
  encrypted = Buffer.concat([encrypted, cipher.final()]);
  return { iv: iv.toString("hex"), encryptedData: encrypted.toString("hex") };
}

function decrypt(text) {
  let iv = Buffer.from(text.iv, "hex");
  let encryptedText = Buffer.from(text.encryptedData, "hex");
  let decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv);
  let decrypted = decipher.update(encryptedText);
  decrypted = Buffer.concat([decrypted, decipher.final()]);
  return decrypted.toString();
}
Bellbella answered 3/2, 2023 at 12:36 Comment(0)
U
-3

I found out the reason for this, is using different key's or iv's for encryption & decryption. We must use the same key & iv used for encryption, for decrypting the content. An only workaround is to save the iv & key in an array used during encrypting the data or concatenating iv & key with the encrypted data with the help of separators

Example One:

function encrypt(text) {
    let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
    let encrypted = cipher.update(text);
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return encrypted.toString('hex') + ':' + iv.toString('hex') + '=' + 
    key.toString('hex');
    //returns encryptedData:iv=key
}

function decrypt(text) {
    let iv = Buffer.from((text.split(':')[1]).split('=')[0], 'hex')//will return iv;
    let enKey = Buffer.from(text.split('=')[1], 'hex')//will return key;
    let encryptedText = Buffer.from(text.split(':')[0], 'hex');//returns encrypted Data
    let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(enKey), iv);
    let decrypted = decipher.update(encryptedText);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
    //returns decryptedData
}

Example Two:

function encrypt(text) {
    let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
    let encrypted = cipher.update(text);
    encrypted = Buffer.concat([encrypted, cipher.final()]);
    return { 
        "encryptedData": encrypted.toString('hex'),
        "iv" : iv.toString('hex'),
        "key" : key.toString('hex');
    //returns an Array of key, iv & encryptedData
  }
}

function decrypt(text) {
    let iv = Buffer.from((text.iv, 'hex')//will return iv;
    let enKey = Buffer.from(text.key, 'hex')//will return key;
    let encryptedText = Buffer.from(text.encryptedData, 'hex');//returns encrypted Data
    let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(enKey), iv);
    let decrypted = decipher.update(encryptedText);
    decrypted = Buffer.concat([decrypted, decipher.final()]);
    return decrypted.toString();
    //returns decryptedData
}
Ununa answered 27/5, 2019 at 3:27 Comment(1)
Wait what? You're storing the key along with the ciphertext. This does not provide any security but rather just simple obfuscation, since the key to do the decryption is right there with the ciphertext. The IV is not supposed to be secret and can be stored alongside the ciphertext. The key on the other hand is the only thing that has to be kept secret.Ericaericaceous

© 2022 - 2024 — McMap. All rights reserved.