You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by jo...@apache.org on 2017/09/26 14:56:04 UTC

[4/7] nifi git commit: NIFI-3116 This closes #2108. Added initial regression test for StringEncryptor to ensure continued functionality during removal of Jasypt. Added external compatibility regression test for StringEncryptor to ensure continued functio

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/scrypt/ScryptGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/scrypt/ScryptGroovyTest.groovy b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/scrypt/ScryptGroovyTest.groovy
new file mode 100644
index 0000000..416d670
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/scrypt/ScryptGroovyTest.groovy
@@ -0,0 +1,400 @@
+/*
+ * 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.scrypt
+
+import org.apache.commons.codec.binary.Hex
+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 java.security.SecureRandom
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@RunWith(JUnit4.class)
+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
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() throws Exception {
+    }
+
+    @After
+    void tearDown() throws Exception {
+
+    }
+
+    @Test
+    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
+    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
+    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
+    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
+    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
+    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
+    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
+    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
+    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
+    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
+    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
+    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
+    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/2c1f5b49/nifi-commons/nifi-security-utils/src/test/resources/openssl_aes.rb
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/test/resources/openssl_aes.rb b/nifi-commons/nifi-security-utils/src/test/resources/openssl_aes.rb
new file mode 100755
index 0000000..e51dbb7
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/test/resources/openssl_aes.rb
@@ -0,0 +1,46 @@
+#!/usr/bin/env ruby
+
+# 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.
+
+require 'openssl'
+
+def bin_to_hex(s)
+  s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
+end
+
+plaintext = "This is a plaintext message."
+puts "Plaintext: #{plaintext}"
+
+cipher = OpenSSL::Cipher.new 'AES-128-CBC'
+cipher.encrypt
+iv = cipher.random_iv
+
+key_len = cipher.key_len
+digest = OpenSSL::Digest::SHA256.new
+key = digest.digest(plaintext)[0..15]
+
+puts ""
+
+puts "  IV: #{bin_to_hex(iv)} #{iv.length}"
+puts " Key: #{bin_to_hex(key)} #{key.length}"
+cipher.key = key
+
+# Now encrypt the data:
+
+encrypted = cipher.update plaintext
+encrypted << cipher.final
+puts "Cipher text length: #{encrypted.length}"
+puts "Cipher text: #{bin_to_hex(encrypted)}"
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-commons/nifi-security-utils/src/test/resources/openssl_bcrypt.rb
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/test/resources/openssl_bcrypt.rb b/nifi-commons/nifi-security-utils/src/test/resources/openssl_bcrypt.rb
new file mode 100755
index 0000000..bcb62ce
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/test/resources/openssl_bcrypt.rb
@@ -0,0 +1,62 @@
+#!/usr/bin/env ruby
+
+# 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.
+
+require 'openssl'
+require 'base64'
+
+# Run `$ gem install bcrypt` >= 2.1.4
+require 'bcrypt'
+
+def bin_to_hex(s)
+  s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
+end
+
+plaintext = "This is a plaintext message."
+puts "Plaintext: #{plaintext}"
+
+cipher = OpenSSL::Cipher.new 'AES-128-CBC'
+cipher.encrypt
+iv = cipher.random_iv
+
+password = 'thisIsABadPassword'
+puts "Password: #{password} #{password.length}"
+work_factor = 10
+puts "Work factor: #{work_factor}"
+key_len = cipher.key_len
+digest = OpenSSL::Digest::SHA512.new
+
+puts ""
+
+hash = BCrypt::Password.create(password, :cost => work_factor)
+puts "Hash: #{hash}"
+full_salt = hash.salt
+puts "Full Salt: #{full_salt} #{full_salt.length}"
+
+key = (digest.digest hash)[0..key_len - 1]
+salt = Base64.decode64(hash.salt[7..-1])
+
+puts "Salt: #{bin_to_hex(salt)} #{salt.length}"
+puts "  IV: #{bin_to_hex(iv)} #{iv.length}"
+puts " Key: #{bin_to_hex(key)} #{key.length}"
+cipher.key = key
+
+# Now encrypt the data:
+
+encrypted = cipher.update plaintext
+encrypted << cipher.final
+puts "Cipher text length: #{encrypted.length}"
+puts "Cipher text: #{bin_to_hex(encrypted)}"
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-commons/nifi-security-utils/src/test/resources/openssl_pbkdf2.rb
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/test/resources/openssl_pbkdf2.rb b/nifi-commons/nifi-security-utils/src/test/resources/openssl_pbkdf2.rb
new file mode 100755
index 0000000..3e81609
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/test/resources/openssl_pbkdf2.rb
@@ -0,0 +1,52 @@
+#!/usr/bin/env ruby
+
+# 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.
+
+require 'openssl'
+
+def bin_to_hex(s)
+  s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
+end
+
+plaintext = "This is a plaintext message."
+puts "Plaintext: #{plaintext}"
+
+cipher = OpenSSL::Cipher.new 'AES-128-CBC'
+cipher.encrypt
+iv = cipher.random_iv
+
+password = 'thisIsABadPassword'
+puts "Password: #{password} #{password.length}"
+salt = OpenSSL::Random.random_bytes 16
+iterations = 1000
+puts "Iterations: #{iterations}"
+key_len = cipher.key_len
+digest = OpenSSL::Digest::SHA256.new
+
+puts ""
+
+key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_len, digest)
+puts "Salt: #{bin_to_hex(salt)} #{salt.length}"
+puts "  IV: #{bin_to_hex(iv)} #{iv.length}"
+puts " Key: #{bin_to_hex(key)} #{key.length}"
+cipher.key = key
+
+# Now encrypt the data:
+
+encrypted = cipher.update plaintext
+encrypted << cipher.final
+puts "Cipher text length: #{encrypted.length}"
+puts "Cipher text: #{bin_to_hex(encrypted)}"
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-commons/nifi-security-utils/src/test/resources/openssl_scrypt.rb
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/test/resources/openssl_scrypt.rb b/nifi-commons/nifi-security-utils/src/test/resources/openssl_scrypt.rb
new file mode 100755
index 0000000..1031b45
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/test/resources/openssl_scrypt.rb
@@ -0,0 +1,58 @@
+#!/usr/bin/env ruby
+
+# 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.
+
+require 'openssl'
+require 'base64'
+
+# Run `$ gem install scrypt`
+require 'scrypt'
+
+def bin_to_hex(s)
+  s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
+end
+
+plaintext = "This is a plaintext message."
+puts "Plaintext: #{plaintext}"
+
+cipher = OpenSSL::Cipher.new 'AES-128-CBC'
+cipher.encrypt
+iv = cipher.random_iv
+
+password = 'thisIsABadPassword'
+puts "Password: #{password} #{password.length}"
+cost = SCrypt::Engine.calibrate
+puts "Cost: #{cost} (N$r$p$)"
+key_len = cipher.key_len
+
+puts ""
+
+hash = SCrypt::Password.create(password, :cost => cost, :key_len => key_len, :salt_size => 16)
+puts "Hash: #{hash}"
+# These values are already hex-encoded strings unlike the bcrypt and PBKDF2 examples, so unpack them to binary
+salt = [hash.salt].pack('H*')
+key = [hash.digest].pack('H*')
+puts "Salt: #{bin_to_hex(salt)} #{salt.length}"
+puts "  IV: #{bin_to_hex(iv)} #{iv.length}"
+puts " Key: #{bin_to_hex(key)} #{key.length}"
+cipher.key = key
+
+# Now encrypt the data:
+
+encrypted = cipher.update plaintext
+encrypted << cipher.final
+puts "Cipher text length: #{encrypted.length}"
+puts "Cipher text: #{bin_to_hex(encrypted)}"
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE
index 099d25b..22c9ba9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework-nar/src/main/resources/META-INF/NOTICE
@@ -40,10 +40,6 @@ The following binary components are provided under the Apache Software License v
     The following NOTICE information applies:
          Copyright 2006 Envoi Solutions LLC
 
-  (ASLv2) Jasypt
-    The following NOTICE information applies:    
-	  Copyright (c) 2007-2010, The JASYPT team (http://www.jasypt.org)
-
   (ASLv2) Apache Commons Codec
     The following NOTICE information applies:
       Apache Commons Codec

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
index 4989be6..edfa5a3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/pom.xml
@@ -100,10 +100,6 @@
             <artifactId>h2</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.jasypt</groupId>
-            <artifactId>jasypt</artifactId>
-        </dependency>
-        <dependency>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk15on</artifactId>
         </dependency>
@@ -166,7 +162,11 @@
             <artifactId>testng</artifactId>
             <scope>test</scope>
         </dependency>
-
+        <dependency>
+            <groupId>org.jasypt</groupId>
+            <artifactId>jasypt</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-mock</artifactId>

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
index f30a71e..d455043 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/FlowFromDOMFactory.java
@@ -48,7 +48,6 @@ import org.apache.nifi.web.api.dto.ProcessorConfigDTO;
 import org.apache.nifi.web.api.dto.ProcessorDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.ReportingTaskDTO;
-import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Element;
@@ -508,7 +507,7 @@ public class FlowFromDOMFactory {
         if (value != null && value.startsWith(FlowSerializer.ENC_PREFIX) && value.endsWith(FlowSerializer.ENC_SUFFIX)) {
             try {
                 return encryptor.decrypt(value.substring(FlowSerializer.ENC_PREFIX.length(), value.length() - FlowSerializer.ENC_SUFFIX.length()));
-            } catch (EncryptionException | EncryptionOperationNotPossibleException e) {
+            } catch (EncryptionException e) {
                 final String moreDescriptiveMessage = "There was a problem decrypting a sensitive flow configuration value. " +
                         "Check that the nifi.sensitive.props.key value in nifi.properties matches the value used to encrypt the flow.xml.gz file";
                 logger.error(moreDescriptiveMessage, e);

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
index dc0e7c7..2f18a16 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/encrypt/StringEncryptor.java
@@ -16,23 +16,43 @@
  */
 package org.apache.nifi.encrypt;
 
