/*
* 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.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.security.util.EncryptionMethod;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.digests.SHA384Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PBKDF2CipherProvider extends RandomIVPBECipherProvider {
private static final Logger logger = LoggerFactory.getLogger(PBKDF2CipherProvider.class);
private static final int DEFAULT_SALT_LENGTH = 16;
private final int iterationCount;
private final Digest prf;
private static final String DEFAULT_PRF = "SHA-512";
/**
* This can be calculated automatically using the code {@see PBKDF2CipherProviderGroovyTest#calculateMinimumIterationCount} or manually updated by a maintainer
*/
private static final int DEFAULT_ITERATION_COUNT = 160_000;
/**
* Instantiates a PBKDF2 cipher provider with the default number of iterations and the default PRF. Currently 128,000 iterations and SHA-512.
*/
public PBKDF2CipherProvider() {
this(DEFAULT_PRF, DEFAULT_ITERATION_COUNT);
}
/**
* Instantiates a PBKDF2 cipher provider with the specified number of iterations and the specified PRF. Currently supports MD5, SHA1, SHA256, SHA384, and SHA512. Unknown PRFs will default to
* SHA512.
*
* @param prf a String representation of the PRF name, e.g. "SHA256", "SHA-384" "sha_512"
* @param iterationCount the number of iterations
*/
public PBKDF2CipherProvider(String prf, int iterationCount) {
this.iterationCount = iterationCount;
if (iterationCount < DEFAULT_ITERATION_COUNT) {
logger.warn("The provided iteration count {} is below the recommended minimum {}", iterationCount, DEFAULT_ITERATION_COUNT);
}
this.prf = resolvePRF(prf);
}
/**
* Returns an initialized cipher for the specified algorithm. The key is derived by the KDF of the implementation. 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 password the secret input
* @param salt the salt
* @param iv the IV
* @param keyLength the desired key length in bits
* @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, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception {
try {
return getInitializedCipher(encryptionMethod, password, salt, iv, keyLength, encryptMode);
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
throw new ProcessException("Error initializing the cipher", e);
}
}
@Override
Logger getLogger() {
return logger;
}
/**
* Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation.
*
* The IV can be retrieved by the calling method using {@link Cipher#getIV()}.
*
* @param encryptionMethod the {@link EncryptionMethod}
* @param password the secret input
* @param salt the salt
* @param keyLength the desired key length in bits
* @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, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception {
return getCipher(encryptionMethod, password, salt, new byte[0], keyLength, encryptMode);
}
protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception {
if (encryptionMethod == null) {
throw new IllegalArgumentException("The encryption method must be specified");
}
if (!encryptionMethod.isCompatibleWithStrongKDFs()) {
throw new IllegalArgumentException(encryptionMethod.name() + " is not compatible with PBKDF2");
}
String algorithm = encryptionMethod.getAlgorithm();
final String cipherName = CipherUtility.parseCipherFromAlgorithm(algorithm);
if (!CipherUtility.isValidKeyLength(keyLength, cipherName)) {
throw new IllegalArgumentException(String.valueOf(keyLength) + " is not a valid key length for " + cipherName);
}
if (StringUtils.isEmpty(password)) {
throw new IllegalArgumentException("Encryption with an empty password is not supported");
}
if (salt == null || salt.length < DEFAULT_SALT_LENGTH) {
throw new IllegalArgumentException("The salt must be at least " + DEFAULT_SALT_LENGTH + " bytes. To generate a salt, use PBKDF2CipherProvider#generateSalt()");
}
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(this.prf);
gen.init(password.getBytes(StandardCharsets.UTF_8), salt, getIterationCount());
byte[] dk = ((KeyParameter) gen.generateDerivedParameters(keyLength)).getKey();
SecretKey tempKey = new SecretKeySpec(dk, algorithm);
KeyedCipherProvider keyedCipherProvider = new AESKeyedCipherProvider();
return keyedCipherProvider.getCipher(encryptionMethod, tempKey, iv, encryptMode);
}
@Override
public byte[] generateSalt() {
byte[] salt = new byte[DEFAULT_SALT_LENGTH];
new SecureRandom().nextBytes(salt);
return salt;
}
@Override
public int getDefaultSaltLength() {
return DEFAULT_SALT_LENGTH;
}
protected int getIterationCount() {
return iterationCount;
}
protected String getPRFName() {
if (prf != null) {
return prf.getAlgorithmName();
} else {
return "No PRF enabled";
}
}
private Digest resolvePRF(final String prf) {
if (StringUtils.isEmpty(prf)) {
throw new IllegalArgumentException("Cannot resolve empty PRF");
}
String formattedPRF = prf.toLowerCase().replaceAll("[\\W]+", "");
logger.debug("Resolved PRF {} to {}", prf, formattedPRF);
switch (formattedPRF) {
case "md5":
return new MD5Digest();
case "sha1":
return new SHA1Digest();
case "sha384":
return new SHA384Digest();
case "sha256":
return new SHA256Digest();
case "sha512":
return new SHA512Digest();
default:
logger.warn("Could not resolve PRF {}. Using default PRF {} instead", prf, DEFAULT_PRF);
return new SHA512Digest();
}
}
}