Skip to content

Commit

Permalink
Implementation of callback data decryption
Browse files Browse the repository at this point in the history
Implementation of callback data decryption
  • Loading branch information
Poltoruhin authored Apr 13, 2022
1 parent 4ca6755 commit e694b4e
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 30 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
Version history
===============
Version 1.8 - 2022-04-13

* added callback data decryption functionality

Version 1.7 - 2022-04-11

Expand Down
79 changes: 69 additions & 10 deletions WebToPay.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ class WebToPayException extends Exception {
* Errors in remote service - it returns some invalid data
*/
const E_SERVICE = 10;

/**
* Deprecated usage errors
*/
Expand Down Expand Up @@ -1218,17 +1218,29 @@ class WebToPay_CallbackValidator {
*/
protected $projectId;

/**
* @var string|null
*/
protected $password;

/**
* Constructs object
*
* @param integer $projectId
* @param integer $projectId
* @param WebToPay_Sign_SignCheckerInterface $signer
* @param WebToPay_Util $util
* @param WebToPay_Util $util
* @param string|null $password
*/
public function __construct($projectId, WebToPay_Sign_SignCheckerInterface $signer, WebToPay_Util $util) {
public function __construct(
$projectId,
WebToPay_Sign_SignCheckerInterface $signer,
WebToPay_Util $util,
$password = null
) {
$this->signer = $signer;
$this->util = $util;
$this->projectId = $projectId;
$this->password = $password;
}

/**
Expand All @@ -1243,16 +1255,32 @@ public function __construct($projectId, WebToPay_Sign_SignCheckerInterface $sign
* @throws WebToPay_Exception_Callback
*/
public function validateAndParseData(array $requestData) {
if (!$this->signer->checkSign($requestData)) {
throw new WebToPay_Exception_Callback('Invalid sign parameters, check $_GET length limit');
}

if (!isset($requestData['data'])) {
throw new WebToPay_Exception_Callback('"data" parameter not found');
}

$data = $requestData['data'];

$queryString = $this->util->decodeSafeUrlBase64($data);
if (isset($requestData['ss1']) || isset($requestData['ss2'])) {
if (!$this->signer->checkSign($requestData)) {
throw new WebToPay_Exception_Callback('Invalid sign parameters, check $_GET length limit');
}

$queryString = $this->util->decodeSafeUrlBase64($data);
} else {
if (null === $this->password) {
throw new WebToPay_Exception_Configuration('You have to provide project password');
}

$queryString = $this->util->decryptGCM(
$this->util->decodeSafeUrlBase64($data),
$this->password
);

if (null === $queryString) {
throw new WebToPay_Exception_Callback('Callback data decryption failed');
}
}
$request = $this->util->parseHttpQuery($queryString);

if (!isset($request['projectid'])) {
Expand Down Expand Up @@ -1843,10 +1871,12 @@ public function getCallbackValidator() {
if (!isset($this->configuration['projectId'])) {
throw new WebToPay_Exception_Configuration('You have to provide project ID');
}

$this->callbackValidator = new WebToPay_CallbackValidator(
$this->configuration['projectId'],
$this->getSigner(),
$this->getUtil()
$this->getUtil(),
isset($this->configuration['password']) ? $this->configuration['password'] : null
);
}
return $this->callbackValidator;
Expand Down Expand Up @@ -2296,6 +2326,9 @@ public function get($uri, array $queryData = array()) {
*/
class WebToPay_Util {

const GCM_CIPHER = 'aes-256-gcm';
const GCM_AUTH_KEY_LENGTH = 16;

/**
* Decodes url-safe-base64 encoded string
* Url-safe-base64 is same as base64, but + is replaced to - and / to _
Expand All @@ -2320,6 +2353,32 @@ public function encodeSafeUrlBase64($text) {
return strtr(base64_encode($text), array('+' => '-', '/' => '_'));
}

/**
* Decrypts string with aes-256-gcm algorithm
*
* @param string $stringToDecrypt
* @param string $key
*
* @return string|null
*/
function decryptGCM($stringToDecrypt, $key) {
$ivLength = openssl_cipher_iv_length(self::GCM_CIPHER);
$iv = substr($stringToDecrypt, 0, $ivLength);
$ciphertext = substr($stringToDecrypt, $ivLength, -self::GCM_AUTH_KEY_LENGTH);
$tag = substr($stringToDecrypt, -self::GCM_AUTH_KEY_LENGTH);

$decryptedText = openssl_decrypt(
$ciphertext,
self::GCM_CIPHER,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);

return $decryptedText === false ? null : $decryptedText;
}

/**
* Parses HTTP query to array
*
Expand Down
46 changes: 37 additions & 9 deletions src/WebToPay/CallbackValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,29 @@ class WebToPay_CallbackValidator {
*/
protected $projectId;

/**
* @var string|null
*/
protected $password;

/**
* Constructs object
*
* @param integer $projectId
* @param integer $projectId
* @param WebToPay_Sign_SignCheckerInterface $signer
* @param WebToPay_Util $util
* @param WebToPay_Util $util
* @param string|null $password
*/
public function __construct($projectId, WebToPay_Sign_SignCheckerInterface $signer, WebToPay_Util $util) {
public function __construct(
$projectId,
WebToPay_Sign_SignCheckerInterface $signer,
WebToPay_Util $util,
$password = null
) {
$this->signer = $signer;
$this->util = $util;
$this->projectId = $projectId;
$this->password = $password;
}

/**
Expand All @@ -45,16 +57,32 @@ public function __construct($projectId, WebToPay_Sign_SignCheckerInterface $sign
* @throws WebToPay_Exception_Callback
*/
public function validateAndParseData(array $requestData) {
if (!$this->signer->checkSign($requestData)) {
throw new WebToPay_Exception_Callback('Invalid sign parameters, check $_GET length limit');
}

if (!isset($requestData['data'])) {
throw new WebToPay_Exception_Callback('"data" parameter not found');
}

$data = $requestData['data'];

$queryString = $this->util->decodeSafeUrlBase64($data);
if (isset($requestData['ss1']) || isset($requestData['ss2'])) {
if (!$this->signer->checkSign($requestData)) {
throw new WebToPay_Exception_Callback('Invalid sign parameters, check $_GET length limit');
}

$queryString = $this->util->decodeSafeUrlBase64($data);
} else {
if (null === $this->password) {
throw new WebToPay_Exception_Configuration('You have to provide project password');
}

$queryString = $this->util->decryptGCM(
$this->util->decodeSafeUrlBase64($data),
$this->password
);

if (null === $queryString) {
throw new WebToPay_Exception_Callback('Callback data decryption failed');
}
}
$request = $this->util->parseHttpQuery($queryString);

if (!isset($request['projectid'])) {
Expand Down Expand Up @@ -101,4 +129,4 @@ public function checkExpectedFields(array $data, array $expected) {
}
}
}
}
}
5 changes: 4 additions & 1 deletion src/WebToPay/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,15 @@ public function getCallbackValidator() {
if (!isset($this->configuration['projectId'])) {
throw new WebToPay_Exception_Configuration('You have to provide project ID');
}

$this->callbackValidator = new WebToPay_CallbackValidator(
$this->configuration['projectId'],
$this->getSigner(),
$this->getUtil()
$this->getUtil(),
isset($this->configuration['password']) ? $this->configuration['password'] : null
);
}

return $this->callbackValidator;
}

Expand Down
31 changes: 30 additions & 1 deletion src/WebToPay/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
/**
* Utility class
*/
class WebToPay_Util {
class WebToPay_Util
{
const GCM_CIPHER = 'aes-256-gcm';
const GCM_AUTH_KEY_LENGTH = 16;

/**
* Decodes url-safe-base64 encoded string
Expand All @@ -29,6 +32,32 @@ public function encodeSafeUrlBase64($text) {
return strtr(base64_encode($text), array('+' => '-', '/' => '_'));
}

/**
* Decrypts string with aes-256-gcm algorithm
*
* @param string $stringToDecrypt
* @param string $key
*
* @return string|null
*/
function decryptGCM($stringToDecrypt, $key) {
$ivLength = openssl_cipher_iv_length(self::GCM_CIPHER);
$iv = substr($stringToDecrypt, 0, $ivLength);
$ciphertext = substr($stringToDecrypt, $ivLength, -self::GCM_AUTH_KEY_LENGTH);
$tag = substr($stringToDecrypt, -self::GCM_AUTH_KEY_LENGTH);

$decryptedText = openssl_decrypt(
$ciphertext,
self::GCM_CIPHER,
$key,
OPENSSL_RAW_DATA,
$iv,
$tag
);

return $decryptedText === false ? null : $decryptedText;
}

/**
* Parses HTTP query to array
*
Expand Down
55 changes: 47 additions & 8 deletions tests/WebToPay/CallbackValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
*/
class WebToPay_CallbackValidatorTest extends TestCase {

const PROJECT_PASSWORD = 'pass';

/**
* @var WebToPay_Sign_SignCheckerInterface
*/
Expand All @@ -27,16 +29,19 @@ class WebToPay_CallbackValidatorTest extends TestCase {
*/
public function setUp(): void {
$this->signer = $this->createMock('WebToPay_Sign_SignCheckerInterface');
$this->util = $this->createMock('WebToPay_Util', array('decodeSafeUrlBase64', 'parseHttpQuery'));
$this->validator = new WebToPay_CallbackValidator(123, $this->signer, $this->util);
$this->util = $this->createMock(
'WebToPay_Util',
array('decodeSafeUrlBase64', 'parseHttpQuery', 'decryptGCM')
);
$this->validator = new WebToPay_CallbackValidator(123, $this->signer, $this->util, self::PROJECT_PASSWORD);
}

/**
* Exception should be thrown on invalid sign
*/
public function testValidateAndParseDataWithInvalidSign() {
$this->expectException(WebToPay_Exception_Callback::class);
$request = array('data' => 'abcdef', 'sign' => 'qwerty');
$request = array('data' => 'abcdef', 'sign' => 'qwerty', 'ss1'=> 'zxcvb');

$this->signer->expects($this->once())->method('checkSign')->with($request)->will($this->returnValue(false));
$this->util->expects($this->never())->method($this->anything());
Expand All @@ -49,7 +54,8 @@ public function testValidateAndParseDataWithInvalidSign() {
*/
public function testValidateAndParseDataWithInvalidProject() {
$this->expectException(WebToPay_Exception_Callback::class);
$request = array('data' => 'abcdef', 'sign' => 'qwerty');

$request = array('data' => 'abcdef', 'sign' => 'qwerty', 'ss1' => 'randomChecksum');
$parsed = array('projectid' => 456);

$this->signer->expects($this->once())->method('checkSign')->with($request)->will($this->returnValue(true));
Expand All @@ -60,10 +66,10 @@ public function testValidateAndParseDataWithInvalidProject() {
}

/**
* Tests validateAndParseData method
* Tests validateAndParseData method with callback data decoding
*/
public function testValidateAndParseData() {
$request = array('data' => 'abcdef', 'sign' => 'qwerty');
public function testValidateAndParseDataWithDecoding() {
$request = array('data' => 'abcdef', 'sign' => 'qwerty', 'ss1' => 'randomChecksum');
$parsed = array('projectid' => 123, 'someparam' => 'qwerty123', 'type' => 'micro');

$this->signer->expects($this->once())->method('checkSign')->with($request)->will($this->returnValue(true));
Expand All @@ -73,6 +79,39 @@ public function testValidateAndParseData() {
$this->assertEquals($parsed, $this->validator->validateAndParseData($request));
}

/**
* Tests validateAndParseData method with callback data decryption
*/
public function testValidateAndParseDataWithDecryption() {
$data = ['firstParam' => 'first', 'secondParam' => 'second', 'projectid' => 123, 'type' => 'macro'];
$dataString = http_build_query($data);
$urlSafeEncodedString = 'ASdzxc+awej_lqkweQWesa==';
$encryptedDataString = 'ASdzxc+awej_lqkweQWesa==';
$request = array('data' => $encryptedDataString);

$this->util->expects($this->once())->method('decodeSafeUrlBase64')->with($urlSafeEncodedString)->will($this->returnValue($encryptedDataString));
$this->util->expects($this->once())->method('decryptGCM')->with($encryptedDataString, self::PROJECT_PASSWORD)->will($this->returnValue($dataString));
$this->util->expects($this->once())->method('parseHttpQuery')->with($dataString)->will($this->returnValue($data));

$this->assertEquals($data, $this->validator->validateAndParseData($request));
}

/**
* Exception should be thrown if decryption has failed
*/
public function testValidateAndParseDataWithDecryptionFailure() {
$this->expectException(WebToPay_Exception_Callback::class);

$urlSafeEncodedString = 'ASdzxc+awej_lqkweQWesa==';
$encryptedDataString = 'ASdzxc+awej_lqkweQWesa==';
$request = array('data' => $encryptedDataString);

$this->util->expects($this->once())->method('decodeSafeUrlBase64')->with($urlSafeEncodedString)->will($this->returnValue($encryptedDataString));
$this->util->expects($this->once())->method('decryptGCM')->with($encryptedDataString, self::PROJECT_PASSWORD)->will($this->returnValue(false));

$this->validator->validateAndParseData($request);
}

/**
* Tests checkExpectedFields method - it should throw exception (only) when some valus are not as expected or
* unspecified
Expand Down Expand Up @@ -127,4 +166,4 @@ public function testCheckExpectedFields() {
}
$this->assertNotNull($exception);
}
}
}
Loading

0 comments on commit e694b4e

Please sign in to comment.