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 plainText   The plain text. Must not be {@code null}.
084         * @param ivContainer The initialisation vector (IV). Must not be
085         *                    {@code null}. This is both input and output
086         *                    parameter. On input, it carries externally
087         *                    generated IV; on output, it carries the IV the
088         *                    cipher actually used. JCA/JCE providers may
089         *                    prefer to use an internally generated IV, e.g. as
090         *                    described in
091         *                    <a href="http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf">NIST
092         *                    Special Publication 800-38D </a>.
093         * @param authData    The authenticated data. Must not be {@code null}.
094         *
095         * @return The authenticated cipher text.
096         *
097         * @throws JOSEException If encryption failed.
098         */
099        public static AuthenticatedCipherText encrypt(final SecretKey secretKey,
100                                                      final Container<byte[]> ivContainer,
101                                                      final byte[] plainText,
102                                                      final byte[] authData,
103                                                      final Provider provider)
104                throws JOSEException {
105
106                // Key alg must be "AES"
107                final SecretKey aesKey = KeyUtils.toAESKey(secretKey);
108                
109                Cipher cipher;
110
111                byte[] iv = ivContainer.get();
112
113                try {
114                        if (provider != null) {
115                                cipher = Cipher.getInstance("AES/GCM/NoPadding", provider);
116                        } else {
117                                cipher = Cipher.getInstance("AES/GCM/NoPadding");
118                        }
119
120                        GCMParameterSpec gcmSpec = new GCMParameterSpec(AUTH_TAG_BIT_LENGTH, iv);
121                        cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
122
123                } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
124                        throw new JOSEException("Couldn't create AES/GCM/NoPadding cipher: " + e.getMessage(), e);
125                }
126
127                cipher.updateAAD(authData);
128
129                byte[] cipherOutput;
130                try {
131                        cipherOutput = cipher.doFinal(plainText);
132                } catch (IllegalBlockSizeException | BadPaddingException e) {
133                        throw new JOSEException("Couldn't encrypt with AES/GCM/NoPadding: " + e.getMessage(), e);
134                }
135
136                final int tagPos = cipherOutput.length - ByteUtils.byteLength(AUTH_TAG_BIT_LENGTH);
137
138                byte[] cipherText = ByteUtils.subArray(cipherOutput, 0, tagPos);
139                byte[] authTag = ByteUtils.subArray(cipherOutput, tagPos, ByteUtils.byteLength(AUTH_TAG_BIT_LENGTH));
140
141                // retrieve the actual IV used by the cipher -- it may be internally-generated.
142                ivContainer.set(actualIVOf(cipher));
143
144                return new AuthenticatedCipherText(cipherText, authTag);
145        }
146
147        
148        /**
149         * Retrieves the actual algorithm parameters and validates them.
150         *
151         * @param cipher The cipher to interrogate for the parameters it
152         *               actually used.
153         *
154         * @return The IV used by the specified cipher.
155         *
156         * @throws JOSEException If retrieval of the algorithm parameters from
157         *                       the cipher failed, or the parameters are
158         *                       deemed unusable.
159         *
160         * @see #actualParamsOf(Cipher)
161         * @see #validate(byte[], int)
162         */
163        private static byte[] actualIVOf(final Cipher cipher)
164                throws JOSEException {
165                
166                GCMParameterSpec actualParams = actualParamsOf(cipher);
167
168                byte[] iv = actualParams.getIV();
169                int tLen = actualParams.getTLen();
170
171                validate(iv, tLen);
172
173                return iv;
174        }
175
176        
177        /**
178         * Validates the specified IV and authentication tag according to the
179         * AES GCM requirements in
180         * <a href="https://tools.ietf.org/html/rfc7518#section-5.3">JWA RFC</a>.
181         *
182         * @param iv            The IV to check for compliance.
183         * @param authTagLength The authentication tag length to check for
184         *                      compliance.
185         *
186         * @throws JOSEException If the parameters don't match the JWA
187         *                       requirements.
188         *
189         * @see #IV_BIT_LENGTH
190         * @see #AUTH_TAG_BIT_LENGTH
191         */
192        private static void validate(final byte[] iv, final int authTagLength)
193                throws JOSEException {
194                
195                if (ByteUtils.safeBitLength(iv) != IV_BIT_LENGTH) {
196                        throw new JOSEException(String.format("IV length of %d bits is required, got %d", IV_BIT_LENGTH, ByteUtils.safeBitLength(iv)));
197                }
198
199                if (authTagLength != AUTH_TAG_BIT_LENGTH) {
200                        throw new JOSEException(String.format("Authentication tag length of %d bits is required, got %d", AUTH_TAG_BIT_LENGTH, authTagLength));
201                }
202        }
203
204        
205        /**
206         * Retrieves the actual AES GCM parameters used by the specified
207         * cipher.
208         *
209         * @param cipher The cipher to interrogate. Non-{@code null}.
210         *
211         * @return The AES GCM parameters. Non-{@code null}.
212         *
213         * @throws JOSEException If the parameters cannot be retrieved, are
214         * uninitialized, or are not in the correct form. We want to have the
215         * actual parameters used by the cipher and not rely on the assumption
216         * that they were the same as those we supplied it with. If at runtime
217         * the assumption is incorrect, the ciphertext would not be
218         * decryptable.
219         */
220        private static GCMParameterSpec actualParamsOf(final Cipher cipher)
221                throws JOSEException {
222                
223                AlgorithmParameters algorithmParameters = cipher.getParameters();
224                
225                if (algorithmParameters == null) {
226                        throw new JOSEException("AES GCM ciphers are expected to make use of algorithm parameters");
227                }
228
229                try {
230                        // Note: GCMParameterSpec appears in Java 7
231                        return algorithmParameters.getParameterSpec(GCMParameterSpec.class);
232                } catch (InvalidParameterSpecException shouldNotHappen) {
233                        throw new JOSEException(shouldNotHappen.getMessage(), shouldNotHappen);
234                }
235        }
236
237        
238        /**
239         * Decrypts the specified cipher text using AES/GCM/NoPadding.
240         *
241         * @param secretKey  The AES key. Must not be {@code null}.
242         * @param iv         The initialisation vector (IV). Must not be
243         *                   {@code null}.
244         * @param cipherText The cipher text. Must not be {@code null}.
245         * @param authData   The authenticated data. Must not be {@code null}.
246         * @param authTag    The authentication tag. Must not be {@code null}.
247         *
248         * @return The decrypted plain text.
249         *
250         * @throws JOSEException If decryption failed.
251         */
252        public static byte[] decrypt(final SecretKey secretKey, 
253                                     final byte[] iv,
254                                     final byte[] cipherText,
255                                     final byte[] authData,
256                                     final byte[] authTag,
257                                     final Provider provider)
258                throws JOSEException {
259                
260                // Key alg must be "AES"
261                final SecretKey aesKey = KeyUtils.toAESKey(secretKey);
262                
263                Cipher cipher;
264                try {
265                        if (provider != null) {
266                                cipher = Cipher.getInstance("AES/GCM/NoPadding", provider);
267                        } else {
268                                cipher = Cipher.getInstance("AES/GCM/NoPadding");
269                        }
270
271                        GCMParameterSpec gcmSpec = new GCMParameterSpec(AUTH_TAG_BIT_LENGTH, iv);
272                        cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec);
273
274                } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
275                        throw new JOSEException("Couldn't create AES/GCM/NoPadding cipher: " + e.getMessage(), e);
276                }
277
278                cipher.updateAAD(authData);
279
280                try {
281                        return cipher.doFinal(ByteUtils.concat(cipherText, authTag));
282                } catch (IllegalBlockSizeException | BadPaddingException e) {
283                        throw new JOSEException("AES/GCM/NoPadding decryption failed: " + e.getMessage(), e);
284                }
285        }
286
287
288        /**
289         * Prevents public instantiation.
290         */
291        private AESGCM() { }
292}