Sagepay 2.22 to 3.00 form upgrade - PHP update to AES encryption

871 views Asked by At

I've been trying with no avail to understand how to upgrade the encryption used in the code for my company's three ecommerce sites from simpleXor to an AES encryption. Without doing this, I cannot upgrade the rest of the code which means after July we won't be able to take any payments online.

I've managed to update the crypt form codes in line with the upgrade to 3.00.

I can identify the encryption code and have looked through the form integration demo download for PHP from Sagepay but can't find anything that looks remotely similar to my encryption code?!

Could somebody point me in the right direction for finding a suitable encryption code to replace the old one with!?

Our sites are based on JShop and I have one file for sending and a response file.

This is the file for sending info:

    <?php   function startProcessor($orderNumber) {     global $dbA,$orderArray,$jssStoreWebDirHTTP,$jssStoreWebDirHTTPS,$cartMain;             $callBack = "$jssStoreWebDirHTTPS"."gateways/response/protx.php";               $cDetails = returnCurrencyDetails($orderArray["currencyID"]);       $gatewayOptions = retrieveGatewayOptions("PROTX");      switch ($gatewayOptions["testMode"]) {          case "S":               $myAction = "https://test.sagepay.com/Simulator/VSPFormGateway.asp";                break;          case "Y":               $myAction = "https://test.sagepay.com/gateway/service/vspform-register.vsp";                break;          case "N":               $myAction = "https://live.sagepay.com/gateway/service/vspform-register.vsp";                break;      }       $myVendor = $gatewayOptions["vendor"];      $myEncryptionPassword = $gatewayOptions["encryptionPassword"];              $billingAddress  = $orderArray["address1"]."\n";        if ($orderArray["address2"] != "") {            $billingAddress .= $orderArray["address2"]."\n";        }       $billingAddress .= $orderArray["town"]."\n";        $billingAddress .= $orderArray["county"]."\n";      $billingAddress .= $orderArray["country"];      $deliveryAddress  = $orderArray["deliveryAddress1"]."\n";       if ($orderArray["deliveryAddress2"] != "") {            $deliveryAddress .= $orderArray["deliveryAddress2"]."\n";       }       $deliveryAddress .= $orderArray["deliveryTown"]."\n";       $deliveryAddress .= $orderArray["deliveryCounty"]."\n";     $deliveryAddress .= $orderArray["deliveryCountry"];                     $crypt = "VendorTxCode=$orderNumber";
        $crypt .= "&Amount=".number_format($orderArray["orderTotal"],$cDetails["decimals"],'.','');
        $crypt .= "&Currency=".@$cDetails["code"];
        $crypt .= "&Description=".$gatewayOptions["description"];
        $crypt .= "&SuccessURL=$callBack?xOid=$orderNumber&xRn=".$orderArray["randID"];
        $crypt .= "&FailureURL=$callBack?xOid=$orderNumber&xRn=".$orderArray["randID"];
        $crypt .= "&BillingSurname=".$orderArray["surname"];
        $crypt .= "&BillingFirstnames=".$orderArray["forename"];
        $crypt .= "&BillingAddress1=".$orderArray["address1"];
        $crypt .= "&BillingCity=".$orderArray["town"];
        $crypt .= "&BillingPostCode=".preg_replace("/[^\s\-a-zA-Z0-9]/", "", $orderArray["postcode"]);
        $crypt .= "&BillingCountry=".$orderArray["country"];
        $crypt .= "&DeliverySurname=".&orderArray["surname"];
        $crypt .= "&DeliveryFirstnames=".&orderArray["forename"];

    if ($orderArray["deliveryPostcode"] != "") {
        $crypt .= "&DeliveryAddress1=".$orderArray["deliveryAddress1"];
        $crypt .= "&DeliveryCity=".$orderArray["deliveryTown"];
        $crypt .= "&DeliveryPostCode=".preg_replace("/[^\s\-a-zA-Z0-9]/", "", $orderArray["deliveryPostcode"]);
        $crypt .= "&DeliveryCountry=".$orderArray["deliveryCountry"]; }
    else {
        $crypt .= "&DeliveryAddress1=".$orderArray["address1"];
        $crypt .= "&DeliveryCity=".$orderArray["town"];
        $crypt .= "&DeliveryPostCode=".preg_replace("/[^\s\-a-zA-Z0-9]/", "", $orderArray["postcode"]);
        $crypt .= "&DeliveryCountry=".$orderArray["country"]; }

        $crypt .= "&BillingPhone=".preg_replace("/[^\sa-zA-Z0-9]/", "", $orderArray["telephone"]);      
if ($gatewayOptions["sendEmail"] == 1) {            
        $crypt .= "&CustomerEmail=".$orderArray["email"];       }
        $crypt .= "&VendorEmail=".$gatewayOptions["vendorEmail"];   
        $crypt .= "&ApplyAVSCV2=".$gatewayOptions["cvvCheck"];  
        $crypt .= "&Apply3DSecure=".$gatewayOptions["3DSecure"];            
        $crypt = base64_encode(protx_simpleXor($crypt,$myEncryptionPassword));      
        $tpl = createTSysObject(templatesCreatePath($cartMain["templateSet"]),"gatewaytransfer.html",$requiredVars,0);          
        $gArray["method"] = "POST"; 
        $gArray["action"] = $myAction;  
        $gArray["fields"][] = array("name"=>"VPSProtocol","value"=>"3.00"); 
        $gArray["fields"][] = array("name"=>"Vendor","value"=>$myVendor);   
        $gArray["fields"][] = array("name"=>"TxType","value"=>$gatewayOptions["txType"]);               $gArray["fields"][] = array("name"=>"Crypt","value"=>$crypt);           
        $mArray = $gArray;          
        $gArray["process"] = "document.automaticForm.submit();";    
        $tpl->addVariable("shop",templateVarsShopRetrieve());   
        $tpl->addVariable("labels",templateVarsLabelsRetrieve());   
        $tpl->addVariable("automaticForm",$gArray); 
        $tpl->addVariable("manualForm",$mArray);    
        $tpl->showPage();   }

