001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2016, Connect2id Ltd. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.jose; 019 020 021import com.nimbusds.jose.crypto.impl.AAD; 022import com.nimbusds.jose.crypto.opts.MaxCompressedCipherTextLength; 023import com.nimbusds.jose.util.Base64URL; 024import net.jcip.annotations.ThreadSafe; 025 026import java.text.ParseException; 027import java.util.Objects; 028import java.util.Set; 029 030 031/** 032 * JSON Web Encryption (JWE) secured object with 033 * <a href="https://datatracker.ietf.org/doc/html/rfc7516#section-7.1">compact 034 * serialisation</a>. 035 * 036 * <p>This class is thread-safe. 037 * 038 * @author Vladimir Dzhuvinov 039 * @author Egor Puzanov 040 * @version 2026-09-16 041 */ 042@ThreadSafe 043public class JWEObject extends JOSEObject { 044 045 046 private static final long serialVersionUID = 1L; 047 048 049 /** 050 * The maximum allowed character length of compressed cipher text, in 051 * bytes. Passive (preemptive) safety limit against zip-bomb attacks. 052 */ 053 public static final int MAX_COMPRESSED_CIPHER_TEXT_LENGTH = 100_000; 054 055 056 /** 057 * The maximum allowed length of decompressed plain text, in bytes. 058 * Active (dynamic) safety limit against zip-bomb attacks. 059 */ 060 public static final int MAX_DECOMPRESSED_PLAIN_TEXT_LENGTH = 1_000_000; 061 062 063 /** 064 * Enumeration of the states of a JSON Web Encryption (JWE) secured 065 * object. 066 */ 067 public enum State { 068 069 070 /** 071 * The JWE secured object is created but not encrypted yet. 072 */ 073 UNENCRYPTED, 074 075 076 /** 077 * The JWE secured object is encrypted. 078 */ 079 ENCRYPTED, 080 081 082 /** 083 * The JWE secured object is decrypted. 084 */ 085 DECRYPTED 086 } 087 088 089 /** 090 * The header. 091 */ 092 private JWEHeader header; 093 094 095 /** 096 * The encrypted key, {@code null} if not computed or applicable. 097 */ 098 private Base64URL encryptedKey; 099 100 101 /** 102 * The initialisation vector, {@code null} if not generated or 103 * applicable. 104 */ 105 private Base64URL iv; 106 107 108 /** 109 * The cipher text, {@code null} if not computed. 110 */ 111 private Base64URL cipherText; 112 113 114 /** 115 * The authentication tag, {@code null} if not computed or applicable. 116 */ 117 private Base64URL authTag; 118 119 120 /** 121 * The JWE object state. 122 */ 123 private State state; 124 125 126 /** 127 * Creates a new to-be-encrypted JSON Web Encryption (JWE) object with 128 * the specified header and payload. The initial state will be 129 * {@link State#UNENCRYPTED unencrypted}. 130 * 131 * @param header The JWE header. Must not be {@code null}. 132 * @param payload The payload. Must not be {@code null}. 133 */ 134 public JWEObject(final JWEHeader header, final Payload payload) { 135 136 this.header = Objects.requireNonNull(header); 137 setPayload(Objects.requireNonNull(payload)); 138 encryptedKey = null; 139 cipherText = null; 140 state = State.UNENCRYPTED; 141 } 142 143 144 /** 145 * Creates a new encrypted JSON Web Encryption (JWE) object with the 146 * specified serialised parts. The state will be {@link State#ENCRYPTED 147 * encrypted}. 148 * 149 * @param firstPart The first part, corresponding to the JWE header. 150 * Must not be {@code null}. 151 * @param secondPart The second part, corresponding to the encrypted 152 * key. Empty or {@code null} if none. 153 * @param thirdPart The third part, corresponding to the 154 * initialisation vector. Empty or {@code null} if 155 * none. 156 * @param fourthPart The fourth part, corresponding to the cipher text. 157 * Must not be {@code null}. 158 * @param fifthPart The fifth part, corresponding to the 159 * authentication tag. Empty of {@code null} if none. 160 * 161 * @throws ParseException If parsing of the serialised parts failed. 162 */ 163 public JWEObject(final Base64URL firstPart, 164 final Base64URL secondPart, 165 final Base64URL thirdPart, 166 final Base64URL fourthPart, 167 final Base64URL fifthPart) 168 throws ParseException { 169 170 try { 171 this.header = JWEHeader.parse(Objects.requireNonNull(firstPart)); 172 } catch (ParseException e) { 173 throw new ParseException("Invalid JWE header: " + e.getMessage(), 0); 174 } 175 176 if (secondPart == null || secondPart.toString().isEmpty()) { 177 178 encryptedKey = null; 179 180 } else { 181 182 encryptedKey = secondPart; 183 } 184 185 if (thirdPart == null || thirdPart.toString().isEmpty()) { 186 187 iv = null; 188 189 } else { 190 191 iv = thirdPart; 192 } 193 194 cipherText = Objects.requireNonNull(fourthPart); 195 196 if (fifthPart == null || fifthPart.toString().isEmpty()) { 197 198 authTag = null; 199 200 } else { 201 202 authTag = fifthPart; 203 } 204 205 state = State.ENCRYPTED; // but not decrypted yet! 206 207 setParsedParts(firstPart, secondPart, thirdPart, fourthPart, fifthPart); 208 } 209 210 211 @Override 212 public JWEHeader getHeader() { 213 214 return header; 215 } 216 217 218 /** 219 * Returns the encrypted key of this JWE object. 220 * 221 * @return The encrypted key, {@code null} not applicable or the JWE 222 * object has not been encrypted yet. 223 */ 224 public Base64URL getEncryptedKey() { 225 226 return encryptedKey; 227 } 228 229 230 /** 231 * Returns the initialisation vector (IV) of this JWE object. 232 * 233 * @return The initialisation vector (IV), {@code null} if not 234 * applicable or the JWE object has not been encrypted yet. 235 */ 236 public Base64URL getIV() { 237 238 return iv; 239 } 240 241 242 /** 243 * Returns the cipher text of this JWE object. 244 * 245 * @return The cipher text, {@code null} if the JWE object has not been 246 * encrypted yet. 247 */ 248 public Base64URL getCipherText() { 249 250 return cipherText; 251 } 252 253 254 /** 255 * Returns the authentication tag of this JWE object. 256 * 257 * @return The authentication tag, {@code null} if not applicable or 258 * the JWE object has not been encrypted yet. 259 */ 260 public Base64URL getAuthTag() { 261 262 return authTag; 263 } 264 265 266 /** 267 * Returns the state of the JWE secured object. 268 * 269 * @return The state. 270 */ 271 public State getState() { 272 273 return state; 274 } 275 276 277 /** 278 * Ensures the current state is {@link State#UNENCRYPTED unencrypted}. 279 * 280 * @throws IllegalStateException If the current state is not 281 * unencrypted. 282 */ 283 private void ensureUnencryptedState() { 284 285 if (state != State.UNENCRYPTED) { 286 287 throw new IllegalStateException("The JWE object must be in an unencrypted state"); 288 } 289 } 290 291 292 /** 293 * Ensures the current state is {@link State#ENCRYPTED encrypted}. 294 * 295 * @throws IllegalStateException If the current state is not encrypted. 296 */ 297 private void ensureEncryptedState() { 298 299 if (state != State.ENCRYPTED) { 300 301 throw new IllegalStateException("The JWE object must be in an encrypted state"); 302 } 303 } 304 305 306 /** 307 * Ensures the current state is {@link State#ENCRYPTED encrypted} or 308 * {@link State#DECRYPTED decrypted}. 309 * 310 * @throws IllegalStateException If the current state is not encrypted 311 * or decrypted. 312 */ 313 private void ensureEncryptedOrDecryptedState() { 314 315 if (state != State.ENCRYPTED && state != State.DECRYPTED) { 316 317 throw new IllegalStateException("The JWE object must be in an encrypted or decrypted state"); 318 } 319 } 320 321 322 /** 323 * Ensures the specified JWE encrypter supports the algorithms of this 324 * JWE object. 325 * 326 * @throws JOSEException If the JWE algorithms are not supported. 327 */ 328 private void ensureJWEEncrypterSupport(final JWEEncrypter encrypter) 329 throws JOSEException { 330 331 if (! encrypter.supportedJWEAlgorithms().contains(getHeader().getAlgorithm())) { 332 333 throw new JOSEException("The " + getHeader().getAlgorithm() + 334 " algorithm is not supported by the JWE encrypter: Supported algorithms: " + encrypter.supportedJWEAlgorithms()); 335 } 336 337 if (! encrypter.supportedEncryptionMethods().contains(getHeader().getEncryptionMethod())) { 338 339 throw new JOSEException("The " + getHeader().getEncryptionMethod() + 340 " encryption method or key size is not supported by the JWE encrypter: Supported methods: " + encrypter.supportedEncryptionMethods()); 341 } 342 } 343 344 345 /** 346 * Encrypts this JWE object with the specified encrypter. The JWE 347 * object must be in an {@link State#UNENCRYPTED unencrypted} state. 348 * 349 * @param encrypter The JWE encrypter. Must not be {@code null}. 350 * 351 * @throws IllegalStateException If the JWE object is not in an 352 * {@link State#UNENCRYPTED unencrypted 353 * state}. 354 * @throws JOSEException If the JWE object couldn't be 355 * encrypted. 356 */ 357 public synchronized void encrypt(final JWEEncrypter encrypter) 358 throws JOSEException { 359 360 ensureUnencryptedState(); 361 362 ensureJWEEncrypterSupport(encrypter); 363 364 JWECryptoParts parts; 365 366 try { 367 parts = encrypter.encrypt(getHeader(), getPayload().toBytes(), AAD.compute(getHeader())); 368 369 } catch (JOSEException e) { 370 371 throw e; 372 373 } catch (Exception e) { 374 375 // Prevent throwing unchecked exceptions at this point, 376 // see issue #20 377 throw new JOSEException(e.getMessage(), e); 378 } 379 380 // Check if the header has been modified 381 if (parts.getHeader() != null) { 382 header = parts.getHeader(); 383 } 384 385 encryptedKey = parts.getEncryptedKey(); 386 iv = parts.getInitializationVector(); 387 cipherText = parts.getCipherText(); 388 authTag = parts.getAuthenticationTag(); 389 390 state = State.ENCRYPTED; 391 } 392 393 394 /** 395 * Decrypts this JWE object with the specified decrypter. The JWE 396 * object must be in a {@link State#ENCRYPTED encrypted} state. 397 * 398 * @param decrypter The JWE decrypter. Must not be {@code null}. 399 * 400 * @throws IllegalStateException If the JWE object is not in an 401 * {@link State#ENCRYPTED encrypted 402 * state}. 403 * @throws JOSEException If the JWE object couldn't be 404 * decrypted. 405 */ 406 public synchronized void decrypt(final JWEDecrypter decrypter) 407 throws JOSEException { 408 409 decrypt(decrypter, null); 410 } 411 412 413 /** 414 * Decrypts this JWE object with the specified decrypter and options. 415 * The JWE object must be in a {@link State#ENCRYPTED encrypted} state. 416 * 417 * @param decrypter The JWE decrypter. Must not be {@code null}. 418 * @param opts The JWE decrypter options, {@code null} if none. 419 * 420 * @throws IllegalStateException If the JWE object is not in an 421 * {@link State#ENCRYPTED encrypted 422 * state}. 423 * @throws JOSEException If the JWE object couldn't be 424 * decrypted. 425 */ 426 public synchronized void decrypt(final JWEDecrypter decrypter, 427 final Set<JWEDecrypterOption> opts) 428 throws JOSEException { 429 430 ensureEncryptedState(); 431 432 if (getHeader().getCompressionAlgorithm() != null) { 433 434 int maxLength = resolveMaxCompressedCipherTextLength(opts); 435 436 if (getCipherText().toString().length() > maxLength) { 437 438 throw new JOSEException( 439 "The JWE compressed cipher text exceeds the " + 440 "maximum allowed length of " + 441 maxLength + 442 " characters"); 443 } 444 } 445 446 try { 447 setPayload(new Payload(decrypter.decrypt(getHeader(), 448 getEncryptedKey(), 449 getIV(), 450 getCipherText(), 451 getAuthTag(), 452 AAD.compute(getHeader())))); 453 454 } catch (JOSEException e) { 455 456 throw e; 457 458 } catch (Exception e) { 459 460 // Prevent throwing unchecked exceptions at this point, 461 // see issue #20 462 throw new JOSEException(e.getMessage(), e); 463 } 464 465 state = State.DECRYPTED; 466 } 467 468 469 /** 470 * Resolves the maximum compressed cipher text length from the 471 * specified options. 472 * 473 * @param opts The options, {@code null} if none. 474 * 475 * @return The maximum compressed cipher text length. 476 */ 477 private static int resolveMaxCompressedCipherTextLength(final Set<JWEDecrypterOption> opts) { 478 479 if (opts != null) { 480 for (JWEDecrypterOption opt : opts) { 481 if (opt instanceof MaxCompressedCipherTextLength) { 482 return ((MaxCompressedCipherTextLength) opt).getMaxLength(); 483 } 484 } 485 } 486 return MAX_COMPRESSED_CIPHER_TEXT_LENGTH; 487 } 488 489 490 /** 491 * Serialises this JWE object to its compact format consisting of 492 * Base64URL-encoded parts delimited by period ('.') characters. It 493 * must be in a {@link State#ENCRYPTED encrypted} or 494 * {@link State#DECRYPTED decrypted} state. 495 * 496 * <pre> 497 * [header-base64url].[encryptedKey-base64url].[iv-base64url].[cipherText-base64url].[authTag-base64url] 498 * </pre> 499 * 500 * @return The serialised JWE object. 501 * 502 * @throws IllegalStateException If the JWE object is not in a 503 * {@link State#ENCRYPTED encrypted} or 504 * {@link State#DECRYPTED decrypted 505 * state}. 506 */ 507 @Override 508 public String serialize() { 509 510 ensureEncryptedOrDecryptedState(); 511 512 StringBuilder sb = new StringBuilder(header.toBase64URL().toString()); 513 sb.append('.'); 514 515 if (encryptedKey != null) { 516 sb.append(encryptedKey); 517 } 518 519 sb.append('.'); 520 521 if (iv != null) { 522 sb.append(iv); 523 } 524 525 sb.append('.'); 526 sb.append(cipherText); 527 sb.append('.'); 528 529 if (authTag != null) { 530 sb.append(authTag); 531 } 532 533 return sb.toString(); 534 } 535 536 537 /** 538 * Parses a JWE object from the specified string in compact form. The 539 * parsed JWE object will be given an {@link State#ENCRYPTED} state. 540 * 541 * @param s The string to parse. Must not be {@code null}. 542 * 543 * @return The JWE object. 544 * 545 * @throws ParseException If the string couldn't be parsed to a valid 546 * JWE object. 547 */ 548 public static JWEObject parse(final String s) 549 throws ParseException { 550 551 Base64URL[] parts = JOSEObject.split(s); 552 553 if (parts.length != 5) { 554 555 throw new ParseException("Unexpected number of Base64URL parts, must be five", 0); 556 } 557 558 return new JWEObject(parts[0], parts[1], parts[2], parts[3], parts[4]); 559 } 560}