Skip to content
This repository was archived by the owner on Feb 28, 2024. It is now read-only.

Commit 56e9846

Browse files
committed
Added interceptor class for encrypting/decrypting PSR RequestInterface and ResponseInterface objects
1 parent d5741ca commit 56e9846

File tree

4 files changed

+500
-0
lines changed

4 files changed

+500
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
namespace Mastercard\Developer\Interceptors;
3+
4+
use Mastercard\Developer\Encryption\EncryptionException;
5+
use Mastercard\Developer\Encryption\FieldLevelEncryption;
6+
use Mastercard\Developer\Encryption\FieldLevelEncryptionParams;
7+
use Psr\Http\Message\RequestInterface;
8+
use Psr\Http\Message\ResponseInterface;
9+
10+
/**
11+
* Utility class for encrypting RequestInterface and decrypting ResponseInterface payloads (see: https://www.php-fig.org/psr/psr-7/)
12+
* @package Mastercard\Developer\Interceptors
13+
*/
14+
class PsrHttpMessageEncryptionInterceptor {
15+
16+
private $config;
17+
18+
/**
19+
* PsrHttpMessageEncryptionInterceptor constructor.
20+
* @param $config A FieldLevelEncryptionConfig instance
21+
*/
22+
public function __construct($config) {
23+
$this->config = $config;
24+
}
25+
26+
/**
27+
* Encrypt payloads from RequestInterface objects when needed.
28+
* @param $request A RequestInterface object
29+
* @return The updated RequestInterface object
30+
* @throws EncryptionException
31+
*/
32+
public function interceptRequest(RequestInterface &$request) {
33+
try {
34+
// Check request actually has a payload
35+
$body = $request->getBody();
36+
$payload = $body->__toString();
37+
if (empty($payload)) {
38+
// Nothing to encrypt
39+
return $request;
40+
}
41+
42+
// Encrypt fields & update headers
43+
if ($this->config->useHttpHeaders()) {
44+
// Generate encryption params and add them as HTTP headers
45+
$params = FieldLevelEncryptionParams::generate($this->config);
46+
self::updateHeader($request, $this->config->getIvHeaderName(), $params->getIvValue());
47+
self::updateHeader($request, $this->config->getEncryptedKeyHeaderName(), $params->getEncryptedKeyValue());
48+
self::updateHeader($request, $this->config->getEncryptionCertificateFingerprintHeaderName(), $params->getEncryptionCertificateFingerprintValue());
49+
self::updateHeader($request, $this->config->getEncryptionKeyFingerprintHeaderName(), $params->getEncryptionKeyFingerprintValue());
50+
self::updateHeader($request, $this->config->getOaepPaddingDigestAlgorithmHeaderName(), $params->getOaepPaddingDigestAlgorithmValue());
51+
$encryptedPayload = FieldLevelEncryption::encryptPayload($payload, $this->config, $params);
52+
} else {
53+
// Encryption params will be stored in the payload
54+
$encryptedPayload = FieldLevelEncryption::encryptPayload($payload, $this->config);
55+
}
56+
57+
// Update body and content length
58+
$updatedBody = new PsrStreamInterfaceImpl();
59+
$updatedBody->write($encryptedPayload);
60+
$request = $request->withBody($updatedBody);
61+
self::updateHeader($request, 'Content-Length', $updatedBody->getSize());
62+
return $request;
63+
64+
} catch (EncryptionException $e) {
65+
throw $e;
66+
} catch (\Exception $e) {
67+
throw new EncryptionException('Failed to intercept and encrypt request!', $e);
68+
}
69+
}
70+
71+
/**
72+
* Decrypt payloads from ResponseInterface objects when needed.
73+
* @param $response A ResponseInterface object
74+
* @return The updated ResponseInterface object
75+
* @throws EncryptionException
76+
*/
77+
public function interceptResponse(ResponseInterface &$response) {
78+
try {
79+
// Read response payload
80+
$body = $response->getBody();
81+
$payload = $body->__toString();
82+
if (empty($payload)) {
83+
// Nothing to decrypt
84+
return $response;
85+
}
86+
87+
// Decrypt fields & update headers
88+
if ($this->config->useHttpHeaders()) {
89+
// Read encryption params from HTTP headers and delete headers
90+
$ivValue = self::readAndRemoveHeader($response, $this->config->getIvHeaderName());
91+
$encryptedKeyValue = self::readAndRemoveHeader($response, $this->config->getEncryptedKeyHeaderName());
92+
$oaepPaddingDigestAlgorithmValue = self::readAndRemoveHeader($response, $this->config->getOaepPaddingDigestAlgorithmHeaderName());
93+
self::readAndRemoveHeader($response, $this->config->getEncryptionCertificateFingerprintHeaderName());
94+
self::readAndRemoveHeader($response, $this->config->getEncryptionKeyFingerprintHeaderName());
95+
$params = new FieldLevelEncryptionParams($this->config, $ivValue, $encryptedKeyValue, $oaepPaddingDigestAlgorithmValue);
96+
$decryptedPayload = FieldLevelEncryption::decryptPayload($payload, $this->config, $params);
97+
} else {
98+
// Encryption params are stored in the payload
99+
$decryptedPayload = FieldLevelEncryption::decryptPayload($payload, $this->config);
100+
}
101+
102+
// Update body and content length
103+
$updatedBody = new PsrStreamInterfaceImpl();
104+
$updatedBody->write($decryptedPayload);
105+
$response = $response->withBody($updatedBody);
106+
self::updateHeader($response, 'Content-Length', $updatedBody->getSize());
107+
return $response;
108+
109+
} catch (EncryptionException $e) {
110+
throw $e;
111+
} catch (\Exception $e) {
112+
throw new EncryptionException('Failed to intercept and decrypt response!', $e);
113+
}
114+
}
115+
116+
private static function updateHeader(&$message, $name, $value) {
117+
if (empty($name)) {
118+
// Do nothing
119+
return $message;
120+
}
121+
$message = $message->withHeader($name, $value);
122+
}
123+
124+
private static function readAndRemoveHeader(&$message, $name) {
125+
if (empty($name)) {
126+
return null;
127+
}
128+
if (!$message->hasHeader($name)) {
129+
return null;
130+
}
131+
$values = $message->getHeader($name);
132+
$message = $message->withoutHeader($name);
133+
return $values[0];
134+
}
135+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
namespace Mastercard\Developer\Interceptors;
4+
5+
use Psr\Http\Message\StreamInterface;
6+
7+
class PsrStreamInterfaceImpl implements StreamInterface {
8+
9+
private $content;
10+
11+
public function __toString() {
12+
return isset($this->content) ? $this->content : '';
13+
}
14+
15+
public function close() {
16+
unset($this->content);
17+
}
18+
19+
public function detach() {
20+
unset($this->content);
21+
}
22+
23+
public function getSize() {
24+
return isset($this->content) ? strlen($this->content) : 0;
25+
}
26+
27+
public function tell() {
28+
return null;
29+
}
30+
31+
public function eof() {
32+
return false;
33+
}
34+
35+
public function isSeekable() {
36+
return false;
37+
}
38+
39+
public function seek($offset, $whence = SEEK_SET) {
40+
return null;
41+
}
42+
43+
public function rewind() {}
44+
45+
public function isWritable() {
46+
return false;
47+
}
48+
49+
public function write($string) {
50+
$this->content = $string;
51+
}
52+
53+
public function isReadable() {
54+
return true;
55+
}
56+
57+
public function read($length) {
58+
return isset($this->content) ? substr($this->content, 0, $length) : '';
59+
}
60+
61+
public function getContents() {
62+
return $this->__toString();
63+
}
64+
65+
public function getMetadata($key = null) {
66+
return [];
67+
}
68+
}

0 commit comments

Comments
 (0)