-1

I have moved my website to a new hosting provider where my Sagepay Form v3 script which receives the encrypted response is now failing.

At the previous hosting provider the script was working (php version there was 5.5.9) and the new hosting offers a choice from 5.4 to 6. At the first hosting provider the php version for a long time earlier was 5.2 (or maybe it was 5.3) and when they finally enforced a change to 5.5 it wrecked a lot of things in my website scripts which resulted in a very difficult period trying to fix them, which I achieved in the end.

One of those things was that the decrypt failed, just like it is doing again now. In that case I eventually fixed it by changing the decrypt line from:

$Decoded = DHclassInFunc::decryptAes($crypt,$EncryptionPassword);

to:

$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);

I had tried many other variations but only this last one worked.

So now the problem is back and I am completely at a loss. I have tried all of the previous variations but nothing works. Also the various php versions on offer at my new host.

My (LONG) question on the previous occasion was also posted here: see Website to Sagepay submit encryption code was working but now fails after server php upgrade

Can anyone suggest why this is failing this time round and what I can do to fix it?

EDIT 14/12/18 More info after investigation plus I am including more explanation and the full code from the two relevant scripts ------------------------

I made no progress and the website orders have had to be manually managed while the Sagepay return was not working. Now I have a little time so I am trying again.

I have now found that if I remove this line (below) on the "completed.php" page (the url to which the Sagepay response is directed) the script does not hang; however it is because it is that line which causes a fatal error.

$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);

Without the line and the resultant error the scipt is able to move on and call the the following page ("return.php") which then displays payment result information to the customer and also does other actions (such as sending the full order detail to our local - not on the internet - database).

However with the line removed the crypt in the url is not processed and therefore there are no values in the result variables which the completed.php page forwards to the return.php page.

This means that the $status variable is empty; in the return.php page this is evaluated as an error and therefore the customer is shown a message which says that there was an error and that no payment was taken - which is incorrect.

The lack of a "success" status value also means that the order in the web mysql database is not flagged as confirmed.

I had tried many other variations of the line to no avail (although the one given here worked before the website was moved to a new host).

The line does of course invoke the function in the class "DHclassInFunc" which is located in the functions.php file.

I am enclosing below the active code from of the two files, completed.php and functions.php

As far as I can work out the root problem is that the line

$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);

does not receive any value in 'crypt' and so there is no string for the function.php decrypt routine to work on resulting in the fatal error when that function is called: "PHP Fatal error: Class 'SagepayApiException' not found in /redacted/redacted/redacted.com/www/redacted/protx/functions.php on line 208"

I have added a line in the functions.php code as below:

echo '$strIn' . "  string in with @ should be here?";

in order to try to expose the value which is being passed to the function but it simple prints the name of the var, not the values from the url content which is in the address bar of the completed.php page when it receives the response from Sagepay - eg:

https://www.redacted.com/redacted/protx/completed.php?crypt=@ad6721a09c786829cd839586df0fe047ea0f0e9c791ddfe5d55b7175881aa4609ccfb4768a8b84dd9f259614d0edf0f03254a1967279693509e72190c8248cd56d1cefa713592f84eca4e8d7477ac89c9dd783b350a21766500c1c91fde3dbe5deb7887bea0e5c07e58274dec93224729f265730a4aecf5cf9c7216dad2b5eecc4d128e6c8389c1c9d5d297b7a10ccb53e37eae5b7a996a308c10f2d0edc0b41b6b38c6e56375a6421d110a0a3fe40cdfa2daa2fa6e0bf767204d209aa300d9f907ea686ee9a9dcc0992c14c325123ab53d7885bc6dc66eebf3c341002034fbce6277ccc6fbb8734c3cdab58dcd294d0a3a4430c7b091beed81fd97cadbf24b9149f9541e5d8e8c45a4e267fc0d14222c45963fe847ec12a9fedf05eba2a78caf769825046584b112d353d92d38aedc3cb086fc0c8250e20ef975dc377438b7c3a34c96cacba9ed1670b2af1bcd0945a5a0424c0532f23b0a6662db8198a2368d60ee3785f07826005593292154abe06abf55ff1d461b714e1fb53b5da3db1f21eb6b01169a2cf78d872de5ac96e41e088a7bf1e6f88aa8cc5c6b4bfd5d82f63