function protx_simpleXor($inString, $key) {     $outString="";      $l=0;       if (strlen($inString)!=0) {         for ($i = 0; $i < strlen($inString); $i++) {                $outString=$outString . ($inString[$i]^$key[$l]);               $l++;               if ($l==strlen($key)) { $l=0; }         }       }       return $outString;  }   ?>

This is the response file:

 <?php

/*================ JShop Server ================

  = (c)2003-2010 Whorl Ltd.                    =

  = All Rights Reserved                        =

  = Redistribution of this file is prohibited. =

  = http://www.jshop.co.uk/                    =

  ==============================================*/

?><?php

    define("IN_JSHOP", TRUE);

    include("../../static/config.php");

    include("../../routines/dbAccess_".$databaseType.".php");

    include("../../routines/tSys.php");

    include("../../routines/general.php");

    include("../../routines/stockControl.php");

    include("../../routines/emailOutput.php");



    dbConnect($dbA);



    $orderID = makeSafe(getFORM("xOid"));

    $newOrderID = $orderID;

    $randID = makeSafe(getFORM("xRn"));

    $crypt = makeSafe(getFORM("crypt"));



    $gatewayOptions = retrieveGatewayOptions("PROTX");



    $orderID = makeInteger($orderID) - retrieveOption("orderNumberOffset");



    $result =  $dbA->query("select * from $tableOrdersHeaders where orderID=$orderID and randID='$randID'");

    if ($dbA->count($result) == 0 || $crypt=="") {

        doRedirect_JavaScript($jssStoreWebDirHTTP."index.php");

        exit;

    }

    $orderArray = $dbA->fetch($result);

    $ccResult = $dbA->query("select * from $tablePaymentOptions where paymentID=".$orderArray["paymentID"]);

    $poRecord = $dbA->fetch($ccResult);

    $paidStatus = $poRecord["statusID"];



    $crypt = str_replace(" ","+",$crypt);

    $crypt = protx_simpleXor(base64_decode($crypt),$gatewayOptions["encryptionPassword"]);  



    $nameValues = explode("&",$crypt);

    $resultCode = "";

    for ($f = 0; $f < count($nameValues); $f++) {

        $thisCode = explode("=",$nameValues[$f]);

        $resultCode[$thisCode[0]] = $thisCode[1];

    }



    if ($resultCode["VendorTxCode"] != $newOrderID) {

        doRedirect_JavaScript($jssStoreWebDirHTTP."index.php");

        exit;

    }



    $authResponse = "&Status Result=".$resultCode["Status"]."&AVS/CV2 Check=".@$resultCode["AVSCV2"]."&Address Result=".@$resultCode["AddressResult"]."&Postcode Result=".@$resultCode["PostCodeResult"]."&CV2 Result=".@$resultCode["CV2Result"]."&3d Secure Status=".@$resultCode["3DSecureStatus"];



    $randID = $orderArray["randID"];

    if ($orderArray["status"] != $paidStatus) {

            $dt=date("YmdHis",createOffsetTime());

            switch ($resultCode["Status"]) {

                case "OK":

                case "AUTHENTICATED":

                case "REGISTERED":

                    $authResponse="Gateway=Sage Pay&Authorisation Code=".$resultCode["TxAuthNo"]."&Sage Pay Transaction ID=".$resultCode["VPSTxId"]."&Status=Payment Confirmed".$authResponse;

                    $dbA->query("update $tableOrdersHeaders set status=$paidStatus, authInfo=\"$authResponse\", paymentDate=\"$dt\" where orderID=$orderID");

                    $orderArray["status"] = $paidStatus;

                    //ok, this is where we should do the stock control then.

                    include("process/paidProcessList.php");

                    doRedirect_JavaScript($jssStoreWebDirHTTPS."process.php?xOid=$newOrderID&xRn=$randID");

                    break;

                case "REJECTED":
                    $authResponse="Gateway=Sage Pay&Status=Payment Rejected Due To Rules".$authResponse;
                    $dbA->query("update $tableOrdersHeaders set status=3, authInfo=\"$authResponse\", paymentDate=\"$dt\" where orderID=$orderID");
                    include("process/failProcessList.php");
                    doRedirect_JavaScript($jssStoreWebDirHTTPS."process.php?xOid=$newOrderID&xRn=$randID");
                    break;

                default:
                    if ($orderArray["status"] == 1) {
                        $authResponse="Gateway=Sage Pay&Status=Payment Failed".$authResponse;
                        $dbA->query("update $tableOrdersHeaders set status=3, authInfo=\"$authResponse\", paymentDate=\"$dt\" where orderID=$orderID");
                        include("process/failProcessList.php");
                }

                    doRedirect_JavaScript($jssStoreWebDirHTTPS."process.php?xOid=$newOrderID&xRn=$randID");
                    break;
            }
    } else {
            doRedirect_JavaScript($jssStoreWebDirHTTPS."process.php?xOid=$newOrderID&xRn=$randID");
    }

    function protx_simpleXor($inString, $key) {
        $outString="";

        $l=0;

        if (strlen($inString)!=0) {
            for ($i = 0; $i < strlen($inString); $i++) {
                $outString=$outString . ($inString[$i]^$key[$l]);
                $l++;

                if ($l==strlen($key)) { $l=0; }
            }
        }

        return $outString;
    }
