1
0
mirror of https://github.com/apache/nifi.git synced 2025-02-13 21:45:26 +00:00

NIFI-9724 Added set-sensitive-properties-algorithm command

Signed-off-by: Joe Gresock <jgresock@gmail.com>

This closes .
This commit is contained in:
exceptionfactory 2022-02-24 22:10:02 -05:00 committed by Joe Gresock
parent a6aba3bf8e
commit 49d1c747ca
No known key found for this signature in database
GPG Key ID: 37F5B9B6E258C8B7
8 changed files with 518 additions and 272 deletions
nifi-commons/nifi-flow-encryptor/src
nifi-docs/src/main/asciidoc
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-resources/src/main/resources/bin

@ -0,0 +1,198 @@
/*
* 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.flow.encryptor.command;
import org.apache.nifi.encrypt.PropertyEncryptor;
import org.apache.nifi.encrypt.PropertyEncryptorBuilder;
import org.apache.nifi.flow.encryptor.FlowEncryptor;
import org.apache.nifi.flow.encryptor.StandardFlowEncryptor;
import org.apache.nifi.security.util.EncryptionMethod;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Flow Encryptor Command capable of updating Sensitive Properties Key or Algorithm as well as Flow Configuration
*/
class FlowEncryptorCommand implements Runnable {
protected static final String PROPERTIES_FILE_PATH = "nifi.properties.file.path";
protected static final String PROPS_KEY = "nifi.sensitive.props.key";
protected static final String PROPS_ALGORITHM = "nifi.sensitive.props.algorithm";
protected static final String CONFIGURATION_FILE = "nifi.flow.configuration.file";
protected static final String CONFIGURATION_JSON_FILE = "nifi.flow.configuration.json.file";
private static final List<String> CONFIGURATION_FILES = Arrays.asList(CONFIGURATION_FILE, CONFIGURATION_JSON_FILE);
private static final String FLOW_PREFIX = "nifi.flow.";
private static final String GZ_EXTENSION = ".gz";
private static final String DEFAULT_PROPERTIES_ALGORITHM = EncryptionMethod.MD5_256AES.getAlgorithm();
private static final String DEFAULT_PROPERTIES_KEY = "nififtw!";
private static final String SENSITIVE_PROPERTIES_KEY = String.format("%s=", PROPS_KEY);
private static final String SENSITIVE_PROPERTIES_ALGORITHM = String.format("%s=", PROPS_ALGORITHM);
private String requestedPropertiesKey;
private String requestedPropertiesAlgorithm;
void setRequestedPropertiesKey(final String requestedPropertiesKey) {
this.requestedPropertiesKey = Objects.requireNonNull(requestedPropertiesKey, "Key required");
}
void setRequestedPropertiesAlgorithm(final String requestedPropertiesAlgorithm) {
this.requestedPropertiesAlgorithm = Objects.requireNonNull(requestedPropertiesAlgorithm, "Algorithm required");
}
/**
* Run command using nifi.properties location read from System Properties
*/
@Override
public void run() {
final String propertiesFilePath = System.getProperty(PROPERTIES_FILE_PATH);
if (propertiesFilePath == null) {
throw new IllegalStateException(String.format("System property not defined [%s]", PROPERTIES_FILE_PATH));
}
final File propertiesFile = new File(propertiesFilePath);
final Properties properties = loadProperties(propertiesFile);
processFlowConfigurationFiles(properties);
try {
storeProperties(propertiesFile);
System.out.printf("NiFi Properties Processed [%s]%n", propertiesFilePath);
} catch (final IOException e) {
final String message = String.format("Failed to Process NiFi Properties [%s]", propertiesFilePath);
throw new UncheckedIOException(message, e);
}
}
private void processFlowConfigurationFiles(final Properties properties) {
final String outputAlgorithm = requestedPropertiesAlgorithm == null ? getAlgorithm(properties) : requestedPropertiesAlgorithm;
final String outputKey = requestedPropertiesKey == null ? getKey(properties) : requestedPropertiesKey;
final PropertyEncryptor outputEncryptor = getPropertyEncryptor(outputKey, outputAlgorithm);
for (final String configurationFilePropertyName : CONFIGURATION_FILES) {
final String configurationFileProperty = properties.getProperty(configurationFilePropertyName);
if (configurationFileProperty == null || configurationFileProperty.isEmpty()) {
System.out.printf("Flow Configuration Property not specified [%s]%n", configurationFileProperty);
} else {
final File configurationFile = new File(configurationFileProperty);
if (configurationFile.exists()) {
processFlowConfiguration(configurationFile, properties, outputEncryptor);
}
}
}
}
private void processFlowConfiguration(final File flowConfigurationFile, final Properties properties, final PropertyEncryptor outputEncryptor) {
try (final InputStream flowInputStream = new GZIPInputStream(new FileInputStream(flowConfigurationFile))) {
final File flowOutputFile = getFlowOutputFile();
final Path flowOutputPath = flowOutputFile.toPath();
try (final OutputStream flowOutputStream = new GZIPOutputStream(new FileOutputStream(flowOutputFile))) {
final String inputAlgorithm = getAlgorithm(properties);
final String inputPropertiesKey = getKey(properties);
final PropertyEncryptor inputEncryptor = getPropertyEncryptor(inputPropertiesKey, inputAlgorithm);
final FlowEncryptor flowEncryptor = new StandardFlowEncryptor();
flowEncryptor.processFlow(flowInputStream, flowOutputStream, inputEncryptor, outputEncryptor);
}
final Path flowConfigurationPath = flowConfigurationFile.toPath();
Files.move(flowOutputPath, flowConfigurationPath, StandardCopyOption.REPLACE_EXISTING);
System.out.printf("Flow Configuration Processed [%s]%n", flowConfigurationPath);
} catch (final IOException | RuntimeException e) {
System.err.printf("Failed to process Flow Configuration [%s]%n", flowConfigurationFile);
e.printStackTrace();
}
}
private String getAlgorithm(final Properties properties) {
String algorithm = properties.getProperty(PROPS_ALGORITHM, DEFAULT_PROPERTIES_ALGORITHM);
if (algorithm.length() == 0) {
algorithm = DEFAULT_PROPERTIES_ALGORITHM;
}
return algorithm;
}
private String getKey(final Properties properties) {
String key = properties.getProperty(PROPS_KEY, DEFAULT_PROPERTIES_KEY);
if (key.length() == 0) {
key = DEFAULT_PROPERTIES_KEY;
}
return key;
}
private File getFlowOutputFile() throws IOException {
final File flowOutputFile = File.createTempFile(FLOW_PREFIX, GZ_EXTENSION);
flowOutputFile.deleteOnExit();
return flowOutputFile;
}
private Properties loadProperties(final File propertiesFile) {
final Properties properties = new Properties();
try (final FileReader reader = new FileReader(propertiesFile)) {
properties.load(reader);
} catch (final IOException e) {
final String message = String.format("Failed to read NiFi Properties [%s]", propertiesFile);
throw new UncheckedIOException(message, e);
}
return properties;
}
private void storeProperties(final File propertiesFile) throws IOException {
final Path propertiesFilePath = propertiesFile.toPath();
final List<String> lines = Files.readAllLines(propertiesFilePath);
final List<String> updatedLines = lines.stream().map(line -> {
if (line.startsWith(SENSITIVE_PROPERTIES_KEY)) {
return requestedPropertiesKey == null ? line : SENSITIVE_PROPERTIES_KEY + requestedPropertiesKey;
} else if (line.startsWith(SENSITIVE_PROPERTIES_ALGORITHM)) {
return requestedPropertiesAlgorithm == null ? line : SENSITIVE_PROPERTIES_ALGORITHM + requestedPropertiesAlgorithm;
} else {
return line;
}
}).collect(Collectors.toList());
Files.write(propertiesFilePath, updatedLines);
}
private PropertyEncryptor getPropertyEncryptor(final String propertiesKey, final String propertiesAlgorithm) {
return new PropertyEncryptorBuilder(propertiesKey).setAlgorithm(propertiesAlgorithm).build();
}
}

