/*
* 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.provenance;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider;
import org.apache.nifi.util.NiFiProperties;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CryptoUtils {
private static final Logger logger = LoggerFactory.getLogger(StaticKeyProvider.class);
private static final String STATIC_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.provenance.StaticKeyProvider";
private static final String FILE_BASED_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.provenance.FileBasedKeyProvider";
private static final Pattern HEX_PATTERN = Pattern.compile("(?i)^[0-9a-f]+$");
private static final List<Integer> UNLIMITED_KEY_LENGTHS = Arrays.asList(32, 48, 64);
public static final int IV_LENGTH = 16;
public static boolean isUnlimitedStrengthCryptoAvailable() {
try {
return Cipher.getMaxAllowedKeyLength("AES") > 128;
} catch (NoSuchAlgorithmException e) {
logger.warn("Tried to determine if unlimited strength crypto is available but the AES algorithm is not available");
return false;
}
}
/**
* Utility method which returns true if the string is null, empty, or entirely whitespace.
*
* @param src the string to evaluate
* @return true if empty
*/
public static boolean isEmpty(String src) {
return src == null || src.trim().isEmpty();
}
/**
* Concatenates multiple byte[] into a single byte[].
*
* @param arrays the component byte[] in order
* @return a concatenated byte[]
* @throws IOException this should never be thrown
*/
public static byte[] concatByteArrays(byte[]... arrays) throws IOException {
int totalByteLength = 0;
for (byte[] bytes : arrays) {
totalByteLength += bytes.length;
}
byte[] totalBytes = new byte[totalByteLength];
int currentLength = 0;
for (byte[] bytes : arrays) {
System.arraycopy(bytes, 0, totalBytes, currentLength, bytes.length);
currentLength += bytes.length;
}
return totalBytes;
}
/**
* Returns true if the provided configuration values successfully define the specified {@link KeyProvider}.
*
* @param keyProviderImplementation the FQ class name of the {@link KeyProvider} implementation
* @param keyProviderLocation the location of the definition (for {@link FileBasedKeyProvider}, etc.)
* @param keyId the active key ID
* @param encryptionKeys a map of key IDs to key material in hex format
* @return true if the provided configuration is valid
*/
public static boolean isValidKeyProvider(String keyProviderImplementation, String keyProviderLocation, String keyId, Map<String, String> encryptionKeys) {
if (STATIC_KEY_PROVIDER_CLASS_NAME.equals(keyProviderImplementation)) {
// Ensure the keyId and key(s) are valid
if (encryptionKeys == null) {
return false;
} else {
boolean everyKeyValid = encryptionKeys.values().stream().allMatch(CryptoUtils::keyIsValid);
return everyKeyValid && StringUtils.isNotEmpty(keyId);
}
} else if (FILE_BASED_KEY_PROVIDER_CLASS_NAME.equals(keyProviderImplementation)) {
// Ensure the file can be read and the keyId is populated (does not read file to validate)
final File kpf = new File(keyProviderLocation);
return kpf.exists() && kpf.canRead() && StringUtils.isNotEmpty(keyId);
} else {
logger.error("The attempt to validate the key provider failed keyProviderImplementation = "
+ keyProviderImplementation + " , keyProviderLocation = "
+ keyProviderLocation + " , keyId = "
+ keyId + " , encryptionKeys = "
+ ((encryptionKeys == null) ? "0" : encryptionKeys.size()));
return false;
}
}
/**
* Returns true if the provided key is valid hex and is the correct length for the current system's JCE policies.
*
* @param encryptionKeyHex the key in hexadecimal
* @return true if this key is valid
*/
public static boolean keyIsValid(String encryptionKeyHex) {
return isHexString(encryptionKeyHex)
&& (isUnlimitedStrengthCryptoAvailable()
? UNLIMITED_KEY_LENGTHS.contains(encryptionKeyHex.length())
: encryptionKeyHex.length() == 32);
}
/**
* Returns true if the input is valid hexadecimal (does not enforce length and is case-insensitive).
*
* @param hexString the string to evaluate
* @return true if the string is valid hex
*/
public static boolean isHexString(String hexString) {
return StringUtils.isNotEmpty(hexString) && HEX_PATTERN.matcher(hexString).matches();
}
/**
* Returns a {@link SecretKey} formed from the hexadecimal key bytes (validity is checked).
*
* @param keyHex the key in hex form
* @return the SecretKey
*/
public static SecretKey formKeyFromHex(String keyHex) throws KeyManagementException {
if (keyIsValid(keyHex)) {
return new SecretKeySpec(Hex.decode(keyHex), "AES");
} else {
throw new KeyManagementException("The provided key material is not valid");
}
}
/**
* Returns a map containing the key IDs and the parsed key from a key provider definition file.
* The values in the file are decrypted using the master key provided. If the file is missing or empty,
* cannot be read, or if no valid keys are read, a {@link KeyManagementException} will be thrown.
*
* @param filepath the key definition file path
* @param masterKey the master key used to decrypt each key definition
* @return a Map of key IDs to SecretKeys
* @throws KeyManagementException if the file is missing or invalid
*/
public static Map<String, SecretKey> readKeys(String filepath, SecretKey masterKey) throws KeyManagementException {
Map<String, SecretKey> keys = new HashMap<>();
if (StringUtils.isBlank(filepath)) {
throw new KeyManagementException("The key provider file is not present and readable");
}
File file = new File(filepath);
if (!file.exists() || !file.canRead()) {
throw new KeyManagementException("The key provider file is not present and readable");
}
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
AESKeyedCipherProvider masterCipherProvider = new AESKeyedCipherProvider();
String line;
int l = 1;
while ((line = br.readLine()) != null) {
String[] components = line.split("=", 2);
if (components.length != 2 || StringUtils.isAnyEmpty(components)) {
logger.warn("Line " + l + " is not properly formatted -- keyId=Base64EncodedKey...");
}
String keyId = components[0];
if (StringUtils.isNotEmpty(keyId)) {
try {
byte[] base64Bytes = Base64.getDecoder().decode(components[1]);
byte[] ivBytes = Arrays.copyOfRange(base64Bytes, 0, IV_LENGTH);
Cipher masterCipher = null;
try {
masterCipher = masterCipherProvider.getCipher(EncryptionMethod.AES_GCM, masterKey, ivBytes, false);
} catch (Exception e) {
throw new KeyManagementException("Error building cipher to decrypt FileBaseKeyProvider definition at " + filepath, e);
}
byte[] individualKeyBytes = masterCipher.doFinal(Arrays.copyOfRange(base64Bytes, IV_LENGTH, base64Bytes.length));
SecretKey key = new SecretKeySpec(individualKeyBytes, "AES");
logger.debug("Read and decrypted key for " + keyId);
if (keys.containsKey(keyId)) {
logger.warn("Multiple key values defined for " + keyId + " -- using most recent value");
}
keys.put(keyId, key);
} catch (IllegalArgumentException e) {
logger.error("Encountered an error decoding Base64 for " + keyId + ": " + e.getLocalizedMessage());
} catch (BadPaddingException | IllegalBlockSizeException e) {
logger.error("Encountered an error decrypting key for " + keyId + ": " + e.getLocalizedMessage());
}
}
l++;
}
if (keys.isEmpty()) {
throw new KeyManagementException("The provided file contained no valid keys");
}
logger.info("Read " + keys.size() + " keys from FileBasedKeyProvider " + filepath);
return keys;
} catch (IOException e) {
throw new KeyManagementException("Error reading FileBasedKeyProvider definition at " + filepath, e);
}
}
public static boolean isProvenanceRepositoryEncryptionConfigured(NiFiProperties niFiProperties) {
final String implementationClassName = niFiProperties.getProperty(NiFiProperties.PROVENANCE_REPO_IMPLEMENTATION_CLASS);
// Referencing EWAPR.class.getName() would require a dependency on the module
boolean encryptedRepo = "org.apache.nifi.provenance.EncryptedWriteAheadProvenanceRepository".equals(implementationClassName);
boolean keyProviderConfigured = isValidKeyProvider(
niFiProperties.getProperty(NiFiProperties.PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_IMPLEMENTATION_CLASS),
niFiProperties.getProperty(NiFiProperties.PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_LOCATION),
niFiProperties.getProvenanceRepoEncryptionKeyId(),
niFiProperties.getProvenanceRepoEncryptionKeys());
return encryptedRepo && keyProviderConfigured;
}
}