You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/09/22 02:11:24 UTC

[25/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
new file mode 100644
index 0000000..1dcb0f7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -0,0 +1,305 @@
+/*
+ * 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.registry.properties;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NiFiRegistryProperties extends Properties {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryProperties.class);
+
+    // Keys
+    public static final String WEB_WAR_DIR = "nifi.registry.web.war.directory";
+    public static final String WEB_HTTP_PORT = "nifi.registry.web.http.port";
+    public static final String WEB_HTTP_HOST = "nifi.registry.web.http.host";
+    public static final String WEB_HTTPS_PORT = "nifi.registry.web.https.port";
+    public static final String WEB_HTTPS_HOST = "nifi.registry.web.https.host";
+    public static final String WEB_WORKING_DIR = "nifi.registry.web.jetty.working.directory";
+    public static final String WEB_THREADS = "nifi.registry.web.jetty.threads";
+
+    public static final String SECURITY_KEYSTORE = "nifi.registry.security.keystore";
+    public static final String SECURITY_KEYSTORE_TYPE = "nifi.registry.security.keystoreType";
+    public static final String SECURITY_KEYSTORE_PASSWD = "nifi.registry.security.keystorePasswd";
+    public static final String SECURITY_KEY_PASSWD = "nifi.registry.security.keyPasswd";
+    public static final String SECURITY_TRUSTSTORE = "nifi.registry.security.truststore";
+    public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.registry.security.truststoreType";
+    public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.registry.security.truststorePasswd";
+    public static final String SECURITY_NEED_CLIENT_AUTH = "nifi.registry.security.needClientAuth";
+    public static final String SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "nifi.registry.security.authorizers.configuration.file";
+    public static final String SECURITY_AUTHORIZER = "nifi.registry.security.authorizer";
+    public static final String SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE = "nifi.registry.security.identity.providers.configuration.file";
+    public static final String SECURITY_IDENTITY_PROVIDER = "nifi.registry.security.identity.provider";
+    public static final String SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX = "nifi.registry.security.identity.mapping.pattern.";
+    public static final String SECURITY_IDENTITY_MAPPING_VALUE_PREFIX = "nifi.registry.security.identity.mapping.value.";
+
+    public static final String EXTENSION_DIR_PREFIX = "nifi.registry.extension.dir.";
+
+    public static final String PROVIDERS_CONFIGURATION_FILE = "nifi.registry.providers.configuration.file";
+
+    // Original DB properties
+    public static final String DATABASE_DIRECTORY = "nifi.registry.db.directory";
+    public static final String DATABASE_URL_APPEND = "nifi.registry.db.url.append";
+
+    // New style DB properties
+    public static final String DATABASE_URL = "nifi.registry.db.url";
+    public static final String DATABASE_DRIVER_CLASS_NAME = "nifi.registry.db.driver.class";
+    public static final String DATABASE_DRIVER_DIR = "nifi.registry.db.driver.directory";
+    public static final String DATABASE_USERNAME = "nifi.registry.db.username";
+    public static final String DATABASE_PASSWORD = "nifi.registry.db.password";
+    public static final String DATABASE_MAX_CONNECTIONS = "nifi.registry.db.maxConnections";
+    public static final String DATABASE_SQL_DEBUG = "nifi.registry.db.sql.debug";
+
+    // Kerberos properties
+    public static final String KERBEROS_KRB5_FILE = "nifi.registry.kerberos.krb5.file";
+    public static final String KERBEROS_SPNEGO_PRINCIPAL = "nifi.registry.kerberos.spnego.principal";
+    public static final String KERBEROS_SPNEGO_KEYTAB_LOCATION = "nifi.registry.kerberos.spnego.keytab.location";
+    public static final String KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION = "nifi.registry.kerberos.spnego.authentication.expiration";
+    public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.registry.kerberos.service.principal";
+    public static final String KERBEROS_SERVICE_KEYTAB_LOCATION = "nifi.registry.kerberos.service.keytab.location";
+
+    // Defaults
+    public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty";
+    public static final String DEFAULT_WAR_DIR = "./lib";
+    public static final String DEFAULT_PROVIDERS_CONFIGURATION_FILE = "./conf/providers.xml";
+    public static final String DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "./conf/authorizers.xml";
+    public static final String DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE = "./conf/identity-providers.xml";
+    public static final String DEFAULT_AUTHENTICATION_EXPIRATION = "12 hours";
+
+    public int getWebThreads() {
+        int webThreads = 200;
+        try {
+            webThreads = Integer.parseInt(getProperty(WEB_THREADS));
+        } catch (final NumberFormatException nfe) {
+            logger.warn(String.format("%s must be an integer value. Defaulting to %s", WEB_THREADS, webThreads));
+        }
+        return webThreads;
+    }
+
+    public Integer getPort() {
+        return getPropertyAsInteger(WEB_HTTP_PORT);
+    }
+
+    public String getHttpHost() {
+        return getProperty(WEB_HTTP_HOST);
+    }
+
+    public Integer getSslPort() {
+        return getPropertyAsInteger(WEB_HTTPS_PORT);
+    }
+
+    public String getHttpsHost() {
+        return getProperty(WEB_HTTPS_HOST);
+    }
+
+    public boolean getNeedClientAuth() {
+        boolean needClientAuth = true;
+        String rawNeedClientAuth = getProperty(SECURITY_NEED_CLIENT_AUTH);
+        if ("false".equalsIgnoreCase(rawNeedClientAuth)) {
+            needClientAuth = false;
+        }
+        return needClientAuth;
+    }
+
+    public String getKeyStorePath() {
+        return getProperty(SECURITY_KEYSTORE);
+    }
+
+    public String getKeyStoreType() {
+        return getProperty(SECURITY_KEYSTORE_TYPE);
+    }
+
+    public String getKeyStorePassword() {
+        return getProperty(SECURITY_KEYSTORE_PASSWD);
+    }
+
+    public String getKeyPassword() {
+        return getProperty(SECURITY_KEY_PASSWD);
+    }
+
+    public String getTrustStorePath() {
+        return getProperty(SECURITY_TRUSTSTORE);
+    }
+
+    public String getTrustStoreType() {
+        return getProperty(SECURITY_TRUSTSTORE_TYPE);
+    }
+
+    public String getTrustStorePassword() {
+        return getProperty(SECURITY_TRUSTSTORE_PASSWD);
+    }
+
+    public File getWarLibDirectory() {
+        return new File(getProperty(WEB_WAR_DIR, DEFAULT_WAR_DIR));
+    }
+
+    public File getWebWorkingDirectory() {
+        return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR));
+    }
+
+    public File getProvidersConfigurationFile() {
+        return getPropertyAsFile(PROVIDERS_CONFIGURATION_FILE, DEFAULT_PROVIDERS_CONFIGURATION_FILE);
+    }
+
+    public String getLegacyDatabaseDirectory() {
+        return getProperty(DATABASE_DIRECTORY);
+    }
+
+    public String getLegacyDatabaseUrlAppend() {
+        return getProperty(DATABASE_URL_APPEND);
+    }
+
+    public String getDatabaseUrl() {
+        return getProperty(DATABASE_URL);
+    }
+
+    public String getDatabaseDriverClassName() {
+        return getProperty(DATABASE_DRIVER_CLASS_NAME);
+    }
+
+    public String getDatabaseDriverDirectory() {
+        return getProperty(DATABASE_DRIVER_DIR);
+    }
+
+    public String getDatabaseUsername() {
+        return getProperty(DATABASE_USERNAME);
+    }
+
+    public String getDatabasePassword() {
+        return getProperty(DATABASE_PASSWORD);
+    }
+
+    public Integer getDatabaseMaxConnections() {
+        return getPropertyAsInteger(DATABASE_MAX_CONNECTIONS);
+    }
+
+    public boolean getDatabaseSqlDebug() {
+        final String value = getProperty(DATABASE_SQL_DEBUG);
+
+        if (StringUtils.isBlank(value)) {
+            return false;
+        }
+
+        return "true".equalsIgnoreCase(value.trim());
+    }
+
+    public File getAuthorizersConfigurationFile() {
+        return getPropertyAsFile(SECURITY_AUTHORIZERS_CONFIGURATION_FILE, DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE);
+    }
+
+    public File getIdentityProviderConfigurationFile() {
+        return getPropertyAsFile(SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE, DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE);
+    }
+
+    public File getKerberosConfigurationFile() {
+        return getPropertyAsFile(KERBEROS_KRB5_FILE);
+    }
+
+    public String getKerberosSpnegoAuthenticationExpiration() {
+        return getProperty(KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION, DEFAULT_AUTHENTICATION_EXPIRATION);
+    }
+
+    public String getKerberosSpnegoPrincipal() {
+        return getPropertyAsTrimmedString(KERBEROS_SPNEGO_PRINCIPAL);
+    }
+
+    public String getKerberosSpnegoKeytabLocation() {
+        return getPropertyAsTrimmedString(KERBEROS_SPNEGO_KEYTAB_LOCATION);
+    }
+
+    public boolean isKerberosSpnegoSupportEnabled() {
+        return !StringUtils.isBlank(getKerberosSpnegoPrincipal()) && !StringUtils.isBlank(getKerberosSpnegoKeytabLocation());
+    }
+
+    public String getKerberosServicePrincipal() {
+        return getPropertyAsTrimmedString(KERBEROS_SERVICE_PRINCIPAL);
+    }
+
+    public String getKerberosServiceKeytabLocation() {
+        return getPropertyAsTrimmedString(KERBEROS_SERVICE_KEYTAB_LOCATION);
+    }
+
+    public Set<String> getExtensionsDirs() {
+        final Set<String> extensionDirs = new HashSet<>();
+        stringPropertyNames().stream().filter(key -> key.startsWith(EXTENSION_DIR_PREFIX)).forEach(key -> extensionDirs.add(getProperty(key)));
+        return extensionDirs;
+    }
+
+    /**
+     * Retrieves all known property keys.
+     *
+     * @return all known property keys
+     */
+    public Set<String> getPropertyKeys() {
+        Set<String> propertyNames = new HashSet<>();
+        Enumeration e = this.propertyNames();
+        for (; e.hasMoreElements(); ){
+            propertyNames.add((String) e.nextElement());
+        }
+
+        return propertyNames;
+    }
+
+    // Helper functions for common ways of interpreting property values
+
+    private String getPropertyAsTrimmedString(String key) {
+        final String value = getProperty(key);
+        if (!StringUtils.isBlank(value)) {
+            return value.trim();
+        } else {
+            return null;
+        }
+    }
+
+    private Integer getPropertyAsInteger(String key) {
+        final String value = getProperty(key);
+        if (StringUtils.isBlank(value)) {
+            return null;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (final NumberFormatException nfe) {
+            throw new IllegalStateException(String.format("%s must be an integer value.", key));
+        }
+    }
+
+    private File getPropertyAsFile(String key) {
+        final String filePath = getProperty(key);
+        if (filePath != null && filePath.trim().length() > 0) {
+            return new File(filePath.trim());
+        } else {
+            return null;
+        }
+    }
+
+    private File getPropertyAsFile(String propertyKey, String defaultFileLocation) {
+        final String value = getProperty(propertyKey);
+        if (StringUtils.isBlank(value)) {
+            return new File(defaultFileLocation);
+        } else {
+            return new File(value);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
new file mode 100644
index 0000000..5ceffd1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
@@ -0,0 +1,148 @@
+/*
+ * 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.registry.properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+public class NiFiRegistryPropertiesLoader {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesLoader.class);
+
+    private static final String RELATIVE_PATH = "conf/nifi-registry.properties";
+
+    private String keyHex;
+
+    // Future enhancement: allow for external registration of new providers
+    private static SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
+
+    /**
+     * Returns an instance of the loader configured with the key.
+     * <p>
+     * <p>
+     * NOTE: This method is used reflectively by the process which starts NiFi
+     * so changes to it must be made in conjunction with that mechanism.</p>
+     *
+     * @param keyHex the key used to encrypt any sensitive properties
+     * @return the configured loader
+     */
+    public static NiFiRegistryPropertiesLoader withKey(String keyHex) {
+        NiFiRegistryPropertiesLoader loader = new NiFiRegistryPropertiesLoader();
+        loader.setKeyHex(keyHex);
+        return loader;
+    }
+
+    /**
+     * Sets the hexadecimal key used to unprotect properties encrypted with
+     * {@link AESSensitivePropertyProvider}. If the key has already been set,
+     * calling this method will throw a {@link RuntimeException}.
+     *
+     * @param keyHex the key in hexadecimal format
+     */
+    public void setKeyHex(String keyHex) {
+        if (this.keyHex == null || this.keyHex.trim().isEmpty()) {
+            this.keyHex = keyHex;
+        } else {
+            throw new RuntimeException("Cannot overwrite an existing key");
+        }
+    }
+
+    private static String getDefaultProviderKey() {
+        try {
+            return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? "256" : "128");
+        } catch (NoSuchAlgorithmException e) {
+            return "aes/gcm/128";
+        }
+    }
+
+    private void initializeSensitivePropertyProviderFactory() {
+        sensitivePropertyProviderFactory = new AESSensitivePropertyProviderFactory(keyHex);
+    }
+
+    private SensitivePropertyProvider getSensitivePropertyProvider() {
+        initializeSensitivePropertyProviderFactory();
+        return sensitivePropertyProviderFactory.getProvider();
+    }
+
+    /**
+     * Returns a {@link ProtectedNiFiRegistryProperties} instance loaded from the
+     * serialized form in the file. Responsible for actually reading from disk
+     * and deserializing the properties. Returns a protected instance to allow
+     * for decryption operations.
+     *
+     * @param file the file containing serialized properties
+     * @return the ProtectedNiFiProperties instance
+     */
+    ProtectedNiFiRegistryProperties readProtectedPropertiesFromDisk(File file) {
+        if (file == null || !file.exists() || !file.canRead()) {
+            String path = (file == null ? "missing file" : file.getAbsolutePath());
+            logger.error("Cannot read from '{}' -- file is missing or not readable", path);
+            throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable");
+        }
+
+        final NiFiRegistryProperties rawProperties = new NiFiRegistryProperties();
+        try (final FileReader reader = new FileReader(file)) {
+            rawProperties.load(reader);
+            logger.info("Loaded {} properties from {}", rawProperties.size(), file.getAbsolutePath());
+            ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = new ProtectedNiFiRegistryProperties(rawProperties);
+            return protectedNiFiRegistryProperties;
+        } catch (final IOException ioe) {
+            logger.error("Cannot load properties file due to " + ioe.getLocalizedMessage());
+            throw new RuntimeException("Cannot load properties file due to " + ioe.getLocalizedMessage(), ioe);
+        }
+    }
+
+    /**
+     * Returns an instance of {@link NiFiRegistryProperties} loaded from the provided
+     * {@link File}. If any properties are protected, will attempt to use the appropriate
+     * {@link SensitivePropertyProvider} to unprotect them transparently.
+     *
+     * @param file the File containing the serialized properties
+     * @return the NiFiProperties instance
+     */
+    public NiFiRegistryProperties load(File file) {
+        ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = readProtectedPropertiesFromDisk(file);
+        if (protectedNiFiRegistryProperties.hasProtectedKeys()) {
+            protectedNiFiRegistryProperties.addSensitivePropertyProvider(getSensitivePropertyProvider());
+        }
+
+        return protectedNiFiRegistryProperties.getUnprotectedProperties();
+    }
+
+    /**
+     * Returns an instance of {@link NiFiRegistryProperties}. The path must not be empty.
+     *
+     * @param path the path of the serialized properties file
+     * @return the NiFiRegistryProperties instance
+     * @see NiFiRegistryPropertiesLoader#load(File)
+     */
+    public NiFiRegistryProperties load(String path) {
+        if (path != null && !path.trim().isEmpty()) {
+            return load(new File(path));
+        } else {
+            logger.error("Cannot read from '{}' -- path is null or empty", path);
+            throw new IllegalArgumentException("NiFi Registry properties file path empty or null");
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
new file mode 100644
index 0000000..5debc4a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
@@ -0,0 +1,528 @@
+/*
+ * 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.registry.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Wrapper class of {@link NiFiRegistryProperties} for intermediate phase when
+ * {@link NiFiRegistryPropertiesLoader} loads the raw properties file and performs
+ * unprotection activities before returning an instance of {@link NiFiRegistryProperties}.
+ */
+class ProtectedNiFiRegistryProperties {
+    private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiRegistryProperties.class);
+
+    private NiFiRegistryProperties properties;
+
+    private Map<String, SensitivePropertyProvider> localProviderCache = new HashMap<>();
+
+    // Additional "sensitive" property key
+    public static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = "nifi.registry.sensitive.props.additional.keys";
+
+    // Default list of "sensitive" property keys
+    public static final List<String> DEFAULT_SENSITIVE_PROPERTIES = new ArrayList<>(asList(
+            NiFiRegistryProperties.SECURITY_KEY_PASSWD,
+            NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD,
+            NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD));
+
+    public ProtectedNiFiRegistryProperties() {
+        this(null);
+    }
+
+    /**
+     * Creates an instance containing the provided {@link NiFiRegistryProperties}.
+     *
+     * @param props the NiFiProperties to contain
+     */
+    public ProtectedNiFiRegistryProperties(NiFiRegistryProperties props) {
+        if (props == null) {
+            props = new NiFiRegistryProperties();
+        }
+        this.properties = props;
+        logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiProperties",
+                getPropertyKeysIncludingProtectionSchemes().size(), getProtectedPropertyKeys().size());
+    }
+
+    /**
+     * Retrieves the property value for the given property key.
+     *
+     * @param key the key of property value to lookup
+     * @return value of property at given key or null if not found
+     */
+    // @Override
+    public String getProperty(String key) {
+        return getInternalNiFiProperties().getProperty(key);
+    }
+
+    /**
+     * Returns the internal representation of the {@link NiFiRegistryProperties} -- protected
+     * or not as determined by the current state. No guarantee is made to the
+     * protection state of these properties. If the internal reference is null, a new
+     * {@link NiFiRegistryProperties} instance is created.
+     *
+     * @return the internal properties
+     */
+    NiFiRegistryProperties getInternalNiFiProperties() {
+        if (this.properties == null) {
+            this.properties = new NiFiRegistryProperties();
+        }
+
+        return this.properties;
+    }
+
+    /**
+     * Returns the number of properties in the NiFiRegistryProperties,
+     * excluding protection scheme properties.
+     *
+     * <p>
+     * Example:
+     * <p>
+     * key: E(value, key)
+     * key.protected: aes/gcm/256
+     * key2: value2
+     * <p>
+     * would return size 2
+     *
+     * @return the count of real properties
+     */
+    int size() {
+        return getPropertyKeysExcludingProtectionSchemes().size();
+    }
+
+    /**
+     * Returns the complete set of property keys in the NiFiRegistryProperties,
+     * including any protection keys (i.e. 'x.y.z.protected').
+     *
+     * @return the set of property keys
+     */
+    Set<String> getPropertyKeysIncludingProtectionSchemes() {
+        return getInternalNiFiProperties().getPropertyKeys();
+    }
+
+    /**
+     * Returns the set of property keys in the NiFiRegistryProperties,
+     * excluding any protection keys (i.e. 'x.y.z.protected').
+     *
+     * @return the set of property keys
+     */
+    Set<String> getPropertyKeysExcludingProtectionSchemes() {
+        Set<String> filteredKeys = getPropertyKeysIncludingProtectionSchemes();
+        filteredKeys.removeIf(p -> p.endsWith(".protected"));
+        return filteredKeys;
+    }
+
+    /**
+     * Splits a single string containing multiple property keys into a List.
+     *
+     * Delimited by ',' or ';' and ignores leading and trailing whitespace around delimiter.
+     *
+     * @param multipleProperties a single String containing multiple properties, i.e.
+     *                           "nifi.registry.property.1; nifi.registry.property.2, nifi.registry.property.3"
+     * @return a List containing the split and trimmed properties
+     */
+    private static List<String> splitMultipleProperties(String multipleProperties) {
+        if (multipleProperties == null || multipleProperties.trim().isEmpty()) {
+            return new ArrayList<>(0);
+        } else {
+            List<String> properties = new ArrayList<>(asList(multipleProperties.split("\\s*[,;]\\s*")));
+            for (int i = 0; i < properties.size(); i++) {
+                properties.set(i, properties.get(i).trim());
+            }
+            return properties;
+        }
+    }
+
+    /**
+     * Returns a list of the keys identifying "sensitive" properties.
+     *
+     * There is a default list, and additional keys can be provided in the
+     * {@code nifi.registry.sensitive.props.additional.keys} property in {@code nifi-registry.properties}.
+     *
+     * @return the list of sensitive property keys
+     */
+    public List<String> getSensitivePropertyKeys() {
+        String additionalPropertiesString = getProperty(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+        if (additionalPropertiesString == null || additionalPropertiesString.trim().isEmpty()) {
+            return DEFAULT_SENSITIVE_PROPERTIES;
+        } else {
+            List<String> additionalProperties = splitMultipleProperties(additionalPropertiesString);
+            /* Remove this key if it was accidentally provided as a sensitive key
+             * because we cannot protect it and read from it
+            */
+            if (additionalProperties.contains(ADDITIONAL_SENSITIVE_PROPERTIES_KEY)) {
+                logger.warn("The key '{}' contains itself. This is poor practice and should be removed", ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+                additionalProperties.remove(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+            }
+            additionalProperties.addAll(DEFAULT_SENSITIVE_PROPERTIES);
+            return additionalProperties;
+        }
+    }
+
+    /**
+     * Returns a list of the keys identifying "sensitive" properties. There is a default list,
+     * and additional keys can be provided in the {@code nifi.sensitive.props.additional.keys} property in {@code nifi.properties}.
+     *
+     * @return the list of sensitive property keys
+     */
+    public List<String> getPopulatedSensitivePropertyKeys() {
+        List<String> allSensitiveKeys = getSensitivePropertyKeys();
+        return allSensitiveKeys.stream().filter(k -> StringUtils.isNotBlank(getProperty(k))).collect(Collectors.toList());
+    }
+
+    /**
+     * Returns true if any sensitive keys are protected.
+     *
+     * @return true if any key is protected; false otherwise
+     */
+    public boolean hasProtectedKeys() {
+        List<String> sensitiveKeys = getSensitivePropertyKeys();
+        for (String k : sensitiveKeys) {
+            if (isPropertyProtected(k)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a Map of the keys identifying "sensitive" properties that are currently protected and the "protection" key for each.
+     *
+     * This may or may not include all properties marked as sensitive.
+     *
+     * @return the Map of protected property keys and the protection identifier for each
+     */
+    public Map<String, String> getProtectedPropertyKeys() {
+        List<String> sensitiveKeys = getSensitivePropertyKeys();
+
+        Map<String, String> traditionalProtectedProperties = new HashMap<>();
+        for (String key : sensitiveKeys) {
+            String protection = getProperty(getProtectionKey(key));
+            if (StringUtils.isNotBlank(protection) && StringUtils.isNotBlank(getProperty(key))) {
+                traditionalProtectedProperties.put(key, protection);
+            }
+        }
+
+        return traditionalProtectedProperties;
+    }
+
+    /**
+     * Returns the unique set of all protection schemes currently in use for this instance.
+     *
+     * @return the set of protection schemes
+     */
+    public Set<String> getProtectionSchemes() {
+        return new HashSet<>(getProtectedPropertyKeys().values());
+    }
+
+    /**
+     * Returns a percentage of the total number of populated properties marked as sensitive that are currently protected.
+     *
+     * @return the percent of sensitive properties marked as protected
+     */
+    public int getPercentOfSensitivePropertiesProtected() {
+        return (int) Math.round(getProtectedPropertyKeys().size() / ((double) getPopulatedSensitivePropertyKeys().size()) * 100);
+    }
+
+    /**
+     * Returns true if the property identified by this key is considered sensitive in this instance of {@code NiFiProperties}.
+     * Some properties are sensitive by default, while others can be specified by
+     * {@link ProtectedNiFiRegistryProperties#ADDITIONAL_SENSITIVE_PROPERTIES_KEY}.
+     *
+     * @param key the key
+     * @return true if it is sensitive
+     * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys()
+     */
+    public boolean isPropertySensitive(String key) {
+        // If the explicit check for ADDITIONAL_SENSITIVE_PROPERTIES_KEY is not here, this could loop infinitely
+        return key != null && !key.equals(ADDITIONAL_SENSITIVE_PROPERTIES_KEY) && getSensitivePropertyKeys().contains(key.trim());
+    }
+
+    /**
+     * Returns true if the property identified by this key is considered protected in this instance of {@code NiFiProperties}.
+     * The property value is protected if the key is sensitive and the sibling key of key.protected is present.
+     *
+     * @param key the key
+     * @return true if it is currently marked as protected
+     * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys()
+     */
+    public boolean isPropertyProtected(String key) {
+        return key != null && isPropertySensitive(key) && !StringUtils.isBlank(getProperty(getProtectionKey(key)));
+    }
+
+    /**
+     * Returns the sibling property key which specifies the protection scheme for this key.
+     * <p>
+     * Example:
+     * <p>
+     * nifi.registry.sensitive.key=ABCXYZ
+     * nifi.registry.sensitive.key.protected=aes/gcm/256
+     * <p>
+     * nifi.registry.sensitive.key -> nifi.sensitive.key.protected
+     *
+     * @param key the key identifying the sensitive property
+     * @return the key identifying the protection scheme for the sensitive property
+     */
+    public static String getProtectionKey(String key) {
+        if (key == null || key.isEmpty()) {
+            throw new IllegalArgumentException("Cannot find protection key for null key");
+        }
+
+        return key + ".protected";
+    }
+
+    /**
+     * Returns the unprotected {@link NiFiRegistryProperties} instance. If none of the
+     * properties loaded are marked as protected, it will simply pass through the
+     * internal instance. If any are protected, it will drop the protection scheme keys
+     * and translate each protected value (encrypted, HSM-retrieved, etc.) into the raw
+     * value and store it under the original key.
+     * <p>
+     * If any property fails to unprotect, it will save that key and continue. After
+     * attempting all properties, it will throw an exception containing all failed
+     * properties. This is necessary because the order is not enforced, so all failed
+     * properties should be gathered together.
+     *
+     * @return the NiFiRegistryProperties instance with all raw values
+     * @throws SensitivePropertyProtectionException if there is a problem unprotecting one or more keys
+     */
+    public NiFiRegistryProperties getUnprotectedProperties() throws SensitivePropertyProtectionException {
+        if (hasProtectedKeys()) {
+            logger.debug("There are {} protected properties of {} sensitive properties ({}%)",
+                    getProtectedPropertyKeys().size(),
+                    getPopulatedSensitivePropertyKeys().size(),
+                    getPercentOfSensitivePropertiesProtected());
+
+            NiFiRegistryProperties unprotectedProperties = new NiFiRegistryProperties();
+
+            Set<String> failedKeys = new HashSet<>();
+
+            for (String key : getPropertyKeysExcludingProtectionSchemes()) {
+                /* Three kinds of keys
+                 * 1. protection schemes -- skip
+                 * 2. protected keys -- unprotect and copy
+                 * 3. normal keys -- copy over
+                 */
+                if (key.endsWith(".protected")) {
+                    // Do nothing
+                } else if (isPropertyProtected(key)) {
+                    try {
+                        unprotectedProperties.setProperty(key, unprotectValue(key, getProperty(key)));
+                    } catch (SensitivePropertyProtectionException e) {
+                        logger.warn("Failed to unprotect '{}'", key, e);
+                        failedKeys.add(key);
+                    }
+                } else {
+                    unprotectedProperties.setProperty(key, getProperty(key));
+                }
+            }
+
+            if (!failedKeys.isEmpty()) {
+                if (failedKeys.size() > 1) {
+                    logger.warn("Combining {} failed keys [{}] into single exception", failedKeys.size(), StringUtils.join(failedKeys, ", "));
+                    throw new MultipleSensitivePropertyProtectionException("Failed to unprotect keys", failedKeys);
+                } else {
+                    throw new SensitivePropertyProtectionException("Failed to unprotect key " + failedKeys.iterator().next());
+                }
+            }
+
+            return unprotectedProperties;
+        } else {
+            logger.debug("No protected properties");
+            return getInternalNiFiProperties();
+        }
+    }
+
+    /**
+     * Registers a new {@link SensitivePropertyProvider}. This method will throw a {@link UnsupportedOperationException} if a provider is already registered for the protection scheme.
+     *
+     * @param sensitivePropertyProvider the provider
+     */
+    void addSensitivePropertyProvider(SensitivePropertyProvider sensitivePropertyProvider) {
+        if (sensitivePropertyProvider == null) {
+            throw new IllegalArgumentException("Cannot add null SensitivePropertyProvider");
+        }
+
+        if (getSensitivePropertyProviders().containsKey(sensitivePropertyProvider.getIdentifierKey())) {
+            throw new UnsupportedOperationException("Cannot overwrite existing sensitive property provider registered for " + sensitivePropertyProvider.getIdentifierKey());
+        }
+
+        getSensitivePropertyProviders().put(sensitivePropertyProvider.getIdentifierKey(), sensitivePropertyProvider);
+    }
+
+    private String getDefaultProtectionScheme() {
+        if (!getSensitivePropertyProviders().isEmpty()) {
+            List<String> schemes = new ArrayList<>(getSensitivePropertyProviders().keySet());
+            Collections.sort(schemes);
+            return schemes.get(0);
+        } else {
+            throw new IllegalStateException("No registered protection schemes");
+        }
+    }
+
+    /**
+     * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the default protection scheme.
+     *
+     * Plain non-sensitive values are copied directly.
+     *
+     * @return the protected properties in a {@link NiFiRegistryProperties} object
+     * @throws IllegalStateException if no protection schemes are registered
+     */
+    NiFiRegistryProperties protectPlainProperties() {
+        try {
+            return protectPlainProperties(getDefaultProtectionScheme());
+        } catch (IllegalStateException e) {
+            final String msg = "Cannot protect properties with default scheme if no protection schemes are registered";
+            logger.warn(msg);
+            throw new IllegalStateException(msg, e);
+        }
+    }
+
+    /**
+     * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the provided protection scheme.
+     *
+     * Plain non-sensitive values are copied directly.
+     *
+     * @param protectionScheme the identifier key of the {@link SensitivePropertyProvider} to use
+     * @return the protected properties in a {@link NiFiRegistryProperties} object
+     */
+    NiFiRegistryProperties protectPlainProperties(String protectionScheme) {
+        SensitivePropertyProvider spp = getSensitivePropertyProvider(protectionScheme);
+
+        NiFiRegistryProperties protectedProperties = new NiFiRegistryProperties();
+
+        // Copy over the plain keys
+        Set<String> plainKeys = getPropertyKeysExcludingProtectionSchemes();
+        plainKeys.removeAll(getSensitivePropertyKeys());
+        for (String key : plainKeys) {
+            protectedProperties.setProperty(key, getInternalNiFiProperties().getProperty(key));
+        }
+
+        // Add the protected keys and the protection schemes
+        for (String key : getSensitivePropertyKeys()) {
+            final String plainValue = getProperty(key);
+            if (plainValue != null && !plainValue.trim().isEmpty()) {
+                final String protectedValue = spp.protect(plainValue);
+                protectedProperties.setProperty(key, protectedValue);
+                protectedProperties.setProperty(getProtectionKey(key), protectionScheme);
+            }
+        }
+
+        return protectedProperties;
+    }
+
+    /**
+     * Returns the number of properties that are marked as protected in the provided {@link NiFiRegistryProperties} instance
+     * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance.
+     *
+     * @param plainProperties the instance to count protected properties
+     * @return the number of protected properties
+     */
+    public static int countProtectedProperties(NiFiRegistryProperties plainProperties) {
+        return new ProtectedNiFiRegistryProperties(plainProperties).getProtectedPropertyKeys().size();
+    }
+
+    /**
+     * Returns the number of properties that are marked as sensitive in the provided {@link NiFiRegistryProperties} instance
+     * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance.
+     *
+     * @param plainProperties the instance to count sensitive properties
+     * @return the number of sensitive properties
+     */
+    public static int countSensitiveProperties(NiFiRegistryProperties plainProperties) {
+        return new ProtectedNiFiRegistryProperties(plainProperties).getSensitivePropertyKeys().size();
+    }
+
+    @Override
+    public String toString() {
+        final Set<String> providers = getSensitivePropertyProviders().keySet();
+        return new StringBuilder("ProtectedNiFiProperties instance with ")
+                .append(getPropertyKeysIncludingProtectionSchemes().size())
+                .append(" properties (")
+                .append(getProtectedPropertyKeys().size())
+                .append(" protected) and ")
+                .append(providers.size())
+                .append(" sensitive property providers: ")
+                .append(StringUtils.join(providers, ", "))
+                .toString();
+    }
+
+    /**
+     * Returns the local provider cache (null-safe) as a Map of protection schemes -> implementations.
+     *
+     * @return the map
+     */
+    private Map<String, SensitivePropertyProvider> getSensitivePropertyProviders() {
+        if (localProviderCache == null) {
+            localProviderCache = new HashMap<>();
+        }
+
+        return localProviderCache;
+    }
+
+    private SensitivePropertyProvider getSensitivePropertyProvider(String protectionScheme) {
+        if (isProviderAvailable(protectionScheme)) {
+            return getSensitivePropertyProviders().get(protectionScheme);
+        } else {
+            throw new SensitivePropertyProtectionException("No provider available for " + protectionScheme);
+        }
+    }
+
+    private boolean isProviderAvailable(String protectionScheme) {
+        return getSensitivePropertyProviders().containsKey(protectionScheme);
+    }
+
+    /**
+     * If the value is protected, unprotects it and returns it. If not, returns the original value.
+     *
+     * @param key            the retrieved property key
+     * @param retrievedValue the retrieved property value
+     * @return the unprotected value
+     */
+    private String unprotectValue(String key, String retrievedValue) {
+        // Checks if the key is sensitive and marked as protected
+        if (isPropertyProtected(key)) {
+            final String protectionScheme = getProperty(getProtectionKey(key));
+
+            // No provider registered for this scheme, so just return the value
+            if (!isProviderAvailable(protectionScheme)) {
+                logger.warn("No provider available for {} so passing the protected {} value back", protectionScheme, key);
+                return retrievedValue;
+            }
+
+            try {
+                SensitivePropertyProvider sensitivePropertyProvider = getSensitivePropertyProvider(protectionScheme);
+                return sensitivePropertyProvider.unprotect(retrievedValue);
+            } catch (SensitivePropertyProtectionException e) {
+                throw new SensitivePropertyProtectionException("Error unprotecting value for " + key, e.getCause());
+            }
+        }
+        return retrievedValue;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
new file mode 100644
index 0000000..2ffa902
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
@@ -0,0 +1,89 @@
+/*
+ * 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.registry.properties;
+
+public class SensitivePropertyProtectionException extends RuntimeException {
+    /**
+     * Constructs a new throwable with {@code null} as its detail message.
+     * The cause is not initialized, and may subsequently be initialized by a
+     * call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     */
+    public SensitivePropertyProtectionException() {
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message.  The
+     * cause is not initialized, and may subsequently be initialized by
+     * a call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message. The detail message is saved for
+     *                later retrieval by the {@link #getMessage()} method.
+     */
+    public SensitivePropertyProtectionException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message and
+     * cause.  <p>Note that the detail message associated with
+     * {@code cause} is <i>not</i> automatically incorporated in
+     * this throwable's detail message.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message (which is saved for later retrieval
+     *                by the {@link #getMessage()} method).
+     * @param cause   the cause (which is saved for later retrieval by the
+     *                {@link #getCause()} method).  (A {@code null} value is
+     *                permitted, and indicates that the cause is nonexistent or
+     *                unknown.)
+     */
+    public SensitivePropertyProtectionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs a new throwable with the specified cause and a detail
+     * message of {@code (cause==null ? null : cause.toString())} (which
+     * typically contains the class and detail message of {@code cause}).
+     * This constructor is useful for throwables that are little more than
+     * wrappers for other throwables (for example, PrivilegedActionException).
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param cause the cause (which is saved for later retrieval by the
+     *              {@link #getCause()} method).  (A {@code null} value is
+     *              permitted, and indicates that the cause is nonexistent or
+     *              unknown.)
+     */
+    public SensitivePropertyProtectionException(Throwable cause) {
+        super(cause);
+    }
+
+    @Override
+    public String toString() {
+        return "SensitivePropertyProtectionException: " + getLocalizedMessage();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
new file mode 100644
index 0000000..c0dd43c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
@@ -0,0 +1,52 @@
+/*
+ * 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.registry.properties;
+
+public interface SensitivePropertyProvider {
+
+    /**
+     * Returns the name of the underlying implementation.
+     *
+     * @return the name of this sensitive property provider
+     */
+    String getName();
+
+    /**
+     * Returns the key used to identify the provider implementation in {@code nifi.properties}.
+     *
+     * @return the key to persist in the sibling property
+     */
+    String getIdentifierKey();
+
+    /**
+     * 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.
+     * An encryption-based provider would return a cipher text, while a remote-lookup provider could return a unique ID to retrieve the secured value.
+     *
+     * @param unprotectedValue the sensitive value
+     * @return the value to persist in the {@code nifi.properties} file
+     */
+    String protect(String unprotectedValue) throws SensitivePropertyProtectionException;
+
+    /**
+     * Returns the "unprotected" form of this value. This is the raw sensitive value which is used by the application logic.
+     * An encryption-based provider would decrypt a cipher text and return the plaintext, while a remote-lookup provider could retrieve the secured value.
+     *
+     * @param protectedValue the protected value read from the {@code nifi.properties} file
+     * @return the raw value to be used by the application
+     */
+    String unprotect(String protectedValue) throws SensitivePropertyProtectionException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..c9d4313
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.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.registry.properties;
+
+public interface SensitivePropertyProviderFactory {
+
+    SensitivePropertyProvider getProvider();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
new file mode 100644
index 0000000..df3bbe6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
@@ -0,0 +1,48 @@
+/*
+ * 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.registry.properties.util;
+
+import java.util.regex.Pattern;
+
+/**
+ * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties.
+ */
+public class IdentityMapping {
+
+    private final String key;
+    private final Pattern pattern;
+    private final String replacementValue;
+
+    public IdentityMapping(String key, Pattern pattern, String replacementValue) {
+        this.key = key;
+        this.pattern = pattern;
+        this.replacementValue = replacementValue;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    public String getReplacementValue() {
+        return replacementValue;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
new file mode 100644
index 0000000..3c9208c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
@@ -0,0 +1,145 @@
+/*
+ * 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.registry.properties.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IdentityMappingUtil {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(IdentityMappingUtil.class);
+    private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)");
+
+    /**
+     * Builds the identity mappings from NiFiRegistryProperties.
+     *
+     * @param properties the NiFiRegistryProperties instance
+     * @return a list of identity mappings
+     */
+    public static List<IdentityMapping> getIdentityMappings(final NiFiRegistryProperties properties) {
+        final List<IdentityMapping> mappings = new ArrayList<>();
+
+        // go through each property
+        for (String propertyName : properties.getPropertyKeys()) {
+            if (StringUtils.startsWith(propertyName, NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) {
+                final String key = StringUtils.substringAfter(propertyName, NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX);
+                final String identityPattern = properties.getProperty(propertyName);
+
+                if (StringUtils.isBlank(identityPattern)) {
+                    LOGGER.warn("Identity Mapping property {} was found, but was empty", new Object[]{propertyName});
+                    continue;
+                }
+
+                final String identityValueProperty = NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX + key;
+                final String identityValue = properties.getProperty(identityValueProperty);
+
+                if (StringUtils.isBlank(identityValue)) {
+                    LOGGER.warn("Identity Mapping property {} was found, but corresponding value {} was not found",
+                            new Object[]{propertyName, identityValueProperty});
+                    continue;
+                }
+
+                final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue);
+                mappings.add(identityMapping);
+
+                LOGGER.debug("Found Identity Mapping with key = {}, pattern = {}, value = {}",
+                        new Object[] {key, identityPattern, identityValue});
+            }
+        }
+
+        // sort the list by the key so users can control the ordering in nifi-registry.properties
+        Collections.sort(mappings, new Comparator<IdentityMapping>() {
+            @Override
+            public int compare(IdentityMapping m1, IdentityMapping m2) {
+                return m1.getKey().compareTo(m2.getKey());
+            }
+        });
+
+        return mappings;
+    }
+
+    /**
+     * Checks the given identity against each provided mapping and performs the mapping using the first one that matches.
+     * If none match then the identity is returned as is.
+     *
+     * @param identity the identity to map
+     * @param mappings the mappings
+     * @return the mapped identity, or the same identity if no mappings matched
+     */
+    public static String mapIdentity(final String identity, List<IdentityMapping> mappings) {
+        for (IdentityMapping mapping : mappings) {
+            Matcher m = mapping.getPattern().matcher(identity);
+            if (m.matches()) {
+                final String pattern = mapping.getPattern().pattern();
+                final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount());
+                return identity.replaceAll(pattern, replacementValue);
+            }
+        }
+
+        return identity;
+    }
+
+    // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing
+    // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather
+    // than attempting to use it as a back reference.
+    private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) {
+        if (numCapturingGroups == 0) {
+            return unescaped;
+        }
+
+        String value = unescaped;
+        final Matcher backRefMatcher = backReferencePattern.matcher(value);
+        while (backRefMatcher.find()) {
+            final String backRefNum = backRefMatcher.group(1);
+            if (backRefNum.startsWith("0")) {
+                continue;
+            }
+            final int originalBackRefIndex = Integer.parseInt(backRefNum);
+            int backRefIndex = originalBackRefIndex;
+
+            // if we have a replacement value like $123, and we have less than 123 capturing groups, then
+            // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups,
+            // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then
+            // we want to truncate the 1 and get 0.
+            while (backRefIndex > numCapturingGroups && backRefIndex >= 10) {
+                backRefIndex /= 10;
+            }
+
+            if (backRefIndex > numCapturingGroups) {
+                final StringBuilder sb = new StringBuilder(value.length() + 1);
+                final int groupStart = backRefMatcher.start(1);
+
+                sb.append(value.substring(0, groupStart - 1));
+                sb.append("\\");
+                sb.append(value.substring(groupStart - 1));
+                value = sb.toString();
+            }
+        }
+
+        return value;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
new file mode 100644
index 0000000..191b5e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
@@ -0,0 +1,81 @@
+/*
+ * 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.registry.security.crypto;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * An implementation of {@link CryptoKeyProvider} that loads the key from disk every time it is needed.
+ *
+ * The persistence-backing of the key is in the bootstrap.conf file, which must be provided to the
+ * constructor of this class.
+ *
+ * As key access for sensitive value decryption is only used a few times during server initialization,
+ * this implementation trades efficiency for security by only keeping the key in memory with an
+ * in-scope reference for a brief period of time (assuming callers do not maintain an in-scope reference).
+ *
+ * @see CryptoKeyProvider
+ */
+public class BootstrapFileCryptoKeyProvider implements CryptoKeyProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(BootstrapFileCryptoKeyProvider.class);
+
+    private final String bootstrapFile;
+
+    /**
+     * Construct a new instance backed by the contents of a bootstrap.conf file.
+     *
+     * @param bootstrapFilePath The path to the bootstrap.conf file for this instance of NiFi Registry.
+     *                          Must not be null.
+     */
+    public BootstrapFileCryptoKeyProvider(final String bootstrapFilePath) {
+        if (bootstrapFilePath == null) {
+            throw new IllegalArgumentException(BootstrapFileCryptoKeyProvider.class.getSimpleName() + " cannot be initialized with null bootstrap file path.");
+        }
+        this.bootstrapFile = bootstrapFilePath;
+    }
+
+    /**
+     * @return The bootstrap file path that backs this provider instance.
+     */
+    public String getBootstrapFile() {
+        return bootstrapFile;
+    }
+
+    @Override
+    public String getKey() throws MissingCryptoKeyException {
+        try {
+            return CryptoKeyLoader.extractKeyFromBootstrapFile(this.bootstrapFile);
+        } catch (IOException ioe) {
+            final String errMsg = "Loading the master crypto key from bootstrap file '" + bootstrapFile + "' failed due to IOException.";
+            logger.warn(errMsg);
+            throw new MissingCryptoKeyException(errMsg, ioe);
+        }
+
+    }
+
+    @Override
+    public String toString() {
+        return "BootstrapFileCryptoKeyProvider{" +
+                "bootstrapFile='" + bootstrapFile + '\'' +
+                '}';
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
new file mode 100644
index 0000000..d828773
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
@@ -0,0 +1,87 @@
+/*
+ * 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.registry.security.crypto;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class CryptoKeyLoader {
+
+    private static final Logger logger = LoggerFactory.getLogger(CryptoKeyLoader.class);
+
+    private static final String BOOTSTRAP_KEY_PREFIX = "nifi.registry.bootstrap.sensitive.key=";
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties.
+     * The key extracted from the bootstrap.conf file at the specified location.
+     *
+     * @param bootstrapPath the path to the bootstrap file
+     * @return the key in hexadecimal format, or {@link CryptoKeyProvider#EMPTY_KEY} if the key is null or empty
+     * @throws IOException if the file is not readable
+     */
+    public static String extractKeyFromBootstrapFile(String bootstrapPath) throws IOException {
+        File bootstrapFile;
+        if (StringUtils.isBlank(bootstrapPath)) {
+            logger.error("Cannot read from bootstrap.conf file to extract encryption key; location not specified");
+            throw new IOException("Cannot read from bootstrap.conf without file location");
+        } else {
+            bootstrapFile = new File(bootstrapPath);
+        }
+
+        String keyValue;
+        if (bootstrapFile.exists() && bootstrapFile.canRead()) {
+            try (Stream<String> stream = Files.lines(Paths.get(bootstrapFile.getAbsolutePath()))) {
+                Optional<String> keyLine = stream.filter(l -> l.startsWith(BOOTSTRAP_KEY_PREFIX)).findFirst();
+                if (keyLine.isPresent()) {
+                    keyValue = keyLine.get().split("=", 2)[1];
+                    keyValue = checkHexKey(keyValue);
+                } else {
+                    keyValue = CryptoKeyProvider.EMPTY_KEY;
+                }
+            } catch (IOException e) {
+                logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key", bootstrapFile.getAbsolutePath());
+                throw new IOException("Cannot read from bootstrap.conf", e);
+            }
+        } else {
+            logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key -- file is missing or permissions are incorrect", bootstrapFile.getAbsolutePath());
+            throw new IOException("Cannot read from bootstrap.conf");
+        }
+
+        if (CryptoKeyProvider.EMPTY_KEY.equals(keyValue)) {
+            logger.info("No encryption key present in the bootstrap.conf file at {}", bootstrapFile.getAbsolutePath());
+        }
+
+        return keyValue;
+    }
+
+    private static String checkHexKey(String input) {
+        if (input == null || input.trim().isEmpty()) {
+            logger.debug("Checking the hex key value that was loaded determined the key is empty.");
+            return CryptoKeyProvider.EMPTY_KEY;
+        }
+        return input;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
new file mode 100644
index 0000000..bab8d7c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
@@ -0,0 +1,68 @@
+/*
+ * 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.registry.security.crypto;
+
+/**
+ * A simple interface that wraps a key that can be used for encryption and decryption.
+ * This allows for more flexibility with the lifecycle of keys and how other classes
+ * can declare dependencies for keys, by depending on a CryptoKeyProvider that will provided
+ * at runtime.
+ */
+public interface CryptoKeyProvider {
+
+    /**
+     * A string literal that indicates the contents of a key are empty.
+     * Can also be used in contexts that a null key is undesirable.
+     */
+    String EMPTY_KEY = "";
+
+    /**
+     * @return The crypto key known to this CryptoKeyProvider instance in hexadecimal format, or
+     *         {@link #EMPTY_KEY} if the key is empty.
+     * @throws MissingCryptoKeyException if the key cannot be provided or determined for any reason.
+     *         If the key is known to be empty, {@link #EMPTY_KEY} will be returned and a
+     *         CryptoKeyMissingException will not be thrown
+     */
+    String getKey() throws MissingCryptoKeyException;
+
+    /**
+     * @return A boolean indicating if the key value held by this CryptoKeyProvider is empty,
+     *         such as 'null' or empty string.
+     */
+    default boolean isEmpty() {
+        String key;
+        try {
+            key = getKey();
+        } catch (MissingCryptoKeyException e) {
+            return true;
+        }
+        return EMPTY_KEY.equals(key);
+    }
+
+    /**
+     * A string representation of this CryptoKeyProvider instance.
+     * <p>
+     * <p>
+     * Note: Implementations of this interface should take care not to leak sensitive
+     * key material in any strings they emmit, including in the toString implementation.
+     *
+     * @return A string representation of this CryptoKeyProvider instance.
+     */
+    @Override
+    public String toString();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
new file mode 100644
index 0000000..dbc3752
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
@@ -0,0 +1,47 @@
+/*
+ * 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.registry.security.crypto;
+
+/**
+ * An exception type used by a {@link CryptoKeyProvider} when a request for the key
+ * cannot be fulfilled for any reason.
+ *
+ * @see CryptoKeyProvider
+ */
+public class MissingCryptoKeyException extends Exception {
+
+    public MissingCryptoKeyException() {
+        super();
+    }
+
+    public MissingCryptoKeyException(String message) {
+        super(message);
+    }
+
+    public MissingCryptoKeyException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public MissingCryptoKeyException(Throwable cause) {
+        super(cause);
+    }
+
+    protected MissingCryptoKeyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
new file mode 100644
index 0000000..0d1d5e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
@@ -0,0 +1,81 @@
+/*
+ * 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.registry.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class AESSensitivePropertyProviderFactoryTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactoryTest.class)
+
+    private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    @Test
+    public void testShouldGetProviderWithKey() throws Exception {
+        // Arrange
+        SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_128)
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert provider.@key
+        assert provider.@cipher
+    }
+
+    @Test
+    public void testShouldGetProviderWith256BitKey() throws Exception {
+        // Arrange
+        Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+        SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_256)
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert provider.@key
+        assert provider.@cipher
+    }
+}
\ No newline at end of file