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}