001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2024, 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 com.nimbusds.jose.JOSEException;
022import com.nimbusds.jose.util.ByteUtils;
023import com.nimbusds.jose.util.Container;
024import com.nimbusds.jose.util.KeyUtils;
025import net.jcip.annotations.ThreadSafe;
026
027import javax.crypto.*;
028import javax.crypto.spec.GCMParameterSpec;
029import java.security.*;
030import java.security.spec.InvalidParameterSpecException;
031
032
033/**
034 * AES/GSM/NoPadding encryption and decryption methods. Falls back to the
035 * BouncyCastle.org provider on Java 6. This class is thread-safe.
036 *
037 * <p>See RFC 7518 (JWA), section 5.1 and appendix 3.
038 *
039 * @author Vladimir Dzhuvinov
040 * @author Axel Nennker
041 * @author Dimitar A. Stoikov
042 * @version 2024-01-01
043 */
044@ThreadSafe
045public class AESGCM {
046
047
048        /**
049         * The standard Initialisation Vector (IV) length (96 bits).
050         */
051        public static final int IV_BIT_LENGTH = 96;
052
053
054        /**
055         * The standard authentication tag length (128 bits).
056         */
057        public static final int AUTH_TAG_BIT_LENGTH = 128;
058
059
060        /**
061         * Generates a random 96 bit (12 byte) Initialisation Vector(IV) for
062         * use in AES-GCM encryption.
063         *
064         * <p>See RFC 7518 (JWA), section 5.3.
065         *
066         * @param randomGen The secure random generator to use. Must be 
067         *                  correctly initialised and not {@code null}.
068         *
069         * @return The random 96 bit IV, as 12 byte array.
070         */
071        public static byte[] generateIV(final SecureRandom randomGen) {
072                
073                byte[] bytes = new byte[IV_BIT_LENGTH / 8];
074                randomGen.nextBytes(bytes);
075                return bytes;
076        }
077
078
079        /**
080         * Encrypts the specified plain text using AES/GCM/NoPadding.
081         *
082         * @param secretKey   The AES key. Must not be {@code null}.
083         * @param ivContainer The initialisation vector (IV). Must not be
084         *                    {@code null}. This is both input and output
085         *                    parameter. On input, it carries externally
086         *                    generated IV; on output, it carries the IV the
087         *                    cipher actually used. JCA/JCE providers may
088         *                    prefer to use an internally generated IV, e.g. as
089         *                    described in
090         *                    <a href="http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf">NIST
091         *                    Special Publication 800-38D </a>.
092         * @param plainText   The plain text. Must not be {@code null}.
093         * @param authData    The authenticated data. Must not be {@code null}.
094         * @param provider    The JCA provider to use, {@code null} implies the
095         *                    default.
096         *
097         * @return The authenticated cipher text.
098         *
099         * @throws JOSEException If encryption failed.
100         */
101        public static AuthenticatedCipherText encrypt(final SecretKey secretKey,
102                                                      final Container<byte[]> ivContainer,
103                                                      final byte[] plainText,
104                                                      final byte[] authData,
105                                                      final Provider provider)
106                throws JOSEException {
107
108                // Key alg must be "AES"
109                final SecretKey aesKey = KeyUtils.toAESKey(secretKey);
110                
111                Cipher cipher;
112
113                byte[] iv = ivContainer.get();
114
115                try {
116                        if (provider != null) {
117                                cipher = Cipher.getInstance("AES/GCM/NoPadding", provider);
118                        } else {
119                                cipher = Cipher.getInstance("AES/GCM/NoPadding");
120                        }
121
122                        GCMParameterSpec gcmSpec = new GCMParameterSpec(AUTH_TAG_BIT_LENGTH, iv);
123                        cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
124
125                } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
126                        throw new JOSEException("Couldn't create AES/GCM/NoPadding cipher: " + e.getMessage(), e);
127                }
128
129                cipher.updateAAD(authData);
130
131                byte[] cipherOutput;
132                try {
133                        cipherOutput = cipher.doFinal(plainText);
134                } catch (IllegalBlockSizeException | BadPaddingException e) {
135                        throw new JOSEException("Couldn't encrypt with AES/GCM/NoPadding: " + e.getMessage(), e);
136                }
137
138                final int tagPos = cipherOutput.length - ByteUtils.byteLength(AUTH_TAG_BIT_LENGTH);
139
140                byte[] cipherText = ByteUtils.subArray(cipherOutput, 0, tagPos);
141                byte[] authTag = ByteUtils.subArray(cipherOutput, tagPos, ByteUtils.byteLength(AUTH_TAG_BIT_LENGTH));
142
143                // retrieve the actual IV used by the cipher -- it may be internally-generated.
144                ivContainer.set(actualIVOf(cipher));
145
146                return new AuthenticatedCipherText(cipherText, authTag);
147        }
148
149        
150        /**
151         * Retrieves the actual algorithm parameters and validates them.
152         *
153         * @param cipher The cipher to interrogate for the parameters it
154         *               actually used.
155         *
156         * @return The IV used by the specified cipher.
157         *
158         * @throws JOSEException If retrieval of the algorithm parameters from
159         *                       the cipher failed, or the parameters are
160         *                       deemed unusable.
161         *
162         * @see #actualParamsOf(Cipher)
163         * @see #validate(byte[], int)
164         */
165        private static byte[] actualIVOf(final Cipher cipher)
166                throws JOSEException {
167                
168                GCMParameterSpec actualParams = actualParamsOf(cipher);
169
170                byte[] iv = actualParams.getIV();
171                int tLen = actualParams.getTLen();
172
173                validate(iv, tLen);
174
175                return iv;
176        }
177
178        
179        /**
180         * Validates the specified IV and authentication tag according to the
181         * AES GCM requirements in
182         * <a href="https://tools.ietf.org/html/rfc7518#section-5.3">JWA RFC</a>.
183         *
184         * @param iv            The IV to check for compliance.
185         * @param authTagLength The authentication tag length to check for
186         *                      compliance.
187         *
188         * @throws JOSEException If the parameters don't match the JWA
189         *                       requirements.
190         *
191         * @see #IV_BIT_LENGTH
192         * @see #AUTH_TAG_BIT_LENGTH
193         */
194        private static void validate(final byte[] iv, final int authTagLength)
195                throws JOSEException {
196                
197                if (ByteUtils.safeBitLength(iv) != IV_BIT_LENGTH) {
198                        throw new JOSEException(String.format("IV length of %d bits is required, got %d", IV_BIT_LENGTH, ByteUtils.safeBitLength(iv)));
199                }
200
201                if (authTagLength != AUTH_TAG_BIT_LENGTH) {
202                        throw new JOSEException(String.format("Authentication tag length of %d bits is required, got %d", AUTH_TAG_BIT_LENGTH, authTagLength));
203                }
204        }
205
206        
207        /**
208         * Retrieves the actual AES GCM parameters used by the specified
209         * cipher.
210         *
211         * @param cipher The cipher to interrogate. Non-{@code null}.
212         *
213         * @return The AES GCM parameters. Non-{@code null}.
214         *
215         * @throws JOSEException If the parameters cannot be retrieved, are
216         * uninitialized, or are not in the correct form. We want to have the
217         * actual parameters used by the cipher and not rely on the assumption
218         * that they were the same as those we supplied it with. If at runtime
219         * the assumption is incorrect, the ciphertext would not be
220         * decryptable.
221         */
222        private static GCMParameterSpec actualParamsOf(final Cipher cipher)
223                throws JOSEException {
224                
225                AlgorithmParameters algorithmParameters = cipher.getParameters();
226                
227                if (algorithmParameters == null) {
228                        throw new JOSEException("AES GCM ciphers are expected to make use of algorithm parameters");
229                }
230
231                try {
232                        // Note: GCMParameterSpec appears in Java 7
233                        return algorithmParameters.getParameterSpec(GCMParameterSpec.class);
234                } catch (InvalidParameterSpecException shouldNotHappen) {
235                        throw new JOSEException(shouldNotHappen.getMessage(), shouldNotHappen);
236                }
237        }
238
239        
240        /**
241         * Decrypts the specified cipher text using AES/GCM/NoPadding.
242         *
243         * @param secretKey  The AES key. Must not be {@code null}.
244         * @param iv         The initialisation vector (IV). Must not be
245         *                   {@code null}.
246         * @param cipherText The cipher text. Must not be {@code null}.
247         * @param authData   The authenticated data. Must not be {@code null}.
248         * @param authTag    The authentication tag. Must not be {@code null}.
249         * @param provider   The JCA provider to use, {@code null} implies the
250         *                   default.
251         *
252         * @return The decrypted plain text.
253         *
254         * @throws JOSEException If decryption failed.
255         */
256        public static byte[] decrypt(final SecretKey secretKey, 
257                                     final byte[] iv,
258                                     final byte[] cipherText,
259                                     final byte[] authData,
260                                     final byte[] authTag,
261                                     final Provider provider)
262                throws JOSEException {
263                
264                // Key alg must be "AES"
265                final SecretKey aesKey = KeyUtils.toAESKey(secretKey);
266                
267                Cipher cipher;
268                try {
269                        if (provider != null) {
270                                cipher = Cipher.getInstance("AES/GCM/NoPadding", provider);
271                        } else {
272                                cipher = Cipher.getInstance("AES/GCM/NoPadding");
273                        }
274
275                        GCMParameterSpec gcmSpec = new GCMParameterSpec(AUTH_TAG_BIT_LENGTH, iv);
276                        cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec);
277
278                } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
279                        throw new JOSEException("Couldn't create AES/GCM/NoPadding cipher: " + e.getMessage(), e);
280                }
281
282                cipher.updateAAD(authData);
283
284                try {
285                        return cipher.doFinal(ByteUtils.concat(cipherText, authTag));
286                } catch (IllegalBlockSizeException | BadPaddingException e) {
287                        throw new JOSEException("AES/GCM/NoPadding decryption failed: " + e.getMessage(), e);
288                }
289        }
290
291
292        /**
293         * Prevents public instantiation.
294         */
295        private AESGCM() { }
296}