Regarding whether it might be a unicode / iso issue I can't see why this would result in an empty value in $strIn since this has not yet been processed at all, merely captured (or not?).

COMPLETED.php ---------------------

<?php
include "functions.php";

$Decoded = DHclassInFunc::decryptAes($_GET['crypt'],$EncryptionPassword); 

$values = getToken($Decoded);
$VendorTxCode = $values['VendorTxCode'];
$Status = $values['Status'];
$VPSTxID = $values['VPSTxId'];
$TxAuthNo = $values['TxAuthNo'];
$AVSCV2 = $values['AVSCV2'];
$Amount = $values['Amount'];
// protocol 2.22 fields
$AddressResult = $values[ 'AddressResult' ];
$PostCodeResult = $values[ 'PostCodeResult' ];
$CV2Result = $values[ 'CV2Result' ];
$GiftAid = $values[ 'GiftAid' ];
$VBVSecureStatus = $values[ '3DSecureStatus' ];
$CAVV = $values[ 'CAVV' ];

// DH my all-in-one details var

$ResultDetails = $ResultDetails . "Vendor Code: " . $VendorTxCode . " - "; 
$ResultDetails = $ResultDetails . "Status: " . $Status . " - "; 
$ResultDetails = $ResultDetails . "VPS Transaction ID: " . $VPSTxID . " - ";                                                                                                                                                                                                         
$ResultDetails = $ResultDetails . "Auth Num: " . $TxAuthNo . " - "; 
$ResultDetails = $ResultDetails . "AVS / CV2 response: " . $TxAuthNo . " - "; 
$ResultDetails = $ResultDetails . "Amount: " . $Amount . " - ";         
$ResultDetails = $ResultDetails . "Address Result: " . $AddressResult . " - ";  
$ResultDetails = $ResultDetails . "PostCode Result: " . $PostCodeResult . " - ";   
$ResultDetails = $ResultDetails . "PostCode Result: " . $PostCodeResult . " - ";    
$ResultDetails = $ResultDetails . "CV2 Result: " . $CV2Result . " - ";  
$ResultDetails = $ResultDetails . "GiftAid Result: " . $GiftAid . " - ";     
$ResultDetails = $ResultDetails . "3DSecure Status: " . $VBVSecureStatus . " - "; 
$ResultDetails = $ResultDetails . "CAVV Result: " . $CAVV . " - ";  

$FindHyphen = strpos($VendorTxCode,'-');
$LastIdChar = $FindHyphen;
$MyOrderID = substr($VendorTxCode,0,$LastIdChar);

$StatusSave = $Status;

echo '  <FORM METHOD="POST" FORM NAME="GoToReturn" ACTION="../MXKart/return.php">'."\n";

echo ' <input type="hidden" name="response_code" value= "';
echo $Status;
echo '">'."\n";
echo ' <input type="hidden" name="order_number" value= "';
echo $MyOrderID;
echo '">'."\n";
echo ' <input type="hidden" name="secretword" value= "';
echo $secret_word;
echo '">'."\n";

//echo addslashes($ResultDetails);
echo ' <input type="hidden" name="response_reason_text" value= "';
echo $ResultDetails;
echo '">'."\n";
echo ' <input type="hidden" name="amount" value= "';
echo $Amount;
echo '">'."\n";
echo ' <input type="hidden" name="force" value= "';
echo $VendorTxCode;
echo '">'."\n";
$msg = "<br><strong>Getting payment result.... </strong> <br><br><h2 style=\"color:green;\">PLEASE WAIT AT THIS PAGE - do not close the page or move on. <br>There can be a delay of up to a minute so please be patient.</h2>";
echo $msg."\n"; 
    echo '</FORM>'."\n";

