[
https://issues.apache.org/jira/browse/NIFI-1831?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15432838#comment-15432838
]
ASF GitHub Bot commented on NIFI-1831:
--------------------------------------
Github user bbende commented on a diff in the pull request:
https://github.com/apache/nifi/pull/834#discussion_r75871505
--- Diff:
nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
---
@@ -0,0 +1,1225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.properties
+
+import ch.qos.logback.classic.spi.LoggingEvent
+import ch.qos.logback.core.AppenderBase
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
+import org.apache.nifi.util.NiFiProperties
+import org.apache.nifi.util.console.TextDevice
+import org.apache.nifi.util.console.TextDevices
+import org.bouncycastle.crypto.generators.SCrypt
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.contrib.java.lang.system.Assertion
+import org.junit.contrib.java.lang.system.ExpectedSystemExit
+import org.junit.contrib.java.lang.system.SystemOutRule
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.nio.file.Files
+import java.nio.file.attribute.PosixFilePermission
+import java.security.KeyException
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class ConfigEncryptionToolTest extends GroovyTestCase {
+ private static final Logger logger =
LoggerFactory.getLogger(ConfigEncryptionToolTest.class)
+
+ @Rule
+ public final ExpectedSystemExit exit = ExpectedSystemExit.none()
+
+ @Rule
+ public final SystemOutRule systemOutRule = new
SystemOutRule().enableLog()
+
+ private static final String KEY_HEX_128 =
"0123456789ABCDEFFEDCBA9876543210"
+ private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+ public static final String KEY_HEX =
isUnlimitedStrengthCryptoAvailable() ? KEY_HEX_256 : KEY_HEX_128
+ private static final String PASSWORD = "thisIsABadPassword"
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider())
+
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join("
")}")
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ TestAppender.reset()
+ }
+
+ private static boolean isUnlimitedStrengthCryptoAvailable() {
+ Cipher.getMaxAllowedKeyLength("AES") > 128
+ }
+
+ @Test
+ void testShouldPrintHelpMessage() {
+ // Arrange
+ def flags = ["-h", "--help"]
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ flags.each { String arg ->
+ def msg = shouldFail(CommandLineParseException) {
+ tool.parse([arg] as String[])
+ }
+
+ // Assert
+ assert msg == null
+ assert systemOutRule.getLog().contains("usage:
org.apache.nifi.properties.ConfigEncryptionTool [")
+ }
+ }
+
+ @Test
+ void testShouldParseBootstrapConfArgument() {
+ // Arrange
+ def flags = ["-b", "--bootstrapConf"]
+ String bootstrapPath = "src/test/resources/bootstrap.conf"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ flags.each { String arg ->
+ tool.parse([arg, bootstrapPath] as String[])
+ logger.info("Parsed bootstrap.conf location:
${tool.bootstrapConfPath}")
+
+ // Assert
+ assert tool.bootstrapConfPath == bootstrapPath
+ }
+ }
+
+ @Test
+ void testParseShouldPopulateDefaultBootstrapConfArgument() {
+ // Arrange
+ String bootstrapPath = "conf/bootstrap.conf"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ tool.parse([] as String[])
+ logger.info("Parsed bootstrap.conf location:
${tool.bootstrapConfPath}")
+
+ // Assert
+ assert new File(tool.bootstrapConfPath).getPath() == new
File(bootstrapPath).getPath()
+ }
+
+ @Test
+ void testShouldParseNiFiPropertiesArgument() {
+ // Arrange
+ def flags = ["-n", "--niFiProperties"]
+ String niFiPropertiesPath = "src/test/resources/nifi.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ flags.each { String arg ->
+ tool.parse([arg, niFiPropertiesPath] as String[])
+ logger.info("Parsed nifi.properties location:
${tool.niFiPropertiesPath}")
+
+ // Assert
+ assert tool.niFiPropertiesPath == niFiPropertiesPath
+ }
+ }
+
+ @Test
+ void testParseShouldPopulateDefaultNiFiPropertiesArgument() {
+ // Arrange
+ String niFiPropertiesPath = "conf/nifi.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ tool.parse([] as String[])
+ logger.info("Parsed nifi.properties location:
${tool.niFiPropertiesPath}")
+
+ // Assert
+ assert new File(tool.niFiPropertiesPath).getPath() == new
File(niFiPropertiesPath).getPath()
+ }
+
+ @Test
+ void testShouldParseOutputNiFiPropertiesArgument() {
+ // Arrange
+ def flags = ["-o", "--outputNiFiProperties"]
+ String niFiPropertiesPath = "src/test/resources/nifi.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ flags.each { String arg ->
+ tool.parse([arg, niFiPropertiesPath] as String[])
+ logger.info("Parsed output nifi.properties location:
${tool.outputNiFiPropertiesPath}")
+
+ // Assert
+ assert tool.outputNiFiPropertiesPath == niFiPropertiesPath
+ }
+ }
+
+ @Test
+ void testParseShouldPopulateDefaultOutputNiFiPropertiesArgument() {
+ // Arrange
+ String niFiPropertiesPath = "conf/nifi.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ tool.parse([] as String[])
+ logger.info("Parsed output nifi.properties location:
${tool.outputNiFiPropertiesPath}")
+
+ // Assert
+ assert new File(tool.outputNiFiPropertiesPath).getPath() == new
File(niFiPropertiesPath).getPath()
+ }
+
+ @Test
+ void testParseShouldWarnIfNiFiPropertiesWillBeOverwritten() {
+ // Arrange
+ String niFiPropertiesPath = "conf/nifi.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ tool.parse("-n ${niFiPropertiesPath} -o
${niFiPropertiesPath}".split(" ") as String[])
+ logger.info("Parsed nifi.properties location:
${tool.niFiPropertiesPath}")
+ logger.info("Parsed output nifi.properties location:
${tool.outputNiFiPropertiesPath}")
+
+ // Assert
+ assert !TestAppender.events.isEmpty()
+ assert TestAppender.events.first().message =~ "The source
nifi.properties and destination nifi.properties are identical \\[.*\\] so the
original will be overwritten"
+ }
+
+ @Test
+ void testShouldParseKeyArgument() {
+ // Arrange
+ def flags = ["-k", "--key"]
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ flags.each { String arg ->
+ tool.parse([arg, KEY_HEX] as String[])
+ logger.info("Parsed key: ${tool.keyHex}")
+
+ // Assert
+ assert tool.keyHex == KEY_HEX
+ }
+ }
+
+ @Test
+ void testShouldLoadNiFiProperties() {
+ // Arrange
+ String niFiPropertiesPath =
"src/test/resources/nifi_with_sensitive_properties_unprotected.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-n", niFiPropertiesPath] as String[]
+
+ String oldFilePath =
System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+
+ tool.parse(args)
+ logger.info("Parsed nifi.properties location:
${tool.niFiPropertiesPath}")
+
+ // Act
+ NiFiProperties properties = tool.loadNiFiProperties()
+ logger.info("Loaded NiFiProperties from
${tool.niFiPropertiesPath}")
+
+ // Assert
+ assert properties
+ assert properties.size() > 0
+
+ // The system variable was reset to the original value
+ assert System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) ==
oldFilePath
+ }
+
+ @Test
+ void testShouldReadKeyFromConsole() {
+ // Arrange
+ List<String> keyValues = [
+ "0123 4567",
+ KEY_HEX,
+ " ${KEY_HEX} ",
+ "non-hex-chars",
+ ]
+
+ // Act
+ keyValues.each { String key ->
+ TextDevice mockConsoleDevice = TextDevices.streamDevice(new
ByteArrayInputStream(key.bytes), new ByteArrayOutputStream())
+ String readKey =
ConfigEncryptionTool.readKeyFromConsole(mockConsoleDevice)
+ logger.info("Read key: [${readKey}]")
+
+ // Assert
+ assert readKey == key
+ }
+ }
+
+ @Test
+ void testShouldReadPasswordFromConsole() {
+ // Arrange
+ List<String> passwords = [
+ "0123 4567",
+ PASSWORD,
+ " ${PASSWORD} ",
+ "non-hex-chars",
+ ]
+
+ // Act
+ passwords.each { String pw ->
+ logger.info("Using password: [${PASSWORD}]")
+ TextDevice mockConsoleDevice = TextDevices.streamDevice(new
ByteArrayInputStream(pw.bytes), new ByteArrayOutputStream())
+ String readPassword =
ConfigEncryptionTool.readPasswordFromConsole(mockConsoleDevice)
+ logger.info("Read password: [${readPassword}]")
+
+ // Assert
+ assert readPassword == pw
+ }
+ }
+
+ @Test
+ void testShouldReadPasswordFromConsoleIfNoKeyPresent() {
+ // Arrange
+ def args = [] as String[]
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ tool.parse(args)
+ logger.info("Using password flag: ${tool.usingPassword}")
+ logger.info("Password: ${tool.password}")
+ logger.info("Key hex: ${tool.keyHex}")
+
+ assert tool.usingPassword
+ assert !tool.password
+ assert !tool.keyHex
+
+ TextDevice mockConsoleDevice = TextDevices.streamDevice(new
ByteArrayInputStream(PASSWORD.bytes), new ByteArrayOutputStream())
+
+ // Mocked for deterministic output and performance in test --
SCrypt is not under test here
+ SCrypt.metaClass.'static'.generate = { byte[] pw, byte[] s, int N,
int r, int p, int dkLen ->
+ logger.mock("Mock SCrypt.generate(${Hex.encodeHexString(pw)},
${Hex.encodeHexString(s)}, ${N}, ${r}, ${p}, ${dkLen})")
+ logger.mock("Returning ${KEY_HEX[0..<dkLen * 2]}")
+ Hex.decodeHex(KEY_HEX[0..<dkLen * 2] as char[])
+ }
+
+ // Act
+ String readKey = tool.getKey(mockConsoleDevice)
+ logger.info("Read key: [${readKey}]")
+
+ // Assert
+ assert readKey == KEY_HEX
+ assert tool.keyHex == readKey
+ assert !tool.password
+ assert !tool.usingPassword
+
+ SCrypt.metaClass.'static' = null
+ }
+
+ @Test
+ void testShouldReadKeyFromConsoleIfFlagProvided() {
+ // Arrange
+ def args = ["-r"] as String[]
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ tool.parse(args)
+ logger.info("Using password flag: ${tool.usingPassword}")
+ logger.info("Password: ${tool.password}")
+ logger.info("Key hex: ${tool.keyHex}")
+
+ assert !tool.usingPassword
+ assert !tool.password
+ assert !tool.keyHex
+
+ TextDevice mockConsoleDevice = TextDevices.streamDevice(new
ByteArrayInputStream(KEY_HEX.bytes), new ByteArrayOutputStream())
+
+ // Act
+ String readKey = tool.getKey(mockConsoleDevice)
+ logger.info("Read key: [${readKey}]")
+
+ // Assert
+ assert readKey == KEY_HEX
+ assert tool.keyHex == readKey
+ assert !tool.password
+ assert !tool.usingPassword
+ }
+
+ @Test
+ void testShouldIgnoreRawKeyFlagIfKeyProvided() {
+ // Arrange
+ def args = ["-r", "-k", KEY_HEX] as String[]
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ tool.parse(args)
+ logger.info("Using password flag: ${tool.usingPassword}")
+ logger.info("Password: ${tool.password}")
+ logger.info("Key hex: ${tool.keyHex}")
+
+ // Assert
+ assert !tool.usingPassword
+ assert !tool.password
+ assert tool.keyHex == KEY_HEX
+
+ assert !TestAppender.events.isEmpty()
+ assert TestAppender.events.collect { it.message }.contains("If the
key or password is provided in the arguments, '-r'/'--useRawKey' is ignored")
+ }
+
+ @Test
+ void testShouldIgnoreRawKeyFlagIfPasswordProvided() {
+ // Arrange
+ def args = ["-r", "-p", PASSWORD] as String[]
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+
+ // Act
+ tool.parse(args)
+ logger.info("Using password flag: ${tool.usingPassword}")
+ logger.info("Password: ${tool.password}")
+ logger.info("Key hex: ${tool.keyHex}")
+
+ // Assert
+ assert tool.usingPassword
+ assert tool.password == PASSWORD
+ assert !tool.keyHex
+
+ assert !TestAppender.events.isEmpty()
+ assert TestAppender.events.collect { it.message }.contains("If the
key or password is provided in the arguments, '-r'/'--useRawKey' is ignored")
+ }
+
+ @Test
+ void testShouldParseKey() {
+ // Arrange
+ Map<String, String> keyValues = [
+ (KEY_HEX) : KEY_HEX,
+ " ${KEY_HEX} " : KEY_HEX,
+ "xxx${KEY_HEX}zzz" : KEY_HEX,
+ ((["0123", "4567"] * 4).join("-")): "01234567" * 4,
+ ((["89ab", "cdef"] * 4).join(" ")): "89ABCDEF" * 4,
+ (KEY_HEX.toLowerCase()) : KEY_HEX,
+ (KEY_HEX[0..<32]) : KEY_HEX[0..<32],
+ ]
+
+ if (isUnlimitedStrengthCryptoAvailable()) {
+ keyValues.put(KEY_HEX[0..<48], KEY_HEX[0..<48])
+ }
+
+ // Act
+ keyValues.each { String key, final String EXPECTED_KEY ->
+ logger.info("Reading key: [${key}]")
+ String parsedKey = ConfigEncryptionTool.parseKey(key)
+ logger.info("Parsed key: [${parsedKey}]")
+
+ // Assert
+ assert parsedKey == EXPECTED_KEY
+ }
+ }
+
+ @Test
+ void testParseKeyShouldThrowExceptionForInvalidKeys() {
+ // Arrange
+ List<String> keyValues = [
+ "0123 4567",
+ "non-hex-chars",
+ KEY_HEX[0..<-1],
+ "&ITD SF^FI&&%SDIF"
+ ]
+
+ def validKeyLengths = ConfigEncryptionTool.getValidKeyLengths()
+ def bitLengths = validKeyLengths.collect { it / 4 }
+ String secondHalf = /\[${validKeyLengths.join(", ")}\] bits / +
+ /\(\[${bitLengths.join(", ")}\]/ + / hex
characters\)/.toString()
+
+ // Act
+ keyValues.each { String key ->
+ logger.info("Reading key: [${key}]")
+ def msg = shouldFail(KeyException) {
+ String parsedKey = ConfigEncryptionTool.parseKey(key)
+ logger.info("Parsed key: [${parsedKey}]")
+ }
+ logger.expected(msg)
+ int trimmedKeySize = key.replaceAll("[^0-9a-fA-F]", "").size()
+
+ // Assert
+ assert msg =~ "The key \\(${trimmedKeySize} hex chars\\) must
be of length ${secondHalf}"
+ }
+ }
+
+ @Test
+ void testShouldDeriveKeyFromPassword() {
+ // Arrange
+
+ // Mocked for deterministic output and performance in test --
SCrypt is not under test here
+ SCrypt.metaClass.'static'.generate = { byte[] pw, byte[] s, int N,
int r, int p, int dkLen ->
+ logger.mock("Mock SCrypt.generate(${Hex.encodeHexString(pw)},
${Hex.encodeHexString(s)}, ${N}, ${r}, ${p}, ${dkLen})")
+ logger.mock("Returning ${KEY_HEX[0..<dkLen * 2]}")
+ Hex.decodeHex(KEY_HEX[0..<dkLen * 2] as char[])
+ }
+
+ logger.info("Using password: [${PASSWORD}]")
+
+ // Act
+ String derivedKey =
ConfigEncryptionTool.deriveKeyFromPassword(PASSWORD)
+ logger.info("Derived key: [${derivedKey}]")
+
+ // Assert
+ assert derivedKey == KEY_HEX
+
+ SCrypt.metaClass.'static' = null
+ }
+
+ @Test
+ void testShouldActuallyDeriveKeyFromPassword() {
+ // Arrange
+ logger.info("Using password: [${PASSWORD}]")
+
+ // Act
+ String derivedKey =
ConfigEncryptionTool.deriveKeyFromPassword(PASSWORD)
+ logger.info("Derived key: [${derivedKey}]")
+
+ // Assert
+ assert derivedKey.length() ==
(Cipher.getMaxAllowedKeyLength("AES") > 128 ? 64 : 32)
+ }
+
+ @Test
+ void testDeriveKeyFromPasswordShouldTrimPassword() {
+ // Arrange
+ final String PASSWORD_SPACES = " ${PASSWORD} "
+
+ def attemptedPasswords = []
+
+ // Mocked for deterministic output and performance in test --
SCrypt is not under test here
+ SCrypt.metaClass.'static'.generate = { byte[] pw, byte[] s, int N,
int r, int p, int dkLen ->
+ logger.mock("Mock SCrypt.generate(${Hex.encodeHexString(pw)},
${Hex.encodeHexString(s)}, ${N}, ${r}, ${p}, ${dkLen})")
+ attemptedPasswords << new String(pw)
+ logger.mock("Returning ${KEY_HEX[0..<dkLen * 2]}")
+ Hex.decodeHex(KEY_HEX[0..<dkLen * 2] as char[])
+ }
+
+ // Act
+ [PASSWORD, PASSWORD_SPACES].each { String password ->
+ logger.info("Using password: [${password}]")
+ String derivedKey =
ConfigEncryptionTool.deriveKeyFromPassword(password)
+ logger.info("Derived key: [${derivedKey}]")
+ }
+
+ // Assert
+ assert attemptedPasswords.size() == 2
+ assert attemptedPasswords.every { it == PASSWORD }
+
+ SCrypt.metaClass.'static' = null
+ }
+
+ @Test
+ void
testDeriveKeyFromPasswordShouldThrowExceptionForInvalidPasswords() {
+ // Arrange
+ List<String> passwords = [
+ (null),
+ "",
+ " ",
+ "shortpass",
+ "shortwith "
+ ]
+
+ // Act
+ passwords.each { String password ->
+ logger.info("Reading password: [${password}]")
+ def msg = shouldFail(KeyException) {
+ String derivedKey =
ConfigEncryptionTool.deriveKeyFromPassword(password)
+ logger.info("Derived key: [${derivedKey}]")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg == "Cannot derive key from empty/short password --
password must be at least 12 characters"
+ }
+ }
+
+ @Test
+ void testShouldHandleKeyAndPasswordFlag() {
+ // Arrange
+ def args = ["-k", KEY_HEX, "-p", PASSWORD]
+ logger.info("Using args: ${args}")
+
+ // Act
+ def msg = shouldFail(CommandLineParseException) {
+ new ConfigEncryptionTool().parse(args as String[])
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg == "Only one of password and key can be used"
+ }
+
+ @Test
+ void testShouldNotLoadMissingNiFiProperties() {
+ // Arrange
+ String niFiPropertiesPath =
"src/test/resources/non_existent_nifi.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-n", niFiPropertiesPath] as String[]
+
+ String oldFilePath =
System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+
+ tool.parse(args)
+ logger.info("Parsed nifi.properties location:
${tool.niFiPropertiesPath}")
+
+ // Act
+ def msg = shouldFail(CommandLineParseException) {
+ NiFiProperties properties = tool.loadNiFiProperties()
+ logger.info("Loaded NiFiProperties from
${tool.niFiPropertiesPath}")
+ }
+
+ // Assert
+ assert msg == "Cannot load NiFiProperties from
[${niFiPropertiesPath}]".toString()
+
+ // The system variable was reset to the original value
+ assert System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH) ==
oldFilePath
+ }
+
+ @Test
+ void testLoadNiFiPropertiesShouldHandleReadFailure() {
+ // Arrange
+ File inputPropertiesFile = new
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+ File workingFile = new File("tmp_nifi.properties")
+ workingFile.delete()
+
+ Files.copy(inputPropertiesFile.toPath(), workingFile.toPath())
+ // Empty set of permissions
+ Files.setPosixFilePermissions(workingFile.toPath(), [] as Set)
+ logger.info("Set POSIX permissions to
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-n", workingFile.path, "-k", KEY_HEX]
+ tool.parse(args)
+
+ // Act
+ def msg = shouldFail(IOException) {
+ tool.loadNiFiProperties()
+ logger.info("Read nifi.properties")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg == "Cannot load NiFiProperties from
[${workingFile.path}]".toString()
+
+ workingFile.deleteOnExit()
+ }
+
+ @Test
+ void testShouldEncryptSensitiveProperties() {
+ // Arrange
+ String niFiPropertiesPath =
"src/test/resources/nifi_with_sensitive_properties_unprotected.properties"
+ String newPropertiesPath =
"src/test/resources/tmp_encrypted_nifi.properties"
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-n", niFiPropertiesPath, "-o",
newPropertiesPath] as String[]
+
+ tool.parse(args)
+ logger.info("Parsed nifi.properties location:
${tool.niFiPropertiesPath}")
+
+ tool.keyHex = KEY_HEX
+
+ NiFiProperties plainNiFiProperties = tool.loadNiFiProperties()
+ ProtectedNiFiProperties protectedWrapper = new
ProtectedNiFiProperties(plainNiFiProperties)
+ assert !protectedWrapper.hasProtectedKeys()
+
+ // Act
+ NiFiProperties encryptedProperties =
tool.encryptSensitiveProperties(plainNiFiProperties)
+ logger.info("Encrypted sensitive properties")
+
+ // Assert
+ ProtectedNiFiProperties protectedWrapperAroundEncrypted = new
ProtectedNiFiProperties(encryptedProperties)
+ assert protectedWrapperAroundEncrypted.hasProtectedKeys()
+
+ // Ensure that all non-empty sensitive properties are marked as
protected
+ final Set<String> EXPECTED_PROTECTED_KEYS =
protectedWrapperAroundEncrypted
+ .getSensitivePropertyKeys().findAll { String k ->
+ plainNiFiProperties.getProperty(k)
+ } as Set<String>
+ assert
protectedWrapperAroundEncrypted.getProtectedPropertyKeys().keySet() ==
EXPECTED_PROTECTED_KEYS
+ }
+
+ @Test
+ void testShouldUpdateBootstrapContentsWithKey() {
+ // Arrange
+ final String EXPECTED_KEY_LINE =
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ tool.keyHex = KEY_HEX
+
+ List<String> originalLines = [
+ ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT,
+ "${ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX}="
+ ]
+
+ // Act
+ List<String> updatedLines =
tool.updateBootstrapContentsWithKey(originalLines)
+ logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+ // Assert
+ assert updatedLines.size() == originalLines.size()
+ assert updatedLines.first() == originalLines.first()
+ assert updatedLines.last() == EXPECTED_KEY_LINE
+ }
+
+ @Test
+ void testUpdateBootstrapContentsWithKeyShouldOverwriteExistingKey() {
+ // Arrange
+ final String EXPECTED_KEY_LINE =
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ tool.keyHex = KEY_HEX
+
+ List<String> originalLines = [
+ ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT,
+ "${ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX}=badKey"
+ ]
+
+ // Act
+ List<String> updatedLines =
tool.updateBootstrapContentsWithKey(originalLines)
+ logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+ // Assert
+ assert updatedLines.size() == originalLines.size()
+ assert updatedLines.first() == originalLines.first()
+ assert updatedLines.last() == EXPECTED_KEY_LINE
+ }
+
+ @Test
+ void testShouldUpdateBootstrapContentsWithKeyAndComment() {
+ // Arrange
+ final String EXPECTED_KEY_LINE =
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ tool.keyHex = KEY_HEX
+
+ List<String> originalLines = [
+ "${ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX}="
+ ]
+
+ // Act
+ List<String> updatedLines =
tool.updateBootstrapContentsWithKey(originalLines.clone() as List<String>)
+ logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+ // Assert
+ assert updatedLines.size() == originalLines.size() + 1
+ assert updatedLines.first() ==
ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT
+ assert updatedLines.last() == EXPECTED_KEY_LINE
+ }
+
+ @Test
+ void testUpdateBootstrapContentsWithKeyShouldAddLines() {
+ // Arrange
+ final String EXPECTED_KEY_LINE =
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ tool.keyHex = KEY_HEX
+
+ List<String> originalLines = []
+
+ // Act
+ List<String> updatedLines =
tool.updateBootstrapContentsWithKey(originalLines.clone() as List<String>)
+ logger.info("Updated bootstrap.conf lines: ${updatedLines}")
+
+ // Assert
+ assert updatedLines.size() == originalLines.size() + 3
+ assert updatedLines.first() == "\n"
+ assert updatedLines[1] ==
ConfigEncryptionTool.BOOTSTRAP_KEY_COMMENT
+ assert updatedLines.last() == EXPECTED_KEY_LINE
+ }
+
+ @Test
+ void testShouldWriteKeyToBootstrapConf() {
+ // Arrange
+ File emptyKeyFile = new
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+ File workingFile = new File("tmp_bootstrap.conf")
+ workingFile.delete()
+
+ Files.copy(emptyKeyFile.toPath(), workingFile.toPath())
+ final List<String> originalLines = workingFile.readLines()
+ String originalKeyLine = originalLines.find {
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) }
+ logger.info("Original key line from bootstrap.conf:
${originalKeyLine}")
+
+ final String EXPECTED_KEY_LINE =
ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX + KEY_HEX
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-b", workingFile.path, "-k", KEY_HEX]
+ tool.parse(args)
+
+ // Act
+ tool.writeKeyToBootstrapConf()
+ logger.info("Updated bootstrap.conf")
+
+ // Assert
+ final List<String> updatedLines = workingFile.readLines()
+ String updatedLine = updatedLines.find {
it.startsWith(ConfigEncryptionTool.BOOTSTRAP_KEY_PREFIX) }
+ logger.info("Updated key line: ${updatedLine}")
+
+ assert updatedLine == EXPECTED_KEY_LINE
+ assert originalLines.size() == updatedLines.size()
+
+ workingFile.deleteOnExit()
+ }
+
+ @Test
+ void testWriteKeyToBootstrapConfShouldHandleReadFailure() {
+ // Arrange
+ File emptyKeyFile = new
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+ File workingFile = new File("tmp_bootstrap.conf")
+ workingFile.delete()
+
+ Files.copy(emptyKeyFile.toPath(), workingFile.toPath())
+ // Empty set of permissions
+ Files.setPosixFilePermissions(workingFile.toPath(), [] as Set)
+ logger.info("Set POSIX permissions to
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-b", workingFile.path, "-k", KEY_HEX]
+ tool.parse(args)
+
+ // Act
+ def msg = shouldFail(IOException) {
+ tool.writeKeyToBootstrapConf()
+ logger.info("Updated bootstrap.conf")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg == "The bootstrap.conf file at tmp_bootstrap.conf must
exist and be readable and writable by the user running this tool"
+
+ workingFile.deleteOnExit()
+ }
+
+ @Test
+ void testWriteKeyToBootstrapConfShouldHandleWriteFailure() {
+ // Arrange
+ File emptyKeyFile = new
File("src/test/resources/bootstrap_with_empty_master_key.conf")
+ File workingFile = new File("tmp_bootstrap.conf")
+ workingFile.delete()
+
+ Files.copy(emptyKeyFile.toPath(), workingFile.toPath())
+ // Read-only set of permissions
+ Files.setPosixFilePermissions(workingFile.toPath(),
[PosixFilePermission.OWNER_READ, PosixFilePermission.GROUP_READ,
PosixFilePermission.OTHERS_READ] as Set)
+ logger.info("Set POSIX permissions to
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-b", workingFile.path, "-k", KEY_HEX]
+ tool.parse(args)
+
+ // Act
+ def msg = shouldFail(IOException) {
+ tool.writeKeyToBootstrapConf()
+ logger.info("Updated bootstrap.conf")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg == "The bootstrap.conf file at tmp_bootstrap.conf must
exist and be readable and writable by the user running this tool"
+
+ workingFile.deleteOnExit()
+ }
+
+ @Test
+ void testShouldSerializeNiFiProperties() {
+ // Arrange
+ Properties rawProperties = [key: "value", key2: "value2"] as
Properties
+ NiFiProperties properties = new
StandardNiFiProperties(rawProperties)
+ logger.info("Loaded ${properties.size()} properties")
+
+ // Act
+ List<String> lines =
ConfigEncryptionTool.serializeNiFiProperties(properties)
+ logger.info("Serialized NiFiProperties to ${lines.size()} lines")
+ logger.info("\n" + lines.join("\n"))
+
+ // Assert
+
+ // The serialization could have occurred > 1 second ago, causing a
rolling date/time mismatch, so use regex
+ // Format -- #Fri Aug 19 16:51:16 PDT 2016
+ String datePattern = /#\w{3} \w{3} \d{2} \d{2}:\d{2}:\d{2} \w{3}
\d{4}/
+
+ // One extra line for the date
+ assert lines.size() == properties.size() + 1
+ assert lines.first() =~ datePattern
+
+ rawProperties.keySet().every { String key ->
+ assert
lines.contains("${key}=${properties.getProperty(key)}".toString())
+ }
+ }
+
+ @Test
+ void testShouldSerializeNiFiPropertiesAndPreserveFormat() {
+ // Arrange
+ String originalNiFiPropertiesPath =
"src/test/resources/nifi_with_few_sensitive_properties_unprotected.properties"
+
+ File originalFile = new File(originalNiFiPropertiesPath)
+ List<String> originalLines = originalFile.readLines()
+ logger.info("Read ${originalLines.size()} lines from
${originalNiFiPropertiesPath}")
+ logger.info("\n" + originalLines[0..3].join("\n") + "...")
+
+ NiFiProperties plainProperties =
NiFiPropertiesLoader.withKey(KEY_HEX).load(originalNiFiPropertiesPath)
+ logger.info("Loaded NiFiProperties from
${originalNiFiPropertiesPath}")
+
+ ProtectedNiFiProperties protectedWrapper = new
ProtectedNiFiProperties(plainProperties)
+ logger.info("Loaded ${plainProperties.size()} properties")
+ logger.info("There are
${protectedWrapper.getSensitivePropertyKeys().size()} sensitive properties")
+
+ protectedWrapper.addSensitivePropertyProvider(new
AESSensitivePropertyProvider(KEY_HEX))
+ NiFiProperties protectedProperties =
protectedWrapper.protectPlainProperties()
+ int protectedPropertyCount =
ProtectedNiFiProperties.countProtectedProperties(protectedProperties)
+ logger.info("Counted ${protectedPropertyCount} protected keys")
+
+ // Act
+ List<String> lines =
ConfigEncryptionTool.serializeNiFiPropertiesAndPreserveFormat(protectedProperties,
originalFile)
+ logger.info("Serialized NiFiProperties to ${lines.size()} lines")
+ lines.eachWithIndex { String entry, int i ->
+ logger.debug("${(i + 1).toString().padLeft(3)}: ${entry}")
+ }
+
+ // Assert
+
+ // Added n new lines for the encrypted properties
+ assert lines.size() == originalLines.size() +
protectedPropertyCount
+
+ protectedProperties.getPropertyKeys().every { String key ->
+ assert
lines.contains("${key}=${protectedProperties.getProperty(key)}".toString())
+ }
+
+ logger.info("Updated nifi.properties:")
+ logger.info("\n" * 2 + lines.join("\n"))
+ }
+
+ @Test
+ void
testShouldSerializeNiFiPropertiesAndPreserveFormatWithNewPropertyAtEOF() {
+ // Arrange
+ String originalNiFiPropertiesPath =
"src/test/resources/nifi_with_few_sensitive_properties_unprotected.properties"
+
+ File originalFile = new File(originalNiFiPropertiesPath)
+ List<String> originalLines = originalFile.readLines()
+ logger.info("Read ${originalLines.size()} lines from
${originalNiFiPropertiesPath}")
+ logger.info("\n" + originalLines[0..3].join("\n") + "...")
+
+ NiFiProperties plainProperties =
NiFiPropertiesLoader.withKey(KEY_HEX).load(originalNiFiPropertiesPath)
+ logger.info("Loaded NiFiProperties from
${originalNiFiPropertiesPath}")
+
+ ProtectedNiFiProperties protectedWrapper = new
ProtectedNiFiProperties(plainProperties)
+ logger.info("Loaded ${plainProperties.size()} properties")
+ logger.info("There are
${protectedWrapper.getSensitivePropertyKeys().size()} sensitive properties")
+
+ // Set a value for the sensitive property which is the last line
in the file
+
+ // Groovy access to avoid duplicating entire object to add one
value
+ (plainProperties as
StandardNiFiProperties)[email protected](NiFiProperties.SECURITY_TRUSTSTORE_PASSWD,
"thisIsABadTruststorePassword")
+
+ protectedWrapper.addSensitivePropertyProvider(new
AESSensitivePropertyProvider(KEY_HEX))
+ NiFiProperties protectedProperties =
protectedWrapper.protectPlainProperties()
+ int protectedPropertyCount =
ProtectedNiFiProperties.countProtectedProperties(protectedProperties)
+ logger.info("Counted ${protectedPropertyCount} protected keys")
+
+ // Act
+ List<String> lines =
ConfigEncryptionTool.serializeNiFiPropertiesAndPreserveFormat(protectedProperties,
originalFile)
+ logger.info("Serialized NiFiProperties to ${lines.size()} lines")
+ lines.eachWithIndex { String entry, int i ->
+ logger.debug("${(i + 1).toString().padLeft(3)}: ${entry}")
+ }
+
+ // Assert
+
+ // Added n new lines for the encrypted properties
+ assert lines.size() == originalLines.size() +
protectedPropertyCount
+
+ protectedProperties.getPropertyKeys().every { String key ->
+ assert
lines.contains("${key}=${protectedProperties.getProperty(key)}".toString())
+ }
+
+ logger.info("Updated nifi.properties:")
+ logger.info("\n" * 2 + lines.join("\n"))
+ }
+
+ @Test
+ void testShouldWriteNiFiProperties() {
+ // Arrange
+ File inputPropertiesFile = new
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+ File workingFile = new File("tmp_nifi.properties")
+ workingFile.delete()
+
+ final List<String> originalLines = inputPropertiesFile.readLines()
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-n", inputPropertiesFile.path, "-o",
workingFile.path, "-k", KEY_HEX]
+ tool.parse(args)
+ NiFiProperties niFiProperties = tool.loadNiFiProperties()
+ tool.@niFiProperties = niFiProperties
+ logger.info("Loaded ${niFiProperties.size()} properties from
${inputPropertiesFile.path}")
+
+ // Act
+ tool.writeNiFiProperties()
+ logger.info("Wrote to ${workingFile.path}")
+
+ // Assert
+ final List<String> updatedLines = workingFile.readLines()
+ niFiProperties.getPropertyKeys().every { String key ->
+ assert
updatedLines.contains("${key}=${niFiProperties.getProperty(key)}".toString())
+ }
+
+ assert originalLines == updatedLines
+
+ logger.info("Updated nifi.properties:")
+ logger.info("\n" * 2 + updatedLines.join("\n"))
+
+ workingFile.deleteOnExit()
+ }
+
+ @Test
+ void testWriteNiFiPropertiesShouldHandleWriteFailureWhenFileExists() {
+ // Arrange
+ File inputPropertiesFile = new
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+ File workingFile = new File("tmp_nifi.properties")
+ workingFile.delete()
+
+ Files.copy(inputPropertiesFile.toPath(), workingFile.toPath())
+ // Read-only set of permissions
+ Files.setPosixFilePermissions(workingFile.toPath(),
[PosixFilePermission.OWNER_READ, PosixFilePermission.GROUP_READ,
PosixFilePermission.OTHERS_READ] as Set)
+ logger.info("Set POSIX permissions to
${Files.getPosixFilePermissions(workingFile.toPath())}")
+
+ ConfigEncryptionTool tool = new ConfigEncryptionTool()
+ String[] args = ["-n", inputPropertiesFile.path, "-o",
workingFile.path, "-k", KEY_HEX]
+ tool.parse(args)
+ NiFiProperties niFiProperties = tool.loadNiFiProperties()
+ tool.@niFiProperties = niFiProperties
+ logger.info("Loaded ${niFiProperties.size()} properties from
${inputPropertiesFile.path}")
+
+ // Act
+ def msg = shouldFail(IOException) {
+ tool.writeNiFiProperties()
+ logger.info("Wrote to ${workingFile.path}")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg == "The nifi.properties file at ${workingFile.path}
must be writable by the user running this tool".toString()
+
+ workingFile.deleteOnExit()
+ }
+
+ @Test
+ void
testWriteNiFiPropertiesShouldHandleWriteFailureWhenFileDoesNotExist() {
+ // Arrange
+ File inputPropertiesFile = new
File("src/test/resources/nifi_with_sensitive_properties_unprotected.properties")
+ File tmpDir = new File("tmp/")
--- End diff --
I think we should make all these references to "tmp/" be "target/tmp/",
right now it leaves a "tmp" directory at the root of
nifi-toolkit-encrypt-config after running a build
> Allow encrypted passwords in configuration files
> ------------------------------------------------
>
> Key: NIFI-1831
> URL: https://issues.apache.org/jira/browse/NIFI-1831
> Project: Apache NiFi
> Issue Type: New Feature
> Components: Configuration, Core Framework
> Affects Versions: 0.6.1
> Reporter: Andy LoPresto
> Assignee: Andy LoPresto
> Priority: Critical
> Labels: configuration, encryption, password, security
> Fix For: 1.0.0
>
> Original Estimate: 504h
> Remaining Estimate: 504h
>
> Storing passwords in plaintext in configuration files is not a security best
> practice. While file access can be restricted through OS permissions, these
> configuration files can be accidentally checked into source control, shared
> or deployed to multiple instances, etc.
> NiFi should allow a deployer to provide an encrypted password in the
> configuration file to minimize exposure of the passwords. On application
> start-up, NiFi should decrypt the passwords in memory. NiFi should also
> include a utility to encrypt the raw passwords (and optionally populate the
> configuration files and provide additional metadata in the configuration
> files).
> I am aware this simply shifts the responsibility/delegation of trust from the
> passwords in the properties file to a new location on the same system, but
> mitigating the visibility of the raw passwords in the properties file can be
> one step in a defense in depth approach and is often mandated by security
> policies within organizations using NiFi.
> The key used for encryption should not be hard-coded into the application
> source code, nor should it be universally consistent. The key could be
> determined by reading static information from the deployed system and feeding
> it to a key derivation function based on a cryptographically-secure hash
> function, such as PBKDF2, bcrypt, or scrypt. However, this does introduce
> upgrade, system migration, and portability issues. These challenges will have
> to be kept in consideration when determining the key derivation process.
> Manual key entry is a possibility, and then the master key would only be
> present in memory, but this prevents automatic reboot on loss of power or
> other recovery scenario.
> This must be backward-compatible to allow systems with plaintext passwords to
> continue operating. Options for achieving this are to only attempt to decrypt
> passwords when a sibling property is present, or to match a specific format.
> For these examples, I have used the following default values:
> {code}
> password: thisIsABadPassword
> key: 0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210
> iv: 0123456789ABCDEFFEDCBA9876543210
> algorithm: AES/CBC 256-bit
> {code}
> **Note: These values should not be used in production systems -- the key and
> IV are common test values, and an AEAD cipher is preferable to provide cipher
> text integrity assurances, however OpenSSL does not support the use of AEAD
> ciphers for command-line encryption at this time**
> Example 1: *here the sibling property indicates the password is encrypted and
> with which implementation; the absence of the property would default to a raw
> password*
> {code}
> hw12203:/Users/alopresto/Workspace/scratch/encrypted-passwords (master)
> alopresto
> 🔓 0s @ 16:25:56 $ echo "thisIsABadPassword" > password.txt
> hw12203:/Users/alopresto/Workspace/scratch/encrypted-passwords (master)
> alopresto
> 🔓 0s @ 16:26:47 $ ossl aes-256-cbc -e -nosalt -p -K
> 0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 -iv
> 0123456789ABCDEFFEDCBA9876543210 -a -in password.txt -out password.enc
> key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210
> iv =0123456789ABCDEFFEDCBA9876543210
> hw12203:/Users/alopresto/Workspace/scratch/encrypted-passwords (master)
> alopresto
> 🔓 0s @ 16:27:09 $ xxd password.enc
> 0000000: 5643 5856 6146 6250 4158 364f 5743 7646 VCXVaFbPAX6OWCvF
> 0000010: 6963 6b76 4a63 7744 3854 6b67 3731 4c76 ickvJcwD8Tkg71Lv
> 0000020: 4d38 6d32 7952 4776 5739 413d 0a M8m2yRGvW9A=.
> hw12203:/Users/alopresto/Workspace/scratch/encrypted-passwords (master)
> alopresto
> 🔓 0s @ 16:27:16 $ more password.enc
> VCXVaFbPAX6OWCvFickvJcwD8Tkg71LvM8m2yRGvW9A=
> hw12203:/Users/alopresto/Workspace/scratch/encrypted-passwords (master)
> alopresto
> 🔓 0s @ 16:27:55 $
> {code}
> In {{nifi.properties}}:
> {code}
> nifi.security.keystorePasswd=VCXVaFbPAX6OWCvFickvJcwD8Tkg71LvM8m2yRGvW9A=
> nifi.security.keystorePasswd.encrypted=AES-CBC-256
> {code}
> Example 2: *here the encrypted password has a header tag indicating both that
> it is encrypted and the algorithm used*
> {code:java}
> @Test
> public void testShouldDecryptPassword() throws Exception {
> // Arrange
> KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
> final String PLAINTEXT = "thisIsABadPassword"
> logger.info("Expected: ${Hex.encodeHexString(PLAINTEXT.bytes)}")
> final byte[] IV = Hex.decodeHex("0123456789ABCDEFFEDCBA9876543210" as
> char[])
> final byte[] LOCAL_KEY =
> Hex.decodeHex("0123456789ABCDEFFEDCBA9876543210" * 2 as char[])
> // Generated via openssl enc -a
> final String CIPHER_TEXT =
> "VCXVaFbPAX6OWCvFickvJcwD8Tkg71LvM8m2yRGvW9A="
> byte[] cipherBytes = Base64.decoder.decode(CIPHER_TEXT)
> SecretKey localKey = new SecretKeySpec(LOCAL_KEY, "AES")
> EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
> logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
> logger.info("Cipher text: \$nifipw\$${CIPHER_TEXT}
> ${cipherBytes.length + 8}")
> // Act
> Cipher cipher = cipherProvider.getCipher(encryptionMethod, localKey,
> IV, false)
> byte[] recoveredBytes = cipher.doFinal(cipherBytes)
>
> // OpenSSL adds a newline character during encryption
> String recovered = new String(recoveredBytes, "UTF-8").trim()
> logger.info("Recovered: ${recovered}
> ${Hex.encodeHexString(recoveredBytes)}")
> // Assert
> assert PLAINTEXT.equals(recovered)
> }
> {code}
> In {{nifi.properties}}:
> {code}
> nifi.security.keystorePasswd=$nifipw$VCXVaFbPAX6OWCvFickvJcwD8Tkg71LvM8m2yRGvW9A=
> {code}
> Ideally, NiFi would use a pluggable implementation architecture to allow
> users to integrate with a variety of secret management services. There are
> both commercial and open source solutions, including CyberArk Enterprise
> Password Vault [1], Hashicorp Vault [2], and Square Keywhiz [3]. In the
> future, this could also be extended to Hardware Security Modules (HSM) like
> SafeNet Luna [4] and Amazon CloudHSM [5].
> [1]
> http://www.cyberark.com/products/privileged-account-security-solution/enterprise-password-vault/
> [2] https://www.vaultproject.io/
> [3] https://square.github.io/keywhiz/
> [4]
> http://www.safenet-inc.com/data-encryption/hardware-security-modules-hsms/luna-hsms-key-management/luna-sa-network-hsm/
> [5] https://aws.amazon.com/cloudhsm/
--
This message was sent by Atlassian JIRA
(v6.3.4#6332)