1: <?php
2: namespace Hyperwallet\Util;
3: use GuzzleHttp\Client;
4: use GuzzleHttp\Exception\BadResponseException;
5: use GuzzleHttp\Exception\ConnectException;
6: use GuzzleHttp\UriTemplate\UriTemplate;
7: use Hyperwallet\Exception\HyperwalletApiException;
8: use Hyperwallet\Exception\HyperwalletException;
9: use Hyperwallet\Model\BaseModel;
10: use Hyperwallet\Response\ErrorResponse;
11: use Composer\Autoload\ClassLoader;
12: use phpseclib\Crypt\RSA;
13: use phpseclib\Math\BigInteger;
14: use phpseclib\Crypt\Hash;
15: use JOSE_URLSafeBase64;
16: use JOSE_JWS;
17: use JOSE_JWE;
18: use JOSE_JWK;
19: use JOSE_JWT;
20:
21: /**
22: * The encryption service for Hyperwallet client's requests/responses
23: *
24: * @package Hyperwallet\Util
25: */
26: class HyperwalletEncryption {
27:
28: /**
29: * String that can be a URL or path to file with client JWK set
30: *
31: * @var string
32: */
33: private $clientPrivateKeySetLocation;
34:
35: /**
36: * String that can be a URL or path to file with hyperwallet JWK set
37: *
38: * @var string
39: */
40: private $hyperwalletKeySetLocation;
41:
42: /**
43: * JWE encryption algorithm, by default value = RSA-OAEP-256
44: *
45: * @var string
46: */
47: private $encryptionAlgorithm;
48:
49: /**
50: * JWS signature algorithm, by default value = RS256
51: *
52: * @var string
53: */
54: private $signAlgorithm;
55:
56: /**
57: * JWE encryption method, by default value = A256CBC-HS512
58: *
59: * @var string
60: */
61: private $encryptionMethod;
62:
63: /**
64: * Minutes when JWS signature is valid, by default value = 5
65: *
66: * @var integer
67: */
68: private $jwsExpirationMinutes;
69:
70: /**
71: * JWS key id header param
72: *
73: * @var string
74: */
75: private $jwsKid;
76:
77: /**
78: * JWE key id header param
79: *
80: * @var string
81: */
82: private $jweKid;
83:
84: /**
85: * Creates a instance of the HyperwalletEncryption
86: *
87: * @param string $clientPrivateKeySetLocation String that can be a URL or path to file with client JWK set
88: * @param string $hyperwalletKeySetLocation String that can be a URL or path to file with hyperwallet JWK set
89: * @param string $encryptionAlgorithm JWE encryption algorithm, by default value = RSA-OAEP-256
90: * @param string $signAlgorithm JWS signature algorithm, by default value = RS256
91: * @param string $encryptionMethod JWE encryption method, by default value = A256CBC-HS512
92: * @param integer $jwsExpirationMinutes Minutes when JWS signature is valid, by default value = 5
93: */
94: public function __construct($clientPrivateKeySetLocation, $hyperwalletKeySetLocation,
95: $encryptionAlgorithm = 'RSA-OAEP-256', $signAlgorithm = 'RS256', $encryptionMethod = 'A256CBC-HS512',
96: $jwsExpirationMinutes = 5) {
97: $this->clientPrivateKeySetLocation = $clientPrivateKeySetLocation;
98: $this->hyperwalletKeySetLocation = $hyperwalletKeySetLocation;
99: $this->encryptionAlgorithm = $encryptionAlgorithm;
100: $this->signAlgorithm = $signAlgorithm;
101: $this->encryptionMethod = $encryptionMethod;
102: $this->jwsExpirationMinutes = $jwsExpirationMinutes;
103: file_put_contents($this->getVendorPath() . "/gree/jose/src/JOSE/JWE.php", file_get_contents(__DIR__ . "/../../JWE"));
104: }
105:
106: /**
107: * Makes an encrypted request : 1) signs the request body; 2) encrypts payload after signature
108: *
109: * @param string $body The request body to be encrypted
110: * @return string
111: *
112: * @throws HyperwalletException
113: */
114: public function encrypt($body) {
115: $privateJwsKey = $this->getPrivateJwsKey();
116: $jws = new JOSE_JWS(new JOSE_JWT($body));
117: $jws->header['exp'] = $this->getSignatureExpirationTime();
118: $jws->header['kid'] = $this->jwsKid;
119: $jws->sign($privateJwsKey, $this->signAlgorithm);
120:
121: $publicJweKey = $this->getPublicJweKey();
122: $jwe = new JOSE_JWE($jws);
123: $jwe->header['kid'] = $this->jweKid;
124: $jwe->encrypt($publicJweKey, $this->encryptionAlgorithm, $this->encryptionMethod);
125: return $jwe->toString();
126: }
127:
128: /**
129: * Decrypts encrypted response : 1) decrypts the request body; 2) verifies the payload signature
130: *
131: * @param string $body The response body to be decrypted
132: * @return string
133: *
134: * @throws HyperwalletException
135: */
136: public function decrypt($body) {
137: $privateJweKey = $this->getPrivateJweKey();
138: $jwe = JOSE_JWT::decode($body);
139: $this->checkJweHeaderAlgorithm($jwe->header);
140: $decryptedBody = $jwe->decrypt($privateJweKey);
141:
142: $publicJwsKey = $this->getPublicJwsKey();
143: $jwsToVerify = JOSE_JWT::decode($decryptedBody->plain_text);
144: $this->checkJwsExpiration($jwsToVerify->header);
145: $jwsVerificationResult = $jwsToVerify->verify($publicJwsKey, $this->signAlgorithm);
146: return $jwsVerificationResult->claims;
147: }
148:
149: /**
150: * Retrieves JWS RSA private key with algorithm = $this->signAlgorithm
151: *
152: * @return RSA
153: *
154: * @throws HyperwalletException
155: */
156: private function getPrivateJwsKey() {
157: $privateKeyData = $this->getJwk($this->clientPrivateKeySetLocation, $this->signAlgorithm);
158: $this->jwsKid = $privateKeyData['kid'];
159: return $this->getPrivateKey($privateKeyData);
160: }
161:
162: /**
163: * Retrieves JWE RSA public key with algorithm = $this->encryptionAlgorithm
164: *
165: * @return RSA
166: *
167: * @throws HyperwalletException
168: */
169: private function getPublicJweKey() {
170: $publicKeyData = $this->getJwk($this->hyperwalletKeySetLocation, $this->encryptionAlgorithm);
171: $this->jweKid = $publicKeyData['kid'];
172: return $this->getPublicKey($this->convertPrivateKeyToPublic($publicKeyData));
173: }
174:
175: /**
176: * Retrieves JWE RSA private key with algorithm = $this->encryptionAlgorithm
177: *
178: * @return RSA
179: *
180: * @throws HyperwalletException
181: */
182: private function getPrivateJweKey() {
183: $privateKeyData = $this->getJwk($this->clientPrivateKeySetLocation, $this->encryptionAlgorithm);
184: return $this->getPrivateKey($privateKeyData);
185: }
186:
187: /**
188: * Retrieves JWS RSA public key with algorithm = $this->signAlgorithm
189: *
190: * @return RSA
191: *
192: * @throws HyperwalletException
193: */
194: private function getPublicJwsKey() {
195: $publicKeyData = $this->getJwk($this->hyperwalletKeySetLocation, $this->signAlgorithm);
196: return $this->getPublicKey($this->convertPrivateKeyToPublic($publicKeyData));
197: }
198:
199: /**
200: * Retrieves RSA private key by JWK key data
201: *
202: * @param array $privateKeyData The JWK key data
203: * @return RSA
204: */
205: private function getPrivateKey($privateKeyData) {
206: $n = $this->keyParamToBigInteger($privateKeyData['n']);
207: $e = $this->keyParamToBigInteger($privateKeyData['e']);
208: $d = $this->keyParamToBigInteger($privateKeyData['d']);
209: $p = $this->keyParamToBigInteger($privateKeyData['p']);
210: $q = $this->keyParamToBigInteger($privateKeyData['q']);
211: $qi = $this->keyParamToBigInteger($privateKeyData['qi']);
212: $dp = $this->keyParamToBigInteger($privateKeyData['dp']);
213: $dq = $this->keyParamToBigInteger($privateKeyData['dq']);
214: $primes = array($p, $q);
215: $exponents = array($dp, $dq);
216: $coefficients = array($qi, $qi);
217: array_unshift($primes, "phoney");
218: unset($primes[0]);
219: array_unshift($exponents, "phoney");
220: unset($exponents[0]);
221: array_unshift($coefficients, "phoney");
222: unset($coefficients[0]);
223:
224: $pemData = (new RSA())->_convertPrivateKey($n, $e, $d, $primes, $exponents, $coefficients);
225: $privateKey = new RSA();
226: $privateKey->loadKey($pemData);
227: if ($privateKeyData['alg'] == 'RSA-OAEP-256') {
228: $privateKey->setHash('sha256');
229: $privateKey->setMGFHash('sha256');
230: }
231: return $privateKey;
232: }
233:
234: /**
235: * Converts base 64 encoded string to BigInteger
236: *
237: * @param string $param base 64 encoded string
238: * @return BigInteger
239: */
240: private function keyParamToBigInteger($param) {
241: return new BigInteger('0x' . bin2hex(JOSE_URLSafeBase64::decode($param)), 16);
242: }
243:
244: /**
245: * Retrieves RSA public key by JWK key data
246: *
247: * @param array $publicKeyData The JWK key data
248: * @return RSA
249: */
250: private function getPublicKey($publicKeyData) {
251: $publicKeyRaw = new JOSE_JWK($publicKeyData);
252: $publicKey = $publicKeyRaw->toKey();
253: if ($publicKeyData['alg'] == 'RSA-OAEP-256') {
254: $publicKey->setHash('sha256');
255: $publicKey->setMGFHash('sha256');
256: }
257: return $publicKey;
258: }
259:
260: /**
261: * Retrieves JWK key by JWK key set location and algorithm
262: *
263: * @param string $keySetLocation The location(URL or path to file) of JWK key set
264: * @param string $alg The target algorithm
265: * @return array
266: *
267: * @throws HyperwalletException
268: */
269: private function getJwk($keySetLocation, $alg) {
270: if (filter_var($keySetLocation, FILTER_VALIDATE_URL) === FALSE) {
271: if (!file_exists($keySetLocation)) {
272: throw new HyperwalletException("Wrong JWK key set location path = " . $keySetLocation);
273: }
274: }
275: return $this->findJwkByAlgorithm(json_decode(file_get_contents($keySetLocation), true), $alg);
276: }
277:
278: /**
279: * Retrieves JWK key from JWK key set by given algorithm
280: *
281: * @param string $jwkSetArray JWK key set
282: * @param string $alg The target algorithm
283: * @return array
284: *
285: * @throws HyperwalletException
286: */
287: private function findJwkByAlgorithm($jwkSetArray, $alg) {
288: foreach($jwkSetArray['keys'] as $jwk) {
289: if ($alg == $jwk['alg']) {
290: return $jwk;
291: }
292: }
293: throw new HyperwalletException("JWK set doesn't contain key with algorithm = " . $alg);
294: }
295:
296: /**
297: * Converts private key to public
298: *
299: * @param string $jwk JWK key
300: * @return array
301: */
302: private function convertPrivateKeyToPublic($jwk) {
303: if (isset($jwk['d'])) {
304: unset($jwk['d']);
305: }
306: if (isset($jwk['p'])) {
307: unset($jwk['p']);
308: }
309: if (isset($jwk['q'])) {
310: unset($jwk['q']);
311: }
312: if (isset($jwk['qi'])) {
313: unset($jwk['qi']);
314: }
315: if (isset($jwk['dp'])) {
316: unset($jwk['dp']);
317: }
318: if (isset($jwk['dq'])) {
319: unset($jwk['dq']);
320: }
321: return $jwk;
322: }
323:
324: /**
325: * Calculates JWS expiration time in seconds
326: *
327: * @return integer
328: */
329: private function getSignatureExpirationTime() {
330: date_default_timezone_set("UTC");
331: $secondsInMinute = 60;
332: return time() + $this->jwsExpirationMinutes * $secondsInMinute;
333: }
334:
335: /**
336: * Checks if header 'exp' param has not expired value
337: *
338: * @param array $header JWS header array
339: *
340: * @throws HyperwalletException
341: */
342: public function checkJwsExpiration($header) {
343: if(!isset($header['exp'])) {
344: throw new HyperwalletException('While trying to verify JWS signature no [exp] header is found');
345: }
346: $exp = $header['exp'];
347: if(!is_numeric($exp)) {
348: throw new HyperwalletException('Wrong value in [exp] header of JWS signature, must be integer');
349: }
350: if((int)time() > (int)$exp) {
351: throw new HyperwalletException('JWS signature has expired, checked by [exp] JWS header');
352: }
353: }
354:
355: /**
356: * Checks that the JWE header advertises the key-management algorithm and content-encryption method
357: * this client expects, before the header-controlled algorithm is ever used to decrypt with the
358: * private key. Prevents an attacker from forcing algorithm downgrade (e.g. to legacy RSA1_5) by
359: * tampering with the untrusted alg/enc header fields of an intercepted response.
360: *
361: * @param array $header JWE header array
362: *
363: * @throws HyperwalletException
364: */
365: public function checkJweHeaderAlgorithm($header) {
366: if (!isset($header['alg']) || $header['alg'] !== $this->encryptionAlgorithm) {
367: throw new HyperwalletException('While trying to decrypt JWE, unexpected [alg] header found');
368: }
369: if (!isset($header['enc']) || $header['enc'] !== $this->encryptionMethod) {
370: throw new HyperwalletException('While trying to decrypt JWE, unexpected [enc] header found');
371: }
372: }
373:
374: /**
375: * Finds the path of composer vendor directory
376: *
377: * @return string
378: *
379: * @throws HyperwalletException
380: */
381: public function getVendorPath() {
382: $reflector = new \ReflectionClass(ClassLoader::class);
383: $vendorPath = preg_replace('/^(.*)\/composer\/ClassLoader\.php$/', '$1', $reflector->getFileName() );
384: if($vendorPath && is_dir($vendorPath)) {
385: return $vendorPath . '/';
386: }
387: throw new HyperwalletException('Failed to find a vendor path');
388: }
389: }
390: