325 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| declare(strict_types=1);
 | |
| 
 | |
| namespace Jose\Component\Encryption;
 | |
| 
 | |
| use InvalidArgumentException;
 | |
| use Jose\Component\Core\Algorithm;
 | |
| use Jose\Component\Core\AlgorithmManager;
 | |
| use Jose\Component\Core\JWK;
 | |
| use Jose\Component\Core\JWKSet;
 | |
| use Jose\Component\Core\Util\KeyChecker;
 | |
| use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
 | |
| use Jose\Component\Encryption\Algorithm\KeyEncryption\DirectEncryption;
 | |
| use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreement;
 | |
| use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyAgreementWithKeyWrapping;
 | |
| use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyEncryption;
 | |
| use Jose\Component\Encryption\Algorithm\KeyEncryption\KeyWrapping;
 | |
| use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
 | |
| use Jose\Component\Encryption\Compression\CompressionMethodManager;
 | |
| use Throwable;
 | |
| use function array_key_exists;
 | |
| use function is_string;
 | |
| 
 | |
| class JWEDecrypter
 | |
| {
 | |
|     private readonly AlgorithmManager $keyEncryptionAlgorithmManager;
 | |
| 
 | |
|     private readonly AlgorithmManager $contentEncryptionAlgorithmManager;
 | |
| 
 | |
|     public function __construct(
 | |
|         AlgorithmManager $algorithmManager,
 | |
|         null|AlgorithmManager $contentEncryptionAlgorithmManager,
 | |
|         private readonly null|CompressionMethodManager $compressionMethodManager = null
 | |
|     ) {
 | |
|         if ($compressionMethodManager !== null) {
 | |
|             trigger_deprecation(
 | |
|                 'web-token/jwt-library',
 | |
|                 '3.3.0',
 | |
|                 'The parameter "$compressionMethodManager" is deprecated and will be removed in 4.0.0. Compression is not recommended for JWE. Please set "null" instead.'
 | |
|             );
 | |
|         }
 | |
|         if ($contentEncryptionAlgorithmManager !== null) {
 | |
|             trigger_deprecation(
 | |
|                 'web-token/jwt-library',
 | |
|                 '3.3.0',
 | |
|                 'The parameter "$contentEncryptionAlgorithmManager" is deprecated and will be removed in 4.0.0. Please set all algorithms in the first argument and set "null" instead.'
 | |
|             );
 | |
|             $this->keyEncryptionAlgorithmManager = $algorithmManager;
 | |
|             $this->contentEncryptionAlgorithmManager = $contentEncryptionAlgorithmManager;
 | |
|         } else {
 | |
|             $keyEncryptionAlgorithms = [];
 | |
|             $contentEncryptionAlgorithms = [];
 | |
|             foreach ($algorithmManager->all() as $key => $algorithm) {
 | |
|                 if ($algorithm instanceof KeyEncryptionAlgorithm) {
 | |
|                     $keyEncryptionAlgorithms[$key] = $algorithm;
 | |
|                 }
 | |
|                 if ($algorithm instanceof ContentEncryptionAlgorithm) {
 | |
|                     $contentEncryptionAlgorithms[$key] = $algorithm;
 | |
|                 }
 | |
|             }
 | |
|             $this->keyEncryptionAlgorithmManager = new AlgorithmManager($keyEncryptionAlgorithms);
 | |
|             $this->contentEncryptionAlgorithmManager = new AlgorithmManager($contentEncryptionAlgorithms);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the key encryption algorithm manager.
 | |
|      */
 | |
|     public function getKeyEncryptionAlgorithmManager(): AlgorithmManager
 | |
|     {
 | |
|         return $this->keyEncryptionAlgorithmManager;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the content encryption algorithm manager.
 | |
|      */
 | |
|     public function getContentEncryptionAlgorithmManager(): AlgorithmManager
 | |
|     {
 | |
|         return $this->contentEncryptionAlgorithmManager;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the compression method manager.
 | |
|      * @deprecated This method is deprecated and will be removed in v4.0. Compression is not recommended for JWE.
 | |
|      */
 | |
|     public function getCompressionMethodManager(): null|CompressionMethodManager
 | |
|     {
 | |
|         return $this->compressionMethodManager;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method will try to decrypt the given JWE and recipient using a JWK.
 | |
|      *
 | |
|      * @param JWE $jwe A JWE object to decrypt
 | |
|      * @param JWK $jwk The key used to decrypt the input
 | |
|      * @param int $recipient The recipient used to decrypt the token
 | |
|      */
 | |
|     public function decryptUsingKey(JWE &$jwe, JWK $jwk, int $recipient, ?JWK $senderKey = null): bool
 | |
|     {
 | |
|         $jwkset = new JWKSet([$jwk]);
 | |
| 
 | |
|         return $this->decryptUsingKeySet($jwe, $jwkset, $recipient, $senderKey);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method will try to decrypt the given JWE and recipient using a JWKSet.
 | |
|      *
 | |
|      * @param JWE $jwe A JWE object to decrypt
 | |
|      * @param JWKSet $jwkset The key set used to decrypt the input
 | |
|      * @param JWK $jwk The key used to decrypt the token in case of success
 | |
|      * @param int $recipient The recipient used to decrypt the token in case of success
 | |
|      */
 | |
|     public function decryptUsingKeySet(
 | |
|         JWE &$jwe,
 | |
|         JWKSet $jwkset,
 | |
|         int $recipient,
 | |
|         ?JWK &$jwk = null,
 | |
|         ?JWK $senderKey = null
 | |
|     ): bool {
 | |
|         if ($jwkset->count() === 0) {
 | |
|             throw new InvalidArgumentException('No key in the key set.');
 | |
|         }
 | |
|         if ($jwe->getPayload() !== null) {
 | |
|             return true;
 | |
|         }
 | |
|         if ($jwe->countRecipients() === 0) {
 | |
|             throw new InvalidArgumentException('The JWE does not contain any recipient.');
 | |
|         }
 | |
| 
 | |
|         $plaintext = $this->decryptRecipientKey($jwe, $jwkset, $recipient, $jwk, $senderKey);
 | |
|         if ($plaintext !== null) {
 | |
|             $jwe = $jwe->withPayload($plaintext);
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     private function decryptRecipientKey(
 | |
|         JWE $jwe,
 | |
|         JWKSet $jwkset,
 | |
|         int $i,
 | |
|         ?JWK &$successJwk = null,
 | |
|         ?JWK $senderKey = null
 | |
|     ): ?string {
 | |
|         $recipient = $jwe->getRecipient($i);
 | |
|         $completeHeader = array_merge(
 | |
|             $jwe->getSharedProtectedHeader(),
 | |
|             $jwe->getSharedHeader(),
 | |
|             $recipient->getHeader()
 | |
|         );
 | |
|         $this->checkCompleteHeader($completeHeader);
 | |
| 
 | |
|         $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($completeHeader);
 | |
|         $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($completeHeader);
 | |
| 
 | |
|         $this->checkIvSize($jwe->getIV(), $content_encryption_algorithm->getIVSize());
 | |
| 
 | |
|         foreach ($jwkset as $recipientKey) {
 | |
|             try {
 | |
|                 KeyChecker::checkKeyUsage($recipientKey, 'decryption');
 | |
|                 if ($key_encryption_algorithm->name() !== 'dir') {
 | |
|                     KeyChecker::checkKeyAlgorithm($recipientKey, $key_encryption_algorithm->name());
 | |
|                 } else {
 | |
|                     KeyChecker::checkKeyAlgorithm($recipientKey, $content_encryption_algorithm->name());
 | |
|                 }
 | |
|                 $cek = $this->decryptCEK(
 | |
|                     $key_encryption_algorithm,
 | |
|                     $content_encryption_algorithm,
 | |
|                     $recipientKey,
 | |
|                     $senderKey,
 | |
|                     $recipient,
 | |
|                     $completeHeader
 | |
|                 );
 | |
|                 $this->checkCekSize($cek, $key_encryption_algorithm, $content_encryption_algorithm);
 | |
|                 $payload = $this->decryptPayload($jwe, $cek, $content_encryption_algorithm, $completeHeader);
 | |
|                 $successJwk = $recipientKey;
 | |
| 
 | |
|                 return $payload;
 | |
|             } catch (Throwable) {
 | |
|                 //We do nothing, we continue with other keys
 | |
|                 continue;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return null;
 | |
|     }
 | |
| 
 | |
|     private function checkCekSize(
 | |
|         string $cek,
 | |
|         KeyEncryptionAlgorithm $keyEncryptionAlgorithm,
 | |
|         ContentEncryptionAlgorithm $algorithm
 | |
|     ): void {
 | |
|         if ($keyEncryptionAlgorithm instanceof DirectEncryption || $keyEncryptionAlgorithm instanceof KeyAgreement) {
 | |
|             return;
 | |
|         }
 | |
|         if (mb_strlen($cek, '8bit') * 8 !== $algorithm->getCEKSize()) {
 | |
|             throw new InvalidArgumentException('Invalid CEK size');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private function checkIvSize(?string $iv, int $requiredIvSize): void
 | |
|     {
 | |
|         if ($iv === null && $requiredIvSize !== 0) {
 | |
|             throw new InvalidArgumentException('Invalid IV size');
 | |
|         }
 | |
|         if (is_string($iv) && mb_strlen($iv, '8bit') !== $requiredIvSize / 8) {
 | |
|             throw new InvalidArgumentException('Invalid IV size');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private function decryptCEK(
 | |
|         Algorithm $key_encryption_algorithm,
 | |
|         ContentEncryptionAlgorithm $content_encryption_algorithm,
 | |
|         JWK $recipientKey,
 | |
|         ?JWK $senderKey,
 | |
|         Recipient $recipient,
 | |
|         array $completeHeader
 | |
|     ): string {
 | |
|         if ($key_encryption_algorithm instanceof DirectEncryption) {
 | |
|             return $key_encryption_algorithm->getCEK($recipientKey);
 | |
|         }
 | |
|         if ($key_encryption_algorithm instanceof KeyAgreement) {
 | |
|             return $key_encryption_algorithm->getAgreementKey(
 | |
|                 $content_encryption_algorithm->getCEKSize(),
 | |
|                 $content_encryption_algorithm->name(),
 | |
|                 $recipientKey,
 | |
|                 $senderKey,
 | |
|                 $completeHeader
 | |
|             );
 | |
|         }
 | |
|         if ($key_encryption_algorithm instanceof KeyAgreementWithKeyWrapping) {
 | |
|             return $key_encryption_algorithm->unwrapAgreementKey(
 | |
|                 $recipientKey,
 | |
|                 $senderKey,
 | |
|                 $recipient->getEncryptedKey() ?? '',
 | |
|                 $content_encryption_algorithm->getCEKSize(),
 | |
|                 $completeHeader
 | |
|             );
 | |
|         }
 | |
|         if ($key_encryption_algorithm instanceof KeyEncryption) {
 | |
|             return $key_encryption_algorithm->decryptKey(
 | |
|                 $recipientKey,
 | |
|                 $recipient->getEncryptedKey() ?? '',
 | |
|                 $completeHeader
 | |
|             );
 | |
|         }
 | |
|         if ($key_encryption_algorithm instanceof KeyWrapping) {
 | |
|             return $key_encryption_algorithm->unwrapKey(
 | |
|                 $recipientKey,
 | |
|                 $recipient->getEncryptedKey() ?? '',
 | |
|                 $completeHeader
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         throw new InvalidArgumentException('Unsupported CEK generation');
 | |
|     }
 | |
| 
 | |
|     private function decryptPayload(
 | |
|         JWE $jwe,
 | |
|         string $cek,
 | |
|         ContentEncryptionAlgorithm $content_encryption_algorithm,
 | |
|         array $completeHeader
 | |
|     ): string {
 | |
|         $payload = $content_encryption_algorithm->decryptContent(
 | |
|             $jwe->getCiphertext() ?? '',
 | |
|             $cek,
 | |
|             $jwe->getIV() ?? '',
 | |
|             $jwe->getAAD(),
 | |
|             $jwe->getEncodedSharedProtectedHeader(),
 | |
|             $jwe->getTag() ?? ''
 | |
|         );
 | |
| 
 | |
|         return $this->decompressIfNeeded($payload, $completeHeader);
 | |
|     }
 | |
| 
 | |
|     private function decompressIfNeeded(string $payload, array $completeHeaders): string
 | |
|     {
 | |
|         if ($this->compressionMethodManager === null || ! array_key_exists('zip', $completeHeaders)) {
 | |
|             return $payload;
 | |
|         }
 | |
| 
 | |
|         $compression_method = $this->compressionMethodManager->get($completeHeaders['zip']);
 | |
| 
 | |
|         return $compression_method->uncompress($payload);
 | |
|     }
 | |
| 
 | |
|     private function checkCompleteHeader(array $completeHeaders): void
 | |
|     {
 | |
|         foreach (['enc', 'alg'] as $key) {
 | |
|             if (! isset($completeHeaders[$key])) {
 | |
|                 throw new InvalidArgumentException(sprintf("Parameter '%s' is missing.", $key));
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private function getKeyEncryptionAlgorithm(array $completeHeaders): KeyEncryptionAlgorithm
 | |
|     {
 | |
|         $key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($completeHeaders['alg']);
 | |
|         if (! $key_encryption_algorithm instanceof KeyEncryptionAlgorithm) {
 | |
|             throw new InvalidArgumentException(sprintf(
 | |
|                 'The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithm interface.',
 | |
|                 $completeHeaders['alg']
 | |
|             ));
 | |
|         }
 | |
| 
 | |
|         return $key_encryption_algorithm;
 | |
|     }
 | |
| 
 | |
|     private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm
 | |
|     {
 | |
|         $content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']);
 | |
|         if (! $content_encryption_algorithm instanceof ContentEncryptionAlgorithm) {
 | |
|             throw new InvalidArgumentException(sprintf(
 | |
|                 'The key encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.',
 | |
|                 $completeHeader['enc']
 | |
|             ));
 | |
|         }
 | |
| 
 | |
|         return $content_encryption_algorithm;
 | |
|     }
 | |
| }
 |