You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by al...@apache.org on 2016/02/05 01:41:35 UTC
[3/7] nifi git commit: NIFI-1257 NIFI-1259
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..0596d7d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/AESKeyedCipherProviderGroovyTest.groovy
@@ -0,0 +1,340 @@
+/*
+ * 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.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.spec.SecretKeySpec
+import java.security.SecureRandom
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@RunWith(JUnit4.class)
+public class AESKeyedCipherProviderGroovyTest {
+ private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProviderGroovyTest.class)
+
+ private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
+
+ private static final List<EncryptionMethod> keyedEncryptionMethods = EncryptionMethod.values().findAll { it.keyedCipher }
+
+ private static final SecretKey key = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider())
+
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ final String plaintext = "This is a plaintext message."
+
+ // Act
+ for (EncryptionMethod em : keyedEncryptionMethods) {
+ logger.info("Using algorithm: ${em.getAlgorithm()}")
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, key, 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, 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
+ public 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
+ 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())
+
+ 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
+ public void testShouldRejectEmptyKey() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+ // Act
+ def msg = shouldFail(IllegalArgumentException) {
+ cipherProvider.getCipher(encryptionMethod, null, true)
+ }
+
+ // Assert
+ assert msg =~ "The key must be specified"
+ }
+
+ @Test
+ public void testShouldRejectIncorrectLengthKey() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ SecretKey localKey = new SecretKeySpec(Hex.decodeHex("0123456789ABCDEF" as char[]), "AES")
+ assert ![128, 192, 256].contains(localKey.encoded.length)
+
+ final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+ // Act
+ def msg = shouldFail(IllegalArgumentException) {
+ cipherProvider.getCipher(encryptionMethod, localKey, true)
+ }
+
+ // Assert
+ assert msg =~ "The key must be of length \\[128, 192, 256\\]"
+ }
+
+ @Test
+ public void testShouldRejectEmptyEncryptionMethod() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ // Act
+ def msg = shouldFail(IllegalArgumentException) {
+ cipherProvider.getCipher(null, key, true)
+ }
+
+ // Assert
+ assert msg =~ "The encryption method must be specified"
+ }
+
+ @Test
+ public void testShouldRejectUnsupportedEncryptionMethod() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+
+ // Act
+ def msg = shouldFail(IllegalArgumentException) {
+ cipherProvider.getCipher(encryptionMethod, key, true)
+ }
+
+ // Assert
+ assert msg =~ " requires a PBECipherProvider"
+ }
+
+ @Test
+ public void testGetCipherShouldSupportExternalCompatibility() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ final String PLAINTEXT = "This is a plaintext message."
+
+ // These values can be generated by running `$ ./openssl_aes.rb` in the terminal
+ final byte[] IV = Hex.decodeHex("e0bc8cc7fbc0bdfdc184dc22ce2fcb5b" as char[])
+ final byte[] LOCAL_KEY = Hex.decodeHex("c72943d27c3e5a276169c5998a779117" as char[])
+ final String CIPHER_TEXT = "a2725ea55c7dd717664d044cab0f0b5f763653e322c27df21954f5be394efb1b"
+ byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
+
+ SecretKey localKey = new SecretKeySpec(LOCAL_KEY, "AES")
+
+ 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, 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
+ public void testGetCipherForDecryptShouldRequireIV() 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}")
+
+ def msg = shouldFail(IllegalArgumentException) {
+ cipher = cipherProvider.getCipher(em, key, false)
+ }
+
+ // Assert
+ assert msg =~ "Cannot decrypt without a valid IV"
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldRejectInvalidIVLengths() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ 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, key, badIV, true)
+
+ // Decrypt should fail
+ def msg = shouldFail(IllegalArgumentException) {
+ cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false)
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "Cannot decrypt without a valid IV"
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldRejectEmptyIV() throws Exception {
+ // Arrange
+ KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
+
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+ byte[] badIV = [0x00 as byte] * 16 as byte[]
+
+ // Act
+ 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, key, badIV, true)
+ logger.info("IV after encrypt: ${Hex.encodeHexString(cipher.getIV())}")
+
+ // Decrypt should fail
+ def msg = shouldFail(IllegalArgumentException) {
+ cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false)
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "Cannot decrypt without a valid IV"
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..84b91c6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/BcryptCipherProviderGroovyTest.groovy
@@ -0,0 +1,549 @@
+/*
+ * 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.Base64
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.processors.standard.util.crypto.bcrypt.BCrypt
+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.mindrot.jbcrypt.BCrypt
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assert.assertTrue
+
+@RunWith(JUnit4.class)
+public class BcryptCipherProviderGroovyTest {
+ private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProviderGroovyTest.class);
+
+ private static List<EncryptionMethod> strongKDFEncryptionMethods
+
+ private static final int DEFAULT_KEY_LENGTH = 128;
+ public static final String MICROBENCHMARK = "microbenchmark"
+ private static ArrayList<Integer> AES_KEY_LENGTHS
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+
+ strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() }
+
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+
+ if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
+ AES_KEY_LENGTHS = [128, 192, 256]
+ } else {
+ AES_KEY_LENGTHS = [128]
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ @Test
+ public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = cipherProvider.generateSalt()
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : strongKDFEncryptionMethods) {
+ logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
+ byte[] iv = cipher.getIV();
+ logger.info("IV: ${Hex.encodeHexString(iv)}")
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: ${recovered}")
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = cipherProvider.generateSalt()
+ final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : strongKDFEncryptionMethods) {
+ logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
+ logger.info("IV: ${Hex.encodeHexString(IV)}")
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: ${recovered}")
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
+ PasswordBasedEncryptor.supportsUnlimitedStrength());
+
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = cipherProvider.generateSalt()
+
+ final int LONG_KEY_LENGTH = 256
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : strongKDFEncryptionMethods) {
+ logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true);
+ byte[] iv = cipher.getIV();
+ logger.info("IV: ${Hex.encodeHexString(iv)}")
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: ${recovered}")
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testHashPWShouldMatchTestVectors() {
+ // Arrange
+ final String PASSWORD = 'abcdefghijklmnopqrstuvwxyz'
+ final String SALT = '$2a$10$fVH8e28OQRj9tqiDXs1e1u'
+ final String EXPECTED_HASH = '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'
+// final int WORK_FACTOR = 10
+
+ // Act
+ String calculatedHash = BCrypt.hashpw(PASSWORD, SALT)
+ logger.info("Generated ${calculatedHash}")
+
+ // Assert
+ assert calculatedHash == EXPECTED_HASH
+ }
+
+ @Test
+ public void testGetCipherShouldSupportExternalCompatibility() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PLAINTEXT = "This is a plaintext message.";
+ final String PASSWORD = "thisIsABadPassword";
+
+ // These values can be generated by running `$ ./openssl_bcrypt` in the terminal
+ final byte[] SALT = Hex.decodeHex("81455b915ce9efd1fc61a08eb0255936" as char[]);
+ final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[]);
+
+ // $v2$w2$base64_salt_22__base64_hash_31
+ final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
+ logger.info("Full Hash: ${FULL_HASH}")
+ final String HASH = FULL_HASH[-31..-1]
+ logger.info(" Hash: ${HASH.padLeft(60, " ")}")
+ logger.info(" B64 Salt: ${CipherUtility.encodeBase64NoPadding(SALT).padLeft(29, " ")}")
+
+ String extractedSalt = FULL_HASH[7..<29]
+ logger.info("Extracted Salt: ${extractedSalt}")
+ String extractedSaltHex = Hex.encodeHexString(Base64.decodeBase64(extractedSalt))
+ logger.info("Extracted Salt (hex): ${extractedSaltHex}")
+ logger.info(" Expected Salt (hex): ${Hex.encodeHexString(SALT)}")
+
+ final String CIPHER_TEXT = "3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15"
+ byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
+
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+ logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+ logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+ // Sanity check
+ Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, "BC")
+ def rubyKey = new SecretKeySpec(Hex.decodeHex("724cd9e1b0b9e87c7f7e7d7b270bca07" as char[]), "AES")
+ def ivSpec = new IvParameterSpec(IV)
+ rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec)
+ byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.bytes)
+ logger.info("Expected cipher text: ${Hex.encodeHexString(rubyCipherBytes)}")
+ rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec)
+ assert rubyCipher.doFinal(rubyCipherBytes) == PLAINTEXT.bytes
+ assert rubyCipher.doFinal(cipherBytes) == PLAINTEXT.bytes
+ logger.sanity("Decrypted external cipher text and generated cipher text successfully")
+
+ // Sanity for hash generation
+ final String FULL_SALT = FULL_HASH[0..<29]
+ logger.sanity("Salt from external: ${FULL_SALT}")
+ String generatedHash = BCrypt.hashpw(PASSWORD, FULL_SALT)
+ logger.sanity("Generated hash: ${generatedHash}")
+ assert generatedHash == FULL_HASH
+
+ // Act
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: ${recovered}")
+
+ // Assert
+ assert PLAINTEXT.equals(recovered);
+ }
+
+ @Test
+ public void testGetCipherShouldHandleFullSalt() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PLAINTEXT = "This is a plaintext message.";
+ final String PASSWORD = "thisIsABadPassword";
+
+ // These values can be generated by running `$ ./openssl_bcrypt.rb` in the terminal
+ final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[]);
+
+ // $v2$w2$base64_salt_22__base64_hash_31
+ final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
+ logger.info("Full Hash: ${FULL_HASH}")
+ final String FULL_SALT = FULL_HASH[0..<29]
+ logger.info(" Salt: ${FULL_SALT}")
+ final String HASH = FULL_HASH[-31..-1]
+ logger.info(" Hash: ${HASH.padLeft(60, " ")}")
+
+ String extractedSalt = FULL_HASH[7..<29]
+ logger.info("Extracted Salt: ${extractedSalt}")
+ String extractedSaltHex = Hex.encodeHexString(Base64.decodeBase64(extractedSalt))
+ logger.info("Extracted Salt (hex): ${extractedSaltHex}")
+
+ final String CIPHER_TEXT = "3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15"
+ byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
+
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+ logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+ logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+ // Act
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: ${recovered}")
+
+ // Assert
+ assert PLAINTEXT.equals(recovered);
+ }
+
+ @Test
+ public void testGetCipherShouldHandleUnformedSalt() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PASSWORD = "thisIsABadPassword";
+
+ final def INVALID_SALTS = ['$ab$00$acbdefghijklmnopqrstuv', 'bad_salt', '$3a$11$', 'x', '$2a$10$']
+
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+ logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+ // Act
+ INVALID_SALTS.each { String salt ->
+ logger.info("Checking salt ${salt}")
+
+ def msg = shouldFail(IllegalArgumentException) {
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
+ }
+
+ // Assert
+ assert msg =~ "The salt must be of the format \\\$2a\\\$10\\\$gUVbkVzp79H8YaCOsCVZNu\\. To generate a salt, use BcryptCipherProvider#generateSalt"
+ }
+ }
+
+ String bytesToBitString(byte[] bytes) {
+ bytes.collect {
+ String.format("%8s", Integer.toBinaryString(it & 0xFF)).replace(' ', '0')
+ }.join("")
+ }
+
+ String spaceString(String input, int blockSize = 4) {
+ input.collect { it.padLeft(blockSize, " ") }.join("")
+ }
+
+ @Test
+ public void testGetCipherShouldRejectEmptySalt() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PASSWORD = "thisIsABadPassword";
+
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+ logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
+
+ // Two different errors -- one explaining the no-salt method is not supported, and the other for an empty byte[] passed
+
+ // Act
+ def msg = shouldFail(UnsupportedOperationException) {
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, DEFAULT_KEY_LENGTH, true);
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "The cipher cannot be initialized without a valid salt\\. Use BcryptCipherProvider#generateSalt\\(\\) to generate a valid salt"
+
+ // Act
+ msg = shouldFail(IllegalArgumentException) {
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true);
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "The salt cannot be empty\\. To generate a salt, use BcryptCipherProvider#generateSalt"
+ }
+
+ @Test
+ public void testGetCipherForDecryptShouldRequireIV() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = cipherProvider.generateSalt()
+ final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : strongKDFEncryptionMethods) {
+ logger.info("Using algorithm: ${em.getAlgorithm()}");
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
+ logger.info("IV: ${Hex.encodeHexString(IV)}")
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+ def msg = shouldFail(IllegalArgumentException) {
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false);
+ }
+
+ // Assert
+ assert msg =~ "Cannot decrypt without a valid IV"
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = cipherProvider.generateSalt()
+ final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
+
+ final String PLAINTEXT = "This is a plaintext message.";
+
+ // Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
+ final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+ // Act
+ VALID_KEY_LENGTHS.each { int keyLength ->
+ logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
+ logger.info("IV: ${Hex.encodeHexString(IV)}")
+
+ byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
+ logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
+
+ cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+ logger.info("Recovered: ${recovered}")
+
+ // Assert
+ assert PLAINTEXT.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = cipherProvider.generateSalt()
+ final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
+
+ final String PLAINTEXT = "This is a plaintext message.";
+
+ // Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
+ final def INVALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+
+ // Act
+ INVALID_KEY_LENGTHS.each { int keyLength ->
+ logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
+
+ // Initialize a cipher for encryption
+ def msg = shouldFail(IllegalArgumentException) {
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
+ }
+
+ // Assert
+ assert msg =~ "${keyLength} is not a valid key length for AES"
+ }
+ }
+
+ @Test
+ public void testGenerateSaltShouldUseProvidedWorkFactor() throws Exception {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(11);
+ int workFactor = cipherProvider.getWorkFactor()
+
+ // Act
+ final byte[] saltBytes = cipherProvider.generateSalt()
+ String salt = new String(saltBytes)
+ logger.info("Salt: ${salt}")
+
+ // Assert
+ assert salt =~ /^\$2[axy]\$\d{2}\$/
+ assert salt.contains("\$${workFactor}\$")
+ }
+
+ @Ignore("This test can be run on a specific machine to evaluate if the default work factor is sufficient")
+ @Test
+ public void testDefaultConstructorShouldProvideStrongWorkFactor() {
+ // Arrange
+ RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider();
+
+ // Values taken from http://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/ and http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
+
+ // Calculate the work factor to reach 500 ms
+ int minimumWorkFactor = calculateMinimumWorkFactor()
+ logger.info("Determined minimum safe work factor to be ${minimumWorkFactor}")
+
+ // Act
+ int workFactor = cipherProvider.getWorkFactor()
+ logger.info("Default work factor ${workFactor}")
+
+ // Assert
+ assertTrue("The default work factor for BcryptCipherProvider is too weak. Please update the default value to a stronger level.", workFactor >= minimumWorkFactor)
+ }
+
+ /**
+ * Returns the work factor required for a derivation to exceed 500 ms on this machine. Code adapted from http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
+ *
+ * @return the minimum bcrypt work factor
+ */
+ private static int calculateMinimumWorkFactor() {
+ // High start-up cost, so run multiple times for better benchmarking
+ final int RUNS = 10
+
+ // Benchmark using a work factor of 5 (the second-lowest allowed)
+ int workFactor = 5
+
+ String salt = BCrypt.gensalt(workFactor)
+
+ // Run once to prime the system
+ double duration = time {
+ BCrypt.hashpw(MICROBENCHMARK, salt)
+ }
+ logger.info("First run of work factor ${workFactor} took ${duration} ms (ignored)")
+
+ def durations = []
+
+ RUNS.times { int i ->
+ duration = time {
+ BCrypt.hashpw(MICROBENCHMARK, salt)
+ }
+ logger.info("Work factor ${workFactor} took ${duration} ms")
+ durations << duration
+ }
+
+ duration = durations.sum() / durations.size()
+ logger.info("Work factor ${workFactor} averaged ${duration} ms")
+
+ // Increasing the work factor by 1 would double the run time
+ // Keep increasing N until the estimated duration is over 500 ms
+ while (duration < 500) {
+ workFactor += 1
+ duration *= 2
+ }
+
+ logger.info("Returning work factor ${workFactor} for ${duration} ms")
+
+ return workFactor
+ }
+
+ private static double time(Closure c) {
+ long start = System.nanoTime()
+ c.call()
+ long end = System.nanoTime()
+ return (end - start) / 1_000_000.0
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy
new file mode 100644
index 0000000..be8d5f4
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherProviderFactoryGroovyTest.groovy
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License") you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.standard.util.crypto
+
+import org.apache.nifi.security.util.KeyDerivationFunction
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class CipherProviderFactoryGroovyTest extends GroovyTestCase {
+ private static final Logger logger = LoggerFactory.getLogger(CipherProviderFactoryGroovyTest.class)
+
+ private static final Map<KeyDerivationFunction, Class> EXPECTED_CIPHER_PROVIDERS = [
+ (KeyDerivationFunction.BCRYPT) : BcryptCipherProvider.class,
+ (KeyDerivationFunction.NIFI_LEGACY) : NiFiLegacyCipherProvider.class,
+ (KeyDerivationFunction.NONE) : AESKeyedCipherProvider.class,
+ (KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): OpenSSLPKCS5CipherProvider.class,
+ (KeyDerivationFunction.PBKDF2) : PBKDF2CipherProvider.class,
+ (KeyDerivationFunction.SCRYPT) : ScryptCipherProvider.class
+ ]
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider())
+
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testGetCipherProviderShouldResolveRegisteredKDFs() {
+ // Arrange
+
+ // Act
+ KeyDerivationFunction.values().each { KeyDerivationFunction kdf ->
+ logger.info("Expected: ${kdf.name} -> ${EXPECTED_CIPHER_PROVIDERS.get(kdf).simpleName}")
+ CipherProvider cp = CipherProviderFactory.getCipherProvider(kdf)
+ logger.info("Resolved: ${kdf.name} -> ${cp.class.simpleName}")
+
+ // Assert
+ assert cp.class == (EXPECTED_CIPHER_PROVIDERS.get(kdf))
+ }
+ }
+
+ @Ignore("Cannot mock enum using Groovy map coercion")
+ @Test
+ public void testGetCipherProviderShouldHandleUnregisteredKDFs() {
+ // Arrange
+
+ // Can't mock this; see http://stackoverflow.com/questions/5323505/mocking-java-enum-to-add-a-value-to-test-fail-case
+ KeyDerivationFunction invalidKDF = [name: "Unregistered", description: "Not a registered KDF"] as KeyDerivationFunction
+ logger.info("Expected: ${invalidKDF.name} -> error")
+
+ // Act
+ def msg = shouldFail(IllegalArgumentException) {
+ CipherProvider cp = CipherProviderFactory.getCipherProvider(invalidKDF)
+ logger.info("Resolved: ${invalidKDF.name} -> ${cp.class.simpleName}")
+ }
+ logger.expected(msg)
+
+ // Assert
+ assert msg =~ "No cipher provider registered for ${invalidKDF.name}"
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy
new file mode 100644
index 0000000..6a6a958
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/CipherUtilityGroovyTest.groovy
@@ -0,0 +1,251 @@
+/*
+ * 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.nifi.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class CipherUtilityGroovyTest extends GroovyTestCase {
+ private static final Logger logger = LoggerFactory.getLogger(CipherUtilityGroovyTest.class)
+
+ // TripleDES must precede DES for automatic grouping precedence
+ private static final List<String> CIPHERS = ["AES", "TRIPLEDES", "DES", "RC2", "RC4", "RC5", "TWOFISH"]
+ private static final List<String> SYMMETRIC_ALGORITHMS = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") || it.algorithm.startsWith("AES") }*.algorithm
+ private static final Map<String, List<String>> ALGORITHMS_MAPPED_BY_CIPHER = SYMMETRIC_ALGORITHMS.groupBy { String algorithm -> CIPHERS.find { algorithm.contains(it) } }
+
+ // Manually mapped as of 01/19/16 0.5.0
+ private static final Map<Integer, List<String>> ALGORITHMS_MAPPED_BY_KEY_LENGTH = [
+ (40) : ["PBEWITHSHAAND40BITRC2-CBC",
+ "PBEWITHSHAAND40BITRC4"],
+ (64) : ["PBEWITHMD5ANDDES",
+ "PBEWITHSHA1ANDDES"],
+ (112): ["PBEWITHSHAAND2-KEYTRIPLEDES-CBC",
+ "PBEWITHSHAAND3-KEYTRIPLEDES-CBC"],
+ (128): ["PBEWITHMD5AND128BITAES-CBC-OPENSSL",
+ "PBEWITHMD5ANDRC2",
+ "PBEWITHSHA1ANDRC2",
+ "PBEWITHSHA256AND128BITAES-CBC-BC",
+ "PBEWITHSHAAND128BITAES-CBC-BC",
+ "PBEWITHSHAAND128BITRC2-CBC",
+ "PBEWITHSHAAND128BITRC4",
+ "PBEWITHSHAANDTWOFISH-CBC",
+ "AES/CBC/PKCS7Padding",
+ "AES/CTR/NoPadding",
+ "AES/GCM/NoPadding"],
+ (192): ["PBEWITHMD5AND192BITAES-CBC-OPENSSL",
+ "PBEWITHSHA256AND192BITAES-CBC-BC",
+ "PBEWITHSHAAND192BITAES-CBC-BC",
+ "AES/CBC/PKCS7Padding",
+ "AES/CTR/NoPadding",
+ "AES/GCM/NoPadding"],
+ (256): ["PBEWITHMD5AND256BITAES-CBC-OPENSSL",
+ "PBEWITHSHA256AND256BITAES-CBC-BC",
+ "PBEWITHSHAAND256BITAES-CBC-BC",
+ "AES/CBC/PKCS7Padding",
+ "AES/CTR/NoPadding",
+ "AES/GCM/NoPadding"]
+ ]
+
+ @BeforeClass
+ static void setUpOnce() {
+ Security.addProvider(new BouncyCastleProvider());
+
+ // Fix because TRIPLEDES -> DESede
+ def tripleDESAlgorithms = ALGORITHMS_MAPPED_BY_CIPHER.remove("TRIPLEDES")
+ ALGORITHMS_MAPPED_BY_CIPHER.put("DESede", tripleDESAlgorithms)
+
+ logger.info("Mapped algorithms: ${ALGORITHMS_MAPPED_BY_CIPHER}")
+ }
+
+ @Before
+ void setUp() throws Exception {
+
+ }
+
+ @After
+ void tearDown() throws Exception {
+
+ }
+
+ @Test
+ void testShouldParseCipherFromAlgorithm() {
+ // Arrange
+ final def EXPECTED_ALGORITHMS = ALGORITHMS_MAPPED_BY_CIPHER
+
+ // Act
+ SYMMETRIC_ALGORITHMS.each { String algorithm ->
+ String cipher = CipherUtility.parseCipherFromAlgorithm(algorithm)
+ logger.info("Extracted ${cipher} from ${algorithm}")
+
+ // Assert
+ assert EXPECTED_ALGORITHMS.get(cipher).contains(algorithm)
+ }
+ }
+
+ @Test
+ void testShouldParseKeyLengthFromAlgorithm() {
+ // Arrange
+ final def EXPECTED_ALGORITHMS = ALGORITHMS_MAPPED_BY_KEY_LENGTH
+
+ // Act
+ SYMMETRIC_ALGORITHMS.each { String algorithm ->
+ int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm)
+ logger.info("Extracted ${keyLength} from ${algorithm}")
+
+ // Assert
+ assert EXPECTED_ALGORITHMS.get(keyLength).contains(algorithm)
+ }
+ }
+
+ @Test
+ void testShouldDetermineValidKeyLength() {
+ // Arrange
+
+ // Act
+ ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
+ algorithms.each { String algorithm ->
+ logger.info("Checking ${keyLength} for ${algorithm}")
+
+ // Assert
+ assert CipherUtility.isValidKeyLength(keyLength, CipherUtility.parseCipherFromAlgorithm(algorithm))
+ }
+ }
+ }
+
+ @Test
+ void testShouldDetermineInvalidKeyLength() {
+ // Arrange
+
+ // Act
+ ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
+ algorithms.each { String algorithm ->
+ def invalidKeyLengths = [-1, 0, 1]
+ if (algorithm =~ "RC\\d") {
+ invalidKeyLengths += [39, 2049]
+ } else {
+ invalidKeyLengths += keyLength + 1
+ }
+ logger.info("Checking ${invalidKeyLengths.join(", ")} for ${algorithm}")
+
+ // Assert
+ invalidKeyLengths.each { int invalidKeyLength ->
+ assert !CipherUtility.isValidKeyLength(invalidKeyLength, CipherUtility.parseCipherFromAlgorithm(algorithm))
+ }
+ }
+ }
+ }
+
+ @Test
+ void testShouldDetermineValidKeyLengthForAlgorithm() {
+ // Arrange
+
+ // Act
+ ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
+ algorithms.each { String algorithm ->
+ logger.info("Checking ${keyLength} for ${algorithm}")
+
+ // Assert
+ assert CipherUtility.isValidKeyLengthForAlgorithm(keyLength, algorithm)
+ }
+ }
+ }
+
+ @Test
+ void testShouldDetermineInvalidKeyLengthForAlgorithm() {
+ // Arrange
+
+ // Act
+ ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
+ algorithms.each { String algorithm ->
+ def invalidKeyLengths = [-1, 0, 1]
+ if (algorithm =~ "RC\\d") {
+ invalidKeyLengths += [39, 2049]
+ } else {
+ invalidKeyLengths += keyLength + 1
+ }
+ logger.info("Checking ${invalidKeyLengths.join(", ")} for ${algorithm}")
+
+ // Assert
+ invalidKeyLengths.each { int invalidKeyLength ->
+ assert !CipherUtility.isValidKeyLengthForAlgorithm(invalidKeyLength, algorithm)
+ }
+ }
+ }
+
+ // Extra hard-coded checks
+ String algorithm = "PBEWITHSHA256AND256BITAES-CBC-BC"
+ int invalidKeyLength = 192
+ logger.info("Checking ${invalidKeyLength} for ${algorithm}")
+ assert !CipherUtility.isValidKeyLengthForAlgorithm(invalidKeyLength, algorithm)
+ }
+
+ @Test
+ void testShouldGetValidKeyLengthsForAlgorithm() {
+ // Arrange
+
+ def rcKeyLengths = (40..2048).asList()
+ def CIPHER_KEY_SIZES = [
+ AES : [128, 192, 256],
+ DES : [56, 64],
+ DESede : [56, 64, 112, 128, 168, 192],
+ RC2 : rcKeyLengths,
+ RC4 : rcKeyLengths,
+ RC5 : rcKeyLengths,
+ TWOFISH: [128, 192, 256]
+ ]
+
+ def SINGLE_KEY_SIZE_ALGORITHMS = EncryptionMethod.values()*.algorithm.findAll { CipherUtility.parseActualKeyLengthFromAlgorithm(it) != -1 }
+ logger.info("Single key size algorithms: ${SINGLE_KEY_SIZE_ALGORITHMS}")
+ def MULTIPLE_KEY_SIZE_ALGORITHMS = EncryptionMethod.values()*.algorithm - SINGLE_KEY_SIZE_ALGORITHMS
+ MULTIPLE_KEY_SIZE_ALGORITHMS.removeAll { it.contains("PGP") }
+ logger.info("Multiple key size algorithms: ${MULTIPLE_KEY_SIZE_ALGORITHMS}")
+
+ // Act
+ SINGLE_KEY_SIZE_ALGORITHMS.each { String algorithm ->
+ def EXPECTED_KEY_SIZES = [CipherUtility.parseKeyLengthFromAlgorithm(algorithm)]
+
+ def validKeySizes = CipherUtility.getValidKeyLengthsForAlgorithm(algorithm)
+ logger.info("Checking ${algorithm} ${validKeySizes} against expected ${EXPECTED_KEY_SIZES}")
+
+ // Assert
+ assert validKeySizes == EXPECTED_KEY_SIZES
+ }
+
+ // Act
+ MULTIPLE_KEY_SIZE_ALGORITHMS.each { String algorithm ->
+ String cipher = CipherUtility.parseCipherFromAlgorithm(algorithm)
+ def EXPECTED_KEY_SIZES = CIPHER_KEY_SIZES[cipher]
+
+ def validKeySizes = CipherUtility.getValidKeyLengthsForAlgorithm(algorithm)
+ logger.info("Checking ${algorithm} ${validKeySizes} against expected ${EXPECTED_KEY_SIZES}")
+
+ // Assert
+ assert validKeySizes == EXPECTED_KEY_SIZES
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptorGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptorGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptorGroovyTest.groovy
new file mode 100644
index 0000000..8e78778
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/KeyedEncryptorGroovyTest.groovy
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License") you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.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.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.SecretKey
+import javax.crypto.spec.SecretKeySpec
+import java.security.Security
+
+public class KeyedEncryptorGroovyTest {
+ private static final Logger logger = LoggerFactory.getLogger(KeyedEncryptorGroovyTest.class)
+
+ private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/"
+ private static final File plainFile = new File("${TEST_RESOURCES_PREFIX}/plain.txt")
+ private static final File encryptedFile = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.asc")
+
+ private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
+ private static final SecretKey KEY = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider())
+
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ @Test
+ public void testShouldEncryptAndDecrypt() throws Exception {
+ // Arrange
+ final String PLAINTEXT = "This is a plaintext message."
+ logger.info("Plaintext: {}", PLAINTEXT)
+ InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"))
+
+ OutputStream cipherStream = new ByteArrayOutputStream()
+ OutputStream recoveredStream = new ByteArrayOutputStream()
+
+ EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+ logger.info("Using ${encryptionMethod.name()}")
+
+ // Act
+ KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, KEY)
+
+ StreamCallback encryptionCallback = encryptor.getEncryptionCallback()
+ StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
+
+ encryptionCallback.process(plainStream, cipherStream)
+
+ final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray()
+ logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes))
+ InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes)
+ decryptionCallback.process(cipherInputStream, recoveredStream)
+
+ // Assert
+ byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray()
+ String recovered = new String(recoveredBytes, "UTF-8")
+ logger.info("Recovered: {}\n\n", recovered)
+ assert PLAINTEXT.equals(recovered)
+ }
+
+ @Test
+ public void testShouldDecryptOpenSSLUnsaltedCipherTextWithKnownIV() throws Exception {
+ // Arrange
+ final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text
+ logger.info("Plaintext: {}", PLAINTEXT)
+ byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.enc").bytes
+
+ final String keyHex = "711E85689CE7AFF6F410AEA43ABC5446"
+ final String ivHex = "842F685B84879B2E00F977C22B9E9A7D"
+
+ InputStream cipherStream = new ByteArrayInputStream(cipherBytes)
+ OutputStream recoveredStream = new ByteArrayOutputStream()
+
+ final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
+ KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, new SecretKeySpec(Hex.decodeHex(keyHex as char[]), "AES"), Hex.decodeHex(ivHex as char[]))
+
+ StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
+ logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}")
+
+ // Act
+ decryptionCallback.process(cipherStream, recoveredStream)
+
+ // Assert
+ byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray()
+ String recovered = new String(recoveredBytes, "UTF-8")
+ logger.info("Recovered: {}", recovered)
+ assert PLAINTEXT.equals(recovered)
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..0472fa3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProviderGroovyTest.groovy
@@ -0,0 +1,288 @@
+/*
+ * 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.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.PBEParameterSpec
+import java.security.Security
+
+import static org.junit.Assert.fail
+
+@RunWith(JUnit4.class)
+public class NiFiLegacyCipherProviderGroovyTest {
+ private static final Logger logger = LoggerFactory.getLogger(NiFiLegacyCipherProviderGroovyTest.class);
+
+ private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>();
+ private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>();
+
+ private static final String PROVIDER_NAME = "BC";
+ private static final int ITERATION_COUNT = 1000;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+
+ pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.toUpperCase().startsWith("PBE") }
+ limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { !it.isUnlimitedStrength() }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ private static Cipher getLegacyCipher(String password, byte[] salt, String algorithm) {
+ try {
+ final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
+ final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
+ SecretKey tempKey = factory.generateSecret(pbeKeySpec);
+
+ final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
+ Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
+ return cipher;
+ } catch (Exception e) {
+ logger.error("Error generating legacy cipher", e);
+ fail(e.getMessage());
+ }
+
+ return null;
+ }
+
+ @Test
+ public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
+ logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
+ continue
+ }
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
+ PasswordBasedEncryptor.supportsUnlimitedStrength());
+
+ NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : pbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldSupportLegacyCode() throws Exception {
+ // Arrange
+ NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
+ logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
+ continue
+ }
+
+ // Initialize a legacy cipher for encryption
+ Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
+
+ byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+ byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
+ // Arrange
+ NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = new byte[0];
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
+ logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
+ continue
+ }
+
+ // Initialize a legacy cipher for encryption
+ Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
+
+ byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false);
+ byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldIgnoreKeyLength() throws Exception {
+ // Arrange
+ NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
+
+ // Initialize a cipher for encryption
+ EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+ final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true);
+ byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ // Act
+ KEY_LENGTHS.each { int keyLength ->
+ logger.info("Decrypting with 'requested' key length: ${keyLength}")
+
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ /**
+ * This test determines for each PBE encryption algorithm if it actually requires the JCE unlimited strength jurisdiction policies to be installed.
+ * Even some algorithms that use 128-bit keys (which should be allowed on all systems) throw exceptions because BouncyCastle derives the key
+ * from the password using a long digest result at the time of key length checking.
+ * @throws IOException
+ */
+ @Test
+ public void testShouldDetermineDependenceOnUnlimitedStrengthCrypto() throws IOException {
+ def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
+
+ boolean unlimitedCryptoSupported = PasswordBasedEncryptor.supportsUnlimitedStrength()
+ logger.info("This JVM supports unlimited strength crypto: ${unlimitedCryptoSupported}")
+
+ def longestSupportedPasswordByEM = [:]
+
+ encryptionMethods.each { EncryptionMethod encryptionMethod ->
+ logger.info("Attempting ${encryptionMethod.name()} (${encryptionMethod.algorithm}) which claims unlimited strength required: ${encryptionMethod.unlimitedStrength}")
+
+ (1..20).find { int length ->
+ String password = "x" * length
+
+ try {
+ NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, password, true)
+ return false
+ } catch (Exception e) {
+ logger.error("Unable to create the cipher with ${encryptionMethod.algorithm} and password ${password} (${password.length()}) due to ${e.getMessage()}")
+ if (!longestSupportedPasswordByEM.containsKey(encryptionMethod)) {
+ longestSupportedPasswordByEM.put(encryptionMethod, password.length() - 1)
+ }
+ return true
+ }
+ }
+ logger.info("\n")
+ }
+
+ logger.info("Longest supported password by encryption method:")
+ longestSupportedPasswordByEM.each { EncryptionMethod encryptionMethod, int length ->
+ logger.info("\t${encryptionMethod.algorithm}\t${length}")
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..31cbd5a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/groovy/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProviderGroovyTest.groovy
@@ -0,0 +1,319 @@
+/*
+ * 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.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.PBEParameterSpec
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assert.fail
+
+@RunWith(JUnit4.class)
+public class OpenSSLPKCS5CipherProviderGroovyTest {
+ private static final Logger logger = LoggerFactory.getLogger(OpenSSLPKCS5CipherProviderGroovyTest.class);
+
+ private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>();
+ private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>();
+
+ private static final String PROVIDER_NAME = "BC";
+ private static final int ITERATION_COUNT = 0;
+
+ @BeforeClass
+ public static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+
+ pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.toUpperCase().startsWith("PBE") }
+ limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { !it.isUnlimitedStrength() }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+
+ }
+
+ private static Cipher getLegacyCipher(String password, byte[] salt, String algorithm) {
+ try {
+ final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
+ final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
+ SecretKey tempKey = factory.generateSecret(pbeKeySpec);
+
+ final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
+ Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
+ cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
+ return cipher;
+ } catch (Exception e) {
+ logger.error("Error generating legacy cipher", e);
+ fail(e.getMessage());
+ }
+
+ return null;
+ }
+
+ @Test
+ public void testGetCipherShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final String PASSWORD = "short";
+ final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
+ logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
+ continue
+ }
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
+ // Arrange
+ Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
+ PasswordBasedEncryptor.supportsUnlimitedStrength());
+
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : pbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ // Initialize a cipher for encryption
+ Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
+
+ byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldSupportLegacyCode() throws Exception {
+ // Arrange
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
+ logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
+ continue
+ }
+
+ // Initialize a legacy cipher for encryption
+ Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
+
+ byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
+ byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
+ // Arrange
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final String PASSWORD = "short";
+ final byte[] SALT = new byte[0];
+
+ final String plaintext = "This is a plaintext message.";
+
+ // Act
+ for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
+ logger.info("Using algorithm: {}", em.getAlgorithm());
+
+ if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
+ logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
+ continue
+ }
+
+ // Initialize a legacy cipher for encryption
+ Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
+
+ byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false);
+ byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldIgnoreKeyLength() throws Exception {
+ // Arrange
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
+
+ final String plaintext = "This is a plaintext message.";
+
+ final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
+
+ // Initialize a cipher for encryption
+ EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+ final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true);
+ byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
+ logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
+
+ // Act
+ KEY_LENGTHS.each { int keyLength ->
+ logger.info("Decrypting with 'requested' key length: ${keyLength}")
+
+ Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false);
+ byte[] recoveredBytes = cipher.doFinal(cipherBytes);
+ String recovered = new String(recoveredBytes, "UTF-8");
+
+ // Assert
+ assert plaintext.equals(recovered);
+ }
+ }
+
+ @Test
+ public void testGetCipherShouldRequireEncryptionMethod() throws Exception {
+ // Arrange
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
+
+ // Act
+ logger.info("Using algorithm: null");
+
+ def msg = shouldFail(IllegalArgumentException) {
+ Cipher providedCipher = cipherProvider.getCipher(null, PASSWORD, SALT, false);
+ }
+
+ // Assert
+ assert msg =~ "The encryption method must be specified"
+ }
+
+ @Test
+ public void testGetCipherShouldRequirePassword() throws Exception {
+ // Arrange
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
+ EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+
+ // Act
+ logger.info("Using algorithm: ${encryptionMethod}");
+
+ def msg = shouldFail(IllegalArgumentException) {
+ Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, "", SALT, false);
+ }
+
+ // Assert
+ assert msg =~ "Encryption with an empty password is not supported"
+ }
+
+ @Test
+ public void testGetCipherShouldValidateSaltLength() throws Exception {
+ // Arrange
+ OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
+
+ final String PASSWORD = "shortPassword";
+ final byte[] SALT = Hex.decodeHex("00112233445566".toCharArray());
+ EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
+
+ // Act
+ logger.info("Using algorithm: ${encryptionMethod}");
+
+ def msg = shouldFail(IllegalArgumentException) {
+ Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, false);
+ }
+
+ // Assert
+ assert msg =~ "Salt must be 8 bytes US-ASCII encoded"
+ }
+
+ @Test
+ public void testGenerateSaltShouldProvideValidSalt() throws Exception {
+ // Arrange
+ PBECipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
+
+ // Act
+ byte[] salt = cipherProvider.generateSalt()
+ logger.info("Checking salt ${Hex.encodeHexString(salt)}")
+
+ // Assert
+ assert salt.length == cipherProvider.getDefaultSaltLength()
+ assert salt != [(0x00 as byte) * cipherProvider.defaultSaltLength]
+ }
+}
\ No newline at end of file