NIFI-4139

- Moved key provider interface and implementations from nifi-data-provenance-utils module to nifi-security-utils module.
- Refactored duplicate byte[] concatenation methods from utility classes and removed deprecation warnings from CipherUtility.
- Created KeyProviderFactory to encapsulate key provider instantiation logic.
- Added logic to handle legacy package configuration values for key providers.
- Added unit tests.
- Added resource files for un/limited strength cryptography scenarios.
- Added ASL to test resources.
- Moved legacy FQCN handling logic to CryptUtils.
- Added unit tests to ensure application startup logic handles legacy FQCNs.
- Moved master key extraction/provision out of FBKP.
- Removed nifi-security-utils dependency on nifi-properties-loader module.
- Added unit tests.
This commit is contained in:
Andy LoPresto 2017-07-27 17:11:10 -07:00 committed by Matt Gilman
parent 8b54c2604c
commit 675d989003
No known key found for this signature in database
GPG Key ID: DF61EC19432AEE37
21 changed files with 471 additions and 86 deletions

View File

@ -31,6 +31,8 @@ import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.security.kms.CryptoUtils;
import org.apache.nifi.security.kms.KeyProvider;
import org.apache.nifi.security.util.EncryptionMethod; import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider; import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;

View File

@ -17,6 +17,7 @@
package org.apache.nifi.provenance; package org.apache.nifi.provenance;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import org.apache.nifi.security.kms.KeyProvider;
public interface ProvenanceEventEncryptor { public interface ProvenanceEventEncryptor {

View File

@ -16,6 +16,8 @@
*/ */
package org.apache.nifi.provenance package org.apache.nifi.provenance
import org.apache.nifi.security.kms.CryptoUtils
import org.apache.nifi.security.kms.KeyProvider
import org.apache.nifi.security.util.EncryptionMethod import org.apache.nifi.security.util.EncryptionMethod
import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.nifi.provenance; package org.apache.nifi.security.kms;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
@ -43,8 +43,13 @@ import org.slf4j.LoggerFactory;
public class CryptoUtils { public class CryptoUtils {
private static final Logger logger = LoggerFactory.getLogger(StaticKeyProvider.class); private static final Logger logger = LoggerFactory.getLogger(StaticKeyProvider.class);
private static final String STATIC_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.provenance.StaticKeyProvider"; private static final String STATIC_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.security.kms.StaticKeyProvider";
private static final String FILE_BASED_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.provenance.FileBasedKeyProvider"; private static final String FILE_BASED_KEY_PROVIDER_CLASS_NAME = "org.apache.nifi.security.kms.FileBasedKeyProvider";
private static final String LEGACY_SKP_FQCN = "org.apache.nifi.provenance.StaticKeyProvider";
private static final String LEGACY_FBKP_FQCN = "org.apache.nifi.provenance.FileBasedKeyProvider";
private static final Pattern HEX_PATTERN = Pattern.compile("(?i)^[0-9a-f]+$"); private static final Pattern HEX_PATTERN = Pattern.compile("(?i)^[0-9a-f]+$");
private static final List<Integer> UNLIMITED_KEY_LENGTHS = Arrays.asList(32, 48, 64); private static final List<Integer> UNLIMITED_KEY_LENGTHS = Arrays.asList(32, 48, 64);
@ -101,6 +106,24 @@ public class CryptoUtils {
* @return true if the provided configuration is valid * @return true if the provided configuration is valid
*/ */
public static boolean isValidKeyProvider(String keyProviderImplementation, String keyProviderLocation, String keyId, Map<String, String> encryptionKeys) { public static boolean isValidKeyProvider(String keyProviderImplementation, String keyProviderLocation, String keyId, Map<String, String> encryptionKeys) {
logger.debug("Attempting to validate the key provider: keyProviderImplementation = "
+ keyProviderImplementation + " , keyProviderLocation = "
+ keyProviderLocation + " , keyId = "
+ keyId + " , encryptionKeys = "
+ ((encryptionKeys == null) ? "0" : encryptionKeys.size()));
try {
keyProviderImplementation = handleLegacyPackages(keyProviderImplementation);
} catch (KeyManagementException e) {
logger.error("The attempt to validate the key provider failed keyProviderImplementation = "
+ keyProviderImplementation + " , keyProviderLocation = "
+ keyProviderLocation + " , keyId = "
+ keyId + " , encryptionKeys = "
+ ((encryptionKeys == null) ? "0" : encryptionKeys.size()));
return false;
}
if (STATIC_KEY_PROVIDER_CLASS_NAME.equals(keyProviderImplementation)) { if (STATIC_KEY_PROVIDER_CLASS_NAME.equals(keyProviderImplementation)) {
// Ensure the keyId and key(s) are valid // Ensure the keyId and key(s) are valid
if (encryptionKeys == null) { if (encryptionKeys == null) {
@ -124,6 +147,19 @@ public class CryptoUtils {
} }
} }
static String handleLegacyPackages(String implementationClassName) throws KeyManagementException {
if (org.apache.nifi.util.StringUtils.isBlank(implementationClassName)) {
throw new KeyManagementException("Invalid key provider implementation provided: " + implementationClassName);
}
if (implementationClassName.equalsIgnoreCase(LEGACY_SKP_FQCN)) {
return StaticKeyProvider.class.getName();
} else if (implementationClassName.equalsIgnoreCase(LEGACY_FBKP_FQCN)) {
return FileBasedKeyProvider.class.getName();
} else {
return implementationClassName;
}
}
/** /**
* Returns true if the provided key is valid hex and is the correct length for the current system's JCE policies. * Returns true if the provided key is valid hex and is the correct length for the current system's JCE policies.
* *
@ -177,6 +213,10 @@ public class CryptoUtils {
if (StringUtils.isBlank(filepath)) { if (StringUtils.isBlank(filepath)) {
throw new KeyManagementException("The key provider file is not present and readable"); throw new KeyManagementException("The key provider file is not present and readable");
} }
if (masterKey == null) {
throw new KeyManagementException("The master key must be provided to decrypt the individual keys");
}
File file = new File(filepath); File file = new File(filepath);
if (!file.exists() || !file.canRead()) { if (!file.exists() || !file.canRead()) {
throw new KeyManagementException("The key provider file is not present and readable"); throw new KeyManagementException("The key provider file is not present and readable");

View File

@ -14,15 +14,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.nifi.provenance; package org.apache.nifi.security.kms;
import java.io.IOException;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.naming.OperationNotSupportedException; import javax.naming.OperationNotSupportedException;
import org.apache.nifi.properties.NiFiPropertiesLoader;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -31,26 +27,11 @@ public class FileBasedKeyProvider extends StaticKeyProvider {
private String filepath; private String filepath;
FileBasedKeyProvider(String location) throws KeyManagementException { public FileBasedKeyProvider(String location, SecretKey masterKey) throws KeyManagementException {
this(location, getMasterKey());
}
FileBasedKeyProvider(String location, SecretKey masterKey) throws KeyManagementException {
super(CryptoUtils.readKeys(location, masterKey)); super(CryptoUtils.readKeys(location, masterKey));
this.filepath = location; this.filepath = location;
} }
private static SecretKey getMasterKey() throws KeyManagementException {
try {
// Get the master encryption key from bootstrap.conf
String masterKeyHex = NiFiPropertiesLoader.extractKeyFromBootstrapFile();
return new SecretKeySpec(Hex.decode(masterKeyHex), "AES");
} catch (IOException e) {
logger.error("Encountered an error: ", e);
throw new KeyManagementException(e);
}
}
/** /**
* Adds the key to the provider and associates it with the given ID. Some implementations may not allow this operation. * Adds the key to the provider and associates it with the given ID. Some implementations may not allow this operation.
* *

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.nifi.provenance; package org.apache.nifi.security.kms;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.util.List; import java.util.List;

View File

@ -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.kms;
import java.security.KeyManagementException;
import java.util.Map;
import java.util.stream.Collectors;
import javax.crypto.SecretKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class KeyProviderFactory {
private static final Logger logger = LoggerFactory.getLogger(KeyProviderFactory.class);
public static KeyProvider buildKeyProvider(String implementationClassName, String keyProviderLocation, String keyId, Map<String, String> encryptionKeys,
SecretKey masterKey) throws KeyManagementException {
KeyProvider keyProvider;
implementationClassName = CryptoUtils.handleLegacyPackages(implementationClassName);
if (StaticKeyProvider.class.getName().equals(implementationClassName)) {
// Get all the keys (map) from config
if (CryptoUtils.isValidKeyProvider(implementationClassName, keyProviderLocation, keyId, encryptionKeys)) {
Map<String, SecretKey> formedKeys = encryptionKeys.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> {
try {
return CryptoUtils.formKeyFromHex(e.getValue());
} catch (KeyManagementException e1) {
// This should never happen because the hex has already been validated
logger.error("Encountered an error: ", e1);
return null;
}
}));
keyProvider = new StaticKeyProvider(formedKeys);
} else {
final String msg = "The StaticKeyProvider definition is not valid";
logger.error(msg);
throw new KeyManagementException(msg);
}
} else if (FileBasedKeyProvider.class.getName().equals(implementationClassName)) {
keyProvider = new FileBasedKeyProvider(keyProviderLocation, masterKey);
if (!keyProvider.keyExists(keyId)) {
throw new KeyManagementException("The specified key ID " + keyId + " is not in the key definition file");
}
} else {
throw new KeyManagementException("Invalid key provider implementation provided: " + implementationClassName);
}
return keyProvider;
}
public static boolean requiresMasterKey(String implementationClassName) throws KeyManagementException {
implementationClassName = CryptoUtils.handleLegacyPackages(implementationClassName);
return FileBasedKeyProvider.class.getName().equals(implementationClassName);
}
}

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.nifi.provenance; package org.apache.nifi.security.kms;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.util.ArrayList; import java.util.ArrayList;
@ -34,11 +34,11 @@ public class StaticKeyProvider implements KeyProvider {
private Map<String, SecretKey> keys = new HashMap<>(); private Map<String, SecretKey> keys = new HashMap<>();
StaticKeyProvider(String keyId, String keyHex) throws KeyManagementException { public StaticKeyProvider(String keyId, String keyHex) throws KeyManagementException {
this.keys.put(keyId, CryptoUtils.formKeyFromHex(keyHex)); this.keys.put(keyId, CryptoUtils.formKeyFromHex(keyHex));
} }
StaticKeyProvider(Map<String, SecretKey> keys) throws KeyManagementException { public StaticKeyProvider(Map<String, SecretKey> keys) throws KeyManagementException {
this.keys.putAll(keys); this.keys.putAll(keys);
} }

View File

@ -16,6 +16,7 @@
*/ */
package org.apache.nifi.security.util.crypto; package org.apache.nifi.security.util.crypto;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -32,7 +33,6 @@ import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.security.util.EncryptionMethod; import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.stream.io.ByteArrayOutputStream;
import org.apache.nifi.stream.io.StreamUtils; import org.apache.nifi.stream.io.StreamUtils;
public class CipherUtility { public class CipherUtility {
@ -316,13 +316,4 @@ public class CipherUtility {
return -1; return -1;
} }
} }
public static byte[] concatBytes(byte[]... arrays) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
for (byte[] bytes : arrays) {
outputStream.write(bytes);
}
return outputStream.toByteArray();
}
} }

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.apache.nifi.provenance package org.apache.nifi.security.kms
import org.apache.commons.lang3.SystemUtils import org.apache.commons.lang3.SystemUtils
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
@ -126,6 +126,20 @@ class CryptoUtilsTest {
assert keyProviderIsValid assert keyProviderIsValid
} }
@Test
void testShouldValidateLegacyStaticKeyProvider() {
// Arrange
String staticProvider = StaticKeyProvider.class.name.replaceFirst("security.kms", "provenance")
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 @Test
void testShouldNotValidateStaticKeyProviderMissingKeyId() { void testShouldNotValidateStaticKeyProviderMissingKeyId() {
// Arrange // Arrange
@ -184,6 +198,22 @@ class CryptoUtilsTest {
assert keyProviderIsValid assert keyProviderIsValid
} }
@Test
void testShouldValidateLegacyFileBasedKeyProvider() {
// Arrange
String fileBasedProvider = FileBasedKeyProvider.class.name.replaceFirst("security.kms", "provenance")
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 @Test
void testShouldNotValidateMissingFileBasedKeyProvider() { void testShouldNotValidateMissingFileBasedKeyProvider() {
// Arrange // Arrange

View File

@ -0,0 +1,229 @@
/*
* 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.kms
import org.apache.nifi.util.NiFiProperties
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.security.KeyManagementException
import java.security.SecureRandom
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
@RunWith(JUnit4.class)
class KeyProviderFactoryTest {
private static final Logger logger = LoggerFactory.getLogger(KeyProviderFactoryTest.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 String LEGACY_SKP_FQCN = "org.apache.nifi.provenance.StaticKeyProvider"
private static final String LEGACY_FBKP_FQCN = "org.apache.nifi.provenance.FileBasedKeyProvider"
private static final String ORIGINAL_PROPERTIES_PATH = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
private static final SecretKey MASTER_KEY = new SecretKeySpec(Hex.decode(KEY_HEX), "AES")
@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(" ")}")
}
logger.info("Original \$PROPERTIES_FILE_PATH is ${ORIGINAL_PROPERTIES_PATH}")
String testPath = new File("src/test/resources/${isUnlimitedStrengthCryptoAvailable() ? "256" : "128"}/conf/.").getAbsolutePath()
logger.info("Temporarily setting to ${testPath}")
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, testPath)
}
@Before
void setUp() throws Exception {
tempFolder.create()
}
@After
void tearDown() throws Exception {
tempFolder?.delete()
}
@AfterClass
static void tearDownOnce() throws Exception {
if (ORIGINAL_PROPERTIES_PATH) {
logger.info("Restored \$PROPERTIES_FILE_PATH to ${ORIGINAL_PROPERTIES_PATH}")
System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, ORIGINAL_PROPERTIES_PATH)
}
}
private static boolean isUnlimitedStrengthCryptoAvailable() {
Cipher.getMaxAllowedKeyLength("AES") > 128
}
private static void populateKeyDefinitionsFile(String path = "src/test/resources/conf/filebased.kp") {
String masterKeyHex = KEY_HEX
SecretKey masterKey = new SecretKeySpec(Hex.decode(masterKeyHex), "AES")
// Generate the file
File keyFile = new File(path)
final int KEY_COUNT = 1
List<String> lines = []
KEY_COUNT.times { int i ->
lines.add("K${i + 1}=${generateEncryptedKey(masterKey)}")
}
keyFile.text = lines.join("\n")
}
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))
}
@Test
void testShouldBuildStaticKeyProvider() {
// Arrange
String staticProvider = StaticKeyProvider.class.name
String providerLocation = null
// Act
KeyProvider keyProvider = KeyProviderFactory.buildKeyProvider(staticProvider, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX], null)
logger.info("Key Provider ${staticProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX} formed: ${keyProvider}")
// Assert
assert keyProvider instanceof StaticKeyProvider
assert keyProvider.getAvailableKeyIds() == [KEY_ID]
}
@Test
void testShouldBuildStaticKeyProviderWithLegacyPackage() {
// Arrange
String staticProvider = LEGACY_SKP_FQCN
String providerLocation = null
// Act
KeyProvider keyProvider = KeyProviderFactory.buildKeyProvider(staticProvider, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX], null)
logger.info("Key Provider ${staticProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX} formed: ${keyProvider}")
// Assert
assert keyProvider instanceof StaticKeyProvider
assert keyProvider.getAvailableKeyIds() == [KEY_ID]
}
@Test
void testShouldBuildFileBasedKeyProvider() {
// Arrange
String fileBasedProvider = FileBasedKeyProvider.class.name
File fileBasedProviderFile = tempFolder.newFile("filebased.kp")
String providerLocation = fileBasedProviderFile.path
populateKeyDefinitionsFile(providerLocation)
logger.info("Created temporary file based key provider: ${providerLocation}")
// Act
KeyProvider keyProvider = KeyProviderFactory.buildKeyProvider(fileBasedProvider, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX], MASTER_KEY)
logger.info("Key Provider ${fileBasedProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX} formed: ${keyProvider}")
// Assert
assert keyProvider instanceof FileBasedKeyProvider
assert keyProvider.getAvailableKeyIds() == [KEY_ID]
}
@Test
void testShouldBuildFileBasedKeyProviderWithLegacyPackage() {
// Arrange
String fileBasedProvider = LEGACY_FBKP_FQCN
File fileBasedProviderFile = tempFolder.newFile("filebased.kp")
String providerLocation = fileBasedProviderFile.path
populateKeyDefinitionsFile(providerLocation)
logger.info("Created temporary file based key provider: ${providerLocation}")
// Act
KeyProvider keyProvider = KeyProviderFactory.buildKeyProvider(fileBasedProvider, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX], MASTER_KEY)
logger.info("Key Provider ${fileBasedProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX} formed: ${keyProvider}")
// Assert
assert keyProvider instanceof FileBasedKeyProvider
assert keyProvider.getAvailableKeyIds() == [KEY_ID]
}
@Test
void testShouldNotBuildFileBasedKeyProviderWithoutMasterKey() {
// Arrange
String fileBasedProvider = FileBasedKeyProvider.class.name
File fileBasedProviderFile = tempFolder.newFile("filebased.kp")
String providerLocation = fileBasedProviderFile.path
populateKeyDefinitionsFile(providerLocation)
logger.info("Created temporary file based key provider: ${providerLocation}")
// Act
def msg = shouldFail(KeyManagementException) {
KeyProvider keyProvider = KeyProviderFactory.buildKeyProvider(fileBasedProvider, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX], null)
logger.info("Key Provider ${fileBasedProvider} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX} formed: ${keyProvider}")
}
// Assert
assert msg =~ "The master key must be provided to decrypt the individual keys"
}
@Test
void testShouldNotBuildUnknownKeyProvider() {
// Arrange
String providerImplementation = "org.apache.nifi.provenance.ImaginaryKeyProvider"
String providerLocation = null
// Act
def msg = shouldFail(KeyManagementException) {
KeyProvider keyProvider = KeyProviderFactory.buildKeyProvider(providerImplementation, providerLocation, KEY_ID, [(KEY_ID): KEY_HEX], null)
logger.info("Key Provider ${providerImplementation} with location ${providerLocation} and keyId ${KEY_ID} / ${KEY_HEX} formed: ${keyProvider}")
}
// Assert
assert msg =~ "Invalid key provider implementation provided"
}
}

View File

@ -0,0 +1,19 @@
#
# 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.
#
# Master key in hexadecimal format for encrypted sensitive configuration values
nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA9876543210

View File

@ -0,0 +1,19 @@
#
# 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.
#
# Master key in hexadecimal format for encrypted sensitive configuration values
nifi.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210

View File

@ -2819,7 +2819,7 @@ The simplest configuration is below:
.... ....
nifi.provenance.repository.implementation=org.apache.nifi.provenance.EncryptedWriteAheadProvenanceRepository nifi.provenance.repository.implementation=org.apache.nifi.provenance.EncryptedWriteAheadProvenanceRepository
nifi.provenance.repository.debug.frequency=100 nifi.provenance.repository.debug.frequency=100
nifi.provenance.repository.encryption.key.provider.implementation=org.apache.nifi.provenance.StaticKeyProvider nifi.provenance.repository.encryption.key.provider.implementation=org.apache.nifi.security.kms.StaticKeyProvider
nifi.provenance.repository.encryption.key.provider.location= nifi.provenance.repository.encryption.key.provider.location=
nifi.provenance.repository.encryption.key.id=Key1 nifi.provenance.repository.encryption.key.id=Key1
nifi.provenance.repository.encryption.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 nifi.provenance.repository.encryption.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210

View File

@ -1920,7 +1920,7 @@ The `StaticKeyProvider` implementation defines keys directly in `nifi.properties
The following configuration section would result in a key provider with two available keys, "Key1" (active) and "AnotherKey". The following configuration section would result in a key provider with two available keys, "Key1" (active) and "AnotherKey".
.... ....
nifi.provenance.repository.encryption.key.provider.implementation=org.apache.nifi.provenance.StaticKeyProvider nifi.provenance.repository.encryption.key.provider.implementation=org.apache.nifi.security.kms.StaticKeyProvider
nifi.provenance.repository.encryption.key.id=Key1 nifi.provenance.repository.encryption.key.id=Key1
nifi.provenance.repository.encryption.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 nifi.provenance.repository.encryption.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210
nifi.provenance.repository.encryption.key.id.AnotherKey=0101010101010101010101010101010101010101010101010101010101010101 nifi.provenance.repository.encryption.key.id.AnotherKey=0101010101010101010101010101010101010101010101010101010101010101

View File

@ -18,11 +18,13 @@ package org.apache.nifi.provenance;
import java.io.IOException; import java.io.IOException;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.util.Map;
import java.util.stream.Collectors;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.authorization.Authorizer; import org.apache.nifi.authorization.Authorizer;
import org.apache.nifi.events.EventReporter; import org.apache.nifi.events.EventReporter;
import org.apache.nifi.properties.NiFiPropertiesLoader;
import org.apache.nifi.provenance.serialization.RecordReaders; import org.apache.nifi.provenance.serialization.RecordReaders;
import org.apache.nifi.provenance.store.EventFileManager; import org.apache.nifi.provenance.store.EventFileManager;
import org.apache.nifi.provenance.store.RecordReaderFactory; import org.apache.nifi.provenance.store.RecordReaderFactory;
@ -30,6 +32,8 @@ import org.apache.nifi.provenance.store.RecordWriterFactory;
import org.apache.nifi.provenance.toc.StandardTocWriter; import org.apache.nifi.provenance.toc.StandardTocWriter;
import org.apache.nifi.provenance.toc.TocUtil; import org.apache.nifi.provenance.toc.TocUtil;
import org.apache.nifi.provenance.toc.TocWriter; import org.apache.nifi.provenance.toc.TocWriter;
import org.apache.nifi.security.kms.KeyProvider;
import org.apache.nifi.security.kms.KeyProviderFactory;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -70,7 +74,13 @@ public class EncryptedWriteAheadProvenanceRepository extends WriteAheadProvenanc
ProvenanceEventEncryptor provenanceEventEncryptor; ProvenanceEventEncryptor provenanceEventEncryptor;
if (getConfig().supportsEncryption()) { if (getConfig().supportsEncryption()) {
try { try {
KeyProvider keyProvider = buildKeyProvider(); KeyProvider keyProvider;
if (KeyProviderFactory.requiresMasterKey(getConfig().getKeyProviderImplementation())) {
SecretKey masterKey = getMasterKey();
keyProvider = buildKeyProvider(masterKey);
} else {
keyProvider = buildKeyProvider();
}
provenanceEventEncryptor = new AESProvenanceEventEncryptor(); provenanceEventEncryptor = new AESProvenanceEventEncryptor();
provenanceEventEncryptor.initialize(keyProvider); provenanceEventEncryptor.initialize(keyProvider);
} catch (KeyManagementException e) { } catch (KeyManagementException e) {
@ -111,6 +121,10 @@ public class EncryptedWriteAheadProvenanceRepository extends WriteAheadProvenanc
} }
private KeyProvider buildKeyProvider() throws KeyManagementException { private KeyProvider buildKeyProvider() throws KeyManagementException {
return buildKeyProvider(null);
}
private KeyProvider buildKeyProvider(SecretKey masterKey) throws KeyManagementException {
RepositoryConfiguration config = super.getConfig(); RepositoryConfiguration config = super.getConfig();
if (config == null) { if (config == null) {
throw new KeyManagementException("The repository configuration is missing"); throw new KeyManagementException("The repository configuration is missing");
@ -122,38 +136,17 @@ public class EncryptedWriteAheadProvenanceRepository extends WriteAheadProvenanc
+ NiFiProperties.PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_IMPLEMENTATION_CLASS); + NiFiProperties.PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_IMPLEMENTATION_CLASS);
} }
// TODO: Extract to factory return KeyProviderFactory.buildKeyProvider(implementationClassName, config.getKeyProviderLocation(), config.getKeyId(), config.getEncryptionKeys(), masterKey);
KeyProvider keyProvider;
if (StaticKeyProvider.class.getName().equals(implementationClassName)) {
// Get all the keys (map) from config
if (CryptoUtils.isValidKeyProvider(implementationClassName, config.getKeyProviderLocation(), config.getKeyId(), config.getEncryptionKeys())) {
Map<String, SecretKey> formedKeys = config.getEncryptionKeys().entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> {
try {
return CryptoUtils.formKeyFromHex(e.getValue());
} catch (KeyManagementException e1) {
// This should never happen because the hex has already been validated
logger.error("Encountered an error: ", e1);
return null;
}
}));
keyProvider = new StaticKeyProvider(formedKeys);
} else {
final String msg = "The StaticKeyProvider definition is not valid";
logger.error(msg);
throw new KeyManagementException(msg);
}
} else if (FileBasedKeyProvider.class.getName().equals(implementationClassName)) {
keyProvider = new FileBasedKeyProvider(config.getKeyProviderLocation());
if (!keyProvider.keyExists(config.getKeyId())) {
throw new KeyManagementException("The specified key ID " + config.getKeyId() + " is not in the key definition file");
}
} else {
throw new KeyManagementException("Invalid key provider implementation provided: " + implementationClassName);
} }
return keyProvider; private static SecretKey getMasterKey() throws KeyManagementException {
try {
// Get the master encryption key from bootstrap.conf
String masterKeyHex = NiFiPropertiesLoader.extractKeyFromBootstrapFile();
return new SecretKeySpec(Hex.decodeHex(masterKeyHex.toCharArray()), "AES");
} catch (IOException | DecoderException e) {
logger.error("Encountered an error: ", e);
throw new KeyManagementException(e);
}
} }
} }

View File

@ -28,6 +28,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.nifi.processor.DataUnit; import org.apache.nifi.processor.DataUnit;
import org.apache.nifi.provenance.search.SearchableField; import org.apache.nifi.provenance.search.SearchableField;
import org.apache.nifi.security.kms.CryptoUtils;
import org.apache.nifi.util.FormatUtils; import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -371,6 +372,8 @@ public class RepositoryConfiguration {
return keyProviderIsConfigured; return keyProviderIsConfigured;
} }
// TODO: Add verbose error output for encryption support failure if requested
public Map<String, String> getEncryptionKeys() { public Map<String, String> getEncryptionKeys() {
return encryptionKeys; return encryptionKeys;
} }

View File

@ -30,7 +30,6 @@ import java.util.zip.GZIPInputStream;
import org.apache.nifi.properties.NiFiPropertiesLoader; import org.apache.nifi.properties.NiFiPropertiesLoader;
import org.apache.nifi.provenance.ByteArraySchemaRecordReader; import org.apache.nifi.provenance.ByteArraySchemaRecordReader;
import org.apache.nifi.provenance.ByteArraySchemaRecordWriter; import org.apache.nifi.provenance.ByteArraySchemaRecordWriter;
import org.apache.nifi.provenance.CryptoUtils;
import org.apache.nifi.provenance.EncryptedSchemaRecordReader; import org.apache.nifi.provenance.EncryptedSchemaRecordReader;
import org.apache.nifi.provenance.EventIdFirstSchemaRecordReader; import org.apache.nifi.provenance.EventIdFirstSchemaRecordReader;
import org.apache.nifi.provenance.EventIdFirstSchemaRecordWriter; import org.apache.nifi.provenance.EventIdFirstSchemaRecordWriter;
@ -39,6 +38,7 @@ import org.apache.nifi.provenance.lucene.LuceneUtil;
import org.apache.nifi.provenance.toc.StandardTocReader; import org.apache.nifi.provenance.toc.StandardTocReader;
import org.apache.nifi.provenance.toc.TocReader; import org.apache.nifi.provenance.toc.TocReader;
import org.apache.nifi.provenance.toc.TocUtil; import org.apache.nifi.provenance.toc.TocUtil;
import org.apache.nifi.security.kms.CryptoUtils;
import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View File

@ -24,6 +24,7 @@ import org.apache.nifi.provenance.toc.StandardTocWriter
import org.apache.nifi.provenance.toc.TocReader import org.apache.nifi.provenance.toc.TocReader
import org.apache.nifi.provenance.toc.TocUtil import org.apache.nifi.provenance.toc.TocUtil
import org.apache.nifi.provenance.toc.TocWriter import org.apache.nifi.provenance.toc.TocWriter
import org.apache.nifi.security.kms.KeyProvider
import org.apache.nifi.util.file.FileUtils import org.apache.nifi.util.file.FileUtils
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.util.encoders.Hex import org.bouncycastle.util.encoders.Hex

View File

@ -20,6 +20,7 @@ import org.apache.nifi.events.EventReporter
import org.apache.nifi.flowfile.FlowFile import org.apache.nifi.flowfile.FlowFile
import org.apache.nifi.provenance.serialization.RecordReaders import org.apache.nifi.provenance.serialization.RecordReaders
import org.apache.nifi.reporting.Severity import org.apache.nifi.reporting.Severity
import org.apache.nifi.security.kms.StaticKeyProvider
import org.apache.nifi.util.file.FileUtils import org.apache.nifi.util.file.FileUtils
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After import org.junit.After

View File

@ -18,6 +18,7 @@ package org.apache.nifi.security.util.crypto
import org.apache.commons.codec.binary.Hex import org.apache.commons.codec.binary.Hex
import org.apache.nifi.processor.io.StreamCallback import org.apache.nifi.processor.io.StreamCallback
import org.apache.nifi.security.kms.CryptoUtils
import org.apache.nifi.security.util.EncryptionMethod import org.apache.nifi.security.util.EncryptionMethod
import org.apache.nifi.security.util.KeyDerivationFunction import org.apache.nifi.security.util.KeyDerivationFunction
import org.apache.nifi.stream.io.ByteArrayOutputStream import org.apache.nifi.stream.io.ByteArrayOutputStream
@ -33,7 +34,7 @@ import org.slf4j.LoggerFactory
import javax.crypto.Cipher import javax.crypto.Cipher
import java.security.Security import java.security.Security
public class PasswordBasedEncryptorGroovyTest { class PasswordBasedEncryptorGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(PasswordBasedEncryptorGroovyTest.class) private static final Logger logger = LoggerFactory.getLogger(PasswordBasedEncryptorGroovyTest.class)
private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/" private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/"
@ -44,7 +45,7 @@ public class PasswordBasedEncryptorGroovyTest {
private static final String LEGACY_PASSWORD = "Hello, World!" private static final String LEGACY_PASSWORD = "Hello, World!"
@BeforeClass @BeforeClass
public static void setUpOnce() throws Exception { static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider()) Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args -> logger.metaClass.methodMissing = { String name, args ->
@ -53,15 +54,15 @@ public class PasswordBasedEncryptorGroovyTest {
} }
@Before @Before
public void setUp() throws Exception { void setUp() throws Exception {
} }
@After @After
public void tearDown() throws Exception { void tearDown() throws Exception {
} }
@Test @Test
public void testShouldEncryptAndDecrypt() throws Exception { void testShouldEncryptAndDecrypt() throws Exception {
// Arrange // Arrange
final String PLAINTEXT = "This is a plaintext message." final String PLAINTEXT = "This is a plaintext message."
logger.info("Plaintext: {}", PLAINTEXT) logger.info("Plaintext: {}", PLAINTEXT)
@ -107,7 +108,7 @@ public class PasswordBasedEncryptorGroovyTest {
} }
@Test @Test
public void testShouldDecryptLegacyOpenSSLSaltedCipherText() throws Exception { void testShouldDecryptLegacyOpenSSLSaltedCipherText() throws Exception {
// Arrange // Arrange
Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength()) Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength())
@ -136,7 +137,7 @@ public class PasswordBasedEncryptorGroovyTest {
} }
@Test @Test
public void testShouldDecryptLegacyOpenSSLUnsaltedCipherText() throws Exception { void testShouldDecryptLegacyOpenSSLUnsaltedCipherText() throws Exception {
// Arrange // Arrange
Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength()) Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength())
@ -165,7 +166,7 @@ public class PasswordBasedEncryptorGroovyTest {
} }
@Test @Test
public void testShouldDecryptNiFiLegacySaltedCipherTextWithVariableSaltLength() throws Exception { void testShouldDecryptNiFiLegacySaltedCipherTextWithVariableSaltLength() throws Exception {
// Arrange // Arrange
final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text
logger.info("Plaintext: {}", PLAINTEXT) logger.info("Plaintext: {}", PLAINTEXT)
@ -201,7 +202,7 @@ public class PasswordBasedEncryptorGroovyTest {
byte[] cipherBytes = legacyCipher.doFinal(PLAINTEXT.bytes) byte[] cipherBytes = legacyCipher.doFinal(PLAINTEXT.bytes)
logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}") logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}")
byte[] completeCipherStreamBytes = CipherUtility.concatBytes(legacySalt, cipherBytes) byte[] completeCipherStreamBytes = CryptoUtils.concatByteArrays(legacySalt, cipherBytes)
logger.info("Complete cipher stream: ${Hex.encodeHexString(completeCipherStreamBytes)}") logger.info("Complete cipher stream: ${Hex.encodeHexString(completeCipherStreamBytes)}")
InputStream cipherStream = new ByteArrayInputStream(completeCipherStreamBytes) InputStream cipherStream = new ByteArrayInputStream(completeCipherStreamBytes)