Repository: nifi
Updated Branches:
  refs/heads/master 82ac81553 -> 6d06defa6


http://git-wip-us.apache.org/repos/asf/nifi/blob/6d06defa/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
index 6b056d7..12ed84f 100644
--- 
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
@@ -16,15 +16,22 @@
  */
 package org.apache.nifi.properties
 
+import groovy.json.JsonBuilder
+import groovy.json.JsonSlurper
+import org.apache.commons.cli.CommandLine
+import org.apache.commons.cli.CommandLineParser
+import org.apache.commons.cli.DefaultParser
 import org.apache.commons.lang3.SystemUtils
 import org.apache.log4j.AppenderSkeleton
 import org.apache.log4j.spi.LoggingEvent
 import org.apache.nifi.encrypt.StringEncryptor
+import org.apache.nifi.security.util.crypto.scrypt.Scrypt
 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.jce.provider.BouncyCastleProvider
+import org.bouncycastle.util.encoders.Hex
 import org.junit.After
 import org.junit.AfterClass
 import org.junit.Assume
@@ -65,6 +72,17 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
     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"
+
+    private static final String STATIC_SALT = 
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUV"
+    private static final String SCRYPT_SALT_PATTERN = 
/\$\w{2}\$\w{5,}\$[\w\/\=\+]+/
+
+    // Hash of "password" with 00 * 16 salt
+    private static
+    final String HASHED_PASSWORD = 
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM"
+    // Hash of [key derived from "password"] with 00 * 16 salt
+    private static
+    final String HASHED_KEY_HEX = 
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$pJOGA9sPL+pRzynnwt6G2FfVTyLQdbKSbk6W8IKId8E"
+
     // From ConfigEncryptionTool.deriveKeyFromPassword("thisIsABadPassword")
     private static
     final String PASSWORD_KEY_HEX_256 = 
"2C576A9585DB862F5ECBEE5B4FFFCCA14B18D8365968D7081651006507AD2BDE"
@@ -107,6 +125,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
 
     @Before
     void setUp() throws Exception {
+        // Manually override the constant path to allow for easy cleanup
+        ConfigEncryptionTool.secureHashPath = "target/tmp/secure_hash.key"
     }
 
     @After
@@ -406,6 +426,81 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
     }
 
     @Test