@ -0,0 +1,35 @@
/*
* 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.flow.encryptor.command;
/**
* Set Sensitive Properties Algorithm for NiFi Properties and update encrypted Flow Configuration
*/
public class SetSensitivePropertiesAlgorithm {
public static void main(final String[] arguments) {
if (arguments.length == 1) {
final String algorithm = arguments[0];
final FlowEncryptorCommand command = new FlowEncryptorCommand();
command.setRequestedPropertiesAlgorithm(algorithm);
command.run();
} else {
System.err.printf("Unexpected number of arguments [%d]%n", arguments.length);
System.err.printf("Usage: %s <algorithm>%n", SetSensitivePropertiesAlgorithm.class.getSimpleName());
}
}
}

@ -16,174 +16,25 @@
*/
package org.apache.nifi.flow.encryptor.command;
import org.apache.nifi.encrypt.PropertyEncryptor;
import org.apache.nifi.encrypt.PropertyEncryptorBuilder;
import org.apache.nifi.flow.encryptor.FlowEncryptor;
import org.apache.nifi.flow.encryptor.StandardFlowEncryptor;
import org.apache.nifi.security.util.EncryptionMethod;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Set Sensitive Properties Key for NiFi Properties and update encrypted Flow Configuration
*/
public class SetSensitivePropertiesKey {
protected static final String PROPERTIES_FILE_PATH = "nifi.properties.file.path";
protected static final String PROPS_KEY = "nifi.sensitive.props.key";
protected static final String PROPS_ALGORITHM = "nifi.sensitive.props.algorithm";
protected static final String CONFIGURATION_FILE = "nifi.flow.configuration.file";
protected static final String CONFIGURATION_JSON_FILE = "nifi.flow.configuration.json.file";
private static final List<String> CONFIGURATION_FILES = Arrays.asList(CONFIGURATION_FILE, CONFIGURATION_JSON_FILE);
private static final int MINIMUM_REQUIRED_LENGTH = 12;
private static final String FLOW_PREFIX = "nifi.flow.";
private static final String GZ_EXTENSION = ".gz";
private static final String DEFAULT_PROPERTIES_ALGORITHM = EncryptionMethod.MD5_256AES.getAlgorithm();
private static final String DEFAULT_PROPERTIES_KEY = "nififtw!";
private static final String SENSITIVE_PROPERTIES_KEY = String.format("%s=", PROPS_KEY);
public static void main(final String[] arguments) {
if (arguments.length == 1) {
final String outputPropertiesKey = arguments[0];
if (outputPropertiesKey.length() < MINIMUM_REQUIRED_LENGTH) {
System.err.printf("Sensitive Properties Key length less than required [%d]%n", MINIMUM_REQUIRED_LENGTH);
} else {
run(outputPropertiesKey);
final FlowEncryptorCommand command = new FlowEncryptorCommand();
command.setRequestedPropertiesKey(outputPropertiesKey);
command.run();
}
} else {
System.err.printf("Unexpected number of arguments [%d]%n", arguments.length);
System.err.printf("Usage: %s <sensitivePropertiesKey>%n", SetSensitivePropertiesKey.class.getSimpleName());
}
}
private static void run(final String outputPropertiesKey) {
final String propertiesFilePath = System.getProperty(PROPERTIES_FILE_PATH);
final File propertiesFile = new File(propertiesFilePath);
final Properties properties = loadProperties(propertiesFile);
try {
storeProperties(propertiesFile, outputPropertiesKey);
System.out.printf("NiFi Properties Processed [%s]%n", propertiesFilePath);
} catch (final IOException e) {
final String message = String.format("Failed to Process NiFi Properties [%s]", propertiesFilePath);
throw new UncheckedIOException(message, e);
}
processFlowConfigurationFiles(properties, outputPropertiesKey);
}
private static void processFlowConfigurationFiles(final Properties properties, final String outputPropertiesKey) {
final String algorithm = getAlgorithm(properties);
final PropertyEncryptor outputEncryptor = getPropertyEncryptor(outputPropertiesKey, algorithm);
for (final String configurationFilePropertyName : CONFIGURATION_FILES) {
final String configurationFileProperty = properties.getProperty(configurationFilePropertyName);
if (configurationFileProperty == null || configurationFileProperty.isEmpty()) {
System.out.printf("Flow Configuration Property not specified [%s]%n", configurationFileProperty);
} else {
final File configurationFile = new File(configurationFileProperty);
if (configurationFile.exists()) {
processFlowConfiguration(configurationFile, properties, outputEncryptor);
}
}
}
}
private static void processFlowConfiguration(final File flowConfigurationFile, final Properties properties, final PropertyEncryptor outputEncryptor) {
try (final InputStream flowInputStream = new GZIPInputStream(new FileInputStream(flowConfigurationFile))) {
final File flowOutputFile = getFlowOutputFile();
final Path flowOutputPath = flowOutputFile.toPath();
try (final OutputStream flowOutputStream = new GZIPOutputStream(new FileOutputStream(flowOutputFile))) {
final String inputAlgorithm = getAlgorithm(properties);
final String inputPropertiesKey = getKey(properties);
final PropertyEncryptor inputEncryptor = getPropertyEncryptor(inputPropertiesKey, inputAlgorithm);
final FlowEncryptor flowEncryptor = new StandardFlowEncryptor();
flowEncryptor.processFlow(flowInputStream, flowOutputStream, inputEncryptor, outputEncryptor);
}
final Path flowConfigurationPath = flowConfigurationFile.toPath();
Files.move(flowOutputPath, flowConfigurationPath, StandardCopyOption.REPLACE_EXISTING);
System.out.printf("Flow Configuration Processed [%s]%n", flowConfigurationPath);
} catch (final IOException | RuntimeException e) {
System.err.printf("Failed to process Flow Configuration [%s]%n", flowConfigurationFile);
e.printStackTrace();
}
}
private static String getAlgorithm(final Properties properties) {
String algorithm = properties.getProperty(PROPS_ALGORITHM, DEFAULT_PROPERTIES_ALGORITHM);
if (algorithm.length() == 0) {
algorithm = DEFAULT_PROPERTIES_ALGORITHM;
}
return algorithm;
}
private static String getKey(final Properties properties) {
String key = properties.getProperty(PROPS_KEY, DEFAULT_PROPERTIES_KEY);
if (key.length() == 0) {
key = DEFAULT_PROPERTIES_KEY;
}
return key;
}
private static File getFlowOutputFile() throws IOException {
final File flowOutputFile = File.createTempFile(FLOW_PREFIX, GZ_EXTENSION);
flowOutputFile.deleteOnExit();
return flowOutputFile;
}
private static Properties loadProperties(final File propertiesFile) {
final Properties properties = new Properties();
try (final FileReader reader = new FileReader(propertiesFile)) {
properties.load(reader);
} catch (final IOException e) {
final String message = String.format("Failed to read NiFi Properties [%s]", propertiesFile);
throw new UncheckedIOException(message, e);
}
return properties;
}
private static void storeProperties(final File propertiesFile, final String propertiesKey) throws IOException {
final Path propertiesFilePath = propertiesFile.toPath();
final List<String> lines = Files.readAllLines(propertiesFilePath);
final List<String> updatedLines = lines.stream().map(line -> {
if (line.startsWith(SENSITIVE_PROPERTIES_KEY)) {
return SENSITIVE_PROPERTIES_KEY + propertiesKey;
} else {
return line;
}
}).collect(Collectors.toList());
Files.write(propertiesFilePath, updatedLines);
}
private static PropertyEncryptor getPropertyEncryptor(final String propertiesKey, final String propertiesAlgorithm) {
return new PropertyEncryptorBuilder(propertiesKey).setAlgorithm(propertiesAlgorithm).build();
}
}

