http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy
deleted file mode 100644
index c154a1f..0000000
--- 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/scrypt/ScryptGroovyTest.groovy
+++ /dev/null
@@ -1,399 +0,0 @@
-/*
- * 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.processors.standard.util.crypto.scrypt
-
-import org.apache.commons.codec.binary.Hex
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.junit.After
-import org.junit.Assume
-import org.junit.Before
-import org.junit.BeforeClass
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-
-import java.security.SecureRandom
-import java.security.Security
-
-import static groovy.test.GroovyAssert.shouldFail
-
-@RunWith(JUnit4.class)
-public class ScryptGroovyTest {
-    private static final Logger logger = 
LoggerFactory.getLogger(ScryptGroovyTest.class)
-
-    private static final String PASSWORD = "shortPassword"
-    private static final String SALT_HEX = "0123456789ABCDEFFEDCBA9876543210"
-    private static final byte[] SALT_BYTES = Hex.decodeHex(SALT_HEX as char[])
-
-    // Small values to test for correctness, not timing
-    private static final int N = 2**4
-    private static final int R = 1
-    private static final int P = 1
-    private static final int DK_LEN = 128
-    private static final long TWO_GIGABYTES = 2048L * 1024 * 1024
-
-    @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 testDeriveScryptKeyShouldBeInternallyConsistent() throws 
Exception {
-        // Arrange
-        def allKeys = []
-        final int RUNS = 10
-
-        logger.info("Running with '${PASSWORD}', '${SALT_HEX}', $N, $R, $P, 
$DK_LEN")
-
-        // Act
-        RUNS.times {
-            byte[] keyBytes = Scrypt.deriveScryptKey(PASSWORD.bytes, 
SALT_BYTES, N, R, P, DK_LEN)
-            logger.info("Derived key: ${Hex.encodeHexString(keyBytes)}")
-            allKeys << keyBytes
-        }
-
-        // Assert
-        assert allKeys.size() == RUNS
-        assert allKeys.every { it == allKeys.first() }
-    }
-
-    /**
-     * This test ensures that the local implementation of Scrypt is compatible 
with the reference implementation from the Colin Percival paper.
-     */
-    @Test
-    public void testDeriveScryptKeyShouldMatchTestVectors() {
-        // Arrange
-
-        // These values are taken from Colin Percival's scrypt paper: 
https://www.tarsnap.com/scrypt/scrypt.pdf
-        final byte[] HASH_2 = Hex.decodeHex("fdbabe1c9d3472007856e7190d01e9fe" 
+
-                "7c6ad7cbc8237830e77376634b373162" +
-                "2eaf30d92e22a3886ff109279d9830da" +
-                "c727afb94a83ee6d8360cbdfa2cc0640" as char[])
-
-        final byte[] HASH_3 = Hex.decodeHex("7023bdcb3afd7348461c06cd81fd38eb" 
+
-                "fda8fbba904f8e3ea9b543f6545da1f2" +
-                "d5432955613f0fcf62d49705242a9af9" +
-                "e61e85dc0d651e40dfcf017b45575887" as char[])
-
-        final def TEST_VECTORS = [
-                // Empty password is not supported by JCE
-                [password: "password",
-                 salt    : "NaCl",
-                 n       : 1024,
-                 r       : 8,
-                 p       : 16,
-                 dkLen   : 64 * 8,
-                 hash    : HASH_2],
-                [password: "pleaseletmein",
-                 salt    : "SodiumChloride",
-                 n       : 16384,
-                 r       : 8,
-                 p       : 1,
-                 dkLen   : 64 * 8,
-                 hash    : HASH_3],
-        ]
-
-        // Act
-        TEST_VECTORS.each { Map params ->
-            logger.info("Running with '${params.password}', '${params.salt}', 
${params.n}, ${params.r}, ${params.p}, ${params.dkLen}")
-            long memoryInBytes = Scrypt.calculateExpectedMemory(params.n, 
params.r, params.p)
-            logger.info("Expected memory usage: (128 * r * N + 128 * r * p) 
${memoryInBytes} bytes")
-            logger.info(" Expected ${Hex.encodeHexString(params.hash)}")
-
-            byte[] calculatedHash = 
Scrypt.deriveScryptKey(params.password.bytes, params.salt.bytes, params.n, 
params.r, params.p, params.dkLen)
-            logger.info("Generated ${Hex.encodeHexString(calculatedHash)}")
-
-            // Assert
-            assert calculatedHash == params.hash
-        }
-    }
-
-    /**
-     * This test ensures that the local implementation of Scrypt is compatible 
with the reference implementation from the Colin Percival paper. The test 
vector requires ~1GB {@code byte[]}
-     * and therefore the Java heap must be at least 1GB. Because {@link 
nifi/pom.xml} has a {@code surefire} rule which appends {@code -Xmx1G}
-     * to the Java options, this overrides any IDE options. To ensure the heap 
is properly set, using the {@code groovyUnitTest} profile will re-append {@code 
-Xmx3072m} to the Java options.
-     */
-    @Test
-    public void testDeriveScryptKeyShouldMatchExpensiveTestVector() {
-        // Arrange
-        long totalMemory = Runtime.getRuntime().totalMemory()
-        logger.info("Required memory: ${TWO_GIGABYTES} bytes")
-        logger.info("Max heap memory: ${totalMemory} bytes")
-        Assume.assumeTrue("Test is being skipped due to JVM heap size. Please 
run with -Xmx3072m to set sufficient heap size",
-                totalMemory >= TWO_GIGABYTES)
-
-        // These values are taken from Colin Percival's scrypt paper: 
https://www.tarsnap.com/scrypt/scrypt.pdf
-        final byte[] HASH = Hex.decodeHex("2101cb9b6a511aaeaddbbe09cf70f881" +
-                "ec568d574a2ffd4dabe5ee9820adaa47" +
-                "8e56fd8f4ba5d09ffa1c6d927c40f4c3" +
-                "37304049e8a952fbcbf45c6fa77a41a4" as char[])
-
-        // This test vector requires 2GB heap space and approximately 10 
seconds on a consumer machine
-        String password = "pleaseletmein"
-        String salt = "SodiumChloride"
-        int n = 1048576
-        int r = 8
-        int p = 1
-        int dkLen = 64 * 8
-
-        // Act
-        logger.info("Running with '${password}', '${salt}', ${n}, ${r}, ${p}, 
${dkLen}")
-        long memoryInBytes = Scrypt.calculateExpectedMemory(n, r, p)
-        logger.info("Expected memory usage: (128 * r * N + 128 * r * p) 
${memoryInBytes} bytes")
-        logger.info(" Expected ${Hex.encodeHexString(HASH)}")
-
-        byte[] calculatedHash = Scrypt.deriveScryptKey(password.bytes, 
salt.bytes, n, r, p, dkLen)
-        logger.info("Generated ${Hex.encodeHexString(calculatedHash)}")
-
-        // Assert
-        assert calculatedHash == HASH
-    }
-
-    @Ignore("This test was just to exercise the heap and debug OOME issues")
-    @Test
-    void testShouldCauseOutOfMemoryError() {
-        SecureRandom secureRandom = new SecureRandom()
-//        int i = 29
-        (10..31).each { int i ->
-            int length = 2**i
-            byte[] bytes = new byte[length]
-            secureRandom.nextBytes(bytes)
-            logger.info("Successfully ran with byte[] of length ${length}")
-            logger.info("${Hex.encodeHexString(bytes[0..<16] as byte[])}...")
-        }
-    }
-
-    @Test
-    public void testDeriveScryptKeyShouldSupportExternalCompatibility() {
-        // Arrange
-
-        // These values can be generated by running `$ ./openssl_scrypt.rb` in 
the terminal
-        final String EXPECTED_KEY_HEX = "a8efbc0a709d3f89b6bb35b05fc8edf5"
-        String password = "thisIsABadPassword"
-        String saltHex = "f5b8056ea6e66edb8d013ac432aba24a"
-        int n = 1024
-        int r = 8
-        int p = 36
-        int dkLen = 16 * 8
-
-        // Act
-        logger.info("Running with '${password}', ${saltHex}, ${n}, ${r}, ${p}, 
${dkLen}")
-        long memoryInBytes = Scrypt.calculateExpectedMemory(n, r, p)
-        logger.info("Expected memory usage: (128 * r * N + 128 * r * p) 
${memoryInBytes} bytes")
-        logger.info(" Expected ${EXPECTED_KEY_HEX}")
-
-        byte[] calculatedHash = Scrypt.deriveScryptKey(password.bytes, 
Hex.decodeHex(saltHex as char[]), n, r, p, dkLen)
-        logger.info("Generated ${Hex.encodeHexString(calculatedHash)}")
-
-        // Assert
-        assert calculatedHash == Hex.decodeHex(EXPECTED_KEY_HEX as char[])
-    }
-
-    @Test
-    public void testScryptShouldBeInternallyConsistent() throws Exception {
-        // Arrange
-        def allHashes = []
-        final int RUNS = 10
-
-        logger.info("Running with '${PASSWORD}', '${SALT_HEX}', $N, $R, $P")
-
-        // Act
-        RUNS.times {
-            String hash = Scrypt.scrypt(PASSWORD, SALT_BYTES, N, R, P, DK_LEN)
-            logger.info("Hash: ${hash}")
-            allHashes << hash
-        }
-
-        // Assert
-        assert allHashes.size() == RUNS
-        assert allHashes.every { it == allHashes.first() }
-    }
-
-    @Test
-    public void testScryptShouldGenerateValidSaltIfMissing() {
-        // Arrange
-
-        // The generated salt should be byte[16], encoded as 22 Base64 chars
-        final def EXPECTED_SALT_PATTERN = /\$.+\$[0-9a-zA-Z\/\+]{22}\$.+/
-
-        // Act
-        String calculatedHash = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
-        logger.info("Generated ${calculatedHash}")
-
-        // Assert
-        assert calculatedHash =~ EXPECTED_SALT_PATTERN
-    }
-
-    @Test
-    public void testScryptShouldNotAcceptInvalidN() throws Exception {
-        // Arrange
-
-        final int MAX_N = Integer.MAX_VALUE / 128 / R - 1
-
-        // N must be a power of 2 > 1 and < Integer.MAX_VALUE / 128 / r
-        final def INVALID_NS = [-2, 0, 1, 3, 4096 - 1, MAX_N + 1]
-
-        // Act
-        INVALID_NS.each { int invalidN ->
-            logger.info("Using N: ${invalidN}")
-
-            def msg = shouldFail(IllegalArgumentException) {
-                Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, invalidN, 
R, P, DK_LEN)
-            }
-
-            // Assert
-            assert msg =~ "N must be a power of 2 greater than 1|Parameter N 
is too large"
-        }
-    }
-
-    @Test
-    public void testScryptShouldAcceptValidR() throws Exception {
-        // Arrange
-
-        // Use a large p value to allow r to exceed MAX_R without normal N 
exceeding MAX_N
-        int largeP = 2**10
-        final int MAX_R = Math.ceil(Integer.MAX_VALUE / 128 / largeP) - 1
-
-        // r must be in (0..Integer.MAX_VALUE / 128 / p)
-        final def INVALID_RS = [0, MAX_R + 1]
-
-        // Act
-        INVALID_RS.each { int invalidR ->
-            logger.info("Using r: ${invalidR}")
-
-            def msg = shouldFail(IllegalArgumentException) {
-                byte[] hash = Scrypt.deriveScryptKey(PASSWORD.bytes, 
SALT_BYTES, N, invalidR, largeP, DK_LEN)
-                logger.info("Generated hash: ${Hex.encodeHexString(hash)}")
-            }
-
-            // Assert
-            assert msg =~ "Parameter r must be 1 or greater|Parameter r is too 
large"
-        }
-    }
-
-    @Test
-    public void testScryptShouldNotAcceptInvalidP() throws Exception {
-        // Arrange
-        final int MAX_P = Math.ceil(Integer.MAX_VALUE / 128) - 1
-
-        // p must be in (0..Integer.MAX_VALUE / 128)
-        final def INVALID_PS = [0, MAX_P + 1]
-
-        // Act
-        INVALID_PS.each { int invalidP ->
-            logger.info("Using p: ${invalidP}")
-
-            def msg = shouldFail(IllegalArgumentException) {
-                byte[] hash = Scrypt.deriveScryptKey(PASSWORD.bytes, 
SALT_BYTES, N, R, invalidP, DK_LEN)
-                logger.info("Generated hash: ${Hex.encodeHexString(hash)}")
-            }
-
-            // Assert
-            assert msg =~ "Parameter p must be 1 or greater|Parameter p is too 
large"
-        }
-    }
-
-    @Test
-    public void testCheckShouldValidateCorrectPassword() throws Exception {
-        // Arrange
-        final String PASSWORD = "thisIsABadPassword"
-        final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
-        logger.info("Password: ${PASSWORD} -> Hash: ${EXPECTED_HASH}")
-
-        // Act
-        boolean matches = Scrypt.check(PASSWORD, EXPECTED_HASH)
-        logger.info("Check matches: ${matches}")
-
-        // Assert
-        assert matches
-    }
-
-    @Test
-    public void testCheckShouldNotValidateIncorrectPassword() throws Exception 
{
-        // Arrange
-        final String PASSWORD = "thisIsABadPassword"
-        final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
-        logger.info("Password: ${PASSWORD} -> Hash: ${EXPECTED_HASH}")
-
-        // Act
-        boolean matches = Scrypt.check(PASSWORD.reverse(), EXPECTED_HASH)
-        logger.info("Check matches: ${matches}")
-
-        // Assert
-        assert !matches
-    }
-
-    @Test
-    public void testCheckShouldNotAcceptInvalidPassword() throws Exception {
-        // Arrange
-        final String HASH = 
'$s0$a0801$abcdefghijklmnopqrstuv$abcdefghijklmnopqrstuv'
-
-        // Even though the spec allows for empty passwords, the JCE does not, 
so extend enforcement of that to the user boundary
-        final def INVALID_PASSWORDS = ['', null]
-
-        // Act
-        INVALID_PASSWORDS.each { String invalidPassword ->
-            logger.info("Using password: ${invalidPassword}")
-
-            def msg = shouldFail(IllegalArgumentException) {
-                boolean matches = Scrypt.check(invalidPassword, HASH)
-            }
-            logger.expected(msg)
-
-            // Assert
-            assert msg =~ "Password cannot be empty"
-        }
-    }
-
-    @Test
-    public void testCheckShouldNotAcceptInvalidHash() throws Exception {
-        // Arrange
-        final String PASSWORD = "thisIsABadPassword"
-
-        // Even though the spec allows for empty salts, the JCE does not, so 
extend enforcement of that to the user boundary
-        final def INVALID_HASHES = ['', null, '$s0$a0801$', 
'$s0$a0801$abcdefghijklmnopqrstuv$']
-
-        // Act
-        INVALID_HASHES.each { String invalidHash ->
-            logger.info("Using hash: ${invalidHash}")
-
-            def msg = shouldFail(IllegalArgumentException) {
-                boolean matches = Scrypt.check(PASSWORD, invalidHash)
-            }
-            logger.expected(msg)
-
-            // Assert
-            assert msg =~ "Hash cannot be empty|Hash is not properly formatted"
-        }
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..e5e001f
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/BcryptCipherProviderGroovyTest.groovy
@@ -0,0 +1,538 @@
+/*
+ * 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.security.util.crypto
+
+import org.apache.commons.codec.binary.Base64
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.security.util.EncryptionMethod
+import org.apache.nifi.security.util.crypto.bcrypt.BCrypt
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assert.assertTrue
+
+@RunWith(JUnit4.class)
+public class BcryptCipherProviderGroovyTest {
+    private static final Logger logger = 
LoggerFactory.getLogger(BcryptCipherProviderGroovyTest.class);
+
+    private static List<EncryptionMethod> strongKDFEncryptionMethods
+
+    private static final int DEFAULT_KEY_LENGTH = 128;
+    public static final String MICROBENCHMARK = "microbenchmark"
+    private static ArrayList<Integer> AES_KEY_LENGTHS
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider());
+
+        strongKDFEncryptionMethods = EncryptionMethod.values().findAll { 
it.isCompatibleWithStrongKDFs() }
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+
+        if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
+            AES_KEY_LENGTHS = [128, 192, 256]
+        } else {
+            AES_KEY_LENGTHS = [128]
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+
+    }
+
+    @Test
+    public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt()
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : strongKDFEncryptionMethods) {
+            logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, 
DEFAULT_KEY_LENGTH, true);
+            byte[] iv = cipher.getIV();
+            logger.info("IV: ${Hex.encodeHexString(iv)}")
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} 
${cipherBytes.length}");
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, 
DEFAULT_KEY_LENGTH, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherWithExternalIVShouldBeInternallyConsistent() 
throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt()
+        final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : strongKDFEncryptionMethods) {
+            logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, 
DEFAULT_KEY_LENGTH, true);
+            logger.info("IV: ${Hex.encodeHexString(IV)}")
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} 
${cipherBytes.length}");
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, 
DEFAULT_KEY_LENGTH, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void 
testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws 
Exception {
+        // Arrange
+        Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE 
Unlimited Strength Jurisdiction Policy file.",
+                PasswordBasedEncryptor.supportsUnlimitedStrength());
+
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt()
+
+        final int LONG_KEY_LENGTH = 256
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : strongKDFEncryptionMethods) {
+            logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, 
LONG_KEY_LENGTH, true);
+            byte[] iv = cipher.getIV();
+            logger.info("IV: ${Hex.encodeHexString(iv)}")
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} 
${cipherBytes.length}");
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, 
LONG_KEY_LENGTH, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testHashPWShouldMatchTestVectors() {
+        // Arrange
+        final String PASSWORD = 'abcdefghijklmnopqrstuvwxyz'
+        final String SALT = '$2a$10$fVH8e28OQRj9tqiDXs1e1u'
+        final String EXPECTED_HASH = 
'$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'
+//        final int WORK_FACTOR = 10
+
+        // Act
+        String calculatedHash = BCrypt.hashpw(PASSWORD, SALT)
+        logger.info("Generated ${calculatedHash}")
+
+        // Assert
+        assert calculatedHash == EXPECTED_HASH
+    }
+
+    @Test
+    public void testGetCipherShouldSupportExternalCompatibility() throws 
Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PLAINTEXT = "This is a plaintext message.";
+        final String PASSWORD = "thisIsABadPassword";
+
+        // These values can be generated by running `$ ./openssl_bcrypt` in 
the terminal
+        final byte[] SALT = Hex.decodeHex("81455b915ce9efd1fc61a08eb0255936" 
as char[]);
+        final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as 
char[]);
+
+        // $v2$w2$base64_salt_22__base64_hash_31
+        final String FULL_HASH = 
"\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
+        logger.info("Full Hash: ${FULL_HASH}")
+        final String HASH = FULL_HASH[-31..-1]
+        logger.info("     Hash: ${HASH.padLeft(60, " ")}")
+        logger.info(" B64 Salt: 
${CipherUtility.encodeBase64NoPadding(SALT).padLeft(29, " ")}")
+
+        String extractedSalt = FULL_HASH[7..<29]
+        logger.info("Extracted Salt:   ${extractedSalt}")
+        String extractedSaltHex = 
Hex.encodeHexString(Base64.decodeBase64(extractedSalt))
+        logger.info("Extracted Salt (hex): ${extractedSaltHex}")
+        logger.info(" Expected Salt (hex): ${Hex.encodeHexString(SALT)}")
+
+        final String CIPHER_TEXT = 
"3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15"
+        byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+        logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} 
${cipherBytes.length}");
+
+        // Sanity check
+        Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, 
"BC")
+        def rubyKey = new 
SecretKeySpec(Hex.decodeHex("724cd9e1b0b9e87c7f7e7d7b270bca07" as char[]), 
"AES")
+        def ivSpec = new IvParameterSpec(IV)
+        rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec)
+        byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.bytes)
+        logger.info("Expected cipher text: 
${Hex.encodeHexString(rubyCipherBytes)}")
+        rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec)
+        assert rubyCipher.doFinal(rubyCipherBytes) == PLAINTEXT.bytes
+        assert rubyCipher.doFinal(cipherBytes) == PLAINTEXT.bytes
+        logger.sanity("Decrypted external cipher text and generated cipher 
text successfully")
+
+        // Sanity for hash generation
+        final String FULL_SALT = FULL_HASH[0..<29]
+        logger.sanity("Salt from external: ${FULL_SALT}")
+        String generatedHash = BCrypt.hashpw(PASSWORD, FULL_SALT)
+        logger.sanity("Generated hash: ${generatedHash}")
+        assert generatedHash == FULL_HASH
+
+        // Act
+        Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, 
FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
+        byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+        String recovered = new String(recoveredBytes, "UTF-8");
+        logger.info("Recovered: ${recovered}")
+
+        // Assert
+        assert PLAINTEXT.equals(recovered);
+    }
+
+    @Test
+    public void testGetCipherShouldHandleFullSalt() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PLAINTEXT = "This is a plaintext message.";
+        final String PASSWORD = "thisIsABadPassword";
+
+        // These values can be generated by running `$ ./openssl_bcrypt.rb` in 
the terminal
+        final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as 
char[]);
+
+        // $v2$w2$base64_salt_22__base64_hash_31
+        final String FULL_HASH = 
"\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
+        logger.info("Full Hash: ${FULL_HASH}")
+        final String FULL_SALT = FULL_HASH[0..<29]
+        logger.info("     Salt: ${FULL_SALT}")
+        final String HASH = FULL_HASH[-31..-1]
+        logger.info("     Hash: ${HASH.padLeft(60, " ")}")
+
+        String extractedSalt = FULL_HASH[7..<29]
+        logger.info("Extracted Salt:   ${extractedSalt}")
+        String extractedSaltHex = 
Hex.encodeHexString(Base64.decodeBase64(extractedSalt))
+        logger.info("Extracted Salt (hex): ${extractedSaltHex}")
+
+        final String CIPHER_TEXT = 
"3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15"
+        byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+        logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} 
${cipherBytes.length}");
+
+        // Act
+        Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, 
FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
+        byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+        String recovered = new String(recoveredBytes, "UTF-8");
+        logger.info("Recovered: ${recovered}")
+
+        // Assert
+        assert PLAINTEXT.equals(recovered);
+    }
+
+    @Test
+    public void testGetCipherShouldHandleUnformedSalt() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PASSWORD = "thisIsABadPassword";
+
+        final def INVALID_SALTS = ['$ab$00$acbdefghijklmnopqrstuv', 
'bad_salt', '$3a$11$', 'x', '$2a$10$']
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+        // Act
+        INVALID_SALTS.each { String salt ->
+            logger.info("Checking salt ${salt}")
+
+            def msg = shouldFail(IllegalArgumentException) {
+                Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
+            }
+
+            // Assert
+            assert msg =~ "The salt must be of the format 
\\\$2a\\\$10\\\$gUVbkVzp79H8YaCOsCVZNu\\. To generate a salt, use 
BcryptCipherProvider#generateSalt"
+        }
+    }
+
+    String bytesToBitString(byte[] bytes) {
+        bytes.collect {
+            String.format("%8s", Integer.toBinaryString(it & 0xFF)).replace(' 
', '0')
+        }.join("")
+    }
+
+    String spaceString(String input, int blockSize = 4) {
+        input.collect { it.padLeft(blockSize, " ") }.join("")
+    }
+
+    @Test
+    public void testGetCipherShouldRejectEmptySalt() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PASSWORD = "thisIsABadPassword";
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+        // Two different errors -- one explaining the no-salt method is not 
supported, and the other for an empty byte[] passed
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true);
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "The salt cannot be empty\\. To generate a salt, use 
BcryptCipherProvider#generateSalt"
+    }
+
+    @Test
+    public void testGetCipherForDecryptShouldRequireIV() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt()
+        final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : strongKDFEncryptionMethods) {
+            logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, 
DEFAULT_KEY_LENGTH, true);
+            logger.info("IV: ${Hex.encodeHexString(IV)}")
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} 
${cipherBytes.length}");
+
+            def msg = shouldFail(IllegalArgumentException) {
+                cipher = cipherProvider.getCipher(em, PASSWORD, SALT, 
DEFAULT_KEY_LENGTH, false);
+            }
+
+            // Assert
+            assert msg =~ "Cannot decrypt without a valid IV"
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt()
+        final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
+
+        final String PLAINTEXT = "This is a plaintext message.";
+
+        // Currently only AES ciphers are compatible with Bcrypt, so redundant 
to test all algorithms
+        final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+        // Act
+        VALID_KEY_LENGTHS.each { int keyLength ->
+            logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} 
with key length ${keyLength}")
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, IV, keyLength, true);
+            logger.info("IV: ${Hex.encodeHexString(IV)}")
+
+            byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
+            logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} 
${cipherBytes.length}");
+
+            cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, 
SALT, IV, keyLength, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert PLAINTEXT.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws 
Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = cipherProvider.generateSalt()
+        final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
+
+        final String PLAINTEXT = "This is a plaintext message.";
+
+        // Currently only AES ciphers are compatible with Bcrypt, so redundant 
to test all algorithms
+        final def INVALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+        // Act
+        INVALID_KEY_LENGTHS.each { int keyLength ->
+            logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} 
with key length ${keyLength}")
+
+            // Initialize a cipher for encryption
+            def msg = shouldFail(IllegalArgumentException) {
+                Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, IV, keyLength, true);
+            }
+
+            // Assert
+            assert msg =~ "${keyLength} is not a valid key length for AES"
+        }
+    }
+
+    @Test
+    public void testGenerateSaltShouldUseProvidedWorkFactor() throws Exception 
{
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new 
BcryptCipherProvider(11);
+        int workFactor = cipherProvider.getWorkFactor()
+
+        // Act
+        final byte[] saltBytes = cipherProvider.generateSalt()
+        String salt = new String(saltBytes)
+        logger.info("Salt: ${salt}")
+
+        // Assert
+        assert salt =~ /^\$2[axy]\$\d{2}\$/
+        assert salt.contains("\$${workFactor}\$")
+    }
+
+    @Ignore("This test can be run on a specific machine to evaluate if the 
default work factor is sufficient")
+    @Test
+    public void testDefaultConstructorShouldProvideStrongWorkFactor() {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider();
+
+        // Values taken from 
http://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/ and 
http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
+
+        // Calculate the work factor to reach 500 ms
+        int minimumWorkFactor = calculateMinimumWorkFactor()
+        logger.info("Determined minimum safe work factor to be 
${minimumWorkFactor}")
+
+        // Act
+        int workFactor = cipherProvider.getWorkFactor()
+        logger.info("Default work factor ${workFactor}")
+
+        // Assert
+        assertTrue("The default work factor for BcryptCipherProvider is too 
weak. Please update the default value to a stronger level.", workFactor >= 
minimumWorkFactor)
+    }
+
+    /**
+     * Returns the work factor required for a derivation to exceed 500 ms on 
this machine. Code adapted from 
http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
+     *
+     * @return the minimum bcrypt work factor
+     */
+    private static int calculateMinimumWorkFactor() {
+        // High start-up cost, so run multiple times for better benchmarking
+        final int RUNS = 10
+
+        // Benchmark using a work factor of 5 (the second-lowest allowed)
+        int workFactor = 5
+
+        String salt = BCrypt.gensalt(workFactor)
+
+        // Run once to prime the system
+        double duration = time {
+            BCrypt.hashpw(MICROBENCHMARK, salt)
+        }
+        logger.info("First run of work factor ${workFactor} took ${duration} 
ms (ignored)")
+
+        def durations = []
+
+        RUNS.times { int i ->
+            duration = time {
+                BCrypt.hashpw(MICROBENCHMARK, salt)
+            }
+            logger.info("Work factor ${workFactor} took ${duration} ms")
+            durations << duration
+        }
+
+        duration = durations.sum() / durations.size()
+        logger.info("Work factor ${workFactor} averaged ${duration} ms")
+
+        // Increasing the work factor by 1 would double the run time
+        // Keep increasing N until the estimated duration is over 500 ms
+        while (duration < 500) {
+            workFactor += 1
+            duration *= 2
+        }
+
+        logger.info("Returning work factor ${workFactor} for ${duration} ms")
+
+        return workFactor
+    }
+
+    private static double time(Closure c) {
+        long start = System.nanoTime()
+        c.call()
+        long end = System.nanoTime()
+        return (end - start) / 1_000_000.0
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy
new file mode 100644
index 0000000..28da9d1
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/CipherProviderFactoryGroovyTest.groovy
@@ -0,0 +1,97 @@
+/*
+ * 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.security.util.crypto
+
+import org.apache.nifi.security.util.KeyDerivationFunction
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class CipherProviderFactoryGroovyTest extends GroovyTestCase {
+    private static final Logger logger = 
LoggerFactory.getLogger(CipherProviderFactoryGroovyTest.class)
+
+    private static final Map<KeyDerivationFunction, Class> 
EXPECTED_CIPHER_PROVIDERS = [
+            (KeyDerivationFunction.BCRYPT)                  : 
BcryptCipherProvider.class,
+            (KeyDerivationFunction.NIFI_LEGACY)             : 
NiFiLegacyCipherProvider.class,
+            (KeyDerivationFunction.NONE)                    : 
AESKeyedCipherProvider.class,
+            (KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): 
OpenSSLPKCS5CipherProvider.class,
+            (KeyDerivationFunction.PBKDF2)                  : 
PBKDF2CipherProvider.class,
+            (KeyDerivationFunction.SCRYPT)                  : 
ScryptCipherProvider.class
+    ]
+
+    @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 testGetCipherProviderShouldResolveRegisteredKDFs() {
+        // Arrange
+
+        // Act
+        KeyDerivationFunction.values().each { KeyDerivationFunction kdf ->
+            logger.info("Expected: ${kdf.name} -> 
${EXPECTED_CIPHER_PROVIDERS.get(kdf).simpleName}")
+            CipherProvider cp = CipherProviderFactory.getCipherProvider(kdf)
+            logger.info("Resolved: ${kdf.name} -> ${cp.class.simpleName}")
+
+            // Assert
+            assert cp.class == (EXPECTED_CIPHER_PROVIDERS.get(kdf))
+        }
+    }
+
+    @Ignore("Cannot mock enum using Groovy map coercion")
+    @Test
+    public void testGetCipherProviderShouldHandleUnregisteredKDFs() {
+        // Arrange
+
+        // Can't mock this; see 
http://stackoverflow.com/questions/5323505/mocking-java-enum-to-add-a-value-to-test-fail-case
+        KeyDerivationFunction invalidKDF = [name: "Unregistered", description: 
"Not a registered KDF"] as KeyDerivationFunction
+        logger.info("Expected: ${invalidKDF.name} -> error")
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            CipherProvider cp = 
CipherProviderFactory.getCipherProvider(invalidKDF)
+            logger.info("Resolved: ${invalidKDF.name} -> 
${cp.class.simpleName}")
+        }
+        logger.expected(msg)
+
+        // Assert
+        assert msg =~ "No cipher provider registered for ${invalidKDF.name}"
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy
new file mode 100644
index 0000000..0487af3
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/KeyedEncryptorGroovyTest.groovy
@@ -0,0 +1,122 @@
+/*
+ * 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.security.util.crypto
+
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.processor.io.StreamCallback
+import org.apache.nifi.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.SecretKey
+import javax.crypto.spec.SecretKeySpec
+import java.security.Security
+
+public class KeyedEncryptorGroovyTest {
+    private static final Logger logger = 
LoggerFactory.getLogger(KeyedEncryptorGroovyTest.class)
+
+    private static final String TEST_RESOURCES_PREFIX = 
"src/test/resources/TestEncryptContent/"
+    private static final File plainFile = new 
File("${TEST_RESOURCES_PREFIX}/plain.txt")
+    private static final File encryptedFile = new 
File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.asc")
+
+    private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
+    private static final SecretKey KEY = new 
SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
+
+    @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 testShouldEncryptAndDecrypt() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext message."
+        logger.info("Plaintext: {}", PLAINTEXT)
+        InputStream plainStream = new 
ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"))
+
+        OutputStream cipherStream = new ByteArrayOutputStream()
+        OutputStream recoveredStream = new ByteArrayOutputStream()
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using ${encryptionMethod.name()}")
+
+        // Act
+        KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, KEY)
+
+        StreamCallback encryptionCallback = encryptor.getEncryptionCallback()
+        StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
+
+        encryptionCallback.process(plainStream, cipherStream)
+
+        final byte[] cipherBytes = ((ByteArrayOutputStream) 
cipherStream).toByteArray()
+        logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes))
+        InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes)
+        decryptionCallback.process(cipherInputStream, recoveredStream)
+
+        // Assert
+        byte[] recoveredBytes = ((ByteArrayOutputStream) 
recoveredStream).toByteArray()
+        String recovered = new String(recoveredBytes, "UTF-8")
+        logger.info("Recovered: {}\n\n", recovered)
+        assert PLAINTEXT.equals(recovered)
+    }
+
+    @Test
+    public void testShouldDecryptOpenSSLUnsaltedCipherTextWithKnownIV() throws 
Exception {
+        // Arrange
+        final String PLAINTEXT = new 
File("${TEST_RESOURCES_PREFIX}/plain.txt").text
+        logger.info("Plaintext: {}", PLAINTEXT)
+        byte[] cipherBytes = new 
File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.enc").bytes
+
+        final String keyHex = "711E85689CE7AFF6F410AEA43ABC5446"
+        final String ivHex = "842F685B84879B2E00F977C22B9E9A7D"
+
+        InputStream cipherStream = new ByteArrayInputStream(cipherBytes)
+        OutputStream recoveredStream = new ByteArrayOutputStream()
+
+        final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, new 
SecretKeySpec(Hex.decodeHex(keyHex as char[]), "AES"), Hex.decodeHex(ivHex as 
char[]))
+
+        StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
+        logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}")
+
+        // Act
+        decryptionCallback.process(cipherStream, recoveredStream)
+
+        // Assert
+        byte[] recoveredBytes = ((ByteArrayOutputStream) 
recoveredStream).toByteArray()
+        String recovered = new String(recoveredBytes, "UTF-8")
+        logger.info("Recovered: {}", recovered)
+        assert PLAINTEXT.equals(recovered)
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..2a3d456
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy
@@ -0,0 +1,299 @@
+/*
+ * 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.security.util.crypto
+
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.PBEParameterSpec
+import java.security.Security
+
+import static org.junit.Assert.fail
+
+@RunWith(JUnit4.class)
+public class NiFiLegacyCipherProviderGroovyTest {
+    private static final Logger logger = 
LoggerFactory.getLogger(NiFiLegacyCipherProviderGroovyTest.class);
+
+    private static List<EncryptionMethod> pbeEncryptionMethods = new 
ArrayList<>();
+    private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods 
= new ArrayList<>();
+
+    private static final String PROVIDER_NAME = "BC";
+    private static final int ITERATION_COUNT = 1000;
+
+    private static final byte[] SALT_16_BYTES = 
Hex.decodeHex("aabbccddeeff00112233445566778899".toCharArray());
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider());
+
+        pbeEncryptionMethods = EncryptionMethod.values().findAll { 
it.algorithm.toUpperCase().startsWith("PBE") }
+        limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { 
!it.isUnlimitedStrength() }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+
+    }
+
+    private static Cipher getLegacyCipher(String password, byte[] salt, String 
algorithm) {
+        try {
+            final PBEKeySpec pbeKeySpec = new 
PBEKeySpec(password.toCharArray());
+            final SecretKeyFactory factory = 
SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
+            SecretKey tempKey = factory.generateSecret(pbeKeySpec);
+
+            final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, 
ITERATION_COUNT);
+            Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
+            cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
+            return cipher;
+        } catch (Exception e) {
+            logger.error("Error generating legacy cipher", e);
+            fail(e.getMessage());
+        }
+
+        return null;
+    }
+
+    @Test
+    public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        NiFiLegacyCipherProvider cipherProvider = new 
NiFiLegacyCipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod encryptionMethod : 
limitedStrengthPbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", 
encryptionMethod.getAlgorithm());
+
+            if 
(!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(),
 encryptionMethod)) {
+                logger.warn("This test is skipped because the password length 
exceeds the undocumented limit BouncyCastle imposes on a JVM with limited 
strength crypto policies")
+                continue
+            }
+
+            byte[] salt = cipherProvider.generateSalt(encryptionMethod)
+            logger.info("Generated salt ${Hex.encodeHexString(salt)} 
(${salt.length})")
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, salt, true);
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, 
salt, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void 
testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws 
Exception {
+        // Arrange
+        Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE 
Unlimited Strength Jurisdiction Policy file.",
+                PasswordBasedEncryptor.supportsUnlimitedStrength());
+
+        NiFiLegacyCipherProvider cipherProvider = new 
NiFiLegacyCipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod encryptionMethod : pbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", 
encryptionMethod.getAlgorithm());
+
+            byte[] salt = cipherProvider.generateSalt(encryptionMethod)
+            logger.info("Generated salt ${Hex.encodeHexString(salt)} 
(${salt.length})")
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, salt, true);
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, 
salt, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldSupportLegacyCode() throws Exception {
+        // Arrange
+        NiFiLegacyCipherProvider cipherProvider = new 
NiFiLegacyCipherProvider();
+
+        final String PASSWORD = "short";
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod encryptionMethod : 
limitedStrengthPbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", 
encryptionMethod.getAlgorithm());
+
+            if 
(!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(),
 encryptionMethod)) {
+                logger.warn("This test is skipped because the password length 
exceeds the undocumented limit BouncyCastle imposes on a JVM with limited 
strength crypto policies")
+                continue
+            }
+
+            byte[] salt = cipherProvider.generateSalt(encryptionMethod)
+            logger.info("Generated salt ${Hex.encodeHexString(salt)} 
(${salt.length})")
+
+            // Initialize a legacy cipher for encryption
+            Cipher legacyCipher = getLegacyCipher(PASSWORD, salt, 
encryptionMethod.getAlgorithm());
+
+            byte[] cipherBytes = 
legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, salt, false);
+            byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws 
Exception {
+        // Arrange
+        NiFiLegacyCipherProvider cipherProvider = new 
NiFiLegacyCipherProvider();
+
+        final String PASSWORD = "short";
+        final byte[] SALT = new byte[0];
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", em.getAlgorithm());
+
+            if 
(!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(),
 em)) {
+                logger.warn("This test is skipped because the password length 
exceeds the undocumented limit BouncyCastle imposes on a JVM with limited 
strength crypto policies")
+                continue
+            }
+
+            // Initialize a legacy cipher for encryption
+            Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, 
em.getAlgorithm());
+
+            byte[] cipherBytes = 
legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, 
false);
+            byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldIgnoreKeyLength() throws Exception {
+        // Arrange
+        NiFiLegacyCipherProvider cipherProvider = new 
NiFiLegacyCipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = SALT_16_BYTES
+
+        final String plaintext = "This is a plaintext message.";
+
+        final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
+
+        // Initialize a cipher for encryption
+        EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+        final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, true);
+        byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
+        logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), 
cipherBytes.length);
+
+        // Act
+        KEY_LENGTHS.each { int keyLength ->
+            logger.info("Decrypting with 'requested' key length: ${keyLength}")
+
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, keyLength, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    /**
+     * This test determines for each PBE encryption algorithm if it actually 
requires the JCE unlimited strength jurisdiction policies to be installed.
+     * Even some algorithms that use 128-bit keys (which should be allowed on 
all systems) throw exceptions because BouncyCastle derives the key
+     * from the password using a long digest result at the time of key length 
checking.
+     * @throws IOException
+     */
+    @Ignore("Only needed once to determine max supported password lengths")
+    @Test
+    public void testShouldDetermineDependenceOnUnlimitedStrengthCrypto() 
throws IOException {
+        def encryptionMethods = EncryptionMethod.values().findAll { 
it.algorithm.startsWith("PBE") }
+
+        boolean unlimitedCryptoSupported = 
PasswordBasedEncryptor.supportsUnlimitedStrength()
+        logger.info("This JVM supports unlimited strength crypto: 
${unlimitedCryptoSupported}")
+
+        def longestSupportedPasswordByEM = [:]
+
+        encryptionMethods.each { EncryptionMethod encryptionMethod ->
+            logger.info("Attempting ${encryptionMethod.name()} 
(${encryptionMethod.algorithm}) which claims unlimited strength required: 
${encryptionMethod.unlimitedStrength}")
+
+            (1..20).find { int length ->
+                String password = "x" * length
+
+                try {
+                    NiFiLegacyCipherProvider cipherProvider = new 
NiFiLegacyCipherProvider();
+                    Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
password, true)
+                    return false
+                } catch (Exception e) {
+                    logger.error("Unable to create the cipher with 
${encryptionMethod.algorithm} and password ${password} (${password.length()}) 
due to ${e.getMessage()}")
+                    if 
(!longestSupportedPasswordByEM.containsKey(encryptionMethod)) {
+                        longestSupportedPasswordByEM.put(encryptionMethod, 
password.length() - 1)
+                    }
+                    return true
+                }
+            }
+            logger.info("\n")
+        }
+
+        logger.info("Longest supported password by encryption method:")
+        longestSupportedPasswordByEM.each { EncryptionMethod encryptionMethod, 
int length ->
+            logger.info("\t${encryptionMethod.algorithm}\t${length}")
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git 
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy
 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..62b7970
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy
@@ -0,0 +1,323 @@
+/*
+ * 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.security.util.crypto
+
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.PBEParameterSpec
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assert.fail
+
+@RunWith(JUnit4.class)
+public class OpenSSLPKCS5CipherProviderGroovyTest {
+    private static final Logger logger = 
LoggerFactory.getLogger(OpenSSLPKCS5CipherProviderGroovyTest.class);
+
+    private static List<EncryptionMethod> pbeEncryptionMethods = new 
ArrayList<>();
+    private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods 
= new ArrayList<>();
+
+    private static final String PROVIDER_NAME = "BC";
+    private static final int ITERATION_COUNT = 0;
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider());
+
+        pbeEncryptionMethods = EncryptionMethod.values().findAll { 
it.algorithm.toUpperCase().startsWith("PBE") }
+        limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { 
!it.isUnlimitedStrength() }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+
+    }
+
+    private static Cipher getLegacyCipher(String password, byte[] salt, String 
algorithm) {
+        try {
+            final PBEKeySpec pbeKeySpec = new 
PBEKeySpec(password.toCharArray());
+            final SecretKeyFactory factory = 
SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
+            SecretKey tempKey = factory.generateSecret(pbeKeySpec);
+
+            final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, 
ITERATION_COUNT);
+            Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
+            cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
+            return cipher;
+        } catch (Exception e) {
+            logger.error("Error generating legacy cipher", e);
+            fail(e.getMessage());
+        }
+
+        return null;
+    }
+
+    @Test
+    public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final String PASSWORD = "short";
+        final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", em.getAlgorithm());
+
+            if 
(!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(),
 em)) {
+                logger.warn("This test is skipped because the password length 
exceeds the undocumented limit BouncyCastle imposes on a JVM with limited 
strength crypto policies")
+                continue
+            }
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void 
testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws 
Exception {
+        // Arrange
+        Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE 
Unlimited Strength Jurisdiction Policy file.",
+                PasswordBasedEncryptor.supportsUnlimitedStrength());
+
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : pbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", em.getAlgorithm());
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldSupportLegacyCode() throws Exception {
+        // Arrange
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", em.getAlgorithm());
+
+            if 
(!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(),
 em)) {
+                logger.warn("This test is skipped because the password length 
exceeds the undocumented limit BouncyCastle imposes on a JVM with limited 
strength crypto policies")
+                continue
+            }
+
+            // Initialize a legacy cipher for encryption
+            Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, 
em.getAlgorithm());
+
+            byte[] cipherBytes = 
legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, 
SALT, false);
+            byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws 
Exception {
+        // Arrange
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final String PASSWORD = "short";
+        final byte[] SALT = new byte[0];
+
+        final String plaintext = "This is a plaintext message.";
+
+        // Act
+        for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+            logger.info("Using algorithm: {}", em.getAlgorithm());
+
+            if 
(!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(),
 em)) {
+                logger.warn("This test is skipped because the password length 
exceeds the undocumented limit BouncyCastle imposes on a JVM with limited 
strength crypto policies")
+                continue
+            }
+
+            // Initialize a legacy cipher for encryption
+            Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, 
em.getAlgorithm());
+
+            byte[] cipherBytes = 
legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+            logger.info("Cipher text: {} {}", 
Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+            Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, 
false);
+            byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldIgnoreKeyLength() throws Exception {
+        // Arrange
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+        final String plaintext = "This is a plaintext message.";
+
+        final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
+
+        // Initialize a cipher for encryption
+        EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+        final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, true);
+        byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
+        logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), 
cipherBytes.length);
+
+        // Act
+        KEY_LENGTHS.each { int keyLength ->
+            logger.info("Decrypting with 'requested' key length: ${keyLength}")
+
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, keyLength, false);
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+            String recovered = new String(recoveredBytes, "UTF-8");
+
+            // Assert
+            assert plaintext.equals(recovered);
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldRequireEncryptionMethod() throws Exception {
+        // Arrange
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
+
+        // Act
+        logger.info("Using algorithm: null");
+
+        def msg = shouldFail(IllegalArgumentException) {
+            Cipher providedCipher = cipherProvider.getCipher(null, PASSWORD, 
SALT, false);
+        }
+
+        // Assert
+        assert msg =~ "The encryption method must be specified"
+    }
+
+    @Test
+    public void testGetCipherShouldRequirePassword() throws Exception {
+        // Arrange
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
+        EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+
+        // Act
+        logger.info("Using algorithm: ${encryptionMethod}");
+
+        def msg = shouldFail(IllegalArgumentException) {
+            Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, 
"", SALT, false);
+        }
+
+        // Assert
+        assert msg =~ "Encryption with an empty password is not supported"
+    }
+
+    @Test
+    public void testGetCipherShouldValidateSaltLength() throws Exception {
+        // Arrange
+        OpenSSLPKCS5CipherProvider cipherProvider = new 
OpenSSLPKCS5CipherProvider();
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex("00112233445566".toCharArray());
+        EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+
+        // Act
+        logger.info("Using algorithm: ${encryptionMethod}");
+
+        def msg = shouldFail(IllegalArgumentException) {
+            Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, 
PASSWORD, SALT, false);
+        }
+
+        // Assert
+        assert msg =~ "Salt must be 8 bytes US-ASCII encoded"
+    }
+
+    @Test
+    public void testGenerateSaltShouldProvideValidSalt() throws Exception {
+        // Arrange
+        PBECipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
+
+        // Act
+        byte[] salt = cipherProvider.generateSalt()
+        logger.info("Checking salt ${Hex.encodeHexString(salt)}")
+
+        // Assert
+        assert salt.length == cipherProvider.getDefaultSaltLength()
+        assert salt != [(0x00 as byte) * cipherProvider.defaultSaltLength]
+    }
+}
\ No newline at end of file

Reply via email to