Decrypting outside Laravel

Published 3 weeks ago by stevedgriffiths

I am trying to decrypt a string that was previously encrypted by Laravel and stored in a DB from a Node.JS application.

I am having a problem with the IV. I have retrieved the encrypted string from the DB, base64 decoded this and JSON parsed the result to get an object with the value, IV and MAC. However, the IV value is not 16 characters long, and running another base 64 decode on this results in garbage text. I'm not sure what step I need to take to obtain a valid IV, so I can pass this to the Node.js decrypt (along with the app key and value for decryption) in order to complete the decryption.

Best Answer (As Selected By stevedgriffiths)
stevedgriffiths

I finally cracked it!

It was a matter of ensuring that the encoding parameters passed to crypto were explicit and accurate - it was treating some strings as buffers and vice versa. Below is my full function in case anyone wants it. There is an app variable which holds the encryption key and also provides a caching layer for previously decrypted values. This could be improved by verifying the MAC code as Laravel itself does when decrypting.

If you are using the app key directly from Laravel, please also take note of @georgeinggs comment

var crypto = require('crypto');
var Base64 = require('js-base64').Base64;
var serialize = require('php-serialize');

/** Decrypts Laravel encrypted data
 *
 * @type {Function}
 * @param {string} app - Express app instance, must have encryptionKey and decryptionCache defined on it during init
 * @param {function} data - Base64 encoded encryption string as created by Laravel
 * @access public
 */
function decrypt(app, data) {
    // If no data - return blank string
    if (data !== "") {
        // Check localised cache to save time
        if (!app.decryptionCache[data]) {
            try {
                //  Decode and parse required properties
                var b64 = Base64.decode(data);
                var json = JSON.parse(b64);
                var iv = Buffer.from(json.iv, "base64");
                var value = Buffer.from(json.value, "base64");

                // Create decipher
                var decipher = crypto.createDecipheriv("aes-256-cbc", app.encryptionKey, iv);

                // Decrypt
                var decrypted = decipher.update(value, 'binary', 'utf8');
                decrypted += decipher.final('utf8');

                // Unserialize
                unserialized = serialize.unserialize(decrypted);

                // Store in cache
                app.decryptionCache[data] = unserialized;

                return unserialized;
            }
            catch(e) {
                console.log(e);
                return "";
            }
        }
        else {
            // Use cached value
            return app.decryptionCache[data];
        }
    }
    else {
        return "";
    }

}

module.exports = {
    decrypt: decrypt
};
bobbybouwmann

I don't have a node.js application running at hand right now, but I believe you need to take the following steps:

  1. Laravel encrypted string
  2. base64 decode
  3. JSON.parse()
  4. Get the IV
  5. base64 decode that.
  6. Done
stevedgriffiths

That's exactly the steps I'm following right now, but the second base 64 decode of the IV (your step 5) gives me back garbage (a bunch of question marks and apostrophes) and is only a few characters long.

I tried putting the encoded text into an online decode tool and got the same results so I don't think it's a node issue - I feel like I am missing something?

bobbybouwmann

Mmh. I see a script in python here that might give you some ideas on how to do this: https://gist.github.com/fideloper/c4806c504e46e8cdb00a

stevedgriffiths

Thanks for the link - that's definitely the same steps I am trying to follow in Node.js.

I have done some more research and I believe the problem lies in the Node.JS crypto createDecipheriv() method. The IV parameter seems to expect a 16 character string (or equivalent buffer). But base64 decoding the IV that Laravel generates doesn't result in this - it looks more like some sort of bytes that are being mangled when it is converted into a string.

I put some debugging code inside Laravel's own decrypt function and can see that it actually obtains exactly the same IV once decoded, but on passing this to openssl_decrypt() within PHP, it works ok.

As an example, here is a real Laravel IV (still encoded):

xixCYMurLUlUYoFnuqsQLA==

If you base64 decode this, you get:

ƬB`˫-ITb᧺됬 (presumably because the original IV was created by Laravel from some random bytes).

There has to be some other step required to convert this for use with createDecipheriv().

bobbybouwmann

Sorry, but I have no clue on what's going on here!

stevedgriffiths

I finally cracked it!

It was a matter of ensuring that the encoding parameters passed to crypto were explicit and accurate - it was treating some strings as buffers and vice versa. Below is my full function in case anyone wants it. There is an app variable which holds the encryption key and also provides a caching layer for previously decrypted values. This could be improved by verifying the MAC code as Laravel itself does when decrypting.

If you are using the app key directly from Laravel, please also take note of @georgeinggs comment

var crypto = require('crypto');
var Base64 = require('js-base64').Base64;
var serialize = require('php-serialize');

/** Decrypts Laravel encrypted data
 *
 * @type {Function}
 * @param {string} app - Express app instance, must have encryptionKey and decryptionCache defined on it during init
 * @param {function} data - Base64 encoded encryption string as created by Laravel
 * @access public
 */
function decrypt(app, data) {
    // If no data - return blank string
    if (data !== "") {
        // Check localised cache to save time
        if (!app.decryptionCache[data]) {
            try {
                //  Decode and parse required properties
                var b64 = Base64.decode(data);
                var json = JSON.parse(b64);
                var iv = Buffer.from(json.iv, "base64");
                var value = Buffer.from(json.value, "base64");

                // Create decipher
                var decipher = crypto.createDecipheriv("aes-256-cbc", app.encryptionKey, iv);

                // Decrypt
                var decrypted = decipher.update(value, 'binary', 'utf8');
                decrypted += decipher.final('utf8');

                // Unserialize
                unserialized = serialize.unserialize(decrypted);

                // Store in cache
                app.decryptionCache[data] = unserialized;

                return unserialized;
            }
            catch(e) {
                console.log(e);
                return "";
            }
        }
        else {
            // Use cached value
            return app.decryptionCache[data];
        }
    }
    else {
        return "";
    }

}

module.exports = {
    decrypt: decrypt
};
bobbybouwmann

Nice job man ;)

georgeinggs

@stevedgriffiths This is an absolute life saver!!!

The only thing I'd add is that if anyone is using an APP_KEY directly from their Laravel app, keep in mind that you need to do the following:

  • Remove the base64: prefix

  • Decode the key prior to passing it in by doing the following:

// original APP_KEY in Laravel .env is base64:VGhpcyBpc24ndCBhIHJlYWwga2V5LCBzaWxseSE=
// APP_KEY here needs to be cleaned up a bit

var laravelAppKey = 'VGhpcyBpc24ndCBhIHJlYWwga2V5LCBzaWxseSE=';

var key = new Buffer(laravelAppKey, 'base64'); 

  • Finally, make sure to pass the key to the decrypt function as a Buffer, not an Ascii string.

Can't thank you enough for this bit of code, it was amazingly useful exactly when I needed it :)

stevedgriffiths

@georgeinggs thanks for this addition - I think that could help quite a few people so I'm going to link to your reply from my answer post.

Please sign in or create an account to participate in this conversation.