http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-data-provenance-utils/src/test/groovy/org/apache/nifi/provenance/CryptoUtilsTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-data-provenance-utils/src/test/groovy/org/apache/nifi/provenance/CryptoUtilsTest.groovy b/nifi-commons/nifi-data-provenance-utils/src/test/groovy/org/apache/nifi/provenance/CryptoUtilsTest.groovy
new file mode 100644
index 0000000..162896f
--- /dev/null
+++ b/nifi-commons/nifi-data-provenance-utils/src/test/groovy/org/apache/nifi/provenance/CryptoUtilsTest.groovy
@@ -0,0 +1,436 @@
+/*
+ * 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.provenance
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.util.encoders.Hex
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.ClassRule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import java.nio.charset.StandardCharsets
+import java.nio.file.Files
+import java.nio.file.attribute.PosixFilePermission
+import java.security.KeyManagementException
+import java.security.SecureRandom
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@RunWith(JUnit4.class)
+class CryptoUtilsTest {
+ private static final Logger logger = LoggerFactory.getLogger(CryptoUtilsTest.class)
+
+ private static final String KEY_ID = "K1"
+ private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
+ private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+ private static final String KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? KEY_HEX_256 : KEY_HEX_128
+
+ private static
+ final Set<PosixFilePermission> ALL_POSIX_ATTRS = PosixFilePermission.values() as Set<PosixFilePermission>
+
+ @ClassRule
+ public static TemporaryFolder tempFolder = new TemporaryFolder()
+
+ @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 {
+ tempFolder.create()
+ }
+
+ @After
+ void tearDown() throws Exception {
+ tempFolder?.delete()
+ }
+
+ @AfterClass
+ static void tearDownOnce() throws Exception {
+
+ }
+
+ private static boolean isUnlimitedStrengthCryptoAvailable() {
+ Cipher.getMaxAllowedKeyLength("AES") > 128
+ }
+
+ @Test
+ void testShouldConcatenateByteArrays() {
+ // Arrange
+ byte[] bytes1 = "These are some bytes".getBytes(StandardCharsets.UTF_8)
+ byte[] bytes2 = "These are some other bytes".getBytes(StandardCharsets.UTF_8)
+ final byte[] EXPECTED_CONCATENATED_BYTES = ((bytes1 as List) << (bytes2 as List)).flatten() as byte[]
+ logger.info("Expected concatenated bytes: ${Hex.toHexString(EXPECTED_CONCATENATED_BYTES)}")
+
+ // Act
+ byte[] concat = CryptoUtils.concatByteArrays(bytes1, bytes2)
+ logger.info(" Actual concatenated bytes: ${Hex.toHexString(concat)}")
+
+ // Assert
+ assert concat == EXPECTED_CONCATENATED_BYTES
+ }
+
+ @Test
+ void testShouldValidateStaticKeyProvider() {
+ // Arrange
+ String staticProvider = StaticKeyProvider.class.name
+ String providerLocation = null
+
+ // Act
+ boolean keyProviderIsValid = CryptoUtils.isValidKeyProvider(staticProvider, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX])
+ logger.info("Key Provider ${staticProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX} is ${keyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert keyProviderIsValid
+ }
+
+ @Test
+ void testShouldNotValidateStaticKeyProviderMissingKeyId() {
+ // Arrange
+ String staticProvider = StaticKeyProvider.class.name
+ String providerLocation = null
+
+ // Act
+ boolean keyProviderIsValid = CryptoUtils.isValidKeyProvider(staticProvider, providerLocation, null, [(KEY_ID): KEY_HEX])
+ logger.info("Key Provider ${staticProvider} with location ${providerLocation} and keyId ${null} / ${KEY_HEX} is ${keyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert !keyProviderIsValid
+ }
+
+ @Test
+ void testShouldNotValidateStaticKeyProviderMissingKey() {
+ // Arrange
+ String staticProvider = StaticKeyProvider.class.name
+ String providerLocation = null
+
+ // Act
+ boolean keyProviderIsValid = CryptoUtils.isValidKeyProvider(staticProvider, providerLocation, KEY_ID, null)
+ logger.info("Key Provider ${staticProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${null} is ${keyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert !keyProviderIsValid
+ }
+
+ @Test
+ void testShouldNotValidateStaticKeyProviderWithInvalidKey() {
+ // Arrange
+ String staticProvider = StaticKeyProvider.class.name
+ String providerLocation = null
+
+ // Act
+ boolean keyProviderIsValid = CryptoUtils.isValidKeyProvider(staticProvider, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX[0..<-2]])
+ logger.info("Key Provider ${staticProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX[0..<-2]} is ${keyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert !keyProviderIsValid
+ }
+
+ @Test
+ void testShouldValidateFileBasedKeyProvider() {
+ // Arrange
+ String fileBasedProvider = FileBasedKeyProvider.class.name
+ File fileBasedProviderFile = tempFolder.newFile("filebased.kp")
+ String providerLocation = fileBasedProviderFile.path
+ logger.info("Created temporary file based key provider: ${providerLocation}")
+
+ // Act
+ boolean keyProviderIsValid = CryptoUtils.isValidKeyProvider(fileBasedProvider, providerLocation, KEY_ID, null)
+ logger.info("Key Provider ${fileBasedProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${null} is ${keyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert keyProviderIsValid
+ }
+
+ @Test
+ void testShouldNotValidateUnreadableOrMissingFileBasedKeyProvider() {
+ // Arrange
+ String fileBasedProvider = FileBasedKeyProvider.class.name
+ File fileBasedProviderFile = tempFolder.newFile("filebased.kp")
+ String providerLocation = fileBasedProviderFile.path
+ logger.info("Created temporary file based key provider: ${providerLocation}")
+
+ // Make it unreadable
+ fileBasedProviderFile.setReadable(false, false)
+ Files.setPosixFilePermissions(fileBasedProviderFile.toPath(), [] as Set<PosixFilePermission>)
+
+ // Act
+ boolean unreadableKeyProviderIsValid = CryptoUtils.isValidKeyProvider(fileBasedProvider, providerLocation, KEY_ID, null)
+ logger.info("Key Provider ${fileBasedProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${null} is ${unreadableKeyProviderIsValid ? "valid" : "invalid"}")
+
+ String missingLocation = providerLocation + "_missing"
+ boolean missingKeyProviderIsValid = CryptoUtils.isValidKeyProvider(fileBasedProvider, missingLocation, KEY_ID, null)
+ logger.info("Key Provider ${fileBasedProvider} with location ${missingLocation} and keyId ${KEY_ID} / ${null} is ${missingKeyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert !unreadableKeyProviderIsValid
+ assert !missingKeyProviderIsValid
+
+ // Make the file deletable so cleanup can occur
+ fileBasedProviderFile.setReadable(true, false)
+ Files.setPosixFilePermissions(fileBasedProviderFile.toPath(), ALL_POSIX_ATTRS)
+ }
+
+ @Test
+ void testShouldNotValidateFileBasedKeyProviderMissingKeyId() {
+ // Arrange
+ String fileBasedProvider = FileBasedKeyProvider.class.name
+ File fileBasedProviderFile = tempFolder.newFile("missing_key_id.kp")
+ String providerLocation = fileBasedProviderFile.path
+ logger.info("Created temporary file based key provider: ${providerLocation}")
+
+ // Act
+ boolean keyProviderIsValid = CryptoUtils.isValidKeyProvider(fileBasedProvider, providerLocation, null, null)
+ logger.info("Key Provider ${fileBasedProvider} with location ${providerLocation} and keyId ${null} / ${null} is ${keyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert !keyProviderIsValid
+ }
+
+ @Test
+ void testShouldNotValidateUnknownKeyProvider() {
+ // Arrange
+ String providerImplementation = "org.apache.nifi.provenance.ImaginaryKeyProvider"
+ String providerLocation = null
+
+ // Act
+ boolean keyProviderIsValid = CryptoUtils.isValidKeyProvider(providerImplementation, providerLocation, KEY_ID, null)
+ logger.info("Key Provider ${providerImplementation} with location ${providerLocation} and keyId ${KEY_ID} / ${null} is ${keyProviderIsValid ? "valid" : "invalid"}")
+
+ // Assert
+ assert !keyProviderIsValid
+ }
+
+ @Test
+ void testShouldValidateKey() {
+ // Arrange
+ String validKey = KEY_HEX
+ String validLowercaseKey = KEY_HEX.toLowerCase()
+
+ String tooShortKey = KEY_HEX[0..<-2]
+ String tooLongKey = KEY_HEX + KEY_HEX // Guaranteed to be 2x the max valid key length
+ String nonHexKey = KEY_HEX.replaceFirst(/A/, "X")
+
+ def validKeys = [validKey, validLowercaseKey]
+ def invalidKeys = [tooShortKey, tooLongKey, nonHexKey]
+
+ // If unlimited strength is available, also validate 128 and 196 bit keys
+ if (isUnlimitedStrengthCryptoAvailable()) {
+ validKeys << KEY_HEX_128
+ validKeys << KEY_HEX_256[0..<48]
+ } else {
+ invalidKeys << KEY_HEX_256[0..<48]
+ invalidKeys << KEY_HEX_256
+ }
+
+ // Act
+ def validResults = validKeys.collect { String key ->
+ logger.info("Validating ${key}")
+ CryptoUtils.keyIsValid(key)
+ }
+
+ def invalidResults = invalidKeys.collect { String key ->
+ logger.info("Validating ${key}")
+ CryptoUtils.keyIsValid(key)
+ }
+
+ // Assert
+ assert validResults.every()
+ assert invalidResults.every { !it }
+ }
+
+ @Test
+ void testShouldReadKeys() {
+ // Arrange
+ String masterKeyHex = KEY_HEX
+ SecretKey masterKey = new SecretKeySpec(Hex.decode(masterKeyHex), "AES")
+
+ // Generate the file
+ String keyFileName = "keys.nkp"
+ File keyFile = tempFolder.newFile(keyFileName)
+ final int KEY_COUNT = 5
+ List<String> lines = []
+ KEY_COUNT.times { int i ->
+ lines.add("key${i + 1}=${generateEncryptedKey(masterKey)}")
+ }
+
+ keyFile.text = lines.join("\n")
+
+ logger.info("File contents: \n${keyFile.text}")
+
+ // Act
+ def readKeys = CryptoUtils.readKeys(keyFile.path, masterKey)
+ logger.info("Read ${readKeys.size()} keys from ${keyFile.path}")
+
+ // Assert
+ assert readKeys.size() == KEY_COUNT
+ }
+
+ @Test
+ void testShouldReadKeysWithDuplicates() {
+ // Arrange
+ String masterKeyHex = KEY_HEX
+ SecretKey masterKey = new SecretKeySpec(Hex.decode(masterKeyHex), "AES")
+
+ // Generate the file
+ String keyFileName = "keys.nkp"
+ File keyFile = tempFolder.newFile(keyFileName)
+ final int KEY_COUNT = 3
+ List<String> lines = []
+ KEY_COUNT.times { int i ->
+ lines.add("key${i + 1}=${generateEncryptedKey(masterKey)}")
+ }
+
+ lines.add("key3=${generateEncryptedKey(masterKey)}")
+
+ keyFile.text = lines.join("\n")
+
+ logger.info("File contents: \n${keyFile.text}")
+
+ // Act
+ def readKeys = CryptoUtils.readKeys(keyFile.path, masterKey)
+ logger.info("Read ${readKeys.size()} keys from ${keyFile.path}")
+
+ // Assert
+ assert readKeys.size() == KEY_COUNT
+ }
+
+ @Test
+ void testShouldReadKeysWithSomeMalformed() {
+ // Arrange
+ String masterKeyHex = KEY_HEX
+ SecretKey masterKey = new SecretKeySpec(Hex.decode(masterKeyHex), "AES")
+
+ // Generate the file
+ String keyFileName = "keys.nkp"
+ File keyFile = tempFolder.newFile(keyFileName)
+ final int KEY_COUNT = 5
+ List<String> lines = []
+ KEY_COUNT.times { int i ->
+ lines.add("key${i + 1}=${generateEncryptedKey(masterKey)}")
+ }
+
+ // Insert the malformed keys in the middle
+ lines.add(2, "keyX1==${generateEncryptedKey(masterKey)}")
+ lines.add(4, "=${generateEncryptedKey(masterKey)}")
+ lines.add(6, "keyX3=non Base64-encoded data")
+
+ keyFile.text = lines.join("\n")
+
+ logger.info("File contents: \n${keyFile.text}")
+
+ // Act
+ def readKeys = CryptoUtils.readKeys(keyFile.path, masterKey)
+ logger.info("Read ${readKeys.size()} keys from ${keyFile.path}")
+
+ // Assert
+ assert readKeys.size() == KEY_COUNT
+ }
+
+ @Test
+ void testShouldNotReadKeysIfAllMalformed() {
+ // Arrange
+ String masterKeyHex = KEY_HEX
+ SecretKey masterKey = new SecretKeySpec(Hex.decode(masterKeyHex), "AES")
+
+ // Generate the file
+ String keyFileName = "keys.nkp"
+ File keyFile = tempFolder.newFile(keyFileName)
+ final int KEY_COUNT = 5
+ List<String> lines = []
+
+ // All of these keys are malformed
+ KEY_COUNT.times { int i ->
+ lines.add("key${i + 1}=${generateEncryptedKey(masterKey)[0..<-4]}")
+ }
+
+ keyFile.text = lines.join("\n")
+
+ logger.info("File contents: \n${keyFile.text}")
+
+ // Act
+ def msg = shouldFail(KeyManagementException) {
+ def readKeys = CryptoUtils.readKeys(keyFile.path, masterKey)
+ logger.info("Read ${readKeys.size()} keys from ${keyFile.path}")
+ }
+
+ // Assert
+ assert msg.getMessage() == "The provided file contained no valid keys"
+ }
+
+ @Test
+ void testShouldNotReadKeysIfEmptyOrMissing() {
+ // Arrange
+ String masterKeyHex = KEY_HEX
+ SecretKey masterKey = new SecretKeySpec(Hex.decode(masterKeyHex), "AES")
+
+ // Generate the file
+ String keyFileName = "empty.nkp"
+ File keyFile = tempFolder.newFile(keyFileName)
+ logger.info("File contents: \n${keyFile.text}")
+
+ // Act
+ def missingMsg = shouldFail(KeyManagementException) {
+ def readKeys = CryptoUtils.readKeys(keyFile.path, masterKey)
+ logger.info("Read ${readKeys.size()} keys from ${keyFile.path}")
+ }
+ logger.expected("Missing file: ${missingMsg}")
+
+ def emptyMsg = shouldFail(KeyManagementException) {
+ def readKeys = CryptoUtils.readKeys(null, masterKey)
+ logger.info("Read ${readKeys.size()} keys from ${null}")
+ }
+ logger.expected("Empty file: ${emptyMsg}")
+
+ // Assert
+ assert missingMsg.getMessage() == "The provided file contained no valid keys"
+ assert emptyMsg.getMessage() == "The key provider file is not present and readable"
+ }
+
+ private static String generateEncryptedKey(SecretKey masterKey) {
+ byte[] ivBytes = new byte[16]
+ byte[] keyBytes = new byte[isUnlimitedStrengthCryptoAvailable() ? 32 : 16]
+
+ SecureRandom sr = new SecureRandom()
+ sr.nextBytes(ivBytes)
+ sr.nextBytes(keyBytes)
+
+ Cipher masterCipher = Cipher.getInstance("AES/GCM/NoPadding", "BC")
+ masterCipher.init(Cipher.ENCRYPT_MODE, masterKey, new IvParameterSpec(ivBytes))
+ byte[] cipherBytes = masterCipher.doFinal(keyBytes)
+
+ Base64.encoder.encodeToString(CryptoUtils.concatByteArrays(ivBytes, cipherBytes))
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-data-provenance-utils/src/test/java/org/apache/nifi/provenance/EncryptionExceptionTest.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-data-provenance-utils/src/test/java/org/apache/nifi/provenance/EncryptionExceptionTest.java b/nifi-commons/nifi-data-provenance-utils/src/test/java/org/apache/nifi/provenance/EncryptionExceptionTest.java
new file mode 100644
index 0000000..e23d492
--- /dev/null
+++ b/nifi-commons/nifi-data-provenance-utils/src/test/java/org/apache/nifi/provenance/EncryptionExceptionTest.java
@@ -0,0 +1,27 @@
+/*
+ * 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.provenance;
+
+import org.junit.Test;
+
+public class EncryptionExceptionTest {
+
+ @Test
+ public void testShouldTriggerGroovyTestExecution() {
+ // This method does nothing but tell Maven to run the groovy tests
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-data-provenance-utils/src/test/resources/logback-test.xml
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-data-provenance-utils/src/test/resources/logback-test.xml b/nifi-commons/nifi-data-provenance-utils/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..c10508d
--- /dev/null
+++ b/nifi-commons/nifi-data-provenance-utils/src/test/resources/logback-test.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<configuration scan="true" scanPeriod="30 seconds">
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%-4r [%t] %-5p %c - %m%n</pattern>
+ </encoder>
+ </appender>
+
+ <!-- valid logging levels: TRACE, DEBUG, INFO, WARN, ERROR -->
+ <logger name="org.apache.nifi" level="DEBUG"/>
+
+ <root level="INFO">
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
+
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index 3d657fb..d69a280 100644
--- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -32,6 +32,7 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* The NiFiProperties class holds all properties which are needed for various
@@ -116,6 +117,11 @@ public abstract class NiFiProperties {
public static final String PROVENANCE_INDEXED_ATTRIBUTES = "nifi.provenance.repository.indexed.attributes";
public static final String PROVENANCE_INDEX_SHARD_SIZE = "nifi.provenance.repository.index.shard.size";
public static final String PROVENANCE_JOURNAL_COUNT = "nifi.provenance.repository.journal.count";
+ public static final String PROVENANCE_REPO_ENCRYPTION_KEY = "nifi.provenance.repository.encryption.key";
+ public static final String PROVENANCE_REPO_ENCRYPTION_KEY_ID = "nifi.provenance.repository.encryption.key.id";
+ public static final String PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_IMPLEMENTATION_CLASS = "nifi.provenance.repository.encryption.key.provider.implementation";
+ public static final String PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_LOCATION = "nifi.provenance.repository.encryption.key.provider.location";
+ public static final String PROVENANCE_REPO_DEBUG_FREQUENCY = "nifi.provenance.repository.debug.frequency";
// component status repository properties
public static final String COMPONENT_STATUS_REPOSITORY_IMPLEMENTATION = "nifi.components.status.repository.implementation";
@@ -769,7 +775,7 @@ public abstract class NiFiProperties {
/**
* Returns true if client certificates are required for REST API. Determined
* if the following conditions are all true:
- *
+ * <p>
* - login identity provider is not populated
* - Kerberos service support is not enabled
*
@@ -1034,6 +1040,61 @@ public abstract class NiFiProperties {
return getPropertyKeys().size();
}
+ public String getProvenanceRepoEncryptionKeyId() {
+ return getProperty(PROVENANCE_REPO_ENCRYPTION_KEY_ID);
+ }
+
+ /**
+ * Returns the active provenance repository encryption key if a {@code StaticKeyProvider} is in use.
+ * If no key ID is specified in the properties file, the default
+ * {@code nifi.provenance.repository.encryption.key} value is returned. If a key ID is specified in
+ * {@code nifi.provenance.repository.encryption.key.id}, it will attempt to read from
+ * {@code nifi.provenance.repository.encryption.key.id.XYZ} where {@code XYZ} is the provided key
+ * ID. If that value is empty, it will use the default property
+ * {@code nifi.provenance.repository.encryption.key}.
+ *
+ * @return the provenance repository encryption key in hex form
+ */
+ public String getProvenanceRepoEncryptionKey() {
+ String keyId = getProvenanceRepoEncryptionKeyId();
+ String keyKey = StringUtils.isBlank(keyId) ? PROVENANCE_REPO_ENCRYPTION_KEY : PROVENANCE_REPO_ENCRYPTION_KEY + ".id." + keyId;
+ return getProperty(keyKey, getProperty(PROVENANCE_REPO_ENCRYPTION_KEY));
+ }
+
+ /**
+ * Returns a map of keyId -> key in hex loaded from the {@code nifi.properties} file if a
+ * {@code StaticKeyProvider} is defined. If {@code FileBasedKeyProvider} is defined, use
+ * {@code CryptoUtils#readKeys()} instead -- this method will return an empty map.
+ *
+ * @return a Map of the keys identified by key ID
+ */
+ public Map<String, String> getProvenanceRepoEncryptionKeys() {
+ Map<String, String> keys = new HashMap<>();
+ List<String> keyProperties = getProvenanceRepositoryEncryptionKeyProperties();
+
+ // Retrieve the actual key values and store non-empty values in the map
+ for (String prop : keyProperties) {
+ final String value = getProperty(prop);
+ if (!StringUtils.isBlank(value)) {
+ if (prop.equalsIgnoreCase(PROVENANCE_REPO_ENCRYPTION_KEY)) {
+ prop = getProvenanceRepoEncryptionKeyId();
+ } else {
+ // Extract nifi.provenance.repository.encryption.key.id.key1 -> key1
+ prop = prop.substring(prop.lastIndexOf(".") + 1);
+ }
+ keys.put(prop, value);
+ }
+ }
+ return keys;
+ }
+
+ private List<String> getProvenanceRepositoryEncryptionKeyProperties() {
+ // Filter all the property keys that define a key
+ return getPropertyKeys().stream().filter(k ->
+ k.startsWith(PROVENANCE_REPO_ENCRYPTION_KEY_ID + ".") || k.equalsIgnoreCase(PROVENANCE_REPO_ENCRYPTION_KEY)
+ ).collect(Collectors.toList());
+ }
+
/**
* Creates an instance of NiFiProperties. This should likely not be called
* by any classes outside of the NiFi framework but can be useful by the
@@ -1042,11 +1103,11 @@ public abstract class NiFiProperties {
* file specified cannot be found/read a runtime exception will be thrown.
* If one is not specified no properties will be loaded by default.
*
- * @param propertiesFilePath if provided properties will be loaded from
- * given file; else will be loaded from System property. Can be null.
+ * @param propertiesFilePath if provided properties will be loaded from
+ * given file; else will be loaded from System property. Can be null.
* @param additionalProperties allows overriding of properties with the
- * supplied values. these will be applied after loading from any properties
- * file. Can be null or empty.
+ * supplied values. these will be applied after loading from any properties
+ * file. Can be null or empty.
* @return NiFiProperties
*/
public static NiFiProperties createBasicNiFiProperties(final String propertiesFilePath, final Map<String, String> additionalProperties) {
@@ -1108,10 +1169,9 @@ public abstract class NiFiProperties {
public void validate() {
// REMOTE_INPUT_HOST should be a valid hostname
String remoteInputHost = getProperty(REMOTE_INPUT_HOST);
- if(!StringUtils.isBlank(remoteInputHost) && remoteInputHost.split(":").length > 1) { // no scheme/port needed here (http://)
+ if (!StringUtils.isBlank(remoteInputHost) && remoteInputHost.split(":").length > 1) { // no scheme/port needed here (http://)
throw new IllegalArgumentException(remoteInputHost + " is not a correct value for " + REMOTE_INPUT_HOST + ". It should be a valid hostname without protocol or port.");
}
// Other properties to validate...
}
-
}
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-security-utils/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/pom.xml b/nifi-commons/nifi-security-utils/pom.xml
index e2e5ee1..b004c8b 100644
--- a/nifi-commons/nifi-security-utils/pom.xml
+++ b/nifi-commons/nifi-security-utils/pom.xml
@@ -44,6 +44,10 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java
index 53664f1..a1ef2a4 100644
--- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/EncryptionMethod.java
@@ -16,13 +16,13 @@
*/
package org.apache.nifi.security.util;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* Enumeration capturing essential information about the various encryption
* methods that might be supported.
- *
*/
public enum EncryptionMethod {
@@ -105,4 +105,15 @@ public enum EncryptionMethod {
builder.append("Keyed cipher", isKeyedCipher());
return builder.toString();
}
+
+ public static EncryptionMethod forAlgorithm(String algorithm) {
+ if (StringUtils.isNotBlank(algorithm)) {
+ for (EncryptionMethod em : EncryptionMethod.values()) {
+ if (em.algorithm.equalsIgnoreCase(algorithm)) {
+ return em;
+ }
+ }
+ }
+ return null;
+ }
}
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AESKeyedCipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AESKeyedCipherProvider.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AESKeyedCipherProvider.java
new file mode 100644
index 0000000..617a3e5
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/AESKeyedCipherProvider.java
@@ -0,0 +1,152 @@
+/*
+ * 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.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.List;
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is a standard implementation of {@link KeyedCipherProvider} which supports {@code AES} cipher families with arbitrary modes of operation (currently only {@code CBC}, {@code CTR}, and {@code
+ * GCM} are supported as {@link EncryptionMethod}s.
+ */
+public class AESKeyedCipherProvider extends KeyedCipherProvider {
+ private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProvider.class);
+ private static final int IV_LENGTH = 16;
+ private static final List<Integer> VALID_KEY_LENGTHS = Arrays.asList(128, 192, 256);
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. 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 key the key
+ * @param iv the IV or nonce (cannot be all 0x00)
+ * @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, SecretKey key, byte[] iv, boolean encryptMode) throws Exception {
+ try {
+ return getInitializedCipher(encryptionMethod, key, iv, encryptMode);
+ } catch (IllegalArgumentException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ProcessException("Error initializing the cipher", e);
+ }
+ }
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception.
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param key the key
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher or if decryption is requested
+ */
+ @Override
+ public Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception {
+ return getCipher(encryptionMethod, key, new byte[0], encryptMode);
+ }
+
+ protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv,
+ boolean encryptMode) throws NoSuchAlgorithmException, NoSuchProviderException,
+ InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, UnsupportedEncodingException {
+ if (encryptionMethod == null) {
+ throw new IllegalArgumentException("The encryption method must be specified");
+ }
+
+ if (!encryptionMethod.isKeyedCipher()) {
+ throw new IllegalArgumentException(encryptionMethod.name() + " requires a PBECipherProvider");
+ }
+
+ String algorithm = encryptionMethod.getAlgorithm();
+ String provider = encryptionMethod.getProvider();
+
+ if (key == null) {
+ throw new IllegalArgumentException("The key must be specified");
+ }
+
+ if (!isValidKeyLength(key)) {
+ throw new IllegalArgumentException("The key must be of length [" + StringUtils.join(VALID_KEY_LENGTHS, ", ") + "]");
+ }
+
+ Cipher cipher = Cipher.getInstance(algorithm, provider);
+ final String operation = encryptMode ? "encrypt" : "decrypt";
+
+ boolean ivIsInvalid = false;
+
+ // If an IV was not provided already, generate a random IV and inject it in the cipher
+ int ivLength = cipher.getBlockSize();
+ if (iv.length != ivLength) {
+ logger.warn("An IV was provided of length {} bytes for {}ion but should be {} bytes", iv.length, operation, ivLength);
+ ivIsInvalid = true;
+ }
+
+ final byte[] emptyIv = new byte[ivLength];
+ if (Arrays.equals(iv, emptyIv)) {
+ logger.warn("An empty IV was provided of length {} for {}ion", iv.length, operation);
+ ivIsInvalid = true;
+ }
+
+ if (ivIsInvalid) {
+ if (encryptMode) {
+ logger.warn("Generating new IV. The value can be obtained in the calling code by invoking 'cipher.getIV()';");
+ iv = generateIV();
+ } else {
+ // Can't decrypt without an IV
+ throw new IllegalArgumentException("Cannot decrypt without a valid IV");
+ }
+ }
+ cipher.init(encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
+
+ return cipher;
+ }
+
+ private boolean isValidKeyLength(SecretKey key) {
+ return VALID_KEY_LENGTHS.contains(key.getEncoded().length * 8);
+ }
+
+ /**
+ * Generates a new random IV of 16 bytes using {@link java.security.SecureRandom}.
+ *
+ * @return the IV
+ */
+ public byte[] generateIV() {
+ byte[] iv = new byte[IV_LENGTH];
+ new SecureRandom().nextBytes(iv);
+ return iv;
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherProvider.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherProvider.java
new file mode 100644
index 0000000..e3632b2
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherProvider.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto;
+
+/**
+ * Marker interface for cipher providers.
+ */
+public interface CipherProvider {
+}
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherUtility.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherUtility.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherUtility.java
new file mode 100644
index 0000000..6ba8056
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/CipherUtility.java
@@ -0,0 +1,328 @@
+/*
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.crypto.Cipher;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.apache.nifi.stream.io.ByteArrayOutputStream;
+import org.apache.nifi.stream.io.StreamUtils;
+
+public class CipherUtility {
+
+ public static final int BUFFER_SIZE = 65536;
+ private static final Pattern KEY_LENGTH_PATTERN = Pattern.compile("([\\d]+)BIT");
+
+ private static final Map<String, Integer> MAX_PASSWORD_LENGTH_BY_ALGORITHM;
+
+ static {
+ Map<String, Integer> aMap = new HashMap<>();
+ /**
+ * These values were determined empirically by running {@link NiFiLegacyCipherProviderGroovyTest#testShouldDetermineDependenceOnUnlimitedStrengthCrypto()}
+ *, which evaluates each algorithm in a try/catch harness with increasing password size until it throws an exception.
+ * This was performed on a JVM without the Unlimited Strength Jurisdiction cryptographic policy files installed.
+ */
+ aMap.put("PBEWITHMD5AND128BITAES-CBC-OPENSSL", 16);
+ aMap.put("PBEWITHMD5AND192BITAES-CBC-OPENSSL", 16);
+ aMap.put("PBEWITHMD5AND256BITAES-CBC-OPENSSL", 16);
+ aMap.put("PBEWITHMD5ANDDES", 16);
+ aMap.put("PBEWITHMD5ANDRC2", 16);
+ aMap.put("PBEWITHSHA1ANDRC2", 16);
+ aMap.put("PBEWITHSHA1ANDDES", 16);
+ aMap.put("PBEWITHSHAAND128BITAES-CBC-BC", 7);
+ aMap.put("PBEWITHSHAAND192BITAES-CBC-BC", 7);
+ aMap.put("PBEWITHSHAAND256BITAES-CBC-BC", 7);
+ aMap.put("PBEWITHSHAAND40BITRC2-CBC", 7);
+ aMap.put("PBEWITHSHAAND128BITRC2-CBC", 7);
+ aMap.put("PBEWITHSHAAND40BITRC4", 7);
+ aMap.put("PBEWITHSHAAND128BITRC4", 7);
+ aMap.put("PBEWITHSHA256AND128BITAES-CBC-BC", 7);
+ aMap.put("PBEWITHSHA256AND192BITAES-CBC-BC", 7);
+ aMap.put("PBEWITHSHA256AND256BITAES-CBC-BC", 7);
+ aMap.put("PBEWITHSHAAND2-KEYTRIPLEDES-CBC", 7);
+ aMap.put("PBEWITHSHAAND3-KEYTRIPLEDES-CBC", 7);
+ aMap.put("PBEWITHSHAANDTWOFISH-CBC", 7);
+ MAX_PASSWORD_LENGTH_BY_ALGORITHM = Collections.unmodifiableMap(aMap);
+ }
+
+ /**
+ * Returns the cipher algorithm from the full algorithm name. Useful for getting key lengths, etc.
+ * <p/>
+ * Ex: PBEWITHMD5AND128BITAES-CBC-OPENSSL -> AES
+ *
+ * @param algorithm the full algorithm name
+ * @return the generic cipher name or the full algorithm if one cannot be extracted
+ */
+ public static String parseCipherFromAlgorithm(final String algorithm) {
+ if (StringUtils.isEmpty(algorithm)) {
+ return algorithm;
+ }
+ String formattedAlgorithm = algorithm.toUpperCase();
+
+ // This is not optimal but the algorithms do not have a standard format
+ final String AES = "AES";
+ final String TDES = "TRIPLEDES";
+ final String TDES_ALTERNATE = "DESEDE";
+ final String DES = "DES";
+ final String RC4 = "RC4";
+ final String RC2 = "RC2";
+ final String TWOFISH = "TWOFISH";
+ final List<String> SYMMETRIC_CIPHERS = Arrays.asList(AES, TDES, TDES_ALTERNATE, DES, RC4, RC2, TWOFISH);
+
+ // The algorithms contain "TRIPLEDES" but the cipher name is "DESede"
+ final String ACTUAL_TDES_CIPHER = "DESede";
+
+ for (String cipher : SYMMETRIC_CIPHERS) {
+ if (formattedAlgorithm.contains(cipher)) {
+ if (cipher.equals(TDES) || cipher.equals(TDES_ALTERNATE)) {
+ return ACTUAL_TDES_CIPHER;
+ } else {
+ return cipher;
+ }
+ }
+ }
+
+ return algorithm;
+ }
+
+ /**
+ * Returns the cipher key length from the full algorithm name. Useful for getting key lengths, etc.
+ * <p/>
+ * Ex: PBEWITHMD5AND128BITAES-CBC-OPENSSL -> 128
+ *
+ * @param algorithm the full algorithm name
+ * @return the key length or -1 if one cannot be extracted
+ */
+ public static int parseKeyLengthFromAlgorithm(final String algorithm) {
+ int keyLength = parseActualKeyLengthFromAlgorithm(algorithm);
+ if (keyLength != -1) {
+ return keyLength;
+ } else {
+ // Key length not explicitly named in algorithm
+ String cipher = parseCipherFromAlgorithm(algorithm);
+ return getDefaultKeyLengthForCipher(cipher);
+ }
+ }
+
+ private static int parseActualKeyLengthFromAlgorithm(final String algorithm) {
+ Matcher matcher = KEY_LENGTH_PATTERN.matcher(algorithm);
+ if (matcher.find()) {
+ return Integer.parseInt(matcher.group(1));
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Returns true if the provided key length is a valid key length for the provided cipher family. Does not reflect if the Unlimited Strength Cryptography Jurisdiction Policies are installed.
+ * Does not reflect if the key length is correct for a specific combination of cipher and PBE-derived key length.
+ * <p/>
+ * Ex:
+ * <p/>
+ * 256 is valid for {@code AES/CBC/PKCS7Padding} but not {@code PBEWITHMD5AND128BITAES-CBC-OPENSSL}. However, this method will return {@code true} for both because it only gets the cipher
+ * family, {@code AES}.
+ * <p/>
+ * 64, AES -> false
+ * [128, 192, 256], AES -> true
+ *
+ * @param keyLength the key length in bits
+ * @param cipher the cipher family
+ * @return true if this key length is valid
+ */
+ public static boolean isValidKeyLength(int keyLength, final String cipher) {
+ if (StringUtils.isEmpty(cipher)) {
+ return false;
+ }
+ return getValidKeyLengthsForAlgorithm(cipher).contains(keyLength);
+ }
+
+ /**
+ * Returns true if the provided key length is a valid key length for the provided algorithm. Does not reflect if the Unlimited Strength Cryptography Jurisdiction Policies are installed.
+ * <p/>
+ * Ex:
+ * <p/>
+ * 256 is valid for {@code AES/CBC/PKCS7Padding} but not {@code PBEWITHMD5AND128BITAES-CBC-OPENSSL}.
+ * <p/>
+ * 64, AES/CBC/PKCS7Padding -> false
+ * [128, 192, 256], AES/CBC/PKCS7Padding -> true
+ * <p/>
+ * 128, PBEWITHMD5AND128BITAES-CBC-OPENSSL -> true
+ * [192, 256], PBEWITHMD5AND128BITAES-CBC-OPENSSL -> false
+ *
+ * @param keyLength the key length in bits
+ * @param algorithm the specific algorithm
+ * @return true if this key length is valid
+ */
+ public static boolean isValidKeyLengthForAlgorithm(int keyLength, final String algorithm) {
+ if (StringUtils.isEmpty(algorithm)) {
+ return false;
+ }
+ return getValidKeyLengthsForAlgorithm(algorithm).contains(keyLength);
+ }
+
+ public static List<Integer> getValidKeyLengthsForAlgorithm(String algorithm) {
+ List<Integer> validKeyLengths = new ArrayList<>();
+ if (StringUtils.isEmpty(algorithm)) {
+ return validKeyLengths;
+ }
+
+ // Some algorithms specify a single key size
+ int keyLength = parseActualKeyLengthFromAlgorithm(algorithm);
+ if (keyLength != -1) {
+ validKeyLengths.add(keyLength);
+ return validKeyLengths;
+ }
+
+ // The algorithm does not specify a key size
+ String cipher = parseCipherFromAlgorithm(algorithm);
+ switch (cipher.toUpperCase()) {
+ case "DESEDE":
+ // 3DES keys have the cryptographic strength of 7/8 because of parity bits, but are often represented with n*8 bytes
+ return Arrays.asList(56, 64, 112, 128, 168, 192);
+ case "DES":
+ return Arrays.asList(56, 64);
+ case "RC2":
+ case "RC4":
+ case "RC5":
+ /** These ciphers can have arbitrary length keys but that's a really bad idea, {@see http://crypto.stackexchange.com/a/9963/12569}.
+ * Also, RC* is deprecated and should be considered insecure */
+ for (int i = 40; i <= 2048; i++) {
+ validKeyLengths.add(i);
+ }
+ return validKeyLengths;
+ case "AES":
+ case "TWOFISH":
+ return Arrays.asList(128, 192, 256);
+ default:
+ return validKeyLengths;
+ }
+ }
+
+ private static int getDefaultKeyLengthForCipher(String cipher) {
+ if (StringUtils.isEmpty(cipher)) {
+ return -1;
+ }
+ cipher = cipher.toUpperCase();
+ switch (cipher) {
+ case "DESEDE":
+ return 112;
+ case "DES":
+ return 64;
+ case "RC2":
+ case "RC4":
+ case "RC5":
+ default:
+ return 128;
+ }
+ }
+
+ public static void processStreams(Cipher cipher, InputStream in, OutputStream out) {
+ try {
+ final byte[] buffer = new byte[BUFFER_SIZE];
+ int len;
+ while ((len = in.read(buffer)) > 0) {
+ final byte[] decryptedBytes = cipher.update(buffer, 0, len);
+ if (decryptedBytes != null) {
+ out.write(decryptedBytes);
+ }
+ }
+
+ out.write(cipher.doFinal());
+ } catch (Exception e) {
+ throw new ProcessException(e);
+ }
+ }
+
+ public static byte[] readBytesFromInputStream(InputStream in, String label, int limit, byte[] delimiter) throws IOException, ProcessException {
+ if (in == null) {
+ throw new IllegalArgumentException("Cannot read " + label + " from null InputStream");
+ }
+
+ // If the value is not detected within the first n bytes, throw an exception
+ in.mark(limit);
+
+ // The first n bytes of the input stream contain the value up to the custom delimiter
+ ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
+ byte[] stoppedBy = StreamUtils.copyExclusive(in, bytesOut, limit + delimiter.length, delimiter);
+
+ if (stoppedBy != null) {
+ byte[] bytes = bytesOut.toByteArray();
+ return bytes;
+ }
+
+ // If no delimiter was found, reset the cursor
+ in.reset();
+ return null;
+ }
+
+ public static void writeBytesToOutputStream(OutputStream out, byte[] value, String label, byte[] delimiter) throws IOException {
+ if (out == null) {
+ throw new IllegalArgumentException("Cannot write " + label + " to null OutputStream");
+ }
+ out.write(value);
+ out.write(delimiter);
+ }
+
+ public static String encodeBase64NoPadding(final byte[] bytes) {
+ String base64UrlNoPadding = Base64.encodeBase64URLSafeString(bytes);
+ base64UrlNoPadding = base64UrlNoPadding.replaceAll("-", "+");
+ base64UrlNoPadding = base64UrlNoPadding.replaceAll("_", "/");
+ return base64UrlNoPadding;
+ }
+
+ public static boolean passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(final int passwordLength, EncryptionMethod encryptionMethod) {
+ if (encryptionMethod == null) {
+ throw new IllegalArgumentException("Cannot evaluate an empty encryption method algorithm");
+ }
+
+ return passwordLength <= getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod);
+ }
+
+ public static int getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(EncryptionMethod encryptionMethod) {
+ if (encryptionMethod == null) {
+ throw new IllegalArgumentException("Cannot evaluate an empty encryption method algorithm");
+ }
+
+ if (MAX_PASSWORD_LENGTH_BY_ALGORITHM.containsKey(encryptionMethod.getAlgorithm())) {
+ return MAX_PASSWORD_LENGTH_BY_ALGORITHM.get(encryptionMethod.getAlgorithm());
+ } else {
+ return -1;
+ }
+ }
+
+ public static byte[] concatBytes(byte[]... arrays) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ for (byte[] bytes : arrays) {
+ outputStream.write(bytes);
+ }
+
+ return outputStream.toByteArray();
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/KeyedCipherProvider.java
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/KeyedCipherProvider.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/KeyedCipherProvider.java
new file mode 100644
index 0000000..719150f
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/crypto/KeyedCipherProvider.java
@@ -0,0 +1,72 @@
+/*
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.security.util.EncryptionMethod;
+
+public abstract class KeyedCipherProvider implements CipherProvider {
+ static final byte[] IV_DELIMITER = "NiFiIV".getBytes(StandardCharsets.UTF_8);
+ // This is 16 bytes for AES but can vary for other ciphers
+ static final int MAX_IV_LIMIT = 16;
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. 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 key the key
+ * @param iv the IV or nonce
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher
+ */
+ abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv, boolean encryptMode) throws Exception;
+
+ /**
+ * Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception.
+ *
+ * @param encryptionMethod the {@link EncryptionMethod}
+ * @param key the key
+ * @param encryptMode true for encrypt, false for decrypt
+ * @return the initialized cipher
+ * @throws Exception if there is a problem initializing the cipher or if decryption is requested
+ */
+ abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception;
+
+ /**
+ * Generates a new random IV of the correct length.
+ *
+ * @return the IV
+ */
+ abstract byte[] generateIV();
+
+ public byte[] readIV(InputStream in) throws IOException, ProcessException {
+ return CipherUtility.readBytesFromInputStream(in, "IV", MAX_IV_LIMIT, IV_DELIMITER);
+ }
+
+ public void writeIV(byte[] iv, OutputStream out) throws IOException {
+ CipherUtility.writeBytesToOutputStream(out, iv, "IV", IV_DELIMITER);
+ }
+}
http://git-wip-us.apache.org/repos/asf/nifi/blob/7d242076/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/crypto/AESKeyedCipherProviderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/crypto/AESKeyedCipherProviderGroovyTest.groovy b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/crypto/AESKeyedCipherProviderGroovyTest.groovy
new file mode 100644
index 0000000..8082149
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/crypto/AESKeyedCipherProviderGroovyTest.groovy
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License") you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.security.util.crypto
+
+import org.apache.commons.codec.binary.Hex
+import org.apache.nifi.security.util.EncryptionMethod
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.SecretKey
+import javax.crypto.spec.SecretKeySpec
+import java.security.SecureRandom
+import java.security.Security
+
+import static groovy.test.GroovyAssert.shouldFail
+
+@RunWith(JUnit4.class)
+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
+ static void setUpOnce() throws Exception {
+ Security.addProvider(new BouncyCastleProvider())
+
+ logger.metaClass.methodMissing = { String name, args ->
+ logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+ }
+ }
+
+ @Before
+ void setUp() throws Exception {
+ }
+
+ @After
+ void tearDown() throws Exception {
+ }
+
+ private static boolean isUnlimitedStrengthCryptoAvailable() {
+ Cipher.getMaxAllowedKeyLength("AES") > 128
+ }
+
+ @Test
+ 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
+ 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 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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/7d242076/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/crypto/CipherUtilityGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/crypto/CipherUtilityGroovyTest.groovy b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/util/crypto/CipherUtilityGroovyTest.groovy
new file mode 100644
index 0000000..8f092f3
--- /dev/null
+++ b/nifi-commons/nifi-security-utils/src/test/groovy/org/apache/nifi/security/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.security.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/7d242076/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
----------------------------------------------------------------------
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
index 7853591..5b2ea41 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/FlowController.java
@@ -16,7 +16,38 @@
*/
package org.apache.nifi.controller;
+import static java.util.Objects.requireNonNull;
+
import com.sun.jersey.api.client.ClientHandlerException;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+import javax.net.ssl.SSLContext;
import org.apache.commons.collections4.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.action.Action;
@@ -85,7 +116,6 @@ import org.apache.nifi.controller.repository.FlowFileRecord;
import org.apache.nifi.controller.repository.FlowFileRepository;
import org.apache.nifi.controller.repository.FlowFileSwapManager;
import org.apache.nifi.controller.repository.QueueProvider;
-import org.apache.nifi.controller.repository.RepositoryRecord;
import org.apache.nifi.controller.repository.RepositoryStatusReport;
import org.apache.nifi.controller.repository.StandardCounterRepository;
import org.apache.nifi.controller.repository.StandardFlowFileRecord;
@@ -217,38 +247,6 @@ import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.net.ssl.SSLContext;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.Future;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.stream.Collectors;
-
-import static java.util.Objects.requireNonNull;
-
public class FlowController implements EventAccess, ControllerServiceProvider, ReportingTaskProvider,
QueueProvider, Authorizable, ProvenanceAuthorizableFactory, NodeTypeProvider, IdentifierLookup, ReloadComponent {
@@ -3841,7 +3839,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
final ProvenanceEventRecord sendEvent = new StandardProvenanceEventRecord.Builder()
.setEventType(ProvenanceEventType.DOWNLOAD)
.setFlowFileUUID(provEvent.getFlowFileUuid())
- .setAttributes(provEvent.getAttributes(), Collections.<String, String>emptyMap())
+ .setAttributes(provEvent.getAttributes(), Collections.emptyMap())
.setCurrentContentClaim(resourceClaim.getContainer(), resourceClaim.getSection(), resourceClaim.getId(), offset, size)
.setTransitUri(requestUri)
.setEventTime(System.currentTimeMillis())
@@ -3883,7 +3881,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
final StandardProvenanceEventRecord.Builder sendEventBuilder = new StandardProvenanceEventRecord.Builder()
.setEventType(ProvenanceEventType.DOWNLOAD)
.setFlowFileUUID(flowFile.getAttribute(CoreAttributes.UUID.key()))
- .setAttributes(flowFile.getAttributes(), Collections.<String, String>emptyMap())
+ .setAttributes(flowFile.getAttributes(), Collections.emptyMap())
.setTransitUri(requestUri)
.setEventTime(System.currentTimeMillis())
.setFlowFileEntryDate(flowFile.getEntryDate())
@@ -4062,7 +4060,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
.addChildUuid(newFlowFileUUID)
.addParentUuid(parentUUID)
.setFlowFileUUID(parentUUID)
- .setAttributes(Collections.<String, String>emptyMap(), flowFileRecord.getAttributes())
+ .setAttributes(Collections.emptyMap(), flowFileRecord.getAttributes())
.setCurrentContentClaim(event.getContentClaimContainer(), event.getContentClaimSection(), event.getContentClaimIdentifier(), event.getContentClaimOffset(), event.getFileSize())
.setDetails("Replay requested by " + user.getIdentity())
.setEventTime(System.currentTimeMillis())
@@ -4077,7 +4075,7 @@ public class FlowController implements EventAccess, ControllerServiceProvider, R
final StandardRepositoryRecord record = new StandardRepositoryRecord(queue);
record.setWorking(flowFileRecord);
record.setDestination(queue);
- flowFileRepository.updateRepository(Collections.<RepositoryRecord>singleton(record));
+ flowFileRepository.updateRepository(Collections.singleton(record));
// Enqueue the data
queue.put(flowFileRecord);
|