David David - 1 month ago 16
PHP Question

Website to Sagepay submit encryption code was working but now fails after server php upgrade

I completed my website's v.3 Sagepay upgrade in April this year and it has been working fine.

But now my web hosting provider has upgraded its PHP version from 5.2.4 to 5.5.9 which has caused major disruption of the website.

I have managed to fix the most dramatic (like all the dynamic product listings and details pages displaying blank) and I am left with a failure on submitting to Sagepay.

More exactly, as soon as the Submit button is pressed, a page is presented which contains a text string which displays most of the code in the functions.php page which handles the encryption of the submission to Sagepay; and decryption of the response from Sagepay. (Fortunately the string does not include the encryption password.) The submission can't progress of course.

The code in this page is a mix of script from the previous version of SagePay's documentation, blended or replaced with updated scripts for version 3 - in fact as I recall it largely utilised an encryption example from a contributor here on Stackoverflow.

Here is the code in the page (I am sorry if I should have left out the commenting which is mostly from SagePay - I am not sure if it frowned upon for posts here):

//************ 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)
{
// 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))
{
throw new SagepayApiException('Invalid encryption string');
}
$strIn = pack('H*', $hex);

// Perform decryption with PHP's MCRYPT module.
$string = 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 would mention an earlier issue with this page which I think is related and perhaps might help point to the source of the problem -

After a trial switch of the server to the new PHP version - whereupon the product displays all disappeared - I reverted to the old version of PHP (reversion was allowed during a testing period but now the upgrade is final). After the reversion the site displays returned to normal but the Sagepay functions were not working correctly - although in that case the Submit to Page worked fine but the Response failed when it hit the function.php page

In that case the eror message cited the line of 'throw new SagepayApiException('Invalid encryption string');' in the segment below, which is in the return part of the page script.

// Throw exception if string is malformed
if (!preg_match('/^[0-9a-fA-F]+$/', $hex))
{
throw new SagepayApiException('Invalid encryption string');
}
$strIn = pack('H*', $hex);


With my minimal php skills overall, and debugging in particular, it seemed to me that in fact it was in the few lines just before that something was going wrong, rather than the returned crypt string itself being faulty.

I copied the crypt string from the url on the page which displayed the return error and tested it against the match string in the the preg-match function by hardcoding the return string as the input instead of $hex and the script then ran beyond the error trap and instead halted at other instances of the line 'throw new SagepayApiException('Invalid encryption string');'

This left me with the conclusion that there was an issue with the preceding segment, namely -

static public function decryptAes($strIn, $password)
{
// 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);


However, I could not work out why this should be, given that the code in this page had not been touched and therefore should be working as it was previously, before the temporary switch to php 5.5.x and then back again 5.2.x.

Finally I thought to try one more thing: during the server php version changes the server automatically disabled my site php.in file (I don't know a great deal about it other than it can be used to alter certain default characteristics of the php version being run by the server). I had reinstated the php.ini file but by now there were quite a few disabled 2.2 ones kicking about in that location.

What about trying another one? ZAP, problem fixed.

However I can't see (or rather understand) whether there is anything in the new default 5.5.9 server ini file settings which might be the root of the latest problem with the encryption code, other than that my local php.ini file set the default_charset to iso-8859-1 for 2.2.4 whereas the default in 5.5.9 is utf-8.

I have now set the default_charset to iso-8859-1 in the 5.5.9 ini file as well but although it has fixed £s displaying as ? in the website content it has had no effect on the SagePay submit error.

Any suggestions please to fix this severe problem (website currently unable to process any payments ..)?

UPDATE 23/10/16 - the problem has changed; but help is still needed

Submit to Sagepay now solved but I still need help because now I can't process the response from Sagepay. The response is sent from SagePay and received by my response file but it results in an error during processing of the returned encrypted string.

As a result the user's browser window appears to hang - it is blank - and the user is not returned to my website.None one of the website's automatic handling of the payment result are initiated because of an error in the decryption process.

The error occurs at the line in the functions.php file which contains the the code which I have already posted in this question. It is generated at the line 184 and the error is:

PHP Fatal error: Class 'SagepayApiException' not found in /web/sites/user/3/84/42307/public/www/My_folder/protx/functions.php on line 184


Line 184 is the line below:

throw new SagepayApiException('Invalid encryption string');


in the following segment:

// Throw exception if string is malformed
if (!preg_match('/^[0-9a-fA-F]+$/', $hex))
{
throw new SagepayApiException('Invalid encryption string');
}
$strIn = pack('H*', $hex);


Immediately before the fatal error there is this line in the web server logs:

PHP Notice: Undefined variable: crypt in /web/sites/user/3/84/42307/public/www/myfolder/protx/completed.php on line 30


completed.php is the file in the return url to which the Sagepay response is directed (and which has the functions.php file included).

Line 30 in the file completed.php file is:

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


I've been working on this all weekend and last night after I got the submit to Sageway working again I though I was getting there (and at least the website can take payments again).

By the way, it looks like the key to the fix on that side was replacing a lot of instances of the function htmlspecialchars in the files preceding the encryption file, so that they now used the new syntax required in php 5.5.9 which can include the character set, as in the example below:

htmlspecialchars($rs->Fields($kartProps['Korders']['orders']['email']), ENT_COMPAT,'ISO-8859-1', true);


Also I changed the default UTF-8 encoding in the php.ini file to ISO-8859-1 via the local character_set setting in the .ini file.

I am reasoning that there is still some sort of character_set clash happening in the decryption of the Sagepay response file and this is causing the preg_match function to fail.

I have pretty much exhausted by abilities to work out what is going on and I would greatly appreciate any suggestions.

SOLVED

The decryption scripts were not working because they were not receiving any data from the contents of the url in the response sent from Sagepay.

This response should be received in the file 'completed.php' which then passes the response string to the included 'functions.php' which contains the decryption code.

In the completed.php file the following line which extracted the url contents used to work under php 5.2.4:

$Decoded = DHclassInFunc::decryptAes($crypt,$EncryptionPassword); this worked in 5.5.3 but not 5.5.59


I changed this to:

$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);// HURRAH ! finally the scripts get the url contents so now works in php 5.5.9.


Probably elementary to a competent php coder; but at least I got there.

Answer

SOLVED

The decryption scripts were not working because they were not receiving any data from the contents of the url in the response sent from Sagepay.

This response should be received in the file 'completed.php' which then passes the response string to the included 'functions.php' which contains the decryption code.

In the completed.php file the following line which extracted the url contents used to work under php 5.2.4:

$Decoded = DHclassInFunc::decryptAes($crypt,$EncryptionPassword); this worked in 5.5.3 but not 5.5.59 I changed this to:

$Decoded = DHclassInFunc::decryptAes(($_GET['crypt']),$EncryptionPassword);// HURRAH ! finally the scripts get the url contents so now works in php 5.5.9. Probably elementary to a competent php coder; but at least I got there.

Comments