@ -0,0 +1,199 @@
/*
* 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.flow.encryptor.command;
import org.apache.nifi.stream.io.GZIPOutputStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class FlowEncryptorCommandTest {
private static final String TEMP_FILE_PREFIX = SetSensitivePropertiesKeyTest.class.getSimpleName();
private static final String FLOW_CONTENTS_JSON = "{\"property\":\"value\"}";
private static final String FLOW_CONTENTS_XML = "<property><value>PROPERTY</value></property>";
private static final String JSON_GZ = ".json.gz";
private static final String XML_GZ = ".xml.gz";
private static final String PROPERTIES_EXTENSION = ".properties";
private static final String BLANK_PROPERTIES = "/blank.nifi.properties";
private static final String POPULATED_PROPERTIES = "/populated.nifi.properties";
private static final String LEGACY_BLANK_PROPERTIES = "/legacy-blank.nifi.properties";
private static final String REQUESTED_ALGORITHM = "NIFI_PBKDF2_AES_GCM_256";
@AfterEach
public void clearProperties() {
System.clearProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH);
}
@Test
public void testRunSystemPropertyNotDefined() {
final FlowEncryptorCommand command = new FlowEncryptorCommand();
assertThrows(IllegalStateException.class, command::run);
}
@Test
public void testRunPropertiesKeyBlankProperties() throws IOException, URISyntaxException {
final Path propertiesPath = getBlankNiFiProperties();
System.setProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH, propertiesPath.toString());
final FlowEncryptorCommand command = new FlowEncryptorCommand();
final String propertiesKey = UUID.randomUUID().toString();
command.setRequestedPropertiesKey(propertiesKey);
command.run();
assertPropertiesKeyUpdated(propertiesPath, propertiesKey);
}
@Test
public void testRunPropertiesKeyLegacyPropertiesWithoutFlowJson() throws IOException, URISyntaxException {
final Path propertiesPath = getLegacyNiFiPropertiesWithoutFlowJson();
System.setProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH, propertiesPath.toString());
final FlowEncryptorCommand command = new FlowEncryptorCommand();
final String propertiesKey = UUID.randomUUID().toString();
command.setRequestedPropertiesKey(propertiesKey);
command.run();
assertPropertiesKeyUpdated(propertiesPath, propertiesKey);
}
@Test
public void testRunPropertiesAlgorithmWithPropertiesKeyPopulatedProperties() throws IOException, URISyntaxException {
final Path propertiesPath = getPopulatedNiFiProperties();
System.setProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH, propertiesPath.toString());
final FlowEncryptorCommand command = new FlowEncryptorCommand();
final String propertiesKey = UUID.randomUUID().toString();
command.setRequestedPropertiesAlgorithm(REQUESTED_ALGORITHM);
command.setRequestedPropertiesKey(propertiesKey);
command.run();
assertPropertiesKeyUpdated(propertiesPath, propertiesKey);
assertPropertiesAlgorithmUpdated(propertiesPath, REQUESTED_ALGORITHM);
}
protected static void assertPropertiesAlgorithmUpdated(final Path propertiesPath, final String sensitivePropertiesAlgorithm) throws IOException {
final Optional<String> keyProperty = Files.readAllLines(propertiesPath)
.stream()
.filter(line -> line.startsWith(FlowEncryptorCommand.PROPS_ALGORITHM))
.findFirst();
assertTrue(keyProperty.isPresent(), "Sensitive Algorithm Property not found");
final String expectedProperty = String.format("%s=%s", FlowEncryptorCommand.PROPS_ALGORITHM, sensitivePropertiesAlgorithm);
assertEquals(expectedProperty, keyProperty.get(), "Sensitive Algorithm Property not updated");
}
protected static void assertPropertiesKeyUpdated(final Path propertiesPath, final String sensitivePropertiesKey) throws IOException {
final Optional<String> keyProperty = Files.readAllLines(propertiesPath)
.stream()
.filter(line -> line.startsWith(FlowEncryptorCommand.PROPS_KEY))
.findFirst();
assertTrue(keyProperty.isPresent(), "Sensitive Key Property not found");
final String expectedProperty = String.format("%s=%s", FlowEncryptorCommand.PROPS_KEY, sensitivePropertiesKey);
assertEquals(expectedProperty, keyProperty.get(), "Sensitive Key Property not updated");
}
protected static Path getBlankNiFiProperties() throws IOException, URISyntaxException {
final Path flowConfiguration = getFlowConfiguration(FLOW_CONTENTS_XML, XML_GZ);
final Path flowConfigurationJson = getFlowConfiguration(FLOW_CONTENTS_JSON, JSON_GZ);
return getNiFiProperties(flowConfiguration, flowConfigurationJson, BLANK_PROPERTIES);
}
protected static Path getPopulatedNiFiProperties() throws IOException, URISyntaxException {
final Path flowConfiguration = getFlowConfiguration(FLOW_CONTENTS_XML, XML_GZ);
final Path flowConfigurationJson = getFlowConfiguration(FLOW_CONTENTS_JSON, JSON_GZ);
return getNiFiProperties(flowConfiguration, flowConfigurationJson, POPULATED_PROPERTIES);
}
protected static Path getLegacyNiFiPropertiesWithoutFlowJson() throws IOException, URISyntaxException {
final Path flowConfiguration = getFlowConfiguration(FLOW_CONTENTS_XML, XML_GZ);
return getNiFiProperties(flowConfiguration, null, LEGACY_BLANK_PROPERTIES);
}
private static Path getNiFiProperties(
final Path flowConfigurationPath,
final Path flowConfigurationJsonPath,
String propertiesResource
) throws IOException, URISyntaxException {
final Path sourcePropertiesPath = Paths.get(getResourceUrl(propertiesResource).toURI());
final List<String> sourceProperties = Files.readAllLines(sourcePropertiesPath);
final List<String> flowProperties = sourceProperties.stream().map(line -> {
if (line.startsWith(FlowEncryptorCommand.CONFIGURATION_FILE)) {
return line + flowConfigurationPath;
} else if (line.startsWith(FlowEncryptorCommand.CONFIGURATION_JSON_FILE)) {
return flowConfigurationJsonPath == null ? line : line + flowConfigurationJsonPath;
} else {
return line;
}
}).collect(Collectors.toList());
final Path propertiesPath = Files.createTempFile(TEMP_FILE_PREFIX, PROPERTIES_EXTENSION);
propertiesPath.toFile().deleteOnExit();
Files.write(propertiesPath, flowProperties);
return propertiesPath;
}
private static URL getResourceUrl(String resource) throws FileNotFoundException {
final URL resourceUrl = FlowEncryptorCommand.class.getResource(resource);
if (resourceUrl == null) {
throw new FileNotFoundException(String.format("Resource [%s] not found", resource));
}
return resourceUrl;
}
private static Path getFlowConfiguration(final String contents, final String extension) throws IOException {
final Path flowConfigurationPath = Files.createTempFile(TEMP_FILE_PREFIX, extension);
final File flowConfigurationFile = flowConfigurationPath.toFile();
flowConfigurationFile.deleteOnExit();
try (final GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(flowConfigurationFile))) {
outputStream.write(contents.getBytes(StandardCharsets.UTF_8));
}
return flowConfigurationPath;
}
}

@ -0,0 +1,48 @@
/*
* 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.flow.encryptor.command;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Path;
public class SetSensitivePropertiesAlgorithmTest {
private static final String REQUESTED_ALGORITHM = "NIFI_PBKDF2_AES_GCM_256";
@AfterEach
public void clearProperties() {
System.clearProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH);
}
@Test
public void testMainNoArguments() {
SetSensitivePropertiesAlgorithm.main(new String[]{});
}
@Test
public void testMainPopulatedKeyAndAlgorithm() throws IOException, URISyntaxException {
final Path propertiesPath = FlowEncryptorCommandTest.getPopulatedNiFiProperties();
System.setProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH, propertiesPath.toString());
SetSensitivePropertiesAlgorithm.main(new String[]{REQUESTED_ALGORITHM});
FlowEncryptorCommandTest.assertPropertiesAlgorithmUpdated(propertiesPath, REQUESTED_ALGORITHM);
}
}

@ -16,50 +16,19 @@
*/
package org.apache.nifi.flow.encryptor.command;
import org.apache.nifi.stream.io.GZIPOutputStream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SetSensitivePropertiesKeyTest {
private static final String TEMP_FILE_PREFIX = SetSensitivePropertiesKeyTest.class.getSimpleName();
private static final String FLOW_CONTENTS_JSON = "{\"property\":\"value\"}";
private static final String FLOW_CONTENTS_XML = "<property><value>PROPERTY</value></property>";
private static final String JSON_GZ = ".json.gz";
private static final String XML_GZ = ".xml.gz";
private static final String PROPERTIES_EXTENSION = ".properties";
private static final String BLANK_PROPERTIES = "/blank.nifi.properties";
private static final String POPULATED_PROPERTIES = "/populated.nifi.properties";
private static final String LEGACY_BLANK_PROPERTIES = "/legacy-blank.nifi.properties";
@AfterEach
public void clearProperties() {
System.clearProperty(SetSensitivePropertiesKey.PROPERTIES_FILE_PATH);
System.clearProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH);
}
@Test
@ -69,95 +38,12 @@ public class SetSensitivePropertiesKeyTest {
@Test
public void testMainBlankKeyAndAlgorithm() throws IOException, URISyntaxException {
final Path flowConfiguration = getFlowConfiguration(FLOW_CONTENTS_XML, XML_GZ);
final Path flowConfigurationJson = getFlowConfiguration(FLOW_CONTENTS_JSON, JSON_GZ);
final Path propertiesPath = getNiFiProperties(flowConfiguration, flowConfigurationJson, BLANK_PROPERTIES);
System.setProperty(SetSensitivePropertiesKey.PROPERTIES_FILE_PATH, propertiesPath.toString());
final Path propertiesPath = FlowEncryptorCommandTest.getBlankNiFiProperties();
System.setProperty(FlowEncryptorCommand.PROPERTIES_FILE_PATH, propertiesPath.toString());
final String sensitivePropertiesKey = UUID.randomUUID().toString();
SetSensitivePropertiesKey.main(new String[]{sensitivePropertiesKey});
assertPropertiesKeyUpdated(propertiesPath, sensitivePropertiesKey);
}
@Test
public void testMainPopulatedKeyAndAlgorithm() throws IOException, URISyntaxException {
final Path flowConfiguration = getFlowConfiguration(FLOW_CONTENTS_XML, XML_GZ);
final Path flowConfigurationJson = getFlowConfiguration(FLOW_CONTENTS_JSON, JSON_GZ);
final Path propertiesPath = getNiFiProperties(flowConfiguration, flowConfigurationJson, POPULATED_PROPERTIES);
System.setProperty(SetSensitivePropertiesKey.PROPERTIES_FILE_PATH, propertiesPath.toString());
final String sensitivePropertiesKey = UUID.randomUUID().toString();
SetSensitivePropertiesKey.main(new String[]{sensitivePropertiesKey});
assertPropertiesKeyUpdated(propertiesPath, sensitivePropertiesKey);
}
@Test
public void testMainLegacyBlankKeyAndAlgorithm() throws IOException, URISyntaxException {
final Path flowConfiguration = getFlowConfiguration(FLOW_CONTENTS_XML, XML_GZ);
final Path propertiesPath = getNiFiProperties(flowConfiguration, null, LEGACY_BLANK_PROPERTIES);
System.setProperty(SetSensitivePropertiesKey.PROPERTIES_FILE_PATH, propertiesPath.toString());
final String sensitivePropertiesKey = UUID.randomUUID().toString();
SetSensitivePropertiesKey.main(new String[]{sensitivePropertiesKey});
assertPropertiesKeyUpdated(propertiesPath, sensitivePropertiesKey);
}
private void assertPropertiesKeyUpdated(final Path propertiesPath, final String sensitivePropertiesKey) throws IOException {
final Optional<String> keyProperty = Files.readAllLines(propertiesPath)
.stream()
.filter(line -> line.startsWith(SetSensitivePropertiesKey.PROPS_KEY))
.findFirst();
assertTrue(keyProperty.isPresent(), "Sensitive Key Property not found");
final String expectedProperty = String.format("%s=%s", SetSensitivePropertiesKey.PROPS_KEY, sensitivePropertiesKey);
assertEquals(expectedProperty, keyProperty.get(), "Sensitive Key Property not updated");
}
private Path getNiFiProperties(
final Path flowConfigurationPath,
final Path flowConfigurationJsonPath,
String propertiesResource
) throws IOException, URISyntaxException {
final Path sourcePropertiesPath = Paths.get(getResourceUrl(propertiesResource).toURI());
final List<String> sourceProperties = Files.readAllLines(sourcePropertiesPath);
final List<String> flowProperties = sourceProperties.stream().map(line -> {
if (line.startsWith(SetSensitivePropertiesKey.CONFIGURATION_FILE)) {
return line + flowConfigurationPath;
} else if (line.startsWith(SetSensitivePropertiesKey.CONFIGURATION_JSON_FILE)) {
return flowConfigurationJsonPath == null ? line : line + flowConfigurationJsonPath;
} else {
return line;
}
}).collect(Collectors.toList());
final Path propertiesPath = Files.createTempFile(TEMP_FILE_PREFIX, PROPERTIES_EXTENSION);
propertiesPath.toFile().deleteOnExit();
Files.write(propertiesPath, flowProperties);
return propertiesPath;
}
private Path getFlowConfiguration(final String contents, final String extension) throws IOException {
final Path flowConfigurationPath = Files.createTempFile(TEMP_FILE_PREFIX, extension);
final File flowConfigurationFile = flowConfigurationPath.toFile();
flowConfigurationFile.deleteOnExit();
try (final GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(flowConfigurationFile))) {
outputStream.write(contents.getBytes(StandardCharsets.UTF_8));
}
return flowConfigurationPath;
}
private URL getResourceUrl(String resource) throws FileNotFoundException {
final URL resourceUrl = SetSensitivePropertiesKey.class.getResource(resource);
if (resourceUrl == null) {
throw new FileNotFoundException(String.format("Resource [%s] not found", resource));
}
return resourceUrl;
FlowEncryptorCommandTest.assertPropertiesKeyUpdated(propertiesPath, sensitivePropertiesKey);
}
}

