4

I have an application running on php which have some values encrypted using openssl encrption by using the code below

<?php
define('OSSLENCKEY','14E2E2D1582A36172AE401CB826003C1');
define('OSSLIVKEY', '747E314D23DBC624E971EE59A0BA6D28');

function encryptString($data) {
    $encrypt_method = "AES-256-CBC";
    $key = hash('sha256', OSSLENCKEY);    
    $iv = substr(hash('sha256', OSSLIVKEY), 0, 16); 
    $output = openssl_encrypt($data, $encrypt_method, $key, 0, $iv);
    $output = base64_encode($output);
    return $output;
}

function decryptString($data){
    $encrypt_method = "AES-256-CBC";
    $key = hash('sha256', OSSLENCKEY);    
    $iv = substr(hash('sha256', OSSLIVKEY), 0, 16);
    $output = openssl_decrypt(base64_decode($data), $encrypt_method, $key, 0, $iv);     
    return $output;
}

echo encryptString("Hello World");
echo "<br>";
echo decryptString("MTZHaEoxb0JYV0dzNnptbEI2UXlPUT09");
?>

I have another endpoint which runs on nodejs where I need to decrypt and encrypt values based on the above php encrypt/decrypt rule. I have searched but could'nt find a solution for this.

I tried with the library crypto But ends up with errors Reference

My nodejs code which I have tried is given below

message         = 'MTZHaEoxb0JYV0dzNnptbEI2UXlPUT09';
const cypher    = Buffer.from(message, "base64");
const key       = crypto.createHash('sha256').update('14E2E2D1582A36172AE401CB826003C1');//.digest('hex');
// $iv          = substr(hash('sha256', '747E314D23DBC624E971EE59A0BA6D28'), 0, 16);  from php  returns '0ed9c2aa27a31693'  need nodejs equivalent
const iv        = '0ed9c2aa27a31693'; 
const decipher  = crypto.createDecipheriv("aes-256-cbc", key, iv);  
console.log( decipher.update(contents) + decipher.final());

Someone please help me to find a nodejs code for openssl encryption and decyption

Thanks in advance

Ajith
  • 2,476
  • 2
  • 17
  • 38

1 Answers1

8

There are the following problems in the code:

  • The key is returned hex encoded in the PHP code, so in the NodeJS code for AES-256 only the first 32 bytes must be considered for the key (PHP does this automatically).
  • The PHP code Base64 encodes the ciphertext implicitly, so because of the explicit Base64 encoding the ciphertext is Base64 encoded twice (which is unnecessary). Therefore, a double Base64 encoding is necessary in the NodeJS code as well.

Also, note that using a static IV is insecure (but you are probably only doing this for testing purposes).

The following NodeJS code produces the same ciphertext as the PHP code:

const crypto = require('crypto');

const plain =  'Hello World';
const hashKey = crypto.createHash('sha256');
hashKey.update('14E2E2D1582A36172AE401CB826003C1');
const key = hashKey.digest('hex').substring(0, 32);
  
const hashIv = crypto.createHash('sha256');
hashIv.update('747E314D23DBC624E971EE59A0BA6D28');
const iv = hashIv.digest('hex').substring(0, 16);
  
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
var encrypted = cipher.update(plain, 'utf-8', 'base64');
encrypted += cipher.final('base64');
encrypted = Buffer.from(encrypted, 'utf-8').toString('base64');
console.log(encrypted); // MTZHaEoxb0JYV0dzNnptbEI2UXlPUT09

encrypted = Buffer.from(encrypted, 'base64').toString('utf-8');
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
var decrypted = decipher.update(encrypted, 'base64', 'utf-8');
decrypted += decipher.final('utf-8');
console.log(decrypted); // Hello World

EDIT:

As mentioned in the comments, PHP's hash() method returns the hash as a hexadecimal string by default (unless the third parameter is explicitly set to true, which is not the case in the reference code). This doubles the length, because in this encoding each byte of the hash is represented by two hex digits (hexits), i.e. 2 bytes.
Therefore it is necessary to shorten the key in the NodeJS code (see the first point of my original answer). This shortening is not necessary in the PHP code, since PHP does this implicitly (which is actually a design flaw, since this way the user does not notice a possible issue with the key).

The use of the hex string has two disadvantages:

  • With a hex encoded string, each byte consists of 16 possible values (0-15), as opposed to 256 possible values of a byte (0-255). This reduces the security from 256 bit to 128 bit (which is arithmetically equivalent to AES-128), see here.
  • Depending on the platform, the hexits a-f can be represented as lowercase or uppercase letters, which can result in different keys and IVs (without explicit agreement on one of the two cases).

