1

I have a PHP class that handles encryption and decryption which stopped decrypting recently after a MySQL upgrade from 5.7 to 8 and migration to a new server. I need to decrypt the data in the database that was encrypted using the encrypt function and I can't for the life of me figure out what may have happened or what I need to fix. After searching around I decided to post for feedback. Thanks!

Quick clarification: While my main issue is retrieving and accessing the encrypted information in the database, this may be a 'red herring' since the decrypt function in the encryption class doesn't work at all for me. If I can solve this while preserving the encrypt function which works then it may result in a solution as to why I can't decrypt the database data. The encrypt function will run error-free and return an encrypted string, while the decrypt function gives the error below.

These are error I have received:

Decryption error: error:0607A082:digital envelope routines:EVP_CIPHER_CTX_set_key_length:invalid key length

Decryption error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length

This is the class:

class Openssl_EncryptDecrypt
{
    private $encryption_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

    public function encrypt($pure_string)
    {
        $cipher = 'AES-256-CBC';
        $options = OPENSSL_RAW_DATA;
        $hash_algo = 'sha256';
        $sha2len = 32;
        $ivlen = openssl_cipher_iv_length($cipher);
        $iv = openssl_random_pseudo_bytes($ivlen);
        $ciphertext_raw = openssl_encrypt($pure_string, $cipher, $this->encryption_key, $options, $iv);
        $hmac = hash_hmac($hash_algo, $ciphertext_raw, $this->encryption_key, true);

        return $iv . $hmac . $ciphertext_raw;
        // return bin2hex($iv . $hmac . $ciphertext_raw);
    }

    public function decrypt($encrypted_string)
    {
        // $encrypted_string = hex2bin($encrypted_string);
        $cipher = 'AES-256-CBC';
        $options = OPENSSL_RAW_DATA;
        $hash_algo = 'sha256';
        $sha2len = 32;
        $ivlen = openssl_cipher_iv_length($cipher);
        $iv = substr($encrypted_string, 0, $ivlen);
        $hmac = substr($encrypted_string, $ivlen, $sha2len);
        $ciphertext_raw = substr($encrypted_string, $ivlen + $sha2len);
        $original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $this->encryption_key, $options, $iv);
        $calcmac = hash_hmac($hash_algo, $ciphertext_raw, $this->encryption_key, true);

        if ($original_plaintext === false) {
            $errorMessage = 'Decryption error: ' . openssl_error_string() . ' for eid ' . $encrypted_string;
            error_log($errorMessage);
            return $errorMessage;
        } 
        
        if (hash_equals($hmac, $calcmac)) {
            return $original_plaintext;
        }
    }
 }

Here is an example of retrieving data and passing it the decrypt function: (Aggregated from several files, trying to be succinct.)

// Grab employee data and decrypt the employee id:
$EmployeeData = getEmployeeDataBasedOnNetId($editNetId);
$EmployeeId = $OpensslEncryption->decrypt($EmployeeData['EmployeeId']);

// Get data from db as called above
function getEmployeeDataBasedOnNetId($NetId)
{
    try {
        $pdo = new pdoConnection('fasp');

        $NetId = strtoupper(trim($NetId));

        $sql = " SELECT NetId, EmployeeId, PrimaryRole, FirstName, MiddleName, LastName, Email, Phone, Mobile, Building, Room, MailStop, Picture, Gender, Ethnicity, CountryOfOrigin, UserName, Comment, OnWebsite
                    FROM Employee
                    WHERE NetId = :field1
                ";

        $pdo->query($sql);
        $row = $pdo->single(array(':field1' => $NetId));

        return $row;
    }
    catch(Exception $e) {
        echo $e;
    }
    finally {
        $pdo->connectionClose();
        $pdo = null;
    }
}

// This is the query function from the pdo class called above:
public function query($query)
{
  $this->stmt = $this->dbh->prepare($query);
}

// This is the single function from the pdo class called above:
public function single($arr = null)
{
  $this->execute($arr);
  return $this->stmt->fetch(PDO::FETCH_ASSOC);
}

// This is the execute function from the pdo class called above:
public function execute($arr = null)
{
  if (is_array($arr) && count($arr)) {
    return $this->stmt->execute($arr);
  }

  return $this->stmt->execute();
}

// This is the connectionClose funstion from the pdo class called above:
public function connectionClose()
{
  $this->error = null;
  $this->stmt = null;
  $this->dbh = null;
}

Here is also the PDO settings I'm using for connecting to the DB, which may not be relevant but I'll add it in case it prompts anything I may not be considering:

$options = array(
  PDO::ATTR_PERSISTENT => true,
  PDO::MYSQL_ATTR_SSL_CA => true,
  PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
  PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci"
);

