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 }