Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.44% covered (success)
94.44%
34 / 36
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Sealed
94.44% covered (success)
94.44%
34 / 36
50.00% covered (danger)
50.00%
2 / 4
14.03
0.00% covered (danger)
0.00%
0 / 1
 unsealEventResponse
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 unseal
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
6.01
 decryptAes256Gcm
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 decompress
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
1<?php
2
3namespace Fingerprint\ServerAPI\Sealed;
4
5use Fingerprint\ServerAPI\Model\EventsGetResponse;
6use Fingerprint\ServerAPI\ObjectSerializer;
7use Fingerprint\ServerAPI\SerializationException;
8use GuzzleHttp\Psr7\Response;
9
10class 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}