http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..c9d4313
--- /dev/null
+++ 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.properties;
+
+public interface SensitivePropertyProviderFactory {
+
+    SensitivePropertyProvider getProvider();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
new file mode 100644
index 0000000..191b5e2
--- /dev/null
+++ 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
@@ -0,0 +1,81 @@
+/*
+ * 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.registry.security.crypto;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * An implementation of {@link CryptoKeyProvider} that loads the key from disk 
every time it is needed.
+ *
+ * The persistence-backing of the key is in the bootstrap.conf file, which 
must be provided to the
+ * constructor of this class.
+ *
+ * As key access for sensitive value decryption is only used a few times 
during server initialization,
+ * this implementation trades efficiency for security by only keeping the key 
in memory with an
+ * in-scope reference for a brief period of time (assuming callers do not 
maintain an in-scope reference).
+ *
+ * @see CryptoKeyProvider
+ */
+public class BootstrapFileCryptoKeyProvider implements CryptoKeyProvider {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(BootstrapFileCryptoKeyProvider.class);
+
+    private final String bootstrapFile;
+
+    /**
+     * Construct a new instance backed by the contents of a bootstrap.conf 
file.
+     *
+     * @param bootstrapFilePath The path to the bootstrap.conf file for this 
instance of NiFi Registry.
+     *                          Must not be null.
+     */
+    public BootstrapFileCryptoKeyProvider(final String bootstrapFilePath) {
+        if (bootstrapFilePath == null) {
+            throw new 
IllegalArgumentException(BootstrapFileCryptoKeyProvider.class.getSimpleName() + 
" cannot be initialized with null bootstrap file path.");
+        }
+        this.bootstrapFile = bootstrapFilePath;
+    }
+
+    /**
+     * @return The bootstrap file path that backs this provider instance.
+     */
+    public String getBootstrapFile() {
+        return bootstrapFile;
+    }
+
+    @Override
+    public String getKey() throws MissingCryptoKeyException {
+        try {
+            return 
CryptoKeyLoader.extractKeyFromBootstrapFile(this.bootstrapFile);
+        } catch (IOException ioe) {
+            final String errMsg = "Loading the master crypto key from 
bootstrap file '" + bootstrapFile + "' failed due to IOException.";
+            logger.warn(errMsg);
+            throw new MissingCryptoKeyException(errMsg, ioe);
+        }
+
+    }
+
+    @Override
+    public String toString() {
+        return "BootstrapFileCryptoKeyProvider{" +
+                "bootstrapFile='" + bootstrapFile + '\'' +
+                '}';
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
new file mode 100644
index 0000000..d828773
--- /dev/null
+++ 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
@@ -0,0 +1,87 @@
+/*
+ * 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.registry.security.crypto;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class CryptoKeyLoader {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(CryptoKeyLoader.class);
+
+    private static final String BOOTSTRAP_KEY_PREFIX = 
"nifi.registry.bootstrap.sensitive.key=";
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties.
+     * The key extracted from the bootstrap.conf file at the specified 
location.
+     *
+     * @param bootstrapPath the path to the bootstrap file
+     * @return the key in hexadecimal format, or {@link 
CryptoKeyProvider#EMPTY_KEY} if the key is null or empty
+     * @throws IOException if the file is not readable
+     */
+    public static String extractKeyFromBootstrapFile(String bootstrapPath) 
throws IOException {
+        File bootstrapFile;
+        if (StringUtils.isBlank(bootstrapPath)) {
+            logger.error("Cannot read from bootstrap.conf file to extract 
encryption key; location not specified");
+            throw new IOException("Cannot read from bootstrap.conf without 
file location");
+        } else {
+            bootstrapFile = new File(bootstrapPath);
+        }
+
+        String keyValue;
+        if (bootstrapFile.exists() && bootstrapFile.canRead()) {
+            try (Stream<String> stream = 
Files.lines(Paths.get(bootstrapFile.getAbsolutePath()))) {
+                Optional<String> keyLine = stream.filter(l -> 
l.startsWith(BOOTSTRAP_KEY_PREFIX)).findFirst();
+                if (keyLine.isPresent()) {
+                    keyValue = keyLine.get().split("=", 2)[1];
+                    keyValue = checkHexKey(keyValue);
+                } else {
+                    keyValue = CryptoKeyProvider.EMPTY_KEY;
+                }
+            } catch (IOException e) {
+                logger.error("Cannot read from bootstrap.conf file at {} to 
extract encryption key", bootstrapFile.getAbsolutePath());
+                throw new IOException("Cannot read from bootstrap.conf", e);
+            }
+        } else {
+            logger.error("Cannot read from bootstrap.conf file at {} to 
extract encryption key -- file is missing or permissions are incorrect", 
bootstrapFile.getAbsolutePath());
+            throw new IOException("Cannot read from bootstrap.conf");
+        }
+
+        if (CryptoKeyProvider.EMPTY_KEY.equals(keyValue)) {
+            logger.info("No encryption key present in the bootstrap.conf file 
at {}", bootstrapFile.getAbsolutePath());
+        }
+
+        return keyValue;
+    }
+
+    private static String checkHexKey(String input) {
+        if (input == null || input.trim().isEmpty()) {
+            logger.debug("Checking the hex key value that was loaded 
determined the key is empty.");
+            return CryptoKeyProvider.EMPTY_KEY;
+        }
+        return input;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
new file mode 100644
index 0000000..bab8d7c
--- /dev/null
+++ 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
@@ -0,0 +1,68 @@
+/*
+ * 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.registry.security.crypto;
+
+/**
+ * A simple interface that wraps a key that can be used for encryption and 
decryption.
+ * This allows for more flexibility with the lifecycle of keys and how other 
classes
+ * can declare dependencies for keys, by depending on a CryptoKeyProvider that 
will provided
+ * at runtime.
+ */
+public interface CryptoKeyProvider {
+
+    /**
+     * A string literal that indicates the contents of a key are empty.
+     * Can also be used in contexts that a null key is undesirable.
+     */
+    String EMPTY_KEY = "";
+
+    /**
+     * @return The crypto key known to this CryptoKeyProvider instance in 
hexadecimal format, or
+     *         {@link #EMPTY_KEY} if the key is empty.
+     * @throws MissingCryptoKeyException if the key cannot be provided or 
determined for any reason.
+     *         If the key is known to be empty, {@link #EMPTY_KEY} will be 
returned and a
+     *         CryptoKeyMissingException will not be thrown
+     */
+    String getKey() throws MissingCryptoKeyException;
+
+    /**
+     * @return A boolean indicating if the key value held by this 
CryptoKeyProvider is empty,
+     *         such as 'null' or empty string.
+     */
+    default boolean isEmpty() {
+        String key;
+        try {
+            key = getKey();
+        } catch (MissingCryptoKeyException e) {
+            return true;
+        }
+        return EMPTY_KEY.equals(key);
+    }
+
+    /**
+     * A string representation of this CryptoKeyProvider instance.
+     * <p>
+     * <p>
+     * Note: Implementations of this interface should take care not to leak 
sensitive
+     * key material in any strings they emmit, including in the toString 
implementation.
+     *
+     * @return A string representation of this CryptoKeyProvider instance.
+     */
+    @Override
+    public String toString();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
new file mode 100644
index 0000000..dbc3752
--- /dev/null
+++ 
b/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
@@ -0,0 +1,47 @@
+/*
+ * 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.registry.security.crypto;
+
+/**
+ * An exception type used by a {@link CryptoKeyProvider} when a request for 
the key
+ * cannot be fulfilled for any reason.
+ *
+ * @see CryptoKeyProvider
+ */
+public class MissingCryptoKeyException extends Exception {
+
+    public MissingCryptoKeyException() {
+        super();
+    }
+
+    public MissingCryptoKeyException(String message) {
+        super(message);
+    }
+
+    public MissingCryptoKeyException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public MissingCryptoKeyException(Throwable cause) {
+        super(cause);
+    }
+
+    protected MissingCryptoKeyException(String message, Throwable cause, 
boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
new file mode 100644
index 0000000..0d1d5e2
--- /dev/null
+++ 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
@@ -0,0 +1,81 @@
+/*
+ * 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.registry.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class AESSensitivePropertyProviderFactoryTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(AESSensitivePropertyProviderFactoryTest.class)
+
+    private static final String KEY_HEX_128 = 
"0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+
+    @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 {
+    }
+
+    @Test
+    public void testShouldGetProviderWithKey() throws Exception {
+        // Arrange
+        SensitivePropertyProviderFactory factory = new 
AESSensitivePropertyProviderFactory(KEY_HEX_128)
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert provider.@key
+        assert provider.@cipher
+    }
+
+    @Test
+    public void testShouldGetProviderWith256BitKey() throws Exception {
+        // Arrange
+        Assume.assumeTrue("JCE unlimited strength crypto policy must be 
installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+        SensitivePropertyProviderFactory factory = new 
AESSensitivePropertyProviderFactory(KEY_HEX_256)
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert provider.@key
+        assert provider.@cipher
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
new file mode 100644
index 0000000..98fdd9b
--- /dev/null
+++ 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
@@ -0,0 +1,471 @@
+/*
+ * 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.registry.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.util.encoders.DecoderException
+import org.bouncycastle.util.encoders.Hex
+import org.junit.*
+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
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() throws Exception {
+
+    }
+
+    @After
+    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
+    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
+    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
+    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
+    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
+    void testShouldUnprotectValueWithWhitespace() 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("\t" + cipherText + "\n")]
+        }
+        plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length 
key: ${pt}") }
+
+        // Assert
+        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
+    }
+
+    @Test
+    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
+    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
+    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
+    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
+    void testShouldGetIdentifierKeyWithDifferentMaxKeyLengths() throws 
Exception {
+        // Arrange
+        def keys = getAvailableKeySizes().collectEntries { int keySize ->
+            [(keySize): getKeyOfSize(keySize)]
+        }
+        logger.info("Keys: ${keys}")
+
+        // Act
+        keys.each { int size, String key ->
+            String identifierKey = new 
AESSensitivePropertyProvider(key).getIdentifierKey()
+            logger.info("Identifier key: ${identifierKey} for size ${size}")
+
+            // Assert
+            assert identifierKey =~ /aes\/gcm\/${size}/
+        }
+    }
+
+    @Test
+    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
+    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
+    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
+    void testShouldEncryptArbitraryValues() {
+        // Arrange
+        def values = ["thisIsABadPassword", "thisIsABadSensitiveKeyPassword", 
"thisIsABadKeystorePassword", "thisIsABadKeyPassword", 
"thisIsABadTruststorePassword", "This is an encrypted banner message", 
"nififtw!"]
+
+        String key = "2C576A9585DB862F5ECBEE5B4FFFCCA1" //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
+    void testShouldDecryptPaddedValueWith256BitKey() {
+        // Arrange
+        Assume.assumeTrue("JCE unlimited strength crypto policy must be 
installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+
+        final String EXPECTED_VALUE = getKeyOfSize(256) // 
"thisIsABadKeyPassword"
+        String cipherText = 
"aYDkDKys1ENr3gp+||sTBPpMlIvHcOLTGZlfWct8r9RY8BuDlDkoaYmGJ/9m9af9tZIVzcnDwvYQAaIKxRGF7vI2yrY7Xd6x9GTDnWGiGiRXlaP458BBMMgfzH2O8"
+        String unpaddedCipherText = cipherText.replaceAll("=", "")
+
+        String key = "AAAABBBBCCCCDDDDEEEEFFFF00001111" * 2 // 
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-registry/blob/64211451/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
new file mode 100644
index 0000000..0c403cd
--- /dev/null
+++ 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
@@ -0,0 +1,121 @@
+/*
+ * 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.registry.properties
+
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+@RunWith(JUnit4.class)
+class NiFiRegistryPropertiesGroovyTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(NiFiRegistryPropertiesGroovyTest.class)
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() throws Exception {
+    }
+
+    @After
+    void tearDown() throws Exception {
+    }
+
+    @AfterClass
+    static void tearDownOnce() {
+    }
+
+    private static NiFiRegistryProperties loadFromFile(String 
propertiesFilePath) {
+        String filePath
+        try {
+            filePath = 
NiFiRegistryPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath()
+        } catch (URISyntaxException ex) {
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex)
+        }
+
+        NiFiRegistryProperties properties = new NiFiRegistryProperties()
+        FileReader reader = new FileReader(filePath)
+
+        try {
+            properties.load(reader)
+            logger.info("Loaded {} properties from {}", properties.size(), 
filePath)
+
+            return properties
+        } 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)
+        }
+    }
+
+    @Test
+    void testConstructorShouldCreateNewInstance() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryProperties NiFiRegistryProperties = new 
NiFiRegistryProperties()
+        logger.info("NiFiRegistryProperties has 
${NiFiRegistryProperties.size()} properties: 
${NiFiRegistryProperties.getPropertyKeys()}")
+
+        // Assert
+        assert NiFiRegistryProperties.size() == 0
+        assert NiFiRegistryProperties.getPropertyKeys() == [] as Set
+    }
+
+    @Test
+    void testConstructorShouldAcceptDefaultProperties() 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
+        NiFiRegistryProperties NiFiRegistryProperties = new 
NiFiRegistryProperties(rawProperties)
+        logger.info("NiFiRegistryProperties has 
${NiFiRegistryProperties.size()} properties: 
${NiFiRegistryProperties.getPropertyKeys()}")
+
+        // Assert
+        assert NiFiRegistryProperties.size() == 1
+        assert NiFiRegistryProperties.getPropertyKeys() == ["key"] as Set
+    }
+
+    @Test
+    void testShouldAllowMultipleInstances() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryProperties properties = new NiFiRegistryProperties()
+        properties.setProperty("key", "value")
+        logger.info("niFiProperties has ${properties.size()} properties: 
${properties.getPropertyKeys()}")
+        NiFiRegistryProperties emptyProperties = new NiFiRegistryProperties()
+        logger.info("emptyProperties has ${emptyProperties.size()} properties: 
${emptyProperties.getPropertyKeys()}")
+
+        // Assert
+        assert properties.size() == 1
+        assert properties.getPropertyKeys() == ["key"] as Set
+
+        assert emptyProperties.size() == 0
+        assert emptyProperties.getPropertyKeys() == [] as Set
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/64211451/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
new file mode 100644
index 0000000..58c8087
--- /dev/null
+++ 
b/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
@@ -0,0 +1,264 @@
+/*
+ * 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.registry.properties
+
+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.security.Security
+
+@RunWith(JUnit4.class)
+class NiFiRegistryPropertiesLoaderGroovyTest extends GroovyTestCase {
+
+    private static final Logger logger = 
LoggerFactory.getLogger(NiFiRegistryPropertiesLoaderGroovyTest.class)
+
+    private static final String KEYSTORE_PASSWORD_KEY = 
NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD
+    private static final String KEY_PASSWORD_KEY = 
NiFiRegistryProperties.SECURITY_KEY_PASSWD
+    private static final String TRUSTSTORE_PASSWORD_KEY = 
NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD
+
+    private static final String KEY_HEX_128 = 
"0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+    private static final String KEY_HEX = Cipher.getMaxAllowedKeyLength("AES") 
< 256 ? KEY_HEX_128 : KEY_HEX_256
+
+    private static final String PASSWORD_KEY_HEX_128 = 
"2C576A9585DB862F5ECBEE5B4FFFCCA1"
+
+    @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
+        NiFiRegistryPropertiesLoader.@sensitivePropertyProviderFactory = null
+    }
+
+    @AfterClass
+    public static void tearDownOnce() {
+    }
+
+    @Test
+    public void testConstructorShouldCreateNewInstance() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryPropertiesLoader propertiesLoader = new 
NiFiRegistryPropertiesLoader()
+
+        // Assert
+        assert !propertiesLoader.@keyHex
+    }
+
+    @Test
+    public void testShouldCreateInstanceWithKey() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryPropertiesLoader propertiesLoader = 
NiFiRegistryPropertiesLoader.withKey(KEY_HEX)
+
+        // Assert
+        assert propertiesLoader.@keyHex == KEY_HEX
+    }
+
+    @Test
+    public void testConstructorShouldCreateMultipleInstances() throws 
Exception {
+        // Arrange
+        NiFiRegistryPropertiesLoader propertiesLoader1 = 
NiFiRegistryPropertiesLoader.withKey(KEY_HEX)
+
+        // Act
+        NiFiRegistryPropertiesLoader propertiesLoader2 = new 
NiFiRegistryPropertiesLoader()
+
+        // Assert
+        assert propertiesLoader1.@keyHex == KEY_HEX
+        assert !propertiesLoader2.@keyHex
+    }
+
+    @Test
+    public void testShouldGetDefaultProviderKey() throws Exception {
+        // Arrange
+        final String expectedProviderKey = 
"aes/gcm/${Cipher.getMaxAllowedKeyLength("AES") > 128 ? 256 : 128}"
+        logger.info("Expected provider key: ${expectedProviderKey}")
+
+        // Act
+        String defaultKey = 
NiFiRegistryPropertiesLoader.getDefaultProviderKey()
+        logger.info("Default key: ${defaultKey}")
+        // Assert
+        assert defaultKey == expectedProviderKey
+    }
+
+    @Test
+    public void testShouldInitializeSensitivePropertyProviderFactory() throws 
Exception {
+        // Arrange
+        NiFiRegistryPropertiesLoader propertiesLoader = new 
NiFiRegistryPropertiesLoader()
+
+        // Act
+        propertiesLoader.initializeSensitivePropertyProviderFactory()
+
+        // Assert
+        assert propertiesLoader.@sensitivePropertyProviderFactory
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromFile() throws Exception 
{
+        // Arrange
+        File unprotectedFile = new 
File("src/test/resources/conf/nifi-registry.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = new 
NiFiRegistryPropertiesLoader()
+
+        // Act
+        NiFiRegistryProperties properties = 
propertiesLoader.load(unprotectedFile)
+
+        // Assert
+        assert properties.size() > 0
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert properties instanceof NiFiRegistryProperties
+    }
+
+    @Test
+    public void testShouldNotLoadUnprotectedPropertiesFromNullFile() throws 
Exception {
+        // Arrange
+        NiFiRegistryPropertiesLoader propertiesLoader = new 
NiFiRegistryPropertiesLoader()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            NiFiRegistryProperties properties = propertiesLoader.load(null as 
File)
+        }
+        logger.info(msg)
+
+        // Assert
+        assert msg == "NiFi Registry properties file missing or unreadable"
+    }
+
+    @Test
+    public void testShouldNotLoadUnprotectedPropertiesFromMissingFile() throws 
Exception {
+        // Arrange
+        File missingFile = new 
File("src/test/resources/conf/nifi-registry.missing.properties")
+        assert !missingFile.exists()
+
+        NiFiRegistryPropertiesLoader propertiesLoader = new 
NiFiRegistryPropertiesLoader()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            NiFiRegistryProperties properties = 
propertiesLoader.load(missingFile)
+        }
+        logger.info(msg)
+
+        // Assert
+        assert msg == "NiFi Registry properties file missing or unreadable"
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromPath() throws Exception 
{
+        // Arrange
+        File unprotectedFile = new 
File("src/test/resources/conf/nifi-registry.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = new 
NiFiRegistryPropertiesLoader()
+
+        // Act
+        NiFiRegistryProperties properties = 
propertiesLoader.load(unprotectedFile.path)
+
+        // Assert
+        assert properties.size() > 0
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert properties instanceof NiFiRegistryProperties
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromProtectedFile() throws 
Exception {
+        // Arrange
+        File protectedFile = new 
File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = 
NiFiRegistryPropertiesLoader.withKey(KEY_HEX_128)
+
+        final def EXPECTED_PLAIN_VALUES = [
+                (KEYSTORE_PASSWORD_KEY): "thisIsABadPassword",
+                (KEY_PASSWORD_KEY): "thisIsABadPassword",
+        ]
+
+        // This method is covered in tests above, so safe to use here to 
retrieve protected properties
+        ProtectedNiFiRegistryProperties protectedNiFiProperties = 
propertiesLoader.readProtectedPropertiesFromDisk(protectedFile)
+        int totalKeysCount = 
protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes().size()
+        int protectedKeysCount = 
protectedNiFiProperties.getProtectedPropertyKeys().size()
+        logger.info("Read ${totalKeysCount} total properties 
(${protectedKeysCount} protected) from ${protectedFile.canonicalPath}")
+
+        // Act
+        NiFiRegistryProperties properties = 
propertiesLoader.load(protectedFile)
+
+        // Assert
+        assert properties.size() == totalKeysCount - protectedKeysCount
+
+        // Ensure that any key marked as protected above is different in this 
instance
+        protectedNiFiProperties.getProtectedPropertyKeys().keySet().each { 
String key ->
+            String plainValue = properties.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 properties instanceof NiFiRegistryProperties
+    }
+
+    @Test
+    public void testShouldUpdateKeyInFactory() throws Exception {
+        // Arrange
+        File originalKeyFile = new 
File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties")
+        File passwordKeyFile = new 
File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = 
NiFiRegistryPropertiesLoader.withKey(KEY_HEX_128)
+
+        NiFiRegistryProperties properties = 
propertiesLoader.load(originalKeyFile)
+        logger.info("Read ${properties.size()} total properties from 
${originalKeyFile.canonicalPath}")
+
+        // Act
+        NiFiRegistryPropertiesLoader passwordNiFiRegistryPropertiesLoader = 
NiFiRegistryPropertiesLoader.withKey(PASSWORD_KEY_HEX_128)
+
+        NiFiRegistryProperties passwordProperties = 
passwordNiFiRegistryPropertiesLoader.load(passwordKeyFile)
+        logger.info("Read ${passwordProperties.size()} total properties from 
${passwordKeyFile.canonicalPath}")
+
+        // Assert
+        assert properties.size() == passwordProperties.size()
+
+
+        def readPropertiesAndValues = 
properties.getPropertyKeys().collectEntries {
+            [(it): properties.getProperty(it)]
+        }
+        def readPasswordPropertiesAndValues = 
passwordProperties.getPropertyKeys().collectEntries {
+            [(it): passwordProperties.getProperty(it)]
+        }
+
+        assert readPropertiesAndValues == readPasswordPropertiesAndValues
+    }
+}

Reply via email to