nifi-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From alopre...@apache.org
Subject [02/13] nifi git commit: NIFI-3594 Implemented encrypted provenance repository. Added src/test/resources/logback-test.xml files resetting log level from DEBUG (in nifi-data-provenance-utils) to WARN because later tests depend on MockComponentLog recordin
Date Tue, 02 May 2017 17:27:27 GMT
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/PBKDF2CipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PBKDF2CipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PBKDF2CipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..51a8177
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PBKDF2CipherProviderGroovyTest.groovy
@@ -0,0 +1,545 @@
+/*
+ * 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 java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assert.assertTrue
+
+@RunWith(JUnit4.class)
+public class PBKDF2CipherProviderGroovyTest {
+    private static final Logger logger = LoggerFactory.getLogger(PBKDF2CipherProviderGroovyTest.class);
+
+    private static List<EncryptionMethod> strongKDFEncryptionMethods
+
+    public static final String MICROBENCHMARK = "microbenchmark"
+    private static final int DEFAULT_KEY_LENGTH = 128;
+    private static final int TEST_ITERATION_COUNT = 1000
+    private final String DEFAULT_PRF = "SHA-512"
+    private final String SALT_HEX = "0123456789ABCDEFFEDCBA9876543210"
+    private final String IV_HEX = "01" * 16
+    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 PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX 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, 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 testGetCipherShouldRejectInvalidIV() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
+        final def INVALID_IVS = (0..15).collect { int length -> new byte[length] }
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+        // Act
+        INVALID_IVS.each { byte[] badIV ->
+            logger.info("IV: ${Hex.encodeHexString(badIV)} ${badIV.length}")
+
+            // Encrypt should print a warning about the bad IV but overwrite it
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, badIV, DEFAULT_KEY_LENGTH, true)
+
+            // Decrypt should fail
+            def msg = shouldFail(IllegalArgumentException) {
+                cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, badIV, DEFAULT_KEY_LENGTH, false)
+            }
+
+            // Assert
+            assert msg =~ "Cannot decrypt without a valid IV"
+        }
+    }
+
+    @Test
+    public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
+        final byte[] IV = Hex.decodeHex(IV_HEX 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 PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
+
+        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 testShouldRejectEmptyPRF() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
+        final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
+
+        final String plaintext = "This is a plaintext message.";
+        final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        String prf = ""
+
+        // Act
+        logger.info("Using PRF ${prf}")
+        def msg = shouldFail(IllegalArgumentException) {
+            cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
+        }
+
+        // Assert
+        assert msg =~ "Cannot resolve empty PRF"
+    }
+
+    @Test
+    public void testShouldResolveDefaultPRF() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
+        final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
+
+        final String plaintext = "This is a plaintext message.";
+        final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+        final PBKDF2CipherProvider SHA512_PROVIDER = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
+
+        String prf = "sha768"
+        logger.info("Using ${prf}")
+
+        // Act
+        cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
+        logger.info("Resolved PRF to ${cipherProvider.getPRFName()}")
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+        // Initialize a cipher for encryption
+        Cipher cipher = cipherProvider.getCipher(encryptionMethod, 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 = SHA512_PROVIDER.getCipher(encryptionMethod, 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 testShouldResolveVariousPRFs() throws Exception {
+        // Arrange
+        final List<String> PRFS = ["SHA-1", "MD5", "SHA-256", "SHA-384", "SHA-512"]
+        RandomIVPBECipherProvider cipherProvider
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
+        final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
+
+        final String plaintext = "This is a plaintext message.";
+        final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+        // Act
+        PRFS.each { String prf ->
+            logger.info("Using ${prf}")
+            cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
+            logger.info("Resolved PRF to ${cipherProvider.getPRFName()}")
+
+            logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, 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(encryptionMethod, 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 testGetCipherShouldSupportExternalCompatibility() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider("SHA-256", TEST_ITERATION_COUNT);
+
+        final String PLAINTEXT = "This is a plaintext message.";
+        final String PASSWORD = "thisIsABadPassword";
+
+        // These values can be generated by running `$ ./openssl_pbkdf2.rb` in the terminal
+        final byte[] SALT = Hex.decodeHex("ae2481bee3d8b5d5b732bf464ea2ff01" as char[]);
+        final byte[] IV = Hex.decodeHex("26db997dcd18472efd74dabe5ff36853" as char[]);
+
+        final String CIPHER_TEXT = "92edbabae06add6275a1d64815755a9ba52afc96e2c1a316d3abbe1826e96f6c"
+        byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+        logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+        // Act
+        Cipher cipher = cipherProvider.getCipher(encryptionMethod, 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 testGetCipherForDecryptShouldRequireIV() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
+        final byte[] IV = Hex.decodeHex(IV_HEX 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 testGetCipherShouldRejectInvalidSalt() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
+
+        final String PASSWORD = "thisIsABadPassword";
+
+        final def INVALID_SALTS = ['pbkdf2', '$3a$11$', 'x', '$2a$10$', '', null]
+
+        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 at least 16 bytes\\. To generate a salt, use PBKDF2CipherProvider#generateSalt"
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
+        final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
+
+        final String PLAINTEXT = "This is a plaintext message.";
+
+        // Currently only AES ciphers are compatible with PBKDF2, 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 PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
+        final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
+
+        // Currently only AES ciphers are compatible with PBKDF2, so redundant to test all algorithms
+        final def VALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
+        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
+            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"
+        }
+    }
+
+    @Ignore("This test can be run on a specific machine to evaluate if the default iteration count is sufficient")
+    @Test
+    public void testDefaultConstructorShouldProvideStrongIterationCount() {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider();
+
+        // 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 iteration count to reach 500 ms
+        int minimumIterationCount = calculateMinimumIterationCount()
+        logger.info("Determined minimum safe iteration count to be ${minimumIterationCount}")
+
+        // Act
+        int iterationCount = cipherProvider.getIterationCount()
+        logger.info("Default iteration count ${iterationCount}")
+
+        // Assert
+        assertTrue("The default iteration count for PBKDF2CipherProvider is too weak. Please update the default value to a stronger level.", iterationCount >= minimumIterationCount)
+    }
+
+    /**
+     * Returns the iteration count required for a derivation to exceed 500 ms on this machine using the default PRF.
+     * Code adapted from http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
+     *
+     * @return the minimum iteration count
+     */
+    private static int calculateMinimumIterationCount() {
+        // High start-up cost, so run multiple times for better benchmarking
+        final int RUNS = 10
+
+        // Benchmark using an iteration count of 10k
+        int iterationCount = 10_000
+
+        final byte[] SALT = [0x00 as byte] * 16
+        final byte[] IV = [0x01 as byte] * 16
+
+        String defaultPrf = new PBKDF2CipherProvider().getPRFName()
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(defaultPrf, iterationCount)
+
+        // Run once to prime the system
+        double duration = time {
+            Cipher cipher = cipherProvider.getCipher(EncryptionMethod.AES_CBC, MICROBENCHMARK, SALT, IV, DEFAULT_KEY_LENGTH, false)
+        }
+        logger.info("First run of iteration count ${iterationCount} took ${duration} ms (ignored)")
+
+        def durations = []
+
+        RUNS.times { int i ->
+            duration = time {
+                // Use encrypt mode with provided salt and IV to minimize overhead during benchmark call
+                Cipher cipher = cipherProvider.getCipher(EncryptionMethod.AES_CBC, "${MICROBENCHMARK}${i}", SALT, IV, DEFAULT_KEY_LENGTH, false)
+            }
+            logger.info("Iteration count ${iterationCount} took ${duration} ms")
+            durations << duration
+        }
+
+        duration = durations.sum() / durations.size()
+        logger.info("Iteration count ${iterationCount} averaged ${duration} ms")
+
+        // Keep increasing iteration count until the estimated duration is over 500 ms
+        while (duration < 500) {
+            iterationCount *= 2
+            duration *= 2
+        }
+
+        logger.info("Returning iteration count ${iterationCount} for ${duration} ms")
+
+        return iterationCount
+    }
+
+    private static double time(Closure c) {
+        long start = System.nanoTime()
+        c.call()
+        long end = System.nanoTime()
+        return (end - start) / 1_000_000.0
+    }
+
+    @Test
+    public void testGenerateSaltShouldProvideValidSalt() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
+
+        // Act
+        byte[] salt = cipherProvider.generateSalt()
+        logger.info("Checking salt ${Hex.encodeHexString(salt)}")
+
+        // Assert
+        assert salt.length == 16
+        assert salt != [(0x00 as byte) * 16]
+    }
+}
\ 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/PasswordBasedEncryptorGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorGroovyTest.groovy
new file mode 100644
index 0000000..7008381
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/PasswordBasedEncryptorGroovyTest.groovy
@@ -0,0 +1,225 @@
+/*
+ * 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.apache.nifi.security.util.KeyDerivationFunction
+import org.apache.nifi.stream.io.ByteArrayOutputStream
+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.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.security.Security
+
+public class PasswordBasedEncryptorGroovyTest {
+    private static final Logger logger = LoggerFactory.getLogger(PasswordBasedEncryptorGroovyTest.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}/salted_128_raw.asc")
+
+    private static final String PASSWORD = "thisIsABadPassword"
+    private static final String LEGACY_PASSWORD = "Hello, World!"
+
+    @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"))
+
+        String shortPassword = "short"
+
+        def encryptionMethodsAndKdfs = [
+                (KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): EncryptionMethod.MD5_128AES,
+                (KeyDerivationFunction.NIFI_LEGACY)             : EncryptionMethod.MD5_128AES,
+                (KeyDerivationFunction.BCRYPT)                  : EncryptionMethod.AES_CBC,
+                (KeyDerivationFunction.SCRYPT)                  : EncryptionMethod.AES_CBC,
+                (KeyDerivationFunction.PBKDF2)                  : EncryptionMethod.AES_CBC
+        ]
+
+        // Act
+        encryptionMethodsAndKdfs.each { KeyDerivationFunction kdf, EncryptionMethod encryptionMethod ->
+            OutputStream cipherStream = new ByteArrayOutputStream()
+            OutputStream recoveredStream = new ByteArrayOutputStream()
+
+            logger.info("Using ${kdf.name} and ${encryptionMethod.name()}")
+            PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, shortPassword.toCharArray(), kdf)
+
+            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)
+
+            // This is necessary to run multiple iterations
+            plainStream.reset()
+        }
+    }
+
+    @Test
+    public void testShouldDecryptLegacyOpenSSLSaltedCipherText() throws Exception {
+        // Arrange
+        Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength())
+
+        final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text
+        logger.info("Plaintext: {}", PLAINTEXT)
+        byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/salted_128_raw.enc").bytes
+        InputStream cipherStream = new ByteArrayInputStream(cipherBytes)
+        OutputStream recoveredStream = new ByteArrayOutputStream()
+
+        final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+        final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY
+
+        PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf)
+
+        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)
+    }
+
+    @Test
+    public void testShouldDecryptLegacyOpenSSLUnsaltedCipherText() throws Exception {
+        // Arrange
+        Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength())
+
+        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
+        InputStream cipherStream = new ByteArrayInputStream(cipherBytes)
+        OutputStream recoveredStream = new ByteArrayOutputStream()
+
+        final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+        final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY
+
+        PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf)
+
+        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)
+    }
+
+    @Test
+    public void testShouldDecryptNiFiLegacySaltedCipherTextWithVariableSaltLength() throws Exception {
+        // Arrange
+        final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text
+        logger.info("Plaintext: {}", PLAINTEXT)
+
+        final String PASSWORD = "short"
+        logger.info("Password: ${PASSWORD}")
+
+        /* The old NiFi legacy KDF code checked the algorithm block size and used it for the salt length.
+         If the block size was not available, it defaulted to 8 bytes based on the default salt size. */
+
+        def pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
+        def encryptionMethodsByBlockSize = pbeEncryptionMethods.groupBy {
+            Cipher cipher = Cipher.getInstance(it.algorithm, it.provider)
+            cipher.getBlockSize()
+        }
+
+        logger.info("Grouped algorithms by block size: ${encryptionMethodsByBlockSize.collectEntries { k, v -> [k, v*.algorithm] }}")
+
+        encryptionMethodsByBlockSize.each { int blockSize, List<EncryptionMethod> encryptionMethods ->
+            encryptionMethods.each { EncryptionMethod encryptionMethod ->
+                final int EXPECTED_SALT_SIZE = (blockSize > 0) ? blockSize : 8
+                logger.info("Testing ${encryptionMethod.algorithm} with expected salt size ${EXPECTED_SALT_SIZE}")
+
+                def legacySaltHex = "aa" * EXPECTED_SALT_SIZE
+                byte[] legacySalt = Hex.decodeHex(legacySaltHex as char[])
+                logger.info("Generated legacy salt ${legacySaltHex} (${legacySalt.length})")
+
+                // Act
+
+                // Encrypt using the raw legacy code
+                NiFiLegacyCipherProvider legacyCipherProvider = new NiFiLegacyCipherProvider()
+                Cipher legacyCipher = legacyCipherProvider.getCipher(encryptionMethod, PASSWORD, legacySalt, true)
+                byte[] cipherBytes = legacyCipher.doFinal(PLAINTEXT.bytes)
+                logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}")
+
+                byte[] completeCipherStreamBytes = CipherUtility.concatBytes(legacySalt, cipherBytes)
+                logger.info("Complete cipher stream: ${Hex.encodeHexString(completeCipherStreamBytes)}")
+
+                InputStream cipherStream = new ByteArrayInputStream(completeCipherStreamBytes)
+                OutputStream resultStream = new ByteArrayOutputStream()
+
+                // Now parse and decrypt using PBE encryptor
+                PasswordBasedEncryptor decryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD as char[], KeyDerivationFunction.NIFI_LEGACY)
+
+                StreamCallback decryptCallback = decryptor.decryptionCallback
+                decryptCallback.process(cipherStream, resultStream)
+
+                logger.info("Decrypted: ${Hex.encodeHexString(resultStream.toByteArray())}")
+                String recovered = new String(resultStream.toByteArray())
+                logger.info("Recovered: ${recovered}")
+
+                // Assert
+                assert recovered == PLAINTEXT
+            }
+        }
+    }
+}
\ 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/ScryptCipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/ScryptCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/ScryptCipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..8fce9f2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/ScryptCipherProviderGroovyTest.groovy
@@ -0,0 +1,597 @@
+/*
+ * 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.scrypt.Scrypt
+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.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import java.security.SecureRandom
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assert.assertTrue
+
+@RunWith(JUnit4.class)
+public class ScryptCipherProviderGroovyTest {
+    private static final Logger logger = LoggerFactory.getLogger(ScryptCipherProviderGroovyTest.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
+
+    RandomIVPBECipherProvider cipherProvider
+
+    @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 {
+        // Very fast parameters to test for correctness rather than production values
+        cipherProvider = new ScryptCipherProvider(4, 1, 1)
+    }
+
+    @After
+    public void tearDown() throws Exception {
+
+    }
+
+    @Test
+    public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        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
+        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());
+
+        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 testScryptShouldSupportExternalCompatibility() throws Exception {
+        // Arrange
+
+        // Default values are N=2^14, r=8, p=1, but the provided salt will contain the parameters used
+        cipherProvider = new ScryptCipherProvider()
+
+        final String PLAINTEXT = "This is a plaintext message.";
+        final String PASSWORD = "thisIsABadPassword"
+        final int DK_LEN = 128
+
+        // These values can be generated by running `$ ./openssl_scrypt.rb` in the terminal
+        final byte[] SALT = Hex.decodeHex("f5b8056ea6e66edb8d013ac432aba24a" as char[])
+        logger.info("Expected salt: ${Hex.encodeHexString(SALT)}")
+        final byte[] IV = Hex.decodeHex("76a00f00878b8c3db314ae67804c00a1" as char[])
+
+        final String CIPHER_TEXT = "604188bf8e9137bc1b24a0ab01973024bc5935e9ae5fedf617bdca028c63c261"
+        logger.sanity("Ruby cipher text: ${CIPHER_TEXT}")
+        byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+        // Sanity check
+        String rubyKeyHex = "a8efbc0a709d3f89b6bb35b05fc8edf5"
+        logger.sanity("Using key: ${rubyKeyHex}")
+        logger.sanity("Using IV:  ${Hex.encodeHexString(IV)}")
+        Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, "BC")
+        def rubyKey = new SecretKeySpec(Hex.decodeHex(rubyKeyHex as char[]), "AES")
+        def ivSpec = new IvParameterSpec(IV)
+        rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec)
+        byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.bytes)
+        logger.sanity("Created cipher text: ${Hex.encodeHexString(rubyCipherBytes)}")
+        rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec)
+        assert rubyCipher.doFinal(rubyCipherBytes) == PLAINTEXT.bytes
+        logger.sanity("Decrypted generated cipher text successfully")
+        assert rubyCipher.doFinal(cipherBytes) == PLAINTEXT.bytes
+        logger.sanity("Decrypted external cipher text successfully")
+
+        // n$r$p$hex_salt_SL$hex_hash_HL
+        final String FULL_HASH = "400\$8\$24\$f5b8056ea6e66edb8d013ac432aba24a\$a8efbc0a709d3f89b6bb35b05fc8edf5"
+        logger.info("Full Hash: ${FULL_HASH}")
+
+        def (String nStr, String rStr, String pStr, String saltHex, String hashHex) = FULL_HASH.split("\\\$")
+        def (n, r, p) = [nStr, rStr, pStr].collect { Integer.valueOf(it, 16) }
+
+        logger.info("N: Hex ${nStr} -> ${n}")
+        logger.info("r: Hex ${rStr} -> ${r}")
+        logger.info("p: Hex ${pStr} -> ${p}")
+        logger.info("Salt: ${saltHex}")
+        logger.info("Hash: ${hashHex}")
+
+        // Form Java-style salt with cost params from Ruby-style
+        String javaSalt = Scrypt.formatSalt(Hex.decodeHex(saltHex as char[]), n, r, p)
+        logger.info("Formed Java-style salt: ${javaSalt}")
+
+        // Convert hash from hex to Base64
+        String base64Hash = CipherUtility.encodeBase64NoPadding(Hex.decodeHex(hashHex as char[]))
+        logger.info("Converted hash from hex ${hashHex} to Base64 ${base64Hash}")
+        assert Hex.encodeHexString(Base64.decodeBase64(base64Hash)) == hashHex
+
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+        logger.info("External cipher text: ${CIPHER_TEXT} ${cipherBytes.length}");
+
+        // Act
+        Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, javaSalt.bytes, IV, DK_LEN, 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 testGetCipherShouldHandleSaltWithoutParameters() throws Exception {
+        // Arrange
+
+        // To help Groovy resolve implementation private methods not known at interface level
+        cipherProvider = cipherProvider as ScryptCipherProvider
+
+        final String PASSWORD = "shortPassword";
+        final byte[] SALT = new byte[cipherProvider.defaultSaltLength]
+        new SecureRandom().nextBytes(SALT)
+
+        final String EXPECTED_FORMATTED_SALT = cipherProvider.formatSaltForScrypt(SALT)
+        logger.info("Expected salt: ${EXPECTED_FORMATTED_SALT}")
+
+        final String plaintext = "This is a plaintext message.";
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+        // Act
+
+        // Initialize a cipher for encryption
+        Cipher cipher = cipherProvider.getCipher(encryptionMethod, 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}");
+
+        // Manually initialize a cipher for decrypt with the expected salt
+        byte[] parsedSalt = new byte[cipherProvider.defaultSaltLength]
+        def params = []
+        cipherProvider.parseSalt(EXPECTED_FORMATTED_SALT, parsedSalt, params)
+        def (int n, int r, int p) = params
+        byte[] keyBytes = Scrypt.deriveScryptKey(PASSWORD.bytes, parsedSalt, n, r, p, DEFAULT_KEY_LENGTH)
+        SecretKey key = new SecretKeySpec(keyBytes, "AES")
+        Cipher manualCipher = Cipher.getInstance(encryptionMethod.algorithm, encryptionMethod.provider)
+        manualCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv))
+        byte[] recoveredBytes = manualCipher.doFinal(cipherBytes);
+        String recovered = new String(recoveredBytes, "UTF-8");
+        logger.info("Recovered: ${recovered}")
+
+        // Assert
+        assert plaintext.equals(recovered);
+    }
+
+    @Test
+    public void testGetCipherShouldNotAcceptInvalidSalts() throws Exception {
+        // Arrange
+        final String PASSWORD = "thisIsABadPassword";
+
+        final def INVALID_SALTS = ['bad_sal', '$3a$11$', 'x', '$2a$10$', '$400$1$1$abcdefghijklmnopqrstuvwxyz']
+        final LENGTH_MESSAGE = "The raw salt must be between 8 and 32 bytes"
+
+        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);
+            }
+            logger.expected(msg)
+
+            // Assert
+            assert msg =~ LENGTH_MESSAGE
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldHandleUnformattedSalts() throws Exception {
+        // Arrange
+        final String PASSWORD = "thisIsABadPassword";
+
+        final def RECOVERABLE_SALTS = ['$ab$00$acbdefghijklmnopqrstuv', '$4$1$1$0123456789abcdef', '$400$1$1$abcdefghijklmnopqrstuv']
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+        // Act
+        RECOVERABLE_SALTS.each { String salt ->
+            logger.info("Checking salt ${salt}")
+
+            Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
+
+            // Assert
+            assert cipher
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldRejectEmptySalt() throws Exception {
+        // Arrange
+        final String PASSWORD = "thisIsABadPassword";
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+        logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+        // 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 ScryptCipherProvider#generateSalt"
+    }
+
+    @Test
+    public void testGetCipherForDecryptShouldRequireIV() throws Exception {
+        // Arrange
+        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);
+            }
+            logger.expected(msg)
+
+            // Assert
+            assert msg =~ "Cannot decrypt without a valid IV"
+        }
+    }
+
+    @Test
+    public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
+        // Arrange
+        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
+        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.";
+
+        // Even though Scrypt can derive keys of arbitrary length, it will fail to validate if the underlying cipher does not support it
+        final def INVALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
+        // Currently only AES ciphers are compatible with Scrypt, so redundant to test all algorithms
+        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);
+            }
+            logger.expected(msg)
+
+            // Assert
+            assert msg =~ "${keyLength} is not a valid key length for AES"
+        }
+    }
+
+    @Test
+    public void testScryptShouldNotAcceptInvalidPassword() {
+        // Arrange
+        String badPassword = ""
+        byte[] salt = [0x01 as byte] * 16
+
+        EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            cipherProvider.getCipher(encryptionMethod, badPassword, salt, DEFAULT_KEY_LENGTH, true)
+        }
+
+        // Assert
+        assert msg =~ "Encryption with an empty password is not supported"
+    }
+
+    @Test
+    public void testGenerateSaltShouldUseProvidedParameters() throws Exception {
+        // Arrange
+        RandomIVPBECipherProvider cipherProvider = new ScryptCipherProvider(8, 2, 2);
+        int n = cipherProvider.getN()
+        int r = cipherProvider.getR()
+        int p = cipherProvider.getP()
+
+        // Act
+        final String salt = new String(cipherProvider.generateSalt())
+        logger.info("Salt: ${salt}")
+
+        // Assert
+        assert salt =~ "^(?i)\\\$s0\\\$[a-f0-9]{5,16}\\\$"
+        String params = Scrypt.encodeParams(n, r, p)
+        assert salt.contains("\$${params}\$")
+    }
+
+    @Test
+    public void testShouldParseSalt() throws Exception {
+        // Arrange
+        cipherProvider = cipherProvider as ScryptCipherProvider
+
+        final byte[] EXPECTED_RAW_SALT = Hex.decodeHex("f5b8056ea6e66edb8d013ac432aba24a" as char[])
+        final int EXPECTED_N = 1024
+        final int EXPECTED_R = 8
+        final int EXPECTED_P = 36
+
+        final String FORMATTED_SALT = "\$s0\$a0824\$9bgFbqbmbtuNATrEMquiSg"
+        logger.info("Using salt: ${FORMATTED_SALT}");
+
+        byte[] rawSalt = new byte[16]
+        def params = []
+
+        // Act
+        cipherProvider.parseSalt(FORMATTED_SALT, rawSalt, params)
+
+        // Assert
+        assert rawSalt == EXPECTED_RAW_SALT
+        assert params[0] == EXPECTED_N
+        assert params[1] == EXPECTED_R
+        assert params[2] == EXPECTED_P
+    }
+
+    @Ignore("This test can be run on a specific machine to evaluate if the default parameters are sufficient")
+    @Test
+    public void testDefaultConstructorShouldProvideStrongParameters() {
+        // Arrange
+        ScryptCipherProvider testCipherProvider = new ScryptCipherProvider()
+
+        /** See this Stack Overflow answer for a good visualization of the interplay between N, r, p <a href="http://stackoverflow.com/a/30308723" rel="noopener">http://stackoverflow.com/a/30308723</a> */
+
+        // Act
+        int n = testCipherProvider.getN()
+        int r = testCipherProvider.getR()
+        int p = testCipherProvider.getP()
+        logger.info("Default parameters N=${n}, r=${r}, p=${p}")
+
+        // Calculate the parameters to reach 500 ms
+        def (int minimumN, int minimumR, int minimumP) = calculateMinimumParameters(r, p)
+        logger.info("Determined minimum safe parameters to be N=${minimumN}, r=${minimumR}, p=${minimumP}")
+
+        // Assert
+        assertTrue("The default parameters for ScryptCipherProvider are too weak. Please update the default values to a stronger level.", n >= minimumN)
+    }
+
+    /**
+     * Returns the parameters 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
+     *
+     * @param r the block size in bytes (defaults to 8)
+     * @param p the parallelization factor (defaults to 1)
+     * @param maxHeapSize the maximum heap size to use in bytes (defaults to 1 GB)
+     *
+     * @return the minimum scrypt parameters as [N, r, p]
+     */
+    private static List<Integer> calculateMinimumParameters(int r = 8, int p = 1, int maxHeapSize = 1024 * 1024 * 1024) {
+        // High start-up cost, so run multiple times for better benchmarking
+        final int RUNS = 10
+
+        // Benchmark using N=2^4
+        int n = 2**4
+        int dkLen = 128
+
+        assert Scrypt.calculateExpectedMemory(n, r, p) <= maxHeapSize
+
+        byte[] salt = new byte[Scrypt.defaultSaltLength]
+        new SecureRandom().nextBytes(salt)
+
+        // Run once to prime the system
+        double duration = time {
+            Scrypt.scrypt(MICROBENCHMARK, salt, n, r, p, dkLen)
+        }
+        logger.info("First run of N=${n}, r=${r}, p=${p} took ${duration} ms (ignored)")
+
+        def durations = []
+
+        RUNS.times { int i ->
+            duration = time {
+                Scrypt.scrypt(MICROBENCHMARK, salt, n, r, p, dkLen)
+            }
+            logger.info("N=${n}, r=${r}, p=${p} took ${duration} ms")
+            durations << duration
+        }
+
+        duration = durations.sum() / durations.size()
+        logger.info("N=${n}, r=${r}, p=${p} averaged ${duration} ms")
+
+        // Doubling N would double the run time
+        // Keep increasing N until the estimated duration is over 500 ms
+        while (duration < 500) {
+            n *= 2
+            duration *= 2
+        }
+
+        logger.info("Returning N=${n}, r=${r}, p=${p} for ${duration} ms")
+
+        return [n, r, p]
+    }
+
+    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/scrypt/ScryptGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/scrypt/ScryptGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/scrypt/ScryptGroovyTest.groovy
new file mode 100644
index 0000000..da34c49
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/security/util/crypto/scrypt/ScryptGroovyTest.groovy
@@ -0,0 +1,399 @@
+/*
+ * 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.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/java/org/apache/nifi/processors/standard/TestEncryptContent.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java
index 3166bd0..063652b 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestEncryptContent.java
@@ -16,12 +16,17 @@
  */
 package org.apache.nifi.processors.standard;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.security.Security;
