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-2015 ForgeRock AS.
015 */
016
017package org.forgerock.json.jose.common;
018
019import org.forgerock.json.JsonValue;
020import org.forgerock.json.jose.exceptions.InvalidJwtException;
021import org.forgerock.json.jose.exceptions.JwtReconstructionException;
022import org.forgerock.json.jose.jwe.EncryptedJwt;
023import org.forgerock.json.jose.jwe.JweHeader;
024import org.forgerock.json.jose.jws.JwsHeader;
025import org.forgerock.json.jose.jws.SignedEncryptedJwt;
026import org.forgerock.json.jose.jws.SignedJwt;
027import org.forgerock.json.jose.jwt.Jwt;
028import org.forgerock.json.jose.jwt.JwtClaimsSet;
029import org.forgerock.json.jose.jwt.JwtType;
030import org.forgerock.json.jose.utils.Utils;
031import org.forgerock.util.encode.Base64url;
032
033import java.util.HashMap;
034import java.util.Map;
035
036/**
037 * A service that provides a method for reconstruct a JWT string back into its relevant JWT object,
038 * (SignedJwt, EncryptedJwt, SignedEncryptedJwt).
039 *
040 * @since 2.0.0
041 */
042public class JwtReconstruction {
043
044    private static final int JWS_NUM_PARTS = 3;
045    private static final int JWE_NUM_PARTS = 5;
046
047    /**
048     * Reconstructs the given JWT string into a JWT object of the specified type.
049     *
050     * @param jwtString The JWT string.
051     * @param jwtClass The JWT class to reconstruct the JWT string to.
052     * @param <T> The type of JWT the JWT string represents.
053     * @return The reconstructed JWT object.
054     * @throws InvalidJwtException If the jwt does not consist of the correct number of parts.
055     * @throws JwtReconstructionException If the jwt does not consist of the correct number of parts.
056     */
057    public <T extends Jwt> T reconstructJwt(String jwtString, Class<T> jwtClass) {
058
059        Jwt jwt;
060
061        //split into parts
062        String[] jwtParts = jwtString.split("\\.", -1);
063        if (jwtParts.length != 3 && jwtParts.length != 5) {
064            throw new InvalidJwtException("not right number of dots, " + jwtParts.length);
065        }
066
067        //first part always header
068        //turn into json value
069        JsonValue headerJson = new JsonValue(Utils.parseJson(Utils.base64urlDecode(jwtParts[0])));
070        JwtType jwtType = JwtType.JWT;
071        if (headerJson.isDefined("typ")) {
072            jwtType = JwtType.valueOf(headerJson.get("typ").asString().toUpperCase());
073        }
074
075        if (headerJson.isDefined("enc")) {
076            //is encrypted jwt
077            verifyNumberOfParts(jwtParts, JWE_NUM_PARTS);
078            jwt = reconstructEncryptedJwt(jwtParts);
079        } else if (JwtType.JWE.equals(jwtType)) {
080            verifyNumberOfParts(jwtParts, JWS_NUM_PARTS);
081            jwt = reconstructSignedEncryptedJwt(jwtParts);
082        } else if (headerJson.isDefined("alg")) {
083            //is signed jwt
084            verifyNumberOfParts(jwtParts, JWS_NUM_PARTS);
085            jwt = reconstructSignedJwt(jwtParts);
086        } else {
087            //plaintext jwt
088            verifyNumberOfParts(jwtParts, JWS_NUM_PARTS);
089            if (!jwtParts[2].isEmpty()) {
090                throw new InvalidJwtException("Third part of Plaintext JWT not empty.");
091            }
092            jwt = reconstructSignedJwt(jwtParts);
093        }
094
095        return jwtClass.cast(jwt);
096    }
097
098    /**
099     * Verifies that the JWT parts are the required length for the JWT type being reconstructed.
100     *
101     * @param jwtParts The JWT parts.
102     * @param required The required number of parts.
103     * @throws JwtReconstructionException If the jwt does not consist of the correct number of parts.
104     */
105    private void verifyNumberOfParts(String[] jwtParts, int required) {
106        if (jwtParts.length != required) {
107            throw new JwtReconstructionException("Not the correct number of JWT parts. Expecting, " + required
108                    + ", actually, " + jwtParts.length);
109        }
110    }
111
112    /**
113     * Reconstructs a Signed JWT from the given JWT string parts.
114     * <p>
115     * As a plaintext JWT is a JWS with an empty signature, this method should be used to reconstruct plaintext JWTs
116     * as well as signed JWTs.
117     *
118     * @param jwtParts The three base64url UTF-8 encoded string parts of a plaintext or signed JWT.
119     * @return A SignedJwt object.
120     */
121    private SignedJwt reconstructSignedJwt(String[] jwtParts) {
122
123        String encodedHeader = jwtParts[0];
124        String encodedClaimsSet = jwtParts[1];
125        String encodedSignature = jwtParts[2];
126
127
128        String header = Utils.base64urlDecode(encodedHeader);
129        String claimsSetString = Utils.base64urlDecode(encodedClaimsSet);
130        byte[] signature = Base64url.decode(encodedSignature);
131
132        JwsHeader jwsHeader = new JwsHeader(Utils.parseJson(header));
133
134        JwtClaimsSet claimsSet = new JwtClaimsSet(Utils.parseJson(claimsSetString));
135
136        return new SignedJwt(jwsHeader, claimsSet, (encodedHeader + "." + encodedClaimsSet).getBytes(Utils.CHARSET),
137                signature);
138    }
139
140    /**
141     * Reconstructs an encrypted JWT from the given JWT string parts.
142     *
143     * @param jwtParts The five base64url UTF-8 encoded string parts of an encrypted JWT.
144     * @return An EncryptedJwt object.
145     */
146    private EncryptedJwt reconstructEncryptedJwt(String[] jwtParts) {
147
148        String encodedHeader = jwtParts[0];
149        String encodedEncryptedKey = jwtParts[1];
150        String encodedInitialisationVector = jwtParts[2];
151        String encodedCiphertext = jwtParts[3];
152        String encodedAuthenticationTag = jwtParts[4];
153
154
155        String header = Utils.base64urlDecode(encodedHeader);
156        byte[] encryptedContentEncryptionKey = Base64url.decode(encodedEncryptedKey);
157        byte[] initialisationVector = Base64url.decode(encodedInitialisationVector);
158        byte[] ciphertext = Base64url.decode(encodedCiphertext);
159        byte[] authenticationTag = Base64url.decode(encodedAuthenticationTag);
160
161
162        JweHeader jweHeader = new JweHeader(Utils.parseJson(header));
163
164
165        return new EncryptedJwt(jweHeader, encodedHeader, encryptedContentEncryptionKey, initialisationVector,
166                ciphertext, authenticationTag);
167    }
168
169    /**
170     * Reconstructs a signed and encrypted JWT from the given JWT string parts.
171     * <p>
172     * First reconstructs the nested encrypted JWT from within the signed JWT and then reconstructs the signed JWT using
173     * the reconstructed nested EncryptedJwt.
174     *
175     * @param jwtParts The three base64url UTF-8 encoded string parts of a signed JWT.
176     * @return A SignedEncryptedJwt object.
177     */
178    private SignedEncryptedJwt reconstructSignedEncryptedJwt(String[] jwtParts) {
179
180        String encodedHeader = jwtParts[0];
181        String encodedPayload = jwtParts[1];
182        String encodedSignature = jwtParts[2];
183
184
185        String header = Utils.base64urlDecode(encodedHeader);
186        String payloadString = Utils.base64urlDecode(encodedPayload);
187        byte[] signature = Base64url.decode(encodedSignature);
188
189        //split into parts
190        String[] encryptedJwtParts = payloadString.split("\\.", -1);
191        verifyNumberOfParts(encryptedJwtParts, JWE_NUM_PARTS);
192        EncryptedJwt encryptedJwt = reconstructEncryptedJwt(encryptedJwtParts);
193
194        Map<String, Object> combinedHeader = new HashMap<>(encryptedJwt.getHeader().getParameters());
195        combinedHeader.putAll(Utils.parseJson(header));
196
197        JwsHeader jwsHeader = new JwsHeader(combinedHeader);
198
199        return new SignedEncryptedJwt(jwsHeader, encryptedJwt,
200                (encodedHeader + "." + encodedPayload).getBytes(Utils.CHARSET), signature);
201    }
202}