Need help with PHP script to migrate data from MCRYPT to OpenSSL in Wordpress

Chris Inman
Chris Inman used Ask the Experts™
on
Using MCRYPT is hindering my move to PHP 7.3. Currently mcrypt works fine for encryption and decryption; however, OpenSSL cannot decrypt my MCRYPT’ed data. So it appears that I need to run some kind of script that will get the 3 post_meta fileds from each post, decrypt them using mcrypt, then re-encrypt them with OpenSSL. Further “day-forward encryption will be done using OpenSSL.

I’ve attempted many options for decrypting the MCRYPT’ed data with OpenSSL and they have all returned NULL.
So I’m giving up on that direction.

I think an ETL like script would work to run through the posts (they are (CPT) Custom Post Types called formlead), see if they have encrypted data in the 3 fields (to which most of them will) , then decrypt each field using current MCRYPT method into new vars, then re-encrypt into new OpenSSL method, then write this back to database with the same postID. Then the new decryption method could read the data when needed in the future.

However, I’m not the greatest WordPress PHP coder. Can someone help with the script please? I could load this as a page and specify date range to make sure I’m not overloading the server. There’s about150k posts with 8 million + postmeta rows.

I would be doing this against a local copy of the DB and not production to test. Then I would snapshot the production DB and run against it after hours.

Mock code:
<?php

vars = manually set date range

get posts within date range

while have_posts

get post ID
get postmeta
if field1 = true
use current decryption to get filed1 data and decrypt with current decryption and assign to new var
if field2 = true
use current decryption to get filed2 data and decrypt with current decryption and assign to new var
if field3 = true
use current decryption to get filed3 data and decrypt with current decryption and assign to new var

if decrypt1 = true
use new method to Encrypt filed1 data and and assign to new var
if decrypt2 = true
use new method to Encrypt filed1 data and and assign to new var
if decrypt3 = true
use new method to Encrypt filed1 data and and assign to new var

update_post_meta for newly encrypted fileds

Notify of status every 10 posts processed++ - echo “10 posts complete”

Notify when complete - echo “process complete”

Open in new window



Current Decrypt
function crypt_decrypt($key, $algo = MCRYPT_BLOWFISH, $data){

    $key = substr($key, 0, mcrypt_get_key_size($algo, MCRYPT_MODE_ECB));
    $algo = $algo;
    if(!$data){
	return '';
    }

    $crypt = base64_decode($data);

    //Optional Part, only necessary if you use other encryption mode than ECB
    $iv_size = mcrypt_get_iv_size($algo, MCRYPT_MODE_ECB);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);

    $decrypt = mcrypt_decrypt($algo, $key, $crypt, MCRYPT_MODE_ECB, $iv);
    return trim($decrypt);

}

Open in new window


NEW Encrypt: (Sample for Encrypting cookies)
$method = 'AES-256-CBC';  <— Method passed in
function crypt_cookies_encrypt($key, $method, $data){

    $ivSize = openssl_cipher_iv_length($method);
    $iv = openssl_random_pseudo_bytes($ivSize);

    $encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);

    // For storage/transmission, we simply concatenate the IV and cipher text
    $encrypted = base64_encode($iv . $encrypted);

    return $encrypted;
}

Open in new window

Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®

Commented:
So to be clear, the problem you're facing is that the mcrypt version of decrypt won't work? That it keeps returning nulls?

What does the encrypt look like for whatever did the encrypt originally?
Chris InmanOne-man IT Show!

Author

Commented:
Thank you for the reply.

Using MCRYPT is hindering my move to PHP 7.3. Currently mcrypt works fine for encryption and decryption; however, OpenSSL cannot decrypt my MCRYPT’ed data. So it appears that I need to run some kind of script that will get the 3 post_meta fileds from each post, decrypt them using mcrypt, then re-encrypt them with OpenSSL. Further “day-forward encryption will be done using OpenSSL.
David FavorFractional CTO
Distinguished Expert 2018

