10

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.

jww
  • 83,594
  • 69
  • 338
  • 732
Sonu Kapoor
  • 1,357
  • 2
  • 15
  • 32
  • 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](https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Leverage_an_adaptive_one-way_function). – zaph Jun 23 '16 at 16:57
  • Thanks, but I have no idea how to change my code to do that. I will try to look at bcrypt. – Sonu Kapoor Jun 23 '16 at 16:59
  • 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. – zaph Jun 23 '16 at 17:05
  • 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. – zaph Jun 23 '16 at 17:06
  • 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: http://stackoverflow.com/questions/33586060/error-when-decrypt-input-data-longer-than-15-chars-with-aes256-and-node-js?rq=1 however my code already has that the suggested changes in the answer. – Sonu Kapoor Jun 23 '16 at 17:34

2 Answers2

8

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');

but 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.

Artjom B.
  • 58,311
  • 24
  • 111
  • 196
-1

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
}
  • 5
    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. – Artjom B. Nov 04 '19 at 19:45