$dsn = "mysql:host=" . $host . ";port=" . $port . ";dbname=" . $db . ";charset=utf8mb4";

$dbh = new PDO($dsn, $user, $pass, $options);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Note about encryption class: You'll see a bin2hex call in the encrypt function as well as a hex2bin call in the decrypt function that are commented out, which was something I was testing and works! The issue is that it could be a solution moving forward, but it obviously doesn't help decrypt the current values in the database. Which is what I'm trying to figure out.

ddavidd
  • 11
  • 4
  • please also paste the code where you retrieve the data from db and pass it to the decrypt function. – Kaii Jun 20 '23 at 15:54
  • I edited the question to add the db retrieval and how it's passed to the decrypt function. Let me know if I missed any logic, it's taken from several files. – ddavidd Jun 20 '23 at 16:25
  • 1
    "a MySQL upgrade from 5.7 to 8 and migration to a new server" - if a MySQL upgrade caused a problem, that would probably be a data corruption issue, not something you can fix in code. How did you get the data from the old server to the new server? Can you inspect the data manually, maybe pull an encrypted field and compare the sha256 on both old and new? Don't both decrypting until you know the data isn't corrupt. If the data is valid, then I'm betting it is more related to a config or library issue, possibly openssl related – Chris Haas Jun 20 '23 at 19:59
  • Do you have access to the old server still? Are you able to just decrypt, transfer, and then re-encrypt? – Chris Haas Jun 21 '23 at 00:15
  • The server was decommissioned as soon as the update happened (not my call.) I requested a restore from a backup and we'll see how that goes. – ddavidd Jun 21 '23 at 14:36
  • @ddavidd, I had a friend go through a similar issue in the past, although their issue was more related to libsodium's full integration in core, but basically an upgrade made all of their encrypted data suddenly dead to them. Luckily we were able to diagnose the change and find a solution to decrypt and re-encrypt things properly. If it would help to have a second set of eyes on this in greater detail, my SO profile has a path to my corporate profile where you should be able to reach out to me. – Chris Haas Jun 21 '23 at 16:45
  • I think I may have explained the issue wrong. I'll update the question, but essentially: I can't decrypt *anything* on the new environment. Yes, I need to decrypt the values in the database, but I can't use this class at all to decrypt even any new data I input into the database. The encrypt function works and stores a value in the database, but the decrypt function will give me the same error as I explained in the original post. – ddavidd Jun 21 '23 at 20:23
  • @ddavidd, when I run your code and pass the output of `encrypt` directly do `decrypt`, it works. https://onlinephp.io/c/16b9c. Can you try that first, ignoring the database completely? – Chris Haas Jun 21 '23 at 20:31
  • @chris-haas You're right, the encrypt and decrypt function do in fact work when I eliminate the db like you did. Both functions work *with the database* if I un-comment the `bin2hex` and `hex2bin` lines of the encrypt and decrypt functions. So my clarification is also a misdirection. Oops! – ddavidd Jun 21 '23 at 20:52
  • I guess my next thing would be to take the output of encrypt and hash it (sha256, md5, crc32, doesn't really matter), put the encrypted stuff into the database, pull it out, re-run the hash and compare. If what you are putting in doesn't match what you are pulling out, you've got a problem there. Could be a collation thing, or possibly something in the MySQL connection. – Chris Haas Jun 21 '23 at 20:56

2 Answers2

0

The error message states that the key length is wrong, EVP_CIPHER_CTX_set_key_length:invalid key length.

This should then refer to $original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $this->encryption_key, $options, $iv);. Where the key appears to be private $encryption_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";.

As strings are byte arrays in PHP, i.e. the same thing internally, I would expect you to have the encryption_key set to a hexadecimal string value, and then converted to binary with hex2bin before being sent into openssl_decrypt(). It doesn't make sense that it could figure it out by itself, since a binary key could happenstance just look like a hexadecimal string and providing a binary non-text-string as a string constant in source will not work well.

Just reasoning from what I see, at a quick glance. I could be barking up the wrong tree, I have not actually tried any code.

Xecrets
  • 79
  • 5
  • Thanks Xecrets for your comment, it did help me eliminate a few things that I thought could be the issue. – ddavidd Jun 23 '23 at 14:17
0

Thank you for the comments. I'm not 100% sure what caused this but I believe one of the changes I made on local that enabled me to decrypt the data was specifying the character encoding in PHP. I thought I tried this initially, so it may be some combination of this as well as some PHP configuration I did on my local vm. After I decrypted the data, I wrote a few scripts to update the db records (and encryption functions) to be more friendly with retrieval and updating. Cheers!

ddavidd
  • 11
  • 4