?>
1

There are 1 answers

4
Phil On BEST ANSWER

It looks like you are using very old code. I would recommend you try to completely port/rewrite your code using the official Sage Pay integration libraries to avoid strange bugs with things like escaping.

If you're in a rush, here is a stripped down version of the class you need to get the encryption/decryption part working.

<?php
class SagepayUtil
{
    /**
     * 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);
    }
}

Setup your variables as you do now.

$gatewayOptions["encryptionPassword"] = "PASSWORDPASSWORD";
$crypt = "Your=Sagepay&Query=String";

Then for encryption (sending to sagepay) use this

//$crypt = base64_encode(protx_simpleXor($crypt,$myEncryptionPassword));
$crypt = SagepayUtil::encryptAes($crypt, $gatewayOptions["encryptionPassword"]);

var_dump($crypt);
//@714DCF7FE82FADA4B9F8CF53982D56FFC8F2021A5FC10481409ED1FD7BF6880E

And for decryption (receiving from sagepay) use this

//$crypt = protx_simpleXor(base64_decode($crypt),$gatewayOptions["encryptionPassword"]);  
$crypt = SagepayUtil::decryptAes($crypt, $gatewayOptions["encryptionPassword"]);

var_dump($crypt);
//Your=Sagepay&Query=String

Edit: Removed the base64encoding layer. Protx will assume a base64 encoded string is using the old XOR method so you must not use base64 encoding. Instead it is necessary to use hex encoding and start the string with the "@" character.