+import java.util.Collection;
 import org.apache.commons.codec.binary.Hex;
 import org.apache.nifi.components.ValidationResult;
-import org.apache.nifi.processors.standard.util.crypto.CipherUtility;
-import org.apache.nifi.processors.standard.util.crypto.PasswordBasedEncryptor;
 import org.apache.nifi.security.util.EncryptionMethod;
 import org.apache.nifi.security.util.KeyDerivationFunction;
+import org.apache.nifi.security.util.crypto.CipherUtility;
+import org.apache.nifi.security.util.crypto.PasswordBasedEncryptor;
 import org.apache.nifi.util.MockFlowFile;
 import org.apache.nifi.util.MockProcessContext;
 import org.apache.nifi.util.TestRunner;
@@ -34,12 +39,6 @@ import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Paths;
-import java.security.Security;
-import java.util.Collection;
-
 public class TestEncryptContent {
 
     private static final Logger logger = LoggerFactory.getLogger(TestEncryptContent.class);

http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptorTest.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptorTest.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptorTest.java
deleted file mode 100644
index b4cd2e3..0000000
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptorTest.java
+++ /dev/null
@@ -1,132 +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;
-
-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.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.security.Security;
-
-public class OpenPGPKeyBasedEncryptorTest {
-    private static final Logger logger = LoggerFactory.getLogger(OpenPGPKeyBasedEncryptorTest.class);
-
-    private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt");
-    private final File unsignedFile = new File("src/test/resources/TestEncryptContent/text.txt.unsigned.gpg");
-    private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.gpg");
-
-    private static final String SECRET_KEYRING_PATH = "src/test/resources/TestEncryptContent/secring.gpg";
-    private static final String PUBLIC_KEYRING_PATH = "src/test/resources/TestEncryptContent/pubring.gpg";
-    private static final String USER_ID = "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) <alopresto.apache+test@gmail.com>";
-
-    private static final String PASSWORD = "thisIsABadPassword";
-
-    @BeforeClass
-    public static void setUpOnce() throws Exception {
-        Security.addProvider(new BouncyCastleProvider());
-    }
-
-    @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();
-
-        // No file, just streams
-        String filename = "tempFile.txt";
-
-        // Encryptor does not require password
-        OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(
-            EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), PUBLIC_KEYRING_PATH, USER_ID, new char[0], filename);
-        StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
-
-        OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor(
-            EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
-        StreamCallback decryptionCallback = decryptor.getDecryptionCallback();
-
-        // Act
-        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: {}", recovered);
-        assert PLAINTEXT.equals(recovered);
-    }
-
-    @Test
-    public void testShouldDecryptExternalFile() throws Exception {
-        // Arrange
-        byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath()));
-        final String PLAINTEXT = new String(plainBytes, "UTF-8");
-
-        InputStream cipherStream = new FileInputStream(unsignedFile);
-        OutputStream recoveredStream = new ByteArrayOutputStream();
-
-        // No file, just streams
-        String filename = unsignedFile.getName();
-
-        OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(
-            EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
-
-        StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
-
-        // Act
-        decryptionCallback.process(cipherStream, recoveredStream);
-
-        // Assert
-        byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
-        String recovered = new String(recoveredBytes, "UTF-8");
-        logger.info("Recovered: {}", recovered);
-        Assert.assertEquals("Recovered text", PLAINTEXT, recovered);
-    }
-}
\ No newline at end of file


Mime
View raw message