For these reasons it is more secure and robust to use the raw binary data of the hash instead of the hex encoded strings. If you want to do this, then the following changes are necessary.

In the PHP code:

$key = hash('sha256', OSSLENCKEY, true);    
$iv = substr(hash('sha256', OSSLIVKEY, true), 0, 16); 

in the NodeJS code:

const key = hashKey.digest();
const iv = hashIv.digest().slice(0, 16)

Note, however, that this version is not compatible with the old one, i.e. encryptions before this change cannot be decrypted after the change. So the old data would have to be migrated.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Works perfectly! Could you please add the decrypt code also :) – Ajith Feb 24 '21 at 07:29
  • 1
    Is completely analog. I have added the decryption code. – Topaco Feb 24 '21 at 07:40
  • I know this may just be pseudo code but I really get alarmed when I see cases like `hashKey.digest('hex').substring(0, 32)` and the key is effectively reduced from 256 bits to 128 bits. Take a look at [three common encryption mistakes that are easy to avoid](https://www.ubiqsecurity.com/blog/three-common-encryption-mistakes-that-are-easy-to-avoid). – Gary Mar 10 '21 at 16:05
  • @Gary - This is about porting the PHP code posted in the question to NodeJS as requested by the OP. By its very nature, design flaws in the reference code can't be fixed in this process (without losing compatibility). In the PHP code, the key is (possibly unknowingly) encoded as hex string (s. the 1. point in my answer) and thus has 64 bytes that are silently truncated by PHP, hence the explicit key truncation in the NodeJS code. I could have shed more light on those issues in the PHP code (and their effects), but that seemed to me beyond the scope (which is certainly a matter of opinion). – Topaco Mar 10 '21 at 18:17
  • @Topaco - I completely understand & agree with your point. I am just trying to raise the awareness that just because something works, it is always right. In this case, from what I understand from your post is that PHP is producing HEX which is then being truncated automatically, thus a 256 bit key is actually just 128 bits. The OP needs to understand that the original implementation is flawed and work from there. Then the Node would have worked perfectly. – Gary Mar 11 '21 at 16:34
  • @Gary - The issue is described in your article as Common Mistake #1. The user misapplies the PHP `hash()` function so that instead of binary data a hex string is returned, i.e. in the case of SHA256 64 bytes. For AES-256 the PHP `openssl_encrypt()` function truncates the key to the required 32 bytes. The user does not notice this, because PHP does this silently and does not display an error message. Since each byte in the truncated key corresponds to a hexit, it can only have 16 (instead of 256) values, see [here](https://paiza.io/projects/rNbSLkM4rJ8QhYs_yrMfaQ). – Topaco Mar 11 '21 at 19:40
  • @Gary - This is a common problem. I have already pointed it out in one or another answer, see e.g. [here](https://stackoverflow.com/a/65513035/9014097), last paragraph: _In addition, using the hexadecimal string as a key weakens the same, since each byte is reduced to the 16 values of the hexadecimal number system..._ You could elaborate on this in a separate answer (or alternatively ask a new question that you answer yourself), which could then be used as a reference on SO in the future. – Topaco Mar 11 '21 at 19:41
  • @Topaco What is wrong in my php code? Does it affect the system in future? – Ajith Mar 16 '21 at 05:00
  • @Ajith - please see the edit section of my answer. – Topaco Mar 16 '21 at 08:32
  • @Topaco I understood what you mean. However I cannot make the changes at the moment since in my system there is already a lot of entries with old encryption. I hope my encryption/ decryption in the question will work with minimal level of security (provided hackers cant break it) . Is that so? – Ajith Mar 17 '21 at 05:33
  • 1
    @Ajith - Only you can answer this question based on the _facts_ and your _requirements_. As described in detail in the [linked article](https://www.ubiqsecurity.com/blog/three-common-encryption-mistakes-that-are-easy-to-avoid), the security is reduced from a 256 bit to a 128 bit key. 128 bit is considered to be sufficiently secure today, but on the other hand it is more likely to be broken in the future than 256 bit, see e.g. [here](https://security.stackexchange.com/q/14068). You have to decide for yourself if the additional security is worth the effort of a data migration. – Topaco Mar 17 '21 at 07:28
  • @Topaco Now It makes sense for me. Thanks for this discussion – Ajith Mar 17 '21 at 10:40
  • Thank you give me this very awesome example! – LianSheng May 12 '22 at 17:41