echo '<script language="javascript">'."\n";
echo 'document.forms[0].submit();'."\n";
echo '</script>'."\n";
?>

FUNCTIONS.php ---------------------

<?

$VendorName="redacted";

$EncryptionPassword="redacted"; //   LIVE  server destination

//************ NEW CRYPT STUFF COPIED FRON SAGEPAY KIT util.php
//DH added class definition as shown in stackoverflow page - trying to fix error when run, on line static private function etc
class DHclassInFunc{
/**
* PHP's mcrypt does not have built in PKCS5 Padding, so we use this.
*
* @param string $input The input string.
*
* @return string The string with padding.
*/

static protected function addPKCS5Padding($input)
{
$blockSize = 16;
$padd = "";

// Pad input to an even block size boundary.
$length = $blockSize - (strlen($input) % $blockSize);
for ($i = 1; $i <= $length; $i++)
{
$padd .= chr($length);
}

return $input . $padd;
}


/**
* Remove PKCS5 Padding from a string.
*
* @param string $input The decrypted string.
*
* @return string String without the padding.
* @throws SagepayApiException
*/
static protected function removePKCS5Padding($input)
{
$blockSize = 16;
$padChar = ord($input[strlen($input) - 1]);

/* Check for PadChar is less then Block size */
if ($padChar > $blockSize)
{
throw new SagepayApiException('Invalid encryption string');
}
/* Check by padding by character mask */
if (strspn($input, chr($padChar), strlen($input) - $padChar) != $padChar)
{
throw new SagepayApiException('Invalid encryption string');
}

$unpadded = substr($input, 0, (-1) * $padChar);
/* Chech result for printable characters */
if (preg_match('/[[:^print:]]/', $unpadded))
{
throw new SagepayApiException('Invalid encryption string');
}
return $unpadded;
}


/**
* Encrypt a string ready to send to SagePay using encryption key.
*
* @param  string  $string  The unencrypyted string.
* @param  string  $key     The encryption key.
*
* @return string The encrypted string.
*/
static public function encryptAes($string, $key)
{
// AES encryption, CBC blocking with PKCS5 padding then HEX encoding.
// Add PKCS5 padding to the text to be encypted.
$string = self::addPKCS5Padding($string);

// Perform encryption with PHP's MCRYPT module.
$crypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $string, MCRYPT_MODE_CBC, $key);

// Perform hex encoding and return.
return "@" . strtoupper(bin2hex($crypt));
}

/**
* Decode a returned string from SagePay.
*
* @param string $strIn         The encrypted String.
* @param string $password      The encyption password used to encrypt the string.
*
* @return string The unecrypted string.
* @throws SagepayApiException
*/

static public function decryptAes($strIn, $password)

{
echo '$strIn' . "  string in with @ should be here?";

$strIn = htmlspecialchars($strIn, ENT_COMPAT,'utf-8', true);

// HEX decoding then AES decryption, CBC blocking with PKCS5 padding.
// Use initialization vector (IV) set from $str_encryption_password.
$strInitVector = $password;

// Remove the first char which is @ to flag this is AES encrypted and HEX decoding.
$hex = substr($strIn, 1);

// Throw exception if string is malformed
if (!preg_match('/^[0-9a-fA-F]+$/', $hex))
{
//DH added section to print result of decryption onto page for debugging
//$hex = "pseudo hex";
//echo "throw error at line 188";
// echo $hex;

throw new SagepayApiException('Invalid encryption string');
}
$strIn = pack('H*', $hex);

// Perform decryption with PHP's MCRYPT module.
$stringReturn = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $password, $strIn, MCRYPT_MODE_CBC, $strInitVector);
return self::removePKCS5Padding($string);
}

}

/* The getToken function.                                                                                         **
** NOTE: A function of convenience that extracts the value from the "name=value&name2=value2..." VSP reply string **
**     Works even if one of the values is a URL containing the & or = signs.                                      */


