http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/properties/JavaMain.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/properties/JavaMain.java
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/properties/JavaMain.java
new file mode 100644
index 0000000..2109ca1
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/properties/JavaMain.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+public class JavaMain {
+    public static void main(String[] args) {
+        System.out.println("The Java class #main ran successfully");
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/CharacterDevice.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/CharacterDevice.java
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/CharacterDevice.java
new file mode 100644
index 0000000..ed254d9
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/CharacterDevice.java
@@ -0,0 +1,73 @@
+/*
+Copyright (c) 2010 McDowell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+package org.apache.nifi.util.console;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+
+/**
+ * @{link TextDevice} implementation wrapping character streams.
+ *
+ * @author McDowell
+ */
+class CharacterDevice extends TextDevice {
+    private final BufferedReader reader;
+    private final PrintWriter writer;
+
+    public CharacterDevice(BufferedReader reader, PrintWriter writer) {
+        this.reader = reader;
+        this.writer = writer;
+    }
+
+    @Override
+    public CharacterDevice printf(String fmt, Object... params)
+            throws ConsoleException {
+        writer.printf(fmt, params);
+        return this;
+    }
+
+    @Override
+    public String readLine() throws ConsoleException {
+        try {
+            return reader.readLine();
+        } catch (IOException e) {
+            throw new IllegalStateException();
+        }
+    }
+
+    @Override
+    public char[] readPassword() throws ConsoleException {
+        return readLine().toCharArray();
+    }
+
+    @Override
+    public Reader reader() throws ConsoleException {
+        return reader;
+    }
+
+    @Override
+    public PrintWriter writer() throws ConsoleException {
+        return writer;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleDevice.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleDevice.java
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleDevice.java
new file mode 100644
index 0000000..3086d66
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleDevice.java
@@ -0,0 +1,66 @@
+/*
+Copyright (c) 2010 McDowell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+package org.apache.nifi.util.console;
+
+import java.io.Console;
+import java.io.PrintWriter;
+import java.io.Reader;
+
+/**
+ * {@link TextDevice} implementation wrapping a {@link Console}.
+ *
+ * @author McDowell
+ */
+class ConsoleDevice extends TextDevice {
+    private final Console console;
+
+    public ConsoleDevice(Console console) {
+        this.console = console;
+    }
+
+    @Override
+    public TextDevice printf(String fmt, Object... params)
+            throws ConsoleException {
+        console.format(fmt, params);
+        return this;
+    }
+
+    @Override
+    public Reader reader() throws ConsoleException {
+        return console.reader();
+    }
+
+    @Override
+    public String readLine() throws ConsoleException {
+        return console.readLine();
+    }
+
+    @Override
+    public char[] readPassword() throws ConsoleException {
+        return console.readPassword();
+    }
+
+    @Override
+    public PrintWriter writer() throws ConsoleException {
+        return console.writer();
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleException.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleException.java
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleException.java
new file mode 100644
index 0000000..c6033ff
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/ConsoleException.java
@@ -0,0 +1,35 @@
+/*
+Copyright (c) 2010 McDowell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+package org.apache.nifi.util.console;
+
+/**
+ * Runtime exception for handling console errors.
+ *
+ * @author McDowell
+ */
+public class ConsoleException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public ConsoleException(Throwable t) {
+        super(t);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevice.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevice.java
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevice.java
new file mode 100644
index 0000000..7f8af8f
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevice.java
@@ -0,0 +1,43 @@
+/*
+Copyright (c) 2010 McDowell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+package org.apache.nifi.util.console;
+
+import java.io.PrintWriter;
+import java.io.Reader;
+
+/**
+ * Abstraction representing a text input/output device.
+ *
+ * @author McDowell
+ */
+public abstract class TextDevice {
+    public abstract TextDevice printf(String fmt, Object... params)
+            throws ConsoleException;
+
+    public abstract String readLine() throws ConsoleException;
+
+    public abstract char[] readPassword() throws ConsoleException;
+
+    public abstract Reader reader() throws ConsoleException;
+
+    public abstract PrintWriter writer() throws ConsoleException;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevices.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevices.java
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevices.java
new file mode 100644
index 0000000..9861118
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/java/org/apache/nifi/util/console/TextDevices.java
@@ -0,0 +1,80 @@
+/*
+Copyright (c) 2010 McDowell
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+package org.apache.nifi.util.console;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+
+/**
+ * Convenience class for providing {@link TextDevice} implementations.
+ *
+ * @author McDowell
+ */
+public final class TextDevices {
+    private TextDevices() {}
+
+    private static TextDevice DEFAULT = (System.console() == null) ? 
streamDevice(
+            System.in, System.out)
+            : new ConsoleDevice(System.console());
+
+    /**
+     * The default system text I/O device.
+     *
+     * @return the default device
+     */
+    public static TextDevice defaultTextDevice() {
+        return DEFAULT;
+    }
+
+    /**
+     * Returns a text I/O device wrapping the given streams. The default system
+     * encoding is used to decode/encode data.
+     *
+     * @param in
+     *          an input source
+     * @param out
+     *          an output target
+     * @return a new device
+     */
+    public static TextDevice streamDevice(InputStream in, OutputStream out) {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+        PrintWriter writer = new PrintWriter(out, true);
+        return new CharacterDevice(reader, writer);
+    }
+
+    /**
+     * Returns a text I/O device wrapping the given streams.
+     *
+     * @param reader
+     *          an input source
+     * @param writer
+     *          an output target
+     * @return a new device
+     */
+    public static TextDevice characterDevice(BufferedReader reader,
+                                             PrintWriter writer) {
+        return new CharacterDevice(reader, writer);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties
new file mode 100644
index 0000000..fc2aaf1
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/log4j.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+log4j.rootLogger=INFO,console
+
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p 
%c{2}: %m%n
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/logback.xml
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/logback.xml 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/logback.xml
new file mode 100644
index 0000000..fb7e961
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/resources/logback.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<configuration scan="true" scanPeriod="30 seconds">
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%date %level [%thread] %logger{40} %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- valid logging levels: TRACE, DEBUG, INFO, WARN, ERROR -->
+
+    <logger name="org.apache.nifi.util.config" level="INFO">
+        <appender-ref ref="CONSOLE"/>
+    </logger>
+
+    <root level="INFO">
+        <appender-ref ref="CONSOLE"/>
+    </root>
+
+</configuration>

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
new file mode 100644
index 0000000..b4ca22d
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
@@ -0,0 +1,1458 @@
+/*
+ * 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 ch.qos.logback.classic.spi.LoggingEvent
+import ch.qos.logback.core.AppenderBase
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.util.NiFiProperties
+import org.apache.nifi.util.console.TextDevice
+import org.apache.nifi.util.console.TextDevices
+import org.bouncycastle.crypto.generators.SCrypt
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.Assertion
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+import org.junit.contrib.java.lang.system.SystemOutRule
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.nio.file.Files
+import java.nio.file.attribute.PosixFilePermission
+import java.security.KeyException
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class ConfigEncryptionToolTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(ConfigEncryptionToolTest.class)
+
+    @Rule
+    public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+    @Rule
+    public final SystemOutRule systemOutRule = new SystemOutRule().enableLog()
+
+    private static final String KEY_HEX_128 = 
"0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+    public static final String KEY_HEX = isUnlimitedStrengthCryptoAvailable() 
? KEY_HEX_256 : KEY_HEX_128
+    private static final String PASSWORD = "thisIsABadPassword"
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        TestAppender.reset()
+    }
+
+    private static boolean isUnlimitedStrengthCryptoAvailable() {
+        Cipher.getMaxAllowedKeyLength("AES") > 128
+    }
+
+    private static void printProperties(NiFiProperties properties) {
+        if (!(properties instanceof ProtectedNiFiProperties)) {
+            properties = new ProtectedNiFiProperties(properties)
+        }
+
+        (properties as 
ProtectedNiFiProperties).getPropertyKeysIncludingProtectionSchemes().sort().each
 { String key ->
+            logger.info("${key}\t\t${properties.getProperty(key)}")
+        }
+    }
+
+    @Test
+    void testShouldPrintHelpMessage() {
+        // Arrange
+        def flags = ["-h", "--help"]
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        flags.each { String arg ->
+            def msg = shouldFail(CommandLineParseException) {
+                tool.parse([arg] as String[])
+            }
+
+            // Assert
+            assert msg == null
+            assert systemOutRule.getLog().contains("usage: 
org.apache.nifi.properties.ConfigEncryptionTool [")
+        }
+    }
+
+    @Test
+    void testShouldParseBootstrapConfArgument() {
+        // Arrange
+        def flags = ["-b", "--bootstrapConf"]
+        String bootstrapPath = "src/test/resources/bootstrap.conf"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        flags.each { String arg ->
+            tool.parse([arg, bootstrapPath] as String[])
+            logger.info("Parsed bootstrap.conf location: 
${tool.bootstrapConfPath}")
+
+            // Assert
+            assert tool.bootstrapConfPath == bootstrapPath
+        }
+    }
+
+    @Test
+    void testParseShouldPopulateDefaultBootstrapConfArgument() {
+        // Arrange
+        String bootstrapPath = "conf/bootstrap.conf"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        tool.parse([] as String[])
+        logger.info("Parsed bootstrap.conf location: 
${tool.bootstrapConfPath}")
+
+        // Assert
+        assert new File(tool.bootstrapConfPath).getPath() == new 
File(bootstrapPath).getPath()
+    }
+
+    @Test
+    void testShouldParseNiFiPropertiesArgument() {
+        // Arrange
+        def flags = ["-n", "--niFiProperties"]
+        String niFiPropertiesPath = "src/test/resources/nifi.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        flags.each { String arg ->
+            tool.parse([arg, niFiPropertiesPath] as String[])
+            logger.info("Parsed nifi.properties location: 
${tool.niFiPropertiesPath}")
+
+            // Assert
+            assert tool.niFiPropertiesPath == niFiPropertiesPath
+        }
+    }
+
+    @Test
+    void testParseShouldPopulateDefaultNiFiPropertiesArgument() {
+        // Arrange
+        String niFiPropertiesPath = "conf/nifi.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        tool.parse([] as String[])
+        logger.info("Parsed nifi.properties location: 
${tool.niFiPropertiesPath}")
+
+        // Assert
+        assert new File(tool.niFiPropertiesPath).getPath() == new 
File(niFiPropertiesPath).getPath()
+    }
+
+    @Test
+    void testShouldParseOutputNiFiPropertiesArgument() {
+        // Arrange
+        def flags = ["-o", "--outputNiFiProperties"]
+        String niFiPropertiesPath = "src/test/resources/nifi.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        flags.each { String arg ->
+            tool.parse([arg, niFiPropertiesPath] as String[])
+            logger.info("Parsed output nifi.properties location: 
${tool.outputNiFiPropertiesPath}")
+
+            // Assert
+            assert tool.outputNiFiPropertiesPath == niFiPropertiesPath
+        }
+    }
+
+    @Test
+    void testParseShouldPopulateDefaultOutputNiFiPropertiesArgument() {
+        // Arrange
+        String niFiPropertiesPath = "conf/nifi.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        tool.parse([] as String[])
+        logger.info("Parsed output nifi.properties location: 
${tool.outputNiFiPropertiesPath}")
+
+        // Assert
+        assert new File(tool.outputNiFiPropertiesPath).getPath() == new 
File(niFiPropertiesPath).getPath()
+    }
+
+    @Test
+    void testParseShouldWarnIfNiFiPropertiesWillBeOverwritten() {
+        // Arrange
+        String niFiPropertiesPath = "conf/nifi.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        tool.parse("-n ${niFiPropertiesPath} -o ${niFiPropertiesPath}".split(" 
") as String[])
+        logger.info("Parsed nifi.properties location: 
${tool.niFiPropertiesPath}")
+        logger.info("Parsed output nifi.properties location: 
${tool.outputNiFiPropertiesPath}")
+
+        // Assert
+        assert !TestAppender.events.isEmpty()
+        assert TestAppender.events.first().message =~ "The source 
nifi.properties and destination nifi.properties are identical \\[.*\\] so the 
original will be overwritten"
+    }
+
+    @Test
+    void testShouldParseKeyArgument() {
+        // Arrange
+        def flags = ["-k", "--key"]
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        flags.each { String arg ->
+            tool.parse([arg, KEY_HEX] as String[])
+            logger.info("Parsed key: ${tool.keyHex}")
+
+            // Assert
+            assert tool.keyHex == KEY_HEX
+        }
+    }
+
+    @Test
+    void testShouldLoadNiFiProperties() {
+        // Arrange
+        String niFiPropertiesPath = 
"src/test/resources/nifi_with_sensitive_properties_unprotected.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", niFiPropertiesPath] as String[]
+
+        String oldFilePath = 
System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+
+        tool.parse(args)
+        logger.info("Parsed nifi.properties location: 
${tool.niFiPropertiesPath}")
+
+        // Act
+        NiFiProperties properties = tool.loadNiFiProperties()
+        logger.info("Loaded NiFiProperties from ${tool.niFiPropertiesPath}")
+
+        // Assert
+        assert properties
+        assert properties.size() > 0
+
+        // The system variable was reset to the original value
+        assert System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) == 
oldFilePath
+    }
+
+    @Test
+    void testShouldReadKeyFromConsole() {
+        // Arrange
+        List<String> keyValues = [
+                "0123 4567",
+                KEY_HEX,
+                "   ${KEY_HEX}   ",
+                "non-hex-chars",
+        ]
+
+        // Act
+        keyValues.each { String key ->
+            TextDevice mockConsoleDevice = TextDevices.streamDevice(new 
ByteArrayInputStream(key.bytes), new ByteArrayOutputStream())
+            String readKey = 
ConfigEncryptionTool.readKeyFromConsole(mockConsoleDevice)
+            logger.info("Read key: [${readKey}]")
+
+            // Assert
+            assert readKey == key
+        }
+    }
+
+    @Test
+    void testShouldReadPasswordFromConsole() {
+        // Arrange
+        List<String> passwords = [
+                "0123 4567",
+                PASSWORD,
+                "   ${PASSWORD}   ",
+                "non-hex-chars",
+        ]
+
+        // Act
+        passwords.each { String pw ->
+            logger.info("Using password: [${PASSWORD}]")
+            TextDevice mockConsoleDevice = TextDevices.streamDevice(new 
ByteArrayInputStream(pw.bytes), new ByteArrayOutputStream())
+            String readPassword = 
ConfigEncryptionTool.readPasswordFromConsole(mockConsoleDevice)
+            logger.info("Read password: [${readPassword}]")
+
+            // Assert
+            assert readPassword == pw
+        }
+    }
+
+    @Test
+    void testShouldReadPasswordFromConsoleIfNoKeyPresent() {
+        // Arrange
+        def args = [] as String[]
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.parse(args)
+        logger.info("Using password flag: ${tool.usingPassword}")
+        logger.info("Password: ${tool.password}")
+        logger.info("Key hex:  ${tool.keyHex}")
+
+        assert tool.usingPassword
+        assert !tool.password
+        assert !tool.keyHex
+
+        TextDevice mockConsoleDevice = TextDevices.streamDevice(new 
ByteArrayInputStream(PASSWORD.bytes), new ByteArrayOutputStream())
+
+        // Mocked for deterministic output and performance in test -- SCrypt 
is not under test here
+        SCrypt.metaClass.'static'.generate = { byte[] pw, byte[] s, int N, int 
r, int p, int dkLen ->
+            logger.mock("Mock SCrypt.generate(${Hex.encodeHexString(pw)}, 
${Hex.encodeHexString(s)}, ${N}, ${r}, ${p}, ${dkLen})")
+            logger.mock("Returning ${KEY_HEX[0..<dkLen * 2]}")
+            Hex.decodeHex(KEY_HEX[0..<dkLen * 2] as char[])
+        }
+
+        // Act
+        String readKey = tool.getKey(mockConsoleDevice)
+        logger.info("Read key: [${readKey}]")
+
+        // Assert
+        assert readKey == KEY_HEX
+        assert tool.keyHex == readKey
+        assert !tool.password
+        assert !tool.usingPassword
+
+        SCrypt.metaClass.'static' = null
+    }
+
+    @Test
+    void testShouldReadKeyFromConsoleIfFlagProvided() {
+        // Arrange
+        def args = ["-r"] as String[]
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.parse(args)
+        logger.info("Using password flag: ${tool.usingPassword}")
+        logger.info("Password: ${tool.password}")
+        logger.info("Key hex:  ${tool.keyHex}")
+
+        assert !tool.usingPassword
+        assert !tool.password
+        assert !tool.keyHex
+
+        TextDevice mockConsoleDevice = TextDevices.streamDevice(new 
ByteArrayInputStream(KEY_HEX.bytes), new ByteArrayOutputStream())
+
+        // Act
+        String readKey = tool.getKey(mockConsoleDevice)
+        logger.info("Read key: [${readKey}]")
+
+        // Assert
+        assert readKey == KEY_HEX
+        assert tool.keyHex == readKey
+        assert !tool.password
+        assert !tool.usingPassword
+    }
+
+    @Test
+    void testShouldIgnoreRawKeyFlagIfKeyProvided() {
+        // Arrange
+        def args = ["-r", "-k", KEY_HEX] as String[]
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        tool.parse(args)
+        logger.info("Using password flag: ${tool.usingPassword}")
+        logger.info("Password: ${tool.password}")
+        logger.info("Key hex:  ${tool.keyHex}")
+
+        // Assert
+        assert !tool.usingPassword
+        assert !tool.password
+        assert tool.keyHex == KEY_HEX
+
+        assert !TestAppender.events.isEmpty()
+        assert TestAppender.events.collect {
+            it.message
+        }.contains("If the key or password is provided in the arguments, 
'-r'/'--useRawKey' is ignored")
+    }
+
+    @Test
+    void testShouldIgnoreRawKeyFlagIfPasswordProvided() {
+        // Arrange
+        def args = ["-r", "-p", PASSWORD] as String[]
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        tool.parse(args)
+        logger.info("Using password flag: ${tool.usingPassword}")
+        logger.info("Password: ${tool.password}")
+        logger.info("Key hex:  ${tool.keyHex}")
+
+        // Assert
+        assert tool.usingPassword
+        assert tool.password == PASSWORD
+        assert !tool.keyHex
+
+        assert !TestAppender.events.isEmpty()
+        assert TestAppender.events.collect {
+            it.message
+        }.contains("If the key or password is provided in the arguments, 
'-r'/'--useRawKey' is ignored")
+    }
+
+    @Test
+    void testShouldParseKey() {
+        // Arrange
+        Map<String, String> keyValues = [
+                (KEY_HEX)                         : KEY_HEX,
+                "   ${KEY_HEX}   "                : KEY_HEX,
+                "xxx${KEY_HEX}zzz"                : KEY_HEX,
+                ((["0123", "4567"] * 4).join("-")): "01234567" * 4,
+                ((["89ab", "cdef"] * 4).join(" ")): "89ABCDEF" * 4,
+                (KEY_HEX.toLowerCase())           : KEY_HEX,
+                (KEY_HEX[0..<32])                 : KEY_HEX[0..<32],
+        ]
+
+        if (isUnlimitedStrengthCryptoAvailable()) {
+            keyValues.put(KEY_HEX[0..<48], KEY_HEX[0..<48])
+        }
+
+        // Act
+        keyValues.each { String key, final String EXPECTED_KEY ->
+            logger.info("Reading key: [${key}]")
+            String parsedKey = ConfigEncryptionTool.parseKey(key)
+            logger.info("Parsed key:  [${parsedKey}]")
+
+            // Assert
+            assert parsedKey == EXPECTED_KEY
+        }
+    }
+
+    @Test
+    void testParseKeyShouldThrowExceptionForInvalidKeys() {
+        // Arrange
+        List<String> keyValues = [
+                "0123 4567",
+                "non-hex-chars",
+                KEY_HEX[0..<-1],
+                "&ITD SF^FI&&%SDIF"
+        ]
+
+        def validKeyLengths = ConfigEncryptionTool.getValidKeyLengths()
+        def bitLengths = validKeyLengths.collect { it / 4 }
+        String secondHalf = /\[${validKeyLengths.join(", ")}\] bits / +
+                /\(\[${bitLengths.join(", ")}\]/ + / hex 
characters\)/.toString()
+
+        // Act
+        keyValues.each { String key ->
+            logger.info("Reading key: [${key}]")
+            def msg = shouldFail(KeyException) {
+                String parsedKey = ConfigEncryptionTool.parseKey(key)
+                logger.info("Parsed key:  [${parsedKey}]")
+            }
+            logger.expected(msg)
+            int trimmedKeySize = key.replaceAll("[^0-9a-fA-F]", "").size()
+
+            // Assert
+            assert msg =~ "The key \\(${trimmedKeySize} hex chars\\) must be 
of length ${secondHalf}"
+        }
+    }
+
+    @Test
+    void testShouldDeriveKeyFromPassword() {
+        // Arrange
+
+        // Mocked for deterministic output and performance in test -- SCrypt 
is not under test here
+        SCrypt.metaClass.'static'.generate = { byte[] pw, byte[] s, int N, int 
r, int p, int dkLen ->
+            logger.mock("Mock SCrypt.generate(${Hex.encodeHexString(pw)}, 
${Hex.encodeHexString(s)}, ${N}, ${r}, ${p}, ${dkLen})")
+            logger.mock("Returning ${KEY_HEX[0..<dkLen * 2]}")
+            Hex.decodeHex(KEY_HEX[0..<dkLen * 2] as char[])
+        }
+
+        logger.info("Using password: [${PASSWORD}]")
+
+        // Act
+        String derivedKey = 
ConfigEncryptionTool.deriveKeyFromPassword(PASSWORD)
+        logger.info("Derived key:  [${derivedKey}]")
+
+        // Assert
+        assert derivedKey == KEY_HEX
+
+        SCrypt.metaClass.'static' = null
+    }
+
+    @Test
+    void testShouldActuallyDeriveKeyFromPassword() {
+        // Arrange
+        logger.info("Using password: [${PASSWORD}]")
+
+        // Act
+        String derivedKey = 
ConfigEncryptionTool.deriveKeyFromPassword(PASSWORD)
+        logger.info("Derived key:  [${derivedKey}]")
+
+        // Assert
+        assert derivedKey.length() == (Cipher.getMaxAllowedKeyLength("AES") > 
128 ? 64 : 32)
+    }
+
+    @Test
+    void testDeriveKeyFromPasswordShouldTrimPassword() {
+        // Arrange
+        final String PASSWORD_SPACES = "   ${PASSWORD}   "
+
+        def attemptedPasswords = []
+
+        // Mocked for deterministic output and performance in test -- SCrypt 
is not under test here
+        SCrypt.metaClass.'static'.generate = { byte[] pw, byte[] s, int N, int 
r, int p, int dkLen ->
+            logger.mock("Mock SCrypt.generate(${Hex.encodeHexString(pw)}, 
${Hex.encodeHexString(s)}, ${N}, ${r}, ${p}, ${dkLen})")
+            attemptedPasswords << new String(pw)
+            logger.mock("Returning ${KEY_HEX[0..<dkLen * 2]}")
+            Hex.decodeHex(KEY_HEX[0..<dkLen * 2] as char[])
+        }
+
+        // Act
+        [PASSWORD, PASSWORD_SPACES].each { String password ->
+            logger.info("Using password: [${password}]")
+            String derivedKey = 
ConfigEncryptionTool.deriveKeyFromPassword(password)
+            logger.info("Derived key:  [${derivedKey}]")
+        }
+
+        // Assert
+        assert attemptedPasswords.size() == 2
+        assert attemptedPasswords.every { it == PASSWORD }
+
+        SCrypt.metaClass.'static' = null
+    }
+
+    @Test
+    void testDeriveKeyFromPasswordShouldThrowExceptionForInvalidPasswords() {
+        // Arrange
+        List<String> passwords = [
+                (null),
+                "",
+                "      ",
+                "shortpass",
+                "shortwith    "
+        ]
+
+        // Act
+        passwords.each { String password ->
+            logger.info("Reading password: [${password}]")
+            def msg = shouldFail(KeyException) {
+                String derivedKey = 
ConfigEncryptionTool.deriveKeyFromPassword(password)
+                logger.info("Derived key:  [${derivedKey}]")
+            }
+            logger.expected(msg)
+
+            // Assert
+            assert msg == "Cannot derive key from empty/short password -- 
password must be at least 12 characters"
+        }
+    }
+
+    @Test
+    void testShouldHandleKeyAndPasswordFlag() {
+        // Arrange
+        def args = ["-k", KEY_HEX, "-p", PASSWORD]
+        logger.info("Using args: ${args}")
+
+        // Act
+        def msg = shouldFail(CommandLineParseException) {
+            new ConfigEncryptionTool().parse(args as String[])
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "Only one of password and key can be used"
+    }
+
+    @Test
+    void testShouldNotLoadMissingNiFiProperties() {
+        // Arrange
+        String niFiPropertiesPath = 
"src/test/resources/non_existent_nifi.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", niFiPropertiesPath] as String[]
+
+        String oldFilePath = 
System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+
+        tool.parse(args)
+        logger.info("Parsed nifi.properties location: 
${tool.niFiPropertiesPath}")
+
+        // Act
+        def msg = shouldFail(CommandLineParseException) {
+            NiFiProperties properties = tool.loadNiFiProperties()
+            logger.info("Loaded NiFiProperties from 
${tool.niFiPropertiesPath}")
+        }
+
+        // Assert
+        assert msg == "Cannot load NiFiProperties from 
[${niFiPropertiesPath}]".toString()
+
+        // The system variable was reset to the original value
+        assert System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) == 
oldFilePath
+    }
+
+    @Test
+    void testLoadNiFiPropertiesShouldHandleReadFailure() {
+        // Arrange
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File workingFile = new File("tmp_nifi.properties")
+        workingFile.delete()
+
+        Files.copy(inputPropertiesFile.toPath(), workingFile.toPath())
+        // Empty set of permissions
+        Files.setPosixFilePermissions(workingFile.toPath(), [] as Set)
+        logger.info("Set POSIX permissions to 
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+
+        // Act
+        def msg = shouldFail(IOException) {
+            tool.loadNiFiProperties()
+            logger.info("Read nifi.properties")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "Cannot load NiFiProperties from 
[${workingFile.path}]".toString()
+
+        workingFile.deleteOnExit()
+    }
+
+    @Test
+    void testShouldEncryptSensitiveProperties() {
+        // Arrange
+        String niFiPropertiesPath = 
"src/test/resources/nifi_with_sensitive_properties_unprotected.properties"
+        String newPropertiesPath = 
"src/test/resources/tmp_encrypted_nifi.properties"
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", niFiPropertiesPath, "-o", newPropertiesPath] as 
String[]
+
+        tool.parse(args)
+        logger.info("Parsed nifi.properties location: 
${tool.niFiPropertiesPath}")
+
+        tool.keyHex = KEY_HEX
+
+        NiFiProperties plainNiFiProperties = tool.loadNiFiProperties()
+        ProtectedNiFiProperties protectedWrapper = new 
ProtectedNiFiProperties(plainNiFiProperties)
+        assert !protectedWrapper.hasProtectedKeys()
+
+        // Act
+        NiFiProperties encryptedProperties = 
tool.encryptSensitiveProperties(plainNiFiProperties)
+        logger.info("Encrypted sensitive properties")
+
+        // Assert
+        ProtectedNiFiProperties protectedWrapperAroundEncrypted = new 
ProtectedNiFiProperties(encryptedProperties)
+        assert protectedWrapperAroundEncrypted.hasProtectedKeys()
+
+        // Ensure that all non-empty sensitive properties are marked as 
protected
+        final Set<String> EXPECTED_PROTECTED_KEYS = 
protectedWrapperAroundEncrypted
+                .getSensitivePropertyKeys().findAll { String k ->
+            plainNiFiProperties.getProperty(k)
+        } as Set<String>
+        assert 
protectedWrapperAroundEncrypted.getProtectedPropertyKeys().keySet() == 
EXPECTED_PROTECTED_KEYS
+    }
+
+    @Test
+    void testShouldUpdateBootstrapContentsWithKey() {
+        // Arrange
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.keyHex = KEY_HEX
+
+        List<String> originalLines = [
+                ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT,
+                "${ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX}="
+        ]
+
+        // Act
+        List<String> updatedLines = 
tool.updateBootstrapContentsWithKey(originalLines)
+        logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+        // Assert
+        assert updatedLines.size() == originalLines.size()
+        assert updatedLines.first() == originalLines.first()
+        assert updatedLines.last() == EXPECTED_KEY_LINE
+    }
+
+    @Test
+    void testUpdateBootstrapContentsWithKeyShouldOverwriteExistingKey() {
+        // Arrange
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.keyHex = KEY_HEX
+
+        List<String> originalLines = [
+                ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT,
+                "${ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX}=badKey"
+        ]
+
+        // Act
+        List<String> updatedLines = 
tool.updateBootstrapContentsWithKey(originalLines)
+        logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+        // Assert
+        assert updatedLines.size() == originalLines.size()
+        assert updatedLines.first() == originalLines.first()
+        assert updatedLines.last() == EXPECTED_KEY_LINE
+    }
+
+    @Test
+    void testShouldUpdateBootstrapContentsWithKeyAndComment() {
+        // Arrange
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.keyHex = KEY_HEX
+
+        List<String> originalLines = [
+                "${ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX}="
+        ]
+
+        // Act
+        List<String> updatedLines = 
tool.updateBootstrapContentsWithKey(originalLines.clone() as List<String>)
+        logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+        // Assert
+        assert updatedLines.size() == originalLines.size() + 1
+        assert updatedLines.first() == 
ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT
+        assert updatedLines.last() == EXPECTED_KEY_LINE
+    }
+
+    @Test
+    void testUpdateBootstrapContentsWithKeyShouldAddLines() {
+        // Arrange
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.keyHex = KEY_HEX
+
+        List<String> originalLines = []
+
+        // Act
+        List<String> updatedLines = 
tool.updateBootstrapContentsWithKey(originalLines.clone() as List<String>)
+        logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+        // Assert
+        assert updatedLines.size() == originalLines.size() + 3
+        assert updatedLines.first() == "\n"
+        assert updatedLines[1] == ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT
+        assert updatedLines.last() == EXPECTED_KEY_LINE
+    }
+
+    @Test
+    void testShouldWriteKeyToBootstrapConf() {
+        // Arrange
+        File emptyKeyFile = new 
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+        File workingFile = new File("tmp_bootstrap.conf")
+        workingFile.delete()
+
+        Files.copy(emptyKeyFile.toPath(), workingFile.toPath())
+        final List<String> originalLines = workingFile.readLines()
+        String originalKeyLine = originalLines.find { 
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) }
+        logger.info("Original key line from bootstrap.conf: 
${originalKeyLine}")
+
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-b", workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+
+        // Act
+        tool.writeKeyToBootstrapConf()
+        logger.info("Updated bootstrap.conf")
+
+        // Assert
+        final List<String> updatedLines = workingFile.readLines()
+        String updatedLine = updatedLines.find { 
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) }
+        logger.info("Updated key line: ${updatedLine}")
+
+        assert updatedLine == EXPECTED_KEY_LINE
+        assert originalLines.size() == updatedLines.size()
+
+        workingFile.deleteOnExit()
+    }
+
+    @Test
+    void testWriteKeyToBootstrapConfShouldHandleReadFailure() {
+        // Arrange
+        File emptyKeyFile = new 
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+        File workingFile = new File("tmp_bootstrap.conf")
+        workingFile.delete()
+
+        Files.copy(emptyKeyFile.toPath(), workingFile.toPath())
+        // Empty set of permissions
+        Files.setPosixFilePermissions(workingFile.toPath(), [] as Set)
+        logger.info("Set POSIX permissions to 
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-b", workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+
+        // Act
+        def msg = shouldFail(IOException) {
+            tool.writeKeyToBootstrapConf()
+            logger.info("Updated bootstrap.conf")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "The bootstrap.conf file at tmp_bootstrap.conf must 
exist and be readable and writable by the user running this tool"
+
+        workingFile.deleteOnExit()
+    }
+
+    @Test
+    void testWriteKeyToBootstrapConfShouldHandleWriteFailure() {
+        // Arrange
+        File emptyKeyFile = new 
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+        File workingFile = new File("tmp_bootstrap.conf")
+        workingFile.delete()
+
+        Files.copy(emptyKeyFile.toPath(), workingFile.toPath())
+        // Read-only set of permissions
+        Files.setPosixFilePermissions(workingFile.toPath(), 
[PosixFilePermission.OWNER_READ, PosixFilePermission.GROUP_READ, 
PosixFilePermission.OTHERS_READ] as Set)
+        logger.info("Set POSIX permissions to 
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-b", workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+
+        // Act
+        def msg = shouldFail(IOException) {
+            tool.writeKeyToBootstrapConf()
+            logger.info("Updated bootstrap.conf")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "The bootstrap.conf file at tmp_bootstrap.conf must 
exist and be readable and writable by the user running this tool"
+
+        workingFile.deleteOnExit()
+    }
+
+    @Test
+    void testShouldEncryptNiFiPropertiesWithEmptyProtectionScheme() {
+        // Arrange
+        String originalNiFiPropertiesPath = 
"src/test/resources/nifi_with_sensitive_properties_unprotected_and_empty_protection_schemes.properties"
+
+        File originalFile = new File(originalNiFiPropertiesPath)
+        List<String> originalLines = originalFile.readLines()
+        logger.info("Read ${originalLines.size()} lines from 
${originalNiFiPropertiesPath}")
+        logger.info("\n" + originalLines[0..3].join("\n") + "...")
+
+        NiFiProperties plainProperties = 
NiFiPropertiesLoader.withKey(KEY_HEX).load(originalNiFiPropertiesPath)
+        logger.info("Loaded NiFiProperties from ${originalNiFiPropertiesPath}")
+
+        ProtectedNiFiProperties protectedWrapper = new 
ProtectedNiFiProperties(plainProperties)
+        logger.info("Loaded ${plainProperties.size()} properties")
+        logger.info("There are 
${protectedWrapper.getSensitivePropertyKeys().size()} sensitive properties")
+
+        SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(KEY_HEX)
+        int protectedPropertyCount = 
protectedWrapper.protectedPropertyKeys.size()
+        logger.info("Counted ${protectedPropertyCount} protected keys")
+        assert protectedPropertyCount < 
protectedWrapper.getSensitivePropertyKeys().size()
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool(keyHex: KEY_HEX)
+
+        // Act
+        NiFiProperties encryptedProperties = 
tool.encryptSensitiveProperties(plainProperties)
+
+        // Assert
+        ProtectedNiFiProperties encryptedWrapper = new 
ProtectedNiFiProperties(encryptedProperties)
+        encryptedWrapper.getProtectedPropertyKeys().every { String key, String 
protectionScheme ->
+            logger.info("${key} is protected by ${protectionScheme}")
+            assert protectionScheme == spp.identifierKey
+        }
+
+        printProperties(encryptedWrapper)
+
+        assert encryptedWrapper.getProtectedPropertyKeys().size() == 
encryptedWrapper.getSensitivePropertyKeys().findAll {
+            encryptedWrapper.getProperty(it)
+        }.size()
+    }
+
+    @Test
+    void testShouldSerializeNiFiProperties() {
+        // Arrange
+        Properties rawProperties = [key: "value", key2: "value2"] as Properties
+        NiFiProperties properties = new StandardNiFiProperties(rawProperties)
+        logger.info("Loaded ${properties.size()} properties")
+
+        // Act
+        List<String> lines = 
ConfigEncryptionTool.serializeNiFiProperties(properties)
+        logger.info("Serialized NiFiProperties to ${lines.size()} lines")
+        logger.info("\n" + lines.join("\n"))
+
+        // Assert
+
+        // The serialization could have occurred > 1 second ago, causing a 
rolling date/time mismatch, so use regex
+        // Format -- #Fri Aug 19 16:51:16 PDT 2016
+        String datePattern = /#\w{3} \w{3} \d{2} \d{2}:\d{2}:\d{2} \w{3} \d{4}/
+
+        // One extra line for the date
+        assert lines.size() == properties.size() + 1
+        assert lines.first() =~ datePattern
+
+        rawProperties.keySet().every { String key ->
+            assert 
lines.contains("${key}=${properties.getProperty(key)}".toString())
+        }
+    }
+
+    @Test
+    void testShouldSerializeNiFiPropertiesAndPreserveFormat() {
+        // Arrange
+        String originalNiFiPropertiesPath = 
"src/test/resources/nifi_with_few_sensitive_properties_unprotected.properties"
+
+        File originalFile = new File(originalNiFiPropertiesPath)
+        List<String> originalLines = originalFile.readLines()
+        logger.info("Read ${originalLines.size()} lines from 
${originalNiFiPropertiesPath}")
+        logger.info("\n" + originalLines[0..3].join("\n") + "...")
+
+        NiFiProperties plainProperties = 
NiFiPropertiesLoader.withKey(KEY_HEX).load(originalNiFiPropertiesPath)
+        logger.info("Loaded NiFiProperties from ${originalNiFiPropertiesPath}")
+
+        ProtectedNiFiProperties protectedWrapper = new 
ProtectedNiFiProperties(plainProperties)
+        logger.info("Loaded ${plainProperties.size()} properties")
+        logger.info("There are 
${protectedWrapper.getSensitivePropertyKeys().size()} sensitive properties")
+
+        protectedWrapper.addSensitivePropertyProvider(new 
AESSensitivePropertyProvider(KEY_HEX))
+        NiFiProperties protectedProperties = 
protectedWrapper.protectPlainProperties()
+        int protectedPropertyCount = 
ProtectedNiFiProperties.countProtectedProperties(protectedProperties)
+        logger.info("Counted ${protectedPropertyCount} protected keys")
+
+        // Act
+        List<String> lines = 
ConfigEncryptionTool.serializeNiFiPropertiesAndPreserveFormat(protectedProperties,
 originalFile)
+        logger.info("Serialized NiFiProperties to ${lines.size()} lines")
+        lines.eachWithIndex { String entry, int i ->
+            logger.debug("${(i + 1).toString().padLeft(3)}: ${entry}")
+        }
+
+        // Assert
+
+        // Added n new lines for the encrypted properties
+        assert lines.size() == originalLines.size() + protectedPropertyCount
+
+        protectedProperties.getPropertyKeys().every { String key ->
+            assert 
lines.contains("${key}=${protectedProperties.getProperty(key)}".toString())
+        }
+
+        logger.info("Updated nifi.properties:")
+        logger.info("\n" * 2 + lines.join("\n"))
+    }
+
+    @Test
+    void 
testShouldSerializeNiFiPropertiesAndPreserveFormatWithExistingProtectionSchemes()
 {
+        // Arrange
+        String originalNiFiPropertiesPath = 
"src/test/resources/nifi_with_few_sensitive_properties_protected_aes.properties"
+
+        File originalFile = new File(originalNiFiPropertiesPath)
+        List<String> originalLines = originalFile.readLines()
+        logger.info("Read ${originalLines.size()} lines from 
${originalNiFiPropertiesPath}")
+        logger.info("\n" + originalLines[0..3].join("\n") + "...")
+
+        ProtectedNiFiProperties protectedProperties = 
NiFiPropertiesLoader.withKey(KEY_HEX).readProtectedPropertiesFromDisk(new 
File(originalNiFiPropertiesPath))
+        logger.info("Loaded NiFiProperties from ${originalNiFiPropertiesPath}")
+
+        logger.info("Loaded ${protectedProperties.getPropertyKeys().size()} 
properties")
+        logger.info("There are 
${protectedProperties.getSensitivePropertyKeys().size()} sensitive properties")
+        logger.info("There are 
${protectedProperties.getProtectedPropertyKeys().size()} protected properties")
+        int originalProtectedPropertyCount = 
protectedProperties.getProtectedPropertyKeys().size()
+
+        protectedProperties.addSensitivePropertyProvider(new 
AESSensitivePropertyProvider(KEY_HEX))
+        NiFiProperties encryptedProperties = 
protectedProperties.protectPlainProperties()
+        int protectedPropertyCount = 
ProtectedNiFiProperties.countProtectedProperties(encryptedProperties)
+        logger.info("Counted ${protectedPropertyCount} protected keys")
+
+        int protectedCountChange = protectedPropertyCount - 
originalProtectedPropertyCount
+        logger.info("Expected line count change: ${protectedCountChange}")
+
+        // Act
+        List<String> lines = 
ConfigEncryptionTool.serializeNiFiPropertiesAndPreserveFormat(protectedProperties,
 originalFile)
+        logger.info("Serialized NiFiProperties to ${lines.size()} lines")
+        lines.eachWithIndex { String entry, int i ->
+            logger.debug("${(i + 1).toString().padLeft(3)}: ${entry}")
+        }
+
+        // Assert
+
+        // Added n new lines for the encrypted properties
+        assert lines.size() == originalLines.size() + protectedCountChange
+
+        protectedProperties.getPropertyKeys().every { String key ->
+            assert 
lines.contains("${key}=${protectedProperties.getProperty(key)}".toString())
+        }
+
+        logger.info("Updated nifi.properties:")
+        logger.info("\n" * 2 + lines.join("\n"))
+    }
+
+    @Test
+    void 
testShouldSerializeNiFiPropertiesAndPreserveFormatWithNewPropertyAtEOF() {
+        // Arrange
+        String originalNiFiPropertiesPath = 
"src/test/resources/nifi_with_few_sensitive_properties_unprotected.properties"
+
+        File originalFile = new File(originalNiFiPropertiesPath)
+        List<String> originalLines = originalFile.readLines()
+        logger.info("Read ${originalLines.size()} lines from 
${originalNiFiPropertiesPath}")
+        logger.info("\n" + originalLines[0..3].join("\n") + "...")
+
+        NiFiProperties plainProperties = 
NiFiPropertiesLoader.withKey(KEY_HEX).load(originalNiFiPropertiesPath)
+        logger.info("Loaded NiFiProperties from ${originalNiFiPropertiesPath}")
+
+        ProtectedNiFiProperties protectedWrapper = new 
ProtectedNiFiProperties(plainProperties)
+        logger.info("Loaded ${plainProperties.size()} properties")
+        logger.info("There are 
${protectedWrapper.getSensitivePropertyKeys().size()} sensitive properties")
+
+        // Set a value for the sensitive property which is the last line in 
the file
+
+        // Groovy access to avoid duplicating entire object to add one value
+        (plainProperties as 
StandardNiFiProperties)[email protected](NiFiProperties.SECURITY_TRUSTSTORE_PASSWD,
 "thisIsABadTruststorePassword")
+
+        protectedWrapper.addSensitivePropertyProvider(new 
AESSensitivePropertyProvider(KEY_HEX))
+        NiFiProperties protectedProperties = 
protectedWrapper.protectPlainProperties()
+        int protectedPropertyCount = 
ProtectedNiFiProperties.countProtectedProperties(protectedProperties)
+        logger.info("Counted ${protectedPropertyCount} protected keys")
+
+        // Act
+        List<String> lines = 
ConfigEncryptionTool.serializeNiFiPropertiesAndPreserveFormat(protectedProperties,
 originalFile)
+        logger.info("Serialized NiFiProperties to ${lines.size()} lines")
+        lines.eachWithIndex { String entry, int i ->
+            logger.debug("${(i + 1).toString().padLeft(3)}: ${entry}")
+        }
+
+        // Assert
+
+        // Added n new lines for the encrypted properties
+        assert lines.size() == originalLines.size() + protectedPropertyCount
+
+        protectedProperties.getPropertyKeys().every { String key ->
+            assert 
lines.contains("${key}=${protectedProperties.getProperty(key)}".toString())
+        }
+
+        logger.info("Updated nifi.properties:")
+        logger.info("\n" * 2 + lines.join("\n"))
+    }
+
+    @Test
+    void testShouldWriteNiFiProperties() {
+        // Arrange
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File workingFile = new File("tmp_nifi.properties")
+        workingFile.delete()
+
+        final List<String> originalLines = inputPropertiesFile.readLines()
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", inputPropertiesFile.path, "-o", 
workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+        NiFiProperties niFiProperties = tool.loadNiFiProperties()
+        tool.@niFiProperties = niFiProperties
+        logger.info("Loaded ${niFiProperties.size()} properties from 
${inputPropertiesFile.path}")
+
+        // Act
+        tool.writeNiFiProperties()
+        logger.info("Wrote to ${workingFile.path}")
+
+        // Assert
+        final List<String> updatedLines = workingFile.readLines()
+        niFiProperties.getPropertyKeys().every { String key ->
+            assert 
updatedLines.contains("${key}=${niFiProperties.getProperty(key)}".toString())
+        }
+
+        assert originalLines == updatedLines
+
+        logger.info("Updated nifi.properties:")
+        logger.info("\n" * 2 + updatedLines.join("\n"))
+
+        workingFile.deleteOnExit()
+    }
+
+    @Test
+    void testShouldWriteNiFiPropertiesInSameLocation() {
+        // Arrange
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File workingFile = new File("tmp_nifi.properties")
+        workingFile.delete()
+        Files.copy(inputPropertiesFile.toPath(), workingFile.toPath())
+
+        final List<String> originalLines = inputPropertiesFile.readLines()
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+        NiFiProperties niFiProperties = tool.loadNiFiProperties()
+        tool.@niFiProperties = niFiProperties
+        logger.info("Loaded ${niFiProperties.size()} properties from 
${workingFile.path}")
+
+        // Act
+        tool.writeNiFiProperties()
+        logger.info("Wrote to ${workingFile.path}")
+
+        // Assert
+        final List<String> updatedLines = workingFile.readLines()
+        niFiProperties.getPropertyKeys().every { String key ->
+            assert 
updatedLines.contains("${key}=${niFiProperties.getProperty(key)}".toString())
+        }
+
+        assert originalLines == updatedLines
+
+        logger.info("Updated nifi.properties:")
+        logger.info("\n" * 2 + updatedLines.join("\n"))
+
+        assert TestAppender.events.collect {
+            it.message
+        }.contains("The source nifi.properties and destination nifi.properties 
are identical [${workingFile.path}] so the original will be 
overwritten".toString())
+
+        workingFile.deleteOnExit()
+    }
+
+    @Test
+    void testWriteNiFiPropertiesShouldHandleWriteFailureWhenFileExists() {
+        // Arrange
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File workingFile = new File("tmp_nifi.properties")
+        workingFile.delete()
+
+        Files.copy(inputPropertiesFile.toPath(), workingFile.toPath())
+        // Read-only set of permissions
+        Files.setPosixFilePermissions(workingFile.toPath(), 
[PosixFilePermission.OWNER_READ, PosixFilePermission.GROUP_READ, 
PosixFilePermission.OTHERS_READ] as Set)
+        logger.info("Set POSIX permissions to 
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", inputPropertiesFile.path, "-o", 
workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+        NiFiProperties niFiProperties = tool.loadNiFiProperties()
+        tool.@niFiProperties = niFiProperties
+        logger.info("Loaded ${niFiProperties.size()} properties from 
${inputPropertiesFile.path}")
+
+        // Act
+        def msg = shouldFail(IOException) {
+            tool.writeNiFiProperties()
+            logger.info("Wrote to ${workingFile.path}")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "The nifi.properties file at ${workingFile.path} must be 
writable by the user running this tool".toString()
+
+        workingFile.deleteOnExit()
+    }
+
+    @Test
+    void testWriteNiFiPropertiesShouldHandleWriteFailureWhenFileDoesNotExist() 
{
+        // Arrange
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        File workingFile = new File("target/tmp/tmp_nifi.properties")
+        workingFile.delete()
+
+        // Read-only set of permissions
+        Files.setPosixFilePermissions(tmpDir.toPath(), 
[PosixFilePermission.OWNER_READ, PosixFilePermission.GROUP_READ, 
PosixFilePermission.OTHERS_READ] as Set)
+        logger.info("Set POSIX permissions on parent directory to 
${Files.getPosixFilePermissions(tmpDir.toPath())}")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        String[] args = ["-n", inputPropertiesFile.path, "-o", 
workingFile.path, "-k", KEY_HEX]
+        tool.parse(args)
+        NiFiProperties niFiProperties = tool.loadNiFiProperties()
+        tool.@niFiProperties = niFiProperties
+        logger.info("Loaded ${niFiProperties.size()} properties from 
${inputPropertiesFile.path}")
+
+        // Act
+        def msg = shouldFail(IOException) {
+            tool.writeNiFiProperties()
+            logger.info("Wrote to ${workingFile.path}")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "The nifi.properties file at ${workingFile.path} must be 
writable by the user running this tool".toString()
+
+        workingFile.deleteOnExit()
+        Files.setPosixFilePermissions(tmpDir.toPath(), 
[PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE] as Set)
+        tmpDir.deleteOnExit()
+    }
+
+    @Test
+    void testShouldPerformFullOperation() {
+        // Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        Files.setPosixFilePermissions(tmpDir.toPath(), 
[PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, 
PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, 
PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, 
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, 
PosixFilePermission.OTHERS_EXECUTE] as Set)
+
+        File emptyKeyFile = new 
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(emptyKeyFile.toPath(), bootstrapFile.toPath())
+        final List<String> originalBootstrapLines = bootstrapFile.readLines()
+        String originalKeyLine = originalBootstrapLines.find {
+            it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+        }
+        logger.info("Original key line from bootstrap.conf: 
${originalKeyLine}")
+        assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX
+
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File outputPropertiesFile = new File("target/tmp/tmp_nifi.properties")
+        outputPropertiesFile.delete()
+
+        NiFiProperties inputProperties = new 
NiFiPropertiesLoader().load(inputPropertiesFile)
+        logger.info("Loaded ${inputProperties.size()} properties from input 
file")
+        ProtectedNiFiProperties protectedInputProperties = new 
ProtectedNiFiProperties(inputProperties)
+        def originalSensitiveValues = 
protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key 
-> [(key): protectedInputProperties.getProperty(key)] }
+        logger.info("Original sensitive values: ${originalSensitiveValues}")
+
+        String[] args = ["-n", inputPropertiesFile.path, "-b", 
bootstrapFile.path, "-o", outputPropertiesFile.path, "-k", KEY_HEX]
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            public void checkAssertion() {
+                final List<String> updatedPropertiesLines = 
outputPropertiesFile.readLines()
+                logger.info("Updated nifi.properties:")
+                logger.info("\n" * 2 + updatedPropertiesLines.join("\n"))
+
+                // Check that the output values for sensitive properties are 
not the same as the original (i.e. it was encrypted)
+                NiFiProperties updatedProperties = new 
NiFiPropertiesLoader().readProtectedPropertiesFromDisk(outputPropertiesFile)
+                assert updatedProperties.size() >= inputProperties.size()
+                originalSensitiveValues.every { String key, String 
originalValue ->
+                    assert updatedProperties.getProperty(key) != originalValue
+                }
+
+                // Check that the new NiFiProperties instance matches the 
output file (values still encrypted)
+                updatedProperties.getPropertyKeys().every { String key ->
+                    assert 
updatedPropertiesLines.contains("${key}=${updatedProperties.getProperty(key)}".toString())
+                }
+
+                // Check that the key was persisted to the bootstrap.conf
+                final List<String> updatedBootstrapLines = 
bootstrapFile.readLines()
+                String updatedKeyLine = updatedBootstrapLines.find {
+                    it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+                }
+                logger.info("Updated key line: ${updatedKeyLine}")
+
+                assert updatedKeyLine == EXPECTED_KEY_LINE
+                assert originalBootstrapLines.size() == 
updatedBootstrapLines.size()
+
+                // Clean up
+                outputPropertiesFile.deleteOnExit()
+                bootstrapFile.deleteOnExit()
+                tmpDir.deleteOnExit()
+            }
+        });
+
+        // Act
+        ConfigEncryptionTool.main(args)
+        logger.info("Invoked #main with ${args.join(" ")}")
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testShouldPerformFullOperationWithPassword() {
+        // Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        Files.setPosixFilePermissions(tmpDir.toPath(), 
[PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, 
PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, 
PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, 
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, 
PosixFilePermission.OTHERS_EXECUTE] as Set)
+
+        File emptyKeyFile = new 
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(emptyKeyFile.toPath(), bootstrapFile.toPath())
+        final List<String> originalBootstrapLines = bootstrapFile.readLines()
+        String originalKeyLine = originalBootstrapLines.find {
+            it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+        }
+        logger.info("Original key line from bootstrap.conf: 
${originalKeyLine}")
+        assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX
+
+        final String EXPECTED_KEY_HEX = 
ConfigEncryptionTool.deriveKeyFromPassword(PASSWORD)
+        logger.info("Derived key from password [${PASSWORD}]: 
${EXPECTED_KEY_HEX}")
+
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + EXPECTED_KEY_HEX
+
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File outputPropertiesFile = new File("target/tmp/tmp_nifi.properties")
+        outputPropertiesFile.delete()
+
+        NiFiProperties inputProperties = new 
NiFiPropertiesLoader().load(inputPropertiesFile)
+        logger.info("Loaded ${inputProperties.size()} properties from input 
file")
+        ProtectedNiFiProperties protectedInputProperties = new 
ProtectedNiFiProperties(inputProperties)
+        def originalSensitiveValues = 
protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key 
-> [(key): protectedInputProperties.getProperty(key)] }
+        logger.info("Original sensitive values: ${originalSensitiveValues}")
+
+        String[] args = ["-n", inputPropertiesFile.path, "-b", 
bootstrapFile.path, "-o", outputPropertiesFile.path, "-p", PASSWORD]
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            public void checkAssertion() {
+                final List<String> updatedPropertiesLines = 
outputPropertiesFile.readLines()
+                logger.info("Updated nifi.properties:")
+                logger.info("\n" * 2 + updatedPropertiesLines.join("\n"))
+
+                // Check that the output values for sensitive properties are 
not the same as the original (i.e. it was encrypted)
+                NiFiProperties updatedProperties = new 
NiFiPropertiesLoader().readProtectedPropertiesFromDisk(outputPropertiesFile)
+                assert updatedProperties.size() >= inputProperties.size()
+                originalSensitiveValues.every { String key, String 
originalValue ->
+                    assert updatedProperties.getProperty(key) != originalValue
+                }
+
+                // Check that the new NiFiProperties instance matches the 
output file (values still encrypted)
+                updatedProperties.getPropertyKeys().every { String key ->
+                    assert 
updatedPropertiesLines.contains("${key}=${updatedProperties.getProperty(key)}".toString())
+                }
+
+                // Check that the key was persisted to the bootstrap.conf
+                final List<String> updatedBootstrapLines = 
bootstrapFile.readLines()
+                String updatedKeyLine = updatedBootstrapLines.find {
+                    it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+                }
+                logger.info("Updated key line: ${updatedKeyLine}")
+
+                assert updatedKeyLine == EXPECTED_KEY_LINE
+                assert originalBootstrapLines.size() == 
updatedBootstrapLines.size()
+
+                // Clean up
+                outputPropertiesFile.deleteOnExit()
+                bootstrapFile.deleteOnExit()
+                tmpDir.deleteOnExit()
+            }
+        });
+
+        // Act
+        ConfigEncryptionTool.main(args)
+        logger.info("Invoked #main with ${args.join(" ")}")
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testShouldPerformFullOperationMultipleTimes() {
+        // Arrange
+        exit.expectSystemExitWithStatus(0)
+
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        Files.setPosixFilePermissions(tmpDir.toPath(), 
[PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, 
PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.GROUP_READ, 
PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE, 
PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, 
PosixFilePermission.OTHERS_EXECUTE] as Set)
+
+        File emptyKeyFile = new 
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(emptyKeyFile.toPath(), bootstrapFile.toPath())
+        final List<String> originalBootstrapLines = bootstrapFile.readLines()
+        String originalKeyLine = originalBootstrapLines.find {
+            it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+        }
+        logger.info("Original key line from bootstrap.conf: 
${originalKeyLine}")
+        assert originalKeyLine == ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX
+
+        final String EXPECTED_KEY_HEX = 
ConfigEncryptionTool.deriveKeyFromPassword(PASSWORD)
+        logger.info("Derived key from password [${PASSWORD}]: 
${EXPECTED_KEY_HEX}")
+
+        final String EXPECTED_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + EXPECTED_KEY_HEX
+
+        File inputPropertiesFile = new 
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+        File outputPropertiesFile = new File("target/tmp/tmp_nifi.properties")
+        outputPropertiesFile.delete()
+
+        NiFiProperties inputProperties = new 
NiFiPropertiesLoader().load(inputPropertiesFile)
+        logger.info("Loaded ${inputProperties.size()} properties from input 
file")
+        ProtectedNiFiProperties protectedInputProperties = new 
ProtectedNiFiProperties(inputProperties)
+        def originalSensitiveValues = 
protectedInputProperties.getSensitivePropertyKeys().collectEntries { String key 
-> [(key): protectedInputProperties.getProperty(key)] }
+        logger.info("Original sensitive values: ${originalSensitiveValues}")
+
+        String[] args = ["-n", inputPropertiesFile.path, "-b", 
bootstrapFile.path, "-o", outputPropertiesFile.path, "-p", PASSWORD, "-v"]
+
+        def msg = shouldFail {
+            logger.info("Invoked #main first time with ${args.join(" ")}")
+            ConfigEncryptionTool.main(args)
+        }
+        logger.expected(msg)
+
+        // Act
+        args = ["-n", outputPropertiesFile.path, "-b", bootstrapFile.path, 
"-p", PASSWORD, "-v"]
+
+        // Add a new property to be encrypted
+        outputPropertiesFile.text = 
outputPropertiesFile.text.replace("nifi.sensitive.props.additional.keys=", 
"nifi.sensitive.props.additional.keys=nifi.ui.banner.text")
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            public void checkAssertion() {
+                final List<String> updatedPropertiesLines = 
outputPropertiesFile.readLines()
+                logger.info("Updated nifi.properties:")
+                logger.info("\n" * 2 + updatedPropertiesLines.join("\n"))
+
+                // Check that the output values for sensitive properties are 
not the same as the original (i.e. it was encrypted)
+                NiFiProperties updatedProperties = new 
NiFiPropertiesLoader().readProtectedPropertiesFromDisk(outputPropertiesFile)
+                assert updatedProperties.size() >= inputProperties.size()
+                originalSensitiveValues.every { String key, String 
originalValue ->
+                    assert updatedProperties.getProperty(key) != originalValue
+                }
+
+                // Check that the new NiFiProperties instance matches the 
output file (values still encrypted)
+                updatedProperties.getPropertyKeys().every { String key ->
+                    assert 
updatedPropertiesLines.contains("${key}=${updatedProperties.getProperty(key)}".toString())
+                }
+
+                // Check that the key was persisted to the bootstrap.conf
+                final List<String> updatedBootstrapLines = 
bootstrapFile.readLines()
+                String updatedKeyLine = updatedBootstrapLines.find {
+                    it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+                }
+                logger.info("Updated key line: ${updatedKeyLine}")
+
+                assert updatedKeyLine == EXPECTED_KEY_LINE
+                assert originalBootstrapLines.size() == 
updatedBootstrapLines.size()
+
+                // Clean up
+                outputPropertiesFile.deleteOnExit()
+                bootstrapFile.deleteOnExit()
+                tmpDir.deleteOnExit()
+            }
+        });
+
+        logger.info("Invoked #main second time with ${args.join(" ")}")
+        ConfigEncryptionTool.main(args)
+
+        // Assert
+
+        // Assertions defined above
+    }
+}
+
+public class TestAppender extends AppenderBase<LoggingEvent> {
+    static List<LoggingEvent> events = new ArrayList<>();
+
+    @Override
+    protected void append(LoggingEvent e) {
+        synchronized (events) {
+            events.add(e);
+        }
+    }
+
+    public static void reset() {
+        synchronized (events) {
+            events.clear();
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap.conf
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap.conf 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap.conf
new file mode 100644
index 0000000..c5bd663
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap.conf
@@ -0,0 +1,72 @@
+#
+# 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.
+#
+
+# Java command to use when running NiFi
+java=java
+
+# Username to use when running NiFi. This value will be ignored on Windows.
+run.as=
+
+# Configure where NiFi's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling NiFi to shutdown before explicitly killing 
the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work 
properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# The G1GC is still considered experimental but has proven to be very 
advantageous in providing great
+# performance without significant "stop-the-world" delays.
+java.arg.13=-XX:+UseG1GC
+
+#Set headless mode by default
+java.arg.14=-Djava.awt.headless=true
+
+
+###
+# Notification Services for notifying interested parties when NiFi is stopped, 
started, dies
+###
+
+# XML File that contains the definitions of the notification services
+notification.services.file=./conf/bootstrap-notification-services.xml
+
+# In the case that we are unable to send a notification for an event, how many 
times should we retry?
+notification.max.attempts=5
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
is started?
+#nifi.start.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
is stopped?
+#nifi.stop.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
dies?
+#nifi.dead.notification.services=email-notification
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_empty_master_key.conf
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_empty_master_key.conf
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_empty_master_key.conf
new file mode 100644
index 0000000..17acc62
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/bootstrap_with_empty_master_key.conf
@@ -0,0 +1,74 @@
+#
+# 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.
+#
+
+# Java command to use when running NiFi
+java=java
+
+# Username to use when running NiFi. This value will be ignored on Windows.
+run.as=
+
+# Configure where NiFi's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling NiFi to shutdown before explicitly killing 
the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work 
properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# The G1GC is still considered experimental but has proven to be very 
advantageous in providing great
+# performance without significant "stop-the-world" delays.
+java.arg.13=-XX:+UseG1GC
+
+#Set headless mode by default
+java.arg.14=-Djava.awt.headless=true
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.bootstrap.sensitive.key=
+
+###
+# Notification Services for notifying interested parties when NiFi is stopped, 
started, dies
+###
+
+# XML File that contains the definitions of the notification services
+notification.services.file=./conf/bootstrap-notification-services.xml
+
+# In the case that we are unable to send a notification for an event, how many 
times should we retry?
+notification.max.attempts=5
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
is started?
+#nifi.start.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
is stopped?
+#nifi.stop.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
dies?
+#nifi.dead.notification.services=email-notification
\ No newline at end of file

Reply via email to