http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy
new file mode 100644
index 0000000..3b06c40
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy
@@ -0,0 +1,463 @@
+/*
+ * 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 org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.util.encoders.DecoderException
+import org.bouncycastle.util.encoders.Hex
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import java.nio.charset.StandardCharsets
+import java.security.SecureRandom
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class AESSensitivePropertyProviderTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(AESSensitivePropertyProviderTest.class)
+
+    private static final String KEY_128_HEX = 
"0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_256_HEX = KEY_128_HEX * 2
+    private static final int IV_LENGTH = 
AESSensitivePropertyProvider.getIvLength()
+
+    private static final List<Integer> KEY_SIZES = getAvailableKeySizes()
+
+    private static final SecureRandom secureRandom = new SecureRandom()
+
+    private static final Base64.Encoder encoder = Base64.encoder
+    private static final Base64.Decoder decoder = Base64.decoder
+
+    @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 {
+
+    }
+
+    private static Cipher getCipher(boolean encrypt = true, int keySize = 256, 
byte[] iv = [0x00] * IV_LENGTH) {
+        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding")
+        String key = getKeyOfSize(keySize)
+        cipher.init((encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as 
int, new SecretKeySpec(Hex.decode(key), "AES"), new IvParameterSpec(iv))
+        logger.setup("Initialized a cipher in ${encrypt ? "encrypt" : 
"decrypt"} mode with a key of length ${keySize} bits")
+        cipher
+    }
+
+    private static String getKeyOfSize(int keySize = 256) {
+        switch (keySize) {
+            case 128:
+                return KEY_128_HEX
+            case 192:
+            case 256:
+                if (Cipher.getMaxAllowedKeyLength("AES") < keySize) {
+                    throw new IllegalArgumentException("The JCE unlimited 
strength cryptographic jurisdiction policies are not installed, so the max key 
size is 128 bits")
+                }
+                return KEY_256_HEX[0..<(keySize / 4)]
+            default:
+                throw new IllegalArgumentException("Key size ${keySize} bits 
is not valid")
+        }
+    }
+
+    private static List<Integer> getAvailableKeySizes() {
+        if (Cipher.getMaxAllowedKeyLength("AES") > 128) {
+            [128, 192, 256]
+        } else {
+            [128]
+        }
+    }
+
+    private static String manipulateString(String input, int start = 0, int 
end = input?.length()) {
+        if ((input[start..end] as List).unique().size() == 1) {
+            throw new IllegalArgumentException("Can't manipulate a String 
where the entire range is identical [${input[start..end]}]")
+        }
+        List shuffled = input[start..end] as List
+        Collections.shuffle(shuffled)
+        String reconstituted = input[0..<start] + shuffled.join() + input[end 
+ 1..-1]
+        return reconstituted != input ? reconstituted : 
manipulateString(input, start, end)
+    }
+
+    @Test
+    public void testShouldThrowExceptionOnInitializationWithoutBouncyCastle() 
throws Exception {
+        // Arrange
+        try {
+            Security.removeProvider(new BouncyCastleProvider().getName())
+
+            // Act
+            def msg = shouldFail(SensitivePropertyProtectionException) {
+                SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(KEY_128_HEX))
+                logger.error("This should not be reached")
+            }
+
+            // Assert
+            assert msg =~ "Error initializing the protection cipher"
+        } finally {
+            Security.addProvider(new BouncyCastleProvider())
+        }
+    }
+
+    // TODO: testShouldGetName()
+
+    @Test
+    public void testShouldProtectValue() throws Exception {
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        Map<Integer, String> CIPHER_TEXTS = KEY_SIZES.collectEntries { int 
keySize ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            [(keySize): spp.protect(PLAINTEXT)]
+        }
+        CIPHER_TEXTS.each { ks, ct -> logger.info("Encrypted for ${ks} length 
key: ${ct}") }
+
+        // Assert
+
+        // The IV generation is part of #protect, so the expected cipher text 
values must be generated after #protect has run
+        Map<Integer, Cipher> decryptionCiphers = CIPHER_TEXTS.collectEntries { 
int keySize, String cipherText ->
+            // The 12 byte IV is the first 16 Base64-encoded characters of the 
"complete" cipher text
+            byte[] iv = decoder.decode(cipherText[0..<16])
+            [(keySize): getCipher(false, keySize, iv)]
+        }
+        Map<Integer, String> plaintexts = decryptionCiphers.collectEntries { 
Map.Entry<Integer, Cipher> e ->
+            String cipherTextWithoutIVAndDelimiter = 
CIPHER_TEXTS[e.key][18..-1]
+            String plaintext = new 
String(e.value.doFinal(decoder.decode(cipherTextWithoutIVAndDelimiter)), 
StandardCharsets.UTF_8)
+            [(e.key): plaintext]
+        }
+        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} 
length key: ${ct}") }
+
+        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
+    }
+
+    @Test
+    public void testShouldHandleProtectEmptyValue() throws Exception {
+        final List<String> EMPTY_PLAINTEXTS = ["", "    ", null]
+
+        // Act
+        KEY_SIZES.collectEntries { int keySize ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            EMPTY_PLAINTEXTS.each { String emptyPlaintext ->
+                def msg = shouldFail(IllegalArgumentException) {
+                    spp.protect(emptyPlaintext)
+                }
+                logger.expected("${msg} for keySize ${keySize} and plaintext 
[${emptyPlaintext}]")
+
+                // Assert
+                assert msg == "Cannot encrypt an empty value"
+            }
+        }
+    }
+
+    @Test
+    public void testShouldUnprotectValue() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { 
int keySize ->
+            byte[] iv = new byte[IV_LENGTH]
+            secureRandom.nextBytes(iv)
+            [(keySize): getCipher(true, keySize, iv)]
+        }
+
+        Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { 
Map.Entry<Integer, Cipher> e ->
+            String iv = encoder.encodeToString(e.value.getIV())
+            String cipherText = 
encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8)))
+            [(e.key): "${iv}||${cipherText}"]
+        }
+        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} 
length key: ${ct}") }
+
+        // Act
+        Map<Integer, String> plaintexts = CIPHER_TEXTS.collectEntries { int 
keySize, String cipherText ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            [(keySize): spp.unprotect(cipherText)]
+        }
+        plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length 
key: ${pt}") }
+
+        // Assert
+        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
+    }
+
+    /**
+     * Tests inputs where the entire String is empty/blank space/{@code null}.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testShouldHandleUnprotectEmptyValue() throws Exception {
+        // Arrange
+        final List<String> EMPTY_CIPHER_TEXTS = ["", "    ", null]
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
+                def msg = shouldFail(IllegalArgumentException) {
+                    spp.unprotect(emptyCipherText)
+                }
+                logger.expected("${msg} for keySize ${keySize} and cipher text 
[${emptyCipherText}]")
+
+                // Assert
+                assert msg == "Cannot decrypt a cipher text shorter than 
${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
+            }
+        }
+    }
+
+    @Test
+    public void testShouldHandleUnprotectMalformedValue() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            String cipherText = spp.protect(PLAINTEXT)
+            // Swap two characters in the cipher text
+            final String MALFORMED_CIPHER_TEXT = manipulateString(cipherText, 
25, 28)
+            logger.info("Manipulated ${cipherText} 
to\n${MALFORMED_CIPHER_TEXT.padLeft(163)}")
+
+            def msg = shouldFail(SensitivePropertyProtectionException) {
+                spp.unprotect(MALFORMED_CIPHER_TEXT)
+            }
+            logger.expected("${msg} for keySize ${keySize} and cipher text 
[${MALFORMED_CIPHER_TEXT}]")
+
+            // Assert
+            assert msg == "Error decrypting a protected value"
+        }
+    }
+
+    @Test
+    public void testShouldHandleUnprotectMissingIV() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            String cipherText = spp.protect(PLAINTEXT)
+            // Remove the IV from the "complete" cipher text
+            final String MISSING_IV_CIPHER_TEXT = cipherText[18..-1]
+            logger.info("Manipulated ${cipherText} 
to\n${MISSING_IV_CIPHER_TEXT.padLeft(163)}")
+
+            def msg = shouldFail(IllegalArgumentException) {
+                spp.unprotect(MISSING_IV_CIPHER_TEXT)
+            }
+            logger.expected("${msg} for keySize ${keySize} and cipher text 
[${MISSING_IV_CIPHER_TEXT}]")
+
+            // Remove the IV from the "complete" cipher text but keep the 
delimiter
+            final String MISSING_IV_CIPHER_TEXT_WITH_DELIMITER = 
cipherText[16..-1]
+            logger.info("Manipulated ${cipherText} 
to\n${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER.padLeft(163)}")
+
+            def msgWithDelimiter = shouldFail(DecoderException) {
+                spp.unprotect(MISSING_IV_CIPHER_TEXT_WITH_DELIMITER)
+            }
+            logger.expected("${msgWithDelimiter} for keySize ${keySize} and 
cipher text [${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER}]")
+
+            // Assert
+            assert msg == "The cipher text does not contain the delimiter || 
-- it should be of the form Base64(IV) || Base64(cipherText)"
+
+            // Assert
+            assert msgWithDelimiter =~ "unable to decode base64 string"
+        }
+    }
+
+    /**
+     * Tests inputs which have a valid IV and delimiter but no "cipher text".
+     *
+     * @throws Exception
+     */
+    @Test
+    public void testShouldHandleUnprotectEmptyCipherText() throws Exception {
+        // Arrange
+        final String IV_AND_DELIMITER = "${encoder.encodeToString("Bad IV 
value".getBytes(StandardCharsets.UTF_8))}||"
+        logger.info("IV and delimiter: ${IV_AND_DELIMITER}")
+
+        final List<String> EMPTY_CIPHER_TEXTS = ["", "      ", "\n"].collect { 
"${IV_AND_DELIMITER}${it}" }
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
+                def msg = shouldFail(IllegalArgumentException) {
+                    spp.unprotect(emptyCipherText)
+                }
+                logger.expected("${msg} for keySize ${keySize} and cipher text 
[${emptyCipherText}]")
+
+                // Assert
+                assert msg == "Cannot decrypt a cipher text shorter than 
${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
+            }
+        }
+    }
+
+    @Test
+    public void testShouldHandleUnprotectMalformedIV() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            String cipherText = spp.protect(PLAINTEXT)
+            // Swap two characters in the IV
+            final String MALFORMED_IV_CIPHER_TEXT = 
manipulateString(cipherText, 8, 11)
+            logger.info("Manipulated ${cipherText} 
to\n${MALFORMED_IV_CIPHER_TEXT.padLeft(163)}")
+
+            def msg = shouldFail(SensitivePropertyProtectionException) {
+                spp.unprotect(MALFORMED_IV_CIPHER_TEXT)
+            }
+            logger.expected("${msg} for keySize ${keySize} and cipher text 
[${MALFORMED_IV_CIPHER_TEXT}]")
+
+            // Assert
+            assert msg == "Error decrypting a protected value"
+        }
+    }
+
+    @Test
+    public void testShouldGetImplementationKeyWithDifferentMaxKeyLengths() 
throws Exception {
+        // Arrange
+        final int MAX_KEY_SIZE = getAvailableKeySizes().max()
+        final String EXPECTED_IMPL_KEY = "aes/gcm/${MAX_KEY_SIZE}"
+        logger.expected("Implementation key: ${EXPECTED_IMPL_KEY}")
+
+        // Act
+        String key = new 
AESSensitivePropertyProvider(getKeyOfSize(MAX_KEY_SIZE)).getIdentifierKey()
+        logger.info("Implementation key: ${key}")
+
+        // Assert
+        assert key == EXPECTED_IMPL_KEY
+    }
+
+    @Test
+    public void testShouldNotAllowEmptyKey() throws Exception {
+        // Arrange
+        final String INVALID_KEY = ""
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            AESSensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(INVALID_KEY)
+        }
+
+        // Assert
+        assert msg == "The key cannot be empty"
+    }
+
+    @Test
+    public void testShouldNotAllowIncorrectlySizedKey() throws Exception {
+        // Arrange
+        final String INVALID_KEY = "Z" * 31
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            AESSensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(INVALID_KEY)
+        }
+
+        // Assert
+        assert msg == "The key must be a valid hexadecimal key"
+    }
+
+    @Test
+    public void testShouldNotAllowInvalidKey() throws Exception {
+        // Arrange
+        final String INVALID_KEY = "Z" * 32
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            AESSensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(INVALID_KEY)
+        }
+
+        // Assert
+        assert msg == "The key must be a valid hexadecimal key"
+    }
+
+    /**
+     * This test is to ensure internal consistency and allow for encrypting 
value for various property files
+     */
+    @Test
+    public void testShouldEncryptArbitraryValues() {
+        // Arrange
+        def values = ["thisIsABadSensitiveKeyPassword", 
"thisIsABadKeystorePassword", "thisIsABadKeyPassword", 
"thisIsABadTruststorePassword", "This is an encrypted banner message"]
+
+        String key = getKeyOfSize(128)
+        // key = "0" * 64
+
+        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
+
+        // Act
+        def encryptedValues = values.collect { String v ->
+            def encryptedValue = spp.protect(v)
+            logger.info("${v} -> ${encryptedValue}")
+            def (String iv, String cipherText) = encryptedValue.tokenize("||")
+            logger.info("Normal Base64 encoding would be 
${encoder.encodeToString(decoder.decode(iv))}||${encoder.encodeToString(decoder.decode(cipherText))}")
+            encryptedValue
+        }
+
+        // Assert
+        assert values == encryptedValues.collect { spp.unprotect(it) }
+    }
+
+    /**
+     * This test is to ensure external compatibility in case someone encodes 
the encrypted value with Base64 and does not remove the padding
+     */
+    @Test
+    public void testShouldDecryptPaddedValue() {
+        // Arrange
+        Assume.assumeTrue("JCE unlimited strength crypto policy must be 
installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+
+        final String EXPECTED_VALUE = "thisIsABadKeyPassword"
+        String cipherText = 
"ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg=="
+        String unpaddedCipherText = cipherText.replaceAll("=", "")
+
+        String key = getKeyOfSize(256)
+
+        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
+
+        // Act
+        String rawValue = spp.unprotect(cipherText)
+        logger.info("Decrypted ${cipherText} to ${rawValue}")
+        String rawUnpaddedValue = spp.unprotect(unpaddedCipherText)
+        logger.info("Decrypted ${unpaddedCipherText} to ${rawUnpaddedValue}")
+
+        // Assert
+        assert rawValue == EXPECTED_VALUE
+        assert rawUnpaddedValue == EXPECTED_VALUE
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy
new file mode 100644
index 0000000..e1a0c65
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy
@@ -0,0 +1,393 @@
+/*
+ * 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 org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+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.Security
+
+@RunWith(JUnit4.class)
+class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(NiFiPropertiesLoaderGroovyTest.class)
+
+    final def DEFAULT_SENSITIVE_PROPERTIES = [
+            "nifi.sensitive.props.key",
+            "nifi.security.keystorePasswd",
+            "nifi.security.keyPasswd",
+            "nifi.security.truststorePasswd"
+    ]
+
+    final def COMMON_ADDITIONAL_SENSITIVE_PROPERTIES = [
+            "nifi.sensitive.props.algorithm",
+            "nifi.kerberos.service.principal",
+            "nifi.kerberos.krb5.file",
+            "nifi.kerberos.keytab.location"
+    ]
+
+    private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" * 
2
+
+    private static String originalPropertiesPath = 
System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+    private
+    final Set<PosixFilePermission> ownerReadWrite = 
[PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_READ]
+
+    @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 {
+        // Clear the sensitive property providers between runs
+//        if (ProtectedNiFiProperties.@localProviderCache) {
+//            ProtectedNiFiProperties.@localProviderCache = [:]
+//        }
+        NiFiPropertiesLoader.@sensitivePropertyProviderFactory = null
+    }
+
+    @AfterClass
+    public static void tearDownOnce() {
+        if (originalPropertiesPath) {
+            System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
originalPropertiesPath)
+        }
+    }
+
+    @Test
+    public void testConstructorShouldCreateNewInstance() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
+
+        // Assert
+        assert !niFiPropertiesLoader.@instance
+        assert !niFiPropertiesLoader.@keyHex
+    }
+
+    @Test
+    public void testShouldCreateInstanceWithKey() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiPropertiesLoader niFiPropertiesLoader = 
NiFiPropertiesLoader.withKey(KEY_HEX)
+
+        // Assert
+        assert !niFiPropertiesLoader.@instance
+        assert niFiPropertiesLoader.@keyHex == KEY_HEX
+    }
+
+    @Test
+    public void testShouldGetDefaultProviderKey() throws Exception {
+        // Arrange
+        final String EXPECTED_PROVIDER_KEY = 
"aes/gcm/${Cipher.getMaxAllowedKeyLength("AES") > 128 ? 256 : 128}"
+        logger.info("Expected provider key: ${EXPECTED_PROVIDER_KEY}")
+
+        // Act
+        String defaultKey = NiFiPropertiesLoader.getDefaultProviderKey()
+        logger.info("Default key: ${defaultKey}")
+        // Assert
+        assert defaultKey == EXPECTED_PROVIDER_KEY
+    }
+
+    @Test
+    public void testShouldInitializeSensitivePropertyProviderFactory() throws 
Exception {
+        // Arrange
+        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
+
+        // Act
+        niFiPropertiesLoader.initializeSensitivePropertyProviderFactory()
+
+        // Assert
+        assert niFiPropertiesLoader.@sensitivePropertyProviderFactory
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromFile() throws Exception 
{
+        // Arrange
+        File unprotectedFile = new 
File("src/test/resources/conf/nifi.properties")
+        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
+
+        // Act
+        NiFiProperties niFiProperties = 
niFiPropertiesLoader.load(unprotectedFile)
+
+        // Assert
+        assert niFiProperties.size() > 0
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert niFiProperties instanceof StandardNiFiProperties
+    }
+
+    @Test
+    public void testShouldNotLoadUnprotectedPropertiesFromNullFile() throws 
Exception {
+        // Arrange
+        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            NiFiProperties niFiProperties = niFiPropertiesLoader.load(null as 
File)
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "NiFi properties file missing or unreadable"
+    }
+
+    @Test
+    public void testShouldNotLoadUnprotectedPropertiesFromMissingFile() throws 
Exception {
+        // Arrange
+        File missingFile = new 
File("src/test/resources/conf/nifi_missing.properties")
+        assert !missingFile.exists()
+
+        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            NiFiProperties niFiProperties = 
niFiPropertiesLoader.load(missingFile)
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "NiFi properties file missing or unreadable"
+    }
+
+    @Test
+    public void testShouldNotLoadUnprotectedPropertiesFromUnreadableFile() 
throws Exception {
+        // Arrange
+        File unreadableFile = new 
File("src/test/resources/conf/nifi_no_permissions.properties")
+        Files.setPosixFilePermissions(unreadableFile.toPath(), [] as Set)
+        assert !unreadableFile.canRead()
+
+        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            NiFiProperties niFiProperties = 
niFiPropertiesLoader.load(unreadableFile)
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "NiFi properties file missing or unreadable"
+
+        // Clean up to allow for indexing, etc.
+        Files.setPosixFilePermissions(unreadableFile.toPath(), ownerReadWrite)
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromPath() throws Exception 
{
+        // Arrange
+        File unprotectedFile = new 
File("src/test/resources/conf/nifi.properties")
+        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
+
+        // Act
+        NiFiProperties niFiProperties = 
niFiPropertiesLoader.load(unprotectedFile.path)
+
+        // Assert
+        assert niFiProperties.size() > 0
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert niFiProperties instanceof StandardNiFiProperties
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromProtectedFile() throws 
Exception {
+        // Arrange
+        File protectedFile = new 
File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes.properties")
+        NiFiPropertiesLoader niFiPropertiesLoader = 
NiFiPropertiesLoader.withKey(KEY_HEX)
+
+        final def EXPECTED_PLAIN_VALUES = [
+                (NiFiProperties.SENSITIVE_PROPS_KEY): 
"thisIsABadSensitiveKeyPassword",
+                (NiFiProperties.SECURITY_KEYSTORE_PASSWD): 
"thisIsABadKeystorePassword",
+                (NiFiProperties.SECURITY_KEY_PASSWD): "thisIsABadKeyPassword",
+        ]
+
+        // This method is covered in tests above, so safe to use here to 
retrieve protected properties
+        ProtectedNiFiProperties protectedNiFiProperties = 
niFiPropertiesLoader.readProtectedPropertiesFromDisk(protectedFile)
+        int totalKeysCount = 
protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes().size()
+        int protectedKeysCount = 
protectedNiFiProperties.getProtectedPropertyKeys().size()
+        logger.info("Read ${totalKeysCount} total properties 
(${protectedKeysCount} protected) from ${protectedFile.canonicalPath}")
+
+        // Act
+        NiFiProperties niFiProperties = 
niFiPropertiesLoader.load(protectedFile)
+
+        // Assert
+        assert niFiProperties.size() == totalKeysCount - protectedKeysCount
+
+        // Ensure that any key marked as protected above is different in this 
instance
+        protectedNiFiProperties.getProtectedPropertyKeys().keySet().each { 
String key ->
+            String plainValue = niFiProperties.getProperty(key)
+            String protectedValue = protectedNiFiProperties.getProperty(key)
+
+            logger.info("Checking that [${protectedValue}] -> [${plainValue}] 
== [${EXPECTED_PLAIN_VALUES[key]}]")
+
+            assert plainValue == EXPECTED_PLAIN_VALUES[key]
+            assert plainValue != protectedValue
+            assert plainValue.length() <= protectedValue.length()
+        }
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert niFiProperties instanceof StandardNiFiProperties
+    }
+
+    @Test
+    public void testShouldExtractKeyFromBootstrapFile() throws Exception {
+        // Arrange
+        def defaultNiFiPropertiesFilePath = 
"src/test/resources/bootstrap_tests/conf/nifi.properties"
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
defaultNiFiPropertiesFilePath)
+
+        // Act
+        String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+
+        // Assert
+        assert key == KEY_HEX
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromBootstrapFileWithoutKeyLine() 
throws Exception {
+        // Arrange
+        def defaultNiFiPropertiesFilePath = 
"src/test/resources/bootstrap_tests/missing_key_line/nifi.properties"
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
defaultNiFiPropertiesFilePath)
+
+        // Act
+        String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+
+        // Assert
+        assert key == ""
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromBootstrapFileWithoutKey() throws 
Exception {
+        // Arrange
+        def defaultNiFiPropertiesFilePath = 
"src/test/resources/bootstrap_tests/missing_key_line/nifi.properties"
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
defaultNiFiPropertiesFilePath)
+
+        // Act
+        String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+
+        // Assert
+        assert key == ""
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromMissingBootstrapFile() throws 
Exception {
+        // Arrange
+        def defaultNiFiPropertiesFilePath = 
"src/test/resources/bootstrap_tests/missing_bootstrap/nifi.properties"
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
defaultNiFiPropertiesFilePath)
+
+        // Act
+        def msg = shouldFail(IOException) {
+            String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "Cannot read from bootstrap.conf"
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromUnreadableBootstrapFile() throws 
Exception {
+        // Arrange
+        File unreadableFile = new 
File("src/test/resources/bootstrap_tests/unreadable_bootstrap/bootstrap.conf")
+        Set<PosixFilePermission> originalPermissions = 
Files.getPosixFilePermissions(unreadableFile.toPath())
+        Files.setPosixFilePermissions(unreadableFile.toPath(), [] as Set)
+        assert !unreadableFile.canRead()
+
+        def defaultNiFiPropertiesFilePath = 
"src/test/resources/bootstrap_tests/unreadable_bootstrap/nifi.properties"
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
defaultNiFiPropertiesFilePath)
+
+        // Act
+        def msg = shouldFail(IOException) {
+            String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "Cannot read from bootstrap.conf"
+
+        // Clean up to allow for indexing, etc.
+        Files.setPosixFilePermissions(unreadableFile.toPath(), 
originalPermissions)
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromUnreadableConfDir() throws 
Exception {
+        // Arrange
+        File unreadableDir = new 
File("src/test/resources/bootstrap_tests/unreadable_conf")
+        Set<PosixFilePermission> originalPermissions = 
Files.getPosixFilePermissions(unreadableDir.toPath())
+        Files.setPosixFilePermissions(unreadableDir.toPath(), [] as Set)
+        assert !unreadableDir.canRead()
+
+        def defaultNiFiPropertiesFilePath = 
"src/test/resources/bootstrap_tests/unreadable_conf/nifi.properties"
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
defaultNiFiPropertiesFilePath)
+
+        // Act
+        def msg = shouldFail(IOException) {
+            String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "Cannot read from bootstrap.conf"
+
+        // Clean up to allow for indexing, etc.
+        Files.setPosixFilePermissions(unreadableDir.toPath(), 
originalPermissions)
+    }
+
+    @Test
+    public void 
testShouldLoadUnprotectedPropertiesFromProtectedDefaultFileAndUseBootstrapKey() 
throws Exception {
+        // Arrange
+        File protectedFile = new 
File("src/test/resources/bootstrap_tests/conf/nifi_with_sensitive_properties_protected_aes.properties")
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
protectedFile.path)
+        NiFiPropertiesLoader niFiPropertiesLoader = 
NiFiPropertiesLoader.withKey(KEY_HEX)
+
+        NiFiProperties normalReadProperties = 
niFiPropertiesLoader.load(protectedFile)
+        logger.info("Read ${normalReadProperties.size()} total properties from 
${protectedFile.canonicalPath}")
+
+        // Act
+        NiFiProperties niFiProperties = 
NiFiPropertiesLoader.loadDefaultWithKeyFromBootstrap()
+
+        // Assert
+        assert niFiProperties.size() == normalReadProperties.size()
+
+
+        def readPropertiesAndValues = 
niFiProperties.getPropertyKeys().collectEntries {
+            [(it): niFiProperties.getProperty(it)]
+        }
+        def expectedPropertiesAndValues = 
normalReadProperties.getPropertyKeys().collectEntries {
+            [(it): normalReadProperties.getProperty(it)]
+        }
+        assert readPropertiesAndValues == expectedPropertiesAndValues
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy
new file mode 100644
index 0000000..bf4e677
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy
@@ -0,0 +1,860 @@
+/*
+ * 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 org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(ProtectedNiFiPropertiesGroovyTest.class)
+
+    final def DEFAULT_SENSITIVE_PROPERTIES = [
+            "nifi.sensitive.props.key",
+            "nifi.security.keystorePasswd",
+            "nifi.security.keyPasswd",
+            "nifi.security.truststorePasswd"
+    ]
+
+    final def COMMON_ADDITIONAL_SENSITIVE_PROPERTIES = [
+            "nifi.sensitive.props.algorithm",
+            "nifi.kerberos.service.principal",
+            "nifi.kerberos.krb5.file",
+            "nifi.kerberos.keytab.location"
+    ]
+
+    private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210" * 
2
+
+    private static String originalPropertiesPath = 
System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+
+    @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 {
+    }
+
+    @AfterClass
+    public static void tearDownOnce() {
+        if (originalPropertiesPath) {
+            System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
originalPropertiesPath)
+        }
+    }
+
+    private static ProtectedNiFiProperties loadFromFile(String 
propertiesFilePath) {
+        String filePath
+        try {
+            filePath = 
ProtectedNiFiPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath()
+        } catch (URISyntaxException ex) {
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex)
+        }
+
+        File file = new File(filePath)
+
+        if (file == null || !file.exists() || !file.canRead()) {
+            String path = (file == null ? "missing file" : 
file.getAbsolutePath())
+            logger.error("Cannot read from '{}' -- file is missing or not 
readable", path)
+            throw new IllegalArgumentException("NiFi properties file missing 
or unreadable")
+        }
+
+        Properties rawProperties = new Properties()
+
+        InputStream inStream = null
+        try {
+            inStream = new BufferedInputStream(new FileInputStream(file))
+            rawProperties.load(inStream)
+            logger.info("Loaded {} properties from {}", rawProperties.size(), 
file.getAbsolutePath())
+
+            ProtectedNiFiProperties protectedNiFiProperties = new 
ProtectedNiFiProperties(rawProperties)
+
+            // If it has protected keys, inject the SPP
+            if (protectedNiFiProperties.hasProtectedKeys()) {
+                protectedNiFiProperties.addSensitivePropertyProvider(new 
AESSensitivePropertyProvider(KEY_HEX))
+            }
+
+            return protectedNiFiProperties
+        } catch (final Exception ex) {
+            logger.error("Cannot load properties file due to " + 
ex.getLocalizedMessage())
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex)
+        } finally {
+            if (null != inStream) {
+                try {
+                    inStream.close()
+                } catch (final Exception ex) {
+                    /**
+                     * do nothing *
+                     */
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testConstructorShouldCreateNewInstance() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiProperties niFiProperties = new StandardNiFiProperties()
+        logger.info("niFiProperties has ${niFiProperties.size()} properties: 
${niFiProperties.getPropertyKeys()}")
+
+        // Assert
+        assert niFiProperties.size() == 0
+        assert niFiProperties.getPropertyKeys() == [] as Set
+    }
+
+    @Test
+    public void testConstructorShouldAcceptRawProperties() throws Exception {
+        // Arrange
+        Properties rawProperties = new Properties()
+        rawProperties.setProperty("key", "value")
+        logger.info("rawProperties has ${rawProperties.size()} properties: 
${rawProperties.stringPropertyNames()}")
+        assert rawProperties.size() == 1
+
+        // Act
+        NiFiProperties niFiProperties = new 
StandardNiFiProperties(rawProperties)
+        logger.info("niFiProperties has ${niFiProperties.size()} properties: 
${niFiProperties.getPropertyKeys()}")
+
+        // Assert
+        assert niFiProperties.size() == 1
+        assert niFiProperties.getPropertyKeys() == ["key"] as Set
+    }
+
+    @Test
+    public void testConstructorShouldAcceptNiFiProperties() throws Exception {
+        // Arrange
+        Properties rawProperties = new Properties()
+        rawProperties.setProperty("key", "value")
+        rawProperties.setProperty("key.protected", "value2")
+        NiFiProperties niFiProperties = new 
StandardNiFiProperties(rawProperties)
+        logger.info("niFiProperties has ${niFiProperties.size()} properties: 
${niFiProperties.getPropertyKeys()}")
+        assert niFiProperties.size() == 2
+
+        // Act
+        ProtectedNiFiProperties protectedNiFiProperties = new 
ProtectedNiFiProperties(niFiProperties)
+        logger.info("protectedNiFiProperties has 
${protectedNiFiProperties.size()} properties: 
${protectedNiFiProperties.getPropertyKeys()}")
+
+        // Assert
+        def allKeys = 
protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes()
+        assert allKeys == ["key", "key.protected"] as Set
+        assert allKeys.size() == niFiProperties.size()
+
+    }
+
+    @Test
+    public void testShouldAllowMultipleInstances() throws Exception {
+        // Arrange
+        Properties rawProperties = new Properties()
+        rawProperties.setProperty("key", "value")
+        logger.info("rawProperties has ${rawProperties.size()} properties: 
${rawProperties.stringPropertyNames()}")
+        assert rawProperties.size() == 1
+
+        // Act
+        NiFiProperties niFiProperties = new 
StandardNiFiProperties(rawProperties)
+        logger.info("niFiProperties has ${niFiProperties.size()} properties: 
${niFiProperties.getPropertyKeys()}")
+        NiFiProperties emptyProperties = new StandardNiFiProperties()
+        logger.info("emptyProperties has ${emptyProperties.size()} properties: 
${emptyProperties.getPropertyKeys()}")
+
+        // Assert
+        assert niFiProperties.size() == 1
+        assert niFiProperties.getPropertyKeys() == ["key"] as Set
+
+        assert emptyProperties.size() == 0
+        assert emptyProperties.getPropertyKeys() == [] as Set
+    }
+
+    @Test
+    public void testShouldDetectIfPropertyIsSensitive() throws Exception {
+        // Arrange
+        final String INSENSITIVE_PROPERTY_KEY = "nifi.ui.banner.text"
+        final String SENSITIVE_PROPERTY_KEY = "nifi.security.keystorePasswd"
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi.properties")
+
+        // Act
+        boolean bannerIsSensitive = 
properties.isPropertySensitive(INSENSITIVE_PROPERTY_KEY)
+        logger.info("${INSENSITIVE_PROPERTY_KEY} is ${bannerIsSensitive ? 
"SENSITIVE" : "NOT SENSITIVE"}")
+        boolean passwordIsSensitive = 
properties.isPropertySensitive(SENSITIVE_PROPERTY_KEY)
+        logger.info("${SENSITIVE_PROPERTY_KEY} is ${passwordIsSensitive ? 
"SENSITIVE" : "NOT SENSITIVE"}")
+
+        // Assert
+        assert !bannerIsSensitive
+        assert passwordIsSensitive
+    }
+
+    @Test
+    public void testShouldGetDefaultSensitiveProperties() throws Exception {
+        // Arrange
+        logger.expected("${DEFAULT_SENSITIVE_PROPERTIES.size()} default 
sensitive properties: ${DEFAULT_SENSITIVE_PROPERTIES.join(", ")}")
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi.properties")
+
+        // Act
+        List defaultSensitiveProperties = properties.getSensitivePropertyKeys()
+        logger.info("${defaultSensitiveProperties.size()} default sensitive 
properties: ${defaultSensitiveProperties.join(", ")}")
+
+        // Assert
+        assert defaultSensitiveProperties.size() == 
DEFAULT_SENSITIVE_PROPERTIES.size()
+        assert 
defaultSensitiveProperties.containsAll(DEFAULT_SENSITIVE_PROPERTIES)
+    }
+
+    @Test
+    public void testShouldGetAdditionalSensitiveProperties() throws Exception {
+        // Arrange
+        def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + 
["nifi.ui.banner.text", "nifi.version"]
+        logger.expected("${completeSensitiveProperties.size()} total sensitive 
properties: ${completeSensitiveProperties.join(", ")}")
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_additional_sensitive_keys.properties")
+
+        // Act
+        List retrievedSensitiveProperties = 
properties.getSensitivePropertyKeys()
+        logger.info("${retrievedSensitiveProperties.size()} retrieved 
sensitive properties: ${retrievedSensitiveProperties.join(", ")}")
+
+        // Assert
+        assert retrievedSensitiveProperties.size() == 
completeSensitiveProperties.size()
+        assert 
retrievedSensitiveProperties.containsAll(completeSensitiveProperties)
+    }
+
+    // TODO: Add negative tests (fuzz additional.keys property, etc.)
+
+    @Test
+    public void testGetAdditionalSensitivePropertiesShouldNotIncludeSelf() 
throws Exception {
+        // Arrange
+        def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + 
["nifi.ui.banner.text", "nifi.version"]
+        logger.expected("${completeSensitiveProperties.size()} total sensitive 
properties: ${completeSensitiveProperties.join(", ")}")
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_additional_sensitive_keys.properties")
+
+        // Act
+        List retrievedSensitiveProperties = 
properties.getSensitivePropertyKeys()
+        logger.info("${retrievedSensitiveProperties.size()} retrieved 
sensitive properties: ${retrievedSensitiveProperties.join(", ")}")
+
+        // Assert
+        assert retrievedSensitiveProperties.size() == 
completeSensitiveProperties.size()
+        assert 
retrievedSensitiveProperties.containsAll(completeSensitiveProperties)
+    }
+
+    /**
+     * In the default (no protection enabled) scenario, a call to retrieve a 
sensitive property should return the raw value transparently.
+     * @throws Exception
+     */
+    @Test
+    public void testShouldGetUnprotectedValueOfSensitiveProperty() throws 
Exception {
+        // Arrange
+        final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
+        final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword"
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_unprotected.properties")
+
+        boolean isSensitive = 
properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        String retrievedKeystorePassword = 
properties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == EXPECTED_KEYSTORE_PASSWORD
+        assert isSensitive
+        assert !isProtected
+    }
+
+    /**
+     * In the default (no protection enabled) scenario, a call to retrieve a 
sensitive property (which is empty) should return the raw value transparently.
+     * @throws Exception
+     */
+    @Test
+    public void testShouldGetEmptyUnprotectedValueOfSensitiveProperty() throws 
Exception {
+        // Arrange
+        final String TRUSTSTORE_PASSWORD_KEY = "nifi.security.truststorePasswd"
+        final String EXPECTED_TRUSTSTORE_PASSWORD = ""
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_unprotected.properties")
+
+        boolean isSensitive = 
properties.isPropertySensitive(TRUSTSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(TRUSTSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiProperties unprotectedProperties = 
properties.getUnprotectedProperties()
+        String retrievedTruststorePassword = 
unprotectedProperties.getProperty(TRUSTSTORE_PASSWORD_KEY)
+        logger.info("${TRUSTSTORE_PASSWORD_KEY}: 
${retrievedTruststorePassword}")
+
+        // Assert
+        assert retrievedTruststorePassword == EXPECTED_TRUSTSTORE_PASSWORD
+        assert isSensitive
+        assert !isProtected
+    }
+
+    /**
+     * The new model no longer needs to maintain the protected state -- it is 
used as a wrapper/decorator during load to unprotect the sensitive properties 
and then return an instance of raw properties.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void 
testShouldGetUnprotectedValueOfSensitivePropertyWhenProtected() throws 
Exception {
+        // Arrange
+        final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
+        final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword"
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties")
+
+        boolean isSensitive = 
properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiProperties unprotectedProperties = 
properties.getUnprotectedProperties()
+        String retrievedKeystorePassword = 
unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == EXPECTED_KEYSTORE_PASSWORD
+        assert isSensitive
+        assert isProtected
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive 
property should handle if the property is protected with an unknown protection 
scheme.
+     * @throws Exception
+     */
+    @Test
+    public void 
testGetValueOfSensitivePropertyShouldHandleUnknownProtectionScheme() throws 
Exception {
+        // Arrange
+        final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
+
+        // Raw properties
+        Properties rawProperties = new Properties()
+        rawProperties.load(new 
File("src/test/resources/conf/nifi_with_sensitive_properties_protected_unknown.properties").newInputStream())
+        final String RAW_KEYSTORE_PASSWORD = 
rawProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: 
${RAW_KEYSTORE_PASSWORD}")
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_unknown.properties")
+
+        boolean isSensitive = 
properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+
+        // While the value is "protected", the scheme is not registered, so 
treat it as raw
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiProperties unprotectedProperties = 
properties.getUnprotectedProperties()
+        String retrievedKeystorePassword = 
unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == RAW_KEYSTORE_PASSWORD
+        assert isSensitive
+        assert isProtected
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive 
property should handle if the property is unable to be unprotected due to a 
malformed value.
+     * @throws Exception
+     */
+    @Test
+    public void 
testGetValueOfSensitivePropertyShouldHandleSingleMalformedValue() throws 
Exception {
+        // Arrange
+        final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
+
+        // Raw properties
+        Properties rawProperties = new Properties()
+        rawProperties.load(new 
File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes_single_malformed.properties").newInputStream())
+        final String RAW_KEYSTORE_PASSWORD = 
rawProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: 
${RAW_KEYSTORE_PASSWORD}")
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes_single_malformed.properties")
+
+        boolean isSensitive = 
properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            NiFiProperties unprotectedProperties = 
properties.getUnprotectedProperties()
+            String retrievedKeystorePassword = 
unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+            logger.info("${KEYSTORE_PASSWORD_KEY}: 
${retrievedKeystorePassword}")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "Failed to unprotect key ${KEYSTORE_PASSWORD_KEY}"
+        assert isSensitive
+        assert isProtected
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive 
property should handle if the property is unable to be unprotected due to a 
malformed value.
+     * @throws Exception
+     */
+    @Test
+    public void 
testGetValueOfSensitivePropertyShouldHandleMultipleMalformedValues() throws 
Exception {
+        // Arrange
+
+        // Raw properties
+        Properties rawProperties = new Properties()
+        rawProperties.load(new 
File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes_multiple_malformed.properties").newInputStream())
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes_multiple_malformed.properties")
+
+        // Iterate over the protected keys and track the ones that fail to 
decrypt
+        SensitivePropertyProvider spp = new 
AESSensitivePropertyProvider(KEY_HEX)
+        Set<String> malformedKeys = properties.getProtectedPropertyKeys()
+                .findAll { String key, String scheme -> scheme == 
spp.identifierKey }
+                .keySet().collect { String key ->
+            try {
+                String rawValue = spp.unprotect(properties.getProperty(key))
+                return
+            } catch (SensitivePropertyProtectionException e) {
+                logger.expected("Caught a malformed value for ${key}")
+                return key
+            }
+        }
+
+        logger.expected("Malformed keys: ${malformedKeys.join(", ")}")
+
+        // Act
+        def e = 
groovy.test.GroovyAssert.shouldFail(SensitivePropertyProtectionException) {
+            NiFiProperties unprotectedProperties = 
properties.getUnprotectedProperties()
+        }
+        logger.expected(e.getMessage())
+
+        // Assert
+        assert e instanceof MultipleSensitivePropertyProtectionException
+        assert e.getMessage() =~ "Failed to unprotect keys"
+        assert e.getFailedKeys() == malformedKeys
+
+    }
+
+    /**
+     * In the default (no protection enabled) scenario, a call to retrieve a 
sensitive property (which is empty) should return the raw value transparently.
+     * @throws Exception
+     */
+    @Test
+    public void 
testShouldGetEmptyUnprotectedValueOfSensitivePropertyWithDefault() throws 
Exception {
+        // Arrange
+        final String TRUSTSTORE_PASSWORD_KEY = "nifi.security.truststorePasswd"
+        final String EXPECTED_TRUSTSTORE_PASSWORD = ""
+        final String DEFAULT_VALUE = "defaultValue"
+
+        // Raw properties
+        Properties rawProperties = new Properties()
+        rawProperties.load(new 
File("src/test/resources/conf/nifi_with_sensitive_properties_unprotected.properties").newInputStream())
+        final String RAW_TRUSTSTORE_PASSWORD = 
rawProperties.getProperty(TRUSTSTORE_PASSWORD_KEY)
+        logger.info("Raw value for ${TRUSTSTORE_PASSWORD_KEY}: 
${RAW_TRUSTSTORE_PASSWORD}")
+        assert RAW_TRUSTSTORE_PASSWORD == EXPECTED_TRUSTSTORE_PASSWORD
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_unprotected.properties")
+
+        boolean isSensitive = 
properties.isPropertySensitive(TRUSTSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(TRUSTSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        String retrievedTruststorePassword = 
properties.getProperty(TRUSTSTORE_PASSWORD_KEY, DEFAULT_VALUE)
+        logger.info("${TRUSTSTORE_PASSWORD_KEY}: 
${retrievedTruststorePassword}")
+
+        // Assert
+        assert retrievedTruststorePassword == DEFAULT_VALUE
+        assert isSensitive
+        assert !isProtected
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive 
property should return the raw value transparently.
+     * @throws Exception
+     */
+    @Test
+    public void 
testShouldGetUnprotectedValueOfSensitivePropertyWhenProtectedWithDefault() 
throws Exception {
+        // Arrange
+        final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
+        final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword"
+        final String DEFAULT_VALUE = "defaultValue"
+
+        // Raw properties
+        Properties rawProperties = new Properties()
+        rawProperties.load(new 
File("src/test/resources/conf/nifi_with_sensitive_properties_protected_aes.properties").newInputStream())
+        final String RAW_KEYSTORE_PASSWORD = 
rawProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: 
${RAW_KEYSTORE_PASSWORD}")
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties")
+
+        boolean isSensitive = 
properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiProperties unprotectedProperties = 
properties.getUnprotectedProperties()
+        String retrievedKeystorePassword = 
unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY, DEFAULT_VALUE)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == EXPECTED_KEYSTORE_PASSWORD
+        assert isSensitive
+        assert isProtected
+    }
+
+    // TODO: Test getProtected with multiple providers
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive 
property should handle if the internal cache of providers is empty.
+     * @throws Exception
+     */
+    @Test
+    public void 
testGetValueOfSensitivePropertyShouldHandleInvalidatedInternalCache() throws 
Exception {
+        // Arrange
+        final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
+        final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword"
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties")
+
+        final String RAW_PASSWORD = 
properties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("Read raw value from properties: ${RAW_PASSWORD}")
+
+        // Overwrite the internal cache
+        properties.localProviderCache = [:]
+
+        boolean isSensitive = 
properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = 
properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not 
sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiProperties unprotectedProperties = 
properties.getUnprotectedProperties()
+        String retrievedKeystorePassword = 
unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == RAW_PASSWORD
+        assert isSensitive
+        assert isProtected
+    }
+
+    @Test
+    public void testShouldDetectIfPropertyIsProtected() throws Exception {
+        // Arrange
+        final String UNPROTECTED_PROPERTY_KEY = 
"nifi.security.truststorePasswd"
+        final String PROTECTED_PROPERTY_KEY = "nifi.security.keystorePasswd"
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties")
+
+        // Act
+        boolean unprotectedPasswordIsSensitive = 
properties.isPropertySensitive(UNPROTECTED_PROPERTY_KEY)
+        boolean unprotectedPasswordIsProtected = 
properties.isPropertyProtected(UNPROTECTED_PROPERTY_KEY)
+        logger.info("${UNPROTECTED_PROPERTY_KEY} is 
${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+        logger.info("${UNPROTECTED_PROPERTY_KEY} is 
${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}")
+        boolean protectedPasswordIsSensitive = 
properties.isPropertySensitive(PROTECTED_PROPERTY_KEY)
+        boolean protectedPasswordIsProtected = 
properties.isPropertyProtected(PROTECTED_PROPERTY_KEY)
+        logger.info("${PROTECTED_PROPERTY_KEY} is 
${protectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+        logger.info("${PROTECTED_PROPERTY_KEY} is 
${protectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}")
+
+        // Assert
+        assert unprotectedPasswordIsSensitive
+        assert !unprotectedPasswordIsProtected
+
+        assert protectedPasswordIsSensitive
+        assert protectedPasswordIsProtected
+    }
+
+    @Test
+    public void 
testShouldDetectIfPropertyWithEmptyProtectionSchemeIsProtected() throws 
Exception {
+        // Arrange
+        final String UNPROTECTED_PROPERTY_KEY = "nifi.sensitive.props.key"
+
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_unprotected_extra_line.properties")
+
+        // Act
+        boolean unprotectedPasswordIsSensitive = 
properties.isPropertySensitive(UNPROTECTED_PROPERTY_KEY)
+        boolean unprotectedPasswordIsProtected = 
properties.isPropertyProtected(UNPROTECTED_PROPERTY_KEY)
+        logger.info("${UNPROTECTED_PROPERTY_KEY} is 
${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+        logger.info("${UNPROTECTED_PROPERTY_KEY} is 
${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}")
+
+        // Assert
+        assert unprotectedPasswordIsSensitive
+        assert !unprotectedPasswordIsProtected
+    }
+
+    @Test
+    public void testShouldGetPercentageOfSensitivePropertiesProtected_0() 
throws Exception {
+        // Arrange
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi.properties")
+
+        logger.info("Sensitive property keys: 
${properties.getSensitivePropertyKeys()}")
+        logger.info("Protected property keys: 
${properties.getProtectedPropertyKeys().keySet()}")
+
+        // Act
+        double percentProtected = 
properties.getPercentOfSensitivePropertiesProtected()
+        logger.info("${percentProtected}% 
(${properties.getProtectedPropertyKeys().size()} of 
${properties.getSensitivePropertyKeys().size()}) protected")
+
+        // Assert
+        assert percentProtected == 0.0
+    }
+
+    @Test
+    public void testShouldGetPercentageOfSensitivePropertiesProtected_50() 
throws Exception {
+        // Arrange
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes.properties")
+
+        logger.info("Sensitive property keys: 
${properties.getSensitivePropertyKeys()}")
+        logger.info("Protected property keys: 
${properties.getProtectedPropertyKeys().keySet()}")
+
+        // Act
+        double percentProtected = 
properties.getPercentOfSensitivePropertiesProtected()
+        logger.info("${percentProtected}% 
(${properties.getProtectedPropertyKeys().size()} of 
${properties.getSensitivePropertyKeys().size()}) protected")
+
+        // Assert
+        assert percentProtected == 50.0
+    }
+
+    @Test
+    public void testShouldGetPercentageOfSensitivePropertiesProtected_100() 
throws Exception {
+        // Arrange
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi_with_all_sensitive_properties_protected_aes.properties")
+
+        logger.info("Sensitive property keys: 
${properties.getSensitivePropertyKeys()}")
+        logger.info("Protected property keys: 
${properties.getProtectedPropertyKeys().keySet()}")
+
+        // Act
+        double percentProtected = 
properties.getPercentOfSensitivePropertiesProtected()
+        logger.info("${percentProtected}% 
(${properties.getProtectedPropertyKeys().size()} of 
${properties.getSensitivePropertyKeys().size()}) protected")
+
+        // Assert
+        assert percentProtected == 100.0
+    }
+
+    @Test
+    public void testInstanceWithNoProtectedPropertiesShouldNotLoadSPP() throws 
Exception {
+        // Arrange
+        ProtectedNiFiProperties properties = 
loadFromFile("/conf/nifi.properties")
+        assert properties.@localProviderCache?.isEmpty()
+
+        logger.info("Has protected properties: 
${properties.hasProtectedKeys()}")
+        assert !properties.hasProtectedKeys()
+
+        // Act
+        Map localCache = properties.@localProviderCache
+        logger.info("Internal cache ${localCache} has ${localCache.size()} 
providers loaded")
+
+        // Assert
+        assert localCache.isEmpty()
+    }
+
+    @Test
+    public void testShouldAddSensitivePropertyProvider() throws Exception {
+        // Arrange
+        ProtectedNiFiProperties properties = new ProtectedNiFiProperties()
+        assert properties.getSensitivePropertyProviders().isEmpty()
+
+        SensitivePropertyProvider mockProvider =
+                [unprotect       : { String input ->
+                    logger.mock("Mock call to #unprotect(${input})")
+                    input.reverse()
+                },
+                 getIdentifierKey: { -> "mockProvider" }] as 
SensitivePropertyProvider
+
+        // Act
+        properties.addSensitivePropertyProvider(mockProvider)
+
+        // Assert
+        assert properties.getSensitivePropertyProviders().size() == 1
+    }
+
+    @Test
+    public void testShouldNotAddNullSensitivePropertyProvider() throws 
Exception {
+        // Arrange
+        ProtectedNiFiProperties properties = new ProtectedNiFiProperties()
+        assert properties.getSensitivePropertyProviders().isEmpty()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            properties.addSensitivePropertyProvider(null)
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert properties.getSensitivePropertyProviders().size() == 0
+        assert msg == "Cannot add null SensitivePropertyProvider"
+    }
+
+    @Test
+    public void testShouldNotAllowOverwriteOfProvider() throws Exception {
+        // Arrange
+        ProtectedNiFiProperties properties = new ProtectedNiFiProperties()
+        assert properties.getSensitivePropertyProviders().isEmpty()
+
+        SensitivePropertyProvider mockProvider =
+                [unprotect       : { String input ->
+                    logger.mock("Mock call to 1#unprotect(${input})")
+                    input.reverse()
+                },
+                 getIdentifierKey: { -> "mockProvider" }] as 
SensitivePropertyProvider
+        properties.addSensitivePropertyProvider(mockProvider)
+        assert properties.getSensitivePropertyProviders().size() == 1
+
+        SensitivePropertyProvider mockProvider2 =
+                [unprotect       : { String input ->
+                    logger.mock("Mock call to 2#unprotect(${input})")
+                    input.reverse()
+                },
+                 getIdentifierKey: { -> "mockProvider" }] as 
SensitivePropertyProvider
+
+        // Act
+        def msg = shouldFail(UnsupportedOperationException) {
+            properties.addSensitivePropertyProvider(mockProvider2)
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg == "Cannot overwrite existing sensitive property provider 
registered for mockProvider"
+        assert properties.getSensitivePropertyProviders().size() == 1
+    }
+
+    @Test
+    void 
testGetUnprotectedPropertiesShouldReturnInternalInstanceWhenNoneProtected() {
+        // Arrange
+        String noProtectedPropertiesPath = "/conf/nifi.properties"
+        ProtectedNiFiProperties protectedNiFiProperties = 
loadFromFile(noProtectedPropertiesPath)
+        logger.info("Loaded ${protectedNiFiProperties.size()} properties from 
${noProtectedPropertiesPath}")
+
+        int hashCode = 
protectedNiFiProperties.internalNiFiProperties.hashCode()
+        logger.info("Hash code of internal instance: ${hashCode}")
+
+        // Act
+        NiFiProperties unprotectedNiFiProperties = 
protectedNiFiProperties.getUnprotectedProperties()
+        logger.info("Unprotected ${unprotectedNiFiProperties.size()} 
properties")
+
+        // Assert
+        assert unprotectedNiFiProperties.size() == 
protectedNiFiProperties.size()
+        assert unprotectedNiFiProperties.getPropertyKeys().every {
+            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+        }
+        logger.info("Hash code from returned unprotected instance: 
${unprotectedNiFiProperties.hashCode()}")
+        assert unprotectedNiFiProperties.hashCode() == hashCode
+    }
+
+    @Test
+    void testGetUnprotectedPropertiesShouldDecryptProtectedProperties() {
+        // Arrange
+        String noProtectedPropertiesPath = 
"/conf/nifi_with_sensitive_properties_protected_aes.properties"
+        ProtectedNiFiProperties protectedNiFiProperties = 
loadFromFile(noProtectedPropertiesPath)
+        logger.info("Loaded ${protectedNiFiProperties.size()} properties from 
${noProtectedPropertiesPath}")
+
+        int protectedPropertyCount = 
protectedNiFiProperties.getProtectedPropertyKeys().size()
+        int protectionSchemeCount = protectedNiFiProperties
+                .getPropertyKeys().findAll { it.endsWith(".protected") }
+                .size()
+        int expectedUnprotectedPropertyCount = protectedNiFiProperties.size() 
- protectionSchemeCount
+
+        String protectedProps = protectedNiFiProperties
+                .getProtectedPropertyKeys()
+                .collectEntries {
+            [(it.key): protectedNiFiProperties.getProperty(it.key)]
+        }.entrySet()
+                .join("\n")
+
+        logger.info("Detected ${protectedPropertyCount} protected properties 
and ${protectionSchemeCount} protection scheme properties")
+        logger.info("Protected properties: \n${protectedProps}")
+
+        logger.info("Expected unprotected property count: 
${expectedUnprotectedPropertyCount}")
+
+        int hashCode = 
protectedNiFiProperties.internalNiFiProperties.hashCode()
+        logger.info("Hash code of internal instance: ${hashCode}")
+
+        // Act
+        NiFiProperties unprotectedNiFiProperties = 
protectedNiFiProperties.getUnprotectedProperties()
+        logger.info("Unprotected ${unprotectedNiFiProperties.size()} 
properties")
+
+        // Assert
+        assert unprotectedNiFiProperties.size() == 
expectedUnprotectedPropertyCount
+        assert unprotectedNiFiProperties.getPropertyKeys().every {
+            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+        }
+        logger.info("Hash code from returned unprotected instance: 
${unprotectedNiFiProperties.hashCode()}")
+        assert unprotectedNiFiProperties.hashCode() != hashCode
+    }
+
+    @Test
+    void testShouldCalculateSize() {
+        // Arrange
+        Properties rawProperties = [key: "protectedValue", "key.protected": 
"scheme", "key2": "value2"] as Properties
+        ProtectedNiFiProperties protectedNiFiProperties = new 
ProtectedNiFiProperties(rawProperties)
+        logger.info("Raw properties (${rawProperties.size()}): 
${rawProperties.keySet().join(", ")}")
+
+        // Act
+        int protectedSize = protectedNiFiProperties.size()
+        logger.info("Protected properties (${protectedNiFiProperties.size()}): 
${protectedNiFiProperties.getPropertyKeys().join(", ")}")
+
+        // Assert
+        assert protectedSize == rawProperties.size() - 1
+    }
+
+    @Test
+    void testGetPropertyKeysShouldMatchSize() {
+        // Arrange
+        Properties rawProperties = [key: "protectedValue", "key.protected": 
"scheme", "key2": "value2"] as Properties
+        ProtectedNiFiProperties protectedNiFiProperties = new 
ProtectedNiFiProperties(rawProperties)
+        logger.info("Raw properties (${rawProperties.size()}): 
${rawProperties.keySet().join(", ")}")
+
+        // Act
+        def filteredKeys = protectedNiFiProperties.getPropertyKeys()
+        logger.info("Protected properties (${protectedNiFiProperties.size()}): 
${filteredKeys.join(", ")}")
+
+        // Assert
+        assert protectedNiFiProperties.size() == rawProperties.size() - 1
+        assert filteredKeys == rawProperties.keySet() - "key.protected"
+    }
+
+    @Test
+    void testShouldGetPropertyKeysIncludingProtectionSchemes() {
+        // Arrange
+        Properties rawProperties = [key: "protectedValue", "key.protected": 
"scheme", "key2": "value2"] as Properties
+        ProtectedNiFiProperties protectedNiFiProperties = new 
ProtectedNiFiProperties(rawProperties)
+        logger.info("Raw properties (${rawProperties.size()}): 
${rawProperties.keySet().join(", ")}")
+
+        // Act
+        def allKeys = 
protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes()
+        logger.info("Protected properties with schemes (${allKeys.size()}): 
${allKeys.join(", ")}")
+
+        // Assert
+        assert allKeys.size() == rawProperties.size()
+        assert allKeys == rawProperties.keySet()
+    }
+
+    // TODO: Add tests for protectPlainProperties
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy
new file mode 100644
index 0000000..c9492fb
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy
@@ -0,0 +1,150 @@
+/*
+ * 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 org.apache.nifi.util.NiFiProperties
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+@RunWith(JUnit4.class)
+class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(StandardNiFiPropertiesGroovyTest.class)
+
+    private static String originalPropertiesPath = 
System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        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 {
+    }
+
+    @AfterClass
+    public static void tearDownOnce() {
+        if (originalPropertiesPath) {
+            System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
originalPropertiesPath)
+        }
+    }
+
+    private static StandardNiFiProperties loadFromFile(String 
propertiesFilePath) {
+        String filePath;
+        try {
+            filePath = 
StandardNiFiPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath();
+        } catch (URISyntaxException ex) {
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex);
+        }
+
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, filePath);
+
+        StandardNiFiProperties properties = new StandardNiFiProperties();
+
+        // clear out existing properties
+        for (String prop : properties.stringPropertyNames()) {
+            properties.remove(prop);
+        }
+
+        InputStream inStream = null;
+        try {
+            inStream = new BufferedInputStream(new FileInputStream(filePath));
+            properties.load(inStream);
+        } catch (final Exception ex) {
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex);
+        } finally {
+            if (null != inStream) {
+                try {
+                    inStream.close();
+                } catch (Exception ex) {
+                    /**
+                     * do nothing *
+                     */
+                }
+            }
+        }
+
+        return properties;
+    }
+
+    @Test
+    public void testConstructorShouldCreateNewInstance() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiProperties niFiProperties = new StandardNiFiProperties()
+        logger.info("niFiProperties has ${niFiProperties.size()} properties: 
${niFiProperties.getPropertyKeys()}")
+
+        // Assert
+        assert niFiProperties.size() == 0
+        assert niFiProperties.getPropertyKeys() == [] as Set
+    }
+
+    @Test
+    public void testConstructorShouldAcceptRawProperties() throws Exception {
+        // Arrange
+        Properties rawProperties = new Properties()
+        rawProperties.setProperty("key", "value")
+        logger.info("rawProperties has ${rawProperties.size()} properties: 
${rawProperties.stringPropertyNames()}")
+        assert rawProperties.size() == 1
+
+        // Act
+        NiFiProperties niFiProperties = new 
StandardNiFiProperties(rawProperties)
+        logger.info("niFiProperties has ${niFiProperties.size()} properties: 
${niFiProperties.getPropertyKeys()}")
+
+        // Assert
+        assert niFiProperties.size() == 1
+        assert niFiProperties.getPropertyKeys() == ["key"] as Set
+    }
+
+    @Test
+    public void testShouldAllowMultipleInstances() throws Exception {
+        // Arrange
+        Properties rawProperties = new Properties()
+        rawProperties.setProperty("key", "value")
+        logger.info("rawProperties has ${rawProperties.size()} properties: 
${rawProperties.stringPropertyNames()}")
+        assert rawProperties.size() == 1
+
+        // Act
+        NiFiProperties niFiProperties = new 
StandardNiFiProperties(rawProperties)
+        logger.info("niFiProperties has ${niFiProperties.size()} properties: 
${niFiProperties.getPropertyKeys()}")
+        NiFiProperties emptyProperties = new StandardNiFiProperties()
+        logger.info("emptyProperties has ${emptyProperties.size()} properties: 
${emptyProperties.getPropertyKeys()}")
+
+
+        // Assert
+        assert niFiProperties.size() == 1
+        assert niFiProperties.getPropertyKeys() == ["key"] as Set
+
+        assert emptyProperties.size() == 0
+        assert emptyProperties.getPropertyKeys() == [] as Set
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf
 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf
new file mode 100644
index 0000000..9225126
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/resources/bootstrap_tests/conf/bootstrap.conf
@@ -0,0 +1,74 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Java command to use when running NiFi
+java=java
+
+# Username to use when running NiFi. This value will be ignored on Windows.
+run.as=
+
+# Configure where NiFi's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling NiFi to shutdown before explicitly killing 
the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work 
properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# The G1GC is still considered experimental but has proven to be very 
advantageous in providing great
+# performance without significant "stop-the-world" delays.
+java.arg.13=-XX:+UseG1GC
+
+#Set headless mode by default
+java.arg.14=-Djava.awt.headless=true
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210
+
+###
+# Notification Services for notifying interested parties when NiFi is stopped, 
started, dies
+###
+
+# XML File that contains the definitions of the notification services
+notification.services.file=./conf/bootstrap-notification-services.xml
+
+# In the case that we are unable to send a notification for an event, how many 
times should we retry?
+notification.max.attempts=5
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
is started?
+#nifi.start.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
is stopped?
+#nifi.stop.notification.services=email-notification
+
+# Comma-separated list of identifiers that are present in the 
notification.services.file; which services should be used to notify when NiFi 
dies?
+#nifi.dead.notification.services=email-notification
\ No newline at end of file

Reply via email to