/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.nifi.security.util.crypto; import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Arrays; import java.util.List; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.security.util.EncryptionMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a standard implementation of {@link KeyedCipherProvider} which supports {@code AES} cipher families with arbitrary modes of operation (currently only {@code CBC}, {@code CTR}, and {@code * GCM} are supported as {@link EncryptionMethod}s. */ public class AESKeyedCipherProvider extends KeyedCipherProvider { private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProvider.class); private static final int IV_LENGTH = 16; private static final List<Integer> VALID_KEY_LENGTHS = Arrays.asList(128, 192, 256); /** * Returns an initialized cipher for the specified algorithm. The IV is provided externally to allow for non-deterministic IVs, as IVs * deterministically derived from the password are a potential vulnerability and compromise semantic security. See * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a> * * @param encryptionMethod the {@link EncryptionMethod} * @param key the key * @param iv the IV or nonce (cannot be all 0x00) * @param encryptMode true for encrypt, false for decrypt * @return the initialized cipher * @throws Exception if there is a problem initializing the cipher */ @Override public Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, boolean encryptMode) throws Exception { try { return getInitializedCipher(encryptionMethod, key, iv, encryptMode); } catch (IllegalArgumentException e) { throw e; } catch (Exception e) { throw new ProcessException("Error initializing the cipher", e); } } /** * Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception. * * @param encryptionMethod the {@link EncryptionMethod} * @param key the key * @param encryptMode true for encrypt, false for decrypt * @return the initialized cipher * @throws Exception if there is a problem initializing the cipher or if decryption is requested */ @Override public Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception { return getCipher(encryptionMethod, key, new byte[0], encryptMode); } protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, boolean encryptMode) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, UnsupportedEncodingException { if (encryptionMethod == null) { throw new IllegalArgumentException("The encryption method must be specified"); } if (!encryptionMethod.isKeyedCipher()) { throw new IllegalArgumentException(encryptionMethod.name() + " requires a PBECipherProvider"); } String algorithm = encryptionMethod.getAlgorithm(); String provider = encryptionMethod.getProvider(); if (key == null) { throw new IllegalArgumentException("The key must be specified"); } if (!isValidKeyLength(key)) { throw new IllegalArgumentException("The key must be of length [" + StringUtils.join(VALID_KEY_LENGTHS, ", ") + "]"); } Cipher cipher = Cipher.getInstance(algorithm, provider); final String operation = encryptMode ? "encrypt" : "decrypt"; boolean ivIsInvalid = false; // If an IV was not provided already, generate a random IV and inject it in the cipher int ivLength = cipher.getBlockSize(); if (iv.length != ivLength) { logger.warn("An IV was provided of length {} bytes for {}ion but should be {} bytes", iv.length, operation, ivLength); ivIsInvalid = true; } final byte[] emptyIv = new byte[ivLength]; if (Arrays.equals(iv, emptyIv)) { logger.warn("An empty IV was provided of length {} for {}ion", iv.length, operation); ivIsInvalid = true; } if (ivIsInvalid) { if (encryptMode) { logger.warn("Generating new IV. The value can be obtained in the calling code by invoking 'cipher.getIV()';"); iv = generateIV(); } else { // Can't decrypt without an IV throw new IllegalArgumentException("Cannot decrypt without a valid IV"); } } cipher.init(encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); return cipher; } private boolean isValidKeyLength(SecretKey key) { return VALID_KEY_LENGTHS.contains(key.getEncoded().length * 8); } /** * Generates a new random IV of 16 bytes using {@link java.security.SecureRandom}. * * @return the IV */ public byte[] generateIV() { byte[] iv = new byte[IV_LENGTH]; new SecureRandom().nextBytes(iv); return iv; } }