Skip to content

Commit

Permalink
[PW-2059] Mask encrypted/sensitive data in the logs (#164)
Browse files Browse the repository at this point in the history
* Add maskParametersRecursive() to mask all the sensitive parameters

Walk through recursively in an associative array containing all the key
path combinations for the sensitive values in the request or response
parameters array
Masking is only effective in live environment

Add maskParameter() to only keep the first 10 characters of the
parameter's value and append 3 asterisks in case the value is not empty

* Use maskParametersRecursive() in logRequest() to mask parameters

Add parameter list that needs to be masked
Add environment as a required parameter to the logRequest() to determine
if masking needs to be done or not
Masking only needs to be done in live environment
Adjust the existing usage of the logRequest to the new parameters list

* Add logResponse() to log response objects

Replace the current response object logging with logResponse()

* Add getEnvironment() to retrieve the config environment value

* fix logResponse() and logRequest()

Json decode response before logging because the logResponse expects and
array
Add paymentData into the list of $paramsToMask for both request and
response logging

* Change maskParameter() to mask short parameters completely

* Add environment as a parameter into the createMockClient()

To be able to mock not just test but live configurations as well

* Replace testAuthoriseSuccess with testAuthoriseSuccessInTestEnvironment

By default in test environment the library will not mask the parameters
only in live environment

* Add testAuthoriseSuccessInLiveEnvironment() to test authorize in live

In live environment the parameters will be masked

* Resolve pull request comments

Make $paramsToMask in response and $paramsToMask in request to a private
static property

* Switch maskParameter() from 10 char to 6 char value length
  • Loading branch information
cyattilakiss authored Feb 6, 2020
1 parent 6026900 commit 3a28b7e
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 29 deletions.
8 changes: 8 additions & 0 deletions src/Adyen/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,12 @@ public function getExternalPlatform()
{
return isset($this->data['externalPlatform']) ? $this->data['externalPlatform'] : null;
}

/**
* @return string|null
*/
public function getEnvironment()
{
return isset($this->data['environment']) ? $this->data['environment'] : null;
}
}
146 changes: 122 additions & 24 deletions src/Adyen/HttpClient/CurlClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@

