├── .github ├── CONTRIBUTING.md ├── FUNDING.yml └── PULL_REQUEST_TEMPLATE.md ├── Algorithm ├── ContentEncryptionAlgorithm.php ├── KeyEncryption │ ├── DirectEncryption.php │ ├── KeyAgreement.php │ ├── KeyAgreementWithKeyWrapping.php │ ├── KeyEncryption.php │ └── KeyWrapping.php └── KeyEncryptionAlgorithm.php ├── Compression ├── CompressionMethod.php ├── CompressionMethodManager.php ├── CompressionMethodManagerFactory.php └── Deflate.php ├── JWE.php ├── JWEBuilder.php ├── JWEBuilderFactory.php ├── JWEDecrypter.php ├── JWEDecrypterFactory.php ├── JWELoader.php ├── JWELoaderFactory.php ├── JWETokenSupport.php ├── LICENSE ├── README.md ├── Recipient.php ├── Serializer ├── CompactSerializer.php ├── JSONFlattenedSerializer.php ├── JSONGeneralSerializer.php ├── JWESerializer.php ├── JWESerializerManager.php └── JWESerializerManagerFactory.php └── composer.json /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY. 4 | Please do not submit any Pull Requests here. It will be automatically closed. 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: FlorentMorselli 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please do not submit any Pull Requests here. It will be automatically closed. 2 | 3 | You should submit it here: https://github.com/web-token/jwt-framework/pulls 4 | -------------------------------------------------------------------------------- /Algorithm/ContentEncryptionAlgorithm.php: -------------------------------------------------------------------------------- 1 | add($method); 21 | } 22 | } 23 | 24 | /** 25 | * Returns true if the givn compression method is supported. 26 | */ 27 | public function has(string $name): bool 28 | { 29 | return array_key_exists($name, $this->compressionMethods); 30 | } 31 | 32 | /** 33 | * This method returns the compression method with the given name. Throws an exception if the method is not 34 | * supported. 35 | * 36 | * @param string $name The name of the compression method 37 | */ 38 | public function get(string $name): CompressionMethod 39 | { 40 | if (! $this->has($name)) { 41 | throw new InvalidArgumentException(sprintf('The compression method "%s" is not supported.', $name)); 42 | } 43 | 44 | return $this->compressionMethods[$name]; 45 | } 46 | 47 | /** 48 | * Returns the list of compression method names supported by the manager. 49 | * 50 | * @return string[] 51 | */ 52 | public function list(): array 53 | { 54 | return array_keys($this->compressionMethods); 55 | } 56 | 57 | /** 58 | * Add the given compression method to the manager. 59 | */ 60 | protected function add(CompressionMethod $compressionMethod): void 61 | { 62 | $name = $compressionMethod->name(); 63 | $this->compressionMethods[$name] = $compressionMethod; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Compression/CompressionMethodManagerFactory.php: -------------------------------------------------------------------------------- 1 | compressionMethods[$alias] = $compressionMethod; 23 | } 24 | 25 | /** 26 | * Returns the list of compression method aliases supported by the factory. 27 | * 28 | * @return string[] 29 | */ 30 | public function aliases(): array 31 | { 32 | return array_keys($this->compressionMethods); 33 | } 34 | 35 | /** 36 | * Returns all compression methods supported by this factory. 37 | * 38 | * @return CompressionMethod[] 39 | */ 40 | public function all(): array 41 | { 42 | return $this->compressionMethods; 43 | } 44 | 45 | /** 46 | * Creates a compression method manager using the compression methods identified by the given aliases. If one of the 47 | * aliases does not exist, an exception is thrown. 48 | * 49 | * @param string[] $aliases 50 | */ 51 | public function create(array $aliases): CompressionMethodManager 52 | { 53 | $compressionMethods = []; 54 | foreach ($aliases as $alias) { 55 | if (! isset($this->compressionMethods[$alias])) { 56 | throw new InvalidArgumentException(sprintf( 57 | 'The compression method with the alias "%s" is not supported.', 58 | $alias 59 | )); 60 | } 61 | $compressionMethods[] = $this->compressionMethods[$alias]; 62 | } 63 | 64 | return new CompressionMethodManager($compressionMethods); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Compression/Deflate.php: -------------------------------------------------------------------------------- 1 | 9) { 18 | throw new InvalidArgumentException( 19 | 'The compression level can be given as 0 for no compression up to 9 for maximum compression. If -1 given, the default compression level will be the default compression level of the zlib library.' 20 | ); 21 | } 22 | $this->compressionLevel = $compressionLevel; 23 | } 24 | 25 | public function name(): string 26 | { 27 | return 'DEF'; 28 | } 29 | 30 | public function compress(string $data): string 31 | { 32 | try { 33 | $bin = gzdeflate($data, $this->getCompressionLevel()); 34 | if (! is_string($bin)) { 35 | throw new InvalidArgumentException('Unable to encode the data'); 36 | } 37 | 38 | return $bin; 39 | } catch (Throwable $throwable) { 40 | throw new InvalidArgumentException('Unable to compress data.', $throwable->getCode(), $throwable); 41 | } 42 | } 43 | 44 | public function uncompress(string $data): string 45 | { 46 | try { 47 | $bin = gzinflate($data); 48 | if (! is_string($bin)) { 49 | throw new InvalidArgumentException('Unable to encode the data'); 50 | } 51 | 52 | return $bin; 53 | } catch (Throwable $throwable) { 54 | throw new InvalidArgumentException('Unable to uncompress data.', $throwable->getCode(), $throwable); 55 | } 56 | } 57 | 58 | private function getCompressionLevel(): int 59 | { 60 | return $this->compressionLevel; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /JWE.php: -------------------------------------------------------------------------------- 1 | payload; 31 | } 32 | 33 | /** 34 | * Set the payload. This method is immutable and a new object will be returned. 35 | */ 36 | public function withPayload(string $payload): self 37 | { 38 | $clone = clone $this; 39 | $clone->payload = $payload; 40 | 41 | return $clone; 42 | } 43 | 44 | /** 45 | * Returns the number of recipients associated with the JWS. 46 | */ 47 | public function countRecipients(): int 48 | { 49 | return count($this->recipients); 50 | } 51 | 52 | /** 53 | * Returns true is the JWE has already been encrypted. 54 | */ 55 | public function isEncrypted(): bool 56 | { 57 | return $this->getCiphertext() !== null; 58 | } 59 | 60 | /** 61 | * Returns the recipients associated with the JWS. 62 | * 63 | * @return Recipient[] 64 | */ 65 | public function getRecipients(): array 66 | { 67 | return $this->recipients; 68 | } 69 | 70 | /** 71 | * Returns the recipient object at the given index. 72 | */ 73 | public function getRecipient(int $id): Recipient 74 | { 75 | if (! isset($this->recipients[$id])) { 76 | throw new InvalidArgumentException('The recipient does not exist.'); 77 | } 78 | 79 | return $this->recipients[$id]; 80 | } 81 | 82 | /** 83 | * Returns the ciphertext. This method will return null is the JWE has not yet been encrypted. 84 | * 85 | * @return string|null The cyphertext 86 | */ 87 | public function getCiphertext(): ?string 88 | { 89 | return $this->ciphertext; 90 | } 91 | 92 | /** 93 | * Returns the Additional Authentication Data if available. 94 | */ 95 | public function getAAD(): ?string 96 | { 97 | return $this->aad; 98 | } 99 | 100 | /** 101 | * Returns the Initialization Vector if available. 102 | */ 103 | public function getIV(): ?string 104 | { 105 | return $this->iv; 106 | } 107 | 108 | /** 109 | * Returns the tag if available. 110 | */ 111 | public function getTag(): ?string 112 | { 113 | return $this->tag; 114 | } 115 | 116 | /** 117 | * Returns the encoded shared protected header. 118 | */ 119 | public function getEncodedSharedProtectedHeader(): string 120 | { 121 | return $this->encodedSharedProtectedHeader ?? ''; 122 | } 123 | 124 | /** 125 | * Returns the shared protected header. 126 | */ 127 | public function getSharedProtectedHeader(): array 128 | { 129 | return $this->sharedProtectedHeader; 130 | } 131 | 132 | /** 133 | * Returns the shared protected header parameter identified by the given key. Throws an exception is the the 134 | * parameter is not available. 135 | * 136 | * @param string $key The key 137 | * 138 | * @return mixed|null 139 | */ 140 | public function getSharedProtectedHeaderParameter(string $key) 141 | { 142 | if (! $this->hasSharedProtectedHeaderParameter($key)) { 143 | throw new InvalidArgumentException(sprintf('The shared protected header "%s" does not exist.', $key)); 144 | } 145 | 146 | return $this->sharedProtectedHeader[$key]; 147 | } 148 | 149 | /** 150 | * Returns true if the shared protected header has the parameter identified by the given key. 151 | * 152 | * @param string $key The key 153 | */ 154 | public function hasSharedProtectedHeaderParameter(string $key): bool 155 | { 156 | return array_key_exists($key, $this->sharedProtectedHeader); 157 | } 158 | 159 | /** 160 | * Returns the shared header. 161 | */ 162 | public function getSharedHeader(): array 163 | { 164 | return $this->sharedHeader; 165 | } 166 | 167 | /** 168 | * Returns the shared header parameter identified by the given key. Throws an exception is the the parameter is not 169 | * available. 170 | * 171 | * @param string $key The key 172 | * 173 | * @return mixed|null 174 | */ 175 | public function getSharedHeaderParameter(string $key) 176 | { 177 | if (! $this->hasSharedHeaderParameter($key)) { 178 | throw new InvalidArgumentException(sprintf('The shared header "%s" does not exist.', $key)); 179 | } 180 | 181 | return $this->sharedHeader[$key]; 182 | } 183 | 184 | /** 185 | * Returns true if the shared header has the parameter identified by the given key. 186 | * 187 | * @param string $key The key 188 | */ 189 | public function hasSharedHeaderParameter(string $key): bool 190 | { 191 | return array_key_exists($key, $this->sharedHeader); 192 | } 193 | 194 | /** 195 | * This method splits the JWE into a list of JWEs. It is only useful when the JWE contains more than one recipient 196 | * (JSON General Serialization). 197 | * 198 | * @return JWE[] 199 | */ 200 | public function split(): array 201 | { 202 | $result = []; 203 | foreach ($this->recipients as $recipient) { 204 | $result[] = new self( 205 | $this->ciphertext, 206 | $this->iv, 207 | $this->tag, 208 | $this->aad, 209 | $this->sharedHeader, 210 | $this->sharedProtectedHeader, 211 | $this->encodedSharedProtectedHeader, 212 | [$recipient] 213 | ); 214 | } 215 | 216 | return $result; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /JWEBuilder.php: -------------------------------------------------------------------------------- 1 | payload = null; 59 | $this->aad = null; 60 | $this->recipients = []; 61 | $this->sharedProtectedHeader = []; 62 | $this->sharedHeader = []; 63 | $this->compressionMethod = null; 64 | $this->keyManagementMode = null; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Returns the key encryption algorithm manager. 71 | */ 72 | public function getKeyEncryptionAlgorithmManager(): AlgorithmManager 73 | { 74 | return $this->keyEncryptionAlgorithmManager; 75 | } 76 | 77 | /** 78 | * Returns the content encryption algorithm manager. 79 | */ 80 | public function getContentEncryptionAlgorithmManager(): AlgorithmManager 81 | { 82 | return $this->contentEncryptionAlgorithmManager; 83 | } 84 | 85 | /** 86 | * Returns the compression method manager. 87 | */ 88 | public function getCompressionMethodManager(): CompressionMethodManager 89 | { 90 | return $this->compressionManager; 91 | } 92 | 93 | /** 94 | * Set the payload of the JWE to build. 95 | */ 96 | public function withPayload(string $payload): self 97 | { 98 | if (mb_detect_encoding($payload, 'UTF-8', true) !== 'UTF-8') { 99 | throw new InvalidArgumentException('The payload must be encoded in UTF-8'); 100 | } 101 | $clone = clone $this; 102 | $clone->payload = $payload; 103 | 104 | return $clone; 105 | } 106 | 107 | /** 108 | * Set the Additional Authenticated Data of the JWE to build. 109 | */ 110 | public function withAAD(?string $aad): self 111 | { 112 | $clone = clone $this; 113 | $clone->aad = $aad; 114 | 115 | return $clone; 116 | } 117 | 118 | /** 119 | * Set the shared protected header of the JWE to build. 120 | */ 121 | public function withSharedProtectedHeader(array $sharedProtectedHeader): self 122 | { 123 | $this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $this->sharedHeader); 124 | foreach ($this->recipients as $recipient) { 125 | $this->checkDuplicatedHeaderParameters($sharedProtectedHeader, $recipient->getHeader()); 126 | } 127 | $clone = clone $this; 128 | $clone->sharedProtectedHeader = $sharedProtectedHeader; 129 | 130 | return $clone; 131 | } 132 | 133 | /** 134 | * Set the shared header of the JWE to build. 135 | */ 136 | public function withSharedHeader(array $sharedHeader): self 137 | { 138 | $this->checkDuplicatedHeaderParameters($this->sharedProtectedHeader, $sharedHeader); 139 | foreach ($this->recipients as $recipient) { 140 | $this->checkDuplicatedHeaderParameters($sharedHeader, $recipient->getHeader()); 141 | } 142 | $clone = clone $this; 143 | $clone->sharedHeader = $sharedHeader; 144 | 145 | return $clone; 146 | } 147 | 148 | /** 149 | * Adds a recipient to the JWE to build. 150 | */ 151 | public function addRecipient(JWK $recipientKey, array $recipientHeader = []): self 152 | { 153 | $this->checkDuplicatedHeaderParameters($this->sharedProtectedHeader, $recipientHeader); 154 | $this->checkDuplicatedHeaderParameters($this->sharedHeader, $recipientHeader); 155 | $clone = clone $this; 156 | $completeHeader = array_merge($clone->sharedHeader, $recipientHeader, $clone->sharedProtectedHeader); 157 | $clone->checkAndSetContentEncryptionAlgorithm($completeHeader); 158 | $keyEncryptionAlgorithm = $clone->getKeyEncryptionAlgorithm($completeHeader); 159 | if ($clone->keyManagementMode === null) { 160 | $clone->keyManagementMode = $keyEncryptionAlgorithm->getKeyManagementMode(); 161 | } else { 162 | if (! $clone->areKeyManagementModesCompatible( 163 | $clone->keyManagementMode, 164 | $keyEncryptionAlgorithm->getKeyManagementMode() 165 | )) { 166 | throw new InvalidArgumentException('Foreign key management mode forbidden.'); 167 | } 168 | } 169 | 170 | $compressionMethod = $clone->getCompressionMethod($completeHeader); 171 | if ($compressionMethod !== null) { 172 | if ($clone->compressionMethod === null) { 173 | $clone->compressionMethod = $compressionMethod; 174 | } elseif ($clone->compressionMethod->name() !== $compressionMethod->name()) { 175 | throw new InvalidArgumentException('Incompatible compression method.'); 176 | } 177 | } 178 | if ($compressionMethod === null && $clone->compressionMethod !== null) { 179 | throw new InvalidArgumentException('Inconsistent compression method.'); 180 | } 181 | $clone->checkKey($keyEncryptionAlgorithm, $recipientKey); 182 | $clone->recipients[] = [ 183 | 'key' => $recipientKey, 184 | 'header' => $recipientHeader, 185 | 'key_encryption_algorithm' => $keyEncryptionAlgorithm, 186 | ]; 187 | 188 | return $clone; 189 | } 190 | 191 | /** 192 | * Builds the JWE. 193 | */ 194 | public function build(): JWE 195 | { 196 | if ($this->payload === null) { 197 | throw new LogicException('Payload not set.'); 198 | } 199 | if (count($this->recipients) === 0) { 200 | throw new LogicException('No recipient.'); 201 | } 202 | 203 | $additionalHeader = []; 204 | $cek = $this->determineCEK($additionalHeader); 205 | 206 | $recipients = []; 207 | foreach ($this->recipients as $recipient) { 208 | $recipient = $this->processRecipient($recipient, $cek, $additionalHeader); 209 | $recipients[] = $recipient; 210 | } 211 | 212 | if ((is_countable($additionalHeader) ? count($additionalHeader) : 0) !== 0 && count($this->recipients) === 1) { 213 | $sharedProtectedHeader = array_merge($additionalHeader, $this->sharedProtectedHeader); 214 | } else { 215 | $sharedProtectedHeader = $this->sharedProtectedHeader; 216 | } 217 | $encodedSharedProtectedHeader = count($sharedProtectedHeader) === 0 ? '' : Base64UrlSafe::encodeUnpadded( 218 | JsonConverter::encode($sharedProtectedHeader) 219 | ); 220 | 221 | [$ciphertext, $iv, $tag] = $this->encryptJWE($cek, $encodedSharedProtectedHeader); 222 | 223 | return new JWE( 224 | $ciphertext, 225 | $iv, 226 | $tag, 227 | $this->aad, 228 | $this->sharedHeader, 229 | $sharedProtectedHeader, 230 | $encodedSharedProtectedHeader, 231 | $recipients 232 | ); 233 | } 234 | 235 | private function checkAndSetContentEncryptionAlgorithm(array $completeHeader): void 236 | { 237 | $contentEncryptionAlgorithm = $this->getContentEncryptionAlgorithm($completeHeader); 238 | if ($this->contentEncryptionAlgorithm === null) { 239 | $this->contentEncryptionAlgorithm = $contentEncryptionAlgorithm; 240 | } elseif ($contentEncryptionAlgorithm->name() !== $this->contentEncryptionAlgorithm->name()) { 241 | throw new InvalidArgumentException('Inconsistent content encryption algorithm'); 242 | } 243 | } 244 | 245 | private function processRecipient(array $recipient, string $cek, array &$additionalHeader): Recipient 246 | { 247 | $completeHeader = array_merge($this->sharedHeader, $recipient['header'], $this->sharedProtectedHeader); 248 | $keyEncryptionAlgorithm = $recipient['key_encryption_algorithm']; 249 | if (! $keyEncryptionAlgorithm instanceof KeyEncryptionAlgorithm) { 250 | throw new InvalidArgumentException('The key encryption algorithm is not valid'); 251 | } 252 | $encryptedContentEncryptionKey = $this->getEncryptedKey( 253 | $completeHeader, 254 | $cek, 255 | $keyEncryptionAlgorithm, 256 | $additionalHeader, 257 | $recipient['key'], 258 | $recipient['sender_key'] ?? null 259 | ); 260 | $recipientHeader = $recipient['header']; 261 | if ((is_countable($additionalHeader) ? count($additionalHeader) : 0) !== 0 && count($this->recipients) !== 1) { 262 | $recipientHeader = array_merge($recipientHeader, $additionalHeader); 263 | $additionalHeader = []; 264 | } 265 | 266 | return new Recipient($recipientHeader, $encryptedContentEncryptionKey); 267 | } 268 | 269 | private function encryptJWE(string $cek, string $encodedSharedProtectedHeader): array 270 | { 271 | if (! $this->contentEncryptionAlgorithm instanceof ContentEncryptionAlgorithm) { 272 | throw new InvalidArgumentException('The content encryption algorithm is not valid'); 273 | } 274 | $iv_size = $this->contentEncryptionAlgorithm->getIVSize(); 275 | $iv = $this->createIV($iv_size); 276 | $payload = $this->preparePayload(); 277 | $tag = null; 278 | $ciphertext = $this->contentEncryptionAlgorithm->encryptContent( 279 | $payload ?? '', 280 | $cek, 281 | $iv, 282 | $this->aad, 283 | $encodedSharedProtectedHeader, 284 | $tag 285 | ); 286 | 287 | return [$ciphertext, $iv, $tag]; 288 | } 289 | 290 | /** 291 | * @return string 292 | */ 293 | private function preparePayload(): ?string 294 | { 295 | $prepared = $this->payload; 296 | if ($this->compressionMethod === null) { 297 | return $prepared; 298 | } 299 | 300 | return $this->compressionMethod->compress($prepared ?? ''); 301 | } 302 | 303 | private function getEncryptedKey( 304 | array $completeHeader, 305 | string $cek, 306 | KeyEncryptionAlgorithm $keyEncryptionAlgorithm, 307 | array &$additionalHeader, 308 | JWK $recipientKey, 309 | ?JWK $senderKey 310 | ): ?string { 311 | if ($keyEncryptionAlgorithm instanceof KeyEncryption) { 312 | return $this->getEncryptedKeyFromKeyEncryptionAlgorithm( 313 | $completeHeader, 314 | $cek, 315 | $keyEncryptionAlgorithm, 316 | $recipientKey, 317 | $additionalHeader 318 | ); 319 | } 320 | if ($keyEncryptionAlgorithm instanceof KeyWrapping) { 321 | return $this->getEncryptedKeyFromKeyWrappingAlgorithm( 322 | $completeHeader, 323 | $cek, 324 | $keyEncryptionAlgorithm, 325 | $recipientKey, 326 | $additionalHeader 327 | ); 328 | } 329 | if ($keyEncryptionAlgorithm instanceof KeyAgreementWithKeyWrapping) { 330 | return $this->getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm( 331 | $completeHeader, 332 | $cek, 333 | $keyEncryptionAlgorithm, 334 | $additionalHeader, 335 | $recipientKey, 336 | $senderKey 337 | ); 338 | } 339 | if ($keyEncryptionAlgorithm instanceof KeyAgreement) { 340 | return null; 341 | } 342 | if ($keyEncryptionAlgorithm instanceof DirectEncryption) { 343 | return null; 344 | } 345 | 346 | throw new InvalidArgumentException('Unsupported key encryption algorithm.'); 347 | } 348 | 349 | private function getEncryptedKeyFromKeyAgreementAndKeyWrappingAlgorithm( 350 | array $completeHeader, 351 | string $cek, 352 | KeyAgreementWithKeyWrapping $keyEncryptionAlgorithm, 353 | array &$additionalHeader, 354 | JWK $recipientKey, 355 | ?JWK $senderKey 356 | ): string { 357 | if ($this->contentEncryptionAlgorithm === null) { 358 | throw new InvalidArgumentException('Invalid content encryption algorithm'); 359 | } 360 | 361 | return $keyEncryptionAlgorithm->wrapAgreementKey( 362 | $recipientKey, 363 | $senderKey, 364 | $cek, 365 | $this->contentEncryptionAlgorithm->getCEKSize(), 366 | $completeHeader, 367 | $additionalHeader 368 | ); 369 | } 370 | 371 | private function getEncryptedKeyFromKeyEncryptionAlgorithm( 372 | array $completeHeader, 373 | string $cek, 374 | KeyEncryption $keyEncryptionAlgorithm, 375 | JWK $recipientKey, 376 | array &$additionalHeader 377 | ): string { 378 | return $keyEncryptionAlgorithm->encryptKey($recipientKey, $cek, $completeHeader, $additionalHeader); 379 | } 380 | 381 | private function getEncryptedKeyFromKeyWrappingAlgorithm( 382 | array $completeHeader, 383 | string $cek, 384 | KeyWrapping $keyEncryptionAlgorithm, 385 | JWK $recipientKey, 386 | array &$additionalHeader 387 | ): string { 388 | return $keyEncryptionAlgorithm->wrapKey($recipientKey, $cek, $completeHeader, $additionalHeader); 389 | } 390 | 391 | private function checkKey(KeyEncryptionAlgorithm $keyEncryptionAlgorithm, JWK $recipientKey): void 392 | { 393 | if ($this->contentEncryptionAlgorithm === null) { 394 | throw new InvalidArgumentException('Invalid content encryption algorithm'); 395 | } 396 | 397 | KeyChecker::checkKeyUsage($recipientKey, 'encryption'); 398 | if ($keyEncryptionAlgorithm->name() !== 'dir') { 399 | KeyChecker::checkKeyAlgorithm($recipientKey, $keyEncryptionAlgorithm->name()); 400 | } else { 401 | KeyChecker::checkKeyAlgorithm($recipientKey, $this->contentEncryptionAlgorithm->name()); 402 | } 403 | } 404 | 405 | private function determineCEK(array &$additionalHeader): string 406 | { 407 | if ($this->contentEncryptionAlgorithm === null) { 408 | throw new InvalidArgumentException('Invalid content encryption algorithm'); 409 | } 410 | 411 | switch ($this->keyManagementMode) { 412 | case KeyEncryption::MODE_ENCRYPT: 413 | case KeyEncryption::MODE_WRAP: 414 | return $this->createCEK($this->contentEncryptionAlgorithm->getCEKSize()); 415 | 416 | case KeyEncryption::MODE_AGREEMENT: 417 | if (count($this->recipients) !== 1) { 418 | throw new LogicException( 419 | 'Unable to encrypt for multiple recipients using key agreement algorithms.' 420 | ); 421 | } 422 | $recipientKey = $this->recipients[0]['key']; 423 | $senderKey = $this->recipients[0]['sender_key'] ?? null; 424 | $algorithm = $this->recipients[0]['key_encryption_algorithm']; 425 | if (! $algorithm instanceof KeyAgreement) { 426 | throw new InvalidArgumentException('Invalid content encryption algorithm'); 427 | } 428 | $completeHeader = array_merge( 429 | $this->sharedHeader, 430 | $this->recipients[0]['header'], 431 | $this->sharedProtectedHeader 432 | ); 433 | 434 | return $algorithm->getAgreementKey( 435 | $this->contentEncryptionAlgorithm->getCEKSize(), 436 | $this->contentEncryptionAlgorithm->name(), 437 | $recipientKey, 438 | $senderKey, 439 | $completeHeader, 440 | $additionalHeader 441 | ); 442 | 443 | case KeyEncryption::MODE_DIRECT: 444 | if (count($this->recipients) !== 1) { 445 | throw new LogicException( 446 | 'Unable to encrypt for multiple recipients using key agreement algorithms.' 447 | ); 448 | } 449 | /** @var JWK $key */ 450 | $key = $this->recipients[0]['key']; 451 | if ($key->get('kty') !== 'oct') { 452 | throw new RuntimeException('Wrong key type.'); 453 | } 454 | $k = $key->get('k'); 455 | if (! is_string($k)) { 456 | throw new RuntimeException('Invalid key.'); 457 | } 458 | 459 | return Base64UrlSafe::decode($k); 460 | 461 | default: 462 | throw new InvalidArgumentException(sprintf( 463 | 'Unsupported key management mode "%s".', 464 | $this->keyManagementMode 465 | )); 466 | } 467 | } 468 | 469 | private function getCompressionMethod(array $completeHeader): ?CompressionMethod 470 | { 471 | if (! array_key_exists('zip', $completeHeader)) { 472 | return null; 473 | } 474 | 475 | return $this->compressionManager->get($completeHeader['zip']); 476 | } 477 | 478 | private function areKeyManagementModesCompatible(string $current, string $new): bool 479 | { 480 | $agree = KeyEncryptionAlgorithm::MODE_AGREEMENT; 481 | $dir = KeyEncryptionAlgorithm::MODE_DIRECT; 482 | $enc = KeyEncryptionAlgorithm::MODE_ENCRYPT; 483 | $wrap = KeyEncryptionAlgorithm::MODE_WRAP; 484 | $supportedKeyManagementModeCombinations = [ 485 | $enc . $enc => true, 486 | $enc . $wrap => true, 487 | $wrap . $enc => true, 488 | $wrap . $wrap => true, 489 | $agree . $agree => false, 490 | $agree . $dir => false, 491 | $agree . $enc => false, 492 | $agree . $wrap => false, 493 | $dir . $agree => false, 494 | $dir . $dir => false, 495 | $dir . $enc => false, 496 | $dir . $wrap => false, 497 | $enc . $agree => false, 498 | $enc . $dir => false, 499 | $wrap . $agree => false, 500 | $wrap . $dir => false, 501 | ]; 502 | 503 | if (array_key_exists($current . $new, $supportedKeyManagementModeCombinations)) { 504 | return $supportedKeyManagementModeCombinations[$current . $new]; 505 | } 506 | 507 | return false; 508 | } 509 | 510 | private function createCEK(int $size): string 511 | { 512 | return random_bytes($size / 8); 513 | } 514 | 515 | private function createIV(int $size): string 516 | { 517 | return random_bytes($size / 8); 518 | } 519 | 520 | private function getKeyEncryptionAlgorithm(array $completeHeader): KeyEncryptionAlgorithm 521 | { 522 | if (! isset($completeHeader['alg'])) { 523 | throw new InvalidArgumentException('Parameter "alg" is missing.'); 524 | } 525 | $keyEncryptionAlgorithm = $this->keyEncryptionAlgorithmManager->get($completeHeader['alg']); 526 | if (! $keyEncryptionAlgorithm instanceof KeyEncryptionAlgorithm) { 527 | throw new InvalidArgumentException(sprintf( 528 | 'The key encryption algorithm "%s" is not supported or not a key encryption algorithm instance.', 529 | $completeHeader['alg'] 530 | )); 531 | } 532 | 533 | return $keyEncryptionAlgorithm; 534 | } 535 | 536 | private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm 537 | { 538 | if (! isset($completeHeader['enc'])) { 539 | throw new InvalidArgumentException('Parameter "enc" is missing.'); 540 | } 541 | $contentEncryptionAlgorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']); 542 | if (! $contentEncryptionAlgorithm instanceof ContentEncryptionAlgorithm) { 543 | throw new InvalidArgumentException(sprintf( 544 | 'The content encryption algorithm "%s" is not supported or not a content encryption algorithm instance.', 545 | $completeHeader['enc'] 546 | )); 547 | } 548 | 549 | return $contentEncryptionAlgorithm; 550 | } 551 | 552 | private function checkDuplicatedHeaderParameters(array $header1, array $header2): void 553 | { 554 | $inter = array_intersect_key($header1, $header2); 555 | if (count($inter) !== 0) { 556 | throw new InvalidArgumentException(sprintf( 557 | 'The header contains duplicated entries: %s.', 558 | implode(', ', array_keys($inter)) 559 | )); 560 | } 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /JWEBuilderFactory.php: -------------------------------------------------------------------------------- 1 | algorithmManagerFactory->create($keyEncryptionAlgorithms); 32 | $contentEncryptionAlgorithmManager = $this->algorithmManagerFactory->create($contentEncryptionAlgorithm); 33 | $compressionMethodManager = $this->compressionMethodManagerFactory->create($compressionMethods); 34 | 35 | return new JWEBuilder( 36 | $keyEncryptionAlgorithmManager, 37 | $contentEncryptionAlgorithmManager, 38 | $compressionMethodManager 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /JWEDecrypter.php: -------------------------------------------------------------------------------- 1 | keyEncryptionAlgorithmManager; 40 | } 41 | 42 | /** 43 | * Returns the content encryption algorithm manager. 44 | */ 45 | public function getContentEncryptionAlgorithmManager(): AlgorithmManager 46 | { 47 | return $this->contentEncryptionAlgorithmManager; 48 | } 49 | 50 | /** 51 | * Returns the compression method manager. 52 | */ 53 | public function getCompressionMethodManager(): CompressionMethodManager 54 | { 55 | return $this->compressionMethodManager; 56 | } 57 | 58 | /** 59 | * This method will try to decrypt the given JWE and recipient using a JWK. 60 | * 61 | * @param JWE $jwe A JWE object to decrypt 62 | * @param JWK $jwk The key used to decrypt the input 63 | * @param int $recipient The recipient used to decrypt the token 64 | */ 65 | public function decryptUsingKey(JWE &$jwe, JWK $jwk, int $recipient, ?JWK $senderKey = null): bool 66 | { 67 | $jwkset = new JWKSet([$jwk]); 68 | 69 | return $this->decryptUsingKeySet($jwe, $jwkset, $recipient, $senderKey); 70 | } 71 | 72 | /** 73 | * This method will try to decrypt the given JWE and recipient using a JWKSet. 74 | * 75 | * @param JWE $jwe A JWE object to decrypt 76 | * @param JWKSet $jwkset The key set used to decrypt the input 77 | * @param JWK $jwk The key used to decrypt the token in case of success 78 | * @param int $recipient The recipient used to decrypt the token in case of success 79 | */ 80 | public function decryptUsingKeySet( 81 | JWE &$jwe, 82 | JWKSet $jwkset, 83 | int $recipient, 84 | JWK &$jwk = null, 85 | ?JWK $senderKey = null 86 | ): bool { 87 | if ($jwkset->count() === 0) { 88 | throw new InvalidArgumentException('No key in the key set.'); 89 | } 90 | if ($jwe->getPayload() !== null) { 91 | return true; 92 | } 93 | if ($jwe->countRecipients() === 0) { 94 | throw new InvalidArgumentException('The JWE does not contain any recipient.'); 95 | } 96 | 97 | $plaintext = $this->decryptRecipientKey($jwe, $jwkset, $recipient, $jwk, $senderKey); 98 | if ($plaintext !== null) { 99 | $jwe = $jwe->withPayload($plaintext); 100 | 101 | return true; 102 | } 103 | 104 | return false; 105 | } 106 | 107 | private function decryptRecipientKey( 108 | JWE $jwe, 109 | JWKSet $jwkset, 110 | int $i, 111 | JWK &$successJwk = null, 112 | ?JWK $senderKey = null 113 | ): ?string { 114 | $recipient = $jwe->getRecipient($i); 115 | $completeHeader = array_merge( 116 | $jwe->getSharedProtectedHeader(), 117 | $jwe->getSharedHeader(), 118 | $recipient->getHeader() 119 | ); 120 | $this->checkCompleteHeader($completeHeader); 121 | 122 | $key_encryption_algorithm = $this->getKeyEncryptionAlgorithm($completeHeader); 123 | $content_encryption_algorithm = $this->getContentEncryptionAlgorithm($completeHeader); 124 | 125 | $this->checkIvSize($jwe->getIV(), $content_encryption_algorithm->getIVSize()); 126 | 127 | foreach ($jwkset as $recipientKey) { 128 | try { 129 | KeyChecker::checkKeyUsage($recipientKey, 'decryption'); 130 | if ($key_encryption_algorithm->name() !== 'dir') { 131 | KeyChecker::checkKeyAlgorithm($recipientKey, $key_encryption_algorithm->name()); 132 | } else { 133 | KeyChecker::checkKeyAlgorithm($recipientKey, $content_encryption_algorithm->name()); 134 | } 135 | $cek = $this->decryptCEK( 136 | $key_encryption_algorithm, 137 | $content_encryption_algorithm, 138 | $recipientKey, 139 | $senderKey, 140 | $recipient, 141 | $completeHeader 142 | ); 143 | $this->checkCekSize($cek, $key_encryption_algorithm, $content_encryption_algorithm); 144 | $payload = $this->decryptPayload($jwe, $cek, $content_encryption_algorithm, $completeHeader); 145 | $successJwk = $recipientKey; 146 | 147 | return $payload; 148 | } catch (Throwable) { 149 | //We do nothing, we continue with other keys 150 | continue; 151 | } 152 | } 153 | 154 | return null; 155 | } 156 | 157 | private function checkCekSize( 158 | string $cek, 159 | KeyEncryptionAlgorithm $keyEncryptionAlgorithm, 160 | ContentEncryptionAlgorithm $algorithm 161 | ): void { 162 | if ($keyEncryptionAlgorithm instanceof DirectEncryption || $keyEncryptionAlgorithm instanceof KeyAgreement) { 163 | return; 164 | } 165 | if (mb_strlen($cek, '8bit') * 8 !== $algorithm->getCEKSize()) { 166 | throw new InvalidArgumentException('Invalid CEK size'); 167 | } 168 | } 169 | 170 | private function checkIvSize(?string $iv, int $requiredIvSize): void 171 | { 172 | if ($iv === null && $requiredIvSize !== 0) { 173 | throw new InvalidArgumentException('Invalid IV size'); 174 | } 175 | if (is_string($iv) && mb_strlen($iv, '8bit') !== $requiredIvSize / 8) { 176 | throw new InvalidArgumentException('Invalid IV size'); 177 | } 178 | } 179 | 180 | private function decryptCEK( 181 | Algorithm $key_encryption_algorithm, 182 | ContentEncryptionAlgorithm $content_encryption_algorithm, 183 | JWK $recipientKey, 184 | ?JWK $senderKey, 185 | Recipient $recipient, 186 | array $completeHeader 187 | ): string { 188 | if ($key_encryption_algorithm instanceof DirectEncryption) { 189 | return $key_encryption_algorithm->getCEK($recipientKey); 190 | } 191 | if ($key_encryption_algorithm instanceof KeyAgreement) { 192 | return $key_encryption_algorithm->getAgreementKey( 193 | $content_encryption_algorithm->getCEKSize(), 194 | $content_encryption_algorithm->name(), 195 | $recipientKey, 196 | $senderKey, 197 | $completeHeader 198 | ); 199 | } 200 | if ($key_encryption_algorithm instanceof KeyAgreementWithKeyWrapping) { 201 | return $key_encryption_algorithm->unwrapAgreementKey( 202 | $recipientKey, 203 | $senderKey, 204 | $recipient->getEncryptedKey() ?? '', 205 | $content_encryption_algorithm->getCEKSize(), 206 | $completeHeader 207 | ); 208 | } 209 | if ($key_encryption_algorithm instanceof KeyEncryption) { 210 | return $key_encryption_algorithm->decryptKey( 211 | $recipientKey, 212 | $recipient->getEncryptedKey() ?? '', 213 | $completeHeader 214 | ); 215 | } 216 | if ($key_encryption_algorithm instanceof KeyWrapping) { 217 | return $key_encryption_algorithm->unwrapKey( 218 | $recipientKey, 219 | $recipient->getEncryptedKey() ?? '', 220 | $completeHeader 221 | ); 222 | } 223 | 224 | throw new InvalidArgumentException('Unsupported CEK generation'); 225 | } 226 | 227 | private function decryptPayload( 228 | JWE $jwe, 229 | string $cek, 230 | ContentEncryptionAlgorithm $content_encryption_algorithm, 231 | array $completeHeader 232 | ): string { 233 | $payload = $content_encryption_algorithm->decryptContent( 234 | $jwe->getCiphertext() ?? '', 235 | $cek, 236 | $jwe->getIV() ?? '', 237 | $jwe->getAAD(), 238 | $jwe->getEncodedSharedProtectedHeader(), 239 | $jwe->getTag() ?? '' 240 | ); 241 | 242 | return $this->decompressIfNeeded($payload, $completeHeader); 243 | } 244 | 245 | private function decompressIfNeeded(string $payload, array $completeHeaders): string 246 | { 247 | if (array_key_exists('zip', $completeHeaders)) { 248 | $compression_method = $this->compressionMethodManager->get($completeHeaders['zip']); 249 | $payload = $compression_method->uncompress($payload); 250 | } 251 | 252 | return $payload; 253 | } 254 | 255 | private function checkCompleteHeader(array $completeHeaders): void 256 | { 257 | foreach (['enc', 'alg'] as $key) { 258 | if (! isset($completeHeaders[$key])) { 259 | throw new InvalidArgumentException(sprintf("Parameter '%s' is missing.", $key)); 260 | } 261 | } 262 | } 263 | 264 | private function getKeyEncryptionAlgorithm(array $completeHeaders): KeyEncryptionAlgorithm 265 | { 266 | $key_encryption_algorithm = $this->keyEncryptionAlgorithmManager->get($completeHeaders['alg']); 267 | if (! $key_encryption_algorithm instanceof KeyEncryptionAlgorithm) { 268 | throw new InvalidArgumentException(sprintf( 269 | 'The key encryption algorithm "%s" is not supported or does not implement KeyEncryptionAlgorithm interface.', 270 | $completeHeaders['alg'] 271 | )); 272 | } 273 | 274 | return $key_encryption_algorithm; 275 | } 276 | 277 | private function getContentEncryptionAlgorithm(array $completeHeader): ContentEncryptionAlgorithm 278 | { 279 | $content_encryption_algorithm = $this->contentEncryptionAlgorithmManager->get($completeHeader['enc']); 280 | if (! $content_encryption_algorithm instanceof ContentEncryptionAlgorithm) { 281 | throw new InvalidArgumentException(sprintf( 282 | 'The key encryption algorithm "%s" is not supported or does not implement the ContentEncryption interface.', 283 | $completeHeader['enc'] 284 | )); 285 | } 286 | 287 | return $content_encryption_algorithm; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /JWEDecrypterFactory.php: -------------------------------------------------------------------------------- 1 | algorithmManagerFactory->create($keyEncryptionAlgorithms); 32 | $contentEncryptionAlgorithmManager = $this->algorithmManagerFactory->create($contentEncryptionAlgorithms); 33 | $compressionMethodManager = $this->compressionMethodManagerFactory->create($compressionMethods); 34 | 35 | return new JWEDecrypter( 36 | $keyEncryptionAlgorithmManager, 37 | $contentEncryptionAlgorithmManager, 38 | $compressionMethodManager 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /JWELoader.php: -------------------------------------------------------------------------------- 1 | jweDecrypter; 29 | } 30 | 31 | /** 32 | * Returns the header checker manager if set. 33 | */ 34 | public function getHeaderCheckerManager(): ?HeaderCheckerManager 35 | { 36 | return $this->headerCheckerManager; 37 | } 38 | 39 | /** 40 | * Returns the serializer manager. 41 | */ 42 | public function getSerializerManager(): JWESerializerManager 43 | { 44 | return $this->serializerManager; 45 | } 46 | 47 | /** 48 | * This method will try to load and decrypt the given token using a JWK. If succeeded, the methods will populate the 49 | * $recipient variable and returns the JWE. 50 | */ 51 | public function loadAndDecryptWithKey(string $token, JWK $key, ?int &$recipient): JWE 52 | { 53 | $keyset = new JWKSet([$key]); 54 | 55 | return $this->loadAndDecryptWithKeySet($token, $keyset, $recipient); 56 | } 57 | 58 | /** 59 | * This method will try to load and decrypt the given token using a JWKSet. If succeeded, the methods will populate 60 | * the $recipient variable and returns the JWE. 61 | */ 62 | public function loadAndDecryptWithKeySet(string $token, JWKSet $keyset, ?int &$recipient): JWE 63 | { 64 | try { 65 | $jwe = $this->serializerManager->unserialize($token); 66 | $nbRecipients = $jwe->countRecipients(); 67 | for ($i = 0; $i < $nbRecipients; ++$i) { 68 | if ($this->processRecipient($jwe, $keyset, $i)) { 69 | $recipient = $i; 70 | 71 | return $jwe; 72 | } 73 | } 74 | } catch (Throwable) { 75 | // Nothing to do. Exception thrown just after 76 | } 77 | 78 | throw new RuntimeException('Unable to load and decrypt the token.'); 79 | } 80 | 81 | private function processRecipient(JWE &$jwe, JWKSet $keyset, int $recipient): bool 82 | { 83 | try { 84 | if ($this->headerCheckerManager !== null) { 85 | $this->headerCheckerManager->check($jwe, $recipient); 86 | } 87 | 88 | return $this->jweDecrypter->decryptUsingKeySet($jwe, $keyset, $recipient); 89 | } catch (Throwable) { 90 | return false; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /JWELoaderFactory.php: -------------------------------------------------------------------------------- 1 | jweSerializerManagerFactory->create($serializers); 31 | $jweDecrypter = $this->jweDecrypterFactory->create( 32 | $keyEncryptionAlgorithms, 33 | $contentEncryptionAlgorithms, 34 | $compressionMethods 35 | ); 36 | if ($this->headerCheckerManagerFactory !== null) { 37 | $headerCheckerManager = $this->headerCheckerManagerFactory->create($headerCheckers); 38 | } else { 39 | $headerCheckerManager = null; 40 | } 41 | 42 | return new JWELoader($serializerManager, $jweDecrypter, $headerCheckerManager); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /JWETokenSupport.php: -------------------------------------------------------------------------------- 1 | getSharedProtectedHeader(); 23 | $unprotectedHeader = $jwt->getSharedHeader(); 24 | $recipient = $jwt->getRecipient($index) 25 | ->getHeader() 26 | ; 27 | 28 | $unprotectedHeader = array_merge($unprotectedHeader, $recipient); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2019 Spomky-Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP JWT Encryption Component 2 | ============================ 3 | 4 | This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY. 5 | 6 | **Please do not submit any Pull Request here.** 7 | You should go to [the main repository](https://github.com/web-token/jwt-framework) instead. 8 | 9 | # Documentation 10 | 11 | The official documentation is available as https://web-token.spomky-labs.com/ 12 | 13 | # Licence 14 | 15 | This software is release under [MIT licence](LICENSE). 16 | -------------------------------------------------------------------------------- /Recipient.php: -------------------------------------------------------------------------------- 1 | header; 27 | } 28 | 29 | /** 30 | * Returns the value of the recipient header parameter with the specified key. 31 | * 32 | * @param string $key The key 33 | * 34 | * @return mixed|null 35 | */ 36 | public function getHeaderParameter(string $key) 37 | { 38 | if (! $this->hasHeaderParameter($key)) { 39 | throw new InvalidArgumentException(sprintf('The header "%s" does not exist.', $key)); 40 | } 41 | 42 | return $this->header[$key]; 43 | } 44 | 45 | /** 46 | * Returns true if the recipient header contains the parameter with the specified key. 47 | * 48 | * @param string $key The key 49 | */ 50 | public function hasHeaderParameter(string $key): bool 51 | { 52 | return array_key_exists($key, $this->header); 53 | } 54 | 55 | /** 56 | * Returns the encrypted key. 57 | */ 58 | public function getEncryptedKey(): ?string 59 | { 60 | return $this->encryptedKey; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Serializer/CompactSerializer.php: -------------------------------------------------------------------------------- 1 | getRecipient($recipientIndex); 37 | 38 | $this->checkHasNoAAD($jwe); 39 | $this->checkHasSharedProtectedHeader($jwe); 40 | $this->checkRecipientHasNoHeader($jwe, $recipientIndex); 41 | 42 | return sprintf( 43 | '%s.%s.%s.%s.%s', 44 | $jwe->getEncodedSharedProtectedHeader(), 45 | Base64UrlSafe::encodeUnpadded($recipient->getEncryptedKey() ?? ''), 46 | Base64UrlSafe::encodeUnpadded($jwe->getIV() ?? ''), 47 | Base64UrlSafe::encodeUnpadded($jwe->getCiphertext() ?? ''), 48 | Base64UrlSafe::encodeUnpadded($jwe->getTag() ?? '') 49 | ); 50 | } 51 | 52 | public function unserialize(string $input): JWE 53 | { 54 | $parts = explode('.', $input); 55 | if (count($parts) !== 5) { 56 | throw new InvalidArgumentException('Unsupported input'); 57 | } 58 | 59 | try { 60 | $encodedSharedProtectedHeader = $parts[0]; 61 | $sharedProtectedHeader = JsonConverter::decode(Base64UrlSafe::decode($encodedSharedProtectedHeader)); 62 | if (! is_array($sharedProtectedHeader)) { 63 | throw new InvalidArgumentException('Unsupported input.'); 64 | } 65 | $encryptedKey = $parts[1] === '' ? null : Base64UrlSafe::decode($parts[1]); 66 | $iv = Base64UrlSafe::decode($parts[2]); 67 | $ciphertext = Base64UrlSafe::decode($parts[3]); 68 | $tag = Base64UrlSafe::decode($parts[4]); 69 | 70 | return new JWE( 71 | $ciphertext, 72 | $iv, 73 | $tag, 74 | null, 75 | [], 76 | $sharedProtectedHeader, 77 | $encodedSharedProtectedHeader, 78 | [new Recipient([], $encryptedKey)] 79 | ); 80 | } catch (Throwable $throwable) { 81 | throw new InvalidArgumentException('Unsupported input', $throwable->getCode(), $throwable); 82 | } 83 | } 84 | 85 | private function checkHasNoAAD(JWE $jwe): void 86 | { 87 | if ($jwe->getAAD() !== null) { 88 | throw new LogicException('This JWE has AAD and cannot be converted into Compact JSON.'); 89 | } 90 | } 91 | 92 | private function checkRecipientHasNoHeader(JWE $jwe, int $id): void 93 | { 94 | if (count($jwe->getSharedHeader()) !== 0 || count($jwe->getRecipient($id)->getHeader()) !== 0) { 95 | throw new LogicException( 96 | 'This JWE has shared header parameters or recipient header parameters and cannot be converted into Compact JSON.' 97 | ); 98 | } 99 | } 100 | 101 | private function checkHasSharedProtectedHeader(JWE $jwe): void 102 | { 103 | if (count($jwe->getSharedProtectedHeader()) === 0) { 104 | throw new LogicException( 105 | 'This JWE does not have shared protected header parameters and cannot be converted into Compact JSON.' 106 | ); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Serializer/JSONFlattenedSerializer.php: -------------------------------------------------------------------------------- 1 | getRecipient($recipientIndex); 36 | $data = [ 37 | 'ciphertext' => Base64UrlSafe::encodeUnpadded($jwe->getCiphertext() ?? ''), 38 | 'iv' => Base64UrlSafe::encodeUnpadded($jwe->getIV() ?? ''), 39 | 'tag' => Base64UrlSafe::encodeUnpadded($jwe->getTag() ?? ''), 40 | ]; 41 | if ($jwe->getAAD() !== null) { 42 | $data['aad'] = Base64UrlSafe::encodeUnpadded($jwe->getAAD()); 43 | } 44 | if (count($jwe->getSharedProtectedHeader()) !== 0) { 45 | $data['protected'] = $jwe->getEncodedSharedProtectedHeader(); 46 | } 47 | if (count($jwe->getSharedHeader()) !== 0) { 48 | $data['unprotected'] = $jwe->getSharedHeader(); 49 | } 50 | if (count($recipient->getHeader()) !== 0) { 51 | $data['header'] = $recipient->getHeader(); 52 | } 53 | if ($recipient->getEncryptedKey() !== null) { 54 | $data['encrypted_key'] = Base64UrlSafe::encodeUnpadded($recipient->getEncryptedKey()); 55 | } 56 | 57 | return JsonConverter::encode($data); 58 | } 59 | 60 | public function unserialize(string $input): JWE 61 | { 62 | $data = JsonConverter::decode($input); 63 | if (! is_array($data)) { 64 | throw new InvalidArgumentException('Unsupported input.'); 65 | } 66 | $this->checkData($data); 67 | 68 | $ciphertext = Base64UrlSafe::decode($data['ciphertext']); 69 | $iv = Base64UrlSafe::decode($data['iv']); 70 | $tag = Base64UrlSafe::decode($data['tag']); 71 | $aad = array_key_exists('aad', $data) ? Base64UrlSafe::decode($data['aad']) : null; 72 | [$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader] = $this->processHeaders($data); 73 | $encryptedKey = array_key_exists('encrypted_key', $data) ? Base64UrlSafe::decode($data['encrypted_key']) : null; 74 | $header = array_key_exists('header', $data) ? $data['header'] : []; 75 | 76 | return new JWE( 77 | $ciphertext, 78 | $iv, 79 | $tag, 80 | $aad, 81 | $sharedHeader, 82 | $sharedProtectedHeader, 83 | $encodedSharedProtectedHeader, 84 | [new Recipient($header, $encryptedKey)] 85 | ); 86 | } 87 | 88 | private function checkData(?array $data): void 89 | { 90 | if ($data === null || ! isset($data['ciphertext']) || isset($data['recipients'])) { 91 | throw new InvalidArgumentException('Unsupported input.'); 92 | } 93 | } 94 | 95 | private function processHeaders(array $data): array 96 | { 97 | $encodedSharedProtectedHeader = array_key_exists('protected', $data) ? $data['protected'] : null; 98 | $sharedProtectedHeader = $encodedSharedProtectedHeader ? JsonConverter::decode( 99 | Base64UrlSafe::decode($encodedSharedProtectedHeader) 100 | ) : []; 101 | $sharedHeader = $data['unprotected'] ?? []; 102 | 103 | return [$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Serializer/JSONGeneralSerializer.php: -------------------------------------------------------------------------------- 1 | countRecipients() === 0) { 34 | throw new LogicException('No recipient.'); 35 | } 36 | 37 | $data = [ 38 | 'ciphertext' => Base64UrlSafe::encodeUnpadded($jwe->getCiphertext() ?? ''), 39 | 'iv' => Base64UrlSafe::encodeUnpadded($jwe->getIV() ?? ''), 40 | 'tag' => Base64UrlSafe::encodeUnpadded($jwe->getTag() ?? ''), 41 | ]; 42 | if ($jwe->getAAD() !== null) { 43 | $data['aad'] = Base64UrlSafe::encodeUnpadded($jwe->getAAD()); 44 | } 45 | if (count($jwe->getSharedProtectedHeader()) !== 0) { 46 | $data['protected'] = $jwe->getEncodedSharedProtectedHeader(); 47 | } 48 | if (count($jwe->getSharedHeader()) !== 0) { 49 | $data['unprotected'] = $jwe->getSharedHeader(); 50 | } 51 | $data['recipients'] = []; 52 | foreach ($jwe->getRecipients() as $recipient) { 53 | $temp = []; 54 | if (count($recipient->getHeader()) !== 0) { 55 | $temp['header'] = $recipient->getHeader(); 56 | } 57 | if ($recipient->getEncryptedKey() !== null) { 58 | $temp['encrypted_key'] = Base64UrlSafe::encodeUnpadded($recipient->getEncryptedKey()); 59 | } 60 | $data['recipients'][] = $temp; 61 | } 62 | 63 | return JsonConverter::encode($data); 64 | } 65 | 66 | public function unserialize(string $input): JWE 67 | { 68 | $data = JsonConverter::decode($input); 69 | if (! is_array($data)) { 70 | throw new InvalidArgumentException('Unsupported input.'); 71 | } 72 | $this->checkData($data); 73 | 74 | $ciphertext = Base64UrlSafe::decode($data['ciphertext']); 75 | $iv = Base64UrlSafe::decode($data['iv']); 76 | $tag = Base64UrlSafe::decode($data['tag']); 77 | $aad = array_key_exists('aad', $data) ? Base64UrlSafe::decode($data['aad']) : null; 78 | [$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader] = $this->processHeaders($data); 79 | $recipients = []; 80 | foreach ($data['recipients'] as $recipient) { 81 | [$encryptedKey, $header] = $this->processRecipient($recipient); 82 | $recipients[] = new Recipient($header, $encryptedKey); 83 | } 84 | 85 | return new JWE( 86 | $ciphertext, 87 | $iv, 88 | $tag, 89 | $aad, 90 | $sharedHeader, 91 | $sharedProtectedHeader, 92 | $encodedSharedProtectedHeader, 93 | $recipients 94 | ); 95 | } 96 | 97 | private function checkData(?array $data): void 98 | { 99 | if ($data === null || ! isset($data['ciphertext']) || ! isset($data['recipients'])) { 100 | throw new InvalidArgumentException('Unsupported input.'); 101 | } 102 | } 103 | 104 | private function processRecipient(array $recipient): array 105 | { 106 | $encryptedKey = array_key_exists('encrypted_key', $recipient) ? Base64UrlSafe::decode( 107 | $recipient['encrypted_key'] 108 | ) : null; 109 | $header = array_key_exists('header', $recipient) ? $recipient['header'] : []; 110 | 111 | return [$encryptedKey, $header]; 112 | } 113 | 114 | private function processHeaders(array $data): array 115 | { 116 | $encodedSharedProtectedHeader = array_key_exists('protected', $data) ? $data['protected'] : null; 117 | $sharedProtectedHeader = $encodedSharedProtectedHeader ? JsonConverter::decode( 118 | Base64UrlSafe::decode($encodedSharedProtectedHeader) 119 | ) : []; 120 | $sharedHeader = array_key_exists('unprotected', $data) ? $data['unprotected'] : []; 121 | 122 | return [$encodedSharedProtectedHeader, $sharedProtectedHeader, $sharedHeader]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Serializer/JWESerializer.php: -------------------------------------------------------------------------------- 1 | add($serializer); 24 | } 25 | } 26 | 27 | /** 28 | * Return the serializer names supported by the manager. 29 | * 30 | * @return string[] 31 | */ 32 | public function names(): array 33 | { 34 | return array_keys($this->serializers); 35 | } 36 | 37 | /** 38 | * Converts a JWE into a string. Throws an exception if none of the serializer was able to convert the input. 39 | */ 40 | public function serialize(string $name, JWE $jws, ?int $recipientIndex = null): string 41 | { 42 | if (! isset($this->serializers[$name])) { 43 | throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name)); 44 | } 45 | 46 | return $this->serializers[$name]->serialize($jws, $recipientIndex); 47 | } 48 | 49 | /** 50 | * Loads data and return a JWE object. Throws an exception if none of the serializer was able to convert the input. 51 | * 52 | * @param string $input A string that represents a JWE 53 | * @param string|null $name the name of the serializer if the input is unserialized 54 | */ 55 | public function unserialize(string $input, ?string &$name = null): JWE 56 | { 57 | foreach ($this->serializers as $serializer) { 58 | try { 59 | $jws = $serializer->unserialize($input); 60 | $name = $serializer->name(); 61 | 62 | return $jws; 63 | } catch (InvalidArgumentException) { 64 | continue; 65 | } 66 | } 67 | 68 | throw new InvalidArgumentException('Unsupported input.'); 69 | } 70 | 71 | /** 72 | * Adds a serializer to the manager. 73 | */ 74 | private function add(JWESerializer $serializer): void 75 | { 76 | $this->serializers[$serializer->name()] = $serializer; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Serializer/JWESerializerManagerFactory.php: -------------------------------------------------------------------------------- 1 | serializers[$name])) { 26 | throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name)); 27 | } 28 | $serializers[] = $this->serializers[$name]; 29 | } 30 | 31 | return new JWESerializerManager($serializers); 32 | } 33 | 34 | /** 35 | * Return the serializer names supported by the manager. 36 | * 37 | * @return string[] 38 | */ 39 | public function names(): array 40 | { 41 | return array_keys($this->serializers); 42 | } 43 | 44 | /** 45 | * Returns all serializers supported by this factory. 46 | * 47 | * @return JWESerializer[] 48 | */ 49 | public function all(): array 50 | { 51 | return $this->serializers; 52 | } 53 | 54 | /** 55 | * Adds a serializer to the manager. 56 | */ 57 | public function add(JWESerializer $serializer): void 58 | { 59 | $this->serializers[$serializer->name()] = $serializer; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-token/jwt-encryption", 3 | "description": "Encryption component of the JWT Framework.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"], 7 | "homepage": "https://github.com/web-token", 8 | "authors": [ 9 | { 10 | "name": "Florent Morselli", 11 | "homepage": "https://github.com/Spomky" 12 | },{ 13 | "name": "All contributors", 14 | "homepage": "https://github.com/web-token/jwt-encryption/contributors" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "Jose\\Component\\Encryption\\": "" 20 | } 21 | }, 22 | "require": { 23 | "php": ">=8.1", 24 | "web-token/jwt-core": "^3.0" 25 | }, 26 | "suggest": { 27 | "web-token/jwt-encryption-algorithm-aescbc": "AES CBC Based Content Encryption Algorithms", 28 | "web-token/jwt-encryption-algorithm-aesgcm": "AES GCM Based Content Encryption Algorithms", 29 | "web-token/jwt-encryption-algorithm-aesgcmkw": "AES GCM Key Wrapping Based Key Encryption Algorithms", 30 | "web-token/jwt-encryption-algorithm-aeskw": "AES Key Wrapping Based Key Encryption Algorithms", 31 | "web-token/jwt-encryption-algorithm-dir": "Direct Key Encryption Algorithms", 32 | "web-token/jwt-encryption-algorithm-ecdh-es": "ECDH-ES Based Key Encryption Algorithms", 33 | "web-token/jwt-encryption-algorithm-pbes2": "PBES2 Based Key Encryption Algorithms", 34 | "web-token/jwt-encryption-algorithm-rsa": "RSA Based Key Encryption Algorithms", 35 | "web-token/jwt-encryption-algorithm-experimental": "Experimental Key and Signature Algorithms" 36 | } 37 | } 38 | --------------------------------------------------------------------------------