nifi-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From bbe...@apache.org
Subject [3/4] nifi-registry git commit: NIFIREG-61 Add support for encrypted config files
Date Tue, 26 Dec 2017 19:57:30 GMT
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
+    }
+}


Mime
View raw message