Skip to content

Commit

Permalink
NIFI-6615 Added Azure Key Vault Key Sensitive Property Provider
Browse files Browse the repository at this point in the history
This closes apache#5274

Signed-off-by: David Handermann <exceptionfactory@apache.org>
  • Loading branch information
emiliosetiadarma authored and exceptionfactory committed Aug 13, 2021
1 parent 4c6bd85 commit 714670b
Show file tree
Hide file tree
Showing 15 changed files with 740 additions and 194 deletions.
11 changes: 11 additions & 0 deletions nifi-assembly/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -2093,6 +2093,17 @@ The following binary components are provided under the Eclipse Distribution Lice
(EDL 1.0) Jakarta Activation (com.sun.activation:jakarta.activation:jar:2.0.1)
(EDL 1.0) Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.2)

************************
The MIT License
************************

(MIT) Azure SDK for Java
The following NOTICE information applies:
Copyright 2021 Microsoft Corporation or its affiliates. All Rights Reserved.

This product includes software developed by
Microsoft Corporation (https://www.microsoft.com/).

*****************
Mozilla Public License v2.0
*****************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public enum BootstrapPropertyKey {
SENSITIVE_KEY("bootstrap.sensitive.key"),
HASHICORP_VAULT_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.hashicorp.vault.conf"),
AWS_KMS_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.aws.kms.conf"),
AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF("bootstrap.protection.azure.keyvault.conf"),
CONTEXT_MAPPING_PREFIX("bootstrap.protection.context.mapping.");

private final String key;
Expand Down
47 changes: 47 additions & 0 deletions nifi-commons/nifi-sensitive-property-provider/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,53 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-keys</artifactId>
<version>4.3.1</version>
<exclusions>
<exclusion>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-netty</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.woodstox</groupId>
<artifactId>woodstox-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>stax2-api</artifactId>
</exclusion>
<exclusion>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.3.4</version>
<exclusions>
<exclusion>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-netty</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.woodstox</groupId>
<artifactId>woodstox-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.codehaus.woodstox</groupId>
<artifactId>stax2-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-http-okhttp</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
import java.nio.file.Paths;
import java.util.Objects;

public class AWSSensitivePropertyProvider extends AbstractSensitivePropertyProvider {
private static final Logger logger = LoggerFactory.getLogger(AWSSensitivePropertyProvider.class);
public class AWSKMSSensitivePropertyProvider extends AbstractSensitivePropertyProvider {
private static final Logger logger = LoggerFactory.getLogger(AWSKMSSensitivePropertyProvider.class);

private static final String AWS_PREFIX = "aws";
private static final String ACCESS_KEY_PROPS_NAME = "aws.access.key.id";
Expand All @@ -60,7 +60,7 @@ public class AWSSensitivePropertyProvider extends AbstractSensitivePropertyProvi
private String keyId;


AWSSensitivePropertyProvider(final BootstrapProperties bootstrapProperties) throws SensitivePropertyProtectionException {
AWSKMSSensitivePropertyProvider(final BootstrapProperties bootstrapProperties) throws SensitivePropertyProtectionException {
super(bootstrapProperties);
Objects.requireNonNull(bootstrapProperties, "The file bootstrap.conf provided to AWS SPP is null");
awsBootstrapProperties = getAWSBootstrapProperties(bootstrapProperties);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
/*
* 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.properties;

import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.properties.BootstrapProperties.BootstrapPropertyKey;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.azure.core.exception.ResourceNotFoundException;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
import com.azure.security.keyvault.keys.cryptography.CryptographyClientBuilder;
import com.azure.security.keyvault.keys.cryptography.models.DecryptResult;
import com.azure.security.keyvault.keys.cryptography.models.EncryptResult;
import com.azure.security.keyvault.keys.cryptography.models.EncryptionAlgorithm;
import com.azure.security.keyvault.keys.models.KeyOperation;
import com.azure.security.keyvault.keys.models.KeyProperties;

import java.util.Base64;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;

public class AzureKeyVaultKeySensitivePropertyProvider extends AbstractSensitivePropertyProvider {
private static final Logger logger = LoggerFactory.getLogger(AzureKeyVaultKeySensitivePropertyProvider.class);

private static final String AZURE_PREFIX = "azure";
private static final String KEYVAULT_KEY_PROPS_NAME = "azure.keyvault.key.id";
private static final String ENCRYPTION_ALGORITHM_PROPS_NAME = "azure.keyvault.encryption.algorithm";

private static final Charset PROPERTY_CHARSET = StandardCharsets.UTF_8;

private final BootstrapProperties azureBootstrapProperties;
private CryptographyClient client;
private String keyId;
private String algorithm;

AzureKeyVaultKeySensitivePropertyProvider(final BootstrapProperties bootstrapProperties) throws SensitivePropertyProtectionException {
super(bootstrapProperties);
Objects.requireNonNull(bootstrapProperties, "Bootstrap Properties required");
azureBootstrapProperties = getAzureBootstrapProperties(bootstrapProperties);
loadRequiredAzureProperties(azureBootstrapProperties);
}

/**
* Initializes the Azure Key Vault Cryptography Client to be used for encrypt, decrypt and other interactions with Azure Key Vault.
* Uses the default Azure credentials provider chain.
*/
private void initializeClient() {
if (azureBootstrapProperties == null) {
logger.warn("Azure Bootstrap Properties are required for Key Vault Client initialization");
return;
}

if (StringUtils.isBlank(keyId)) {
logger.warn("Cannot initialize client if Azure Key Vault Key ID is blank");
return;
}

try {
client = new CryptographyClientBuilder()
.credential(new DefaultAzureCredentialBuilder().build())
.keyIdentifier(keyId)
.buildClient();
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("Azure Key Vault Client initialization failed", e);
}
}

/**
* Validates the key provided by the user.
* Note: This function performs checks on the key and indirectly also validates the credentials provided
* during the initialization of the client.
*/
private void validate() throws SensitivePropertyProtectionException {
if (client == null) {
throw new SensitivePropertyProtectionException("Azure Key Vault validation failed: Client not initialized");
}

if (StringUtils.isBlank(keyId)) {
throw new SensitivePropertyProtectionException("Azure Key Vault validation failed: Key not specified");
}

try {
final KeyProperties keyProps = client.getKey().getProperties();
if (!keyProps.isEnabled()) {
throw new SensitivePropertyProtectionException("Azure Key Vault validation failed: Key not enabled");
}

final List<KeyOperation> keyOps = client.getKey().getKeyOperations();
if (!(keyOps.contains(KeyOperation.ENCRYPT) && keyOps.contains(KeyOperation.DECRYPT))) {
throw new SensitivePropertyProtectionException("Azure Key Vault validation failed: Encrypt and Decrypt not supported");
}
} catch (final ResourceNotFoundException e) {
throw new SensitivePropertyProtectionException("Azure Key Vault validation failed: Key not found", e);
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("Azure Key Vault validation failed", e);
}
}

/**
* Checks if we have the required properties {@link #keyId} and {@link #algorithm} from bootstrap-azure.conf
* for Azure KeyVault and loads it into the appropriate variables, will load null if values don't exist.
* Note: This function does not verify if the properties are valid.
* @param props the properties representing bootstrap-azure.conf
*/
private void loadRequiredAzureProperties(final BootstrapProperties props) {
if (props != null) {
keyId = props.getProperty(KEYVAULT_KEY_PROPS_NAME);
algorithm = props.getProperty(ENCRYPTION_ALGORITHM_PROPS_NAME);
}
}


/**
* Checks bootstrap.conf to check if BootstrapPropertyKey.AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF property is configured to the
* bootstrap-azure.conf file. Also will load bootstrap-azure.conf to {@link #azureBootstrapProperties} if possible
* @param bootstrapProperties BootstrapProperties object corresponding to bootstrap.conf
* @return BootstrapProperties object corresponding to bootstrap-azure.conf, null otherwise
*/
private BootstrapProperties getAzureBootstrapProperties(final BootstrapProperties bootstrapProperties) {
final BootstrapProperties cloudBootstrapProperties;

// Load the bootstrap-azure.conf file based on path specified in
// "nifi.bootstrap.protection.azure.keyvault.conf" property of bootstrap.conf
final String filePath = bootstrapProperties.getProperty(BootstrapPropertyKey.AZURE_KEYVAULT_SENSITIVE_PROPERTY_PROVIDER_CONF).orElse(null);
if (StringUtils.isBlank(filePath)) {
logger.warn("Azure Key Vault properties file path not configured in bootstrap properties");
return null;
}

try {
cloudBootstrapProperties = AbstractBootstrapPropertiesLoader.loadBootstrapProperties(
Paths.get(filePath), AZURE_PREFIX);
} catch (final IOException e) {
throw new SensitivePropertyProtectionException("Could not load " + filePath, e);
}

return cloudBootstrapProperties;
}

/**
* Checks the BootstrapProperties corresponding to bootstrap-azure.conf for the required configurations
* for Azure encrypt/decrypt operations.
* Note: This does not check for credentials/region configurations.
* Credentials/configuration will be checked during the first protect/unprotect call during runtime.
* @return True if bootstrap-azure.conf contains the required properties for Azure SPP, False otherwise
*/
private boolean hasRequiredAzureProperties() {
return azureBootstrapProperties != null && StringUtils.isNoneBlank(keyId, algorithm);
}

/**
* Return true if this SensitivePropertyProvider is supported, given the provided Bootstrap properties.
* @return True if this SensitivePropertyProvider is supported
*/
@Override
public boolean isSupported() {
return hasRequiredAzureProperties();
}

/**
* Return the appropriate PropertyProtectionScheme for this provider.
*
* @return The PropertyProtectionScheme
*/
@Override
protected PropertyProtectionScheme getProtectionScheme() {
return PropertyProtectionScheme.AZURE_KEYVAULT_KEY;
}

/**
* Returns the name of the underlying implementation.
*
* @return the name of this sensitive property provider
*/
@Override
public String getName() {
return PropertyProtectionScheme.AZURE_KEYVAULT_KEY.getName();
}

/**
* Returns the key used to identify the provider implementation in {@code nifi.properties}.
*
* @return the key to persist in the sibling property
*/
@Override
public String getIdentifierKey() {
return PropertyProtectionScheme.AZURE_KEYVAULT_KEY.getIdentifier();
}


/**
* Returns the ciphertext of this value encrypted using a key stored in Azure Key Vault.
*
* @return the ciphertext blob to persist in the {@code nifi.properties} file
*/
private byte[] encrypt(final byte[] input) {
EncryptResult encryptResult = client.encrypt(EncryptionAlgorithm.fromString(algorithm), input);
return encryptResult.getCipherText();
}

/**
* Returns the value corresponding to a ciphertext decrypted using a key stored in Azure Key Vault
*
* @return the "unprotected" byte[] of this value, which could be used by the application
*/
private byte[] decrypt(final byte[] input) {
DecryptResult decryptResult = client.decrypt(EncryptionAlgorithm.fromString(algorithm), input);
return decryptResult.getPlainText();
}

/**
* Checks if the client is open and if not, initializes the client and validates the configuration required for Azure Key Vault.
*/
private void checkAndInitializeClient() {
if (client == null) {
initializeClient();
validate();
}
}

/**
* Returns the "protected" form of this value. This is a form which can safely be persisted in the {@code nifi.properties} file without compromising the value.
* Encrypts a sensitive value using a key managed by Azure Key Vault.
*
* @param unprotectedValue the sensitive value
* @param context The context of the value (ignored in this implementation)
* @return the value to persist in the {@code nifi.properties} file
*/
@Override
public String protect(final String unprotectedValue, final ProtectedPropertyContext context) throws SensitivePropertyProtectionException {
if (StringUtils.isBlank(unprotectedValue)) {
throw new IllegalArgumentException("Cannot encrypt a blank value");
}

checkAndInitializeClient();

try {
final byte[] plainBytes = unprotectedValue.getBytes(PROPERTY_CHARSET);
final byte[] cipherBytes = encrypt(plainBytes);
return Base64.getEncoder().encodeToString(cipherBytes);
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("Encrypt failed", e);
}
}

/**
* Returns the "unprotected" form of this value. This is the raw sensitive value which is used by the application logic.
* Decrypts a secured value from a ciphertext using a key managed by Azure Key Vault.
*
* @param protectedValue the protected value read from the {@code nifi.properties} file
* @param context The context of the value (ignored in this implementation)
* @return the raw value to be used by the application
*/
@Override
public String unprotect(final String protectedValue, final ProtectedPropertyContext context) throws SensitivePropertyProtectionException {
if (StringUtils.isBlank(protectedValue)) {
throw new IllegalArgumentException("Cannot decrypt a blank value");
}

checkAndInitializeClient();

try {
final byte[] cipherBytes = Base64.getDecoder().decode(protectedValue);
final byte[] plainBytes = decrypt(cipherBytes);
return new String(plainBytes, PROPERTY_CHARSET);
} catch (final RuntimeException e) {
throw new SensitivePropertyProtectionException("Decrypt failed", e);
}
}

/**
* Nothing required to be done for Azure Client cleanUp function.
*/
@Override
public void cleanUp() {}
}
Loading

0 comments on commit 714670b

Please sign in to comment.