+import java.nio.charset.StandardCharsets;
+import java.security.Provider;
+import java.security.SecureRandom;
 import java.security.Security;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import javax.crypto.Cipher;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.apache.nifi.security.util.KeyDerivationFunction;
+import org.apache.nifi.security.util.crypto.CipherProvider;
+import org.apache.nifi.security.util.crypto.CipherProviderFactory;
+import org.apache.nifi.security.util.crypto.CipherUtility;
+import org.apache.nifi.security.util.crypto.KeyedCipherProvider;
+import org.apache.nifi.security.util.crypto.NiFiLegacyCipherProvider;
+import org.apache.nifi.security.util.crypto.PBECipherProvider;
 import org.apache.nifi.util.NiFiProperties;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
-import org.jasypt.exceptions.EncryptionInitializationException;
-import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
+import org.bouncycastle.util.encoders.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * <p>
  * An application specific string encryptor that collects configuration from the
  * application properties, system properties, and/or system environment.
  * </p>
- *
+ * <p>
  * <p>
  * Instance of this class are thread-safe</p>
- *
+ * <p>
  * <p>
  * The encryption provider and algorithm is configured using the application
  * properties:
@@ -41,95 +61,298 @@ import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
  * <li>nifi.sensitive.props.algorithm</li>
  * </ul>
  * </p>
- *
+ * <p>
  * <p>
  * The encryptor's password may be set by configuring the below property:
  * <ul>
  * <li>nifi.sensitive.props.key</li>
  * </ul>
  * </p>
- *
  */
