首页 > 解决方案 > 我的 php 脚本未解密 Sagepay Form 3.00 版中的返回 crypt

问题描述

我已将我的网站移至新的托管服务提供商,我的 Sagepay Form v3 脚本接收加密响应现在失败。

在以前的托管服务提供商处,脚本正在运行(php 版本是 5.5.9),而新的托管服务提供了从 5.4 到 6 的选择。在第一个托管服务提供商处,很久以前的 php 版本是 5.2(或者可能是5.3),当他们最终强制更改为 5.5 时,它破坏了我的网站脚本中的很多东西,导致尝试修复它们非常困难,我最终实现了这一点。

其中一件事是解密失败,就像它现在再次这样做一样。在那种情况下,我最终通过更改解密行来修复它:

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

至:

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

我尝试了许多其他变体,但只有最后一个有效。

所以现在问题又回来了,我完全不知所措。我已经尝试了所有以前的变体,但没有任何效果。还有我的新主机上提供的各种 php 版本。

上次我的(长)问题也在这里发布:see Website to Sagepay submit encryption code was working but now failed after server php upgrade

谁能提出为什么这次失败以及我能做些什么来解决它?

编辑 18年 12 月 14 日调查后的更多信息加上我包括更多解释和来自两个相关脚本的完整代码------------

我没有取得任何进展,并且必须在 Sagepay 退货不起作用时手动管理网站订单。现在我有一点时间,所以我再试一次。

我现在发现,如果我在“completed.php”页面(Sagepay 响应指向的 url)上删除这一行(下面),脚本不会挂起;然而这是因为它是导致致命错误的那一行。

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

没有该行和由此产生的错误,scipt 能够继续并调用以下页面(“return.php”),然后向客户显示付款结果信息并执行其他操作(例如将完整的订单详细信息发送到我们的本地 - 不是在互联网上 - 数据库)。

但是,删除该行后,不会处理 url 中的 crypt,因此在完成的.php 页面转发到 return.php 页面的结果变量中没有值。

这意味着 $status 变量为空;在 return.php 页面中,这被评估为错误,因此向客户显示一条消息,指出存在错误并且没有付款 - 这是不正确的。

缺少“成功”状态值也意味着 web mysql 数据库中的订单未标记为已确认。

我尝试了该行的许多其他变体,但均无济于事(尽管此处给出的变体在将网站移至新主机之前有效)。

该行当然会调用位于 functions.php 文件中的类“DHclassInFunc”中的函数。

我附在两个文件的活动代码下方,完成.php 和functions.php

据我所知,根本问题是这条线

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

在 'crypt' 中没有收到任何值,因此没有字符串供 function.php 解密例程处理,导致调用该函数时出现致命错误:“PHP 致命错误:在 /redacted 中找不到类 'SagepayApiException' /redacted/redacted.com/www/redacted/protx/functions.php 在第 208 行"

我在 functions.php 代码中添加了一行,如下所示:

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

为了尝试公开传递给函数的值,但它简单地打印 var 的名称,而不是在收到来自的响应时在 completed.php 页面的地址栏中的 url 内容中的值Sagepay - 例如:

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

关于它是否可能是一个 unicode / iso 问题,我不明白为什么这会导致 $strIn 中的空值,因为这还没有被处理,只是被捕获(或没有?)。

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";
?>

函数.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);
}


?>

我非常需要帮助来解决代码方面的问题,或者我是否在尝试公开看似空字符串的值时犯了错误,从而得出错误的结论。

编辑 2018 年 12 月 18 日星期二---------------

我已经取得了一些进展,因为我发现了页面“completed.php”中的 $_GET 根本没有从 Sagepay 发送的返回回复中的页面 url 中获取任何值的原因。

这是因为托管平台的默认 php 服务器设置只接受 url 中最多 512 个字符;我能够将其更改为 2000 个字符(请参阅后面的评论),从而解决了部分问题;致命错误消失了,但解密仍然失败。但是我现在可以调试了,因为这些函数现在有数据可以使用,而且我可以跟踪脚本不同部分的值。

不幸的是,我现在完全无法理解调试输出——因为首先,尽管寻求帮助,但我根本不理解解密功能。

就解密行而言,输出似乎是合理的

$hex = substr($strIn, 1);

在“functions.php”中,它在“@”被剥离后产生传入 crypt 的内容。

但是一旦脚本移动到该行

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

它出错了,因为如果变量内容现在到处都是“垃圾”字符,则输出。我不明白“包”是如何工作的,但我认为这些字符都应该保持可读性,因此这是一个编码问题。

链接到人物截图的图片

在上面链接的图像中,在黑色菱形中显示为问号的字符似乎是其中的一些 -

e? g!xh)̓G]/|CՖ'#]Ws?

当我通过快速选择和复制捕获文本然后传递到文本编辑器时。

但我不知道垃圾字符是否仅限于“pack”函数插入的那些字符,因此编码不匹配仅限于函数,而不是提交和返回数据的整体编码问题和来自 Sagepay。

不幸的是,自从网站移动到新主机、更改脚本、标头、显式编码语句、脚本文件编码、php.ini 编码声明、Mysql 数据库编码等之后,我很困惑(尝试一切)编码从旧的(主要是)ISO 到 utf_8。主要是试图摆脱网站上用户实际可见的异常字符。压扁一个,你会得到一个不同的。

因此,如果这纯粹是一个编码问题,那么现在我一想到要追查如何处理这个问题就头疼不已。Sagepay 告诉我 Form 3unicode 兼容,但我知道这与我收到的其他建议相矛盾,实际上我自己的经验是在我的旧托管服务提供商处经历过以前的 php 和 sagepay 版本更改。

网站和数据库基础不可能改回 ISO,但如果我不得不以某种方式让 Sagepay 独自在 ISO 上用餐,我怎样才能最容易地做到这一点 - 什么是基本要素?

提交到 Sagepay 在 utf-8 下工作得很好,但是我是否必须将其更改为在 ISO 中提交,然后才能指定 ISO 作为回报,这才是真正的问题所在?以及如何最好地强制使用这个 ISO - 仅适用于 Sagepay - 鉴于编码通常似乎不会“坚持”,这是影响编码的网络技术的战场。

另一方面,如果只是“包装”功能不合时宜,那就太好了。如果有一个简单的方法或地方来解决这个问题。任何人都可以建议。


标签: phpencryptionopayo

解决方案


在 Omnipay Sage Pay 驱动程序(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 不再支持官方 Sage Pay 库(以及许多基于该旧代码的插件)使用的旧加密/解密功能。改用openssl函数。

返回的所有$data内容都是 ASCII(它将仅返回定义明确的 ID 和代码,不返回用户输入的数据)。我不相信它会包含任何扩展的 ASCII 字符,因此如果需要,可以将其视为 UTF-8 而无需任何转换。


推荐阅读