function getToken($thisString) {
// List the possible tokens
$Tokens = array(
"Status",
"StatusDetail",
"VendorTxCode",
"VPSTxId",
"TxAuthNo",
"Amount",
"AVSCV2", 
"AddressResult", 
"PostCodeResult", 
"CV2Result", 
"GiftAid", 
"3DSecureStatus", 
"CAVV" );

// Initialise arrays
$output = array();
$resultArray = array();

// Get the next token in the sequence
for ($i = count($Tokens)-1; $i >= 0 ; $i--){
// Find the position in the string
$start = strpos($thisString, $Tokens[$i]);
// If it's present
if ($start !== false){
// Record position and token name
$resultArray[$i]->start = $start;
$resultArray[$i]->token = $Tokens[$i];
}
}

// Sort in order of position
sort($resultArray);

// Go through the result array, getting the token values
for ($i = 0; $i<count($resultArray); $i++){
// Get the start point of the value
$valueStart = $resultArray[$i]->start + strlen($resultArray[$i]->token) + 1;
// Get the length of the value
if ($i==(count($resultArray)-1)) {
$output[$resultArray[$i]->token] = substr($thisString, $valueStart);
} else {
$valueLength = $resultArray[$i+1]->start - $resultArray[$i]->start - strlen($resultArray[$i]->token) - 2;
$output[$resultArray[$i]->token] = substr($thisString, $valueStart, $valueLength);
}      

}

// Return the ouput array
return $output;

}

// Randomise based on time
function randomise() {
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 100000);
}


?>

I am much in need of help to fix the problem in respect of the code or whether I am making mistakes in how I try to expose the value of the seemingly empty string, and therefore jumping to the wrong conclusion.

EDIT TUESDAY 18/12/18 ---------------

I have made some progress in that I have discovered the reason that the $_GET in the page "completed.php" was not obtaining any value at all from the page url in the return reply sent by Sagepay.

It was because the hosting platform's default php server settings only accepted up to 512 characters in the url; I was was able to change this to 2000 characters (see later comments) which fixed part the problem; the fatal error was gone but the decrypt still failed. However I could now debug because the functions now had data to work with and I could trace values in different parts of the script.

Unfortunately I am now completely at a loss in understanding the debug outputs - because in the first place I don't understand the decrypt functions at all despite searching for help.

The output seems to be reasonable as far as the decrypt line

$hex = substr($strIn, 1);

in "functions.php" which yields the content of the incoming crypt after the "@" has been stripped off.

But once the script moves to the line

$strIn = pack('H*', $hex);

it goes wrong because the output if the variable content now is littered with 'garbage' characters. I don't understand how the 'pack' works but I assume that the characters should all remain readable and therefore this is an encoding problem.

Link to image of a screenshot of the characters

The characters which display as a question mark within a black diamond in the image linked to above seem to be some of these these -

e? g!xh)̓G]/|CՖ'#]Ws܀͝Y?Ig@uQ*ߎ@KѦ

when I capture the text with a quick select-and-copy and then pass into a text editor.

But I don't know if the garbage characters are confined to those which are inserted by the 'pack' function and therefore the encoding mismatch is limited to just the function, rather than being an overall encoding issue with the submission and return data to and from Sagepay.

Unfortunately I am pretty bemused after a long long process of messing (trying everything) with encodings since the website move to the new host, changing scripts, headers, explicit encoding statements, script file encodings, php.ini encoding declarations, Mysql database encodings etc from the old (largely) ISO to utf_8. Mostly just trying to get rid of anomalous characters which are actually visible to users on the website. Squish one and you get a different one in its place.

So now my head throbs at the thought of tracking down how to deal with this if it is purely a encoding problem. Sagepay has told me that Form 3 is compatible with unicode but I know that this is contradicted by other advice I have received, and indeed my own experience having been through previous php and sagepay version changes at my old hosting provider.

There is no way that the website and database fundamentals are going to get changed back to ISO but if it is the case that I have to somehow let Sagepay alone dine on ISO, how can I do this most easily - what are the essentials?