Commented:
1) I’ve attempted many options for decrypting the MCRYPT’ed data with OpenSSL and they have all returned NULL.

This will never work because...

mcrypt != openssl

2x different beasts.

2) Just write a short custom script to use mcrypt decryption + then openssl encryption to rewrite all your data.

3) Once you have this working, take your production system offline + run your script against the database, then bring your site back online only using openssl encrypt/decrypt functions.
Should you be charging more for IT Services?

Do you wonder if your IT business is truly profitable or if you should raise your prices? Learn how to calculate your overhead burden using our free interactive tool and use it to determine the right price for your IT services. Start calculating Now!

Chris InmanOne-man IT Show!

Author

Commented:
Thanks David,

My issue is writing that short custom script.

php script > my skillset

Commented:
While David is somewhat correct that mcrypt is not openssl, they do perform many similar functions and there shouldn't really be an issue with OpenSSL decrypting data encrypted with mcrypt. The OpenSSL extension should provide virtually everything that the mcrypt extension does AND more.

Where mcrypt focuses only on symmetric encryption, OpenSSL can do symmetric encryption AND asymmetric encryption (PKI / public-private key).

Neither one implements functionality that is SPECIFIC to that extension. There is no special "mcrypt-only" encryption, nor any special "OpenSSL-only" encryption. They both implement industry-standard ciphers/methods. So really the main thing is just making sure that you're using the SAME parameters in both situations.

That means:
1. Same padding.
2. Same cipher.
3. Same key.
4. Same keygen (if applicable).
5. Same encoding and IV placement.
6. Same block chain method (e.g. CBC/ECB/etc)

The reason I was asking about your specific issue is because you've shown us the function definition for your mcrypt decryption function, which has a parameter for algorithm, and it defaults to Blowfish and uses ECB, where-as you've shown an OpenSSL encryption function that is geared up to do AES-256-CBC.

So if you're not overriding the optional $algo parameter in your "crypt_decrypt" custom function you've shown above, AND if it decrypts successfully, then that means your data is encrypted with Blowfish, which is NOT AES.

So if you want a version of "crypt_decrypt" that uses OpenSSL instead but keeps the same underlying crypto, then that's fine, but you need to be SURE that's what you're using. So you need to find out where crypt_decrypt() is called in your code and then make sure that you're not overriding any other parameters.

To recap, we need to see the current code for your mcrypt-based functions (e.g. "crypt_encrypt" function) and we need to see how it's called (or you can just write the parameters to a file and then use whatever script does the encryption and then check the file).

