2

I'm having some trouble to get work an AES-256-CTR encrypt/decrypt in PHP, having a previosly encrypted string made with NodeJS crypto.

Here my JS code:

var crypto = require('crypto'),
    algorithm = 'aes-256-ctr',
    password = 'd6F3Efeq';

function encrypt(text){
  var cipher = crypto.createCipher(algorithm,password)
  var crypted = cipher.update(text,'utf8','hex')
  crypted += cipher.final('hex');
  return crypted;
}

function decrypt(text){
  var decipher = crypto.createDecipher(algorithm,password)
  var dec = decipher.update(text,'hex','utf8')
  dec += decipher.final('utf8');
  return dec;
}

Here my PHP code:

$text = "pereira";

echo bin2hex(openssl_encrypt($text, "aes-256-ctr", "d6F3Efeq",OPENSSL_RAW_DATA));

The JS's version return this on encrypt:

148bc695286379

The PHP's version return this on my encrypt test:

2f2ad5bb09fb56

Am I missing something here? Obviosly, I neither can decrypt correctly in PHP.

Thanks in advance.

Esteban M.
  • 174
  • 1
  • 9
  • AES with CTR should not be used with createCipher (it generates the same iv for the same key) rather use createCipheriv() and generate your own iv (which you will therefore know and can then use when decrypting in php) – Alex K. Jan 25 '18 at 11:11
  • Hi, Alex. Yes, I understand that; the problem is that this NodeJS code is from a third-party software and I don't have access to the code to modify it, but I must make my PHP script compatible. – Esteban M. Jan 25 '18 at 11:38
  • @AlexK. how can I know what is the generated iv for given key used by the createCipher function? – Esteban M. Jan 26 '18 at 12:32
  • 1
    It does not seem [documented](https://nodejs.org/api/crypto.html#crypto_crypto_createcipher_algorithm_password_options) but then the same documentation warns you against using this mode because of the iv issue. If you have no alternative you would have to dig into the sources. – Alex K. Jan 26 '18 at 12:38
  • 1
    @EstebanM. It might suffice to change "cbc" to "ctr" in my answer here: https://stackoverflow.com/a/27678978/1816580 If it does, please notify me. – Artjom B. Jan 26 '18 at 20:26

2 Answers2

4

You must set iv (initialization vector) in both side.

Node JS code:

var crypto = require('crypto'),
  password = '1234567890abcdef1234567890abcdef',
  iv = '1234567890abcdef',
  text = "pereira";

function encrypt(iv, text, password){
  var cipher = crypto.createCipheriv('aes-256-ctr', password, iv)
  var crypted = cipher.update(text,'utf8','hex')
  crypted += cipher.final('hex');
  return crypted;
}

function decrypt(iv, text, password){
  var decipher = crypto.createDecipheriv('aes-256-ctr', password, iv)
  var dec = decipher.update(text,'hex','utf8')
  dec += decipher.final('utf8');
  return dec;
}
console.log(encrypt(iv, text, password));

And PHP code:

$text = 'pereira';
$algorithm = 'aes-256-ctr';
$password = '1234567890abcdef1234567890abcdef'; //node js required 32 byte length key
$iv = '1234567890abcdef'; //must be 16 byte length
echo bin2hex(openssl_encrypt($text, $algorithm, $password, OPENSSL_RAW_DATA, $iv));
Ermat Kiyomov
  • 151
  • 1
  • 3
4

Finally I got a solution for my question, thanks to @Alex K and @Artjom B.

As I mention in a comment, I must get a PHP encrypt/descrypt compatible with the NodeJS createCipher because the NodeJS application is a thrid party one.

So, the JS code is like this:

var crypto = require('crypto'),
    algorithm = 'aes-256-ctr',
    password = 'd6F3Efeq';

function encrypt(text){
  var cipher = crypto.createCipher(algorithm,password)
  var crypted = cipher.update(text,'utf8','hex')
  crypted += cipher.final('hex');
  return crypted;
}

function decrypt(text){
  var decipher = crypto.createDecipher(algorithm,password)
  var dec = decipher.update(text,'hex','utf8')
  dec += decipher.final('utf8');
  return dec;
}

I wrote a little PHP class to achieve this encrypt/decrypt job (Based on @Artjon's solution):

class Crypto
{
   const METHOD = 'aes-256-ctr';

   public function encrypt($plaintext, $password, $salt='', $encode = false)
   {
      $keyAndIV = self::evpKDF($password, $salt);

      $ciphertext = openssl_encrypt(
                                       $plaintext,
                                       self::METHOD,
                                       $keyAndIV["key"],
                                       OPENSSL_RAW_DATA,
                                       $keyAndIV["iv"]
                                    );

      $ciphertext = bin2hex($ciphertext);

      if ($encode)
      {
         $ciphertext = base64_encode($ciphertext);
      }

      return $ciphertext;
   }


   public function decrypt($ciphertext, $password, $salt='', $encoded = false)
   {
      if ( $encoded )
      {
         $ciphertext = base64_decode($ciphertext, true);

         if ($ciphertext === false)
         {
            throw new Exception('Encryption failure');
         }
      }

      $ciphertext = hex2bin($ciphertext);
      $keyAndIV   = self::evpKDF($password, $salt);

      $plaintext = openssl_decrypt(
                                       $ciphertext,
                                       self::METHOD,
                                       $keyAndIV["key"],
                                       OPENSSL_RAW_DATA,
                                       $keyAndIV["iv"]
                                    );

      return $plaintext;
   }

   public function evpKDF($password, $salt, $keySize = 8, $ivSize = 4, $iterations = 1, $hashAlgorithm = "md5")
   {
      $targetKeySize = $keySize + $ivSize;
      $derivedBytes  = "";

      $numberOfDerivedWords = 0;
      $block         = NULL;
      $hasher        = hash_init($hashAlgorithm);

      while ($numberOfDerivedWords < $targetKeySize)
      {
         if ($block != NULL)
         {
            hash_update($hasher, $block);
         }

         hash_update($hasher, $password);
         hash_update($hasher, $salt);

         $block   = hash_final($hasher, TRUE);
         $hasher  = hash_init($hashAlgorithm);

         // Iterations
         for ($i = 1; $i < $iterations; $i++)
         {
            hash_update($hasher, $block);
            $block   = hash_final($hasher, TRUE);
            $hasher  = hash_init($hashAlgorithm);
         }

         $derivedBytes .= substr($block, 0, min(strlen($block), ($targetKeySize - $numberOfDerivedWords) * 4));

         $numberOfDerivedWords += strlen($block)/4;
      }

      return array(
                     "key" => substr($derivedBytes, 0, $keySize * 4),
                     "iv"  => substr($derivedBytes, $keySize * 4, $ivSize * 4)
                   );
   }
}

According to the NodeJS documentation, It derives the IV from the password with no salt, so I used an empty string as salt.

I'm using this PHP class in CodeIgniter like this:

$this->load->library("Crypto");

$plain_text = "pereira";
$password   = "d6F3Efeq";

$enc_text = $this->crypto->encrypt($plain_text,$password);
$pla_text = $this->crypto->decrypt($enc_text,$password);

echo $enc_text."<br>";
echo $pla_text;

Both of them (NodeJS and PHP) return the same result for encrypt and decrypt function:

pereira = 148bc695286379

Regards.

Esteban M.
  • 174
  • 1
  • 9
  • Please note that `evpKDF` with only one iteration is quite insecure when the password is short. You should use a password of at least 20 characters in that case. – Artjom B. Jan 29 '18 at 18:43