-public final class StringEncryptor {
+public class StringEncryptor {
+    private static final Logger logger = LoggerFactory.getLogger(StringEncryptor.class);
+
+    private static final List<String> SUPPORTED_ALGORITHMS = new ArrayList<>();
+    private static final List<String> SUPPORTED_PROVIDERS = new ArrayList<>();
+
+    private final String algorithm;
+    private final String provider;
+    private final PBEKeySpec password;
+    private final SecretKeySpec key;
+
+    private String encoding = "HEX";
+
+    private CipherProvider cipherProvider;
 
     static {
         Security.addProvider(new BouncyCastleProvider());
+
+        for (EncryptionMethod em : EncryptionMethod.values()) {
+            SUPPORTED_ALGORITHMS.add(em.getAlgorithm());
+        }
+        logger.debug("Supported encryption algorithms: " + StringUtils.join(SUPPORTED_ALGORITHMS, "\n"));
+
+        for (Provider provider : Security.getProviders()) {
+            SUPPORTED_PROVIDERS.add(provider.getName());
+        }
+        logger.debug("Supported providers: " + StringUtils.join(SUPPORTED_PROVIDERS, "\n"));
     }
 
     public static final String NF_SENSITIVE_PROPS_KEY = "nifi.sensitive.props.key";
     public static final String NF_SENSITIVE_PROPS_ALGORITHM = "nifi.sensitive.props.algorithm";
     public static final String NF_SENSITIVE_PROPS_PROVIDER = "nifi.sensitive.props.provider";
     private static final String DEFAULT_SENSITIVE_PROPS_KEY = "nififtw!";
-    private static final String TEST_PLAINTEXT = "this is a test";
-    private final StandardPBEStringEncryptor encryptor;
 
-    private StringEncryptor(final String aglorithm, final String provider, final String key) {
-        encryptor = new StandardPBEStringEncryptor();
-        encryptor.setAlgorithm(aglorithm);
-        encryptor.setProviderName(provider);
-        encryptor.setPassword(key);
-        encryptor.setStringOutputType("hexadecimal");
-        encryptor.initialize();
+    /**
+     * This constructor creates an encryptor using <em>Password-Based Encryption</em> (PBE). The <em>key</em> value is the direct value provided in <code>nifi.sensitive.props.key</code> in
+     * <code>nifi.properties</code>, which is a <em>PASSWORD</em> rather than a <em>KEY</em>, but is named such for backward/legacy logical compatibility throughout the rest of the codebase.
+     * <p>
+     * For actual raw key provision, see {@link #StringEncryptor(String, String, byte[])}.
+     *
+     * @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
+     * @param provider  the JCA Security provider ({@link EncryptionMethod#provider})
+     * @param key       the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
+     */
+    protected StringEncryptor(final String algorithm, final String provider, final String key) {
+        this.algorithm = algorithm;
+        this.provider = provider;
+        this.key = null;
+        this.password = new PBEKeySpec(key == null
+                ? DEFAULT_SENSITIVE_PROPS_KEY.toCharArray()
+                : key.toCharArray());
+        initialize();
+    }
+
+    /**
+     * This constructor creates an encryptor using <em>Keyed Encryption</em>. The <em>key</em> value is the raw byte value of a symmetric encryption key
+     * (usually expressed for human-readability/transmission in hexadecimal or Base64 encoded format).
+     *
+     * @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#algorithm})
+     * @param provider  the JCA Security provider ({@link EncryptionMethod#provider})
+     * @param key       a raw encryption key in bytes
+     */
+    public StringEncryptor(final String algorithm, final String provider, final byte[] key) {
+        this.algorithm = algorithm;
+        this.provider = provider;
+        this.key = new SecretKeySpec(key, extractKeyTypeFromAlgorithm(algorithm));
+        this.password = null;
+        initialize();
+    }
+
+    /**
+     * A default constructor for mocking during testing.
+     */
+    protected StringEncryptor() {
+        this.algorithm = null;
+        this.provider = null;
+        this.key = null;
+        this.password = null;
     }
 
     /**
-     * Creates an instance of the nifi sensitive property encryptor. Validates
-     * that the encryptor is actually working.
+     * Extracts the cipher "family" (i.e. "AES", "DES", "RC4") from the full algorithm name.
+     *
+     * @param algorithm the algorithm ({@link EncryptionMethod#algorithm})
+     * @return the cipher family
+     * @throws EncryptionException if the algorithm is null/empty or not supported
+     */
+    private String extractKeyTypeFromAlgorithm(String algorithm) throws EncryptionException {
+        if (StringUtils.isBlank(algorithm)) {
+            throw new EncryptionException("The algorithm cannot be null or empty");
+        }
+        String parsedCipher = CipherUtility.parseCipherFromAlgorithm(algorithm);
+        if (parsedCipher.equals(algorithm)) {
+            throw new EncryptionException("No supported algorithm detected");
+        } else {
+            return parsedCipher;
+        }
+    }
+
+    /**
+     * Creates an instance of the NiFi sensitive property encryptor.
      *
      * @param niFiProperties properties
      * @return encryptor
      * @throws EncryptionException if any issues arise initializing or
-     * validating the encryptor
+     *                             validating the encryptor
+     * @see #createEncryptor(String, String, String)
+     * @deprecated as of NiFi 1.4.0 because the entire {@link NiFiProperties} object is not necessary to generate the encryptor.
      */
+    @Deprecated
     public static StringEncryptor createEncryptor(final NiFiProperties niFiProperties) throws EncryptionException {
 
-        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+        // Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
 
         final String sensitivePropAlgorithmVal = niFiProperties.getProperty(NF_SENSITIVE_PROPS_ALGORITHM);
         final String sensitivePropProviderVal = niFiProperties.getProperty(NF_SENSITIVE_PROPS_PROVIDER);
         final String sensitivePropValueNifiPropVar = niFiProperties.getProperty(NF_SENSITIVE_PROPS_KEY, DEFAULT_SENSITIVE_PROPS_KEY);
 
-        if (StringUtils.isBlank(sensitivePropAlgorithmVal)) {
-            throw new EncryptionException(NF_SENSITIVE_PROPS_ALGORITHM + "must bet set");
+        return createEncryptor(sensitivePropAlgorithmVal, sensitivePropProviderVal, sensitivePropValueNifiPropVar);
+    }
+
+    /**
+     * Creates an instance of the NiFi sensitive property encryptor.
+     *
+     * @param algorithm the encryption (and key derivation) algorithm ({@link EncryptionMethod#algorithm})
+     * @param provider  the JCA Security provider ({@link EncryptionMethod#provider})
+     * @param password  the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
+     * @return the initialized encryptor
+     */
+    public static StringEncryptor createEncryptor(String algorithm, String provider, String password) {
+        if (StringUtils.isBlank(algorithm)) {
+            throw new EncryptionException(NF_SENSITIVE_PROPS_ALGORITHM + " must be set");
         }
 
-        if (StringUtils.isBlank(sensitivePropProviderVal)) {
-            throw new EncryptionException(NF_SENSITIVE_PROPS_PROVIDER + "must bet set");
+        if (StringUtils.isBlank(provider)) {
+            throw new EncryptionException(NF_SENSITIVE_PROPS_PROVIDER + " must be set");
         }
 
-        if (StringUtils.isBlank(sensitivePropValueNifiPropVar)) {
-            throw new EncryptionException(NF_SENSITIVE_PROPS_KEY + "must bet set");
+        if (StringUtils.isBlank(password)) {
+            throw new EncryptionException(NF_SENSITIVE_PROPS_KEY + " must be set");
         }
 
-        final StringEncryptor nifiEncryptor;
-        try {
-            nifiEncryptor = new StringEncryptor(sensitivePropAlgorithmVal, sensitivePropProviderVal, sensitivePropValueNifiPropVar);
-            //test that we can infact encrypt and decrypt something
-            if (!nifiEncryptor.decrypt(nifiEncryptor.encrypt(TEST_PLAINTEXT)).equals(TEST_PLAINTEXT)) {
-                throw new EncryptionException("NiFi property encryptor does appear to be working - decrypt/encrypt return invalid results");
+        return new StringEncryptor(algorithm, provider, password);
+    }
+
+    protected void initialize() {
+        if (isInitialized()) {
+            logger.debug("Attempted to initialize an already-initialized StringEncryptor");
+            return;
+        }
+
+        if (paramsAreValid()) {
+            if (CipherUtility.isPBECipher(algorithm)) {
+                cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NIFI_LEGACY);
+            } else {
+                cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
             }
+        } else {
+            throw new EncryptionException("Cannot initialize the StringEncryptor because some configuration values are invalid");
+        }
+    }
 
-        } catch (final EncryptionInitializationException | EncryptionOperationNotPossibleException ex) {
-            throw new EncryptionException("Cannot initialize sensitive property encryptor", ex);
+    private boolean paramsAreValid() {
+        boolean algorithmAndProviderValid = algorithmIsValid(algorithm) && providerIsValid(provider);
+        boolean secretIsValid = false;
+        if (CipherUtility.isPBECipher(algorithm)) {
+            secretIsValid = passwordIsValid(password);
+        } else if (CipherUtility.isKeyedCipher(algorithm)) {
+            secretIsValid = keyIsValid(key, algorithm);
+        }
+
+        return algorithmAndProviderValid && secretIsValid;
+    }
+
+    private boolean keyIsValid(SecretKeySpec key, String algorithm) {
+        return key != null && CipherUtility.getValidKeyLengthsForAlgorithm(algorithm).contains(key.getEncoded().length * 8);
+    }
+
+    private boolean passwordIsValid(PBEKeySpec password) {
+        try {
+            return password.getPassword() != null;
+        } catch (IllegalStateException | NullPointerException e) {
+            return false;
+        }
+    }
 
+    public void setEncoding(String base) {
+        if ("HEX".equalsIgnoreCase(base)) {
+            this.encoding = "HEX";
+        } else if ("BASE64".equalsIgnoreCase(base)) {
+            this.encoding = "BASE64";
+        } else {
+            throw new IllegalArgumentException("The encoding base must be 'HEX' or 'BASE64'");
         }
-        return nifiEncryptor;
     }
 
     /**
      * Encrypts the given clear text.
      *
      * @param clearText the message to encrypt
-     *
      * @return the cipher text
-     *
      * @throws EncryptionException if the encrypt fails
      */
     public String encrypt(String clearText) throws EncryptionException {
         try {
-            return encryptor.encrypt(clearText);
-        } catch (final EncryptionOperationNotPossibleException | EncryptionInitializationException eonpe) {
-            throw new EncryptionException(eonpe);
+            if (isInitialized()) {
+                byte[] rawBytes;
+                if (CipherUtility.isPBECipher(algorithm)) {
+                    rawBytes = encryptPBE(clearText);
+                } else {
+                    rawBytes = encryptKeyed(clearText);
+                }
+                return encode(rawBytes);
+            } else {
+                throw new EncryptionException("The encryptor is not initialized");
+            }
+        } catch (final Exception e) {
+            throw new EncryptionException(e);
+        }
+    }
+
+    private byte[] encryptPBE(String plaintext) {
+        PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
+        final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
+
+        // Generate salt
+        byte[] salt;
+        // NiFi legacy code determined the salt length based on the cipher block size
+        if (pbecp instanceof NiFiLegacyCipherProvider) {
+            salt = ((NiFiLegacyCipherProvider) pbecp).generateSalt(encryptionMethod);
+        } else {
+            salt = pbecp.generateSalt();
+        }
+
+        // Determine necessary key length
+        int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
+
+        // Generate cipher
+        try {
+            Cipher cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, true);
+
+            // Write IV if necessary (allows for future use of PBKDF2, Bcrypt, or Scrypt)
+            // byte[] iv = new byte[0];
+            // if (cipherProvider instanceof RandomIVPBECipherProvider) {
+            //     iv = cipher.getIV();
+            // }
+
+            // Encrypt the plaintext
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
+
+            // Combine the output
+            // byte[] rawBytes = CryptoUtils.concatByteArrays(salt, iv, cipherBytes);
+            return CryptoUtils.concatByteArrays(salt, cipherBytes);
+        } catch (Exception e) {
+            throw new EncryptionException("Could not encrypt sensitive value", e);
+        }
+    }
+
+    private byte[] encryptKeyed(String plaintext) {
+        KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
+
+        // Generate cipher
+        try {
+            SecureRandom sr = new SecureRandom();
+            byte[] iv = new byte[16];
+            sr.nextBytes(iv);
+
+            Cipher cipher = keyedcp.getCipher(EncryptionMethod.forAlgorithm(algorithm), key, iv, true);
+
+            // Encrypt the plaintext
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
+
+            // Combine the output
+            return CryptoUtils.concatByteArrays(iv, cipherBytes);
+        } catch (Exception e) {
+            throw new EncryptionException("Could not encrypt sensitive value", e);
+        }
+    }
+
+    private String encode(byte[] rawBytes) {
+        if (this.encoding.equalsIgnoreCase("HEX")) {
+            return Hex.encodeHexString(rawBytes);
+        } else {
+            return Base64.toBase64String(rawBytes);
         }
     }
 
@@ -137,17 +360,96 @@ public final class StringEncryptor {
      * Decrypts the given cipher text.
      *
      * @param cipherText the message to decrypt
-     *
      * @return the clear text
-     *
      * @throws EncryptionException if the decrypt fails
      */
     public String decrypt(String cipherText) throws EncryptionException {
         try {
-            return encryptor.decrypt(cipherText);
-        } catch (final EncryptionOperationNotPossibleException | EncryptionInitializationException eonpe) {
-            throw new EncryptionException(eonpe);
+            if (isInitialized()) {
+                byte[] plainBytes;
+                byte[] cipherBytes = decode(cipherText);
+                if (CipherUtility.isPBECipher(algorithm)) {
+                    plainBytes = decryptPBE(cipherBytes);
+                } else {
+                    plainBytes = decryptKeyed(cipherBytes);
+                }
+                return new String(plainBytes, StandardCharsets.UTF_8);
+            } else {
+                throw new EncryptionException("The encryptor is not initialized");
+            }
+        } catch (final Exception e) {
+            throw new EncryptionException(e);
+        }
+    }
+
+    private byte[] decryptPBE(byte[] cipherBytes) throws DecoderException {
+        PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
+        final EncryptionMethod encryptionMethod = EncryptionMethod.forAlgorithm(algorithm);
+
+        // Extract salt
+        int saltLength = CipherUtility.getSaltLengthForAlgorithm(algorithm);
+        byte[] salt = new byte[saltLength];
+        System.arraycopy(cipherBytes, 0, salt, 0, saltLength);
+
+        byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, saltLength, cipherBytes.length);
+
+        // Determine necessary key length
+        int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
+
+        // Generate cipher
+        try {
+            Cipher cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false);
+
+            // Write IV if necessary (allows for future use of PBKDF2, Bcrypt, or Scrypt)
+            // byte[] iv = new byte[0];
+            // if (cipherProvider instanceof RandomIVPBECipherProvider) {
+            //     iv = cipher.getIV();
+            // }
+
+            // Decrypt the plaintext
+            return cipher.doFinal(actualCipherBytes);
+        } catch (Exception e) {
+            throw new EncryptionException("Could not decrypt sensitive value", e);
         }
     }
 
+    private byte[] decryptKeyed(byte[] cipherBytes) {
+        KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
+
+        // Generate cipher
+        try {
+            int ivLength = 16;
+            byte[] iv = new byte[ivLength];
+            System.arraycopy(cipherBytes, 0, iv, 0, ivLength);
+
+            byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, ivLength, cipherBytes.length);
+
+            Cipher cipher = keyedcp.getCipher(EncryptionMethod.forAlgorithm(algorithm), key, iv, false);
+
+            // Encrypt the plaintext
+            return cipher.doFinal(actualCipherBytes);
+        } catch (Exception e) {
+            throw new EncryptionException("Could not decrypt sensitive value", e);
+        }
+    }
+
+    private byte[] decode(String encoded) throws DecoderException {
+        if (this.encoding.equalsIgnoreCase("HEX")) {
+            return Hex.decodeHex(encoded.toCharArray());
+        } else {
+            return Base64.decode(encoded);
+        }
+    }
+
+    public boolean isInitialized() {
+        return this.cipherProvider != null;
+    }
+
+    protected static boolean algorithmIsValid(String algorithm) {
+        return SUPPORTED_ALGORITHMS.contains(algorithm);
+    }
+
+    protected static boolean providerIsValid(String provider) {
+        return SUPPORTED_PROVIDERS.contains(provider);
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy
new file mode 100644
index 0000000..37e0302
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/groovy/org/apache/nifi/encrypt/StringEncryptorTest.groovy
@@ -0,0 +1,482 @@
+/*
+ * 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.encrypt
+
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.properties.StandardNiFiProperties
+import org.apache.nifi.security.kms.CryptoUtils
+import org.apache.nifi.security.util.EncryptionMethod
+import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider
+import org.apache.nifi.security.util.crypto.CipherUtility
+import org.apache.nifi.security.util.crypto.KeyedCipherProvider
+import org.apache.nifi.util.NiFiProperties
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.jasypt.encryption.pbe.StandardPBEStringEncryptor
+import org.jasypt.encryption.pbe.config.PBEConfig
+import org.jasypt.salt.SaltGenerator
+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.IvParameterSpec
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.PBEParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import java.security.SecureRandom
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@RunWith(JUnit4.class)
+class StringEncryptorTest {
+    private static final Logger logger = LoggerFactory.getLogger(StringEncryptorTest.class)
+
+    private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
+
+    private static final List<EncryptionMethod> keyedEncryptionMethods = EncryptionMethod.values().findAll {
+        it.keyedCipher
+    }
+    private static final List<EncryptionMethod> pbeEncryptionMethods = EncryptionMethod.values().findAll {
+        it.algorithm =~ "PBE"
+    }
+
+    // Unlimited elements are removed in static initializer
+    private static final List<EncryptionMethod> limitedPbeEncryptionMethods = pbeEncryptionMethods
+
+    private static final SecretKey key = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
+
+    private static final String KEY = "nifi.sensitive.props.key"
+    private static final String ALGORITHM = "nifi.sensitive.props.algorithm"
+    private static final String PROVIDER = "nifi.sensitive.props.provider"
+
+    private static final String DEFAULT_ALGORITHM = "PBEWITHMD5AND128BITAES-CBC-OPENSSL"
+    private static final String DEFAULT_PROVIDER = "BC"
+    private static final String DEFAULT_PASSWORD = "nififtw!"
+    private static final String OTHER_PASSWORD = "thisIsABadPassword"
+    private static
+    final Map RAW_PROPERTIES = [(ALGORITHM): DEFAULT_ALGORITHM, (PROVIDER): DEFAULT_PROVIDER, (KEY): DEFAULT_PASSWORD]
+    private static final NiFiProperties STANDARD_PROPERTIES = new StandardNiFiProperties(new Properties(RAW_PROPERTIES))
+
+    private static final byte[] DEFAULT_SALT = new byte[8]
+    private static final byte[] DEFAULT_IV = new byte[16]
+    private static final int DEFAULT_ITERATION_COUNT = 0
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        limitedPbeEncryptionMethods.removeAll { it.algorithm =~ "SHA.*(CBC)?"}
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() throws Exception {
+    }
+
+    @After
+    void tearDown() throws Exception {
+    }
+
+    private static boolean isUnlimitedStrengthCryptoAvailable() {
+        Cipher.getMaxAllowedKeyLength("AES") > 128
+    }
+
+    private
+    static Cipher generatePBECipher(boolean encryptMode, EncryptionMethod em = EncryptionMethod.MD5_128AES, String password = DEFAULT_PASSWORD, byte[] salt = DEFAULT_SALT, int iterationCount = DEFAULT_ITERATION_COUNT) {
+        // Initialize secret key from password
+        final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray())
+        final SecretKeyFactory factory = SecretKeyFactory.getInstance(em.algorithm, em.provider)
+        SecretKey tempKey = factory.generateSecret(pbeKeySpec)
+
+        final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, iterationCount)
+        Cipher cipher = Cipher.getInstance(em.algorithm, em.provider)
+        cipher.init((encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, tempKey, parameterSpec)
+        cipher
+    }
+
+    private
+    static Cipher generateKeyedCipher(boolean encryptMode, EncryptionMethod em = EncryptionMethod.MD5_128AES, String keyHex = KEY_HEX, byte[] iv = DEFAULT_IV) {
+        SecretKey tempKey = new SecretKeySpec(Hex.decodeHex(keyHex as char[]), CipherUtility.parseCipherFromAlgorithm(em.algorithm))
+
+        IvParameterSpec ivSpec = new IvParameterSpec(iv)
+        Cipher cipher = Cipher.getInstance(em.algorithm, em.provider)
+        cipher.init((encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, tempKey, ivSpec)
+        cipher
+    }
+
+    @Test
+    void testPBEncryptionShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        final String plaintext = "This is a plaintext message."
+
+        // Act
+        for (EncryptionMethod em : limitedPbeEncryptionMethods) {
+            logger.info("Using algorithm: ${em.getAlgorithm()}")
+            NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
+            StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
+
+            String cipherText = encryptor.encrypt(plaintext)
+            logger.info("Cipher text: ${cipherText}")
+
+            String recovered = encryptor.decrypt(cipherText)
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext == recovered
+        }
+    }
+
+    @Test
+    void testPBEncryptionShouldBeExternallyConsistent() throws Exception {
+        // Arrange
+        final String plaintext = "This is a plaintext message."
+
+        for (EncryptionMethod em : pbeEncryptionMethods) {
+
+            // Hard-coded 0x00 * 16
+            byte[] salt = new byte[16]
+            int iterationCount = DEFAULT_ITERATION_COUNT
+            // DES/RC* algorithms use 8 byte salts and custom iteration counts
+            if (em.algorithm =~ "DES|RC") {
+                salt = new byte[8]
+                iterationCount = 1000
+            } else if (em.algorithm =~ "SHAA|SHA256") {
+                // SHA-1/-256 use 16 byte salts but custom iteration counts
+                iterationCount = 1000
+            }
+            logger.info("Using algorithm: ${em.getAlgorithm()} with ${salt.length} byte salt and ${iterationCount} iterations")
+
+            // Encrypt the value manually
+            Cipher cipher = generatePBECipher(true, em, DEFAULT_PASSWORD, salt, iterationCount)
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.bytes)
+            byte[] saltAndCipherBytes = CryptoUtils.concatByteArrays(salt, cipherBytes)
+            String cipherTextHex = Hex.encodeHexString(saltAndCipherBytes)
+            logger.info("Cipher text: ${cipherTextHex}")
+
+            NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
+            StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
+
+            // Act
+            String recovered = encryptor.decrypt(cipherTextHex)
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext == recovered
+        }
+    }
+
+    /**
+     * This test uses the Jasypt library {@see StandardPBEStringEncryptor} to encrypt raw messages as the legacy (pre-1.4.0) NiFi application did. Then the messages are decrypted with the "new"/current primitive implementation to ensure backward compatibility. This test method only exercises limited strength key sizes (even this is not technically accurate as the SHA KDF is restricted even when using 128-bit AES).
+     *
+     * @throws Exception
+     */
+    @Test
+    void testLimitedPBEncryptionShouldBeConsistentWithLegacyEncryption() throws Exception {
+        // Arrange
+        final String plaintext = "This is a plaintext message."
+
+        for (EncryptionMethod em : limitedPbeEncryptionMethods) {
+
+            // Hard-coded 0x00 * 16
+            byte[] salt = new byte[16]
+            // DES/RC* algorithms use 8 byte salts
+            if (em.algorithm =~ "DES|RC") {
+                salt = new byte[8]
+            }
+            logger.info("Using algorithm: ${em.getAlgorithm()} with ${salt.length} byte salt")
+
+            StandardPBEStringEncryptor legacyEncryptor = new StandardPBEStringEncryptor()
+            SaltGenerator mockSaltGenerator = [generateSalt: { int l ->
+                logger.mock("Generating ${l} byte salt")
+                new byte[l]
+            }, includePlainSaltInEncryptionResults         : {
+                -> true
+            }] as SaltGenerator
+            PBEConfig mockConfig = [getAlgorithm             : { -> em.algorithm },
+                                    getPassword              : { -> DEFAULT_PASSWORD },
+                                    getKeyObtentionIterations: { -> 1000 },
+                                    getProviderName          : { -> em.provider },
+                                    getProvider              : { -> new BouncyCastleProvider() },
+                                    getSaltGenerator         : { -> mockSaltGenerator }
+            ] as PBEConfig
+            legacyEncryptor.setConfig(mockConfig)
+            legacyEncryptor.setStringOutputType("hexadecimal")
+
+            String cipherText = legacyEncryptor.encrypt(plaintext)
+            logger.info("Cipher text: ${cipherText}")
+
+            NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
+            StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
+
+            // Act
+            String recovered = encryptor.decrypt(cipherText)
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext == recovered
+        }
+    }
+
+    /**
+     * This test uses the Jasypt library {@see StandardPBEStringEncryptor} to encrypt raw messages as the legacy (pre-1.4.0) NiFi application did. Then the messages are decrypted with the "new"/current primitive implementation to ensure backward compatibility. This test method exercises all strength key sizes.
+     *
+     * @throws Exception
+     */
+    @Test
+    void testPBEncryptionShouldBeConsistentWithLegacyEncryption() throws Exception {
+        // Arrange
+        Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", isUnlimitedStrengthCryptoAvailable())
+
+        final String plaintext = "This is a plaintext message."
+
+        for (EncryptionMethod em : pbeEncryptionMethods) {
+
+            // Hard-coded 0x00 * 16
+            byte[] salt = new byte[16]
+            // DES/RC* algorithms use 8 byte salts
+            if (em.algorithm =~ "DES|RC") {
+                salt = new byte[8]
+            }
+            logger.info("Using algorithm: ${em.getAlgorithm()} with ${salt.length} byte salt")
+
+            StandardPBEStringEncryptor legacyEncryptor = new StandardPBEStringEncryptor()
+            SaltGenerator mockSaltGenerator = [generateSalt: { int l ->
+                logger.mock("Generating ${l} byte salt")
+                new byte[l]
+            }, includePlainSaltInEncryptionResults         : {
+                -> true
+            }] as SaltGenerator
+            PBEConfig mockConfig = [getAlgorithm             : { -> em.algorithm },
+                                    getPassword              : { -> DEFAULT_PASSWORD },
+                                    getKeyObtentionIterations: { -> 1000 },
+                                    getProviderName          : { -> em.provider },
+                                    getProvider              : { -> new BouncyCastleProvider() },
+                                    getSaltGenerator         : { -> mockSaltGenerator }
+            ] as PBEConfig
+            legacyEncryptor.setConfig(mockConfig)
+            legacyEncryptor.setStringOutputType("hexadecimal")
+
+            String cipherText = legacyEncryptor.encrypt(plaintext)
+            logger.info("Cipher text: ${cipherText}")
+
+            NiFiProperties niFiProperties = new StandardNiFiProperties(new Properties(RAW_PROPERTIES + [(ALGORITHM): em.algorithm]))
+            StringEncryptor encryptor = StringEncryptor.createEncryptor(niFiProperties)
+
+            // Act
+            String recovered = encryptor.decrypt(cipherText)
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext == recovered
+        }
+    }
+
+    @Test
+    void testKeyedEncryptionShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        final String plaintext = "This is a plaintext message."
+
+        // Act
+        for (EncryptionMethod em : keyedEncryptionMethods) {
+            logger.info("Using algorithm: ${em.getAlgorithm()}")
+            StringEncryptor encryptor = new StringEncryptor(em.algorithm, em.provider, Hex.decodeHex(KEY_HEX as char[]))
+
+            String cipherText = encryptor.encrypt(plaintext)
+            logger.info("Cipher text: ${cipherText}")
+
+            String recovered = encryptor.decrypt(cipherText)
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext == recovered
+        }
+    }
+
+    @Test
+    void testKeyedEncryptionShouldBeExternallyConsistent() throws Exception {
+        // Arrange
+        final String plaintext = "This is a plaintext message."
+
+        for (EncryptionMethod em : keyedEncryptionMethods) {
+            // IV is actually used for keyed encryption
+            byte[] iv = Hex.decodeHex(("AA" * 16) as char[])
+            logger.info("Using algorithm: ${em.getAlgorithm()} with ${iv.length} byte IV")
+
+            // Encrypt the value manually
+            Cipher cipher = generateKeyedCipher(true, em, KEY_HEX, iv)
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.bytes)
+            byte[] ivAndCipherBytes = CryptoUtils.concatByteArrays(iv, cipherBytes)
+            String cipherTextHex = Hex.encodeHexString(ivAndCipherBytes)
+            logger.info("Cipher text: ${cipherTextHex}")
+
+            StringEncryptor encryptor = new StringEncryptor(em.algorithm, em.provider, Hex.decodeHex(KEY_HEX.chars))
+
+            // Act
+            String recovered = encryptor.decrypt(cipherTextHex)
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext == recovered
+        }
+    }
+
+    @Test
+    void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+        final String plaintext = "This is a plaintext message."
+
+        // Act
+        keyedEncryptionMethods.each { EncryptionMethod em ->
+            logger.info("Using algorithm: ${em.getAlgorithm()}")
+            byte[] iv = cipherProvider.generateIV()
+            logger.info("IV: ${Hex.encodeHexString(iv)}")
+
+            // Initialize a cipher for encryption
+            Cipher cipher = cipherProvider.getCipher(em, key, iv, true)
+
+            byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
+            logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
+
+            cipher = cipherProvider.getCipher(em, key, iv, false)
+            byte[] recoveredBytes = cipher.doFinal(cipherBytes)
+            String recovered = new String(recoveredBytes, "UTF-8")
+            logger.info("Recovered: ${recovered}")
+
+            // Assert
+            assert plaintext.equals(recovered)
+        }
+    }
+
+    @Test
+    void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
+        // Arrange
+        Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.", isUnlimitedStrengthCryptoAvailable())
+
+        KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+        final List<Integer> LONG_KEY_LENGTHS = [192, 256]
+
+        final String plaintext = "This is a plaintext message."
+
+        SecureRandom secureRandom = new SecureRandom()
+
+        // Act
+        keyedEncryptionMethods.each { EncryptionMethod em ->
+            // Re-use the same IV for the different length keys to ensure the encryption is different
+            byte[] iv = cipherProvider.generateIV()
+            logger.info("IV: ${Hex.encodeHexString(iv)}")
+
+            LONG_KEY_LENGTHS.each { int keyLength ->
+                logger.info("Using algorithm: ${em.getAlgorithm()} with key length ${keyLength}")
+
+                // Generate a key
+                byte[] keyBytes = new byte[keyLength / 8]
+                secureRandom.nextBytes(keyBytes)
+                SecretKey localKey = new SecretKeySpec(keyBytes, "AES")
+                logger.info("Key: ${Hex.encodeHexString(keyBytes)} ${keyBytes.length}")
+
+                // Initialize a cipher for encryption
+                Cipher cipher = cipherProvider.getCipher(em, localKey, iv, true)
+
+                byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
+                logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
+
+                cipher = cipherProvider.getCipher(em, localKey, iv, false)
+                byte[] recoveredBytes = cipher.doFinal(cipherBytes)
+                String recovered = new String(recoveredBytes, "UTF-8")
+                logger.info("Recovered: ${recovered}")
+
+                // Assert
+                assert plaintext.equals(recovered)
+            }
+        }
+    }
+
+    @Test
+    void testStringEncryptorShouldNotBeFinal() throws Exception {
+        // Arrange
+        final String plaintext = "This is a plaintext message."
+
+        StringEncryptor mockEncryptor = [encrypt: { String pt -> pt.reverse() },
+                                         decrypt: { String ct -> ct.reverse() }] as StringEncryptor
+
+        // Act
+        String cipherText = mockEncryptor.encrypt(plaintext)
+        logger.info("Encrypted ${plaintext} to ${cipherText}")
+        String recovered = mockEncryptor.decrypt(cipherText)
+        logger.info("Decrypted ${cipherText} to ${recovered}")
+
+        // Assert
+        assert recovered == plaintext
+        assert cipherText != plaintext
+    }
+
+    @Test
+    void testStringEncryptorShouldNotOperateIfNotInitialized() throws Exception {
+        // Arrange
+        final String plaintext = "This is a plaintext message."
+
+        StringEncryptor uninitializedEncryptor = new StringEncryptor()
+
+        // Act
+        def encryptMsg = shouldFail(EncryptionException) {
+            String cipherText = uninitializedEncryptor.encrypt(plaintext)
+            logger.info("Encrypted ${plaintext} to ${cipherText}")
+        }
+        def decryptMsg = shouldFail(EncryptionException) {
+            String recovered = uninitializedEncryptor.decrypt(plaintext)
+            logger.info("Decrypted ${plaintext} to ${recovered}")
+        }
+
+        // Assert
+        assert encryptMsg =~ "encryptor is not initialized"
+        assert decryptMsg =~ "encryptor is not initialized"
+    }
+
+    @Test
+    void testStringEncryptorShouldDetermineIfInitialized() throws Exception {
+        // Arrange
+        StringEncryptor uninitializedEncryptor = new StringEncryptor()
+        EncryptionMethod em = EncryptionMethod.MD5_128AES
+        StringEncryptor initializedEncryptor = new StringEncryptor(em.algorithm, em.provider, DEFAULT_PASSWORD)
+
+        // Act
+        boolean uninitializedIsInitialized = uninitializedEncryptor.isInitialized()
+        logger.info("Uninitialized encryptor is initialized: ${uninitializedIsInitialized}")
+        boolean initializedIsInitialized = initializedEncryptor.isInitialized()
+        logger.info("Initialized encryptor is initialized: ${initializedIsInitialized}")
+
+        // Assert
+        assert !uninitializedIsInitialized
+        assert initializedIsInitialized
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/logback-test.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/logback-test.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/logback-test.xml
index 5a64b9f..fd00fef 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/logback-test.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/resources/logback-test.xml
@@ -30,6 +30,7 @@
     
     
     <logger name="org.apache.nifi" level="INFO"/>
+    <logger name="org.apache.nifi.encrypt" level="DEBUG"/>
     <logger name="org.apache.nifi.controller.service.mock" level="ERROR"/>
     <logger name="org.apache.nifi.controller.service.StandardControllerServiceProvider" level="WARN" />
 

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml
index 3440f68..bcaa99e 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml
@@ -508,7 +508,7 @@
                         <exclude>src/test/resources/TestUpdateRecord/schema/person-with-name-string-fields.avsc</exclude>
                         <exclude>src/test/resources/TestUpdateRecord/schema/name-fields-only.avsc</exclude>
                         <exclude>src/test/resources/TestUpdateRecord/schema/person-with-name-and-mother.avsc</exclude>
-                        <!-- This file is copied from https://github.com/jeremyh/jBCrypt 
+                        <!-- This file is copied from https://github.com/jeremyh/jBCrypt
                             because the binary is compiled for Java 8 and we must support Java 7 -->
                         <exclude>src/main/java/org/apache/nifi/security/util/crypto/bcrypt/BCrypt.java</exclude>
                     </excludes>

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java
deleted file mode 100644
index f28cde9..0000000
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/BcryptCipherProvider.java
+++ /dev/null
@@ -1,176 +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.security.util.crypto;
-
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.util.Arrays;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.processor.exception.ProcessException;
-import org.apache.nifi.security.util.EncryptionMethod;
-import org.apache.nifi.security.util.crypto.bcrypt.BCrypt;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class BcryptCipherProvider extends RandomIVPBECipherProvider {
-    private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProvider.class);
-
-    private final int workFactor;
-    /**
-     * This can be calculated automatically using the code {@see BcryptCipherProviderGroovyTest#calculateMinimumWorkFactor} or manually updated by a maintainer
-     */
-    private static final int DEFAULT_WORK_FACTOR = 12;
-    private static final int DEFAULT_SALT_LENGTH = 16;
-
-    private static final Pattern BCRYPT_SALT_FORMAT = Pattern.compile("^\\$\\d\\w\\$\\d{2}\\$[\\w\\/\\.]{22}");
-
-    /**
-     * Instantiates a Bcrypt cipher provider with the default work factor 12 (2^12 key expansion rounds).
-     */
-    public BcryptCipherProvider() {
-        this(DEFAULT_WORK_FACTOR);
-    }
-
-    /**
-     * Instantiates a Bcrypt cipher provider with the specified work factor w (2^w key expansion rounds).
-     *
-     * @param workFactor the (log) number of key expansion rounds [4..30]
-     */
-    public BcryptCipherProvider(int workFactor) {
-        this.workFactor = workFactor;
-        if (workFactor < DEFAULT_WORK_FACTOR) {
-            logger.warn("The provided work factor {} is below the recommended minimum {}", workFactor, DEFAULT_WORK_FACTOR);
-        }
-    }
-
-    /**
-     * Returns an initialized cipher for the specified algorithm. The key is derived by the KDF of the implementation. The IV is provided externally to allow for non-deterministic IVs, as IVs
-     * deterministically derived from the password are a potential vulnerability and compromise semantic security. See
-     * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a>
-     *
-     * @param encryptionMethod the {@link EncryptionMethod}
-     * @param password         the secret input
-     * @param salt             the complete salt (e.g. {@code "$2a$10$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)})
-     * @param iv               the IV
-     * @param keyLength        the desired key length in bits
-     * @param encryptMode      true for encrypt, false for decrypt
-     * @return the initialized cipher
-     * @throws Exception if there is a problem initializing the cipher
-     */
-    @Override
-    public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception {
-        try {
-            return getInitializedCipher(encryptionMethod, password, salt, iv, keyLength, encryptMode);
-        } catch (IllegalArgumentException e) {
-            throw e;
-        } catch (Exception e) {
-            throw new ProcessException("Error initializing the cipher", e);
-        }
-    }
-
-    @Override
-    Logger getLogger() {
-        return logger;
-    }
-
-    /**
-     * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation.
-     *
-     * The IV can be retrieved by the calling method using {@link Cipher#getIV()}.
-     *
-     * @param encryptionMethod the {@link EncryptionMethod}
-     * @param password         the secret input
-     * @param salt             the complete salt (e.g. {@code "$2a$10$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)})
-     * @param keyLength        the desired key length in bits
-     * @param encryptMode      true for encrypt, false for decrypt
-     * @return the initialized cipher
-     * @throws Exception if there is a problem initializing the cipher
-     */
-    @Override
-    public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception {
-        return getCipher(encryptionMethod, password, salt, new byte[0], keyLength, encryptMode);
-    }
-
-    protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception {
-        if (encryptionMethod == null) {
-            throw new IllegalArgumentException("The encryption method must be specified");
-        }
-        if (!encryptionMethod.isCompatibleWithStrongKDFs()) {
-            throw new IllegalArgumentException(encryptionMethod.name() + " is not compatible with Bcrypt");
-        }
-
-        if (StringUtils.isEmpty(password)) {
-            throw new IllegalArgumentException("Encryption with an empty password is not supported");
-        }
-
-        String algorithm = encryptionMethod.getAlgorithm();
-        String provider = encryptionMethod.getProvider();
-
-        final String cipherName = CipherUtility.parseCipherFromAlgorithm(algorithm);
-        if (!CipherUtility.isValidKeyLength(keyLength, cipherName)) {
-            throw new IllegalArgumentException(String.valueOf(keyLength) + " is not a valid key length for " + cipherName);
-        }
-
-        String bcryptSalt = formatSaltForBcrypt(salt);
-
-        String hash = BCrypt.hashpw(password, bcryptSalt);
-
-        /* The SHA-512 hash is required in order to derive a key longer than 184 bits (the resulting size of the Bcrypt hash) and ensuring the avalanche effect causes higher key entropy (if all
-        derived keys follow a consistent pattern, it weakens the strength of the encryption) */
-        MessageDigest digest = MessageDigest.getInstance("SHA-512", provider);
-        byte[] dk = digest.digest(hash.getBytes(StandardCharsets.UTF_8));
-        dk = Arrays.copyOf(dk, keyLength / 8);
-        SecretKey tempKey = new SecretKeySpec(dk, algorithm);
-
-        KeyedCipherProvider keyedCipherProvider = new AESKeyedCipherProvider();
-        return keyedCipherProvider.getCipher(encryptionMethod, tempKey, iv, encryptMode);
-    }
-
-    private String formatSaltForBcrypt(byte[] salt) {
-        if (salt == null || salt.length == 0) {
-            throw new IllegalArgumentException("The salt cannot be empty. To generate a salt, use BcryptCipherProvider#generateSalt()");
-        }
-
-        String rawSalt = new String(salt, StandardCharsets.UTF_8);
-        Matcher matcher = BCRYPT_SALT_FORMAT.matcher(rawSalt);
-
-        if (matcher.find()) {
-            return rawSalt;
-        } else {
-            throw new IllegalArgumentException("The salt must be of the format $2a$10$gUVbkVzp79H8YaCOsCVZNu. To generate a salt, use BcryptCipherProvider#generateSalt()");
-        }
-    }
-
-    @Override
-    public byte[] generateSalt() {
-        return BCrypt.gensalt(workFactor).getBytes(StandardCharsets.UTF_8);
-    }
-
-    @Override
-    public int getDefaultSaltLength() {
-        return DEFAULT_SALT_LENGTH;
-    }
-
-    protected int getWorkFactor() {
-        return workFactor;
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi/blob/2c1f5b49/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/CipherProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/CipherProviderFactory.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/CipherProviderFactory.java
deleted file mode 100644
index 09004bf..0000000
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/security/util/crypto/CipherProviderFactory.java
+++ /dev/null
@@ -1,56 +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.security.util.crypto;
-
-import java.util.HashMap;
-import java.util.Map;
-import org.apache.nifi.processor.exception.ProcessException;
-import org.apache.nifi.security.util.KeyDerivationFunction;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class CipherProviderFactory {
-    private static final Logger logger = LoggerFactory.getLogger(CipherProviderFactory.class);
-
-    private static Map<KeyDerivationFunction, Class<? extends CipherProvider>> registeredCipherProviders;
-
-    static {
-        registeredCipherProviders = new HashMap<>();
-        registeredCipherProviders.put(KeyDerivationFunction.NIFI_LEGACY, NiFiLegacyCipherProvider.class);
-        registeredCipherProviders.put(KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY, OpenSSLPKCS5CipherProvider.class);
-        registeredCipherProviders.put(KeyDerivationFunction.PBKDF2, PBKDF2CipherProvider.class);
-        registeredCipherProviders.put(KeyDerivationFunction.BCRYPT, BcryptCipherProvider.class);
-        registeredCipherProviders.put(KeyDerivationFunction.SCRYPT, ScryptCipherProvider.class);
-        registeredCipherProviders.put(KeyDerivationFunction.NONE, AESKeyedCipherProvider.class);
-    }
-
-    public static CipherProvider getCipherProvider(KeyDerivationFunction kdf) {
-        logger.debug("{} KDFs registered", registeredCipherProviders.size());
-
-        if (registeredCipherProviders.containsKey(kdf)) {
-            Class<? extends CipherProvider> clazz = registeredCipherProviders.get(kdf);
-            try {
-                return clazz.newInstance();
-            } catch (Exception e) {
-               logger.error("Error instantiating new {} with default parameters for {}", clazz.getName(), kdf.getName());
-                throw new ProcessException("Error instantiating cipher provider");
-            }
-        }
-
-        throw new IllegalArgumentException("No cipher provider registered for " + kdf.getName());
-    }
-}