/*
* 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.properties;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Stream;
import javax.crypto.Cipher;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.util.NiFiProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class NiFiPropertiesLoader {
private static final Logger logger = LoggerFactory.getLogger(NiFiPropertiesLoader.class);
private static final String RELATIVE_PATH = "conf/nifi.properties";
private static final String BOOTSTRAP_KEY_PREFIX = "nifi.bootstrap.sensitive.key=";
private NiFiProperties instance;
private String keyHex;
// Future enhancement: allow for external registration of new providers
private static SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
public NiFiPropertiesLoader() {
}
/**
* Returns an instance of the loader configured with the key.
* <p>
* <p>
* NOTE: This method is used reflectively by the process which starts NiFi
* so changes to it must be made in conjunction with that mechanism.</p>
*
* @param keyHex the key used to encrypt any sensitive properties
* @return the configured loader
*/
public static NiFiPropertiesLoader withKey(String keyHex) {
NiFiPropertiesLoader loader = new NiFiPropertiesLoader();
loader.setKeyHex(keyHex);
return loader;
}
/**
* Sets the hexadecimal key used to unprotect properties encrypted with
* {@link AESSensitivePropertyProvider}. If the key has already been set,
* calling this method will throw a {@link RuntimeException}.
*
* @param keyHex the key in hexadecimal format
*/
public void setKeyHex(String keyHex) {
if (this.keyHex == null || this.keyHex.trim().isEmpty()) {
this.keyHex = keyHex;
} else {
throw new RuntimeException("Cannot overwrite an existing key");
}
}
/**
* Returns a {@link NiFiProperties} instance with any encrypted properties
* decrypted using the key from the {@code conf/bootstrap.conf} file. This
* method is exposed to allow Spring factory-method loading at application
* startup.
*
* @return the populated and decrypted NiFiProperties instance
* @throws IOException if there is a problem reading from the bootstrap.conf
* or nifi.properties files
*/
public static NiFiProperties loadDefaultWithKeyFromBootstrap() throws IOException {
try {
String keyHex = extractKeyFromBootstrapFile();
return NiFiPropertiesLoader.withKey(keyHex).loadDefault();
} catch (IOException e) {
logger.error("Encountered an exception loading the default nifi.properties file {} with the key provided in bootstrap.conf", getDefaultFilePath(), e);
throw e;
}
}
/**
* Returns the key (if any) used to encrypt sensitive properties, extracted from {@code $NIFI_HOME/conf/bootstrap.conf}.
*
* @return the key in hexadecimal format
* @throws IOException if the file is not readable
*/
public static String extractKeyFromBootstrapFile() throws IOException {
return extractKeyFromBootstrapFile("");
}
/**
* Returns the key (if any) used to encrypt sensitive properties, extracted from {@code $NIFI_HOME/conf/bootstrap.conf}.
*
* @param bootstrapPath the path to the bootstrap file
* @return the key in hexadecimal format
* @throws IOException if the file is not readable
*/
public static String extractKeyFromBootstrapFile(String bootstrapPath) throws IOException {
File expectedBootstrapFile;
if (StringUtils.isBlank(bootstrapPath)) {
// Guess at location of bootstrap.conf file from nifi.properties file
String defaultNiFiPropertiesPath = getDefaultFilePath();
File propertiesFile = new File(defaultNiFiPropertiesPath);
File confDir = new File(propertiesFile.getParent());
if (confDir.exists() && confDir.canRead()) {
expectedBootstrapFile = new File(confDir, "bootstrap.conf");
} else {
logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key -- conf/ directory is missing or permissions are incorrect", confDir.getAbsolutePath());
throw new IOException("Cannot read from bootstrap.conf");
}
} else {
expectedBootstrapFile = new File(bootstrapPath);
}
if (expectedBootstrapFile.exists() && expectedBootstrapFile.canRead()) {
try (Stream<String> stream = Files.lines(Paths.get(expectedBootstrapFile.getAbsolutePath()))) {
Optional<String> keyLine = stream.filter(l -> l.startsWith(BOOTSTRAP_KEY_PREFIX)).findFirst();
if (keyLine.isPresent()) {
return keyLine.get().split("=", 2)[1];
} else {
logger.warn("No encryption key present in the bootstrap.conf file at {}", expectedBootstrapFile.getAbsolutePath());
return "";
}
} catch (IOException e) {
logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key", expectedBootstrapFile.getAbsolutePath());
throw new IOException("Cannot read from bootstrap.conf", e);
}
} else {
logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key -- file is missing or permissions are incorrect", expectedBootstrapFile.getAbsolutePath());
throw new IOException("Cannot read from bootstrap.conf");
}
}
private static String getDefaultFilePath() {
String systemPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH);
if (systemPath == null || systemPath.trim().isEmpty()) {
logger.warn("The system variable {} is not set, so it is being set to '{}'", NiFiProperties.PROPERTIES_FILE_PATH, RELATIVE_PATH);
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, RELATIVE_PATH);
systemPath = RELATIVE_PATH;
}
logger.info("Determined default nifi.properties path to be '{}'", systemPath);
return systemPath;
}
private NiFiProperties loadDefault() {
return load(getDefaultFilePath());
}
private static String getDefaultProviderKey() {
try {
return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? "256" : "128");
} catch (NoSuchAlgorithmException e) {
return "aes/gcm/128";
}
}
private void initializeSensitivePropertyProviderFactory() {
sensitivePropertyProviderFactory = new AESSensitivePropertyProviderFactory(keyHex);
}
private SensitivePropertyProvider getSensitivePropertyProvider() {
initializeSensitivePropertyProviderFactory();
return sensitivePropertyProviderFactory.getProvider();
}
/**
* Returns a {@link ProtectedNiFiProperties} instance loaded from the
* serialized form in the file. Responsible for actually reading from disk
* and deserializing the properties. Returns a protected instance to allow
* for decryption operations.
*
* @param file the file containing serialized properties
* @return the ProtectedNiFiProperties instance
*/
ProtectedNiFiProperties readProtectedPropertiesFromDisk(File file) {
if (file == null || !file.exists() || !file.canRead()) {
String path = (file == null ? "missing file" : file.getAbsolutePath());
logger.error("Cannot read from '{}' -- file is missing or not readable", path);
throw new IllegalArgumentException("NiFi properties file missing or unreadable");
}
Properties rawProperties = new Properties();
InputStream inStream = null;
try {
inStream = new BufferedInputStream(new FileInputStream(file));
rawProperties.load(inStream);
logger.info("Loaded {} properties from {}", rawProperties.size(), file.getAbsolutePath());
ProtectedNiFiProperties protectedNiFiProperties = new ProtectedNiFiProperties(rawProperties);
return protectedNiFiProperties;
} catch (final Exception ex) {
logger.error("Cannot load properties file due to " + ex.getLocalizedMessage());
throw new RuntimeException("Cannot load properties file due to "
+ ex.getLocalizedMessage(), ex);
} finally {
if (null != inStream) {
try {
inStream.close();
} catch (final Exception ex) {
/**
* do nothing *
*/
}
}
}
}
/**
* Returns an instance of {@link NiFiProperties} loaded from the provided
* {@link File}. If any properties are protected, will attempt to use the
* appropriate {@link SensitivePropertyProvider} to unprotect them
* transparently.
*
* @param file the File containing the serialized properties
* @return the NiFiProperties instance
*/
public NiFiProperties load(File file) {
ProtectedNiFiProperties protectedNiFiProperties = readProtectedPropertiesFromDisk(file);
if (protectedNiFiProperties.hasProtectedKeys()) {
Security.addProvider(new BouncyCastleProvider());
protectedNiFiProperties.addSensitivePropertyProvider(getSensitivePropertyProvider());
}
return protectedNiFiProperties.getUnprotectedProperties();
}
/**
* Returns an instance of {@link NiFiProperties}. If the path is empty, this
* will load the default properties file as specified by
* {@code NiFiProperties.PROPERTY_FILE_PATH}.
*
* @param path the path of the serialized properties file
* @return the NiFiProperties instance
* @see NiFiPropertiesLoader#load(File)
*/
public NiFiProperties load(String path) {
if (path != null && !path.trim().isEmpty()) {
return load(new File(path));
} else {
return loadDefault();
}
}
/**
* Returns the loaded {@link NiFiProperties} instance. If none is currently
* loaded, attempts to load the default instance.
* <p>
* <p>
* NOTE: This method is used reflectively by the process which starts NiFi
* so changes to it must be made in conjunction with that mechanism.</p>
*
* @return the current NiFiProperties instance
*/
public NiFiProperties get() {
if (instance == null) {
instance = loadDefault();
}
return instance;
}
}