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}