class CurlClient implements ClientInterface
{
// List of parameters that needs to be masked with the same array structure as it appears in
// the response array
private static $responseParamsToMask = array(
'paymentData',
'action' => array(
'paymentData'
)
);

// List of parameters that needs to be masked with the same array structure as it appears in
// the request array

private static $requestParamsToMask = array(
'paymentData',
'card' => array(
'number',
'cvc'
),
'additionalData' => array(
'card.encrypted.json',
),
'paymentMethod' => array(
'number',
'expiryMonth',
'expiryYear',
'cvc',
'encryptedCardNumber',
'encryptedExpiryMonth',
'encryptedExpiryYear',
'encryptedSecurityCode',
'applepay.token',
'paywithgoogle.token'
)
);
/**
* Json API request to Adyen
*
Expand All @@ -24,11 +58,12 @@ public function requestJson(\Adyen\Service $service, $requestUrl, $params, $requ
$password = $config->getPassword();
$xApiKey = $config->getXApiKey();
$httpProxy = $config->getHttpProxy();
$environment = $config->getEnvironment();

$jsonRequest = json_encode($params);

// log the request
$this->logRequest($logger, $requestUrl, $params);
$this->logRequest($logger, $requestUrl, $environment, $params);

//Initiate cURL.
$ch = curl_init($requestUrl);
Expand Down Expand Up @@ -83,8 +118,9 @@ public function requestJson(\Adyen\Service $service, $requestUrl, $params, $requ
//Execute the request
list($result, $httpStatus) = $this->curlRequest($ch);

// log the raw response
$logger->info("JSON Response is: " . $result);
// log the response
$decodedResult = json_decode($result, true);
$this->logResponse($logger, $environment, $decodedResult);

// Get errors
list($errno, $message) = $this->curlError($ch);
Expand All @@ -102,9 +138,6 @@ public function requestJson(\Adyen\Service $service, $requestUrl, $params, $requ
if ($config->getOutputType() == 'array') {
// transform to PHP Array
$result = json_decode($result, true);

// log the array result
$logger->info('Params in response from Adyen:' . print_r($result, 1));
}

return $result;
Expand Down Expand Up @@ -156,10 +189,10 @@ public function requestPost(\Adyen\Service $service, $requestUrl, $params)
$username = $config->getUsername();
$password = $config->getPassword();
$httpProxy = $config->getHttpProxy();
$environment = $config->getEnvironment();

// log the requestUr, params and json request
$logger->info("Request url to Adyen: " . $requestUrl);
$logger->info('Params in request to Adyen:' . print_r($params, 1));
// log the request
$this->logRequest($logger, $requestUrl, $environment, $params);

//Initiate cURL.
$ch = curl_init($requestUrl);
Expand Down Expand Up @@ -193,16 +226,17 @@ public function requestPost(\Adyen\Service $service, $requestUrl, $params)
//Execute the request
list($result, $httpStatus) = $this->curlRequest($ch);

// log the raw response
$logger->info("JSON Response is: " . $result);
// log the response
$decodedResult = json_decode($result, true);
$this->logResponse($logger, $environment, $decodedResult);

// Get errors
list($errno, $message) = $this->curlError($ch);

curl_close($ch);

$resultOKHttpStatusCodes = array(200, 201, 202, 204);

if (!in_array($httpStatus, $resultOKHttpStatusCodes) && $result) {
$this->handleResultError($result, $logger);
} elseif (!$result) {
Expand All @@ -219,9 +253,6 @@ public function requestPost(\Adyen\Service $service, $requestUrl, $params)
$logger->error($msg);
throw new AdyenException($msg);
}

// log the array result
$logger->info('Params in response from Adyen:' . print_r($result, 1));
}

return $result;
Expand Down Expand Up @@ -292,23 +323,90 @@ protected function handleResultError($result, $logger)
* Logs the API request, removing sensitive data
*
* @param \Psr\Log\LoggerInterface $logger
* @param $requestUrl
* @param $params
* @param string requestUrl
* @param string $environment
* @param array $params
*/
private function logRequest(\Psr\Log\LoggerInterface $logger, $requestUrl, $params)
private function logRequest(\Psr\Log\LoggerInterface $logger, $requestUrl, $environment, $params)
{
// log the requestUr, params and json request
$logger->info("Request url to Adyen: " . $requestUrl);
if (isset($params["additionalData"]) && isset($params["additionalData"]["card.encrypted.json"])) {
$params["additionalData"]["card.encrypted.json"] = "*";
}
if (isset($params["card"]) && isset($params["card"]["number"])) {
$params["card"]["number"] = "*";
$params["card"]["cvc"] = "*";

// Filter sensitive data from logs when live
if (\Adyen\Environment::LIVE == $environment) {
$params = $this->maskParametersRecursive(self::$requestParamsToMask, $params);
}

$logger->info('JSON Request to Adyen:' . json_encode($params));
}

/**
* Logs the API request, removing sensitive data
*
* @param \Psr\Log\LoggerInterface $logger
* @param string requestUrl
* @param string $environment
* @param array $params
*/
private function logResponse(\Psr\Log\LoggerInterface $logger, $environment, $params)
{
// Filter sensitive data from logs when live
if (\Adyen\Environment::LIVE == $environment) {
$params = $this->maskParametersRecursive(self::$responseParamsToMask, $params);
}

$logger->info('JSON Response to Adyen:' . json_encode($params));
}

/**
* @param $value
* @param $key
* @param $param
*/
private function maskParametersRecursive($paramsToMaskList, $params)
{
if (is_array($paramsToMaskList)) {
foreach ($paramsToMaskList as $key => $paramsToMask) {

if (is_array($paramsToMask) && isset($params[$key])) {
// if $paramsToMask is an array and $params[$key] exists, $paramsToMask is an array of keys
$params[$key] = $this->maskParametersRecursive($paramsToMask, $params[$key]);
} elseif (!is_array($paramsToMask) && isset($params[$paramsToMask])) {
// if $paramsToMask is not an array and $params[$paramsToMask] exists, $params[$paramsToMask] is a parameter that needs to be masked
$params[$paramsToMask] = $this->maskParameter($params[$paramsToMask]);
}
}
} else {
// in case $paramsToMaskList is not an array then it is a parameter that needs to be masked
$params[$paramsToMaskList] = $this->maskParameter($params[$paramsToMaskList]);
}

return $params;
}

/**
* Masks the parameter
* If the value is longer than 6 char then 3 asterisks are appended to the first 6 char of the value
* If the value is shorter than 6 char then replace all the chars with asterisks
*
* @param $parameter
* @return string
*/
private function maskParameter($parameter)
{
if (empty($parameter)) {
return $parameter;
}

if (strlen($parameter) > 6) {
$parameter = substr($parameter, 0, 6) . '***';
} else {
$parameter = str_repeat('*', strlen($parameter));
}

return $parameter;
}

/**
* Execute curl, return the result and the http response code
*
Expand Down
58 changes: 55 additions & 3 deletions tests/MockTest/PaymentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class PaymentTest extends TestCaseMock
*
* @dataProvider successAuthoriseProvider
*/
public function testAuthoriseSuccess($jsonFile, $httpStatus)
public function testAuthoriseSuccessInTestEnvironment($jsonFile, $httpStatus)
{
// create client
$client = $this->createMockClient($jsonFile, $httpStatus);
Expand Down Expand Up @@ -54,9 +54,61 @@ public function testAuthoriseSuccess($jsonFile, $httpStatus)
$this->assertArrayHasKey('resultCode', $result);
$this->assertEquals('Authorised', $result['resultCode']);

$this->assertFalse($handler->hasInfoThatContains('4111'));
$this->assertTrue($handler->hasInfoThatContains('4111111111111111'));
$this->assertTrue($handler->hasInfoThatContains('737'));
$this->assertTrue($handler->hasInfoThatContains('adyenjs....'));
}

/**
* @param $jsonFile Json file location
* @param $httpStatus expected http status code
*
* @dataProvider successAuthoriseProvider
*/
public function testAuthoriseSuccessInLiveEnvironment($jsonFile, $httpStatus)
{
// create client
$client = $this->createMockClient($jsonFile, $httpStatus, null, \Adyen\Environment::LIVE);

$handler = new TestHandler();

$logger = new Logger('test', array($handler));

// Stub Logger to prevent full card data being logged
$client->setLogger($logger);

// initialize service
$service = new \Adyen\Service\Payment($client);

$json = '{
"card": {
"number": "4111111111111111",
"expiryMonth": "08",
"expiryYear": "2018",
"cvc": "737",
"holderName": "John Smith"
},
"amount": {
"value": 1500,
"currency": "EUR"
},
"reference": "payment-test",
"merchantAccount": "YourMerchantReference",
"additionalData": {
"card.encrypted.json" : "adyenjs_0897248234342242524232...."
}
}';

$params = json_decode($json, true);

$result = $service->authorise($params);

$this->assertArrayHasKey('resultCode', $result);
$this->assertEquals('Authorised', $result['resultCode']);

$this->assertFalse($handler->hasInfoThatContains('4111111111111111'));
$this->assertFalse($handler->hasInfoThatContains('737'));
$this->assertFalse($handler->hasInfoThatContains('adyenjs....'));
$this->assertFalse($handler->hasInfoThatContains('adyenjs_0897248234342242524232...'));
}

public static function successAuthoriseProvider()
Expand Down
4 changes: 2 additions & 2 deletions tests/MockTest/TestCaseMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class TestCaseMock extends \PHPUnit\Framework\TestCase
{
protected function createMockClient($jsonFile, $httpStatus, $errno = null)
protected function createMockClient($jsonFile, $httpStatus, $errno = null, $environment = \Adyen\Environment::TEST)
{
$json = null;
if ($jsonFile != null) {
Expand All @@ -20,7 +20,7 @@ protected function createMockClient($jsonFile, $httpStatus, $errno = null)

$client = new \Adyen\Client();
$client->setApplicationName("My Test Application");
$client->setEnvironment(\Adyen\Environment::TEST);
$client->setEnvironment($environment);
$client->setXApiKey("MockAPIKey");
$client->setHttpClient($curlClient);
return $client;
Expand Down

0 comments on commit 3a28b7e

Please sign in to comment.