001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, 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.crypto.impl;
019
020
021import java.security.SecureRandom;
022import java.util.*;
023import javax.crypto.SecretKey;
024import javax.crypto.spec.SecretKeySpec;
025
026import com.nimbusds.jose.*;
027import com.nimbusds.jose.jca.JWEJCAContext;
028import com.nimbusds.jose.util.Base64URL;
029import com.nimbusds.jose.util.ByteUtils;
030import com.nimbusds.jose.util.Container;
031import com.nimbusds.jose.util.IntegerOverflowException;
032
033
034/**
035 * JWE content encryption / decryption provider.
036 *
037 * @author Vladimir Dzhuvinov
038 * @version 2022-09-20
039 */
040public class ContentCryptoProvider {
041
042
043        /**
044         * The supported encryption methods.
045         */
046        public static final Set<EncryptionMethod> SUPPORTED_ENCRYPTION_METHODS;
047
048
049        /**
050         * The encryption methods compatible with each key size in bits.
051         */
052        public static final Map<Integer,Set<EncryptionMethod>> COMPATIBLE_ENCRYPTION_METHODS;
053
054
055        static {
056                Set<EncryptionMethod> methods = new LinkedHashSet<>();
057                methods.add(EncryptionMethod.A128CBC_HS256);
058                methods.add(EncryptionMethod.A192CBC_HS384);
059                methods.add(EncryptionMethod.A256CBC_HS512);
060                methods.add(EncryptionMethod.A128GCM);
061                methods.add(EncryptionMethod.A192GCM);
062                methods.add(EncryptionMethod.A256GCM);
063                methods.add(EncryptionMethod.A128CBC_HS256_DEPRECATED);
064                methods.add(EncryptionMethod.A256CBC_HS512_DEPRECATED);
065                methods.add(EncryptionMethod.XC20P);
066                SUPPORTED_ENCRYPTION_METHODS = Collections.unmodifiableSet(methods);
067
068                Map<Integer,Set<EncryptionMethod>> encsMap = new HashMap<>();
069                Set<EncryptionMethod> bit128Encs = new HashSet<>();
070                Set<EncryptionMethod> bit192Encs = new HashSet<>();
071                Set<EncryptionMethod> bit256Encs = new HashSet<>();
072                Set<EncryptionMethod> bit384Encs = new HashSet<>();
073                Set<EncryptionMethod> bit512Encs = new HashSet<>();
074                bit128Encs.add(EncryptionMethod.A128GCM);
075                bit192Encs.add(EncryptionMethod.A192GCM);
076                bit256Encs.add(EncryptionMethod.A256GCM);
077                bit256Encs.add(EncryptionMethod.A128CBC_HS256);
078                bit256Encs.add(EncryptionMethod.A128CBC_HS256_DEPRECATED);
079                bit256Encs.add(EncryptionMethod.XC20P);
080                bit384Encs.add(EncryptionMethod.A192CBC_HS384);
081                bit512Encs.add(EncryptionMethod.A256CBC_HS512);
082                bit512Encs.add(EncryptionMethod.A256CBC_HS512_DEPRECATED);
083                encsMap.put(128,Collections.unmodifiableSet(bit128Encs));
084                encsMap.put(192,Collections.unmodifiableSet(bit192Encs));
085                encsMap.put(256,Collections.unmodifiableSet(bit256Encs));
086                encsMap.put(384,Collections.unmodifiableSet(bit384Encs));
087                encsMap.put(512, Collections.unmodifiableSet(bit512Encs));
088                COMPATIBLE_ENCRYPTION_METHODS = Collections.unmodifiableMap(encsMap);
089        }
090
091
092        /**
093         * Generates a Content Encryption Key (CEK) for the specified JOSE
094         * encryption method.
095         *
096         * @param enc       The encryption method. Must not be {@code null}.
097         * @param randomGen The secure random generator to use. Must not be
098         *                  {@code null}.
099         *
100         * @return The generated CEK (with algorithm "AES").
101         *
102         * @throws JOSEException If the encryption method is not supported.
103         */
104        public static SecretKey generateCEK(final EncryptionMethod enc, final SecureRandom randomGen)
105                throws JOSEException {
106
107                if (! SUPPORTED_ENCRYPTION_METHODS.contains(enc)) {
108                        throw new JOSEException(AlgorithmSupportMessage.unsupportedEncryptionMethod(enc, SUPPORTED_ENCRYPTION_METHODS));
109                }
110
111                final byte[] cekMaterial = new byte[ByteUtils.byteLength(enc.cekBitLength())];
112
113                randomGen.nextBytes(cekMaterial);
114
115                return new SecretKeySpec(cekMaterial, "AES");
116        }
117
118
119        /**
120         * Checks the length of the Content Encryption Key (CEK) according to
121         * the encryption method.
122         *
123         * @param cek The CEK. Must not be {@code null}.
124         * @param enc The encryption method. Must not be {@code null}.
125         *
126         * @throws KeyLengthException If the CEK length doesn't match the
127         *                            encryption method.
128         */
129        private static void checkCEKLength(final SecretKey cek, final EncryptionMethod enc)
130                throws KeyLengthException {
131                
132                final int cekBitLength;
133                try {
134                        cekBitLength = ByteUtils.safeBitLength(cek.getEncoded());
135                } catch (IntegerOverflowException e) {
136                        throw new KeyLengthException("The Content Encryption Key (CEK) is too long: " + e.getMessage());
137                }
138                
139                if (cekBitLength == 0) {
140                        // Suspect HSM that doesn't expose key material
141                        // https://bitbucket.org/connect2id/nimbus-jose-jwt/issues/490/jwe-with-shared-key-support-for-android
142                        return;
143                }
144                
145                if (enc.cekBitLength() != cekBitLength) {
146                        throw new KeyLengthException("The Content Encryption Key (CEK) length for " + enc + " must be " + enc.cekBitLength() + " bits");
147                }
148        }
149
150
151        /**
152         * Encrypts the specified clear text (content).
153         *
154         * @param header       The final JWE header. Must not be {@code null}.
155         * @param clearText    The clear text to encrypt and optionally
156         *                     compress. Must not be {@code null}.
157         * @param cek          The Content Encryption Key (CEK). Must not be
158         *                     {@code null}.
159         * @param encryptedKey The encrypted CEK, {@code null} if not required.
160         * @param jcaProvider  The JWE JCA provider specification. Must not be
161         *                     {@code null}.
162         *
163         * @return The JWE crypto parts.
164         *
165         * @throws JOSEException If encryption failed.
166         */
167        public static JWECryptoParts encrypt(final JWEHeader header,
168                                             final byte[] clearText,
169                                             final SecretKey cek,
170                                             final Base64URL encryptedKey,
171                                             final JWEJCAContext jcaProvider)
172                throws JOSEException {
173
174                checkCEKLength(cek, header.getEncryptionMethod());
175
176                // Apply compression if instructed
177                final byte[] plainText = DeflateHelper.applyCompression(header, clearText);
178
179                // Compose the AAD
180                final byte[] aad = AAD.compute(header);
181
182                // Encrypt the plain text according to the JWE enc
183                final byte[] iv;
184                final AuthenticatedCipherText authCipherText;
185
186                if (    header.getEncryptionMethod().equals(EncryptionMethod.A128CBC_HS256) ||
187                        header.getEncryptionMethod().equals(EncryptionMethod.A192CBC_HS384) ||
188                        header.getEncryptionMethod().equals(EncryptionMethod.A256CBC_HS512)    ) {
189
190                        iv = AESCBC.generateIV(jcaProvider.getSecureRandom());
191
192                        authCipherText = AESCBC.encryptAuthenticated(
193                                cek, iv, plainText, aad,
194                                jcaProvider.getContentEncryptionProvider(),
195                                jcaProvider.getMACProvider());
196
197                } else if (header.getEncryptionMethod().equals(EncryptionMethod.A128GCM) ||
198                           header.getEncryptionMethod().equals(EncryptionMethod.A192GCM) ||
199                           header.getEncryptionMethod().equals(EncryptionMethod.A256GCM)    ) {
200
201                        Container<byte[]> ivContainer = new Container<>(AESGCM.generateIV(jcaProvider.getSecureRandom()));
202
203                        authCipherText = AESGCM.encrypt(
204                                cek, ivContainer, plainText, aad,
205                                jcaProvider.getContentEncryptionProvider());
206
207                        iv = ivContainer.get();
208
209                } else if (header.getEncryptionMethod().equals(EncryptionMethod.A128CBC_HS256_DEPRECATED) ||
210                           header.getEncryptionMethod().equals(EncryptionMethod.A256CBC_HS512_DEPRECATED)    ) {
211
212                        iv = AESCBC.generateIV(jcaProvider.getSecureRandom());
213
214                        authCipherText = AESCBC.encryptWithConcatKDF(
215                                header, cek, encryptedKey, iv, plainText,
216                                jcaProvider.getContentEncryptionProvider(),
217                                jcaProvider.getMACProvider());
218
219                } else if (header.getEncryptionMethod().equals(EncryptionMethod.XC20P)) {
220
221                        Container<byte[]> ivContainer = new Container<>(null);
222
223                        authCipherText = XC20P.encryptAuthenticated(cek, ivContainer, plainText, aad);
224
225                        iv = ivContainer.get();
226
227                } else {
228
229                        throw new JOSEException(AlgorithmSupportMessage.unsupportedEncryptionMethod(
230                                header.getEncryptionMethod(),
231                                SUPPORTED_ENCRYPTION_METHODS));
232                }
233
234                return new JWECryptoParts(
235                        header,
236                        encryptedKey,
237                        Base64URL.encode(iv),
238                        Base64URL.encode(authCipherText.getCipherText()),
239                        Base64URL.encode(authCipherText.getAuthenticationTag()));
240        }
241
242
243        /**
244         * Decrypts the specified cipher text.
245         *
246         * @param header       The JWE header. Must not be {@code null}.
247         * @param encryptedKey The encrypted key, {@code null} if not
248         *                     specified.
249         * @param iv           The initialisation vector (IV). Must not be
250         *                     {@code null}.
251         * @param cipherText   The cipher text. Must not be {@code null}.
252         * @param authTag      The authentication tag. Must not be
253         *                     {@code null}.
254         * @param cek          The Content Encryption Key (CEK). Must not be
255         *                     {@code null}.
256         * @param jcaProvider  The JWE JCA provider specification. Must not be
257         *                     {@code null}.
258         *
259         * @return The clear text.
260         *
261         * @throws JOSEException If decryption failed.
262         */
263        public static byte[] decrypt(final JWEHeader header,
264                                     final Base64URL encryptedKey,
265                                     final Base64URL iv,
266                                     final Base64URL cipherText,
267                                     final Base64URL authTag,
268                                     final SecretKey cek,
269                                     final JWEJCAContext jcaProvider)
270                throws JOSEException {
271
272                checkCEKLength(cek, header.getEncryptionMethod());
273
274                // Compose the AAD
275                byte[] aad = AAD.compute(header);
276
277                // Decrypt the cipher text according to the JWE enc
278
279                byte[] plainText;
280
281                if (header.getEncryptionMethod().equals(EncryptionMethod.A128CBC_HS256) ||
282                        header.getEncryptionMethod().equals(EncryptionMethod.A192CBC_HS384) ||
283                        header.getEncryptionMethod().equals(EncryptionMethod.A256CBC_HS512)) {
284
285                        plainText = AESCBC.decryptAuthenticated(
286                                cek,
287                                iv.decode(),
288                                cipherText.decode(),
289                                aad,
290                                authTag.decode(),
291                                jcaProvider.getContentEncryptionProvider(),
292                                jcaProvider.getMACProvider());
293
294                } else if (header.getEncryptionMethod().equals(EncryptionMethod.A128GCM) ||
295                        header.getEncryptionMethod().equals(EncryptionMethod.A192GCM) ||
296                        header.getEncryptionMethod().equals(EncryptionMethod.A256GCM)) {
297
298                        plainText = AESGCM.decrypt(
299                                cek,
300                                iv.decode(),
301                                cipherText.decode(),
302                                aad,
303                                authTag.decode(),
304                                jcaProvider.getContentEncryptionProvider());
305
306                } else if (header.getEncryptionMethod().equals(EncryptionMethod.A128CBC_HS256_DEPRECATED) ||
307                        header.getEncryptionMethod().equals(EncryptionMethod.A256CBC_HS512_DEPRECATED)) {
308
309                        plainText = AESCBC.decryptWithConcatKDF(
310                                header,
311                                cek,
312                                encryptedKey,
313                                iv,
314                                cipherText,
315                                authTag,
316                                jcaProvider.getContentEncryptionProvider(),
317                                jcaProvider.getMACProvider());
318
319                } else if (header.getEncryptionMethod().equals(EncryptionMethod.XC20P)) {
320
321                        plainText = XC20P.decryptAuthenticated(
322                                        cek,
323                                        iv.decode(),
324                                        cipherText.decode(),
325                                        aad,
326                                        authTag.decode()
327                        );
328
329                } else {
330                        throw new JOSEException(AlgorithmSupportMessage.unsupportedEncryptionMethod(
331                                header.getEncryptionMethod(),
332                                SUPPORTED_ENCRYPTION_METHODS));
333                }
334
335
336                // Apply decompression if requested
337                return DeflateHelper.applyDecompression(header, plainText);
338        }
339}