Commented:
Here's a version of your crypt_decrypt() that uses the OpenSSL extension in PHP and will decrypt Blowfish-ECB:
function crypt_decrypt($key, $algo = 'BF-ECB', $data){

    // OpenSSL doesn't have a get_key_size function, so we'll do a little guessing
    if($algo == 'BF-ECB')
    {
      $key_size = 448 / 8; // Blowfish has a 448-bit key size
    }
    else
    {
      throw new Exception("The function crypt_decrypt was called with algorithm: {$algo}");
    }
    
    $key = substr($key, 0, $key_size);
    $algo = $algo;
    if(!$data){
	    return '';
    }

    $crypt = base64_decode($data);

    $decrypt = openssl_decrypt($crypt, $algo, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
    return trim($decrypt);
}

Open in new window

It will throw an error/exception if any other algorithm/cipher is passed in.

I should also note that your original code has some lines in it referring to an IV. The "ECB" block chain mode does not use IVs. For some reason, mcrypt returns an "8" for the IV size, but I don't believe it actually USES the IV when encrypting. The IV will be used when you use a block chain mode like CBC. So that's why my version of your function doesn't have anything about IVs in there - it's designed specifically for Blowfish-ECB.

If you want to change to using AES-256-CBC, that's fine, but you would need to write a script like David suggested, where you decrypt everything with Blowfish-ECB, then re-encrypt with AES-256-CBC, and save the data. You don't want to try to do this incrementally - take your web server offline so nobody's changing data, then run the script to do it, then change your code to use AES-256-CBC, and THEN bring your server back online.

If you're not comfortable doing it yourself and/or it's urgent, you can always hit one of us up for an hour or two of contract work. Otherwise, you can just try it yourself and ask additional questions as they come up.
Chris InmanOne-man IT Show!

Author

Commented:
Thanks gr8gonzo again for the reply,

I believe we're getting closer to a solution.
I had an earlier question posted here , and the direction I was going with that was to use OpenSSL to decrypt the mcrypt'ed data. So please take a look at that for me as it has all the original code being used for mcrypt and perhaps the code you supplied may need a tweak from that, or not. I will see what I can do with it tomorrow when I'm back in the office.

In this question here, I was attempting to decrypt using mcrypt , blowfish and ecb as per current code does, and then re-encrypt with OpenSSL using AES-256-CBC with the same for decrypt when needed.

So, now i'm not sure which direction to go. The easiest would be to decode mcrypt with OpenSSL and perhaps code OpenSSL to encrypt in the same fashion to move day-forward with one decryption function. Or, I could perhaps have 2 decryption functions, simply based on a conditional date as the cut-off between the two.

Since that choice was not looking good, that's why this question came about for basically a one-time go at the data to decrypt with mcrypt then  encrypt again with day-forward OpenSSL function.

What do you think?
Commented:
So I took your "crypt_ecrypt" function from your linked answer and ran a test on it to encrypt (with mcrypt) the word "Hello" :
<?php
$key = "0123456789012345678901234567890123456789";
$data = "Hello";

echo crypt_ecrypt($key,MCRYPT_BLOWFISH,$data); // Output: u5z6gIMh1qA=

function crypt_ecrypt($key, $algo = MCRYPT_BLOWFISH, $data){
    $key = substr($key, 0, mcrypt_get_key_size($algo, MCRYPT_MODE_ECB));
    $algo = $algo;

    if(!$data){
	return '';
    }
    
    //Optional Part, only necessary if you use other encryption mode than ECB
    $iv_size = mcrypt_get_iv_size($algo, MCRYPT_MODE_ECB);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    
    $crypt = mcrypt_encrypt($algo, $key, $data, MCRYPT_MODE_ECB, $iv);
    return trim(base64_encode($crypt));
}

Open in new window


As noted in the code comments, the output is the string "u5z6gIMh1qA=", so I took my OpenSSL-based function from above and used the same key and encrypted string:
<?php
$key = "0123456789012345678901234567890123456789";
$data = "u5z6gIMh1qA="; // <--- Output from the above script

echo crypt_decrypt($key, 'BF-ECB', $data); // Output: Hello

function crypt_decrypt($key, $algo = 'BF-ECB', $data){

    // OpenSSL doesn't have a get_key_size function, so we'll do a little guessing
    if($algo == 'BF-ECB')
    {
      $key_size = 448 / 8; // Blowfish has a 448-bit key size
    }
    else
    {
      throw new Exception("The function crypt_decrypt was called with algorithm: {$algo}");
    }
    
    $key = substr($key, 0, $key_size);
    $algo = $algo;
    if(!$data){
	    return '';
    }

    $crypt = base64_decode($data);

    $decrypt = openssl_decrypt($crypt, $algo, $key, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
    return trim($decrypt);
}

Open in new window


The decrypted result is correct - the word "Hello".

So that means that my OpenSSL-based function above will decrypt data that was encrypted using your mcrypt-based function from your original post.

So given that, if my function does NOT decrypt some of the encrypted data you're dealing with, then that means that either:
#1. You have the wrong encryption function.
or
#2. You're not actually passing in "MCRYPT_BLOWFISH" as the 2nd parameter to crypt_ecrypt.
or
#3. You're passing the wrong key.
or
#4. You're passing the wrong data.

It has to be one of those 4 possibilities, because the code itself is correct and I just demonstrated that it is.

Either way, your best bet is to simply log the parameters, so adjust your encrypt function like this:
function crypt_ecrypt($key, $algo = MCRYPT_BLOWFISH, $data){
  file_put_contents("crypt.log", "ENCRYPT INPUT: " . md5($key) . "," . $algo . "," . md5($data) . "\n", FILE_APPEND);

  ...rest of the original function contents here up until the return line...

   $result = trim(base64_encode($crypt));
  file_put_contents("crypt.log", "ENCRYPT OUTPUT: " . md5($result) . "\n", FILE_APPEND);
  return $result;
}

Open in new window


And then do something similar with my OpenSSL decrypt function:
function crypt_decrypt($key, $algo = 'BF-ECB', $data){
   file_put_contents("crypt.log", "DECRYPT INPUT: " . md5($key) . "," . $algo . "," . md5($data) . "\n", FILE_APPEND);

    ...rest of the original function contents here up until the return line...
    $result = trim($decrypt);
    file_put_contents("crypt.log", "DECRYPT OUTPUT: " . md5($result) . "\n", FILE_APPEND);
    return $result;
}

Open in new window


Finally, run your test again. Use your existing system to encrypt a value (I'm assuming it's just part of a Wordpress plugin or something, so maybe you click a button in WP admin to trigger the encryption to occur) and then look for the "crypt.log" file. It will probably be in your web site / document root folder or possibly in your admin folder.

Then run your test to decrypt the data with my function and then check the crypt.log file, and share the crypt.log file here. The logging code is using MD5 to hash your values so we can see the consistency (or inconsistency) in values without actually seeing your data or keys.

For example, in my demo above, the crypt.log looks like this:
ENCRYPT INPUT: 9f0ae0380ed27dbf6b852843d2eece1f,blowfish,8b1a9953c4611296a827abf8c47804d7
ENCRYPT OUTPUT: 9759e68dcbd075f96d6f8ed5b1525a65
DECRYPT INPUT: 9f0ae0380ed27dbf6b852843d2eece1f,BF-ECB,9759e68dcbd075f96d6f8ed5b1525a65
DECRYPT OUTPUT: 8b1a9953c4611296a827abf8c47804d7

I added styling so you can see how the parameters and input/output should match up.

Commented:
As to which direction you should go, I'll be blunt - you said you weren't that skilled with PHP, and you don't really seem confident in the crypto concepts (this is not an insult - everyone starts at the beginning), so even though I would personally switch from Blowfish-ECB to AES-256-CBC and do the whole re-encryption thing, I don't think it's a good idea for you here.

My personal opinion is that you should simply get your functions converted over to OpenSSL so they encrypt/decrypt using the exact same methodology you use today (and using the logging that I've mentioned should help establish where things are going wrong).

If something went wrong (and if you're new at this, it's hard to know when it goes wrong), you could end up permanently losing data. I wouldn't even do it myself without doing a couple dry runs on a clone of the data first, and I've been doing this stuff for a long time now.

Just get everything working and converted over to using the OpenSSL extension with the same crypto, and then maybe set up a copy of your database and then test out a script on that copy later on.
Chris InmanOne-man IT Show!

Author

Commented:
So I ran the code with your decrypt and I didn’t see a ‘null’ issue in the cosole, but it didn’t return anything either. However, the browser did throw your exception. Any idea?

Fatal error: Uncaught exception 'Exception' with message 'The function crypt_decrypt was called with algorithm: blowfish' in /Applications/AMPPS/www/dashboard-live/wp-content/themes/czar/functions.php:3544 Stack trace: #0 /Applications/AMPPS/www/dashboard-live/wp-content/themes/czar/functions.php(3490): crypt_decrypt('(XXXXXX} BkfG...', 'blowfish', 'CJj4eeM9UCY=') #1 /Applications/AMPPS/www/dashboard-live/wp-content/themes/czar/page-diamond.php(984): czar_get_info('197506', 'exp') #2 /Applications/AMPPS/www/dashboard-live/wp-includes/template-loader.php(74): include('/Applications/A...') #3 /Applications/AMPPS/www/dashboard-live/wp-blog-header.php(19): require_once('/Applications/A...') #4 /Applications/AMPPS/www/dashboard-live/index.php(17): require('/Applications/A...') #5 {main} thrown in /Applications/AMPPS/www/dashboard-live/wp-content/themes/czar/functions.php on line 3544
Commented:
Yes! So there's a constant in play here: MCRYPT_BLOWFISH, which has the string value "blowfish".

So $algo = MCRYPT_BLOWFISH is the exact same thing as doing $algo = "blowfish".

Now, "blowfish" is a valid algorithm name for mcrypt, but in OpenSSL, it's not called "blowfish", it's called "BF-ECB".

So basically you're passing an old/invalid algorithm name to OpenSSL.

That stack trace tells us where it's happening: on line 3490 in your /Applications/AMPPS/www/dashboard-live/wp-content/themes/czar/functions.php file.

So go to that line of code in that file and you will likely see something like:
$some_variable = crypt_decrypt($key, MCRYPT_BLOWFISH, $data);

Change that to:
$some_variable = crypt_decrypt($key, "BF-ECB", $data);

While you could add code to detect the old constant and change "blowfish" to "BF-ECB", eventually you will not have the mcrypt extension installed at all, so the MCRYPT_BLOWFISH constant will disappear on that day. So you need to change how the function is called.
Chris InmanOne-man IT Show!

Author

Commented:
Disregard my last - -I got it to work; needed to add some extra in the middle-man function that gets called to then decrypt.

Thank you very much for the help!

I’m still testing, but thought you’d like to see  that it’s working ,, so far :)

function czar_get_info($postid, $field){
    $data = get_post_meta( $postid, $field, true );

    if( defined('CRYPT_KEY') && !empty($data) ){
	//$data = crypt_decrypt(CRYPT_KEY, MCRYPT_BLOWFISH, $data);
        $data = crypt_decrypt(CRYPT_KEY, $algo = 'BF-ECB', $data);

    } else{
	$data = '';
    }
    
    return $data;
}

Open in new window

Commented:
Great to hear!
Chris InmanOne-man IT Show!

Author

Commented:
So,, now,, what do I need to change my new OpenSSL encryption to in order for the decrypt to be able to handle both encryption types?

function crypt_ecrypt($key, $algo = MCRYPT_BLOWFISH, $data){
    $key = substr($key, 0, mcrypt_get_key_size($algo, MCRYPT_MODE_ECB));
    $algo = $algo;

    if(!$data){
	return '';
    }
    
    //Optional Part, only necessary if you use other encryption mode than ECB
    $iv_size = mcrypt_get_iv_size($algo, MCRYPT_MODE_ECB);
    $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
    
    $crypt = mcrypt_encrypt($algo, $key, $data, MCRYPT_MODE_ECB, $iv);
    return trim(base64_encode($crypt));
}

Open in new window

Chris InmanOne-man IT Show!

Author

Commented:
Actually, the code that is Ecrypting is this:
interface ICrypter{
	public function __construct($Key, $Algo = MCRYPT_BLOWFISH);
	public function Encrypt($data);
	public function Decrypt($data);
}

class Crypter implements ICrypter{
	private $Key;
	private $Algo;

	public function __construct($Key, $Algo = MCRYPT_BLOWFISH){
		$this->Key = substr($Key, 0, mcrypt_get_key_size($Algo, MCRYPT_MODE_ECB));
		$this->Algo = $Algo;
	}

	public function Encrypt($data){
		if(!$data){
			return false;
		}
        file_put_contents("crypt.log", "ENCRYPT INPUT: " . md5($key) . "," . $algo . "," . md5($data) . "\n", FILE_APPEND);
		//Optional Part, only necessary if you use other encryption mode than ECB
		$iv_size = mcrypt_get_iv_size($this->Algo, MCRYPT_MODE_ECB);
		$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
		
		$crypt = mcrypt_encrypt($this->Algo, $this->Key, $data, MCRYPT_MODE_ECB, $iv);
        $result = trim(base64_encode($crypt));
        file_put_contents("crypt.log", "ENCRYPT OUTPUT: " . md5($result) . "\n", FILE_APPEND);
        return $result;
	}

Open in new window


And called like this:
if( defined('CRYPT_KEY') ){
        $crypter = new Crypter(CRYPT_KEY, MCRYPT_BLOWFISH);
        if( !empty($cc_num) ){
          $cc_num = $GLOBALS["crypter"]->Encrypt($cc_num);
        }
        if( !empty($exp) ){
          $exp = $GLOBALS["crypter"]->Encrypt($exp);
        }
        if( !empty($cvv) ){
          $cvv = $GLOBALS["crypter"]->Encrypt($cvv);
        }
      }

Open in new window

Commented:
interface ICrypter{
	public function __construct($Key, $Algo = "BF-ECB");
	public function Encrypt($data);
	public function Decrypt($data);
}

class Crypter implements ICrypter{
	private $Key;
	private $Algo;

	public function __construct($Key, $Algo = "BF-ECB"){
		$this->Key = substr($Key, 0, 56); // 56 = 56 bytes = 448-bit key for Blowfish
		$this->Algo = $Algo;
	}

	public function Encrypt($data){
		if(!$data){
			return false;
		}
		
		// OpenSSL is finicky about block lengths, even for ECB, so we'll pad the data with blanks that are trimmed during decryption
    $block_size = 8; // Could be different for non-Blowfish algorithms
    $data_length_needed = strlen($data) + (8 % strlen($data));
    $data = str_pad($data,$data_length_needed," ");
    
    $crypt = openssl_encrypt($data, $this->Algo, $this->Key, OPENSSL_RAW_DATA);
    $result = base64_encode($encrypted);
    return $result;
	}

Open in new window


...and just strip out "MCRYPT_BLOWFISH" in your calling code:

$crypter = new Crypter(CRYPT_KEY);

That way, you're not specifying "Blowfish" all over the place - you can let the parameter default value define which cipher is used.

On a side note, if and when you get to the point of converting over to AES-256-CBC, I'd recommend just sticking with your Crypter class for both encryption/decryption and then fleshing it out a bit with some cleaner code. I'm not sure that you really need a separate interface for this if you only have one instance of it, but whatever.
Chris InmanOne-man IT Show!

Author

Commented:
Testing now!
Chris InmanOne-man IT Show!

Author

Commented:
It’s not e=giving an error, just not encrypting the data. I think there’s a couple typos and the block_size isn’t being used, does it need to?

$crypt = openssl_encrypt($data, $this->Algo, $this->Key, OPENSSL_RAW_DATA);
        $result = base64_encode($encrypted); <— think needs to be $crypt
Chris InmanOne-man IT Show!

Author

Commented:
So far so good now. Just needed that one typo fixed. It doesn’t appear to need the block_size

Commented:
Yes, sorry - I started to make it dynamic for other methods, but just ended up hardcoding "8" instead of using $block_size. You can pick which approach you'd prefer.
Chris InmanOne-man IT Show!

Author

Commented:
After many days, trials, errors and amongst those who said it couldn’t be done while attempting a solution on my own; the gr8gonzo pulled off a coding miracle.
Thank you so much sir!

Commented:
On a side note, here's how I would implement a simple, easy-to-use Crypto class:

Crypto.php
<?php
/* Crypto package by gr8gonzo for https://www.experts-exchange.com/questions/29154796
 * Date: 2019-08-14 
 * Description: This implements a simplified wrapper for encrypting/decrypting
 *              data using either Blowfish-ECB, or AES-256-CBC.
 *
 * Examples:
 * $encrypted = Crypto\Blowfish_ECB::Encrypt($key, $original_data);
 * $decrypted = Crypto\Blowfish_ECB::Decrypt($key, $encrypted);
 *
 * $encrypted = Crypto\AES_256_CBC::Encrypt($key, $original_data);
 * $decrypted = Crypto\AES_256_CBC::Decrypt($key, $encrypted); 
 */
namespace Crypto;

abstract class Base
{
  protected static $algorithm;
  protected static $key_length;
  protected static $block_size;
  protected static $uses_iv;
  protected static $uses_keygen;


  protected static function dumpParameters()
  {
    echo "Algorithm   : " . static::$algorithm . "\n";
    echo "Key Length  : " . static::$key_length . "\n";
    echo "Block Size  : " . static::$block_size . "\n";
    echo "Uses IV     : " . (static::$uses_iv ? "true" : "false") . "\n";
    echo "Uses Keygen : " . (static::$uses_keygen ? "true" : "false") . "\n";
  }
  
  protected static function checkParameters($key, $data)
  {
    // Make sure we're encrypting -something-
    if(($data === null) || !strlen($data))
    {
      throw new \Exception("Cannot encrypt an empty data string!");
    }
    
    // Make sure we have a key
    if(($key === null) || !strlen($key))
    {
      throw new \Exception("Key cannot be blank!");
    }
  }
  
  protected static function padData($data, $padCharacter = " ")
  {
    $data_length_needed = strlen($data) + (static::$block_size % strlen($data));
    $data = str_pad($data,$data_length_needed, $padCharacter);
    return $data;
  }
  
  protected static function deriveKey($key, $iterations = 10000)
  {
    return openssl_pbkdf2($key, sha1($key), static::$key_length, $iterations);
  }
  
  public static function Encrypt($key, $data)
  {
    // Error-checking on parameters
    self::checkParameters($key, $data);
    
    // Optionally apply PBKDF2 key-generation
    if(static::$uses_keygen)
    {
      $key = self::deriveKey($key);
    }
    
    // Pad data as necessary (manual call required for some older ECB-based algorithms)
    $data = static::padData(trim($data));

    // Generate an IV if necessary
    if(static::$uses_iv)
    {
      // Generate a random IV according to the cipher/algorithm length
      $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(static::$algorithm));
    }
    else
    {
      // A blank string is the default parameter for algorithms without IVs
      $iv = "";
    }

    // Encrypt
    $result = openssl_encrypt($data, static::$algorithm, $key, OPENSSL_RAW_DATA, $iv);
    
    // Check for errors
    if($result === false)
    {
      throw new \Exception("Error while encrypting: " . openssl_error_string());
    }
    
    // Return Base64-encoded Optional-IV-and-result
    return base64_encode($iv . $result);
  }
  
  
  public static function Decrypt($key, $data)
  {
    // Error-checking on parameters
    self::checkParameters($key, $data);

    // Optionally apply PBKDF2 key-generation
    if(static::$uses_keygen)
    {
      $key = self::deriveKey($key);
    }
        
    // Base64-decode the data
    $data = base64_decode($data);
    
    // Extract IV if necessary
    if(static::$uses_iv)
    {
      $iv_length = openssl_cipher_iv_length(static::$algorithm);
      $iv = substr($data, 0, $iv_length); // Extract IV from beginning of data
      $data = substr($data, $iv_length); // Remove IV from beginning of data
    }
    else
    {
      // A blank string is the default parameter for algorithms without IVs
      $iv = "";
    }

    // Decrypt
    $result = openssl_decrypt($data, static::$algorithm, $key, OPENSSL_RAW_DATA, $iv);
    
    // Check for errors
    if($result === false)
    {
      throw new \Exception("Error while decrypting: " . openssl_error_string());
    }
    
    // Return decrypted result
    return trim($result);
  }
}

class Blowfish_ECB extends Base
{  
  protected static $algorithm = "BF-ECB";
  protected static $key_length = 56; // 56 = 56 bytes = 448-bit key for Blowfish
  protected static $block_size = 8;
  protected static $uses_iv = false;
  protected static $uses_keygen = false; // Set to false because the original data didn't use a keygen
}

class AES_256_CBC extends Base
{  
  protected static $algorithm = "AES-256-CBC";
  protected static $key_length = 32; // 32 = 32 bytes = 256-bit key for AES-256
  protected static $block_size = 16; // AES algorithm uses 128-bit blocks
  protected static $uses_iv = true;
  protected static $uses_keygen = true; // Keygen is a simple additional step to reduce risk from brute-force attacks
}

Open in new window


Example script demonstrating encryption using both Blowfish-ECB and AES-256-CBC, as well as how you would convert from Blowfish to AES:
<?php
require("Crypto.php");

echo "TEST #1: Testing Blowfish_ECB crypto...\n";
$key = "0123456789";
$original_string = "Hello world!";
$encrypted = Crypto\Blowfish_ECB::Encrypt($key, $original_string);
$decrypted = Crypto\Blowfish_ECB::Decrypt($key, $encrypted);
echo "  Original: " . $original_string . "\n";
echo "  Encrypted: " . $encrypted . "\n";
echo "  Decrypted: " . $decrypted . "\n";
echo "  Test Result: " . ($decrypted == $original_string ? "PASSED!" : "FAILED!") . "\n";
echo "\n";

echo "TEST #2: Testing AES-256-CBC crypto...\n";
$key = "0123456789";
$original_string = "Hello world!";
$encrypted = Crypto\AES_256_CBC::Encrypt($key, $original_string);
$decrypted = Crypto\AES_256_CBC::Decrypt($key, $encrypted);
echo "  Original: " . $original_string . "\n";
echo "  Encrypted: " . $encrypted . "\n";
echo "  Decrypted: " . $decrypted . "\n";
echo "  Test Result: " . ($decrypted == $original_string ? "PASSED!" : "FAILED!") . "\n";
echo "\n";

// Algorithm Conversion Example:
echo "TEST #3: Testing conversion from Blowfish-encrypted data to AES-encrypted data...\n";
echo "  AES Re-encrypted Data: " . convertFromBlowfishToAES($key, "ejquA+ltgPuqqwtYb//bfpcK6IpmqL6r");

function convertFromBlowfishToAES($key, $encrypted_data)
{
  return Crypto\AES_256_CBC::Encrypt($key, Crypto\Blowfish_ECB::Decrypt($key, $encrypted_data));
}

Open in new window


NOTE: This Crypto class is set up using namespaces, which requires PHP 5.3 or higher. If you try it on PHP 5.2 or below, it'll throw an error because it doesn't know what the "\" character in "Crypto\Blowfish_ECB" is, for example. It could always be changed to remove namespaces and just use longer class names like Crypto_Blowfish_ECB and Crypto_Base and Crypto_AES_256_CBC but if you have 5.3 or higher, namespaces are better.

NOTE #2: I implemented a very basic key-derivation step for the AES-256-CBC class ($uses_keygen = true). Since you didn't use it for your Blowfish encryption, I set the Blowfish class's $use_keygen to false. The key derivation / keygen is simply a crypto technique that takes YOUR password and then creates a much more complex, random key from it. If you run the above example script multiple times, you'll see the AES-encrypted data changing every time, but it will decrypt perfectly each time, too. The point of doing this is to protect against brute-force attacks. If you encrypt "Hello world" into the same "ejquA+ltgPuqqwtYb//bfpcK6IpmqL6r" string every time, it can introduce a possible way to attack the encryption and exposes a little bit of info about what you're doing. However, if the encrypted version of the data is changing all the time, it's harder to recognize encrypted data that might be being transmitted, for example.

Long story short, it's a good extra step to take if you're going to convert your data over to AES.
Chris InmanOne-man IT Show!

Author

Commented:
Thanks again! So far the previous code is working great and on all the different “keys” that my different sites use. So hopefully now I can move to PHP 7.3 soon.

I’ll look you up next time I’m in Montana and take you to lunch or something!

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start Today