The submit to Sagepay works just fine under utf-8, but do I have to change this to submit in ISO before I can specify ISO for the return which is where the real problem lies? And how best to force this ISO anyway - just for Sagepay - given that encoding often seem not to 'stick', a battleground of web technologies influencing the encoding.

On the other hand it would be great if it was just the 'pack' function that's out of kilter; and if there is an easy way or place to fix that. Can anyone advise please.


David
  • 11
  • 7
  • Obvious question, does `$_GET['crypt']` contain the encrypted data in the URL? If it does, and `$EncryptionPassword` is correct, then you can focus on what is inside `DHclassInFunc::decryptAes()`. – Jason Oct 14 '18 at 10:14
  • Hello @Jason - I had to set this aside for a while but I had been focussing on exploring possible issues with Unicode / ISO conflicts as a possible reason for the problem. I will look again to see if I can find enlightenment via your suggestion. Thanks. – David Oct 16 '18 at 20:04
  • When you encrypt the transaction request, it MUST be ISO 8859-1 encoded. The encrypted response will generally only contain ASCII anyway, but it is *likely* any non-ASCII characters will be ISO8859-1 and not UTF-8. I've recently update the [Omnipay Sage Pay driver](https://github.com/thephpleague/omnipay-sagepay) that may offer some clues. – Jason Oct 17 '18 at 12:18
  • Hello @Jason - I have tried declaring the encoding as ISO and as UTF but it does not affect the result. I have now posted an edit to my original question; the edit includes more information on the detail of what happens. I hope this might be allow you to suggest what I need to do. – David Dec 14 '18 at 14:25
  • "PHP Fatal error: Class 'SagepayApiException' not found in /foo/protx/functions.php on line 208" might be *happening* when you call "DHclassInFunc::decryptAes", but fixing it won't help UNLESS you also fix the issue that's *causing* the exception. Seems to me some general debugging (writing values to a log, for example) to find out just exactly what the new server thinks $_GET['crypt'] *is* would be a good start, as well as making sure that things work As Advertised with a "real" string that you supply directly to decryptAes() in the script for the sake of a test ... – Kevin_Kinsey Dec 14 '18 at 14:40
  • Hello @Kevin_Kinsey - my understanding is that the 'SagepayApiException' is happening in the decrypt section in the functions.php page and that it throws the error because either it is trying to work with an empty input (nothing received from the call in the completed.php page) or because it is failing the regex character check, perhaps because there is after all some sort of encoding issue with the characters in the sagepay return. But I don't seem to be able to echo anything to screen: how would I go about writing it to a log? – David Dec 14 '18 at 16:50
  • Hello @Kevin_Kinsey - sorry, update: yes I can after all echo to screen the value of $strIn in functions.php if the value assigned in the call from completed.php page is entered manually in the code as a text string. But when $_GET['crypt'] is assigned instead of a random text string the value is empty, ie it is not collecting anything from the url. So it looks like I have to focus on that but still not sure how to dig deeper into that. – David Dec 14 '18 at 17:13
  • print_r($_GET) ...? – Kevin_Kinsey Dec 14 '18 at 19:46
  • I have experimented with this in the line which calls the decrypt function instead of $_GET['crypt']; also used it on a new line on its own to assign the value to a test var but no method actually extracts any value from the url. It does echo any text variable which I hard code. Am I not correct that I should be able to echo the crypt value from the url if the function was indeed extracting something? I can't see that unicode / iso would make any difference - only that if there was a problem it would echo garbage text? One more thought: new host is https, old was not; could this be a barrier? – David Dec 15 '18 at 16:41
  • I'm not familiar with `DHclassInFunc::decryptAes`, but does it remove the leading "@" before trying to decode the `crypt` string? And "protx" in the path - just how old is the library you are using? protx was rebranded as Sage Pay a decade ago. – Jason Dec 16 '18 at 00:49
  • Hello @Jason - yes it does remove the '@'. Full code of the class can be seen in "functions.php" code in my edit to the original question. The website and the sagepay code goes back more than ten years but it has been updated at times to higher versions of php and to version 3 of Sagepay Form. The "protx" folder name is not important because it contains the Sagepay.3 versions of the code. During revisions there has been much painful (for me) updating the code - but was working perfectly before hosting move. There are no **code** changes to explain why the $_Get line now yields an empty string. – David Dec 16 '18 at 11:49
  • I'm not sure why `$strIn` is being run through `htmlspecialchars()`. That function is used to create HTML entities for some characters, and you should not need to do that here. Otherwise all I can suggest is stepping through the decode function one line at a time and follow the data through. – Jason Dec 17 '18 at 12:17
  • Hello @Jason, I agree, can't see the need for it but I have tried removing it and makes no difference. I'd love to be able to follow the data as you suggest - but the issue is that there is never any data to be follow because the $_GET['crypt'] fails to get any value at all from the return url; please see the exchanges with Kevin_Kinsey. – David Dec 17 '18 at 14:57
  • PROGRESS! Pursuing possible reasons for $_GET being empty, I wondered if it was to do with url length settings on the server at the new host. I found and followed this posting on Stackoverflow: https://stackoverflow.com/questions/7724270/max-size-of-url-parameters-in-get and added the setting to the php.ini and user.ini files. GET is now fixed! There is still an issue with the decoding but now I can at least debug this because I no longer get the Fatal Error in the decode section in the file "functions.php". I will give a more complete account - as an answer - once it is fully working. – David Dec 17 '18 at 17:12
  • Hello @Jason, if you can bear with me a little longer, I have now edited the main post again to include what I have found since I have been able to collect the crypt contents from the url. I think that the decryption 'pack' function is adding characters with the wrong encoding to the contents of the $strIn variable which is then supplied to the final mcrypt_decrypt function. Any idea why this might happen and how I can fix it? More details are in the edit. – David Dec 18 '18 at 16:57
  • The `htmlspecialchars()` in `decryptAes()` - remove it - this has nothing to do with HTML. The `crypt` string is ASCII - it will look the same in any character encoding. It is just hexadecimal. The packing and unpacking - throw that away - it's old PHP stuff. Use openSSL - like this: https://github.com/thephpleague/omnipay-sagepay/blob/master/src/Message/Form/CompleteAuthorizeRequest.php#L47 – Jason Dec 19 '18 at 17:18
  • Hello @Jason - thank you for this, it gives me something nourishing to chew on instead of gnashing my teeth trying to work out why the old stuff has stopped working (although the website is running on php 5.6, so in theory not that different to the previous host). Tomorrow I will tuck in and try to integrate the changes you suggest. My scouting for answers had already come across the ssl methods but without your signposting it for me I would be very uncertain that it was appropriate for the Sagepay case. – David Dec 19 '18 at 20:10
  • Hello@Jason, I am using your code example for openSSL for the decrypt; it is still called from the 'completed.php' page where the $decoded var now dumps an array which contains the expected values but I can't get the values out of $decoded and into the $values variables needed for further processing. The exisinting code used – David Dec 20 '18 at 21:02
  • Hello@Jason - Sorry time out. I am using your code example for openSSL for the decrypt; it is still called from the 'completed.php' page where the $decoded var now has success in dumping an array which contains the expected values but I can't get the values out of $decoded and into the $values variables needed for further processing. The existing code uses $values = getToken($Decoded); - but the values which are in the array are not acquired. Do I need to do something to 'tokenise' the array which is output from the openSSL function? – David Dec 20 '18 at 21:14
  • My posted answer should provide an array in `$data`. Try that first. If that works, then that's your solution. – Jason Dec 21 '18 at 10:21
  • Hello @Jason - yes I do get the array in $data which I return to the calling page 'completed.php' where it can be received into either $data or into $decoded. The var name I use makes no difference; it is not processed by the line $values = getToken($data) where the $values vars remain empty. I guess that getToken (I don't understand it) is not appropriate when the decrypt is done by openSSL and I will need to use some other function to use the values from the $data array. I will post the array content in a further comment. – David Dec 21 '18 at 13:32
  • Hello @Jason - here the array vardump for $data (snipped to fit in here): array(17) { ["VendorTxCode"]=> string(36) "5531525654755525653961079671878Z2442" ["VPSTxId"]=> string(38) "{F2057F97-0B75-A671-98C0-454C62B7F3C1}" ["Status"]=> string(2) "OK" ["StatusDetail"]=> string(40) "0000 : The Authorisation was Successful." ["TxAuthNo"]=> string(8) "26528423" ["AVSCV2"]=> string(9) "ALL MATCH" ["AddressResult"]=> string(7) "MATCHED" ["PostCodeResult"]=> string(7) "MATCHED" ["CV2Result"]=> string(7) "MATCHED" ["GiftAid"]=> string(1) "0" ["3DSecureStatus"]=> string(2) "OK" ["CAVV"]=> – David Dec 21 '18 at 13:36
  • Hello @Jason - YES! FIXED. I dropped the code which collected the values from $data via the getToken segment and instead collected each value directly from the $data array via $Status = ($data['Status']); and $VendorTxCode = ($data['VendorTxCode']); etc. Many thanks for you for your help and sorry if it has been long-winded on my part. – David Dec 21 '18 at 16:25

1 Answers1

2

As implemented in the Omnipay Sage Pay driver (Omnipay Common v3.x) https://github.com/thephpleague/omnipay-sagepay/blob/master/src/Message/Form/CompleteAuthorizeRequest.php#L47

$crypt = $_GET['crypt'];

// Remove the leading '@' and decrypt the remainder into a query string.

$hexString = substr($crypt, 1);

// Last minute check to make sure we have data that looks sensible.

if (! preg_match('/^[0-9a-f]+$/i', $hexString)) {
    throw new \Exception('Invalid "crypt" parameter; not hexadecimal');
}

// Decrypt the crypt string.

$queryString = openssl_decrypt(
    hex2bin($hexString),
    'aes-128-cbc',
    $yourEncryptionKey,
    OPENSSL_RAW_DATA,
    $yourEncryptionKey
);

// Parse ...&VPSTxId={AE43BAA6-52FF-0C30-635B-2D5E13B75ACE}&...
// into an array of values.

parse_str($queryString, $data);

var_dump($data);

/*
array(17) {
  ["VendorTxCode"]=>
  string(19) "your-original-unique-id"
  ["VPSTxId"]=>
  string(38) "{AE43BAA6-52FF-0C30-635B-2D5E13B75ACE}"
  ["Status"]=>
  string(2) "OK"
  ["StatusDetail"]=>
  string(40) "0000 : The Authorisation was Successful."
  ["TxAuthNo"]=>
  string(6) "376048"
  ["AVSCV2"]=>
  string(24) "SECURITY CODE MATCH ONLY"
  ["AddressResult"]=>
  string(10) "NOTMATCHED"
  ["PostCodeResult"]=>
  string(10) "NOTMATCHED"
  ["CV2Result"]=>
  string(7) "MATCHED"
  ["GiftAid"]=>
  string(1) "0"
  ["3DSecureStatus"]=>
  string(10) "NOTCHECKED"
  ["CardType"]=>
  string(4) "VISA"
  ["Last4Digits"]=>
  string(4) "0006"
  ["DeclineCode"]=>
  string(2) "00"
  ["ExpiryDate"]=>
  string(4) "1220"
  ["Amount"]=>
  string(5) "99.99"
  ["BankAuthCode"]=>
  string(6) "999777"
}
*/

PHP 7 no longer supports the older encryption/decryption functions that the official Sage Pay library (and many plugins based off that old code) use. Use openssl functions instead.

Everything returned in $data will be ASCII (it will return well-defined IDs and codes only, and no user-entered data). I don't believe it will contain any extended ASCII characters, so can be treated as UTF-8 without any conversion if desired.

Jason
  • 4,411
  • 7
  • 40
  • 53
  • 1
    This has saved me a _great_ deal of time. I'd have spent a long time debugging removePKCS5Padding otherwise. Thanks! – PaulSkinner Jan 23 '19 at 11:32