Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.44% |
34 / 36 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
Sealed | |
94.44% |
34 / 36 |
|
50.00% |
2 / 4 |
14.03 | |
0.00% |
0 / 1 |
unsealEventResponse | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
unseal | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
6.01 | |||
decryptAes256Gcm | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
decompress | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
4.07 |
1 | <?php |
2 | |
3 | namespace Fingerprint\ServerAPI\Sealed; |
4 | |
5 | use Fingerprint\ServerAPI\Model\EventsGetResponse; |
6 | use Fingerprint\ServerAPI\ObjectSerializer; |
7 | use Fingerprint\ServerAPI\SerializationException; |
8 | use GuzzleHttp\Psr7\Response; |
9 | |
10 | class Sealed |
11 | { |
12 | private const NONCE_LENGTH = 12; |
13 | private const AUTH_TAG_LENGTH = 16; |
14 | private static $SEAL_HEADER = "\x9E\x85\xDC\xED"; |
15 | |
16 | /** |
17 | * @param DecryptionKey[] $keys |
18 | * |
19 | * @throws UnsealAggregateException |
20 | * @throws SerializationException |
21 | */ |
22 | public static function unsealEventResponse(string $sealed, array $keys): EventsGetResponse |
23 | { |
24 | $unsealed = self::unseal($sealed, $keys); |
25 | |
26 | $data = json_decode($unsealed, true); |
27 | |
28 | if (!isset($data['products'])) { |
29 | throw new InvalidSealedDataException(); |
30 | } |
31 | |
32 | $response = new Response(200, [], $unsealed); |
33 | |
34 | return ObjectSerializer::deserialize($response, EventsGetResponse::class); |
35 | } |
36 | |
37 | /** |
38 | * Decrypts the sealed response with the provided keys. |
39 | * |
40 | * @param string $sealed Base64 encoded sealed data |
41 | * @param DecryptionKey[] $keys Decryption keys. The SDK will try to decrypt the result with each key until it succeeds. |
42 | * |
43 | * @throws UnsealAggregateException |
44 | */ |
45 | public static function unseal(string $sealed, array $keys): string |
46 | { |
47 | if (substr($sealed, 0, strlen(self::$SEAL_HEADER)) !== self::$SEAL_HEADER) { |
48 | throw new InvalidSealedDataHeaderException(); |
49 | } |
50 | |
51 | $aggregateException = new UnsealAggregateException(); |
52 | |
53 | foreach ($keys as $key) { |
54 | switch ($key->getAlgorithm()) { |
55 | case DecryptionAlgorithm::AES_256_GCM: |
56 | try { |
57 | $data = substr($sealed, strlen(self::$SEAL_HEADER)); |
58 | |
59 | return self::decryptAes256Gcm($data, $key->getKey()); |
60 | } catch (\Exception $exception) { |
61 | $aggregateException->addException(new UnsealException( |
62 | 'Failed to decrypt', |
63 | $exception, |
64 | $key |
65 | )); |
66 | } |
67 | |
68 | break; |
69 | |
70 | default: |
71 | throw new \InvalidArgumentException('Invalid decryption algorithm'); |
72 | } |
73 | } |
74 | |
75 | throw $aggregateException; |
76 | } |
77 | |
78 | /** |
79 | * @param mixed $sealedData |
80 | * @param mixed $decryptionKey |
81 | * |
82 | * @throws \Exception |
83 | */ |
84 | private static function decryptAes256Gcm($sealedData, $decryptionKey): string |
85 | { |
86 | $nonce = substr($sealedData, 0, self::NONCE_LENGTH); |
87 | $ciphertext = substr($sealedData, self::NONCE_LENGTH); |
88 | |
89 | $tag = substr($ciphertext, -self::AUTH_TAG_LENGTH); |
90 | $ciphertext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH); |
91 | |
92 | $decryptedData = openssl_decrypt($ciphertext, 'aes-256-gcm', $decryptionKey, OPENSSL_RAW_DATA, $nonce, $tag); |
93 | |
94 | if (false === $decryptedData) { |
95 | throw new \Exception('Decryption failed'); |
96 | } |
97 | |
98 | return self::decompress($decryptedData); |
99 | } |
100 | |
101 | /** |
102 | * @param mixed $data |
103 | * |
104 | * @throws \Exception |
105 | */ |
106 | private static function decompress($data): string |
107 | { |
108 | if (false === $data || 0 === strlen($data)) { |
109 | throw new DecompressionException(); |
110 | } |
111 | $inflated = @gzinflate($data); // Ignore warnings, because we check the decompressed data's validity and throw error if necessary |
112 | |
113 | if (false === $inflated) { |
114 | throw new DecompressionException(); |
115 | } |
116 | |
117 | return $inflated; |
118 | } |
119 | } |