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