/* * 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; } }