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