@ -1757,6 +1757,8 @@ These algorithms use a strong Key Derivation Function to derive a secret key of
Each Key Derivation Function uses a static salt in order to support flow configuration comparison across cluster nodes.
Each Key Derivation Function also uses default iteration and cost parameters as defined in the associated secure hashing implementation class.
[[property-encryption-algorithms]]
=== Property Encryption Algorithms
The following strong encryption methods can be configured in the `nifi.sensitive.props.algorithm` property:
* `NIFI_ARGON2_AES_GCM_128`
@ -4332,6 +4334,23 @@ where:
For more information see the <<toolkit-guide.adoc#encrypt_config_tool,Encrypt-Config Tool>> section in the NiFi Toolkit Guide.
==== Updating the Sensitive Properties Algorithm
The following command can be used to read an existing flow configuration and set a new sensitive properties algorithm in _nifi.properties_:
```
$ ./bin/nifi.sh set-sensitive-properties-algorithm <algorithm>
```
The command reads the following flow configuration file properties from _nifi.properties_:
- `nifi.flow.configuration.file`
- `nifi.flow.configuration.json.file`
The command checks for the existence of each file and updates the sensitive property values found.
See <<property-encryption-algorithms>> for supported values.
==== Updating the Sensitive Properties Key
Starting with version 1.14.0, NiFi requires a value for `nifi.sensitive.props.key` in _nifi.properties_.

