001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2023, Connect2id Ltd and contributors. 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.JWEObject.State; 022import com.nimbusds.jose.util.Base64URL; 023import com.nimbusds.jose.util.JSONArrayUtils; 024import com.nimbusds.jose.util.JSONObjectUtils; 025import net.jcip.annotations.Immutable; 026import net.jcip.annotations.ThreadSafe; 027 028import java.nio.charset.StandardCharsets; 029import java.text.ParseException; 030import java.util.*; 031 032 033/** 034 * JSON Web Encryption (JWE) secured object with 035 * <a href="https://datatracker.ietf.org/doc/html/rfc7516#section-7.2">JSON 036 * serialisation</a>. 037 * 038 * <p>This class is thread-safe. 039 * 040 * @author Egor Puzanov 041 * @author Vladimir Dzhuvinov 042 * @version 2024-04-20 043 */ 044@ThreadSafe 045public class JWEObjectJSON extends JOSEObjectJSON { 046 047 048 private static final long serialVersionUID = 1L; 049 050 051 /** 052 * Individual recipient in a JWE object serialisable to JSON. 053 */ 054 @Immutable 055 public static final class Recipient { 056 057 058 /** 059 * The per-recipient unprotected header. 060 */ 061 private final UnprotectedHeader unprotectedHeader; 062 063 064 /** 065 * The encrypted key, {@code null} if none. 066 */ 067 private final Base64URL encryptedKey; 068 069 070 /** 071 * Creates a new parsed recipient. 072 * 073 * @param unprotectedHeader The per-recipient unprotected 074 * header, {@code null} if none. 075 * @param encryptedKey The encrypted key, {@code null} if 076 * none. 077 */ 078 public Recipient(final UnprotectedHeader unprotectedHeader, 079 final Base64URL encryptedKey) { 080 this.unprotectedHeader = unprotectedHeader; 081 this.encryptedKey = encryptedKey; 082 } 083 084 085 /** 086 * Returns the per-recipient unprotected header. 087 * 088 * @return The per-recipient unprotected header, {@code null} 089 * if none. 090 */ 091 public UnprotectedHeader getUnprotectedHeader() { 092 return unprotectedHeader; 093 } 094 095 096 /** 097 * Returns the encrypted key. 098 * 099 * @return The encryptedKey. 100 */ 101 public Base64URL getEncryptedKey() { 102 return encryptedKey; 103 } 104 105 106 /** 107 * Returns a JSON object representation for use in the general 108 * and flattened serialisations. 109 * 110 * @return The JSON object. 111 */ 112 public Map<String, Object> toJSONObject() { 113 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 114 115 if (unprotectedHeader != null && ! unprotectedHeader.getIncludedParams().isEmpty()) { 116 jsonObject.put("header", unprotectedHeader.toJSONObject()); 117 } 118 if (encryptedKey != null) { 119 jsonObject.put("encrypted_key", encryptedKey.toString()); 120 } 121 return jsonObject; 122 } 123 124 125 /** 126 * Parses a recipients object from the specified JSON object. 127 * 128 * @param jsonObject The JSON object to parse. Must not be 129 * {@code null}. 130 * 131 * @return The recipient object. 132 * 133 * @throws ParseException If the string couldn't be parsed to a 134 * JWE object. 135 */ 136 public static Recipient parse(final Map<String, Object> jsonObject) 137 throws ParseException { 138 139 final UnprotectedHeader header = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "header")); 140 final Base64URL encryptedKey = JSONObjectUtils.getBase64URL(jsonObject, "encrypted_key"); 141 142 return new Recipient(header, encryptedKey); 143 } 144 } 145 146 147 /** 148 * The JWE protected header. 149 */ 150 private final JWEHeader header; 151 152 153 /** 154 * The shared unprotected header. 155 */ 156 private UnprotectedHeader unprotectedHeader; 157 158 159 /** 160 * The recipients list. 161 */ 162 private final List<Recipient> recipients = new LinkedList<>(); 163 164 165 /** 166 * The initialisation vector, {@code null} if not generated or 167 * applicable. 168 */ 169 private Base64URL iv; 170 171 172 /** 173 * The cipher text, {@code null} if not computed. 174 */ 175 private Base64URL cipherText; 176 177 178 /** 179 * The authentication tag, {@code null} if not computed or applicable. 180 */ 181 private Base64URL authTag; 182 183 184 /** 185 * The additional authenticated data, {@code null} if not computed or 186 * applicable. 187 */ 188 private final byte[] aad; 189 190 191 /** 192 * The JWE object state. 193 */ 194 private JWEObject.State state; 195 196 197 /** 198 * Creates a new JWE JSON object from the specified JWE object with 199 * compact serialisation. The initial state is copied from the JWE 200 * object. 201 * 202 * @param jweObject The JWE object. Must not be {@code null}. 203 */ 204 public JWEObjectJSON(final JWEObject jweObject) { 205 206 super(jweObject.getPayload()); 207 208 this.header = jweObject.getHeader(); 209 this.aad = null; 210 this.iv = jweObject.getIV(); 211 this.cipherText = jweObject.getCipherText(); 212 this.authTag = jweObject.getAuthTag(); 213 if (jweObject.getState() == JWEObject.State.ENCRYPTED) { 214 this.recipients.add(new Recipient(null, jweObject.getEncryptedKey())); 215 this.state = State.ENCRYPTED; 216 } else if (jweObject.getState() == JWEObject.State.DECRYPTED) { 217 this.recipients.add(new Recipient(null, jweObject.getEncryptedKey())); 218 this.state = State.DECRYPTED; 219 } else { 220 this.state = State.UNENCRYPTED; 221 } 222 } 223 224 225 /** 226 * Creates a new to-be-encrypted JSON Web Encryption (JWE) object with 227 * the specified JWE protected header and payload. The initial state 228 * will be {@link State#UNENCRYPTED unencrypted}. 229 * 230 * @param header The JWE protected header. Must not be {@code null}. 231 * @param payload The payload. Must not be {@code null}. 232 */ 233 public JWEObjectJSON(final JWEHeader header, final Payload payload) { 234 235 this(header, payload, null, null); 236 } 237 238 239 /** 240 * Creates a new to-be-encrypted JSON Web Encryption (JWE) object with 241 * the specified JWE protected header, payload and Additional 242 * Authenticated Data (AAD). The initial state will be 243 * {@link State#UNENCRYPTED unencrypted}. 244 * 245 * @param header The JWE protected header. Must not be 246 * {@code null}. 247 * @param payload The payload. Must not be {@code null}. 248 * @param unprotectedHeader The shared unprotected header, empty or 249 * {@code null} if none. 250 * @param aad The additional authenticated data (AAD), 251 * {@code null} if none. 252 */ 253 public JWEObjectJSON(final JWEHeader header, 254 final Payload payload, 255 final UnprotectedHeader unprotectedHeader, 256 final byte[] aad) { 257 258 super(payload); 259 this.header = Objects.requireNonNull(header); 260 setPayload(Objects.requireNonNull(payload)); 261 this.unprotectedHeader = unprotectedHeader; 262 this.aad = aad; 263 this.cipherText = null; 264 this.state = State.UNENCRYPTED; 265 } 266 267 268 /** 269 * Creates a new encrypted JSON Web Encryption (JWE) object. The state 270 * will be {@link State#ENCRYPTED encrypted}. 271 * 272 * @param header The JWE protected header. Must not be 273 * {@code null}. 274 * @param cipherText The cipher text. Must not be {@code null}. 275 * @param iv The initialisation vector, empty or 276 * {@code null} if none. 277 * @param authTag The authentication tag, empty or 278 * {@code null} if none. 279 * @param recipients The recipients list. Must not be 280 * {@code null}. 281 * @param unprotectedHeader The shared unprotected header, empty or 282 * {@code null} if none. 283 * @param aad The additional authenticated data. Must not 284 * be {@code null}. 285 * 286 */ 287 public JWEObjectJSON(final JWEHeader header, 288 final Base64URL cipherText, 289 final Base64URL iv, 290 final Base64URL authTag, 291 final List<Recipient> recipients, 292 final UnprotectedHeader unprotectedHeader, 293 final byte[] aad) { 294 295 super(null); // Payload not decrypted yet, must be null 296 297 this.header = Objects.requireNonNull(header); 298 this.recipients.addAll(recipients); 299 this.unprotectedHeader = unprotectedHeader; 300 this.aad = aad; 301 this.iv = iv; 302 this.cipherText = Objects.requireNonNull(cipherText); 303 this.authTag = authTag; 304 305 state = State.ENCRYPTED; // but not decrypted yet! 306 } 307 308 309 /** 310 * Returns the JWE protected header of this JWE object. 311 * 312 * @return The JWE protected header. 313 */ 314 public JWEHeader getHeader() { 315 return header; 316 } 317 318 319 /** 320 * Returns the shared unprotected header of this JWE object. 321 * 322 * @return The shared unprotected header, empty or {@code null} if 323 * none. 324 */ 325 public UnprotectedHeader getUnprotectedHeader() { 326 return unprotectedHeader; 327 } 328 329 330 /** 331 * Returns the encrypted key of this JWE object. 332 * 333 * @return The encrypted key, {@code null} not applicable or the JWE 334 * object has not been encrypted yet. 335 */ 336 public Base64URL getEncryptedKey() { 337 if (recipients.isEmpty()) { 338 return null; 339 } else if (recipients.size() == 1) { 340 return recipients.get(0).getEncryptedKey(); 341 } 342 List<Object> recipientsList = JSONArrayUtils.newJSONArray(); 343 for (Recipient recipient : recipients) { 344 recipientsList.add(recipient.toJSONObject()); 345 } 346 Map<String, Object> recipientsMap = JSONObjectUtils.newJSONObject(); 347 recipientsMap.put("recipients", recipientsList); 348 return Base64URL.encode(JSONObjectUtils.toJSONString(recipientsMap)); 349 } 350 351 352 /** 353 * Returns the initialisation vector (IV) of this JWE object. 354 * 355 * @return The initialisation vector (IV), {@code null} if not 356 * applicable or the JWE object has not been encrypted yet. 357 */ 358 public Base64URL getIV() { 359 return iv; 360 } 361 362 363 /** 364 * Returns the cipher text of this JWE object. 365 * 366 * @return The cipher text, {@code null} if the JWE object has not been 367 * encrypted yet. 368 */ 369 public Base64URL getCipherText() { 370 return cipherText; 371 } 372 373 374 /** 375 * Returns the authentication tag of this JWE object. 376 * 377 * @return The authentication tag, {@code null} if not applicable or 378 * the JWE object has not been encrypted yet. 379 */ 380 public Base64URL getAuthTag() { 381 return authTag; 382 } 383 384 385 /** 386 * Returns the Additional Authenticated Data (AAD) of this JWE object. 387 * 388 * @return The Additional Authenticated Data (AAD). 389 */ 390 public byte[] getAAD() { 391 StringBuilder aadSB = new StringBuilder(header.toBase64URL().toString()); 392 if (aad != null && aad.length > 0) { 393 aadSB.append(".").append(new String(aad, StandardCharsets.US_ASCII)); 394 } 395 return aadSB.toString().getBytes(StandardCharsets.US_ASCII); 396 } 397 398 399 /** 400 * Returns the recipients list of the JWE object. 401 * 402 * @return The recipients list. 403 */ 404 public List<Recipient> getRecipients() { 405 return Collections.unmodifiableList(recipients); 406 } 407 408 409 /** 410 * Returns the state of this JWE object. 411 * 412 * @return The state. 413 */ 414 public State getState() { 415 return state; 416 } 417 418 419 /** 420 * Ensures the current state is {@link State#UNENCRYPTED unencrypted}. 421 * 422 * @throws IllegalStateException If the current state is not 423 * unencrypted. 424 */ 425 private void ensureUnencryptedState() { 426 if (state != State.UNENCRYPTED) { 427 throw new IllegalStateException("The JWE object must be in an unencrypted state"); 428 } 429 } 430 431 432 /** 433 * Ensures the current state is {@link State#ENCRYPTED encrypted}. 434 * 435 * @throws IllegalStateException If the current state is not encrypted. 436 */ 437 private void ensureEncryptedState() { 438 if (state != State.ENCRYPTED) { 439 throw new IllegalStateException("The JWE object must be in an encrypted state"); 440 } 441 } 442 443 444 /** 445 * Ensures the current state is {@link State#ENCRYPTED encrypted} or 446 * {@link State#DECRYPTED decrypted}. 447 * 448 * @throws IllegalStateException If the current state is not encrypted 449 * or decrypted. 450 */ 451 private void ensureEncryptedOrDecryptedState() { 452 if (state != State.ENCRYPTED && state != State.DECRYPTED) { 453 throw new IllegalStateException("The JWE object must be in an encrypted or decrypted state"); 454 } 455 } 456 457 458 /** 459 * Ensures the specified JWE encrypter supports the algorithms of this 460 * JWE object. 461 * 462 * @throws JOSEException If the JWE algorithms are not supported. 463 */ 464 private void ensureJWEEncrypterSupport(final JWEEncrypter encrypter) 465 throws JOSEException { 466 467 if (! encrypter.supportedJWEAlgorithms().contains(getHeader().getAlgorithm())) { 468 throw new JOSEException("The " + getHeader().getAlgorithm() + 469 " algorithm is not supported by the JWE encrypter: Supported algorithms: " + encrypter.supportedJWEAlgorithms()); 470 } 471 472 if (! encrypter.supportedEncryptionMethods().contains(getHeader().getEncryptionMethod())) { 473 throw new JOSEException("The " + getHeader().getEncryptionMethod() + 474 " encryption method or key size is not supported by the JWE encrypter: Supported methods: " + encrypter.supportedEncryptionMethods()); 475 } 476 } 477 478 479 /** 480 * Encrypts this JWE object with the specified encrypter. The JWE 481 * object must be in an {@link State#UNENCRYPTED unencrypted} state. 482 * 483 * @param encrypter The JWE encrypter. Must not be {@code null}. 484 * 485 * @throws IllegalStateException If the JWE object is not in an 486 * {@link State#UNENCRYPTED unencrypted 487 * state}. 488 * @throws JOSEException If the JWE object couldn't be 489 * encrypted. 490 */ 491 public synchronized void encrypt(final JWEEncrypter encrypter) 492 throws JOSEException { 493 494 ensureUnencryptedState(); 495 496 ensureJWEEncrypterSupport(encrypter); 497 498 JWECryptoParts parts; 499 500 JWEHeader jweJoinedHeader = getHeader(); 501 try { 502 jweJoinedHeader = (JWEHeader) getHeader().join(unprotectedHeader); 503 parts = encrypter.encrypt(jweJoinedHeader, getPayload().toBytes(), getAAD()); 504 } catch (JOSEException e) { 505 throw e; 506 } catch (Exception e) { 507 // Prevent throwing unchecked exceptions at this point, 508 // see issue #20 509 throw new JOSEException(e.getMessage(), e); 510 } 511 512 Base64URL encryptedKey = parts.getEncryptedKey(); 513 try { 514 for (Map<String, Object> recipientMap : JSONObjectUtils.getJSONObjectArray((JSONObjectUtils.parse(encryptedKey.decodeToString())), "recipients")) { 515 recipients.add(Recipient.parse(recipientMap)); 516 } 517 } catch (Exception e) { 518 Map<String, Object> recipientHeader = parts.getHeader().toJSONObject(); 519 for (String param : jweJoinedHeader.getIncludedParams()) { 520 if (recipientHeader.containsKey(param)) { 521 recipientHeader.remove(param); 522 } 523 } 524 try { 525 recipients.add(new Recipient(UnprotectedHeader.parse(recipientHeader), encryptedKey)); 526 } catch (Exception ex) { 527 throw new JOSEException(ex.getMessage(), ex); 528 } 529 } 530 iv = parts.getInitializationVector(); 531 cipherText = parts.getCipherText(); 532 authTag = parts.getAuthenticationTag(); 533 534 state = State.ENCRYPTED; 535 } 536 537 538 /** 539 * Decrypts this JWE object with the specified decrypter. The JWE 540 * object must be in a {@link State#ENCRYPTED encrypted} state. 541 * 542 * @param decrypter The JWE decrypter. Must not be {@code null}. 543 * 544 * @throws IllegalStateException If the JWE object is not in an 545 * {@link State#ENCRYPTED encrypted 546 * state}. 547 * @throws JOSEException If the JWE object couldn't be 548 * decrypted. 549 */ 550 public synchronized void decrypt(final JWEDecrypter decrypter) 551 throws JOSEException { 552 553 ensureEncryptedState(); 554 555 try { 556 setPayload(new Payload(decrypter.decrypt(getHeader(), 557 getEncryptedKey(), 558 getIV(), 559 getCipherText(), 560 getAuthTag(), 561 getAAD()))); 562 } catch (JOSEException e) { 563 throw e; 564 } catch (Exception e) { 565 // Prevent throwing unchecked exceptions at this point, 566 // see issue #20 567 throw new JOSEException(e.getMessage(), e); 568 } 569 570 state = State.DECRYPTED; 571 } 572 573 574 /** 575 * Returns the JSON object with the common members in general and 576 * flattened JWE JSON serialisation. 577 */ 578 private Map<String,Object> toBaseJSONObject() { 579 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 580 jsonObject.put("protected", header.toBase64URL().toString()); 581 if (aad != null) { 582 jsonObject.put("aad", new String(aad, StandardCharsets.US_ASCII)); 583 } 584 jsonObject.put("ciphertext", cipherText.toString()); 585 jsonObject.put("iv", iv.toString()); 586 jsonObject.put("tag", authTag.toString()); 587 return jsonObject; 588 } 589 590 591 @Override 592 public Map<String, Object> toGeneralJSONObject() { 593 594 ensureEncryptedOrDecryptedState(); 595 596 if (recipients.isEmpty() || (recipients.get(0).getUnprotectedHeader() == null && recipients.get(0).getEncryptedKey() == null)) { 597 throw new IllegalStateException("The general JWE JSON serialization requires at least one recipient"); 598 } 599 600 Map<String, Object> jsonObject = toBaseJSONObject(); 601 602 if (unprotectedHeader != null) { 603 jsonObject.put("unprotected", unprotectedHeader.toJSONObject()); 604 } 605 606 List<Object> recipientsJSONArray = JSONArrayUtils.newJSONArray(); 607 608 for (Recipient recipient: recipients) { 609 Map<String, Object> recipientJSONObject = recipient.toJSONObject(); 610 recipientsJSONArray.add(recipientJSONObject); 611 } 612 613 jsonObject.put("recipients", recipientsJSONArray); 614 return jsonObject; 615 } 616 617 618 @Override 619 public Map<String, Object> toFlattenedJSONObject() { 620 621 ensureEncryptedOrDecryptedState(); 622 623 if (recipients.size() != 1) { 624 throw new IllegalStateException("The flattened JWE JSON serialization requires exactly one recipient"); 625 } 626 627 Map<String, Object> jsonObject = toBaseJSONObject(); 628 629 Map<String, Object> recipientHeader = JSONObjectUtils.newJSONObject(); 630 if (recipients.get(0).getUnprotectedHeader() != null) { 631 recipientHeader.putAll(recipients.get(0).getUnprotectedHeader().toJSONObject()); 632 } 633 if (unprotectedHeader != null) { 634 recipientHeader.putAll(unprotectedHeader.toJSONObject()); 635 } 636 if (recipientHeader.size() > 0) { 637 jsonObject.put("unprotected", recipientHeader); 638 } 639 if (recipients.get(0).getEncryptedKey() != null) { 640 jsonObject.put("encrypted_key", recipients.get(0).getEncryptedKey().toString()); 641 } 642 return jsonObject; 643 } 644 645 646 @Override 647 public String serializeGeneral() { 648 return JSONObjectUtils.toJSONString(toGeneralJSONObject()); 649 } 650 651 652 @Override 653 public String serializeFlattened() { 654 return JSONObjectUtils.toJSONString(toFlattenedJSONObject()); 655 } 656 657 658 /** 659 * Parses a JWE object from the specified JSON object representation. 660 * 661 * @param jsonObject The JSON object to parse. Must not be 662 * {@code null}. 663 * 664 * @return The JWE secured object. 665 * 666 * @throws ParseException If the JSON object couldn't be parsed to a 667 * JWE secured object. 668 */ 669 public static JWEObjectJSON parse(final Map<String, Object> jsonObject) 670 throws ParseException { 671 672 if (!jsonObject.containsKey("protected")) { 673 throw new ParseException("The JWE protected header mast be present", 0); 674 } 675 676 List<Recipient> recipientList = new LinkedList<>(); 677 final JWEHeader jweHeader = JWEHeader.parse(JSONObjectUtils.getBase64URL(jsonObject, "protected")); 678 final UnprotectedHeader unprotected = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "unprotected")); 679 final Base64URL cipherText = JSONObjectUtils.getBase64URL(jsonObject, "ciphertext"); 680 final Base64URL iv = JSONObjectUtils.getBase64URL(jsonObject, "iv"); 681 final Base64URL authTag = JSONObjectUtils.getBase64URL(jsonObject, "tag"); 682 final Base64URL aad = JSONObjectUtils.getBase64URL(jsonObject, "aad"); 683 final JWEHeader jweJoinedHeader = (JWEHeader) jweHeader.join(unprotected); 684 685 if (jsonObject.containsKey("recipients")) { 686 Map<String, Object>[] recipients = JSONObjectUtils.getJSONObjectArray(jsonObject, "recipients"); 687 if (recipients == null || recipients.length == 0) { 688 throw new ParseException("The \"recipients\" member must be present in general JSON Serialization", 0); 689 } 690 for (Map<String, Object> recipientJSONObject: recipients) { 691 Recipient recipient = Recipient.parse(recipientJSONObject); 692 try { 693 HeaderValidation.ensureDisjoint(jweJoinedHeader, recipient.getUnprotectedHeader()); 694 } catch (IllegalHeaderException e) { 695 throw new ParseException(e.getMessage(), 0); 696 } 697 recipientList.add(recipient); 698 } 699 } else { 700 Base64URL encryptedKey = JSONObjectUtils.getBase64URL(jsonObject, "encrypted_key"); 701 recipientList.add(new Recipient(null, encryptedKey)); 702 } 703 704 return new JWEObjectJSON(jweHeader, cipherText, iv, authTag, recipientList, unprotected, aad == null ? null : aad.toString().getBytes(StandardCharsets.US_ASCII)); 705 } 706 707 708 /** 709 * Parses a JWE object from the specified JSON object string. 710 * 711 * @param json The JSON object string to parse. Must not be 712 * {@code null}. 713 * 714 * @return The JWE object. 715 * 716 * @throws ParseException If the string couldn't be parsed to a JWE 717 * object. 718 */ 719 public static JWEObjectJSON parse(final String json) 720 throws ParseException { 721 722 return parse(JSONObjectUtils.parse(Objects.requireNonNull(json))); 723 } 724}