From e694b4e1b599d7a11d85acbabe73c950ede5978f Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 13 Apr 2022 14:09:25 +0300 Subject: [PATCH] Implementation of callback data decryption Implementation of callback data decryption --- CHANGELOG.md | 3 + WebToPay.php | 79 +++++++++++++++++++++--- src/WebToPay/CallbackValidator.php | 46 +++++++++++--- src/WebToPay/Factory.php | 5 +- src/WebToPay/Util.php | 31 +++++++++- tests/WebToPay/CallbackValidatorTest.php | 55 ++++++++++++++--- tests/WebToPay/UtilTest.php | 58 ++++++++++++++++- 7 files changed, 247 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbd1778..95ab0ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ Version history =============== +Version 1.8 - 2022-04-13 + + * added callback data decryption functionality Version 1.7 - 2022-04-11 diff --git a/WebToPay.php b/WebToPay.php index 059b31d..5060477 100644 --- a/WebToPay.php +++ b/WebToPay.php @@ -372,7 +372,7 @@ class WebToPayException extends Exception { * Errors in remote service - it returns some invalid data */ const E_SERVICE = 10; - + /** * Deprecated usage errors */ @@ -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; } /** @@ -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'])) { @@ -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; @@ -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 _ @@ -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 * diff --git a/src/WebToPay/CallbackValidator.php b/src/WebToPay/CallbackValidator.php index eecb41c..65cb591 100644 --- a/src/WebToPay/CallbackValidator.php +++ b/src/WebToPay/CallbackValidator.php @@ -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; } /** @@ -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'])) { @@ -101,4 +129,4 @@ public function checkExpectedFields(array $data, array $expected) { } } } -} \ No newline at end of file +} diff --git a/src/WebToPay/Factory.php b/src/WebToPay/Factory.php index d91953f..9f09bcb 100644 --- a/src/WebToPay/Factory.php +++ b/src/WebToPay/Factory.php @@ -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; } diff --git a/src/WebToPay/Util.php b/src/WebToPay/Util.php index 52f519c..a263819 100644 --- a/src/WebToPay/Util.php +++ b/src/WebToPay/Util.php @@ -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 @@ -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 * diff --git a/tests/WebToPay/CallbackValidatorTest.php b/tests/WebToPay/CallbackValidatorTest.php index fb01345..d2e1721 100644 --- a/tests/WebToPay/CallbackValidatorTest.php +++ b/tests/WebToPay/CallbackValidatorTest.php @@ -7,6 +7,8 @@ */ class WebToPay_CallbackValidatorTest extends TestCase { + const PROJECT_PASSWORD = 'pass'; + /** * @var WebToPay_Sign_SignCheckerInterface */ @@ -27,8 +29,11 @@ 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); } /** @@ -36,7 +41,7 @@ public function setUp(): void { */ 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()); @@ -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)); @@ -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)); @@ -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 @@ -127,4 +166,4 @@ public function testCheckExpectedFields() { } $this->assertNotNull($exception); } -} \ No newline at end of file +} diff --git a/tests/WebToPay/UtilTest.php b/tests/WebToPay/UtilTest.php index 116f5b0..9996435 100644 --- a/tests/WebToPay/UtilTest.php +++ b/tests/WebToPay/UtilTest.php @@ -75,4 +75,60 @@ public function testParseHttpQuery() { ) ); } -} \ No newline at end of file + + public function testDecryptGCM() + { + $key = 'encryption_key'; + $dataString = http_build_query( + array( + 'firstParam' => 'first', + 'secondParam' => 'second', + ) + ); + $encryptedData = $this->getEncryptedData($dataString, $key); + + $this->assertEquals( + $dataString, + $this->util->decryptGCM($encryptedData, $key) + ); + } + + public function testDecryptGCMFailed() + { + $dataString = http_build_query( + array( + 'firstParam' => 'first', + 'secondParam' => 'second', + ) + ); + $encryptedData = $this->getEncryptedData($dataString, 'encryption_key'); + + $this->assertNull($this->util->decryptGCM($encryptedData, 'wrong_key')); + } + + /** + * @param string $data - callback data string to encrypt + * + * @return string - encrypted string + */ + private function getEncryptedData($data, $key) + { + // move to util test + $ivLength = openssl_cipher_iv_length(WebToPay_Util::GCM_CIPHER); + $iv = openssl_random_pseudo_bytes($ivLength); + $tag = ''; + + $ciphertext = openssl_encrypt( + $data, + WebToPay_Util::GCM_CIPHER, + $key, + OPENSSL_RAW_DATA, + $iv, + $tag, + '', + WebToPay_Util::GCM_AUTH_KEY_LENGTH + ); + + return $iv.$ciphertext.$tag; + } +}