diff --git a/src/Sealed/DecompressionException.php b/src/Sealed/DecompressionException.php index 6f6d3763..1b903424 100644 --- a/src/Sealed/DecompressionException.php +++ b/src/Sealed/DecompressionException.php @@ -2,8 +2,16 @@ namespace Fingerprint\ServerAPI\Sealed; -class DecompressionException extends \Exception +use Exception; + +/** + * Thrown when decompression of the decrypted sealed data fails. + */ +class DecompressionException extends Exception { + /** + * Creates a new DecompressionException instance. + */ public function __construct() { parent::__construct('Decompression failed'); diff --git a/src/Sealed/DecryptionAlgorithm.php b/src/Sealed/DecryptionAlgorithm.php index 80427cf9..2e171a1b 100644 --- a/src/Sealed/DecryptionAlgorithm.php +++ b/src/Sealed/DecryptionAlgorithm.php @@ -2,6 +2,9 @@ namespace Fingerprint\ServerAPI\Sealed; +/** + * Supported decryption algorithms for sealed results. + */ class DecryptionAlgorithm { public const AES_256_GCM = 'aes-256-gcm'; diff --git a/src/Sealed/DecryptionKey.php b/src/Sealed/DecryptionKey.php index 98799a73..3deba21f 100644 --- a/src/Sealed/DecryptionKey.php +++ b/src/Sealed/DecryptionKey.php @@ -2,23 +2,38 @@ namespace Fingerprint\ServerAPI\Sealed; +/** + * Holds a decryption key and its algorithm for unsealing sealed results. + */ class DecryptionKey { - private $key; - private $algorithm; + private readonly string $key; + private readonly string $algorithm; - public function __construct($key, $algorithm) + /** + * Creates a new DecryptionKey instance. + * + * @param string $key raw binary decryption key + * @param string $algorithm algorithm identifier ({@see DecryptionAlgorithm}) + */ + public function __construct(string $key, string $algorithm) { $this->key = $key; $this->algorithm = $algorithm; } - public function getKey() + /** + * Returns the raw binary decryption key. + */ + public function getKey(): string { return $this->key; } - public function getAlgorithm() + /** + * Returns the algorithm identifier ({@see DecryptionAlgorithm}). + */ + public function getAlgorithm(): string { return $this->algorithm; } diff --git a/src/Sealed/InvalidSealedDataException.php b/src/Sealed/InvalidSealedDataException.php index 809199f9..0b9229da 100644 --- a/src/Sealed/InvalidSealedDataException.php +++ b/src/Sealed/InvalidSealedDataException.php @@ -2,8 +2,16 @@ namespace Fingerprint\ServerAPI\Sealed; -class InvalidSealedDataException extends \InvalidArgumentException +use InvalidArgumentException; + +/** + * Thrown when decrypted sealed data does not contain a valid event response. + */ +class InvalidSealedDataException extends InvalidArgumentException { + /** + * Creates a new InvalidSealedDataException instance. + */ public function __construct() { parent::__construct('Invalid sealed data'); diff --git a/src/Sealed/InvalidSealedDataHeaderException.php b/src/Sealed/InvalidSealedDataHeaderException.php index 9f4d70e9..16a6e694 100644 --- a/src/Sealed/InvalidSealedDataHeaderException.php +++ b/src/Sealed/InvalidSealedDataHeaderException.php @@ -2,8 +2,16 @@ namespace Fingerprint\ServerAPI\Sealed; -class InvalidSealedDataHeaderException extends \InvalidArgumentException +use InvalidArgumentException; + +/** + * Thrown when the sealed payload does not start with the expected header bytes. + */ +class InvalidSealedDataHeaderException extends InvalidArgumentException { + /** + * Creates a new InvalidSealedDataHeaderException instance. + */ public function __construct() { parent::__construct('Invalid sealed data header'); diff --git a/src/Sealed/Sealed.php b/src/Sealed/Sealed.php index c5aac281..3ad6e609 100644 --- a/src/Sealed/Sealed.php +++ b/src/Sealed/Sealed.php @@ -2,22 +2,37 @@ namespace Fingerprint\ServerAPI\Sealed; +use Exception; use Fingerprint\ServerAPI\Model\EventsGetResponse; use Fingerprint\ServerAPI\ObjectSerializer; -use Fingerprint\ServerAPI\SerializationException; use GuzzleHttp\Psr7\Response; +use InvalidArgumentException; +/** + * Provides methods to decrypt and deserialize sealed results. + * + * Sealed results are encrypted and compressed payloads. + */ class Sealed { + /** @var int AES-256-GCM nonce length in bytes. */ private const NONCE_LENGTH = 12; + + /** @var int AES-256-GCM authentication tag length in bytes. */ private const AUTH_TAG_LENGTH = 16; - private static $SEAL_HEADER = "\x9E\x85\xDC\xED"; + + /** @var string Magic bytes that prefix every sealed payload. */ + private const SEAL_HEADER = "\x9E\x85\xDC\xED"; /** - * @param DecryptionKey[] $keys + * Unseals and deserializes a sealed response into an EventsGetResponse. + * + * @param string $sealed raw sealed payload + * @param DecryptionKey[] $keys decryption keys to try in order * - * @throws UnsealAggregateException - * @throws SerializationException + * @throws UnsealAggregateException when decryption fails with every provided key + * @throws InvalidSealedDataException when decrypted payload is not a valid event response + * @throws Exception */ public static function unsealEventResponse(string $sealed, array $keys): EventsGetResponse { @@ -37,14 +52,17 @@ public static function unsealEventResponse(string $sealed, array $keys): EventsG /** * Decrypts the sealed response with the provided keys. * - * @param string $sealed Base64 encoded sealed data - * @param DecryptionKey[] $keys Decryption keys. The SDK will try to decrypt the result with each key until it succeeds. + * Tries each key in order; returns the decrypted plaintext on the first + * successful decryption. If all keys fail, throws an aggregate exception. * - * @throws UnsealAggregateException + * @param string $sealed raw sealed payload + * @param DecryptionKey[] $keys decryption keys to try in order + * + * @throws UnsealAggregateException when decryption fails with every provided key */ public static function unseal(string $sealed, array $keys): string { - if (substr($sealed, 0, strlen(self::$SEAL_HEADER)) !== self::$SEAL_HEADER) { + if (!str_starts_with($sealed, self::SEAL_HEADER)) { throw new InvalidSealedDataHeaderException(); } @@ -54,10 +72,10 @@ public static function unseal(string $sealed, array $keys): string switch ($key->getAlgorithm()) { case DecryptionAlgorithm::AES_256_GCM: try { - $data = substr($sealed, strlen(self::$SEAL_HEADER)); + $data = substr($sealed, strlen(self::SEAL_HEADER)); return self::decryptAes256Gcm($data, $key->getKey()); - } catch (\Exception $exception) { + } catch (Exception $exception) { $aggregateException->addException(new UnsealException( 'Failed to decrypt', $exception, @@ -68,7 +86,7 @@ public static function unseal(string $sealed, array $keys): string break; default: - throw new \InvalidArgumentException('Invalid decryption algorithm'); + throw new InvalidArgumentException('Invalid decryption algorithm'); } } @@ -76,12 +94,16 @@ public static function unseal(string $sealed, array $keys): string } /** - * @param mixed $sealedData - * @param mixed $decryptionKey + * Decrypts an AES-256-GCM payload and decompresses the result. + * + * @param string $sealedData nonce + ciphertext + auth tag + * @param string $decryptionKey raw 256-bit key * - * @throws \Exception + * @return string decompressed plaintext + * + * @throws Exception on decryption failure */ - private static function decryptAes256Gcm($sealedData, $decryptionKey): string + private static function decryptAes256Gcm(string $sealedData, string $decryptionKey): string { $nonce = substr($sealedData, 0, self::NONCE_LENGTH); $ciphertext = substr($sealedData, self::NONCE_LENGTH); @@ -92,23 +114,29 @@ private static function decryptAes256Gcm($sealedData, $decryptionKey): string $decryptedData = openssl_decrypt($ciphertext, 'aes-256-gcm', $decryptionKey, OPENSSL_RAW_DATA, $nonce, $tag); if (false === $decryptedData) { - throw new \Exception('Decryption failed'); + throw new Exception('Decryption failed'); } return self::decompress($decryptedData); } /** - * @param mixed $data + * Decompresses raw-deflated data. + * + * @param bool|string $data raw deflate-compressed data * - * @throws \Exception + * @return string decompressed data + * + * @throws DecompressionException when decompression fails or input is empty */ - private static function decompress($data): string + private static function decompress(bool|string $data): string { if (false === $data || 0 === strlen($data)) { throw new DecompressionException(); } - $inflated = @gzinflate($data); // Ignore warnings, because we check the decompressed data's validity and throw error if necessary + + // Ignore warnings, because we check the decompressed data's validity and throw error if necessary + $inflated = @gzinflate($data); if (false === $inflated) { throw new DecompressionException(); diff --git a/src/Sealed/UnsealAggregateException.php b/src/Sealed/UnsealAggregateException.php index 00f3969b..51edf7a8 100644 --- a/src/Sealed/UnsealAggregateException.php +++ b/src/Sealed/UnsealAggregateException.php @@ -2,24 +2,40 @@ namespace Fingerprint\ServerAPI\Sealed; -class UnsealAggregateException extends \Exception +use Exception; + +/** + * Thrown when unsealing fails with all provided decryption keys. + * + * Contains the individual {@see UnsealException} for each key that was tried. + */ +class UnsealAggregateException extends Exception { + /** @var Exception[] */ + private array $exceptions = []; + /** - * @var \Exception[] + * Creates a new UnsealAggregateException instance. */ - private $exceptions; - public function __construct() { parent::__construct('Failed to unseal with all decryption keys'); } - public function addException(\Exception $exception) + /** + * Adds a decryption failure to this aggregate. + */ + public function addException(Exception $exception): void { $this->exceptions[] = $exception; } - public function getExceptions() + /** + * Returns all collected decryption exceptions. + * + * @return Exception[] + */ + public function getExceptions(): array { return $this->exceptions; } diff --git a/src/Sealed/UnsealException.php b/src/Sealed/UnsealException.php index 16bbad3a..6399620d 100644 --- a/src/Sealed/UnsealException.php +++ b/src/Sealed/UnsealException.php @@ -2,20 +2,48 @@ namespace Fingerprint\ServerAPI\Sealed; -class UnsealException extends \Exception +use Exception; + +/** + * Thrown when a single decryption key fails to unseal the data. + * + * Carries the {@see DecryptionKey} that was used, so callers can + * identify which key failed. + */ +class UnsealException extends Exception { - public $decryptionKeyDescription; + private readonly DecryptionKey $decryptionKey; - public function __construct($message, $cause, $decryptionKey) + /** + * Creates a new UnsealException instance. + * + * @param string $message error description + * @param Exception $cause underlying decryption exception + * @param DecryptionKey $decryptionKey the key that failed + */ + public function __construct(string $message, Exception $cause, DecryptionKey $decryptionKey) { parent::__construct($message, 0, $cause); - $this->decryptionKeyDescription = $decryptionKey; + $this->decryptionKey = $decryptionKey; + } + + /** + * Returns the decryption key that was used when this failure occurred. + */ + public function getDecryptionKey(): DecryptionKey + { + return $this->decryptionKey; } - public function __toString() + /** + * String representation of the exception. + * + * @return string + */ + public function __toString(): string { return 'UnsealException{'. - 'decryptionKey='.$this->decryptionKeyDescription. + 'decryptionKey='.$this->decryptionKey->getAlgorithm(). ', message='.$this->getMessage(). ', cause='.$this->getPrevious(). '}'; diff --git a/test/Sealed/SealedTest.php b/test/SealedTest.php similarity index 71% rename from test/Sealed/SealedTest.php rename to test/SealedTest.php index 2a549d3e..188dbe28 100644 --- a/test/Sealed/SealedTest.php +++ b/test/SealedTest.php @@ -1,37 +1,53 @@ validKey = base64_decode(self::VALID_KEY); @@ -43,6 +59,12 @@ protected function setUp(): void ]; } + /** + * Verifies that a valid sealed payload is decrypted and deserialized. + * + * @throws UnsealAggregateException + * @throws Exception + */ public function testUnsealEventResponse() { $sealedResult = base64_decode(self::SEALED_RESULT); @@ -59,6 +81,11 @@ public function testUnsealEventResponse() $this->assertEquals($expectedResponse, $eventResponse); } + /** + * Verifies that decrypted invalid data throws InvalidSealedDataException. + * + * @throws UnsealAggregateException + */ public function testUnsealEventResponseWithInvalidSealedResult() { // "{\"invalid\":true}" @@ -72,6 +99,11 @@ public function testUnsealEventResponseWithInvalidSealedResult() ); } + /** + * Verifies that decrypted data containing invalid JSON throws InvalidSealedDataException. + * + * @throws UnsealAggregateException + */ public function testUnsealEventResponseWithInvalidJsonSealedResult() { $sealedResult = base64_decode('noXc7XdbEp5JpFNJaMxCB5leuFeW9Fs0tqvwnbU3ND2yShYn+dgeUWvdk32YrXam4yuvhmpO8gww//Qmsu2sbyvyMRuXmlKoriV9EVPYVCB2xszskg34ngrAh4sreRZV3c8d0DcXZulbMiiXrli931fEABWRHM0NtcoPuubqb+TysNSoFIYVZxpRVDR8jDiTXuQyPzvqBJD4+xeQTOOAOjPlqRTQSSBrlWjeZLNA70wWX7VRDXA1SoR+1k7bkBFK4OwRnh5rVGeGvGeHisOe/SyOL6GlQyBk3sRdSCQiI/g0ywdqLsOk4xDdCgg5vMI07APvL9FSaQrglMvD8NRmQOr5glZoV6S3DoBgaYQVvEygTZy2gfJ0z6hLY6Q8WSW0hpb3t9m4MP9WC5Vc2r0fmfqX7gjYZpwyfJxsyyk4iksminhm2T8N8DTYuZuz82jjaGNDqAPn1PZKqiEh8H9TpcgewAP8mlVrB5CUPJMHH+p7dM5zibfKM9+1MPxvZNp0PBkljBwrfGjiKlmYhn7bb5UW5TeEMtiP27KoA26PX+NV130Vi9Y/LUgMivLwaIc+jnlFyaoqg6Kg6H8G3WhT0r/pc4KP0mwyHJzfXjep8kQZGKxbMd0Sc3h4kpoWR1hdYM4QZRvKQzh7BqBPtPiVgHYoEJf9qFVxYhel9UFONz65q5bA2Y25oFKpzfsiXQqFEo/LRANnW7iUdfesGtGjjP4N6rd8ssNpYf57FmPBpWC4RwjG45MHRUSajCVLKiwUgFQbOo7/t5hgQIQOui3jmCBDjCjpjGZK8vd2nFputUTqI/MmZK7THaDPFsn8h9M1boF3VMCzDXygJFhd5lwdVErXGtQcc1lApEvdOr24QB5Io4SjfjJCfEQ7g4ulBXuqsh6I4VkcuMh5zgBIdmGm'); @@ -84,6 +116,11 @@ public function testUnsealEventResponseWithInvalidJsonSealedResult() ); } + /** + * Verifies that a payload encrypted without compression throws DecompressionException. + * + * @throws Exception + */ public function testUnsealEventResponseWithNotCompressedSealedResult() { $sealedResult = base64_decode('noXc7dtuk0smGE+ZbaoXzrp6Rq8ySxLepejTsu7+jUXlPhV1w+WuHx9gbPhaENJnOQo8BcGmsaRhL5k2NVj+DRNzYO9cQD7wHxmXKCyTbl/dvSYOMoHziUZ2VbQ7tmaorFny26v8jROr/UBGfvPE0dLKC36IN9ZlJ3X0NZJO8SY+8bCr4mTrkVZsv/hpvZp+OjC4h7e5vxcpmnBWXzxfaO79Lq3aMRIEf9XfK7/bVIptHaEqtPKCTwl9rz1KUpUUNQSHTPM0NlqJe9bjYf5mr1uYvWHhcJoXSyRyVMxIv/quRiw3SKJzAMOTBiAvFICpWuRFa+T/xIMHK0g96w/IMQo0jdY1E067ZEvBUOBmsJnGJg1LllS3rbJVe+E2ClFNL8SzFphyvtlcfvYB+SVSD4bzI0w/YCldv5Sq42BFt5bn4n4aE5A6658DYsfSRYWqP6OpqPJx96cY34W7H1t/ZG0ulez6zF5NvWhc1HDQ1gMtXd+K/ogt1n+FyFtn8xzvtSGkmrc2jJgYNI5Pd0Z0ent73z0MKbJx9v2ta/emPEzPr3cndN5amdr6TmRkDU4bq0vyhAh87DJrAnJQLdrvYLddnrr8xTdeXxj1i1Yug6SGncPh9sbTYkdOfuamPAYOuiJVBAMcfYsYEiQndZe8mOQ4bpCr+hxAAqixhZ16pQ8CeUwa247+D2scRymLB8qJXlaERuFZtWGVAZ8VP/GS/9EXjrzpjGX9vlrIPeJP8fh2S5QPzw55cGNJ7JfAdOyManXnoEw2/QzDhSZQARVl+akFgSO0Y13YmbiL7H6HcKWGcJ2ipDKIaj2fJ7GE0Vzyt+CBEezSQR99Igd8x3p2JtvsVKp35iLPksjS1VqtSCTbuIRUlINlfQHNjeQiE/B/61jo3Mf7SmjYjqtvXt5e9RKb+CQku2qH4ZU8xN3DSg+4mLom3BgKBkm/MoyGBpMK41c96d2tRp3tp4hV0F6ac02Crg7P2lw8IUct+i2VJ8VUjcbRfTIPQs0HjNjM6/gLfLCkWOHYrlFjwusXWQCJz91Kq+hVxj7M9LtplPO4AUq6RUMNhlPGUmyOI2tcUMrjq9vMLXGlfdkH185zM4Mk+O7DRLC8683lXZFZvcBEmxr855PqLLH/9SpYKHBoGRatDRdQe3oRp6gHS0jpQ1SW/si4kvLKiUNjiBExvbQVOUV7/VFXvG1RpM9wbzSoOd40gg7ZzD/72QshUC/25DkM/Pm7RBzwtjgmnRKjT+mROeC/7VQLoz3amv09O8Mvbt+h/lX5+51Q834F7NgIGagbB20WtWcMtrmKrvCEZlaoiZrmYVSbi1RfknRK7CTPJkopw9IjO7Ut2EhKZ+jL4rwk6TlVm6EC6Kuj7KNqp6wB/UNe9eM2Eym/aiHAcja8XN4YQhSIuJD2Wxb0n3LkKnAjK1/GY65c8K6rZsVYQ0MQL1j4lMl0UZPjG/vzKyetIsVDyXc4J9ZhOEMYnt/LaxEeSt4EMJGBA9wpTmz33X4h3ij0Y3DY/rH7lrEScUknw20swTZRm5T6q1bnimj7M1OiOkebdI09MZ0nyaTWRHdB7B52C/moh89Q7qa2Fulp5h8Us1FYRkWBLt37a5rGI1IfVeP38KaPbagND+XzWpNqX4HVrAVPLQVK5EwUvGamED3ooJ0FMieTc0IH0N+IeUYG7Q8XmrRVBcw32W8pEfYLO9L71An/J0jQZCIP8DuQnUG0mOvunOuloBGvP/9LvkBlkamh68F0a5f5ny1jloyIFJhRh5dt2SBlbsXS9AKqUwARYSSsA9Ao4WJWOZMyjp8A+qIBAfW65MdhhUDKYMBgIAbMCc3uiptzElQQopE5TT5xIhwfYxa503jVzQbz1Q=='); @@ -105,6 +142,11 @@ public function testUnsealEventResponseWithNotCompressedSealedResult() } } + /** + * Verifies that a payload with a wrong magic header throws InvalidSealedDataHeaderException. + * + * @throws UnsealAggregateException + */ public function testUnsealEventResponseWithInvalidHeader() { $sealedResult = base64_decode('noXc7xXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNxlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw=='); @@ -117,6 +159,11 @@ public function testUnsealEventResponseWithInvalidHeader() ); } + /** + * Verifies that an empty payload throws InvalidSealedDataHeaderException. + * + * @throws UnsealAggregateException + */ public function testUnsealEventResponseWithEmptyData() { $sealedResult = ''; @@ -129,6 +176,11 @@ public function testUnsealEventResponseWithEmptyData() ); } + /** + * Verifies that using only invalid keys throws UnsealAggregateException. + * + * @throws UnsealAggregateException + */ public function testUnsealEventResponseWithInvalidKeys() { $sealedResult = base64_decode('noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw=='); @@ -141,6 +193,40 @@ public function testUnsealEventResponseWithInvalidKeys() ); } + /** + * Verifies that UnsealException exposes the DecryptionKey that caused the failure. + * + * @throws Exception + */ + public function testUnsealExceptionContainsDecryptionKey() + { + $sealedResult = base64_decode('noXc7SXO+mqeAGrvBMgObi/S0fXTpP3zupk8qFqsO/1zdtWCD169iLA3VkkZh9ICHpZ0oWRzqG0M9/TnCeKFohgBLqDp6O0zEfXOv6i5q++aucItznQdLwrKLP+O0blfb4dWVI8/aSbd4ELAZuJJxj9bCoVZ1vk+ShbUXCRZTD30OIEAr3eiG9aw00y1UZIqMgX6CkFlU9L9OnKLsNsyomPIaRHTmgVTI5kNhrnVNyNsnzt9rY7fUD52DQxJILVPrUJ1Q+qW7VyNslzGYBPG0DyYlKbRAomKJDQIkdj/Uwa6bhSTq4XYNVvbk5AJ/dGwvsVdOnkMT2Ipd67KwbKfw5bqQj/cw6bj8Cp2FD4Dy4Ud4daBpPRsCyxBM2jOjVz1B/lAyrOp8BweXOXYugwdPyEn38MBZ5oL4D38jIwR/QiVnMHpERh93jtgwh9Abza6i4/zZaDAbPhtZLXSM5ztdctv8bAb63CppLU541Kf4OaLO3QLvfLRXK2n8bwEwzVAqQ22dyzt6/vPiRbZ5akh8JB6QFXG0QJF9DejsIspKF3JvOKjG2edmC9o+GfL3hwDBiihYXCGY9lElZICAdt+7rZm5UxMx7STrVKy81xcvfaIp1BwGh/HyMsJnkE8IczzRFpLlHGYuNDxdLoBjiifrmHvOCUDcV8UvhSV+UAZtAVejdNGo5G/bz0NF21HUO4pVRPu6RqZIs/aX4hlm6iO/0Ru00ct8pfadUIgRcephTuFC2fHyZxNBC6NApRtLSNLfzYTTo/uSjgcu6rLWiNo5G7yfrM45RXjalFEFzk75Z/fu9lCJJa5uLFgDNKlU+IaFjArfXJCll3apbZp4/LNKiU35ZlB7ZmjDTrji1wLep8iRVVEGht/DW00MTok7Zn7Fv+MlxgWmbZB3BuezwTmXb/fNw=='); + + $invalidKey = new DecryptionKey($this->invalidKey, DecryptionAlgorithm::AES_256_GCM); + + try { + Sealed::unsealEventResponse($sealedResult, [$invalidKey]); + $this->fail('Expected UnsealAggregateException'); + } catch (UnsealAggregateException $e) { + $exceptions = $e->getExceptions(); + $this->assertCount(1, $exceptions); + + /** @var UnsealException $unsealException */ + $unsealException = $exceptions[0]; + $this->assertInstanceOf(UnsealException::class, $unsealException); + + $returnedKey = $unsealException->getDecryptionKey(); + $this->assertSame($invalidKey, $returnedKey); + $this->assertSame($this->invalidKey, $returnedKey->getKey()); + $this->assertSame(DecryptionAlgorithm::AES_256_GCM, $returnedKey->getAlgorithm()); + } + } + + /** + * Verifies that a payload with an invalid nonce throws UnsealAggregateException. + * + * @throws UnsealAggregateException + */ public function testUnsealEventResponseWithInvalidNonce() { $sealedResult = "\x9E\x85\xDC\xED\xAA\xBB\xCC"; @@ -153,6 +239,11 @@ public function testUnsealEventResponseWithInvalidNonce() ); } + /** + * Verifies that deserialized model properties have the correct PHP types. + * + * @throws UnsealAggregateException + */ public function testTypesAreDefinedCorrect() { $sealed = base64_decode(self::SEALED_RESULT); @@ -164,8 +255,8 @@ public function testTypesAreDefinedCorrect() $confidence = $unsealed->getProducts()->getIdentification()->getData()->getConfidence(); $botResult = $unsealed->getProducts()->getBotd()->getData()->getBot(); $this->assertEquals(DeprecatedGeolocation::class, get_class($ipLocation)); - $this->assertEquals(\DateTime::class, get_class($identificationTime)); - $this->assertEquals(\DateTime::class, get_class($botTime)); + $this->assertEquals(DateTime::class, get_class($identificationTime)); + $this->assertEquals(DateTime::class, get_class($botTime)); $this->assertEquals(BrowserDetails::class, get_class($browserDetails)); $this->assertEquals(IdentificationConfidence::class, get_class($confidence)); $this->assertEquals(BotdBot::class, get_class($botResult));