+    void testParseShouldFailIfMigrationPasswordAndHashedPasswordBothProvided() 
{
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        def msg = shouldFail {
+            tool.parse("-m -n nifi.properties -w oldPassword -z 
oldPasswordHashed".split(" ") as String[])
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' 
arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' 
cannot be used"
+    }
+
+    @Test
+    void testParseShouldFailIfMigrationPasswordAndHashedKeyBothProvided() {
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        def msg = shouldFail {
+            tool.parse("-m -n nifi.properties -w oldPassword -y 
oldKeyHashed".split(" ") as String[])
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' 
arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' 
cannot be used"
+    }
+
+    @Test
+    void testParseShouldFailIfMigrationKeyAndHashedPasswordBothProvided() {
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        def msg = shouldFail {
+            tool.parse("-m -n nifi.properties -e oldKey -z 
oldPasswordHashed".split(" ") as String[])
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' 
arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' 
cannot be used"
+    }
+
+    @Test
+    void testParseShouldFailIfMigrationKeyAndHashedKeyBothProvided() {
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        def msg = shouldFail {
+            tool.parse("-m -n nifi.properties -e oldKey -y 
oldKeyHashed".split(" ") as String[])
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "If the '-w'/'--oldPassword' or '-e'/'--oldKey' 
arguments are present, '-z'/'--secureHashPassword' and '-y'/'--secureHashKey' 
cannot be used"
+    }
+
+    @Test
+    void testParseShouldFailIfHashedPasswordAndHashedKeyBothProvided() {
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        // Act
+        def msg = shouldFail {
+            tool.parse("-m -n nifi.properties -z oldPasswordHashed -y 
oldKeyHashed".split(" ") as String[])
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "Only one of '-z'/'--secureHashPassword' and 
'-y'/'--secureHashKey' can be used together"
+    }
+
+    @Test
     void testParseShouldFailIfPropertiesAndProvidersMissing() {
         // Arrange
         ConfigEncryptionTool tool = new ConfigEncryptionTool()
@@ -1441,8 +1536,7 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         assert msg == "The nifi.properties file at ${workingFile.path} must be 
writable by the user running this tool".toString()
 
         workingFile.deleteOnExit()
-        setFilePermissions(tmpDir, [PosixFilePermission.OWNER_READ, 
PosixFilePermission.OWNER_WRITE])
-        tmpDir.deleteOnExit()
+        setupTmpDir()
     }
 
     @Ignore("Setting the Windows file permissions fails in the test harness, 
so the test does not throw the expected exception")
@@ -1915,6 +2009,621 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         // Assertions in common method above
     }
 
+    /**
+     * Helper method to execute key migration test for varying combinations of 
new key/password with securely hashed key/password.
+     *
+     * @param scenario a human-readable description of the test scenario
+     * @param scenarioArgs a list of the arguments specific to this scenario 
to be passed to the tool
+     * @param oldHashedPassword the secure hash of the original password
+     * @param newPassword the new password
+     * @param oldHashedKeyHex the original key hex (if present, original 
hashed password is ignored)
+     * @param newKeyHex the new key hex (if present, new password is ignored; 
if not, this is derived)
+     */
+    private void performSecureHashKeyMigration(String scenario, List 
scenarioArgs, String oldHashedPassword = HASHED_PASSWORD, String newPassword = 
PASSWORD.reverse(), String oldHashedKeyHex = "", String newKeyHex = "", int 
desiredExitCode = 0) {
+        // Arrange
+        exit.expectSystemExitWithStatus(desiredExitCode)
+
+        // Initial set up
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        setFilePermissions(tmpDir, [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])
+
+        String bootstrapPath = isUnlimitedStrengthCryptoAvailable() ? 
"src/test/resources/bootstrap_with_master_key_password.conf" :
+                
"src/test/resources/bootstrap_with_master_key_password_128.conf"
+        File originalKeyFile = new File(bootstrapPath)
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(originalKeyFile.toPath(), bootstrapFile.toPath())
+        final List<String> originalBootstrapLines = bootstrapFile.readLines()
+        String originalKeyLine = originalBootstrapLines.find {
+            it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX)
+        }
+
+        // Copy the hashed credentials file
+        String secureHashedPasswordPath = isUnlimitedStrengthCryptoAvailable() 
? "src/test/resources/secure_hash.key" :
+                "src/test/resources/secure_hash_128.key"
+        File originalSecureHashedPasswordFile = new 
File(secureHashedPasswordPath)
+        File secureHashedFile = new File("target/tmp/tmp_secure_hash.key")
+        secureHashedFile.delete()
+        Files.copy(originalSecureHashedPasswordFile.toPath(), 
secureHashedFile.toPath())
+
+        // Perform necessary key derivations
+        if (!newKeyHex) {
+            newKeyHex = ConfigEncryptionTool.deriveKeyFromPassword(newPassword)
+            logger.info("Migration key derived from password [${newPassword}]: 
\t${newKeyHex}")
+        } else {
+            logger.info("Migration key provided directly: \t${newKeyHex}")
+        }
+
+        // Extract old key hex from bootstrap.conf
+        String oldKeyHex = originalKeyLine.split("=", 2).last()
+        logger.info("Extracted old key hex from bootstrap.conf: ${oldKeyHex}")
+
+        String inputPropertiesPath = isUnlimitedStrengthCryptoAvailable() ?
+                
"src/test/resources/nifi_with_sensitive_properties_protected_aes_password.properties"
 :
+                
"src/test/resources/nifi_with_sensitive_properties_protected_aes_password_128.properties"
+        File inputPropertiesFile = new File(inputPropertiesPath)
+        File outputPropertiesFile = new File("target/tmp/tmp_nifi.properties")
+        outputPropertiesFile.delete()
+
+        // Log original sensitive properties (encrypted with first key)
+        NiFiProperties inputProperties = 
NiFiPropertiesLoader.withKey(oldKeyHex).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}")
+
+        final String EXPECTED_NEW_KEY_LINE = 
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + newKeyHex
+
+        // Act
+        String[] args = ["-n", inputPropertiesFile.path,
+                         "-b", bootstrapFile.path,
+                         "-o", outputPropertiesFile.path,
+                         "-m",
+                         "-v"]
+
+        List<String> localArgs = args + scenarioArgs
+        logger.info("Running [${scenario}] with args: ${localArgs}")
+
+        // If an error is expected, check that the nifi.properties file 
doesn't exist (i.e. nothing happened)
+        Assertion assertion
+        if (desiredExitCode != 0) {
+            assertion = new Assertion() {
+                void checkAssertion() {
+                    assert !outputPropertiesFile.exists()
+                    logger.expected("No output nifi.properties found")
+
+                    // Check that the key was NOT 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 == originalKeyLine
+                    assert originalBootstrapLines.size() == 
updatedBootstrapLines.size()
+
+                    // Check that the secure hash was NOT persisted to the 
secure_hash.key
+                    final List<String> updatedSecureHashLines = 
secureHashedFile.readLines()
+                    String updatedSecureHashKeyLine = 
updatedSecureHashLines.find { it.startsWith("secureHashKey=") }
+                    logger.info("'Updated' secure hash lines: 
\n${updatedSecureHashLines.join("\n")}")
+
+                    int expectedSecureHashLineCount = 1
+
+                    List<String> originalSecureHashLines = 
originalSecureHashedPasswordFile.readLines()
+
+                    // Only evaluate the secure hash password line if the raw 
password was provided (otherwise, can't store hash)
+                    if (newPassword) {
+                        String updatedSecureHashPasswordLine = 
updatedSecureHashLines.find {
+                            it.startsWith("secureHashPassword=")
+                        }
+                        expectedSecureHashLineCount = 2
+
+                        logger.info("Asserting 
\n${updatedSecureHashPasswordLine} == \n${originalSecureHashLines.last()}")
+                        assert updatedSecureHashPasswordLine == 
originalSecureHashLines.last()
+                    }
+
+                    logger.info("Asserting \n${updatedSecureHashKeyLine} == 
\n${originalSecureHashLines.first()}")
+                    assert updatedSecureHashKeyLine == 
originalSecureHashLines.first()
+                    assert updatedSecureHashLines.size() == 
expectedSecureHashLineCount
+
+                    // Clean up
+                    outputPropertiesFile.deleteOnExit()
+                    bootstrapFile.deleteOnExit()
+                    tmpDir.deleteOnExit()
+                    secureHashedFile.deleteOnExit()
+                }
+            }
+        } else {
+            assertion = new Assertion() {
+                void checkAssertion() {
+                    assert outputPropertiesFile.exists()
+                    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 re-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_NEW_KEY_LINE
+                    assert originalBootstrapLines.size() == 
updatedBootstrapLines.size()
+
+                    // Check that the secure hash was persisted to the 
secure_hash.key
+                    final List<String> updatedSecureHashLines = 
secureHashedFile.readLines()
+                    String updatedSecureHashKeyLine = 
updatedSecureHashLines.find { it.startsWith("secureHashKey=") }
+                    logger.info("Updated secure hash lines: 
\n${updatedSecureHashLines.join("\n")}")
+
+                    int expectedSecureHashLineCount = 1
+
+                    // Extract the salt(s) so the credentials can be hashed 
with the same salt
+                    final String keySalt = 
updatedSecureHashKeyLine.find(SCRYPT_SALT_PATTERN)
+                    logger.info("Extracted key salt:      ${keySalt}")
+
+                    final String EXPECTED_NEW_SECURE_HASH_KEY_LINE = 
"secureHashKey=${ConfigEncryptionTool.secureHashKey(newKeyHex, keySalt)}"
+
+                    // Only evaluate the secure hash password line if the raw 
password was provided (otherwise, can't store hash)
+                    if (newPassword) {
+                        String updatedSecureHashPasswordLine = 
updatedSecureHashLines.find {
+                            it.startsWith("secureHashPassword=")
+                        }
+                        final String passwordSalt = 
updatedSecureHashPasswordLine.find(SCRYPT_SALT_PATTERN)
+                        logger.info("Extracted password salt: ${passwordSalt}")
+                        final String EXPECTED_NEW_SECURE_HASH_PASSWORD_LINE = 
"secureHashPassword=${ConfigEncryptionTool.secureHashPassword(newPassword, 
passwordSalt)}"
+                        expectedSecureHashLineCount = 2
+
+                        logger.info("Asserting 
\n${updatedSecureHashPasswordLine} == 
\n${EXPECTED_NEW_SECURE_HASH_PASSWORD_LINE}")
+                        assert updatedSecureHashPasswordLine == 
EXPECTED_NEW_SECURE_HASH_PASSWORD_LINE
+                    }
+
+                    logger.info("Asserting \n${updatedSecureHashKeyLine} == 
\n${EXPECTED_NEW_SECURE_HASH_KEY_LINE}")
+                    assert updatedSecureHashKeyLine == 
EXPECTED_NEW_SECURE_HASH_KEY_LINE
+                    assert updatedSecureHashLines.size() == 
expectedSecureHashLineCount
+
+                    // Clean up
+                    outputPropertiesFile.deleteOnExit()
+                    bootstrapFile.deleteOnExit()
+                    tmpDir.deleteOnExit()
+                    secureHashedFile.deleteOnExit()
+                }
+            }
+        }
+
+        exit.checkAssertionAfterwards(assertion)
+
+        logger.info("Migrating key (${scenario}) with ${localArgs.join(" ")}")
+
+        // Override the "final" secure hash file path
+        ConfigEncryptionTool.secureHashPath = secureHashedFile.path
+        ConfigEncryptionTool.main(localArgs as String[])
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    /**
+     * Ideally all of the combination tests would be a single test with 
iterative argument lists, but due to the System.exit(), it can only be captured 
once per test.
+     */
+    @Test
+    void testShouldMigrateFromHashedPasswordToPassword() {
+        // Arrange
+        String scenario = "hashed password to password"
+        def args = ["-z", HASHED_PASSWORD, "-p", PASSWORD.reverse()]
+
+        // Act
+        performSecureHashKeyMigration(scenario, args, HASHED_PASSWORD, 
PASSWORD.reverse())
+
+        // Assert
+
+        // Assertions in common method above
+    }
+
+    @Test
+    void testShouldMigrateFromHashedPasswordToKey() {
+        // Arrange
+        String scenario = "hashed password to key"
+        def args = ["-z", HASHED_PASSWORD, "-k", KEY_HEX]
+
+        // Act
+        performSecureHashKeyMigration(scenario, args, HASHED_PASSWORD, "", "", 
KEY_HEX)
+
+        // Assert
+
+        // Assertions in common method above
+    }
+
+    @Test
+    void testShouldMigrateFromHashedKeyToPassword() {
+        // Arrange
+        String scenario = "hashed key to password"
+        def args = ["-y", HASHED_KEY_HEX, "-p", PASSWORD.reverse()]
+
+        // Act
+        performSecureHashKeyMigration(scenario, args, "", PASSWORD.reverse(), 
HASHED_KEY_HEX, "")
+
+        // Assert
+
+        // Assertions in common method above
+    }
+
+    @Test
+    void testShouldMigrateFromHashedKeyToKey() {
+        // Arrange
+        String scenario = "hashed key to key"
+        def args = ["-y", HASHED_KEY_HEX, "-k", KEY_HEX]
+
+        // Act
+        performSecureHashKeyMigration(scenario, args, "", "", HASHED_KEY_HEX, 
KEY_HEX)
+
+        // Assert
+
+        // Assertions in common method above
+    }
+
+    @Test
+    void testShouldFailToMigrateFromIncorrectHashedPasswordToPassword() {
+        // Arrange
+        String scenario = "(incorrect) hashed password to password"
+        final String INCORRECT_HASHED_PASSWORD = 
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$thisIsDefinitelyNotTheCorrectPasswordHashxx"
+        def args = ["-z", INCORRECT_HASHED_PASSWORD, "-p", PASSWORD.reverse()]
+
+        // Act
+        performSecureHashKeyMigration(scenario, args, HASHED_PASSWORD, 
PASSWORD.reverse(), "", "", 4)
+
+        // Assert
+
+        // Assertions in common method above
+    }
+
+    @Test
+    void testShouldDeriveSecureHashOfPassword() {
+        // Arrange
+        def testPasswords = ["password", "thisIsABadPassword", 
"bWZerzZo6fw9ZrDz*YfM6CVj2Ktx(YJd"]
+
+        // All zero, 22 (16B) Base64 static, 40 (32B) Base64 randomly-generated
+        def salts = [
+                Hex.decode("00" * 16),
+                Base64.decoder.decode("ABCDEFGHIJKLMNOPQRSTUV"),
+                
Base64.decoder.decode("eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc=")
+        ]
+
+        // These values were generated using CET#secureHashPassword() and 
verified using src/test/resources/scrypt.py
+        def passwordHashes = [
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$hxU5g0eH6sRkBqcsiApI8jxvKRT+2QMCenV0GToiMQ8",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$99aTTB39TJo69aZCONQmRdyWOgYsDi+1MI+8D0EgMNM"
+        ]
+        def badPasswordHashes = [
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$Gk7K9YmlsWbd8FS7e4RKVWnkg9vlsqYnlD593pJ71gg",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$Ri78VZbrp2cCVmGh2a9Nbfdov8LPnFb49MYyzPCaXmE",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$rZIrP2qdIY7LN4CZAMgbCzl3YhXz6WhaNyXJXqFIjaI"
+        ]
+        def randomPasswordHashes = [
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$GxH68bGykmPDZ6gaPIGOONOT2omlZ7cd0xlcZ9UsY/0",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$KLGZjWlo59sbCbtmTg5b4k0Nu+biWZRRzhPhN7K5kkI",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$6Ql6Efd2ac44ERoV31CL3Q0J3LffNZKN4elyMHux99Y"
+        ]
+
+        def expectedHashes = [
+                (testPasswords[0]): passwordHashes,
+                (testPasswords[1]): badPasswordHashes,
+                (testPasswords[2]): randomPasswordHashes
+        ]
+
+        // Low cost factors for performance
+        int n = 2**4
+        int r = 8
+        int p = 1
+        logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
+
+        // Act
+        testPasswords.each { String password ->
+            salts.eachWithIndex { byte[] rawSalt, int i ->
+                logger.info("Hashing '${password}' with salt 
${Base64.encoder.encodeToString(rawSalt)}")
+                String formattedSalt = Scrypt.formatSalt(rawSalt, n, r, p)
+                logger.info("Formatted salt: ${formattedSalt}")
+                String generatedHash = 
ConfigEncryptionTool.secureHashPassword(password, formattedSalt)
+                logger.info("Generated hash: ${generatedHash}")
+
+                // Assert
+                String expectedHash = expectedHashes[(password)][i]
+                logger.info("Comparing to expectedHashes['${password}'][${i}]: 
${expectedHash}")
+
+                // Remember to perform constant-time equality check in 
production code
+                assert generatedHash == expectedHash
+            }
+        }
+    }
+
+    @Test
+    void testShouldDeriveSecureHashOfKey() {
+        // Arrange
+        def testKeys = [
+                "00" * 32,
+                
"0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210",
+                
"0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"
+        ]
+
+        // All zero, 22 (16B) Base64 static, 40 (32B) Base64 randomly-generated
+        def salts = [
+                Hex.decode("00" * 16),
+                Base64.decoder.decode("ABCDEFGHIJKLMNOPQRSTUV"),
+                
Base64.decoder.decode("eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc=")
+        ]
+
+        // These values were generated using CET#secureHashKey() and verified 
using src/test/resources/scrypt.py
+        def zeroHashes = [
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$pOoIk4K9OPYxusXBFNGtEaoHzIIxlgDOTiVO9OiLJrE",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$kQJ7CeAt5qHK4/r2lMnuBzNyBt1h1WDDkmgXH7N0hRc",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$diExTMVvETmC6gjKx+9ITn1L/0FOYNHeQq2oPLMsFvY"
+        ]
+        def uppercaseHashes = [
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$K5uQBtbkmq2b2M1H6kX/U7g5QiPgmoLCuJYfpOar8w4",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$TbPrKP7+/xPlc74L15QFG+iDqIysPW/dOFVRaj4Rk/k",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$yGpGz7FyBE3nf8Ed/o84o8Glyd4m091HxdVQEhN55zI"
+        ]
+
+        // Should be identical to uppercase hashes due to case-normalization 
in method
+        def lowercaseHashes = [
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$K5uQBtbkmq2b2M1H6kX/U7g5QiPgmoLCuJYfpOar8w4",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$TbPrKP7+/xPlc74L15QFG+iDqIysPW/dOFVRaj4Rk/k",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$yGpGz7FyBE3nf8Ed/o84o8Glyd4m091HxdVQEhN55zI"
+        ]
+
+        def expectedHashes = [
+                (testKeys[0]): zeroHashes,
+                (testKeys[1]): uppercaseHashes,
+                (testKeys[2]): lowercaseHashes
+        ]
+
+        // Low cost factors for performance
+        int n = 2**4
+        int r = 8
+        int p = 1
+        logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
+
+        // Act
+        testKeys.each { String key ->
+            salts.eachWithIndex { byte[] rawSalt, int i ->
+                logger.info("Hashing '${key}' with salt 
${Base64.encoder.encodeToString(rawSalt)}")
+                String formattedSalt = Scrypt.formatSalt(rawSalt, n, r, p)
+                logger.info("Formatted salt: ${formattedSalt}")
+                String generatedHash = ConfigEncryptionTool.secureHashKey(key, 
formattedSalt)
+                logger.info("Generated hash: ${generatedHash}")
+
+                // Assert
+                String expectedHash = expectedHashes[(key)][i]
+                logger.info("Comparing to expectedHashes['${key}'][${i}]: 
${expectedHash}")
+
+                // Remember to perform constant-time equality check in 
production code
+                assert generatedHash == expectedHash
+            }
+        }
+    }
+
+    @Test
+    void testShouldVerifySecureHashOfPassword() {
+        // Arrange
+        String password = "password"
+
+        // This is a known existing hash of "password"
+        String existingHashedPassword = 
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM"
+        logger.info("Known existing hash: ${existingHashedPassword}")
+
+        // Low cost factors for performance
+        int n = 2**4
+        int r = 8
+        int p = 1
+        logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
+
+        byte[] rawSalt = Hex.decode("00" * 16)
+
+        // This is a generated hash of "password"
+        String formattedSalt = Scrypt.formatSalt(rawSalt, n, r, p)
+        logger.info("Formatted salt: ${formattedSalt}")
+        String hashedPassword = 
ConfigEncryptionTool.secureHashPassword(password, formattedSalt)
+        logger.info("Generated hash: ${hashedPassword}")
+
+        // This is a generated hash of "password" using a different salt
+        byte[] otherRawSalt = Hex.decode("01" * 16)
+        String otherFormattedSalt = Scrypt.formatSalt(otherRawSalt, n, r, p)
+        logger.info("Formatted salt: ${otherFormattedSalt}")
+        String otherHashedPassword = 
ConfigEncryptionTool.secureHashPassword(password, otherFormattedSalt)
+        logger.info("Generated hash: ${otherHashedPassword}")
+
+        // Act
+        logger.info("Checking \n${existingHashedPassword} against 
hash(${password}, ${formattedSalt}) -> \n${hashedPassword}")
+        boolean hashIsIdentical = 
ConfigEncryptionTool.checkHashedValue(existingHashedPassword, hashedPassword)
+        logger.info("Hash values equal: ${hashIsIdentical}")
+
+        logger.info("Checking \n${existingHashedPassword} against 
hash(${password}, ${otherFormattedSalt}) -> \n${otherHashedPassword}")
+        boolean otherHashIsIdentical = 
ConfigEncryptionTool.checkHashedValue(existingHashedPassword, 
otherHashedPassword)
+        logger.info("Hash values equal: ${otherHashIsIdentical}")
+
+        // Assert
+        assert hashIsIdentical
+        assert !otherHashIsIdentical
+    }
+
+    @Test
+    void testCheckHashedValueShouldVerifyScryptFormat() {
+        // Arrange
+        def validHashes = [
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$hxU5g0eH6sRkBqcsiApI8jxvKRT+2QMCenV0GToiMQ8",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$99aTTB39TJo69aZCONQmRdyWOgYsDi+1MI+8D0EgMNM",
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$Gk7K9YmlsWbd8FS7e4RKVWnkg9vlsqYnlD593pJ71gg",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$Ri78VZbrp2cCVmGh2a9Nbfdov8LPnFb49MYyzPCaXmE",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$rZIrP2qdIY7LN4CZAMgbCzl3YhXz6WhaNyXJXqFIjaI",
+                
"\$s0\$40801\$AAAAAAAAAAAAAAAAAAAAAA\$GxH68bGykmPDZ6gaPIGOONOT2omlZ7cd0xlcZ9UsY/0",
+                
"\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$KLGZjWlo59sbCbtmTg5b4k0Nu+biWZRRzhPhN7K5kkI",
+                
"\$s0\$40801\$eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc\$6Ql6Efd2ac44ERoV31CL3Q0J3LffNZKN4elyMHux99Y"
+        ]
+
+        // Some of these are valid "scrypt" hashes but do not conform to the 
additional requirements NiFi imposes
+        def invalidHashes = [
+                
"\$s1\$40801\$AAAAAAA\$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM",
+                
"\$s0\$\$ABCDEFGHIJKLMNOPQRSTUQ\$hxU5g0eH6sRkBqcsiApI8jxvKRT+2QMCenV0GToiMQ8",
+                "\$s0\$40801\$\$99aTTB39TJo69aZCONQmRdyWOgYsDi+1MI+8D0EgMNM",
+                
"\$s0\$40801\$!!!!\$Gk7K9YmlsWbd8FS7e4RKVWnkg9vlsqYnlD593pJ71gg",
+                "\$s0\$40801\$ABCDEFGHIJKLMNOPQRSTUQ\$xxxx",
+        ]
+
+        // Low cost factors for performance
+        int n = 2**4
+        int r = 8
+        int p = 1
+        logger.info("Cost factors for test: N=${n}, R=${r}, P=${p}")
+
+        // Act
+        logger.info("Checking ${validHashes.size()} valid hashes")
+        validHashes.each { String hash ->
+            logger.info("Verifying hash format: ${hash}")
+            boolean validFormat = ConfigEncryptionTool.verifyHashFormat(hash)
+            logger.info("Valid format: ${validFormat}")
+
+            // Assert
+            assert validFormat
+        }
+
+        logger.info("Checking ${invalidHashes.size()} invalid hashes")
+        invalidHashes.each { String invalidHash ->
+            logger.info("Verifying hash format: ${invalidHash}")
+            boolean validFormat = 
ConfigEncryptionTool.verifyHashFormat(invalidHash)
+            logger.info("Valid format: ${validFormat}")
+
+            // Assert
+            assert !validFormat
+        }
+    }
+
+    @Test
+    void testGetMigrationKeyShouldVerifySecureHashOfPassword() {
+        // Arrange
+        File bootstrapWithKeyFile = new 
File("src/test/resources/bootstrap_with_master_key_password.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(bootstrapWithKeyFile.toPath(), bootstrapFile.toPath())
+
+        String expectedMigrationKey = bootstrapFile.readLines().find {
+            it.startsWith("nifi.bootstrap.sensitive.key=")
+        }.split("=").last()
+        logger.info("Retrieved expected migration key ${expectedMigrationKey} 
from bootstrap.conf")
+
+        File secureHashSourceFile = new 
File("src/test/resources/secure_hash.key")
+        File secureHashFile = new File("target/tmp/secure_hash.key")
+        secureHashFile.delete()
+
+        Files.copy(secureHashSourceFile.toPath(), secureHashFile.toPath())
+
+        // The second line in the file is for the password
+        String expectedHash = 
secureHashFile.readLines().last().split("=").last()
+        logger.info("Retrieved expected hash ${expectedHash} from 
secure_hash.key")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.usingSecureHash = true
+        tool.secureHashPath = secureHashFile.path
+        tool.bootstrapConfPath = bootstrapFile.path
+
+        String correctHash = expectedHash
+        String incorrectHash = correctHash[0..-10] + ("x" * 9)
+
+        // Act
+        tool.secureHashPassword = correctHash
+        logger.info("Trying to retrieve migration key comparing: \n" +
+                "Command-line provided hash: ${correctHash}\n" +
+                " Hash from secure_hash.key: ${expectedHash}")
+        String correctRetrievedMigrationKey = tool.getMigrationKey()
+        logger.info("  [Correct] Retrieved migration key: 
${correctRetrievedMigrationKey}")
+
+        tool.secureHashPassword = incorrectHash
+        logger.info("Trying to retrieve migration key comparing: \n" +
+                "Command-line provided hash: ${incorrectHash}\n" +
+                " Hash from secure_hash.key: ${expectedHash}")
+        def msg = shouldFail() {
+            String incorrectRetrievedMigrationKey = tool.getMigrationKey()
+            logger.info("[Incorrect] Retrieved migration key: 
${incorrectRetrievedMigrationKey}")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert correctRetrievedMigrationKey == expectedMigrationKey
+        assert msg =~ "The provided hashed key/password is not correct"
+    }
+
+    @Test
+    void testGetMigrationKeyShouldVerifySecureHashOfKey() {
+        // Arrange
+        File bootstrapWithKeyFile = new 
File("src/test/resources/bootstrap_with_master_key_password.conf")
+        File bootstrapFile = new File("target/tmp/tmp_bootstrap.conf")
+        bootstrapFile.delete()
+
+        Files.copy(bootstrapWithKeyFile.toPath(), bootstrapFile.toPath())
+
+        String expectedMigrationKey = bootstrapFile.readLines().find {
+            it.startsWith("nifi.bootstrap.sensitive.key=")
+        }.split("=").last()
+        logger.info("Retrieved expected migration key ${expectedMigrationKey} 
from bootstrap.conf")
+
+        File secureHashSourceFile = new 
File("src/test/resources/secure_hash.key")
+        File secureHashFile = new File("target/tmp/secure_hash.key")
+        secureHashFile.delete()
+
+        Files.copy(secureHashSourceFile.toPath(), secureHashFile.toPath())
+
+        // The first line in the file is for the key
+        String expectedHash = 
secureHashFile.readLines().first().split("=").last()
+        logger.info("Retrieved expected hash ${expectedHash} from 
secure_hash.key")
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.usingSecureHash = true
+        tool.secureHashPath = secureHashFile.path
+        tool.bootstrapConfPath = bootstrapFile.path
+
+        String correctHash = expectedHash
+        String incorrectHash = correctHash[0..-10] + ("x" * 9)
+
+        // Act
+        tool.secureHashKey = correctHash
+        logger.info("Trying to retrieve migration key comparing: \n" +
+                "Command-line provided hash: ${correctHash}\n" +
+                " Hash from secure_hash.key: ${expectedHash}")
+        String correctRetrievedMigrationKey = tool.getMigrationKey()
+        logger.info("  [Correct] Retrieved migration key: 
${correctRetrievedMigrationKey}")
+
+        tool.secureHashKey = incorrectHash
+        logger.info("Trying to retrieve migration key comparing: \n" +
+                "Command-line provided hash: ${incorrectHash}\n" +
+                " Hash from secure_hash.key: ${expectedHash}")
+        def msg = shouldFail() {
+            String incorrectRetrievedMigrationKey = tool.getMigrationKey()
+            logger.info("[Incorrect] Retrieved migration key: 
${incorrectRetrievedMigrationKey}")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert correctRetrievedMigrationKey == expectedMigrationKey
+        assert msg =~ "The provided hashed key/password is not correct"
+    }
+
     @Test
     void testShouldDecryptLoginIdentityProviders() {
         // Arrange
@@ -3463,7 +4172,9 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
                 def originalAuthorizersParsedXml = new 
XmlSlurper().parseText(originalAuthorizersXmlContent)
                 def updatedAuthorizersParsedXml = new 
XmlSlurper().parseText(updatedAuthorizersXmlContent)
                 assert originalAuthorizersParsedXml != 
updatedAuthorizersParsedXml
-                assert originalAuthorizersParsedXml.'**'.findAll { 
it.@encryption } != updatedAuthorizersParsedXml.'**'.findAll {
+                assert originalAuthorizersParsedXml.'**'.findAll {
+                    it.@encryption
+                } != updatedAuthorizersParsedXml.'**'.findAll {
                     it.@encryption
                 }
                 def authorizersEncryptedValues = 
updatedAuthorizersParsedXml.userGroupProvider.find {
@@ -3476,7 +4187,9 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
                 }
                 // Check that the comments are still there
                 def authorizersTrimmedLines = 
inputAuthorizersFile.readLines().collect { it.trim() }.findAll { it }
-                def authorizersTrimmedSerializedLines = 
updatedAuthorizersXmlContent.split("\n").collect { it.trim() }.findAll { it }
+                def authorizersTrimmedSerializedLines = 
updatedAuthorizersXmlContent.split("\n").collect {
+                    it.trim()
+                }.findAll { it }
                 assert authorizersTrimmedLines.size() == 
authorizersTrimmedSerializedLines.size()
 
                 /*** Bootstrap assertions ***/
@@ -3998,7 +4711,9 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
                 // Assert
                 // Get the updated nifi.properties and check the sensitive key
                 final List<String> updatedPropertiesLines = 
workingNiFiPropertiesFile.readLines()
-                String updatedSensitiveKeyLine = updatedPropertiesLines.find { 
it.startsWith(NiFiProperties.SENSITIVE_PROPS_KEY) }
+                String updatedSensitiveKeyLine = updatedPropertiesLines.find {
+                    it.startsWith(NiFiProperties.SENSITIVE_PROPS_KEY)
+                }
                 logger.info("Updated key line: ${updatedSensitiveKeyLine}")
 
                 // Check that the output values for everything are the same 
except the sensitive props key
@@ -4436,7 +5151,199 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
         assert tool.loadFlowXml() == xmlContent
     }
 
-    // TODO: Test with 128/256-bit available
+    @Test
+    void testShouldDetectActionFlags() {
+        // Arrange
+        final def HELP_AND_VERBOSE_ARGS = [["-h", "--help"], ["-v", 
"--verbose"]]
+        final List<String> IGNORED_ARGS = ["currentHashParams"]
+
+        // Create a list with combinations of h[elp] and v[erbose], individual 
flags, and empty flag
+        def args = GroovyCollections.combinations(HELP_AND_VERBOSE_ARGS as 
Iterable) + HELP_AND_VERBOSE_ARGS.flatten().collect {
+            [it]
+        } + [[""]]
+        String acceptableArg = "--currentHashParams"
+        String unacceptableArg = "--migrate"
+
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+        tool.isVerbose = true
+        CommandLineParser parser = new DefaultParser()
+
+        // Act
+        args.each { List<String> invocationArgs ->
+            // Run each scenario with an allowed argument and without
+            [IGNORED_ARGS, []].each { List<String> acceptableArgs ->
+                // Check ""/-h/-v alone
+                logger.info("Checking '${invocationArgs.join(" ")}' with 
acceptable args: ${acceptableArgs}")
+                CommandLine commandLine = 
parser.parse(ConfigEncryptionTool.getCliOptions(), invocationArgs as String[])
+                boolean cleanRun = tool.commandLineHasActionFlags(commandLine, 
acceptableArgs)
+                logger.info("Clean run has action flags: ${cleanRun} | 
Expected: false")
+
+                // Check with an allowed/ignored arg
+                def allowedArgs = invocationArgs + acceptableArg
+                logger.info("Checking '${allowedArgs.join(" ")}' with 
acceptable args: ${acceptableArgs}")
+                commandLine = 
parser.parse(ConfigEncryptionTool.getCliOptions(), allowedArgs as String[])
+                boolean allowedRun = 
tool.commandLineHasActionFlags(commandLine, acceptableArgs)
+                logger.info("Allowed run has action flags: ${allowedRun} | 
Expected: ${acceptableArgs.isEmpty().toString()}")
+
+                // Check with an unallowed arg
+                def unallowedArgs = invocationArgs + unacceptableArg
+                logger.info("Checking '${unallowedArgs.join(" ")}' with 
acceptable args: ${acceptableArgs}")
+                commandLine = 
parser.parse(ConfigEncryptionTool.getCliOptions(), unallowedArgs as String[])
+                boolean unallowedRun = 
tool.commandLineHasActionFlags(commandLine, acceptableArgs)
+                logger.info("Unallowed run has action flags: ${unallowedRun} | 
Expected: true")
+
+                // Assert
+                assert !cleanRun
+                assert allowedRun == acceptableArgs.isEmpty()
+                assert unallowedRun
+            }
+        }
+    }
+
+    @Test
+    void testShouldReturnCurrentHashParams() {
+        // Arrange
+
+        // Params from secure_hash.key
+        int N = 2**4
+        int r = 8
+        int p = 1
+        String base64Salt = "A" * 22
+
+        String expectedJsonParams = new JsonBuilder([N: N, r: r, p: p, salt: 
base64Salt]).toString()
+        logger.info("Expected JSON params: ${expectedJsonParams}")
+
+        // Set up assertions for after System.exit()
+        exit.expectSystemExitWithStatus(0)
+
+        // Initial set up
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        setFilePermissions(tmpDir, [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])
+
+        // Copy the hashed credentials file
+        String secureHashedPasswordPath = isUnlimitedStrengthCryptoAvailable() 
? "src/test/resources/secure_hash.key" :
+                "src/test/resources/secure_hash_128.key"
+        File originalSecureHashedPasswordFile = new 
File(secureHashedPasswordPath)
+        File secureHashedFile = new File("target/tmp/tmp_secure_hash.key")
+        secureHashedFile.delete()
+        Files.copy(originalSecureHashedPasswordFile.toPath(), 
secureHashedFile.toPath())
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            void checkAssertion() {
+                // If JSON ordering changes, may need to capture and build 
JSON object from this text
+                assert systemOutRule.getLog().contains(expectedJsonParams)
+
+                // Clean up
+                tmpDir.deleteOnExit()
+                secureHashedFile.deleteOnExit()
+            }
+        })
+
+        // Override the "final" secure hash file path
+        ConfigEncryptionTool.secureHashPath = secureHashedFile.path
+
+        // Act
+        ConfigEncryptionTool.main(["--currentHashParams"] as String[])
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testShouldReturnDefaultHashParamsIfNonePresent() {
+        // Arrange
+
+        // Default params
+        int N = ConfigEncryptionTool.SCRYPT_N
+        int r = ConfigEncryptionTool.SCRYPT_R
+        int p = ConfigEncryptionTool.SCRYPT_P
+
+        String expectedJsonParams = new JsonBuilder([N: N, r: r, p: p, salt: 
"<some 22 char B64 str>"]).toString()
+        logger.info("Expected JSON params: ${expectedJsonParams}")
+
+        // Set up assertions for after System.exit()
+        exit.expectSystemExitWithStatus(0)
+
+        // Initial set up
+        File tmpDir = new File("target/tmp/")
+        tmpDir.mkdirs()
+        setFilePermissions(tmpDir, [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])
+
+        // Ensure the file is not present
+        File secureHashedFile = new File("target/tmp/tmp_secure_hash.key")
+        secureHashedFile.delete()
+
+        exit.checkAssertionAfterwards(new Assertion() {
+            void checkAssertion() {
+                // If JSON ordering changes, may need to capture and build 
JSON object from this text
+                List<String> returnedJSONParams = 
systemOutRule.getLog().readLines()
+                logger.returned("Returned JSON params: 
${returnedJSONParams.join("\n")}")
+
+                JsonSlurper slurper = new JsonSlurper()
+                def expectedJson = slurper.parseText(expectedJsonParams)
+                def returnedJson = 
slurper.parseText(returnedJSONParams.first())
+                assert returnedJson.N == expectedJson.N
+                assert returnedJson.r == expectedJson.r
+                assert returnedJson.p == expectedJson.p
+                assert returnedJson.salt =~ /[\w\/]{22}/
+
+                // Clean up
+                tmpDir.deleteOnExit()
+            }
+        })
+
+        // Override the "final" secure hash file path
+        ConfigEncryptionTool.secureHashPath = secureHashedFile.path
+
+        // Act
+        ConfigEncryptionTool.main(["--currentHashParams"] as String[])
+
+        // Assert
+
+        // Assertions defined above
+    }
+
+    @Test
+    void testShouldFailOnCurrentHashParamsIfOtherFlagsPresent() {
+        // Arrange
+        ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+        def validOpts = [
+                "",
+                "-v",
+                "--verbose"
+        ]
+
+        def invalidOpts = [
+                "--migrate",
+                "-f flow.xml.gz",
+                "-n nifi.properties",
+                "-o output"
+        ]
+
+        // Act
+        validOpts.each { String valid ->
+            def args = (valid + " --currentHashParams").split(" ")
+            logger.info("Testing with ${args}")
+            tool.parse(args as String[])
+        }
+
+        invalidOpts.each { String invalid ->
+            def args = (invalid + " --currentHashParams").split(" ")
+            logger.info("Testing with ${args}")
+            def msg = shouldFail(CommandLineParseException) {
+                tool.parse(args as String[])
+            }
+
+            // Assert
+            assert msg == "When '--currentHashParams' is specified, only 
'-h'/'--help' and '-v'/'--verbose' are allowed"
+            assert systemOutRule.getLog().contains("usage: 
org.apache.nifi.properties.ConfigEncryptionTool [")
+        }
+    }
+
+// TODO: Test with 128/256-bit available
 }
 
 class TestAppender extends AppenderSkeleton {

http://git-wip-us.apache.org/repos/asf/nifi/blob/6d06defa/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/scrypt.py
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/scrypt.py 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/scrypt.py
new file mode 100644
index 0000000..97ba86f
--- /dev/null
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/scrypt.py
@@ -0,0 +1,18 @@
+#!/bin/env python
+
+import base64
+from passlib.hash import scrypt
+
+
+def secure_hash(password, base64_encoded_salt):
+    hash = scrypt.using(salt=base64.b64decode(base64_encoded_salt), rounds=4, 
block_size=8, parallelism=1).hash(password)
+    return hash
+
+
+passwords=["password", "thisIsABadPassword", 
"bWZerzZo6fw9ZrDz*YfM6CVj2Ktx(YJd"]
+salts=["AAAAAAAAAAAAAAAAAAAAAA==", "ABCDEFGHIJKLMNOPQRSTUV==", 
"eO+UUcKYL2gnpD51QCc+gnywQ7Eg9tZeLMlf0XXr2zc="]
+
+for pw in passwords:
+    for s in salts:
+        print('Hashed "{}" with salt "{}": \t{}'.format(pw, s, secure_hash(pw, 
s)))
+

http://git-wip-us.apache.org/repos/asf/nifi/blob/6d06defa/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash.key
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash.key 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash.key
new file mode 100644
index 0000000..ef7097e
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash.key
@@ -0,0 +1,2 @@
+secureHashKey=$s0$40801$AAAAAAAAAAAAAAAAAAAAAA$pJOGA9sPL+pRzynnwt6G2FfVTyLQdbKSbk6W8IKId8E
+secureHashPassword=$s0$40801$AAAAAAAAAAAAAAAAAAAAAA$gLSh7ChbHdOIMvZ74XGjV6qF65d9qvQ8n75FeGnM8YM
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/6d06defa/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash_128.key
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash_128.key
 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash_128.key
new file mode 100644
index 0000000..58aa040
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/resources/secure_hash_128.key
@@ -0,0 +1,2 @@
+secureHashKey=
+secureHashPassword=
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/6d06defa/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java
 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java
index 6ff733c..234faf0 100644
--- 
a/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java
+++ 
b/nifi-toolkit/nifi-toolkit-tls/src/main/java/org/apache/nifi/toolkit/tls/commandLine/ExitCode.java
@@ -69,5 +69,10 @@ public enum ExitCode {
     /**
      * Unable to read nifi.properties
      */
-    ERROR_READING_NIFI_PROPERTIES
+    ERROR_READING_NIFI_PROPERTIES,
+
+    /**
+     * Unable to read existing configuration value or file
+     */
+    ERROR_READING_CONFIG
 }

Reply via email to