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}