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: $decryptedBody = $jwe->decrypt($privateJweKey);
140:
141: $publicJwsKey = $this->getPublicJwsKey();
142: $jwsToVerify = JOSE_JWT::decode($decryptedBody->plain_text);
143: $this->checkJwsExpiration($jwsToVerify->header);
144: $jwsVerificationResult = $jwsToVerify->verify($publicJwsKey, $this->signAlgorithm);
145: return $jwsVerificationResult->claims;
146: }
147:
148: /**
149: * Retrieves JWS RSA private key with algorithm = $this->signAlgorithm
150: *
151: * @return RSA
152: *
153: * @throws HyperwalletException
154: */
155: private function getPrivateJwsKey() {
156: $privateKeyData = $this->getJwk($this->clientPrivateKeySetLocation, $this->signAlgorithm);
157: $this->jwsKid = $privateKeyData['kid'];
158: return $this->getPrivateKey($privateKeyData);
159: }
160:
161: /**
162: * Retrieves JWE RSA public key with algorithm = $this->encryptionAlgorithm
163: *
164: * @return RSA
165: *
166: * @throws HyperwalletException
167: */
168: private function getPublicJweKey() {
169: $publicKeyData = $this->getJwk($this->hyperwalletKeySetLocation, $this->encryptionAlgorithm);
170: $this->jweKid = $publicKeyData['kid'];
171: return $this->getPublicKey($this->convertPrivateKeyToPublic($publicKeyData));
172: }
173:
174: /**
175: * Retrieves JWE RSA private key with algorithm = $this->encryptionAlgorithm
176: *
177: * @return RSA
178: *
179: * @throws HyperwalletException
180: */
181: private function getPrivateJweKey() {
182: $privateKeyData = $this->getJwk($this->clientPrivateKeySetLocation, $this->encryptionAlgorithm);
183: return $this->getPrivateKey($privateKeyData);
184: }
185:
186: /**
187: * Retrieves JWS RSA public key with algorithm = $this->signAlgorithm
188: *
189: * @return RSA
190: *
191: * @throws HyperwalletException
192: */
193: private function getPublicJwsKey() {
194: $publicKeyData = $this->getJwk($this->hyperwalletKeySetLocation, $this->signAlgorithm);
195: return $this->getPublicKey($this->convertPrivateKeyToPublic($publicKeyData));
196: }
197:
198: /**
199: * Retrieves RSA private key by JWK key data
200: *
201: * @param array $privateKeyData The JWK key data
202: * @return RSA
203: */
204: private function getPrivateKey($privateKeyData) {
205: $n = $this->keyParamToBigInteger($privateKeyData['n']);
206: $e = $this->keyParamToBigInteger($privateKeyData['e']);
207: $d = $this->keyParamToBigInteger($privateKeyData['d']);
208: $p = $this->keyParamToBigInteger($privateKeyData['p']);
209: $q = $this->keyParamToBigInteger($privateKeyData['q']);
210: $qi = $this->keyParamToBigInteger($privateKeyData['qi']);
211: $dp = $this->keyParamToBigInteger($privateKeyData['dp']);
212: $dq = $this->keyParamToBigInteger($privateKeyData['dq']);
213: $primes = array($p, $q);
214: $exponents = array($dp, $dq);
215: $coefficients = array($qi, $qi);
216: array_unshift($primes, "phoney");
217: unset($primes[0]);
218: array_unshift($exponents, "phoney");
219: unset($exponents[0]);
220: array_unshift($coefficients, "phoney");
221: unset($coefficients[0]);
222:
223: $pemData = (new RSA())->_convertPrivateKey($n, $e, $d, $primes, $exponents, $coefficients);
224: $privateKey = new RSA();
225: $privateKey->loadKey($pemData);
226: if ($privateKeyData['alg'] == 'RSA-OAEP-256') {
227: $privateKey->setHash('sha256');
228: $privateKey->setMGFHash('sha256');
229: }
230: return $privateKey;
231: }
232:
233: /**
234: * Converts base 64 encoded string to BigInteger
235: *
236: * @param string $param base 64 encoded string
237: * @return BigInteger
238: */
239: private function keyParamToBigInteger($param) {
240: return new BigInteger('0x' . bin2hex(JOSE_URLSafeBase64::decode($param)), 16);
241: }
242:
243: /**
244: * Retrieves RSA public key by JWK key data
245: *
246: * @param array $publicKeyData The JWK key data
247: * @return RSA
248: */
249: private function getPublicKey($publicKeyData) {
250: $publicKeyRaw = new JOSE_JWK($publicKeyData);
251: $publicKey = $publicKeyRaw->toKey();
252: if ($publicKeyData['alg'] == 'RSA-OAEP-256') {
253: $publicKey->setHash('sha256');
254: $publicKey->setMGFHash('sha256');
255: }
256: return $publicKey;
257: }
258:
259: /**
260: * Retrieves JWK key by JWK key set location and algorithm
261: *
262: * @param string $keySetLocation The location(URL or path to file) of JWK key set
263: * @param string $alg The target algorithm
264: * @return array
265: *
266: * @throws HyperwalletException
267: */
268: private function getJwk($keySetLocation, $alg) {
269: if (filter_var($keySetLocation, FILTER_VALIDATE_URL) === FALSE) {
270: if (!file_exists($keySetLocation)) {
271: throw new HyperwalletException("Wrong JWK key set location path = " . $keySetLocation);
272: }
273: }
274: return $this->findJwkByAlgorithm(json_decode(file_get_contents($keySetLocation), true), $alg);
275: }
276:
277: /**
278: * Retrieves JWK key from JWK key set by given algorithm
279: *
280: * @param string $jwkSetArray JWK key set
281: * @param string $alg The target algorithm
282: * @return array
283: *
284: * @throws HyperwalletException
285: */
286: private function findJwkByAlgorithm($jwkSetArray, $alg) {
287: foreach($jwkSetArray['keys'] as $jwk) {
288: if ($alg == $jwk['alg']) {
289: return $jwk;
290: }
291: }
292: throw new HyperwalletException("JWK set doesn't contain key with algorithm = " . $alg);
293: }
294:
295: /**
296: * Converts private key to public
297: *
298: * @param string $jwk JWK key
299: * @return array
300: */
301: private function convertPrivateKeyToPublic($jwk) {
302: if (isset($jwk['d'])) {
303: unset($jwk['d']);
304: }
305: if (isset($jwk['p'])) {
306: unset($jwk['p']);
307: }
308: if (isset($jwk['q'])) {
309: unset($jwk['q']);
310: }
311: if (isset($jwk['qi'])) {
312: unset($jwk['qi']);
313: }
314: if (isset($jwk['dp'])) {
315: unset($jwk['dp']);
316: }
317: if (isset($jwk['dq'])) {
318: unset($jwk['dq']);
319: }
320: return $jwk;
321: }
322:
323: /**
324: * Calculates JWS expiration time in seconds
325: *
326: * @return integer
327: */
328: private function getSignatureExpirationTime() {
329: date_default_timezone_set("UTC");
330: $secondsInMinute = 60;
331: return time() + $this->jwsExpirationMinutes * $secondsInMinute;
332: }
333:
334: /**
335: * Checks if header 'exp' param has not expired value
336: *
337: * @param array $header JWS header array
338: *
339: * @throws HyperwalletException
340: */
341: public function checkJwsExpiration($header) {
342: if(!isset($header['exp'])) {
343: throw new HyperwalletException('While trying to verify JWS signature no [exp] header is found');
344: }
345: $exp = $header['exp'];
346: if(!is_numeric($exp)) {
347: throw new HyperwalletException('Wrong value in [exp] header of JWS signature, must be integer');
348: }
349: if((int)time() > (int)$exp) {
350: throw new HyperwalletException('JWS signature has expired, checked by [exp] JWS header');
351: }
352: }
353:
354: /**
355: * Finds the path of composer vendor directory
356: *
357: * @return string
358: *
359: * @throws HyperwalletException
360: */
361: public function getVendorPath() {
362: $reflector = new \ReflectionClass(ClassLoader::class);
363: $vendorPath = preg_replace('/^(.*)\/composer\/ClassLoader\.php$/', '$1', $reflector->getFileName() );
364: if($vendorPath && is_dir($vendorPath)) {
365: return $vendorPath . '/';
366: }
367: throw new HyperwalletException('Failed to find a vendor path');
368: }
369: }
370: