001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2013-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.json.jose.common; 018 019import java.util.HashMap; 020import java.util.Map; 021 022import org.forgerock.json.JsonValue; 023import org.forgerock.json.jose.exceptions.InvalidJwtException; 024import org.forgerock.json.jose.exceptions.JwtReconstructionException; 025import org.forgerock.json.jose.jwe.CompressionManager; 026import org.forgerock.json.jose.jwe.EncryptedJwt; 027import org.forgerock.json.jose.jwe.JweHeader; 028import org.forgerock.json.jose.jwe.SignedThenEncryptedJwt; 029import org.forgerock.json.jose.jws.EncryptedThenSignedJwt; 030import org.forgerock.json.jose.jws.JwsHeader; 031import org.forgerock.json.jose.jws.SignedEncryptedJwt; 032import org.forgerock.json.jose.jws.SignedJwt; 033import org.forgerock.json.jose.jwt.Jwt; 034import org.forgerock.json.jose.jwt.JwtClaimsSet; 035import org.forgerock.json.jose.jwt.JwtType; 036import org.forgerock.json.jose.utils.Utils; 037import org.forgerock.util.encode.Base64url; 038 039/** 040 * A service that provides a method for reconstruct a JWT string back into its relevant JWT object, 041 * (SignedJwt, EncryptedJwt, SignedEncryptedJwt). 042 * 043 * @since 2.0.0 044 */ 045public class JwtReconstruction { 046 private static final String PAYLOAD_CONTENT_TYPE = "cty"; 047 private static final String JWT_TYPE = "typ"; 048 private static final String ENCRYPTION_METHOD = "enc"; 049 private static final String ALGORITHM = "alg"; 050 051 private static final int JWS_NUM_PARTS = 3; 052 private static final int JWE_NUM_PARTS = 5; 053 054 /** 055 * Reconstructs the given JWT string into a JWT object of the specified type. 056 * 057 * @param jwtString The JWT string. 058 * @param jwtClass The JWT class to reconstruct the JWT string to. 059 * @param <T> The type of JWT the JWT string represents. 060 * @return The reconstructed JWT object. 061 * @throws InvalidJwtException If the jwt does not consist of the correct number of parts. 062 * @throws JwtReconstructionException If the jwt does not consist of the correct number of parts. 063 */ 064 public <T extends Jwt> T reconstructJwt(String jwtString, Class<T> jwtClass) { 065 066 067 //split into parts 068 String[] jwtParts = jwtString.split("\\.", -1); 069 if (jwtParts.length != 3 && jwtParts.length != 5) { 070 throw new InvalidJwtException("not right number of dots, " + jwtParts.length); 071 } 072 073 //first part always header 074 //turn into json value 075 JsonValue headerJson = new JsonValue(Utils.parseJson(Utils.base64urlDecode(jwtParts[0]))); 076 JwtType contentType = null; 077 if (headerJson.isDefined(PAYLOAD_CONTENT_TYPE)) { 078 contentType = JwtType.jwtType(headerJson.get(PAYLOAD_CONTENT_TYPE).asString()); 079 } 080 081 JwtType jwtType = null; 082 if (headerJson.isDefined(JWT_TYPE)) { 083 jwtType = JwtType.jwtType(headerJson.get(JWT_TYPE).asString()); 084 } 085 086 final Jwt jwt; 087 if (headerJson.isDefined(ENCRYPTION_METHOD)) { 088 //is encrypted jwt 089 verifyNumberOfParts(jwtParts, JWE_NUM_PARTS); 090 jwt = reconstructEncryptedJwt(jwtParts); 091 } else if (JwtType.JWE == contentType || JwtType.JWT == contentType || JwtType.JWE == jwtType) { 092 verifyNumberOfParts(jwtParts, JWS_NUM_PARTS); 093 jwt = reconstructSignedEncryptedJwt(jwtParts); 094 } else if (headerJson.isDefined(ALGORITHM)) { 095 //is signed jwt 096 verifyNumberOfParts(jwtParts, JWS_NUM_PARTS); 097 jwt = reconstructSignedJwt(jwtParts); 098 } else { 099 //plaintext jwt 100 verifyNumberOfParts(jwtParts, JWS_NUM_PARTS); 101 if (!jwtParts[2].isEmpty()) { 102 throw new InvalidJwtException("Third part of Plaintext JWT not empty."); 103 } 104 jwt = reconstructSignedJwt(jwtParts); 105 } 106 107 return jwtClass.cast(jwt); 108 } 109 110 /** 111 * Verifies that the JWT parts are the required length for the JWT type being reconstructed. 112 * 113 * @param jwtParts The JWT parts. 114 * @param required The required number of parts. 115 * @throws JwtReconstructionException If the jwt does not consist of the correct number of parts. 116 */ 117 private void verifyNumberOfParts(String[] jwtParts, int required) { 118 if (jwtParts.length != required) { 119 throw new JwtReconstructionException("Not the correct number of JWT parts. Expecting, " + required 120 + ", actually, " + jwtParts.length); 121 } 122 } 123 124 /** 125 * Reconstructs a Signed JWT from the given JWT string parts. 126 * <p> 127 * As a plaintext JWT is a JWS with an empty signature, this method should be used to reconstruct plaintext JWTs 128 * as well as signed JWTs. 129 * 130 * @param jwtParts The three base64url UTF-8 encoded string parts of a plaintext or signed JWT. 131 * @return A SignedJwt object. 132 */ 133 private SignedJwt reconstructSignedJwt(String[] jwtParts) { 134 135 String encodedHeader = jwtParts[0]; 136 String encodedClaimsSet = jwtParts[1]; 137 String encodedSignature = jwtParts[2]; 138 139 String header = Utils.base64urlDecode(encodedHeader); 140 141 byte[] signature = Base64url.decode(encodedSignature); 142 143 JwsHeader jwsHeader = new JwsHeader(Utils.parseJson(header)); 144 145 byte[] payload = new CompressionManager().decompress(jwsHeader.getCompressionAlgorithm(), encodedClaimsSet); 146 JwtClaimsSet claimsSet = new JwtClaimsSet(Utils.parseJson(new String(payload, Utils.CHARSET))); 147 148 return new SignedJwt(jwsHeader, claimsSet, (encodedHeader + "." + encodedClaimsSet).getBytes(Utils.CHARSET), 149 signature); 150 } 151 152 /** 153 * Reconstructs an encrypted JWT from the given JWT string parts. 154 * 155 * @param jwtParts The five base64url UTF-8 encoded string parts of an encrypted JWT. 156 * @return An EncryptedJwt object. 157 */ 158 private EncryptedJwt reconstructEncryptedJwt(String[] jwtParts) { 159 160 String encodedHeader = jwtParts[0]; 161 String encodedEncryptedKey = jwtParts[1]; 162 String encodedInitialisationVector = jwtParts[2]; 163 String encodedCiphertext = jwtParts[3]; 164 String encodedAuthenticationTag = jwtParts[4]; 165 166 167 String header = Utils.base64urlDecode(encodedHeader); 168 byte[] encryptedContentEncryptionKey = Base64url.decode(encodedEncryptedKey); 169 byte[] initialisationVector = Base64url.decode(encodedInitialisationVector); 170 byte[] ciphertext = Base64url.decode(encodedCiphertext); 171 byte[] authenticationTag = Base64url.decode(encodedAuthenticationTag); 172 173 174 JweHeader jweHeader = new JweHeader(Utils.parseJson(header)); 175 176 if (jweHeader.getContentType() != null) { 177 return new SignedThenEncryptedJwt(jweHeader, encodedHeader, encryptedContentEncryptionKey, 178 initialisationVector, ciphertext, authenticationTag); 179 } else { 180 return new EncryptedJwt(jweHeader, encodedHeader, encryptedContentEncryptionKey, initialisationVector, 181 ciphertext, authenticationTag); 182 } 183 } 184 185 /** 186 * Reconstructs a signed and encrypted JWT from the given JWT string parts. 187 * <p> 188 * First reconstructs the nested encrypted JWT from within the signed JWT and then reconstructs the signed JWT using 189 * the reconstructed nested EncryptedJwt. 190 * 191 * @param jwtParts The three base64url UTF-8 encoded string parts of a signed JWT. 192 * @return A SignedEncryptedJwt object. 193 */ 194 private EncryptedThenSignedJwt reconstructSignedEncryptedJwt(String[] jwtParts) { 195 196 String encodedHeader = jwtParts[0]; 197 String encodedPayload = jwtParts[1]; 198 String encodedSignature = jwtParts[2]; 199 200 201 String header = Utils.base64urlDecode(encodedHeader); 202 String payloadString = Utils.base64urlDecode(encodedPayload); 203 byte[] signature = Base64url.decode(encodedSignature); 204 205 //split into parts 206 String[] encryptedJwtParts = payloadString.split("\\.", -1); 207 verifyNumberOfParts(encryptedJwtParts, JWE_NUM_PARTS); 208 EncryptedJwt encryptedJwt = reconstructEncryptedJwt(encryptedJwtParts); 209 210 Map<String, Object> combinedHeader = new HashMap<>(encryptedJwt.getHeader().getParameters()); 211 combinedHeader.putAll(Utils.parseJson(header)); 212 213 JwsHeader jwsHeader = new JwsHeader(combinedHeader); 214 215 // This can be changed to return EncryptedThenSignedJwt once SignedEncryptedJwt is removed 216 return new SignedEncryptedJwt(jwsHeader, encryptedJwt, 217 (encodedHeader + "." + encodedPayload).getBytes(Utils.CHARSET), signature); 218 } 219}