@ -344,6 +344,16 @@ run() {
run_nifi_cmd="exec ${run_nifi_cmd}"
fi
if [ "$1" = "set-sensitive-properties-algorithm" ]; then
run_command="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' '-Dnifi.properties.file.path=${NIFI_HOME}/conf/nifi.properties' 'org.apache.nifi.flow.encryptor.command.SetSensitivePropertiesAlgorithm'"
eval "cd ${NIFI_HOME}"
shift
eval "${run_command}" '"$@"'
EXIT_STATUS=$?
echo
return;
fi
if [ "$1" = "set-sensitive-properties-key" ]; then
run_command="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' '-Dnifi.properties.file.path=${NIFI_HOME}/conf/nifi.properties' 'org.apache.nifi.flow.encryptor.command.SetSensitivePropertiesKey'"
eval "cd ${NIFI_HOME}"
@ -460,7 +470,7 @@ case "$1" in
install "$@"
;;
start|stop|decommission|run|status|is_loaded|dump|diagnostics|status-history|env|stateless|set-sensitive-properties-key|set-single-user-credentials)
start|stop|decommission|run|status|is_loaded|dump|diagnostics|status-history|env|stateless|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials)
main "$@"
;;
@ -470,6 +480,6 @@ case "$1" in
run "start"
;;
*)
echo "Usage nifi {start|stop|decommission|run|restart|status|dump|diagnostics|status-history|install|stateless|set-sensitive-properties-key|set-single-user-credentials}"
echo "Usage nifi {start|stop|decommission|run|restart|status|dump|diagnostics|status-history|install|stateless|set-sensitive-properties-algorithm|set-sensitive-properties-key|set-single-user-credentials}"
;;
esac