Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.59% covered (success)
94.59%
35 / 37
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Sealed
94.59% covered (success)
94.59%
35 / 37
50.00% covered (danger)
50.00%
2 / 4
15.04
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
3
 unseal
94.12% covered (success)
94.12%
16 / 17
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\ServerSdk\Sealed;
4
5use Fingerprint\ServerSdk\Model\Event;
6use Fingerprint\ServerSdk\ObjectSerializer;
7
8class Sealed
9{
10    private const NONCE_LENGTH = 12;
11    private const AUTH_TAG_LENGTH = 16;
12    private static string $SEAL_HEADER = "\x9E\x85\xDC\xED";
13
14    /**
15     * @param DecryptionKey[] $keys
16     *
17     * @throws UnsealAggregateException
18     * @throws \DateMalformedStringException
19     * @throws InvalidSealedDataException
20     */
21    public static function unsealEventResponse(string $sealed, array $keys): Event
22    {
23        $unsealed = self::unseal($sealed, $keys);
24
25        $data = json_decode($unsealed, true);
26
27        if (!isset($data['event_id']) || !isset($data['timestamp'])) {
28            throw new InvalidSealedDataException();
29        }
30
31        /** @var Event $event */
32        $event = ObjectSerializer::deserialize($unsealed, Event::class);
33
34        return $event;
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     * @throws InvalidSealedDataHeaderException
45     * @throws \InvalidArgumentException
46     *
47     * @noinspection PhpUnusedSwitchBranchInspection
48     */
49    public static function unseal(string $sealed, array $keys): string
50    {
51        if (!str_starts_with($sealed, self::$SEAL_HEADER)) {
52            throw new InvalidSealedDataHeaderException();
53        }
54
55        $aggregateException = new UnsealAggregateException();
56
57        foreach ($keys as $key) {
58            switch ($key->getAlgorithm()) {
59                case DecryptionAlgorithm::AES_256_GCM:
60                    try {
61                        $data = substr($sealed, strlen(self::$SEAL_HEADER));
62
63                        return self::decryptAes256Gcm($data, $key->getKey());
64                    } catch (\Exception $exception) {
65                        $aggregateException->addException(new UnsealException(
66                            'Failed to decrypt',
67                            $exception,
68                            $key
69                        ));
70                    }
71
72                    break;
73
74                default:
75                    throw new \InvalidArgumentException('Invalid decryption algorithm');
76            }
77        }
78
79        throw $aggregateException;
80    }
81
82    /**
83     * @throws \Exception
84     */
85    private static function decryptAes256Gcm(string $sealedData, string $decryptionKey): string
86    {
87        $nonce = substr($sealedData, 0, self::NONCE_LENGTH);
88        $ciphertext = substr($sealedData, self::NONCE_LENGTH);
89
90        $tag = substr($ciphertext, -self::AUTH_TAG_LENGTH);
91        $ciphertext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH);
92
93        $decryptedData = openssl_decrypt($ciphertext, 'aes-256-gcm', $decryptionKey, OPENSSL_RAW_DATA, $nonce, $tag);
94
95        if (false === $decryptedData) {
96            throw new \Exception('Decryption failed');
97        }
98
99        return self::decompress($decryptedData);
100    }
101
102    /**
103     * @param bool|string $data
104     *
105     * @throws DecompressionException
106     */
107    private static function decompress(mixed $data): string
108    {
109        if (false === $data || 0 === strlen($data)) {
110            throw new DecompressionException();
111        }
112        $inflated = @gzinflate($data); // Ignore warnings, because we check the decompressed data's validity and throw error if necessary
113
114        if (false === $inflated) {
115            throw new DecompressionException();
116        }
117
118        return $inflated;
119    }
120}