You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2021/06/11 20:48:39 UTC

[nifi] branch main updated: NIFI-8651: Refactor Sensitive Properties Providers for extension

This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 1ccc4fb  NIFI-8651: Refactor Sensitive Properties Providers for extension
1ccc4fb is described below

commit 1ccc4fbb0ff6a4041ddff180ab07a1675308e4a3
Author: Joe Gresock <jg...@gmail.com>
AuthorDate: Sun Jun 6 21:53:39 2021 -0400

    NIFI-8651: Refactor Sensitive Properties Providers for extension
    
    This closes #5131
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 .../nifi/bootstrap/util/SecureNiFiConfigUtil.java  |   4 +-
 nifi-commons/nifi-properties/pom.xml               |   8 +
 .../nifi/util/NiFiBootstrapPropertiesLoader.java   |  19 +-
 .../org/apache/nifi/util/NiFiBootstrapUtils.java   |  82 ++++
 .../java/org/apache/nifi/util/NiFiProperties.java  |  48 +-
 .../pom.xml                                        |   2 +-
 .../AbstractBootstrapPropertiesLoader.java         | 162 +++++++
 .../nifi/properties/ApplicationProperties.java     |  14 +-
 .../nifi/properties/BootstrapProperties.java       |  86 ++++
 .../nifi/properties/ProtectedProperties.java       |  59 +++
 .../apache/nifi/properties/ReadableProperties.java |  50 ++
 .../properties/StandardReadableProperties.java     |  57 +--
 .../org/apache/nifi/security/kms/CryptoUtils.java  | 112 +----
 .../security/util/StandardTlsConfiguration.java    |  32 +-
 .../nifi-sensitive-property-provider}/pom.xml      |  78 ++--
 .../properties/AESSensitivePropertyProvider.java   | 117 +++--
 .../AbstractSensitivePropertyProvider.java         |  63 +++
 .../properties/ApplicationPropertiesProtector.java | 340 ++++++++++++++
 ...ltipleSensitivePropertyProtectionException.java |   0
 .../nifi/properties/PropertyProtectionScheme.java  |  79 ++++
 .../SensitivePropertyProtectionException.java      |   0
 .../properties/SensitivePropertyProtector.java     | 146 ++++++
 .../nifi/properties/SensitivePropertyProvider.java |   7 +
 .../SensitivePropertyProviderFactory.java          |  15 +-
 .../SensitivePropertyProviderFactoryAware.java     |  46 ++
 .../StandardSensitivePropertyProviderFactory.java  | 121 +++++
 .../AESSensitivePropertyProviderTest.groovy        |   0
 ...andardSensitivePropertyProviderFactoryTest.java | 140 ++++++
 nifi-commons/pom.xml                               |   2 +
 .../nifi/authorization/AuthorizerFactoryBean.java  |  89 ++--
 .../authorization/AuthorizerFactoryBeanTest.groovy |  29 +-
 ...ncryptedRepositoryRecordSerdeFactoryTest.groovy |   7 +-
 .../PopularVoteFlowElectionFactoryBeanTest.groovy  |   4 +-
 .../okhttp/OkHttpReplicationClientTest.groovy      |  20 +-
 .../nifi/cluster/ZooKeeperClientConfigTest.java    |  38 +-
 .../state/server/TestZooKeeperStateServer.java     |  10 +-
 .../election/ITSecureClientZooKeeperFactory.java   |  16 +-
 .../nifi-framework/nifi-properties-loader/pom.xml  |   4 +
 .../AESSensitivePropertyProviderFactory.java       |  53 ---
 .../nifi/properties/NiFiPropertiesLoader.java      |  92 +---
 .../nifi/properties/ProtectedNiFiProperties.java   | 466 ++++---------------
 .../AESSensitivePropertyProviderFactoryTest.groovy | 105 -----
 ...Test.groovy => NiFiPropertiesGroovyTest.groovy} |  88 ++--
 .../NiFiPropertiesLoaderGroovyTest.groovy          |  51 +--
 .../ProtectedNiFiPropertiesGroovyTest.groovy       |  80 ++--
 .../groovy/org/apache/nifi/NiFiGroovyTest.groovy   |  25 +-
 .../remote/StandardPublicPortGroovyTest.groovy     |   4 +-
 .../nifi/remote/TestHttpRemoteSiteListener.java    |   7 +-
 .../nifi/remote/TestPeerDescriptionModifier.java   |  23 +-
 .../http/TestHttpFlowFileServerProtocol.java       |   7 +-
 .../nifi/web/server/JettyServerGroovyTest.groovy   |  45 +-
 .../nifi/web/server/HostHeaderHandlerTest.groovy   |  10 +-
 .../nifi/web/api/ApplicationResourceTest.groovy    |  16 +-
 .../nifi/integration/util/NiFiTestServer.java      |   3 +-
 .../spring/LoginIdentityProviderFactoryBean.java   |  80 ++--
 .../web/security/oidc/OidcServiceGroovyTest.groovy |  24 +-
 .../StandardOidcIdentityProviderGroovyTest.groovy  |  29 +-
 .../LoginIdentityProviderFactoryBeanTest.groovy    |  48 +-
 .../jwt/JwtAuthenticationProviderTest.java         |   3 +-
 .../nifi/web/security/jwt/JwtServiceTest.java      |   4 +-
 .../saml/impl/TestStandardSAMLService.java         |  11 +
 nifi-nar-bundles/nifi-framework-bundle/pom.xml     |   5 +
 .../EncryptedWriteAheadProvenanceRepository.java   |  16 +-
 .../authentication/IdentityProviderFactory.java    |   4 +-
 .../security/authorization/AuthorizerFactory.java  |   4 +-
 .../SensitivePropertyProviderConfiguration.java    |  36 +-
 .../provider/TestStandardProviderFactory.java      |  21 +-
 .../provider/hook/TestScriptEventHookProvider.java |   6 +-
 .../database/TestDatabaseAccessPolicyProvider.java |  15 +-
 .../database/TestDatabaseUserGroupProvider.java    |   7 +-
 .../nifi-registry-properties/pom.xml               |   6 +
 .../properties/AESSensitivePropertyProvider.java   | 265 -----------
 .../AESSensitivePropertyProviderFactory.java       |  54 ---
 ...ltipleSensitivePropertyProtectionException.java | 129 ------
 .../properties/NiFiRegistryProperties.java         |  41 +-
 .../properties/NiFiRegistryPropertiesLoader.java   |  63 +--
 .../ProtectedNiFiRegistryProperties.java           | 505 ++++-----------------
 .../SensitivePropertyProtectionException.java      |  89 ----
 .../properties/SensitivePropertyProvider.java      |  52 ---
 .../NiFiRegistryBootstrapPropertiesLoader.java     |  20 +-
 .../util/NiFiRegistryBootstrapUtils.java           |  82 ++++
 .../crypto/BootstrapFileCryptoKeyProvider.java     |   3 +-
 .../registry/security/crypto/CryptoKeyLoader.java  |  87 ----
 .../AESSensitivePropertyProviderFactoryTest.groovy |  81 ----
 .../AESSensitivePropertyProviderTest.groovy        | 471 -------------------
 .../NiFiRegistryPropertiesGroovyTest.groovy        |  13 +-
 .../NiFiRegistryPropertiesLoaderGroovyTest.groovy  |  40 +-
 ...otectedNiFiRegistryPropertiesGroovyTest.groovy} | 111 +++--
 .../NiFiRegistryBootstrapUtilsGroovyTest.groovy}   |  17 +-
 .../org/apache/nifi/registry/NiFiRegistry.java     |  18 +-
 .../nifi/registry/web/api/IntegrationTestBase.java |  11 +-
 .../apache/nifi/registry/web/api/SecureLdapIT.java |   8 +-
 .../FlowPersistenceProviderMigrator.java           |  12 +-
 .../admin/nodemanager/NodeManagerTool.groovy       |   3 +-
 .../toolkit/admin/notify/NotificationTool.groovy   |   3 +-
 .../admin/client/NiFiClientFactorySpec.groovy      |   3 +-
 nifi-toolkit/nifi-toolkit-encrypt-config/pom.xml   |   7 +-
 .../nifi/properties/ConfigEncryptionTool.groovy    | 177 +++++---
 .../nifi/toolkit/encryptconfig/DecryptMode.groovy  |  33 +-
 .../encryptconfig/NiFiRegistryDecryptMode.groovy   |  12 +-
 .../toolkit/encryptconfig/NiFiRegistryMode.groovy  |  70 ++-
 .../NiFiRegistryAuthorizersXmlEncryptor.groovy     |   2 +-
 .../properties/ConfigEncryptionToolTest.groovy     | 176 ++-----
 .../encryptconfig/EncryptConfigMainTest.groovy     |   7 +-
 .../nifi/toolkit/encryptconfig/TestUtil.groovy     |   7 +-
 .../src/test/resources/bootstrap.conf              |   6 +-
 .../test/resources/login-identity-providers.xml    |   2 +-
 pom.xml                                            |   2 +-
 108 files changed, 2766 insertions(+), 3416 deletions(-)

diff --git a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java
index 10d825f..bb1abee 100644
--- a/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java
+++ b/nifi-bootstrap/src/main/java/org/apache/nifi/bootstrap/util/SecureNiFiConfigUtil.java
@@ -96,7 +96,7 @@ public class SecureNiFiConfigUtil {
      * @throws IOException can be thrown when writing keystores to disk
      * @throws RuntimeException indicates a security exception while generating keystores
      */
-    public static void configureSecureNiFiProperties(String nifiPropertiesFilename, Logger cmdLogger) throws IOException, RuntimeException {
+    public static void configureSecureNiFiProperties(final String nifiPropertiesFilename, final Logger cmdLogger) throws IOException, RuntimeException {
         final File propertiesFile = new File(nifiPropertiesFilename);
         final Properties nifiProperties = loadProperties(propertiesFile);
 
@@ -111,7 +111,7 @@ public class SecureNiFiConfigUtil {
         boolean truststoreExists = fileExists(truststorePath);
 
         if (!keystoreExists && !truststoreExists) {
-            TlsConfiguration tlsConfiguration = null;
+            TlsConfiguration tlsConfiguration;
             cmdLogger.info("Generating Self-Signed Certificate: Expires on {}", LocalDate.now().plus(CERT_DURATION_DAYS, ChronoUnit.DAYS));
             try {
                 String[] subjectAlternativeNames = getSubjectAlternativeNames(nifiProperties, cmdLogger);
diff --git a/nifi-commons/nifi-properties/pom.xml b/nifi-commons/nifi-properties/pom.xml
index fbcfd9d..5a81dad 100644
--- a/nifi-commons/nifi-properties/pom.xml
+++ b/nifi-commons/nifi-properties/pom.xml
@@ -15,6 +15,14 @@
 -->
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-property-utils</artifactId>
+            <version>1.14.0-SNAPSHOT</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
     <parent>
         <groupId>org.apache.nifi</groupId>
         <artifactId>nifi-commons</artifactId>
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiBootstrapPropertiesLoader.java
similarity index 60%
rename from nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
rename to nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiBootstrapPropertiesLoader.java
index c9d4313..da0e889 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiBootstrapPropertiesLoader.java
@@ -14,10 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.registry.properties;
+package org.apache.nifi.util;
 
-public interface SensitivePropertyProviderFactory {
+import org.apache.nifi.properties.AbstractBootstrapPropertiesLoader;
 
-    SensitivePropertyProvider getProvider();
+public class NiFiBootstrapPropertiesLoader extends AbstractBootstrapPropertiesLoader {
+    @Override
+    protected String getApplicationPrefix() {
+        return "nifi";
+    }
 
+    @Override
+    protected String getApplicationPropertiesFilename() {
+        return "nifi.properties";
+    }
+
+    @Override
+    protected String getApplicationPropertiesFilePathSystemProperty() {
+        return NiFiProperties.PROPERTIES_FILE_PATH;
+    }
 }
diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiBootstrapUtils.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiBootstrapUtils.java
new file mode 100644
index 0000000..1360578
--- /dev/null
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiBootstrapUtils.java
@@ -0,0 +1,82 @@
+/*
+ * 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.util;
+
+import org.apache.nifi.properties.AbstractBootstrapPropertiesLoader;
+import org.apache.nifi.properties.BootstrapProperties;
+
+import java.io.IOException;
+
+/**
+ * Encapsulates utility methods for dealing with bootstrap.conf or nifi.properties.
+ */
+public class NiFiBootstrapUtils {
+    private static final AbstractBootstrapPropertiesLoader BOOTSTRAP_PROPERTIES_LOADER = new NiFiBootstrapPropertiesLoader();
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties, extracted from
+     * {@code $NIFI_HOME/conf/bootstrap.conf}.
+     *
+     * @return the key in hexadecimal format
+     * @throws IOException if the file is not readable
+     */
+    public static String extractKeyFromBootstrapFile() throws IOException {
+        return BOOTSTRAP_PROPERTIES_LOADER.extractKeyFromBootstrapFile();
+    }
+
+    /**
+     * Loads the default bootstrap.conf file into a BootstrapProperties object.
+     * @return The default bootstrap.conf as a BootstrapProperties object
+     * @throws IOException If the file is not readable
+     */
+    public static BootstrapProperties loadBootstrapProperties() throws IOException {
+        return loadBootstrapProperties(null);
+    }
+
+    /**
+     * Loads the bootstrap.conf file into a BootstrapProperties object.
+     * @param bootstrapPath the path to the bootstrap file
+     * @return The bootstrap.conf as a BootstrapProperties object
+     * @throws IOException If the file is not readable
+     */
+    public static BootstrapProperties loadBootstrapProperties(final String bootstrapPath) throws IOException {
+        return BOOTSTRAP_PROPERTIES_LOADER.loadBootstrapProperties(bootstrapPath);
+    }
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties, extracted from
+     * {@code $NIFI_HOME/conf/bootstrap.conf}.
+     *
+     * @param bootstrapPath the path to the bootstrap file (if null, returns the sensitive key
+     *                      found in $NIFI_HOME/conf/bootstrap.conf)
+     * @return the key in hexadecimal format
+     * @throws IOException if the file is not readable
+     */
+    public static String extractKeyFromBootstrapFile(final String bootstrapPath) throws IOException {
+        return BOOTSTRAP_PROPERTIES_LOADER.extractKeyFromBootstrapFile(bootstrapPath);
+    }
+
+    /**
+     * Returns the default file path to {@code $NIFI_HOME/conf/nifi.properties}. If the system
+     * property nifi.properties.file.path is not set, it will be set to the relative conf/nifi.properties
+     *
+     * @return the path to the nifi.properties file
+     */
+    public static String getDefaultApplicationPropertiesFilePath() {
+        return BOOTSTRAP_PROPERTIES_LOADER.getDefaultApplicationPropertiesFilePath();
+    }
+}
diff --git a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
index 371cc58..fdf473e 100644
--- a/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
+++ b/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.util;
 
+import org.apache.nifi.properties.ApplicationProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,7 +47,7 @@ import java.util.stream.Stream;
  * this class or passing it along. Its use should be refactored and minimized
  * over time.
  */
-public abstract class NiFiProperties {
+public class NiFiProperties extends ApplicationProperties {
     private static final Logger logger = LoggerFactory.getLogger(NiFiProperties.class);
 
     // core properties
@@ -392,20 +393,17 @@ public abstract class NiFiProperties {
     public static final int DEFAULT_COMPONENT_STATUS_REPOSITORY_PERSIST_COMPONENT_DAYS = 3;
     public static final String DEFAULT_COMPONENT_STATUS_REPOSITORY_PERSIST_LOCATION = "./status_repository";
 
-    /**
-     * 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
-     */
-    public abstract String getProperty(String key);
+    public NiFiProperties() {
+        this(Collections.EMPTY_MAP);
+    }
 
-    /**
-     * Retrieves all known property keys.
-     *
-     * @return all known property keys
-     */
-    public abstract Set<String> getPropertyKeys();
+    public NiFiProperties(final Map<String, String> props) {
+        super(props);
+    }
+
+    public NiFiProperties(final Properties props) {
+        super(props);
+    }
 
     // getters for core properties //
     public File getFlowConfigurationFile() {
@@ -1591,14 +1589,10 @@ public abstract class NiFiProperties {
     }
 
     public boolean isTlsConfigurationPresent() {
-        return StringUtils.isNotBlank(getProperty(NiFiProperties.SECURITY_KEYSTORE))
-            && getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD) != null
-            && StringUtils.isNotBlank(getProperty(NiFiProperties.SECURITY_TRUSTSTORE))
-            && getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD) != null;
-    }
-
-    public int size() {
-        return getPropertyKeys().size();
+        return StringUtils.isNotBlank(getProperty(SECURITY_KEYSTORE))
+            && getProperty(SECURITY_KEYSTORE_PASSWD) != null
+            && StringUtils.isNotBlank(getProperty(SECURITY_TRUSTSTORE))
+            && getProperty(SECURITY_TRUSTSTORE_PASSWD) != null;
     }
 
     public String getFlowFileRepoEncryptionKeyId() {
@@ -2026,6 +2020,11 @@ public abstract class NiFiProperties {
             public Set<String> getPropertyKeys() {
                 return properties.stringPropertyNames();
             }
+
+            @Override
+            public int size() {
+                return getPropertyKeys().size();
+            }
         };
     }
 
@@ -2077,4 +2076,9 @@ public abstract class NiFiProperties {
         }
         // Other properties to validate...
     }
+
+    @Override
+    public String toString() {
+        return "NiFiProperties instance with " + size() + " properties";
+    }
 }
diff --git a/nifi-commons/nifi-properties/pom.xml b/nifi-commons/nifi-property-utils/pom.xml
similarity index 96%
copy from nifi-commons/nifi-properties/pom.xml
copy to nifi-commons/nifi-property-utils/pom.xml
index fbcfd9d..6ef57b2 100644
--- a/nifi-commons/nifi-properties/pom.xml
+++ b/nifi-commons/nifi-property-utils/pom.xml
@@ -20,5 +20,5 @@
         <artifactId>nifi-commons</artifactId>
         <version>1.14.0-SNAPSHOT</version>
     </parent>
-    <artifactId>nifi-properties</artifactId>
+    <artifactId>nifi-property-utils</artifactId>
 </project>
diff --git a/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/AbstractBootstrapPropertiesLoader.java b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/AbstractBootstrapPropertiesLoader.java
new file mode 100644
index 0000000..cbeea84
--- /dev/null
+++ b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/AbstractBootstrapPropertiesLoader.java
@@ -0,0 +1,162 @@
+/*
+ * 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+
+/**
+ * An abstract base class for an application-specific BootstrapProperties loader.
+ */
+public abstract class AbstractBootstrapPropertiesLoader {
+    private static final Logger logger = LoggerFactory.getLogger(AbstractBootstrapPropertiesLoader.class);
+
+    private static final String RELATIVE_APPLICATION_PROPERTIES_PATTERN = "conf/%s";
+    private static final String BOOTSTRAP_CONF = "bootstrap.conf";
+
+    /**
+     * Return the property prefix used in the bootstrap.conf file for this application.
+     * @return the property prefix
+     */
+    protected abstract String getApplicationPrefix();
+
+    /**
+     * Return the name of the main application properties file (e.g., nifi.properties).  This will be
+     * used to determine the default location of the application properties file.
+     * @return The name of the application properties file
+     */
+    protected abstract String getApplicationPropertiesFilename();
+
+    /**
+     * Return the system property name that should specify the file path of the main
+     * application properties file.
+     * @return The system property name that should provide the file path of the main application
+     * properties file
+     */
+    protected abstract String getApplicationPropertiesFilePathSystemProperty();
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties, extracted from
+     * {@code $APPLICATION_HOME/conf/bootstrap.conf}.
+     *
+     * @return the key in hexadecimal format
+     * @throws IOException if the file is not readable
+     */
+    public String extractKeyFromBootstrapFile() throws IOException {
+        return extractKeyFromBootstrapFile(null);
+    }
+
+    /**
+     * Loads the bootstrap.conf file into a BootstrapProperties object.
+     * @param bootstrapPath the path to the bootstrap file
+     * @return The bootstrap.conf as a BootstrapProperties object
+     * @throws IOException If the file is not readable
+     */
+    public BootstrapProperties loadBootstrapProperties(final String bootstrapPath) throws IOException {
+        final Properties properties = new Properties();
+        final Path bootstrapFilePath = getBootstrapFile(bootstrapPath).toPath();
+        try (final InputStream bootstrapInput = Files.newInputStream(bootstrapFilePath)) {
+            properties.load(bootstrapInput);
+            return new BootstrapProperties(getApplicationPrefix(), properties, bootstrapFilePath);
+        } catch (final IOException e) {
+            logger.error("Cannot read from bootstrap.conf file at {}", bootstrapFilePath);
+            throw new IOException("Cannot read from bootstrap.conf", e);
+        }
+    }
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties, extracted from
+     * {@code $APPLICATION_HOME/conf/bootstrap.conf}.
+     *
+     * @param bootstrapPath the path to the bootstrap file (if null, returns the sensitive key
+     *                      found in $APPLICATION_HOME/conf/bootstrap.conf)
+     * @return the key in hexadecimal format
+     * @throws IOException if the file is not readable
+     */
+    public String extractKeyFromBootstrapFile(final String bootstrapPath) throws IOException {
+        final BootstrapProperties bootstrapProperties = loadBootstrapProperties(bootstrapPath);
+
+        return bootstrapProperties.getBootstrapSensitiveKey().orElseGet(() -> {
+            logger.warn("No encryption key present in the bootstrap.conf file at {}", bootstrapProperties.getConfigFilePath());
+            return "";
+        });
+    }
+
+    /**
+     * Returns the file for bootstrap.conf.
+     *
+     * @param bootstrapPath the path to the bootstrap file (defaults to $APPLICATION_HOME/conf/bootstrap.conf
+     *                     if null)
+     * @return the {@code $APPLICATION_HOME/conf/bootstrap.conf} file
+     * @throws IOException if the directory containing the file is not readable
+     */
+    private File getBootstrapFile(final String bootstrapPath) throws IOException {
+        final File expectedBootstrapFile;
+        if (bootstrapPath == null) {
+            // Guess at location of bootstrap.conf file from nifi.properties file
+            final String defaultApplicationPropertiesFilePath = getDefaultApplicationPropertiesFilePath();
+            final File propertiesFile = new File(defaultApplicationPropertiesFilePath);
+            final File confDir = new File(propertiesFile.getParent());
+            if (confDir.exists() && confDir.canRead()) {
+                expectedBootstrapFile = new File(confDir, BOOTSTRAP_CONF);
+            } else {
+                logger.error("Cannot read from bootstrap.conf file at {} -- conf/ directory is missing or permissions are incorrect", confDir.getAbsolutePath());
+                throw new IOException("Cannot read from bootstrap.conf");
+            }
+        } else {
+            expectedBootstrapFile = new File(bootstrapPath);
+        }
+
+        if (expectedBootstrapFile.exists() && expectedBootstrapFile.canRead()) {
+            return expectedBootstrapFile;
+        } else {
+            logger.error("Cannot read from bootstrap.conf file at {} -- file is missing or permissions are incorrect", expectedBootstrapFile.getAbsolutePath());
+            throw new IOException("Cannot read from bootstrap.conf");
+        }
+    }
+
+    /**
+     * Returns the default file path to {@code $APPLICATION_HOME/conf/$APPLICATION.properties}. If the system
+     * property provided by {@code AbstractBootstrapPropertiesLoader#getApplicationPropertiesFilePathSystemProperty()}
+     * is not set, it will be set to the relative path provided by
+     * {@code AbstractBootstrapPropertiesLoader#getRelativeApplicationPropertiesFilePath()}.
+     *
+     * @return the path to the application properties file
+     */
+    public String getDefaultApplicationPropertiesFilePath() {
+        final String systemPropertyName = getApplicationPropertiesFilePathSystemProperty();
+        final String defaultRelativePath = String.format(RELATIVE_APPLICATION_PROPERTIES_PATTERN, getApplicationPropertiesFilename());
+
+        String systemPath = System.getProperty(systemPropertyName);
+
+        if (systemPath == null || systemPath.trim().isEmpty()) {
+            logger.warn("The system property {} is not set, so it is being set to '{}'", systemPropertyName, defaultRelativePath);
+            System.setProperty(systemPropertyName, defaultRelativePath);
+            systemPath = defaultRelativePath;
+        }
+
+        logger.info("Determined default application properties path to be '{}'", systemPath);
+        return systemPath;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ApplicationProperties.java
similarity index 65%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
copy to nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ApplicationProperties.java
index c800b3a..16bbee1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
+++ b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ApplicationProperties.java
@@ -16,8 +16,18 @@
  */
 package org.apache.nifi.properties;
 
-public interface SensitivePropertyProviderFactory {
+import java.util.Map;
+import java.util.Properties;
 
-    SensitivePropertyProvider getProvider();
+/**
+ * A tagging class that represents the main configuration properties for an application (e.g. NiFi or NiFi Registry).
+ */
+public class ApplicationProperties extends StandardReadableProperties {
+    public ApplicationProperties(Properties properties) {
+        super(properties);
+    }
 
+    public ApplicationProperties(Map<String, String> properties) {
+        super(properties);
+    }
 }
diff --git a/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/BootstrapProperties.java b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/BootstrapProperties.java
new file mode 100644
index 0000000..4713e27
--- /dev/null
+++ b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/BootstrapProperties.java
@@ -0,0 +1,86 @@
+/*
+ * 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 java.nio.file.Path;
+import java.util.Enumeration;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Properties;
+
+/**
+ * Properties representing bootstrap.conf.
+ */
+public class BootstrapProperties extends StandardReadableProperties {
+    private static final String PROPERTY_KEY_FORMAT = "%s.%s";
+    private static final String BOOTSTRAP_SENSITIVE_KEY = "bootstrap.sensitive.key";
+
+    private final String propertyPrefix;
+    private final Path configFilePath;
+
+    public BootstrapProperties(final String propertyPrefix, final Properties properties, final Path configFilePath) {
+        super(new Properties());
+
+        Objects.requireNonNull(properties, "Properties are required");
+        this.propertyPrefix = Objects.requireNonNull(propertyPrefix, "Property prefix is required");
+        this.configFilePath = configFilePath;
+
+        this.filterProperties(properties);
+
+    }
+
+    /**
+     * Returns the path to the bootstrap config file.
+     * @return The path to the file
+     */
+    public Path getConfigFilePath() {
+        return configFilePath;
+    }
+
+    /**
+     * Includes only the properties starting with the propertyPrefix.
+     * @param properties Unfiltered properties
+     */
+    private void filterProperties(final Properties properties) {
+        getRawProperties().clear();
+        final Properties filteredProperties = new Properties();
+        for(final Enumeration<Object> e = properties.keys() ; e.hasMoreElements(); ) {
+            final String key = e.nextElement().toString();
+            if (key.startsWith(propertyPrefix)) {
+                filteredProperties.put(key, properties.getProperty(key));
+            }
+        }
+        getRawProperties().putAll(filteredProperties);
+    }
+
+    private String getPropertyKey(final String subKey) {
+        return String.format(PROPERTY_KEY_FORMAT, propertyPrefix, subKey);
+    }
+
+    /**
+     * Returns the bootstrap sensitive key.
+     * @return The bootstrap sensitive key
+     */
+    public Optional<String> getBootstrapSensitiveKey() {
+        return Optional.ofNullable(getProperty(getPropertyKey(BOOTSTRAP_SENSITIVE_KEY)));
+    }
+
+    @Override
+    public String toString() {
+        return String.format("Bootstrap properties [%s] with prefix [%s]", configFilePath, propertyPrefix);
+    }
+}
diff --git a/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ProtectedProperties.java b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ProtectedProperties.java
new file mode 100644
index 0000000..f04b1a8
--- /dev/null
+++ b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ProtectedProperties.java
@@ -0,0 +1,59 @@
+/*
+ * 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 java.util.List;
+import java.util.Properties;
+
+/**
+ * Represents a protected set of ApplicationProperties, with methods regarding which sensitive properties
+ * are protected.
+ * @param <T> The ApplicationProperties type
+ */
+public interface ProtectedProperties<T extends ApplicationProperties> {
+
+    /**
+     * Additional sensitive properties keys
+     * @return Additional sensitive properties keys
+     */
+    String getAdditionalSensitivePropertiesKeys();
+
+    /**
+     * Returns the name of the property that specifies the additional sensitive properties keys
+     * @return Name of additional sensitive properties keys
+     */
+    String getAdditionalSensitivePropertiesKeysName();
+
+    /**
+     * Additional sensitive properties keys
+     * @return Additional sensitive properties keys
+     */
+    List<String> getDefaultSensitiveProperties();
+
+    /**
+     * Returns the application properties.
+     * @return The application properties
+     */
+    T getApplicationProperties();
+
+    /**
+     * Create a new ApplicationProperties object of the generic type.
+     * @param rawProperties Plain old properties
+     * @return The ApplicationProperties
+     */
+    T createApplicationProperties(Properties rawProperties);
+}
diff --git a/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ReadableProperties.java b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ReadableProperties.java
new file mode 100644
index 0000000..1dd3285
--- /dev/null
+++ b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/ReadableProperties.java
@@ -0,0 +1,50 @@
+/*
+ * 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 java.util.Set;
+
+/**
+ * A base interface for providing a readable set of properties.
+ */
+public interface ReadableProperties {
+
+    /**
+     * 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
+     */
+    String getProperty(String key);
+
+    /**
+     * Retrieves the property value for the given property key.
+     *
+     * @param key the key of property value to lookup
+     * @param defaultValue The default value to use if the property does not exist
+     * @return value of property at given key or null if not found
+     */
+    String getProperty(String key, String defaultValue);
+
+    /**
+     * Retrieves all known property keys.
+     *
+     * @return all known property keys
+     */
+    Set<String> getPropertyKeys();
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/StandardNiFiProperties.java b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/StandardReadableProperties.java
similarity index 55%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/StandardNiFiProperties.java
rename to nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/StandardReadableProperties.java
index b7561ed..d23b677 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/StandardNiFiProperties.java
+++ b/nifi-commons/nifi-property-utils/src/main/java/org/apache/nifi/properties/StandardReadableProperties.java
@@ -18,42 +18,39 @@ package org.apache.nifi.properties;
 
 import java.util.Enumeration;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
-import org.apache.nifi.util.NiFiProperties;
 
-public class StandardNiFiProperties extends NiFiProperties {
+/**
+ * A Properties-backed implementation of ReadableProperties.
+ */
+public class StandardReadableProperties implements ReadableProperties {
 
-    private Properties rawProperties = new Properties();
+    private final Properties rawProperties = new Properties();
 
-    public StandardNiFiProperties() {
-        this(null);
+    public StandardReadableProperties(final Properties properties) {
+        rawProperties.putAll(properties);
     }
 
-    public StandardNiFiProperties(Properties props) {
-        this.rawProperties = props == null ? new Properties() : props;
+    public StandardReadableProperties(final Map<String, String> properties) {
+        rawProperties.putAll(properties);
     }
 
-    /**
-     * 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) {
+    public String getProperty(final String key) {
         return rawProperties.getProperty(key);
     }
 
-    /**
-     * Retrieves all known property keys.
-     *
-     * @return all known property keys
-     */
+    @Override
+    public String getProperty(final String key, String defaultValue) {
+        return rawProperties.getProperty(key, defaultValue);
+    }
+
     @Override
     public Set<String> getPropertyKeys() {
         Set<String> propertyNames = new HashSet<>();
-        Enumeration e = getRawProperties().propertyNames();
+        Enumeration e = rawProperties.propertyNames();
         for (; e.hasMoreElements(); ){
             propertyNames.add((String) e.nextElement());
         }
@@ -61,21 +58,15 @@ public class StandardNiFiProperties extends NiFiProperties {
         return propertyNames;
     }
 
-    Properties getRawProperties() {
-        if (this.rawProperties == null) {
-            this.rawProperties = new Properties();
-        }
-
-        return this.rawProperties;
+    protected Properties getRawProperties() {
+        return rawProperties;
     }
 
-    @Override
+    /**
+     * Returns the size of the properties.
+     * @return The size of the properties (number of keys)
+     */
     public int size() {
-        return getRawProperties().size();
-    }
-
-    @Override
-    public String toString() {
-        return "StandardNiFiProperties instance with " + size() + " properties";
+        return rawProperties.size();
     }
 }
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/kms/CryptoUtils.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/kms/CryptoUtils.java
index 6aa7fdb..c0b60c8 100644
--- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/kms/CryptoUtils.java
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/kms/CryptoUtils.java
@@ -16,6 +16,21 @@
  */
 package org.apache.nifi.security.kms;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.security.repository.config.RepositoryEncryptionConfiguration;
+import org.apache.nifi.security.util.EncryptionMethod;
+import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider;
+import org.apache.nifi.util.NiFiBootstrapUtils;
+import org.bouncycastle.util.encoders.DecoderException;
+import org.bouncycastle.util.encoders.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
@@ -24,8 +39,6 @@ import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Paths;
 import java.security.KeyManagementException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -34,22 +47,7 @@ import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.regex.Pattern;
-import java.util.stream.Stream;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.security.repository.config.RepositoryEncryptionConfiguration;
-import org.apache.nifi.security.util.EncryptionMethod;
-import org.apache.nifi.security.util.crypto.AESKeyedCipherProvider;
-import org.apache.nifi.util.NiFiProperties;
-import org.bouncycastle.util.encoders.Hex;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 public class CryptoUtils {
     private static final Logger logger = LoggerFactory.getLogger(CryptoUtils.class);
@@ -60,9 +58,6 @@ public class CryptoUtils {
     public static final String LEGACY_SKP_FQCN = "org.apache.nifi.provenance.StaticKeyProvider";
     public static final String LEGACY_FBKP_FQCN = "org.apache.nifi.provenance.FileBasedKeyProvider";
 
-    private static final String RELATIVE_NIFI_PROPS_PATH = "conf/nifi.properties";
-    private static final String BOOTSTRAP_KEY_PREFIX = "nifi.bootstrap.sensitive.key=";
-
     // TODO: Enforce even length
     private static final Pattern HEX_PATTERN = Pattern.compile("(?i)^[0-9a-f]+$");
 
@@ -309,88 +304,15 @@ public class CryptoUtils {
     public static SecretKey getRootKey() throws KeyManagementException {
         try {
             // Get the root encryption key from bootstrap.conf
-            String rootKeyHex = extractKeyFromBootstrapFile();
+            String rootKeyHex = NiFiBootstrapUtils.extractKeyFromBootstrapFile();
             return new SecretKeySpec(Hex.decode(rootKeyHex), "AES");
-        } catch (IOException e) {
+        } catch (IOException | DecoderException e) {
             logger.error("Encountered an error: ", e);
             throw new KeyManagementException(e);
         }
     }
 
     /**
-     * Returns the key (if any) used to encrypt sensitive properties, extracted from {@code $NIFI_HOME/conf/bootstrap.conf}.
-     *
-     * @return the key in hexadecimal format
-     * @throws IOException if the file is not readable
-     */
-    public static String extractKeyFromBootstrapFile() throws IOException {
-        return extractKeyFromBootstrapFile("");
-    }
-
-    /**
-     * Returns the key (if any) used to encrypt sensitive properties, extracted from {@code $NIFI_HOME/conf/bootstrap.conf}.
-     *
-     * @param bootstrapPath the path to the bootstrap file
-     * @return the key in hexadecimal format
-     * @throws IOException if the file is not readable
-     */
-    public static String extractKeyFromBootstrapFile(String bootstrapPath) throws IOException {
-        File expectedBootstrapFile;
-        if (StringUtils.isBlank(bootstrapPath)) {
-            // Guess at location of bootstrap.conf file from nifi.properties file
-            String defaultNiFiPropertiesPath = getDefaultFilePath();
-            File propertiesFile = new File(defaultNiFiPropertiesPath);
-            File confDir = new File(propertiesFile.getParent());
-            if (confDir.exists() && confDir.canRead()) {
-                expectedBootstrapFile = new File(confDir, "bootstrap.conf");
-            } else {
-                logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key -- conf/ directory is missing or permissions are incorrect", confDir.getAbsolutePath());
-                throw new IOException("Cannot read from bootstrap.conf");
-            }
-        } else {
-            expectedBootstrapFile = new File(bootstrapPath);
-        }
-
-        if (expectedBootstrapFile.exists() && expectedBootstrapFile.canRead()) {
-            try (Stream<String> stream = Files.lines(Paths.get(expectedBootstrapFile.getAbsolutePath()))) {
-                Optional<String> keyLine = stream.filter(l -> l.startsWith(BOOTSTRAP_KEY_PREFIX)).findFirst();
-                if (keyLine.isPresent()) {
-                    return keyLine.get().split("=", 2)[1];
-                } else {
-                    logger.warn("No encryption key present in the bootstrap.conf file at {}", expectedBootstrapFile.getAbsolutePath());
-                    return "";
-                }
-            } catch (IOException e) {
-                logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key", expectedBootstrapFile.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", expectedBootstrapFile.getAbsolutePath());
-            throw new IOException("Cannot read from bootstrap.conf");
-        }
-    }
-
-    /**
-     * Returns the default file path to {@code $NIFI_HOME/conf/nifi.properties}. If the system
-     * property {@code nifi.properties.file.path} is not set, it will be set to the relative
-     * path {@code conf/nifi.properties}.
-     *
-     * @return the path to the nifi.properties file
-     */
-    public static String getDefaultFilePath() {
-        String systemPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH);
-
-        if (systemPath == null || systemPath.trim().isEmpty()) {
-            logger.warn("The system variable {} is not set, so it is being set to '{}'", NiFiProperties.PROPERTIES_FILE_PATH, RELATIVE_NIFI_PROPS_PATH);
-            System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, RELATIVE_NIFI_PROPS_PATH);
-            systemPath = RELATIVE_NIFI_PROPS_PATH;
-        }
-
-        logger.info("Determined default nifi.properties path to be '{}'", systemPath);
-        return systemPath;
-    }
-
-    /**
      * Returns true if the two parameters are equal. This method is null-safe and evaluates the
      * equality in constant-time rather than "short-circuiting" on the first inequality. This
      * prevents timing attacks (side channel attacks) when comparing passwords or hash values.
diff --git a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java
index 4b731d4..c627e5c 100644
--- a/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java
+++ b/nifi-commons/nifi-security-utils/src/main/java/org/apache/nifi/security/util/StandardTlsConfiguration.java
@@ -174,42 +174,24 @@ public class StandardTlsConfiguration implements TlsConfiguration {
     // Static factory method from NiFiProperties
 
     /**
-     * Returns a {@link org.apache.nifi.security.util.TlsConfiguration} instantiated from the relevant {@link NiFiProperties} properties.
+     * Returns a {@link org.apache.nifi.security.util.TlsConfiguration} instantiated from the relevant NiFi properties.
      *
      * @param niFiProperties the NiFi properties
      * @return a populated TlsConfiguration container object
      */
-    public static TlsConfiguration fromNiFiProperties(NiFiProperties niFiProperties) {
-        Objects.requireNonNull("The NiFi properties cannot be null");
-
-        String keystorePath = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE);
-        String keystorePassword = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_PASSWD);
-        String keyPassword = niFiProperties.getProperty(NiFiProperties.SECURITY_KEY_PASSWD);
-        String keystoreType = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE_TYPE);
-        String truststorePath = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE);
-        String truststorePassword = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD);
-        String truststoreType = niFiProperties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE);
-        String protocol = TLS_PROTOCOL_VERSION;
-
-        final StandardTlsConfiguration tlsConfiguration = new StandardTlsConfiguration(keystorePath, keystorePassword, keyPassword,
-                keystoreType, truststorePath, truststorePassword,
-                truststoreType, protocol);
-        if (logger.isDebugEnabled()) {
-            logger.debug("Instantiating TlsConfiguration from NiFi properties: {}, {}, {}, {}, {}, {}, {}, {}",
-                    keystorePath, tlsConfiguration.getKeystorePasswordForLogging(), tlsConfiguration.getKeyPasswordForLogging(), keystoreType,
-                    truststorePath, tlsConfiguration.getTruststorePasswordForLogging(), truststoreType, protocol);
-        }
-
-        return tlsConfiguration;
+    public static TlsConfiguration fromNiFiProperties(final NiFiProperties niFiProperties) {
+        final Properties properties = new Properties();
+        niFiProperties.getPropertyKeys().forEach(key -> properties.setProperty(key, niFiProperties.getProperty(key)));
+        return fromNiFiProperties(properties);
     }
 
     /**
-     * Returns a {@link org.apache.nifi.security.util.TlsConfiguration} instantiated from the relevant {@link NiFiProperties} properties.
+     * Returns a {@link org.apache.nifi.security.util.TlsConfiguration} instantiated from the relevant NiFi properties.
      *
      * @param niFiProperties the NiFi properties, as a simple java.util.Properties object
      * @return a populated TlsConfiguration container object
      */
-    public static TlsConfiguration fromNiFiProperties(Properties niFiProperties) {
+    public static TlsConfiguration fromNiFiProperties(final Properties niFiProperties) {
         Objects.requireNonNull("The NiFi properties cannot be null");
 
         String keystorePath = niFiProperties.getProperty(NiFiProperties.SECURITY_KEYSTORE);
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml b/nifi-commons/nifi-sensitive-property-provider/pom.xml
similarity index 61%
copy from nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml
copy to nifi-commons/nifi-sensitive-property-provider/pom.xml
index 6458aec..bdfa9ba 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml
+++ b/nifi-commons/nifi-sensitive-property-provider/pom.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0"?>
 <!--
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
@@ -16,61 +16,55 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <parent>
-        <groupId>org.apache.nifi.registry</groupId>
-        <artifactId>nifi-registry-core</artifactId>
+        <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-commons</artifactId>
         <version>1.14.0-SNAPSHOT</version>
     </parent>
-    <artifactId>nifi-registry-properties</artifactId>
-    <packaging>jar</packaging>
+    <artifactId>nifi-sensitive-property-provider</artifactId>
 
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-properties</artifactId>
+            <version>1.14.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.12.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-security-utils</artifactId>
+            <version>1.14.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
     <build>
+        <!-- Required to run Groovy tests without any Java tests -->
         <plugins>
             <plugin>
-                <groupId>org.codehaus.gmavenplus</groupId>
-                <artifactId>gmavenplus-plugin</artifactId>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
                 <version>1.5</version>
                 <executions>
                     <execution>
+                        <id>add-test-source</id>
+                        <phase>generate-test-sources</phase>
                         <goals>
-                            <goal>addTestSources</goal>
-                            <goal>testCompile</goal>
+                            <goal>add-test-source</goal>
                         </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/test/groovy</source>
+                            </sources>
+                        </configuration>
                     </execution>
                 </executions>
             </plugin>
         </plugins>
     </build>
-
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.commons</groupId>
-            <artifactId>commons-lang3</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcprov-jdk15on</artifactId>
-            <version>${org.bouncycastle.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.codehaus.groovy</groupId>
-            <artifactId>groovy-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>cglib</groupId>
-            <artifactId>cglib-nodep</artifactId>
-            <version>2.2.2</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
-            <version>${org.slf4j.version}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-        </dependency>
-    </dependencies>
 </project>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
similarity index 78%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
rename to nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
index 062e352..3999a3a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
@@ -16,6 +16,21 @@
  */
 package org.apache.nifi.properties;
 
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.util.encoders.Base64;
+import org.bouncycastle.util.encoders.DecoderException;
+import org.bouncycastle.util.encoders.EncoderException;
+import org.bouncycastle.util.encoders.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
 import java.nio.charset.StandardCharsets;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
@@ -25,48 +40,51 @@ import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-import org.apache.commons.lang3.StringUtils;
-import org.bouncycastle.util.encoders.Base64;
-import org.bouncycastle.util.encoders.DecoderException;
-import org.bouncycastle.util.encoders.EncoderException;
-import org.bouncycastle.util.encoders.Hex;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
+public class AESSensitivePropertyProvider extends AbstractSensitivePropertyProvider {
     private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProvider.class);
 
     private static final String IMPLEMENTATION_NAME = "AES Sensitive Property Provider";
-    private static final String IMPLEMENTATION_KEY = "aes/gcm/";
     private static final String ALGORITHM = "AES/GCM/NoPadding";
     private static final String PROVIDER = "BC";
     private static final String DELIMITER = "||"; // "|" is not a valid Base64 character, so ensured not to be present in cipher text
     private static final int IV_LENGTH = 12;
     private static final int MIN_CIPHER_TEXT_LENGTH = IV_LENGTH * 4 / 3 + DELIMITER.length() + 1;
 
-    private Cipher cipher;
+    private final Cipher cipher;
     private final SecretKey key;
+    private final int keySize;
+
+    AESSensitivePropertyProvider(final byte[] keyHex) {
+        this(keyHex == null ? "" : Hex.toHexString(keyHex));
+    }
 
-    public AESSensitivePropertyProvider(String keyHex) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
-        byte[] key = validateKey(keyHex);
+    AESSensitivePropertyProvider(final String keyHex) {
+        super(null);
+
+        byte[] keyBytes = validateKey(keyHex);
 
         try {
-            cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
+            this.cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
             // Only store the key if the cipher was initialized successfully
-            this.key = new SecretKeySpec(key, "AES");
+            this.key = new SecretKeySpec(keyBytes, "AES");
+            this.keySize = getKeySize(Hex.toHexString(this.key.getEncoded()));
         } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
             logger.error("Encountered an error initializing the {}: {}", IMPLEMENTATION_NAME, e.getMessage());
             throw new SensitivePropertyProtectionException("Error initializing the protection cipher", e);
         }
     }
 
+    @Override
+    protected PropertyProtectionScheme getProtectionScheme() {
+        return PropertyProtectionScheme.AES_GCM;
+    }
+
+    @Override
+    protected boolean isSupported(final BootstrapProperties bootstrapProperties) {
+        return true; // AES protection is always supported
+    }
+
     private byte[] validateKey(String keyHex) {
         if (keyHex == null || StringUtils.isBlank(keyHex)) {
             throw new SensitivePropertyProtectionException("The key cannot be empty");
@@ -84,10 +102,6 @@ public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
         return key;
     }
 
-    public AESSensitivePropertyProvider(byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
-        this(key == null ? "" : Hex.toHexString(key));
-    }
-
     private static String formatHexKey(String input) {
         if (input == null || StringUtils.isBlank(input)) {
             return "";
@@ -122,32 +136,18 @@ public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
     }
 
     /**
-     * Returns the name of the underlying implementation.
-     *
-     * @return the name of this sensitive property provider
-     */
-    @Override
-    public String getName() {
-        return IMPLEMENTATION_NAME;
-    }
-
-    /**
      * 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 IMPLEMENTATION_KEY + getKeySize(Hex.toHexString(key.getEncoded()));
+        return getProtectionScheme().getIdentifier(String.valueOf(keySize));
     }
 
-    private int getKeySize(String key) {
-        if (StringUtils.isBlank(key)) {
-            return 0;
-        } else {
-            // A key in hexadecimal format has one char per nibble (4 bits)
-            return formatHexKey(key).length() * 4;
-        }
+    private static int getKeySize(final String key) {
+        // A key in hexadecimal format has one char per nibble (4 bits)
+        return StringUtils.isBlank(key) ? 0 : formatHexKey(key).length() * 4;
     }
 
     /**
@@ -158,8 +158,8 @@ public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
      * @throws SensitivePropertyProtectionException if there is an exception encrypting the value
      */
     @Override
-    public String protect(String unprotectedValue) throws SensitivePropertyProtectionException {
-        if (unprotectedValue == null || unprotectedValue.trim().length() == 0) {
+    public String protect(final String unprotectedValue) throws SensitivePropertyProtectionException {
+        if (StringUtils.isBlank(unprotectedValue)) {
             throw new IllegalArgumentException("Cannot encrypt an empty value");
         }
 
@@ -185,7 +185,7 @@ public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
         }
     }
 
-    private String base64Encode(byte[] input) {
+    private String base64Encode(final byte[] input) {
         return Base64.toBase64String(input).replaceAll("=", "");
     }
 
@@ -195,7 +195,7 @@ public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
      * @return the IV
      */
     private byte[] generateIV() {
-        byte[] iv = new byte[IV_LENGTH];
+        final byte[] iv = new byte[IV_LENGTH];
         new SecureRandom().nextBytes(iv);
         return iv;
     }
@@ -208,7 +208,7 @@ public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
      * @throws SensitivePropertyProtectionException if there is an error decrypting the cipher text
      */
     @Override
-    public String unprotect(String protectedValue) throws SensitivePropertyProtectionException {
+    public String unprotect(final String protectedValue) throws SensitivePropertyProtectionException {
         if (protectedValue == null || protectedValue.trim().length() < MIN_CIPHER_TEXT_LENGTH) {
             throw new IllegalArgumentException("Cannot decrypt a cipher text shorter than " + MIN_CIPHER_TEXT_LENGTH + " chars");
         }
@@ -216,28 +216,27 @@ public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
         if (!protectedValue.contains(DELIMITER)) {
             throw new IllegalArgumentException("The cipher text does not contain the delimiter " + DELIMITER + " -- it should be of the form Base64(IV) || Base64(cipherText)");
         }
+        final String trimmedProtectedValue = protectedValue.trim();
 
-        protectedValue = protectedValue.trim();
-
-        final String IV_B64 = protectedValue.substring(0, protectedValue.indexOf(DELIMITER));
-        byte[] iv = Base64.decode(IV_B64);
+        final String armoredIV = trimmedProtectedValue.substring(0, trimmedProtectedValue.indexOf(DELIMITER));
+        final byte[] iv = Base64.decode(armoredIV);
         if (iv.length < IV_LENGTH) {
-            throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes");
+            throw new IllegalArgumentException(String.format("The IV (%s bytes) must be at least %s bytes", iv.length, IV_LENGTH));
         }
 
-        String CIPHERTEXT_B64 = protectedValue.substring(protectedValue.indexOf(DELIMITER) + 2);
+        String armoredCipherText = trimmedProtectedValue.substring(trimmedProtectedValue.indexOf(DELIMITER) + 2);
 
         // Restore the = padding if necessary to reconstitute the GCM MAC check
-        if (CIPHERTEXT_B64.length() % 4 != 0) {
-            final int paddedLength = CIPHERTEXT_B64.length() + 4 - (CIPHERTEXT_B64.length() % 4);
-            CIPHERTEXT_B64 = StringUtils.rightPad(CIPHERTEXT_B64, paddedLength, '=');
+        if (armoredCipherText.length() % 4 != 0) {
+            final int paddedLength = armoredCipherText.length() + 4 - (armoredCipherText.length() % 4);
+            armoredCipherText = StringUtils.rightPad(armoredCipherText, paddedLength, '=');
         }
 
         try {
-            byte[] cipherBytes = Base64.decode(CIPHERTEXT_B64);
+            final byte[] cipherBytes = Base64.decode(armoredCipherText);
 
             cipher.init(Cipher.DECRYPT_MODE, this.key, new IvParameterSpec(iv));
-            byte[] plainBytes = cipher.doFinal(cipherBytes);
+            final byte[] plainBytes = cipher.doFinal(cipherBytes);
             logger.debug(getName() + " decrypted a sensitive value successfully");
             return new String(plainBytes, StandardCharsets.UTF_8);
         } catch (BadPaddingException | IllegalBlockSizeException | DecoderException | InvalidAlgorithmParameterException | InvalidKeyException e) {
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AbstractSensitivePropertyProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AbstractSensitivePropertyProvider.java
new file mode 100644
index 0000000..b52fb73
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/AbstractSensitivePropertyProvider.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+public abstract class AbstractSensitivePropertyProvider implements SensitivePropertyProvider {
+    private final BootstrapProperties bootstrapProperties;
+
+    public AbstractSensitivePropertyProvider(final BootstrapProperties bootstrapProperties) {
+        this.bootstrapProperties = bootstrapProperties;
+    }
+
+    protected BootstrapProperties getBootstrapProperties() {
+        return bootstrapProperties;
+    }
+
+    /**
+     * Return the appropriate PropertyProtectionScheme for this provider.
+     * @return The PropertyProtectionScheme
+     */
+    protected abstract PropertyProtectionScheme getProtectionScheme();
+
+    /**
+     * Return true if this SensitivePropertyProvider is supported, given the provided
+     * Bootstrap properties.
+     * @param bootstrapProperties The Bootstrap properties
+     * @return True if this SensitivePropertyProvider is supported
+     */
+    protected abstract boolean isSupported(BootstrapProperties bootstrapProperties);
+
+    @Override
+    public String getName() {
+        return getProtectionScheme().getName();
+    }
+
+    /**
+     * Default implementation to return the protection scheme identifier, with no args to populate the identifier key.
+     * Concrete classes may choose to override this in order to fill in the identifier with specific args.
+     * @return The identifier key
+     */
+    @Override
+    public String getIdentifierKey() {
+        return getProtectionScheme().getIdentifier();
+    }
+
+    @Override
+    public boolean isSupported() {
+        return isSupported(bootstrapProperties);
+    }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/ApplicationPropertiesProtector.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/ApplicationPropertiesProtector.java
new file mode 100644
index 0000000..3f03a9f
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/ApplicationPropertiesProtector.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Class performing unprotection activities before returning a clean
+ * implementation of {@link ApplicationProperties}.
+ * This encapsulates the sensitive property access logic from external consumers
+ * of {@code ApplicationProperties}.
+ *
+ * @param <T> The type of protected application properties
+ * @param <U> The type of standard application properties that backs the protected application properties
+ */
+public class ApplicationPropertiesProtector<T extends ProtectedProperties<U>, U extends ApplicationProperties>
+        implements SensitivePropertyProtector<T, U> {
+    public static final String PROTECTED_KEY_SUFFIX = ".protected";
+
+    private static final Logger logger = LoggerFactory.getLogger(ApplicationPropertiesProtector.class);
+
+    private T protectedProperties;
+
+    private Map<String, SensitivePropertyProvider> localProviderCache = new HashMap<>();
+
+    /**
+     * Creates an instance containing the provided {@link ProtectedProperties}.
+     *
+     * @param protectedProperties the ProtectedProperties to contain
+     */
+    public ApplicationPropertiesProtector(final T protectedProperties) {
+        this.protectedProperties = protectedProperties;
+        logger.debug("Loaded {} properties (including {} protection schemes) into {}", getPropertyKeysIncludingProtectionSchemes().size(),
+                getProtectedPropertyKeys().size(), this.getClass().getName());
+    }
+
+    /**
+     * Returns the sibling property key which specifies the protection scheme for this key.
+     * <p>
+     * Example:
+     * <p>
+     * nifi.sensitive.key=ABCXYZ
+     * nifi.sensitive.key.protected=aes/gcm/256
+     * <p>
+     * nifi.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(final String key) {
+        if (key == null || key.isEmpty()) {
+            throw new IllegalArgumentException("Cannot find protection key for null key");
+        }
+
+        return key + PROTECTED_KEY_SUFFIX;
+    }
+
+    /**
+     * Retrieves all known property keys.
+     *
+     * @return all known property keys
+     */
+    @Override
+    public Set<String> getPropertyKeys() {
+        Set<String> filteredKeys = getPropertyKeysIncludingProtectionSchemes();
+        filteredKeys.removeIf(p -> p.endsWith(PROTECTED_KEY_SUFFIX));
+        return filteredKeys;
+    }
+
+    @Override
+    public int size() {
+        return getPropertyKeys().size();
+    }
+
+    @Override
+    public Set<String> getPropertyKeysIncludingProtectionSchemes() {
+        return protectedProperties.getApplicationProperties().getPropertyKeys();
+    }
+
+    /**
+     * 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.property.1; nifi.property.2, nifi.property.3"
+     * @return a List containing the split and trimmed properties
+     */
+    private static List<String> splitMultipleProperties(final 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;
+        }
+    }
+
+    private String getProperty(final String key) {
+        return protectedProperties.getApplicationProperties().getProperty(key);
+    }
+
+    private String getAdditionalSensitivePropertiesKeys() {
+        return getProperty(protectedProperties.getAdditionalSensitivePropertiesKeysName());
+    }
+
+    private String getAdditionalSensitivePropertiesKeysName() {
+        return protectedProperties.getAdditionalSensitivePropertiesKeysName();
+    }
+
+    @Override
+    public List<String> getSensitivePropertyKeys() {
+        final String additionalPropertiesString = getAdditionalSensitivePropertiesKeys();
+        final String additionalPropertiesKeyName = protectedProperties.getAdditionalSensitivePropertiesKeysName();
+        if (additionalPropertiesString == null || additionalPropertiesString.trim().isEmpty()) {
+            return protectedProperties.getDefaultSensitiveProperties();
+        } 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(additionalPropertiesKeyName)) {
+                logger.warn("The key '{}' contains itself. This is poor practice and should be removed", additionalPropertiesKeyName);
+                additionalProperties.remove(additionalPropertiesKeyName);
+            }
+            additionalProperties.addAll(protectedProperties.getDefaultSensitiveProperties());
+            return additionalProperties;
+        }
+    }
+
+    @Override
+    public List<String> getPopulatedSensitivePropertyKeys() {
+        List<String> allSensitiveKeys = getSensitivePropertyKeys();
+        return allSensitiveKeys.stream().filter(k -> StringUtils.isNotBlank(getProperty(k))).collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean hasProtectedKeys() {
+        final List<String> sensitiveKeys = getSensitivePropertyKeys();
+        for (String k : sensitiveKeys) {
+            if (isPropertyProtected(k)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public Map<String, String> getProtectedPropertyKeys() {
+        final List<String> sensitiveKeys = getSensitivePropertyKeys();
+
+        final Map<String, String> traditionalProtectedProperties = new HashMap<>();
+        for (final String key : sensitiveKeys) {
+            final String protection = getProperty(getProtectionKey(key));
+            if (StringUtils.isNotBlank(protection) && StringUtils.isNotBlank(getProperty(key))) {
+                traditionalProtectedProperties.put(key, protection);
+            }
+        }
+
+        return traditionalProtectedProperties;
+    }
+
+    @Override
+    public Set<String> getProtectionSchemes() {
+        return new HashSet<>(getProtectedPropertyKeys().values());
+    }
+
+    @Override
+    public boolean isPropertySensitive(final String key) {
+        // If the explicit check for ADDITIONAL_SENSITIVE_PROPERTIES_KEY is not here, this will loop infinitely
+        return key != null && !key.equals(getAdditionalSensitivePropertiesKeysName()) && 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 ApplicationPropertiesProtector#getSensitivePropertyKeys()
+     */
+    @Override
+    public boolean isPropertyProtected(final String key) {
+        return key != null && isPropertySensitive(key) && !StringUtils.isBlank(getProperty(getProtectionKey(key)));
+    }
+
+    @Override
+    public U getUnprotectedProperties() throws SensitivePropertyProtectionException {
+        if (hasProtectedKeys()) {
+            logger.debug("Protected Properties [{}] Sensitive Properties [{}]",
+                    getProtectedPropertyKeys().size(),
+                    getSensitivePropertyKeys().size());
+
+            final Properties rawProperties = new Properties();
+
+            final Set<String> failedKeys = new HashSet<>();
+
+            for (final String key : getPropertyKeys()) {
+                /* Three kinds of keys
+                 * 1. protection schemes -- skip
+                 * 2. protected keys -- unprotect and copy
+                 * 3. normal keys -- copy over
+                 */
+                if (key.endsWith(PROTECTED_KEY_SUFFIX)) {
+                    // Do nothing
+                } else if (isPropertyProtected(key)) {
+                    try {
+                        rawProperties.setProperty(key, unprotectValue(key, getProperty(key)));
+                    } catch (final SensitivePropertyProtectionException e) {
+                        logger.warn("Failed to unprotect '{}'", key, e);
+                        failedKeys.add(key);
+                    }
+                } else {
+                    rawProperties.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());
+                }
+            }
+
+            final U unprotected = protectedProperties.createApplicationProperties(rawProperties);
+
+            return unprotected;
+        } else {
+            logger.debug("No protected properties");
+            return protectedProperties.getApplicationProperties();
+        }
+    }
+
+    @Override
+    public void addSensitivePropertyProvider(final SensitivePropertyProvider sensitivePropertyProvider) {
+        Objects.requireNonNull(sensitivePropertyProvider, "Cannot add null 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);
+    }
+
+    @Override
+    public String toString() {
+        final Set<String> providers = getSensitivePropertyProviders().keySet();
+        return new StringBuilder("ApplicationPropertiesProtector instance with ")
+                .append(size()).append(" properties (")
+                .append(getProtectedPropertyKeys().size())
+                .append(" protected) and ")
+                .append(providers.size())
+                .append(" sensitive property providers: ")
+                .append(StringUtils.join(providers, ", "))
+                .toString();
+    }
+
+    @Override
+    public Map<String, SensitivePropertyProvider> getSensitivePropertyProviders() {
+        if (localProviderCache == null) {
+            localProviderCache = new HashMap<>();
+        }
+
+        return localProviderCache;
+    }
+
+    private SensitivePropertyProvider getSensitivePropertyProvider(final String protectionScheme) {
+        if (isProviderAvailable(protectionScheme)) {
+            return getSensitivePropertyProviders().get(protectionScheme);
+        } else {
+            throw new SensitivePropertyProtectionException("No provider available for " + protectionScheme);
+        }
+    }
+
+    private boolean isProviderAvailable(final 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(final String key, final 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
+            if (!isProviderAvailable(protectionScheme)) {
+                throw new IllegalStateException(String.format("No provider available for " + key));
+            }
+
+            try {
+                final SensitivePropertyProvider sensitivePropertyProvider = getSensitivePropertyProvider(protectionScheme);
+                return sensitivePropertyProvider.unprotect(retrievedValue);
+            } catch (SensitivePropertyProtectionException e) {
+                logger.error("Error unprotecting value for " + key, e);
+                throw e;
+            } catch (IllegalArgumentException e) {
+                throw new SensitivePropertyProtectionException("Error unprotecting value for " + key, e);
+            }
+        }
+        return retrievedValue;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/MultipleSensitivePropertyProtectionException.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/MultipleSensitivePropertyProtectionException.java
similarity index 100%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/MultipleSensitivePropertyProtectionException.java
rename to nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/MultipleSensitivePropertyProtectionException.java
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
new file mode 100644
index 0000000..4017406
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/PropertyProtectionScheme.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A scheme for protecting sensitive properties.  Each scheme is intended to be backed by an implementation of
+ * SensitivePropertyProvider.
+ */
+public enum PropertyProtectionScheme {
+    AES_GCM("aes/gcm/(128|192|256)", "aes/gcm/%s", "AES Sensitive Property Provider", true);
+
+    PropertyProtectionScheme(final String identifierPattern, final String identifierFormat, final String name, final boolean requiresSecretKey) {
+        this.identifierPattern = identifierPattern;
+        this.identifierFormat = identifierFormat;
+        this.name = name;
+        this.requiresSecretKey = requiresSecretKey;
+    }
+
+    private final String identifierFormat;
+    private final String identifierPattern;
+    private final String name;
+    private final boolean requiresSecretKey;
+
+    /**
+     * Returns a the identifier of the PropertyProtectionScheme.
+     * @param args scheme-specific arguments used to fill in the formatted identifierPattern
+     * @return The identifier of the PropertyProtectionScheme
+     */
+    public String getIdentifier(final String... args) {
+        return String.format(identifierFormat, args);
+    }
+
+    /**
+     * Returns whether this scheme requires a secret key.
+     * @return True if this scheme requires a secret key
+     */
+    public boolean requiresSecretKey() {
+        return requiresSecretKey;
+    }
+
+    /**
+     * Returns the name of the PropertyProtectionScheme.
+     * @return The name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns the PropertyProtectionScheme matching the provided name.
+     * @param identifier The unique PropertyProtectionScheme identifier
+     * @return The matching PropertyProtectionScheme
+     * @throws IllegalArgumentException If the name was not recognized
+     */
+    public static PropertyProtectionScheme fromIdentifier(final String identifier) {
+        Objects.requireNonNull(identifier, "Identifier must be specified");
+        return Arrays.stream(PropertyProtectionScheme.values())
+                .filter(scheme -> identifier.matches(scheme.identifierPattern))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("Unrecognized protection scheme :" + identifier));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProtectionException.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProtectionException.java
similarity index 100%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProtectionException.java
rename to nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProtectionException.java
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProtector.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProtector.java
new file mode 100644
index 0000000..b78dd6d
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProtector.java
@@ -0,0 +1,146 @@
+/*
+ * 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 java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Encapsulates methods needed to protect application properties.
+ * @param <T> The ProtectedProperties type
+ * @param <U> The ApplicationProperties type
+ */
+public interface SensitivePropertyProtector<T extends ProtectedProperties<U>, U extends ApplicationProperties> {
+
+    /**
+     * Returns the number of properties, 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();
+
+    /**
+     * Retrieves all known property keys.
+     *
+     * @return all known property keys
+     */
+    Set<String> getPropertyKeys();
+
+    /**
+     * Returns the complete set of property keys, including any protection keys (i.e. 'x.y.z.protected').
+     *
+     * @return the set of property keys
+     */
+    Set<String> getPropertyKeysIncludingProtectionSchemes();
+
+    /**
+     * 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 the ApplicationProperties.
+     *
+     * @return the list of sensitive property keys
+     */
+    List<String> getSensitivePropertyKeys();
+
+    /**
+     * 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 the ApplicationProperties.
+     *
+     * @return the list of sensitive property keys
+     */
+    List<String> getPopulatedSensitivePropertyKeys();
+
+    /**
+     * Returns true if any sensitive keys are protected.
+     *
+     * @return true if any key is protected; false otherwise
+     */
+    boolean hasProtectedKeys();
+
+    /**
+     * Returns the unique set of all protection schemes currently in use for this instance.
+     *
+     * @return the set of protection schemes
+     */
+    Set<String> getProtectionSchemes();
+
+    /**
+     * 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
+     */
+    Map<String, String> getProtectedPropertyKeys();
+
+    /**
+     * Returns the local provider cache (null-safe) as a Map of protection schemes -> implementations.
+     *
+     * @return the map
+     */
+    Map<String, SensitivePropertyProvider> getSensitivePropertyProviders();
+
+    /**
+     * Returns true if the property identified by this key is considered sensitive in this instance of {@code ApplicationProperties}.
+     * Some properties are sensitive by default, while others can be specified by
+     * {@link ProtectedProperties#getAdditionalSensitivePropertiesKeys()}.
+     *
+     * @param key the key
+     * @return true if it is sensitive
+     * @see ApplicationPropertiesProtector#getSensitivePropertyKeys()
+     */
+    boolean isPropertySensitive(String key);
+
+    /**
+     * Returns the unprotected {@link ApplicationProperties} 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 ApplicationProperties instance with all raw values
+     * @throws SensitivePropertyProtectionException if there is a problem unprotecting one or more keys
+     */
+    boolean isPropertyProtected(String key);
+
+    /**
+     * Returns the unprotected ApplicationProperties.
+     * @return The unprotected properties
+     * @throws SensitivePropertyProtectionException if there is a problem unprotecting one or more keys
+     */
+    U getUnprotectedProperties() throws SensitivePropertyProtectionException;
+
+    /**
+     * 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);
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
similarity index 91%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
rename to nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
index b0c0be2..bb26ecf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
@@ -33,6 +33,13 @@ public interface SensitivePropertyProvider {
     String getIdentifierKey();
 
     /**
+     * Returns whether this SensitivePropertyProvider is supported with the current system
+     * configuration.
+     * @return Whether this SensitivePropertyProvider is supported
+     */
+    boolean isSupported();
+
+    /**
      * 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.
      *
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
similarity index 61%
copy from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
copy to nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
index c800b3a..834e708 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
@@ -16,8 +16,21 @@
  */
 package org.apache.nifi.properties;
 
+import java.util.Collection;
+
 public interface SensitivePropertyProviderFactory {
 
-    SensitivePropertyProvider getProvider();
+    /**
+     * Gives the appropriate SensitivePropertyProvider, given a protection scheme.
+     * @param protectionScheme The protection scheme to use
+     * @return The appropriate SensitivePropertyProvider
+     */
+    SensitivePropertyProvider getProvider(PropertyProtectionScheme protectionScheme);
+
+    /**
+     * Returns a collection of all supported sensitive property providers.
+     * @return The supported sensitive property providers
+     */
+    Collection<SensitivePropertyProvider> getSupportedSensitivePropertyProviders();
 
 }
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactoryAware.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactoryAware.java
new file mode 100644
index 0000000..88bf1d6
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactoryAware.java
@@ -0,0 +1,46 @@
+/*
+ * 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 java.util.function.Supplier;
+
+/**
+ * Provides a default SensitivePropertyProviderFactory to subclasses.
+ */
+public class SensitivePropertyProviderFactoryAware {
+
+    private SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
+
+    protected SensitivePropertyProviderFactory getSensitivePropertyProviderFactory() throws SensitivePropertyProtectionException {
+        if (sensitivePropertyProviderFactory == null) {
+            sensitivePropertyProviderFactory = StandardSensitivePropertyProviderFactory.withDefaults();
+        }
+        return sensitivePropertyProviderFactory;
+    }
+
+    /**
+     * Configures and sets the SensitivePropertyProviderFactory.
+     * @param keyHex An key in hex format, which some providers may use for encryption
+     * @param bootstrapPropertiesSupplier The bootstrap.conf properties supplier
+     * @return The configured SensitivePropertyProviderFactory
+     */
+    public SensitivePropertyProviderFactory configureSensitivePropertyProviderFactory(final String keyHex,
+                                                                                      final Supplier<BootstrapProperties> bootstrapPropertiesSupplier) {
+        sensitivePropertyProviderFactory = StandardSensitivePropertyProviderFactory.withKeyAndBootstrapSupplier(keyHex, bootstrapPropertiesSupplier);
+        return sensitivePropertyProviderFactory;
+    }
+}
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..ef59333
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/main/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactory.java
@@ -0,0 +1,121 @@
+/*
+ * 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.nifi.util.NiFiBootstrapUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class StandardSensitivePropertyProviderFactory implements SensitivePropertyProviderFactory {
+    private static final Logger logger = LoggerFactory.getLogger(StandardSensitivePropertyProviderFactory.class);
+
+    private final Optional<String> keyHex;
+    private final Supplier<BootstrapProperties> bootstrapPropertiesSupplier;
+    private final Map<PropertyProtectionScheme, SensitivePropertyProvider> providerMap;
+
+    /**
+     * Creates a StandardSensitivePropertyProviderFactory using the default bootstrap.conf location and
+     * the keyHex extracted from this bootstrap.conf.
+     */
+    public static SensitivePropertyProviderFactory withDefaults() {
+        return withKeyAndBootstrapSupplier(null, null);
+    }
+
+    /**
+     * Creates a StandardSensitivePropertyProviderFactory using only the provided secret key hex.  The default
+     * bootstrap.conf will be used for any providers that may require it, but the provided keyHex will be used instead
+     * of the one from the default bootstrap.conf.
+     * @param keyHex The secret key hex for encrypting properties
+     * @return A StandardSensitivePropertyProviderFactory
+     */
+    public static SensitivePropertyProviderFactory withKey(final String keyHex) {
+        return new StandardSensitivePropertyProviderFactory(keyHex, null);
+    }
+
+    /**
+     * Creates a new StandardSensitivePropertyProviderFactory using a separate keyHex and provided bootstrap.conf.
+     * The provided keyHex will be used instead of the one from the bootstrap.conf.
+     * @param keyHex The secret key hex for encrypting properties
+     * @param bootstrapPropertiesSupplier A supplier for the BootstrapProperties that represent bootstrap.conf.
+     *                                    If the supplier returns null, the default bootstrap.conf will be used instead.
+     * @return A StandardSensitivePropertyProviderFactory
+     */
+    public static SensitivePropertyProviderFactory withKeyAndBootstrapSupplier(final String keyHex,
+                                                                               final Supplier<BootstrapProperties> bootstrapPropertiesSupplier) {
+        return new StandardSensitivePropertyProviderFactory(keyHex, bootstrapPropertiesSupplier);
+    }
+
+    private StandardSensitivePropertyProviderFactory(final String keyHex, final Supplier<BootstrapProperties> bootstrapPropertiesSupplier) {
+        this.keyHex = Optional.ofNullable(keyHex);
+        this.bootstrapPropertiesSupplier = bootstrapPropertiesSupplier == null ? () -> null : bootstrapPropertiesSupplier;
+        this.providerMap = new HashMap<>();
+    }
+
+    private String getKeyHex() {
+        return keyHex.orElseGet(() -> getBootstrapProperties().getBootstrapSensitiveKey()
+                .orElseThrow(() -> new SensitivePropertyProtectionException("Could not read root key from bootstrap.conf")));
+    }
+
+    /**
+     * Returns the configured bootstrap properties, or the default bootstrap.conf properties if
+     * not provided.
+     * @return The bootstrap.conf properties
+     */
+    private BootstrapProperties getBootstrapProperties() {
+        return Optional.ofNullable(bootstrapPropertiesSupplier.get()).orElseGet(() -> {
+            try {
+                return NiFiBootstrapUtils.loadBootstrapProperties();
+            } catch (final IOException e) {
+                logger.error("Error extracting root key from bootstrap.conf for login identity provider decryption", e);
+                throw new SensitivePropertyProtectionException("Could not read root key from bootstrap.conf");
+            }
+        });
+    }
+
+    @Override
+    public SensitivePropertyProvider getProvider(final PropertyProtectionScheme protectionScheme) throws SensitivePropertyProtectionException {
+        Objects.requireNonNull(protectionScheme, "Protection scheme is required");
+        // Only look up the secret key, which can perform a disk read, if this provider actually requires one
+        final String keyHex = protectionScheme.requiresSecretKey() ? getKeyHex() : null;
+        switch (protectionScheme) {
+            case AES_GCM:
+                return providerMap.computeIfAbsent(protectionScheme, s -> new AESSensitivePropertyProvider(keyHex));
+            // Other providers may choose to pass getBootstrapProperties() into the constructor
+            default:
+                throw new SensitivePropertyProtectionException("Unsupported protection scheme " + protectionScheme);
+        }
+    }
+
+    @Override
+    public Collection<SensitivePropertyProvider> getSupportedSensitivePropertyProviders() {
+        return Arrays.stream(PropertyProtectionScheme.values())
+                .map(this::getProvider)
+                .filter(SensitivePropertyProvider::isSupported)
+                .collect(Collectors.toList());
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy b/nifi-commons/nifi-sensitive-property-provider/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy
similarity index 100%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy
rename to nifi-commons/nifi-sensitive-property-provider/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderTest.groovy
diff --git a/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactoryTest.java b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactoryTest.java
new file mode 100644
index 0000000..f36f259
--- /dev/null
+++ b/nifi-commons/nifi-sensitive-property-provider/src/test/java/org/apache/nifi/properties/StandardSensitivePropertyProviderFactoryTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.nifi.util.NiFiProperties;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.mockito.internal.util.io.IOUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.Security;
+import java.util.Properties;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class StandardSensitivePropertyProviderFactoryTest {
+
+    private static final String AES_GCM_128 = "aes/gcm/128";
+    private SensitivePropertyProviderFactory factory;
+
+    private static final String BOOTSTRAP_KEY_HEX = "0123456789ABCDEFFEDCBA9876543210";
+    private static final String AD_HOC_KEY_HEX = "123456789ABCDEFFEDCBA98765432101";
+
+    private static Path tempConfDir;
+    private static Path mockBootstrapConf;
+    private static Path mockNifiProperties;
+
+    private static NiFiProperties niFiProperties;
+
+    @BeforeClass
+    public static void initOnce() throws IOException {
+        Security.addProvider(new BouncyCastleProvider());
+        tempConfDir = Files.createTempDirectory("conf");
+        mockBootstrapConf = Files.createTempFile("bootstrap", ".conf").toAbsolutePath();
+
+        mockNifiProperties = Files.createTempFile("nifi", ".properties").toAbsolutePath();
+
+        mockBootstrapConf = Files.move(mockBootstrapConf, tempConfDir.resolve("bootstrap.conf"));
+        mockNifiProperties = Files.move(mockNifiProperties, tempConfDir.resolve("nifi.properties"));
+
+        IOUtil.writeText("nifi.bootstrap.sensitive.key=" + BOOTSTRAP_KEY_HEX, mockBootstrapConf.toFile());
+        System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, mockNifiProperties.toString());
+
+        niFiProperties = new NiFiProperties();
+    }
+
+    @AfterClass
+    public static void tearDownOnce() throws IOException {
+        Files.deleteIfExists(mockBootstrapConf);
+        Files.deleteIfExists(mockNifiProperties);
+        Files.deleteIfExists(tempConfDir);
+        System.clearProperty(NiFiProperties.PROPERTIES_FILE_PATH);
+    }
+
+    /**
+     * Configures the factory using the default bootstrap location.
+     */
+    private void configureDefaultFactory() {
+        factory = StandardSensitivePropertyProviderFactory.withDefaults();
+    }
+
+    /**
+     * Configures the factory using an ad hoc key hex.
+     */
+    private void configureAdHocKeyFactory() {
+        factory = StandardSensitivePropertyProviderFactory.withKey(AD_HOC_KEY_HEX);
+    }
+
+    /**
+     * Configures the factory using an ad hoc key hex and bootstrap.conf properties.  The key should override
+     * the on in the bootstrap.conf.
+     */
+    private void configureAdHocKeyAndPropertiesFactory() throws IOException {
+        factory = StandardSensitivePropertyProviderFactory.withKeyAndBootstrapSupplier(AD_HOC_KEY_HEX, mockBootstrapProperties());
+    }
+
+    private Supplier<BootstrapProperties> mockBootstrapProperties() throws IOException {
+        final Properties bootstrapProperties = new Properties();
+        try (final InputStream inputStream = Files.newInputStream(mockBootstrapConf)) {
+            bootstrapProperties.load(inputStream);
+            return () -> new BootstrapProperties("nifi", bootstrapProperties, mockBootstrapConf);
+        }
+    }
+
+    @Test
+    public void testAES_GCM() throws IOException {
+        configureDefaultFactory();
+
+        final SensitivePropertyProvider spp = factory.getProvider(PropertyProtectionScheme.AES_GCM);
+        assertNotNull(spp);
+        assertTrue(spp.isSupported());
+
+        final String cleartext = "test";
+        assertEquals(cleartext, spp.unprotect(spp.protect(cleartext)));
+        assertNotEquals(cleartext, spp.protect(cleartext));
+        assertEquals(AES_GCM_128, spp.getIdentifierKey());
+
+        // Key is now different
+        configureAdHocKeyFactory();
+        final SensitivePropertyProvider sppAdHocKey = factory.getProvider(PropertyProtectionScheme.AES_GCM);
+        assertNotNull(sppAdHocKey);
+        assertTrue(sppAdHocKey.isSupported());
+        assertEquals(AES_GCM_128, sppAdHocKey.getIdentifierKey());
+
+        assertNotEquals(spp.protect(cleartext), sppAdHocKey.protect(cleartext));
+        assertEquals(cleartext, sppAdHocKey.unprotect(sppAdHocKey.protect(cleartext)));
+
+        // This should use the same keyHex as the second one
+        configureAdHocKeyAndPropertiesFactory();
+        final SensitivePropertyProvider sppKeyProperties = factory.getProvider(PropertyProtectionScheme.AES_GCM);
+        assertNotNull(sppKeyProperties);
+        assertTrue(sppKeyProperties.isSupported());
+        assertEquals(AES_GCM_128, sppKeyProperties.getIdentifierKey());
+
+        assertEquals(cleartext, sppKeyProperties.unprotect(sppKeyProperties.protect(cleartext)));
+    }
+}
diff --git a/nifi-commons/pom.xml b/nifi-commons/pom.xml
index 43bcb2f..368a67a 100644
--- a/nifi-commons/pom.xml
+++ b/nifi-commons/pom.xml
@@ -35,7 +35,9 @@
         <module>nifi-metrics</module>
         <module>nifi-parameter</module>
         <module>nifi-property-encryptor</module>
+        <module>nifi-property-utils</module>
         <module>nifi-properties</module>
+        <module>nifi-sensitive-property-provider</module>
         <module>nifi-record</module>
         <module>nifi-record-path</module>
         <module>nifi-rocksdb-utils</module>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java
index 5006ca5..0056ef9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/main/java/org/apache/nifi/authorization/AuthorizerFactoryBean.java
@@ -16,29 +16,6 @@
  */
 package org.apache.nifi.authorization;
 
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import javax.xml.XMLConstants;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.XMLStreamReader;
-import javax.xml.transform.stream.StreamSource;
-import javax.xml.validation.Schema;
-import javax.xml.validation.SchemaFactory;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.annotation.AuthorizerContext;
 import org.apache.nifi.authorization.exception.AuthorizationAccessException;
@@ -48,11 +25,9 @@ import org.apache.nifi.authorization.generated.Authorizers;
 import org.apache.nifi.authorization.generated.Property;
 import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.nar.ExtensionManager;
-import org.apache.nifi.properties.AESSensitivePropertyProviderFactory;
+import org.apache.nifi.properties.PropertyProtectionScheme;
 import org.apache.nifi.properties.SensitivePropertyProtectionException;
-import org.apache.nifi.properties.SensitivePropertyProvider;
-import org.apache.nifi.properties.SensitivePropertyProviderFactory;
-import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.properties.SensitivePropertyProviderFactoryAware;
 import org.apache.nifi.security.xml.XmlUtils;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.util.file.classloader.ClassLoaderUtils;
@@ -62,18 +37,41 @@ import org.springframework.beans.factory.DisposableBean;
 import org.springframework.beans.factory.FactoryBean;
 import org.xml.sax.SAXException;
 
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
 /**
  * Factory bean for loading the configured authorizer.
  */
-public class AuthorizerFactoryBean implements FactoryBean, DisposableBean, UserGroupProviderLookup, AccessPolicyProviderLookup, AuthorizerLookup {
+public class AuthorizerFactoryBean extends SensitivePropertyProviderFactoryAware
+        implements FactoryBean, DisposableBean, UserGroupProviderLookup, AccessPolicyProviderLookup, AuthorizerLookup {
 
     private static final Logger logger = LoggerFactory.getLogger(AuthorizerFactoryBean.class);
     private static final String AUTHORIZERS_XSD = "/authorizers.xsd";
     private static final String JAXB_GENERATED_PATH = "org.apache.nifi.authorization.generated";
     private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
 
-    private static SensitivePropertyProviderFactory SENSITIVE_PROPERTY_PROVIDER_FACTORY;
-    private static SensitivePropertyProvider SENSITIVE_PROPERTY_PROVIDER;
+    private NiFiProperties properties;
 
     /**
      * Load the JAXBContext.
@@ -87,12 +85,15 @@ public class AuthorizerFactoryBean implements FactoryBean, DisposableBean, UserG
     }
 
     private Authorizer authorizer;
-    private NiFiProperties properties;
     private ExtensionManager extensionManager;
     private final Map<String, UserGroupProvider> userGroupProviders = new HashMap<>();
     private final Map<String, AccessPolicyProvider> accessPolicyProviders = new HashMap<>();
     private final Map<String, Authorizer> authorizers = new HashMap<>();
 
+    public void setProperties(final NiFiProperties properties) {
+        this.properties = properties;
+    }
+
     @Override
     public UserGroupProvider getUserGroupProvider(String identifier) {
         return userGroupProviders.get(identifier);
@@ -480,26 +481,8 @@ public class AuthorizerFactoryBean implements FactoryBean, DisposableBean, UserG
         };
     }
 
-    private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException {
-        initializeSensitivePropertyProvider(encryptionScheme);
-        return SENSITIVE_PROPERTY_PROVIDER.unprotect(cipherText);
-    }
-
-    private static void initializeSensitivePropertyProvider(String encryptionScheme) throws SensitivePropertyProtectionException {
-        if (SENSITIVE_PROPERTY_PROVIDER == null || !SENSITIVE_PROPERTY_PROVIDER.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) {
-            try {
-                String keyHex = getRootKey();
-                SENSITIVE_PROPERTY_PROVIDER_FACTORY = new AESSensitivePropertyProviderFactory(keyHex);
-                SENSITIVE_PROPERTY_PROVIDER = SENSITIVE_PROPERTY_PROVIDER_FACTORY.getProvider();
-            } catch (IOException e) {
-                logger.error("Error extracting root key from bootstrap.conf for login identity provider decryption", e);
-                throw new SensitivePropertyProtectionException("Could not read root key from bootstrap.conf");
-            }
-        }
-    }
-
-    private static String getRootKey() throws IOException {
-        return CryptoUtils.extractKeyFromBootstrapFile();
+    private String decryptValue(final String cipherText, final String protectionScheme) throws SensitivePropertyProtectionException {
+        return getSensitivePropertyProviderFactory().getProvider(PropertyProtectionScheme.fromIdentifier(protectionScheme)).unprotect(cipherText);
     }
 
     @Override
@@ -555,10 +538,6 @@ public class AuthorizerFactoryBean implements FactoryBean, DisposableBean, UserG
         }
     }
 
-    public void setProperties(NiFiProperties properties) {
-        this.properties = properties;
-    }
-
     public void setExtensionManager(ExtensionManager extensionManager) {
         this.extensionManager = extensionManager;
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/test/groovy/org/apache/nifi/authorization/AuthorizerFactoryBeanTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/test/groovy/org/apache/nifi/authorization/AuthorizerFactoryBeanTest.groovy
index dccc46c..9a548ba 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/test/groovy/org/apache/nifi/authorization/AuthorizerFactoryBeanTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-authorizer/src/test/groovy/org/apache/nifi/authorization/AuthorizerFactoryBeanTest.groovy
@@ -17,10 +17,7 @@
 package org.apache.nifi.authorization
 
 import org.apache.nifi.authorization.generated.Property
-import org.apache.nifi.properties.AESSensitivePropertyProvider
 import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.junit.After
-import org.junit.AfterClass
 import org.junit.Before
 import org.junit.BeforeClass
 import org.junit.Test
@@ -52,8 +49,10 @@ class AuthorizerFactoryBeanTest extends GroovyTestCase {
 
     private static final String PASSWORD = "thisIsABadPassword"
 
+    private AuthorizerFactoryBean bean
+
     @BeforeClass
-    public static void setUpOnce() throws Exception {
+    static void setUpOnce() throws Exception {
         Security.addProvider(new BouncyCastleProvider())
 
         logger.metaClass.methodMissing = { String name, args ->
@@ -61,29 +60,16 @@ class AuthorizerFactoryBeanTest extends GroovyTestCase {
         }
     }
 
-    @AfterClass
-    public static void tearDownOnce() throws Exception {
-    }
-
     @Before
-    public void setUp() throws Exception {
-        AuthorizerFactoryBean.SENSITIVE_PROPERTY_PROVIDER = new AESSensitivePropertyProvider(KEY_HEX)
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        AuthorizerFactoryBean.SENSITIVE_PROPERTY_PROVIDER = null
-        AuthorizerFactoryBean.SENSITIVE_PROPERTY_PROVIDER_FACTORY = null
+    void setUp() throws Exception {
+        bean = new AuthorizerFactoryBean()
+        bean.configureSensitivePropertyProviderFactory(KEY_HEX, null)
     }
 
     private static boolean isUnlimitedStrengthCryptoAvailable() {
         Cipher.getMaxAllowedKeyLength("AES") > 128
     }
 
-    private static int getKeyLength(String keyHex = KEY_HEX) {
-        keyHex?.size() * 4
-    }
-
     @Test
     void testShouldDecryptValue() {
         // Arrange
@@ -91,7 +77,7 @@ class AuthorizerFactoryBeanTest extends GroovyTestCase {
         logger.info("Cipher text: ${CIPHER_TEXT}")
 
         // Act
-        String decrypted = new AuthorizerFactoryBean().decryptValue(CIPHER_TEXT, ENCRYPTION_SCHEME)
+        String decrypted = bean.decryptValue(CIPHER_TEXT, ENCRYPTION_SCHEME)
         logger.info("Decrypted ${CIPHER_TEXT} -> ${decrypted}")
 
         // Assert
@@ -107,7 +93,6 @@ class AuthorizerFactoryBeanTest extends GroovyTestCase {
         List<Property> properties = [managerPasswordProperty]
 
         logger.info("Manager Password property: ${managerPasswordProperty.dump()}")
-        def bean = new AuthorizerFactoryBean()
 
         // Act
         def context = bean.loadAuthorizerConfiguration(identifier, properties)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/test/groovy/org/apache/nifi/controller/repository/EncryptedRepositoryRecordSerdeFactoryTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/test/groovy/org/apache/nifi/controller/repository/EncryptedRepositoryRecordSerdeFactoryTest.groovy
index f12a695..fe690df 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/test/groovy/org/apache/nifi/controller/repository/EncryptedRepositoryRecordSerdeFactoryTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-flowfile-repo-serialization/src/test/groovy/org/apache/nifi/controller/repository/EncryptedRepositoryRecordSerdeFactoryTest.groovy
@@ -20,7 +20,6 @@ package org.apache.nifi.controller.repository
 import org.apache.commons.lang3.SystemUtils
 import org.apache.nifi.controller.repository.claim.ResourceClaimManager
 import org.apache.nifi.controller.repository.claim.StandardResourceClaimManager
-import org.apache.nifi.properties.StandardNiFiProperties
 import org.apache.nifi.security.kms.EncryptionException
 import org.apache.nifi.util.NiFiProperties
 import org.bouncycastle.jce.provider.BouncyCastleProvider
@@ -74,7 +73,7 @@ class EncryptedRepositoryRecordSerdeFactoryTest extends GroovyTestCase {
                 (NiFiProperties.FLOWFILE_REPOSITORY_ENCRYPTION_KEY)                              : KEY_1_HEX,
                 (NiFiProperties.FLOWFILE_REPOSITORY_ENCRYPTION_KEY_ID)                           : KEY_ID
         ]
-        mockNiFiProperties = new StandardNiFiProperties(new Properties(flowfileEncryptionProps))
+        mockNiFiProperties = new NiFiProperties(new Properties(flowfileEncryptionProps))
     }
 
     @After
@@ -124,7 +123,7 @@ class EncryptedRepositoryRecordSerdeFactoryTest extends GroovyTestCase {
     @Test
     void testCreateSerDeShouldFailWithUnpopulatedNiFiProperties() {
         // Arrange
-        NiFiProperties emptyNiFiProperties = new StandardNiFiProperties(new Properties([:]))
+        NiFiProperties emptyNiFiProperties = new NiFiProperties(new Properties([:]))
 
         // Act
         def msg = shouldFail(EncryptionException) {
@@ -144,7 +143,7 @@ class EncryptedRepositoryRecordSerdeFactoryTest extends GroovyTestCase {
                 (NiFiProperties.FLOWFILE_REPOSITORY_ENCRYPTION_KEY)                              : KEY_1_HEX,
                 (NiFiProperties.FLOWFILE_REPOSITORY_ENCRYPTION_KEY_ID)                           : KEY_ID
         ]
-        NiFiProperties invalidNiFiProperties = new StandardNiFiProperties(new Properties(invalidFlowfileEncryptionProps))
+        NiFiProperties invalidNiFiProperties = new NiFiProperties(new Properties(invalidFlowfileEncryptionProps))
 
         // Act
         def msg = shouldFail(EncryptionException) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBeanTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBeanTest.groovy
index 4e7f7f9..cb3dac8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBeanTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/flow/PopularVoteFlowElectionFactoryBeanTest.groovy
@@ -18,7 +18,7 @@ package org.apache.nifi.cluster.coordination.flow
 
 import org.apache.nifi.encrypt.PropertyEncryptor
 import org.apache.nifi.encrypt.PropertyEncryptorFactory
-import org.apache.nifi.properties.StandardNiFiProperties
+
 import org.apache.nifi.security.util.EncryptionMethod
 import org.apache.nifi.util.NiFiProperties
 import org.junit.Before
@@ -51,7 +51,7 @@ class PopularVoteFlowElectionFactoryBeanTest extends GroovyTestCase {
     }
 
     NiFiProperties mockProperties(Map<String, String> defaults = [:]) {
-        def mockProps = new StandardNiFiProperties(new Properties([
+        def mockProps = new NiFiProperties(new Properties([
                 (NiFiProperties.SENSITIVE_PROPS_ALGORITHM):DEFAULT_ENCRYPTION_METHOD.algorithm,
                 (NiFiProperties.SENSITIVE_PROPS_PROVIDER):DEFAULT_ENCRYPTION_METHOD.provider,
         ] + defaults))
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/http/replication/okhttp/OkHttpReplicationClientTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/http/replication/okhttp/OkHttpReplicationClientTest.groovy
index d288aed..248c1a1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/http/replication/okhttp/OkHttpReplicationClientTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/groovy/org/apache/nifi/cluster/coordination/http/replication/okhttp/OkHttpReplicationClientTest.groovy
@@ -18,7 +18,7 @@
 
 package org.apache.nifi.cluster.coordination.http.replication.okhttp
 
-import org.apache.nifi.properties.StandardNiFiProperties
+
 import org.apache.nifi.util.NiFiProperties
 import org.junit.BeforeClass
 import org.junit.Test
@@ -38,13 +38,13 @@ class OkHttpReplicationClientTest extends GroovyTestCase {
         }
     }
 
-    private static StandardNiFiProperties mockNiFiProperties() {
+    private static NiFiProperties mockNiFiProperties() {
         [getClusterNodeConnectionTimeout: { -> "10 ms" },
          getClusterNodeReadTimeout      : { -> "10 ms" },
          getProperty                    : { String prop ->
              logger.mock("Requested getProperty(${prop}) -> \"\"")
              ""
-         }] as StandardNiFiProperties
+         }] as NiFiProperties
     }
 
     @Test
@@ -150,7 +150,7 @@ class OkHttpReplicationClientTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_HOST)            : "localhost",
                 (NiFiProperties.WEB_HTTPS_PORT)            : "51552",
         ]
-        NiFiProperties mockNiFiProperties = new StandardNiFiProperties(new Properties(propsMap))
+        NiFiProperties mockNiFiProperties = new NiFiProperties(new Properties(propsMap))
 
         // Act
         OkHttpReplicationClient client = new OkHttpReplicationClient(mockNiFiProperties)
@@ -173,7 +173,7 @@ class OkHttpReplicationClientTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_HOST)            : "localhost",
                 (NiFiProperties.WEB_HTTPS_PORT)            : "51552",
         ]
-        NiFiProperties mockNiFiProperties = new StandardNiFiProperties(new Properties(flowfileEncryptionProps))
+        NiFiProperties mockNiFiProperties = new NiFiProperties(new Properties(flowfileEncryptionProps))
 
         // Act
         OkHttpReplicationClient client = new OkHttpReplicationClient(mockNiFiProperties)
@@ -197,7 +197,7 @@ class OkHttpReplicationClientTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_HOST)            : "localhost",
                 (NiFiProperties.WEB_HTTPS_PORT)            : "51552",
         ]
-        NiFiProperties mockNiFiProperties = new StandardNiFiProperties(new Properties(propsMap))
+        NiFiProperties mockNiFiProperties = new NiFiProperties(new Properties(propsMap))
 
         // Act
         OkHttpReplicationClient client = new OkHttpReplicationClient(mockNiFiProperties)
@@ -221,7 +221,7 @@ class OkHttpReplicationClientTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_HOST)            : "localhost",
                 (NiFiProperties.WEB_HTTPS_PORT)            : "51552",
         ]
-        NiFiProperties mockNiFiProperties = new StandardNiFiProperties(new Properties(propsMap))
+        NiFiProperties mockNiFiProperties = new NiFiProperties(new Properties(propsMap))
 
         // Act
         OkHttpReplicationClient client = new OkHttpReplicationClient(mockNiFiProperties)
@@ -248,13 +248,13 @@ class OkHttpReplicationClientTest extends GroovyTestCase {
         ] + propsMap
 
 
-        NiFiProperties mockNiFiProperties = new StandardNiFiProperties(new Properties(propsMap))
-        NiFiProperties mockTLSNiFiProperties = new StandardNiFiProperties(new Properties(tlsPropsMap))
+        NiFiProperties mockNiFiProperties = new NiFiProperties(new Properties(propsMap))
+        NiFiProperties mockTLSNiFiProperties = new NiFiProperties(new Properties(tlsPropsMap))
 
         // Remove the keystore password to create an invalid configuration
         Map invalidTlsPropsMap = tlsPropsMap
         invalidTlsPropsMap.remove(NiFiProperties.SECURITY_KEYSTORE_PASSWD)
-        NiFiProperties mockInvalidTLSNiFiProperties = new StandardNiFiProperties(new Properties(invalidTlsPropsMap))
+        NiFiProperties mockInvalidTLSNiFiProperties = new NiFiProperties(new Properties(invalidTlsPropsMap))
 
         // Act
         OkHttpReplicationClient client = new OkHttpReplicationClient(mockNiFiProperties)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/cluster/ZooKeeperClientConfigTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/cluster/ZooKeeperClientConfigTest.java
index ef832c5..f54c10e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/cluster/ZooKeeperClientConfigTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/cluster/ZooKeeperClientConfigTest.java
@@ -17,18 +17,16 @@
 package org.apache.nifi.cluster;
 
 import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
-import org.apache.nifi.properties.StandardNiFiProperties;
 import org.apache.nifi.util.NiFiProperties;
+import org.junit.Test;
+
+import java.util.Properties;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-import org.junit.Test;
-
-import java.util.Properties;
-
 public class ZooKeeperClientConfigTest {
 
     private static final String LOCAL_CONNECT_STRING = "local:1234";
@@ -95,7 +93,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_CONNECT_STRING, LOCAL_CONNECT_STRING);
         properties.setProperty(NiFiProperties.ZOOKEEPER_CLIENT_SECURE, Boolean.TRUE.toString());
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertTrue(zkClientConfig.isClientSecure());
         assertEquals(zkClientConfig.getConnectionSocket(), ZooKeeperClientConfig.NETTY_CLIENT_CNXN_SOCKET);
     }
@@ -106,7 +104,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_CONNECT_STRING, LOCAL_CONNECT_STRING);
         properties.setProperty(NiFiProperties.ZOOKEEPER_CLIENT_SECURE, Boolean.FALSE.toString());
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertFalse(zkClientConfig.isClientSecure());
         assertEquals(zkClientConfig.getConnectionSocket(), ZooKeeperClientConfig.NIO_CLIENT_CNXN_SOCKET);
     }
@@ -117,7 +115,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_CONNECT_STRING, LOCAL_CONNECT_STRING);
         properties.setProperty(NiFiProperties.ZOOKEEPER_CLIENT_SECURE, "");
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertFalse(zkClientConfig.isClientSecure());
         assertEquals(zkClientConfig.getConnectionSocket(), ZooKeeperClientConfig.NIO_CLIENT_CNXN_SOCKET);
     }
@@ -128,7 +126,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_CONNECT_STRING, LOCAL_CONNECT_STRING);
         properties.setProperty(NiFiProperties.ZOOKEEPER_CLIENT_SECURE, " true ");
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertTrue(zkClientConfig.isClientSecure());
         assertEquals(zkClientConfig.getConnectionSocket(), ZooKeeperClientConfig.NETTY_CLIENT_CNXN_SOCKET);
     }
@@ -138,9 +136,9 @@ public class ZooKeeperClientConfigTest {
         final Properties properties = new Properties();
         properties.setProperty(NiFiProperties.ZOOKEEPER_CONNECT_STRING, LOCAL_CONNECT_STRING);
         properties.setProperty(NiFiProperties.ZOOKEEPER_CLIENT_SECURE, Boolean.TRUE.toString());
-        ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertTrue(zkClientConfig.isClientSecure());
         assertEquals(zkClientConfig.getConnectionSocket(), ZooKeeperClientConfig.NETTY_CLIENT_CNXN_SOCKET);
     }
@@ -150,7 +148,7 @@ public class ZooKeeperClientConfigTest {
         final Properties properties = new Properties();
         properties.setProperty(NiFiProperties.ZOOKEEPER_CONNECT_STRING, LOCAL_CONNECT_STRING);
         properties.setProperty(NiFiProperties.ZOOKEEPER_CLIENT_SECURE, "meh");
-        ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
     }
 
     @Test
@@ -161,7 +159,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_TYPE, storeType);
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_TYPE, storeType);
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertEquals(storeType, zkClientConfig.getKeyStoreType());
         assertEquals(storeType, zkClientConfig.getTrustStoreType());
     }
@@ -174,7 +172,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_TYPE, storeType);
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_TYPE, storeType);
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         final String expectedStoreType = "JKS";
         assertEquals(expectedStoreType, zkClientConfig.getKeyStoreType());
         assertEquals(expectedStoreType, zkClientConfig.getTrustStoreType());
@@ -187,7 +185,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_TYPE, "");
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_TYPE, "");
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertNull(zkClientConfig.getKeyStoreType());
         assertNull(zkClientConfig.getTrustStoreType());
     }
@@ -199,7 +197,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_KEYSTORE_TYPE, "    ");
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_TYPE, "    ");
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertNull(zkClientConfig.getKeyStoreType());
         assertNull(zkClientConfig.getTrustStoreType());
     }
@@ -219,7 +217,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE, ZOOKEEPER_TRUSTSTORE);
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_TYPE, ZOOKEEPER_STORE_TYPE);
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertEquals(ZOOKEEPER_KEYSTORE, zkClientConfig.getKeyStore());
         assertEquals(ZOOKEEPER_TRUSTSTORE, zkClientConfig.getTrustStore());
     }
@@ -233,7 +231,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE, DEFAULT_TRUSTSTORE);
         properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, DEFAULT_STORE_TYPE);
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertEquals(DEFAULT_KEYSTORE, zkClientConfig.getKeyStore());
         assertEquals(DEFAULT_TRUSTSTORE, zkClientConfig.getTrustStore());
     }
@@ -251,7 +249,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE, DEFAULT_TRUSTSTORE);
         properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, DEFAULT_STORE_TYPE);
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertEquals(ZOOKEEPER_KEYSTORE, zkClientConfig.getKeyStore());
         assertEquals(ZOOKEEPER_TRUSTSTORE, zkClientConfig.getTrustStore());
         assertEquals(ZOOKEEPER_STORE_TYPE, zkClientConfig.getKeyStoreType());
@@ -271,7 +269,7 @@ public class ZooKeeperClientConfigTest {
         properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE, DEFAULT_TRUSTSTORE);
         properties.setProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE, DEFAULT_STORE_TYPE);
 
-        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new StandardNiFiProperties(properties));
+        final ZooKeeperClientConfig zkClientConfig = ZooKeeperClientConfig.createConfig(new NiFiProperties(properties));
         assertEquals(DEFAULT_KEYSTORE, zkClientConfig.getKeyStore());
         assertEquals(DEFAULT_TRUSTSTORE, zkClientConfig.getTrustStore());
         assertEquals(DEFAULT_STORE_TYPE, zkClientConfig.getKeyStoreType());
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/server/TestZooKeeperStateServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/server/TestZooKeeperStateServer.java
index df0a08e..ff12c7e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/server/TestZooKeeperStateServer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/controller/state/server/TestZooKeeperStateServer.java
@@ -16,19 +16,15 @@
  */
 package org.apache.nifi.controller.state.server;
 
-import org.apache.curator.retry.RetryOneTime;
-import org.apache.curator.test.InstanceSpec;
 import org.apache.curator.framework.CuratorFramework;
 import org.apache.curator.framework.CuratorFrameworkFactory;
-
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.curator.test.InstanceSpec;
 import org.apache.nifi.util.NiFiProperties;
-import org.apache.nifi.properties.StandardNiFiProperties;
-
 import org.apache.zookeeper.client.FourLetterWordMain;
 import org.apache.zookeeper.common.X509Exception.SSLContextException;
 import org.apache.zookeeper.data.Stat;
 import org.apache.zookeeper.server.quorum.QuorumPeerConfig.ConfigException;
-
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -73,7 +69,7 @@ public class TestZooKeeperStateServer {
         properties.setProperty(NiFiProperties.STATE_MANAGEMENT_ZOOKEEPER_PROPERTIES, zkServerConfig.toString());
         properties.setProperty(NiFiProperties.STATE_MANAGEMENT_START_EMBEDDED_ZOOKEEPER, Boolean.TRUE.toString());
 
-        zkServer = ZooKeeperStateServer.create(new StandardNiFiProperties(properties));
+        zkServer = ZooKeeperStateServer.create(new NiFiProperties(properties));
 
         if (zkServer != null) zkServer.start();
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/leader/election/ITSecureClientZooKeeperFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/leader/election/ITSecureClientZooKeeperFactory.java
index 9e0a1ae..b0a4f22 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/leader/election/ITSecureClientZooKeeperFactory.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/leader/election/ITSecureClientZooKeeperFactory.java
@@ -16,22 +16,18 @@
  */
 package org.apache.nifi.leader.election;
 
-import org.apache.curator.retry.RetryOneTime;
-import org.apache.curator.test.InstanceSpec;
 import org.apache.curator.framework.CuratorFramework;
 import org.apache.curator.framework.CuratorFrameworkFactory;
-
-import org.apache.nifi.util.NiFiProperties;
-import org.apache.nifi.properties.StandardNiFiProperties;
-import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
+import org.apache.curator.retry.RetryOneTime;
+import org.apache.curator.test.InstanceSpec;
 import org.apache.nifi.controller.cluster.SecureClientZooKeeperFactory;
+import org.apache.nifi.controller.cluster.ZooKeeperClientConfig;
 import org.apache.nifi.security.util.CertificateUtils;
-
+import org.apache.nifi.util.NiFiProperties;
 import org.apache.zookeeper.common.ClientX509Util;
 import org.apache.zookeeper.data.Stat;
 import org.apache.zookeeper.server.ServerCnxnFactory;
 import org.apache.zookeeper.server.ZooKeeperServer;
-
 import org.junit.AfterClass;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -42,7 +38,6 @@ import java.net.InetSocketAddress;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.cert.CertificateException;
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
@@ -50,6 +45,7 @@ import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.List;
@@ -218,7 +214,7 @@ public class ITSecureClientZooKeeperFactory {
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_TYPE, trustStoreType);
         properties.setProperty(NiFiProperties.ZOOKEEPER_SECURITY_TRUSTSTORE_PASSWD, trustStorePassword);
 
-        return new StandardNiFiProperties(properties);
+        return new NiFiProperties(properties);
     }
 
     public static X509Certificate createKeyStore(final String alias,
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/pom.xml
index f49c66d..ad5058d 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/pom.xml
@@ -46,6 +46,10 @@
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-security-utils</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-sensitive-property-provider</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <!-- Required to run Groovy tests without any Java tests -->
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProviderFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProviderFactory.java
deleted file mode 100644
index 56a1cc0..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProviderFactory.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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 java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import javax.crypto.NoSuchPaddingException;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class AESSensitivePropertyProviderFactory implements SensitivePropertyProviderFactory {
-    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactory.class);
-
-    private String keyHex;
-
-    public AESSensitivePropertyProviderFactory(String keyHex) {
-        this.keyHex = keyHex;
-    }
-
-    public SensitivePropertyProvider getProvider() throws SensitivePropertyProtectionException {
-        try {
-            if (keyHex != null && !StringUtils.isBlank(keyHex)) {
-                return new AESSensitivePropertyProvider(keyHex);
-            } else {
-                throw new SensitivePropertyProtectionException("The provider factory cannot generate providers without a key");
-            }
-        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
-            String msg = "Error creating AES Sensitive Property Provider";
-            logger.warn(msg, e);
-            throw new SensitivePropertyProtectionException(msg, e);
-        }
-    }
-
-    @Override
-    public String toString() {
-        return "SensitivePropertyProviderFactory for creating AESSensitivePropertyProviders";
-    }
-}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java
index 250227f..480f801 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java
@@ -16,6 +16,12 @@
  */
 package org.apache.nifi.properties;
 
+import org.apache.nifi.util.NiFiBootstrapUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -25,20 +31,13 @@ import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.Security;
 import java.util.Base64;
 import java.util.List;
 import java.util.Properties;
-import java.util.stream.Collectors;
 import java.util.Set;
-import javax.crypto.Cipher;
-import org.apache.nifi.security.kms.CryptoUtils;
-import org.apache.nifi.util.NiFiProperties;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.stream.Collectors;
 
 public class NiFiPropertiesLoader {
 
@@ -49,12 +48,12 @@ public class NiFiPropertiesLoader {
     private static final String MIGRATION_INSTRUCTIONS = "See Admin Guide section [Updating the Sensitive Properties Key]";
     private static final String PROPERTIES_KEY_MESSAGE = String.format("Sensitive Properties Key [%s] not found: %s", NiFiProperties.SENSITIVE_PROPS_KEY, MIGRATION_INSTRUCTIONS);
 
-    private final String defaultPropertiesFilePath = CryptoUtils.getDefaultFilePath();
+    private final String defaultPropertiesFilePath = NiFiBootstrapUtils.getDefaultApplicationPropertiesFilePath();
     private NiFiProperties instance;
     private String keyHex;
 
     // Future enhancement: allow for external registration of new providers
-    private static SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
+    private SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
 
     public NiFiPropertiesLoader() {
     }
@@ -69,20 +68,20 @@ public class NiFiPropertiesLoader {
      * @param keyHex the key used to encrypt any sensitive properties
      * @return the configured loader
      */
-    public static NiFiPropertiesLoader withKey(String keyHex) {
-        NiFiPropertiesLoader loader = new NiFiPropertiesLoader();
+    public static NiFiPropertiesLoader withKey(final String keyHex) {
+        final NiFiPropertiesLoader loader = new NiFiPropertiesLoader();
         loader.setKeyHex(keyHex);
         return loader;
     }
 
     /**
-     * Sets the hexadecimal key used to unprotect properties encrypted with
-     * {@link AESSensitivePropertyProvider}. If the key has already been set,
+     * Sets the hexadecimal key used to unprotect properties encrypted with a
+     * {@link SensitivePropertyProvider}. 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) {
+    public void setKeyHex(final String keyHex) {
         if (this.keyHex == null || this.keyHex.trim().isEmpty()) {
             this.keyHex = keyHex;
         } else {
@@ -101,68 +100,25 @@ public class NiFiPropertiesLoader {
      *                     or nifi.properties files
      */
     public static NiFiProperties loadDefaultWithKeyFromBootstrap() throws IOException {
-        // The nifi.properties file may not be encrypted, so attempt to naively load it first
         try {
+            // The default behavior of StandardSensitivePropertiesFactory is to use the key
+            // from bootstrap.conf if no key is provided
             return new NiFiPropertiesLoader().loadDefault();
         } catch (Exception e) {
             logger.warn("Encountered an error naively loading the nifi.properties file because one or more properties are protected: {}", e.getLocalizedMessage());
-        }
-
-        try {
-            String keyHex = CryptoUtils.extractKeyFromBootstrapFile();
-            return NiFiPropertiesLoader.withKey(keyHex).loadDefault();
-        } catch (IOException e) {
-            logger.error("Encountered an exception loading the default nifi.properties file {} with the key provided in bootstrap.conf", CryptoUtils.getDefaultFilePath(), e);
             throw e;
         }
     }
 
-    /**
-     * Returns the key (if any) used to encrypt sensitive properties, extracted from {@code $NIFI_HOME/conf/bootstrap.conf}.
-     *
-     * @return the key in hexadecimal format
-     * @throws IOException if the file is not readable
-     * @deprecated Use {@link CryptoUtils#extractKeyFromBootstrapFile()} instead.
-     */
-    @Deprecated
-    public static String extractKeyFromBootstrapFile() throws IOException {
-        // TODO: Replace all existing uses with direct reference to CryptoUtils
-        return extractKeyFromBootstrapFile("");
-    }
-
-    /**
-     * Returns the key (if any) used to encrypt sensitive properties, extracted from {@code $NIFI_HOME/conf/bootstrap.conf}.
-     *
-     * @param bootstrapPath the path to the bootstrap file
-     * @return the key in hexadecimal format
-     * @throws IOException if the file is not readable
-     * @deprecated Use {@link CryptoUtils#extractKeyFromBootstrapFile(String)} instead.
-     */
-    @Deprecated
-    public static String extractKeyFromBootstrapFile(String bootstrapPath) throws IOException {
-        // TODO: Replace all existing uses with direct reference to CryptoUtils
-        return CryptoUtils.extractKeyFromBootstrapFile(bootstrapPath);
-    }
-
     private NiFiProperties loadDefault() {
         return load(defaultPropertiesFilePath);
     }
 
-    static String getDefaultProviderKey() {
-        try {
-            return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? "256" : "128");
-        } catch (NoSuchAlgorithmException e) {
-            return "aes/gcm/128";
+    private SensitivePropertyProviderFactory getSensitivePropertyProviderFactory() {
+        if (sensitivePropertyProviderFactory == null) {
+            sensitivePropertyProviderFactory = StandardSensitivePropertyProviderFactory.withKey(keyHex);
         }
-    }
-
-    private void initializeSensitivePropertyProviderFactory() {
-        sensitivePropertyProviderFactory = new AESSensitivePropertyProviderFactory(keyHex);
-    }
-
-    private SensitivePropertyProvider getSensitivePropertyProvider() {
-        initializeSensitivePropertyProviderFactory();
-        return sensitivePropertyProviderFactory.getProvider();
+        return sensitivePropertyProviderFactory;
     }
 
     /**
@@ -209,11 +165,13 @@ public class NiFiPropertiesLoader {
      * @param file the File containing the serialized properties
      * @return the NiFiProperties instance
      */
-    public NiFiProperties load(File file) {
-        ProtectedNiFiProperties protectedNiFiProperties = readProtectedPropertiesFromDisk(file);
+    public NiFiProperties load(final File file) {
+        final ProtectedNiFiProperties protectedNiFiProperties = readProtectedPropertiesFromDisk(file);
         if (protectedNiFiProperties.hasProtectedKeys()) {
             Security.addProvider(new BouncyCastleProvider());
-            protectedNiFiProperties.addSensitivePropertyProvider(getSensitivePropertyProvider());
+            getSensitivePropertyProviderFactory()
+                    .getSupportedSensitivePropertyProviders()
+                    .forEach(protectedNiFiProperties::addSensitivePropertyProvider);
         }
 
         return protectedNiFiProperties.getUnprotectedProperties();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/ProtectedNiFiProperties.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/ProtectedNiFiProperties.java
index 16fb463..368ff03 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/ProtectedNiFiProperties.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/ProtectedNiFiProperties.java
@@ -16,35 +16,33 @@
  */
 package org.apache.nifi.properties;
 
-import static java.util.Arrays.asList;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.util.NiFiProperties;
+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.Properties;
 import java.util.Set;
-import java.util.stream.Collectors;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.util.NiFiProperties;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+
+import static java.util.Arrays.asList;
 
 /**
  * Decorator class for intermediate phase when {@link NiFiPropertiesLoader} loads the
  * raw properties file and performs unprotection activities before returning a clean
- * implementation of {@link NiFiProperties}, likely {@link StandardNiFiProperties}.
+ * implementation of {@link NiFiProperties}.
  * This encapsulates the sensitive property access logic from external consumers
  * of {@code NiFiProperties}.
  */
-class ProtectedNiFiProperties extends StandardNiFiProperties {
+class ProtectedNiFiProperties extends NiFiProperties implements ProtectedProperties<NiFiProperties>,
+        SensitivePropertyProtector<ProtectedNiFiProperties, NiFiProperties> {
     private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiProperties.class);
 
-    private NiFiProperties niFiProperties;
+    private SensitivePropertyProtector<ProtectedNiFiProperties, NiFiProperties> propertyProtectionDelegate;
 
-    private Map<String, SensitivePropertyProvider> localProviderCache = new HashMap<>();
+    private NiFiProperties applicationProperties;
 
     // Additional "sensitive" property key
     public static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = "nifi.sensitive.props.additional.keys";
@@ -54,7 +52,7 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
             SECURITY_KEYSTORE_PASSWD, SECURITY_TRUSTSTORE_PASSWD, SENSITIVE_PROPS_KEY, PROVENANCE_REPO_ENCRYPTION_KEY));
 
     public ProtectedNiFiProperties() {
-        this(new StandardNiFiProperties());
+        this(new NiFiProperties());
     }
 
     /**
@@ -62,9 +60,11 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
      *
      * @param props the NiFiProperties to contain
      */
-    public ProtectedNiFiProperties(NiFiProperties props) {
-        this.niFiProperties = props;
-        logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiProperties", getPropertyKeysIncludingProtectionSchemes().size(), getProtectedPropertyKeys().size());
+    public ProtectedNiFiProperties(final NiFiProperties props) {
+        this.applicationProperties = props;
+        this.propertyProtectionDelegate = new ApplicationPropertiesProtector<>(this);
+        logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiProperties", getApplicationProperties()
+                .getPropertyKeys().size(), getProtectedPropertyKeys().size());
     }
 
     /**
@@ -72,8 +72,44 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
      *
      * @param rawProps the Properties to contain
      */
-    public ProtectedNiFiProperties(Properties rawProps) {
-        this(new StandardNiFiProperties(rawProps));
+    public ProtectedNiFiProperties(final Properties rawProps) {
+        this(new NiFiProperties(rawProps));
+    }
+
+    @Override
+    public String getAdditionalSensitivePropertiesKeys() {
+        return getProperty(getAdditionalSensitivePropertiesKeysName());
+    }
+
+    @Override
+    public String getAdditionalSensitivePropertiesKeysName() {
+        return ADDITIONAL_SENSITIVE_PROPERTIES_KEY;
+    }
+
+    @Override
+    public List<String> getDefaultSensitiveProperties() {
+        return DEFAULT_SENSITIVE_PROPERTIES;
+    }
+
+    /**
+     * Returns the internal representation of the {@link NiFiProperties} -- 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 NiFiProperties} instance is created.
+     *
+     * @return the internal properties
+     */
+    public NiFiProperties getApplicationProperties() {
+        if (this.applicationProperties == null) {
+            this.applicationProperties = new NiFiProperties();
+        }
+
+        return this.applicationProperties;
+    }
+
+    @Override
+    public NiFiProperties createApplicationProperties(final Properties rawProperties) {
+        return new NiFiProperties(rawProperties);
     }
 
     /**
@@ -84,7 +120,7 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
      */
     @Override
     public String getProperty(String key) {
-        return getInternalNiFiProperties().getProperty(key);
+        return getApplicationProperties().getProperty(key);
     }
 
     /**
@@ -94,25 +130,7 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
      */
     @Override
     public Set<String> getPropertyKeys() {
-        Set<String> filteredKeys = getPropertyKeysIncludingProtectionSchemes();
-        filteredKeys.removeIf(p -> p.endsWith(".protected"));
-        return filteredKeys;
-    }
-
-    /**
-     * Returns the internal representation of the {@link NiFiProperties} -- 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 StandardNiFiProperties} instance is created.
-     *
-     * @return the internal properties
-     */
-    NiFiProperties getInternalNiFiProperties() {
-        if (this.niFiProperties == null) {
-            this.niFiProperties = new StandardNiFiProperties();
-        }
-
-        return this.niFiProperties;
+        return propertyProtectionDelegate.getPropertyKeys();
     }
 
     /**
@@ -130,317 +148,62 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
      */
     @Override
     public int size() {
-        return getPropertyKeys().size();
+        return propertyProtectionDelegate.size();
     }
 
-    /**
-     * Returns the complete set of property keys, including any protection keys (i.e. 'x.y.z.protected').
-     *
-     * @return the set of property keys
-     */
-    Set<String> getPropertyKeysIncludingProtectionSchemes() {
-        return getInternalNiFiProperties().getPropertyKeys();
-    }
-
-    /**
-     * 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.property.1; nifi.property.2, nifi.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;
-        }
+    @Override
+    public Set<String> getPropertyKeysIncludingProtectionSchemes() {
+        return propertyProtectionDelegate.getPropertyKeysIncludingProtectionSchemes();
     }
 
-    /**
-     * 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
-     */
+    @Override
     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;
-        }
+        return propertyProtectionDelegate.getSensitivePropertyKeys();
     }
 
-    /**
-     * 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
-     */
+    @Override
     public List<String> getPopulatedSensitivePropertyKeys() {
-        List<String> allSensitiveKeys = getSensitivePropertyKeys();
-        return allSensitiveKeys.stream().filter(k -> StringUtils.isNotBlank(getProperty(k))).collect(Collectors.toList());
+        return propertyProtectionDelegate.getPopulatedSensitivePropertyKeys();
     }
 
-    /**
-     * Returns true if any sensitive keys are protected.
-     *
-     * @return true if any key is protected; false otherwise
-     */
+    @Override
     public boolean hasProtectedKeys() {
-        List<String> sensitiveKeys = getSensitivePropertyKeys();
-        for (String k : sensitiveKeys) {
-            if (isPropertyProtected(k)) {
-                return true;
-            }
-        }
-        return false;
+        return propertyProtectionDelegate.hasProtectedKeys();
     }
 
-    /**
-     * 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
-     */
+    @Override
     public Map<String, String> getProtectedPropertyKeys() {
-        List<String> sensitiveKeys = getSensitivePropertyKeys();
-
-        // This is the Java 8 way, but can likely be optimized (and not sure of correctness)
-        // Map<String, String> protectedProperties = sensitiveKeys.stream().filter(key ->
-        // getProperty(getProtectionKey(key)) != null).collect(Collectors.toMap(Function.identity(), key ->
-        // getProperty(getProtectionKey(key))));
-
-        // Groovy
-        // Map<String, String> groovyProtectedProperties = sensitiveKeys.collectEntries { key ->
-        // [(key): getProperty(getProtectionKey(key))] }.findAll { k, v -> v }
-
-        // Traditional way
-        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;
+        return propertyProtectionDelegate.getProtectedPropertyKeys();
     }
 
-    /**
-     * Returns the unique set of all protection schemes currently in use for this instance.
-     *
-     * @return the set of protection schemes
-     */
+    @Override
     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 ProtectedNiFiProperties#ADDITIONAL_SENSITIVE_PROPERTIES_KEY}.
-     *
-     * @param key the key
-     * @return true if it is sensitive
-     * @see ProtectedNiFiProperties#getSensitivePropertyKeys()
-     */
-    public boolean isPropertySensitive(String key) {
-        // If the explicit check for ADDITIONAL_SENSITIVE_PROPERTIES_KEY is not here, this will loop infinitely
-        return key != null && !key.equals(ADDITIONAL_SENSITIVE_PROPERTIES_KEY) && getSensitivePropertyKeys().contains(key.trim());
+        return propertyProtectionDelegate.getProtectionSchemes();
     }
 
-    /**
-     * 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 ProtectedNiFiProperties#getSensitivePropertyKeys()
-     */
-    public boolean isPropertyProtected(String key) {
-        return key != null && isPropertySensitive(key) && !StringUtils.isBlank(getProperty(getProtectionKey(key)));
+    @Override
+    public boolean isPropertySensitive(final String key) {
+        return propertyProtectionDelegate.isPropertySensitive(key);
     }
 
-    /**
-     * Returns the sibling property key which specifies the protection scheme for this key.
-     * <p>
-     * Example:
-     * <p>
-     * nifi.sensitive.key=ABCXYZ
-     * nifi.sensitive.key.protected=aes/gcm/256
-     * <p>
-     * nifi.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";
+    @Override
+    public boolean isPropertyProtected(final String key) {
+        return propertyProtectionDelegate.isPropertyProtected(key);
     }
 
-    /**
-     * Returns the unprotected {@link NiFiProperties} 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 NiFiProperties instance with all raw values
-     * @throws SensitivePropertyProtectionException if there is a problem unprotecting one or more keys
-     */
+    @Override
     public NiFiProperties getUnprotectedProperties() throws SensitivePropertyProtectionException {
-        if (hasProtectedKeys()) {
-            logger.info("There are {} protected properties of {} sensitive properties ({}%)",
-                    getProtectedPropertyKeys().size(),
-                    getSensitivePropertyKeys().size(),
-                    getPercentOfSensitivePropertiesProtected());
-
-            Properties rawProperties = new Properties();
-
-            Set<String> failedKeys = new HashSet<>();
-
-            for (String key : getPropertyKeys()) {
-                /* 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 {
-                        rawProperties.setProperty(key, unprotectValue(key, getProperty(key)));
-                    } catch (SensitivePropertyProtectionException e) {
-                        logger.warn("Failed to unprotect '{}'", key, e);
-                        failedKeys.add(key);
-                    }
-                } else {
-                    rawProperties.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());
-                }
-            }
-
-            NiFiProperties unprotected = new StandardNiFiProperties(rawProperties);
-
-            return unprotected;
-        } else {
-            logger.debug("No protected properties");
-            return getInternalNiFiProperties();
-        }
+        return propertyProtectionDelegate.getUnprotectedProperties();
     }
 
-    /**
-     * 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 NiFiProperties} 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 StandardNiFiProperties} object
-     * @throws IllegalStateException if no protection schemes are registered
-     */
-    NiFiProperties 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);
-        }
+    @Override
+    public void addSensitivePropertyProvider(final SensitivePropertyProvider sensitivePropertyProvider) {
+        propertyProtectionDelegate.addSensitivePropertyProvider(sensitivePropertyProvider);
     }
 
-    /**
-     * Returns a new instance of {@link NiFiProperties} 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 StandardNiFiProperties} object
-     */
-    NiFiProperties protectPlainProperties(String protectionScheme) {
-        SensitivePropertyProvider spp = getSensitivePropertyProvider(protectionScheme);
-
-        // Make a new holder (settable)
-        Properties protectedProperties = new Properties();
-
-        // Copy over the plain keys
-        Set<String> plainKeys = getPropertyKeys();
-        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 = getInternalNiFiProperties().getProperty(key);
-            if (plainValue != null && !plainValue.trim().isEmpty()) {
-                final String protectedValue = spp.protect(plainValue);
-                protectedProperties.setProperty(key, protectedValue);
-                protectedProperties.setProperty(getProtectionKey(key), protectionScheme);
-            }
-        }
-
-        return new StandardNiFiProperties(protectedProperties);
+    @Override
+    public Map<String, SensitivePropertyProvider> getSensitivePropertyProviders() {
+        return propertyProtectionDelegate.getSensitivePropertyProviders();
     }
 
     /**
@@ -449,7 +212,7 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
      * @param plainProperties the instance to count protected properties
      * @return the number of protected properties
      */
-    public static int countProtectedProperties(NiFiProperties plainProperties) {
+    public static int countProtectedProperties(final NiFiProperties plainProperties) {
         return new ProtectedNiFiProperties(plainProperties).getProtectedPropertyKeys().size();
     }
 
@@ -459,7 +222,7 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
      * @param plainProperties the instance to count sensitive properties
      * @return the number of sensitive properties
      */
-    public static int countSensitiveProperties(NiFiProperties plainProperties) {
+    public static int countSensitiveProperties(final NiFiProperties plainProperties) {
         return new ProtectedNiFiProperties(plainProperties).getSensitivePropertyKeys().size();
     }
 
@@ -475,59 +238,4 @@ class ProtectedNiFiProperties extends StandardNiFiProperties {
                 .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());
-            } catch (IllegalArgumentException e) {
-                throw new SensitivePropertyProtectionException("Error unprotecting value for " + key, e);
-            }
-        }
-        return retrievedValue;
-    }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy
deleted file mode 100644
index 26352c0..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.SystemUtils
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.junit.After
-import org.junit.Assume
-import org.junit.Before
-import org.junit.BeforeClass
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-
-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 = "0123456789ABCDEFFEDCBA9876543210" * 2
-
-    @BeforeClass
-    public static void setUpOnce() throws Exception {
-        Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
-        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 {
-
-    }
-
-    @Ignore("This is resolved in PR 1216")
-    @Test
-    public void testShouldNotGetProviderWithoutKey() throws Exception {
-        // Arrange
-        SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory()
-
-        // Act
-        def msg = shouldFail(SensitivePropertyProtectionException) {
-            SensitivePropertyProvider provider = factory.getProvider()
-        }
-        logger.expected(msg)
-
-        // Assert
-        assert msg == "The provider factory cannot generate providers without a key"
-    }
-
-    @Test
-    public void testShouldGetProviderWithKey() throws Exception {
-        // Arrange
-        SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX)
-
-        // Act
-        SensitivePropertyProvider provider = factory.getProvider()
-
-        // Assert
-        assert provider instanceof AESSensitivePropertyProvider
-        assert provider.@key
-        assert provider.@cipher
-    }
-
-    @Ignore("This is resolved in PR 1216")
-    @Test
-    public void testGetProviderShouldHandleEmptyKey() throws Exception {
-        // Arrange
-        SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory("")
-
-        // Act
-        def msg = shouldFail(SensitivePropertyProtectionException) {
-            SensitivePropertyProvider provider = factory.getProvider()
-        }
-        logger.expected(msg)
-
-        // Assert
-        assert msg == "The provider factory cannot generate providers without a key"
-    }
-}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesGroovyTest.groovy
similarity index 92%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy
rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesGroovyTest.groovy
index b73ac42..485050b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/StandardNiFiPropertiesGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesGroovyTest.groovy
@@ -28,8 +28,8 @@ import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
 @RunWith(JUnit4.class)
-class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
-    private static final Logger logger = LoggerFactory.getLogger(StandardNiFiPropertiesGroovyTest.class)
+class NiFiPropertiesGroovyTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(NiFiPropertiesGroovyTest.class)
 
     private static String originalPropertiesPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
     private static final String PREK = NiFiProperties.PROVENANCE_REPO_ENCRYPTION_KEY
@@ -63,18 +63,18 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         }
     }
 
-    private static StandardNiFiProperties loadFromFile(String propertiesFilePath) {
+    private static NiFiProperties loadFromFile(String propertiesFilePath) {
         String filePath
         try {
-            filePath = StandardNiFiPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath()
+            filePath = NiFiPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath()
         } catch (URISyntaxException ex) {
-            throw new RuntimeException("Cannot load properties file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load properties file due to " +
+                    ex.getLocalizedMessage(), ex)
         }
 
         System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, filePath)
 
-        StandardNiFiProperties properties = new StandardNiFiProperties()
+        NiFiProperties properties = new NiFiProperties()
 
         // clear out existing properties
         for (String prop : properties.stringPropertyNames()) {
@@ -86,8 +86,8 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
             inStream = new BufferedInputStream(new FileInputStream(filePath))
             properties.load(inStream)
         } catch (final Exception ex) {
-            throw new RuntimeException("Cannot load properties file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load properties file due to " +
+                    ex.getLocalizedMessage(), ex)
         } finally {
             if (null != inStream) {
                 try {
@@ -108,7 +108,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
 
         // Act
-        NiFiProperties niFiProperties = new StandardNiFiProperties()
+        NiFiProperties niFiProperties = new NiFiProperties()
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Assert
@@ -125,7 +125,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         assert rawProperties.size() == 1
 
         // Act
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Assert
@@ -142,9 +142,9 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         assert rawProperties.size() == 1
 
         // Act
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
-        NiFiProperties emptyProperties = new StandardNiFiProperties()
+        NiFiProperties emptyProperties = new NiFiProperties()
         logger.info("emptyProperties has ${emptyProperties.size()} properties: ${emptyProperties.getPropertyKeys()}")
 
         // Assert
@@ -163,7 +163,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
         rawProperties.setProperty(PREKID, KEY_ID)
         rawProperties.setProperty(PREK, KEY_HEX)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -196,7 +196,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty(PREK, KEY_HEX)
         rawProperties.setProperty("${PREK}.id.${KEY_ID_2}", KEY_HEX_2)
         rawProperties.setProperty("${PREK}.id.${KEY_ID_3}", KEY_HEX_3)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -236,7 +236,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty(FFREK, "")
         rawProperties.setProperty("${FFREK}.id.${KEY_ID}", KEY_HEX)
         rawProperties.setProperty("${FFREK}.id.${KEY_ID_2}", KEY_HEX_2)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -276,7 +276,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
 //        rawProperties.setProperty(FFREK, "")
         rawProperties.setProperty("${FFREK}.id.${KEY_ID}", KEY_HEX)
         rawProperties.setProperty("${FFREK}.id.${KEY_ID_2}", KEY_HEX_2)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -318,7 +318,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
 //        rawProperties.setProperty(FFREK, "")
         rawProperties.setProperty("${FFREK}.${KEY_ID}", KEY_HEX)
         rawProperties.setProperty("${FFREK}.${KEY_ID_2}", KEY_HEX_2)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -365,7 +365,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty(FFREK, KEY_HEX_DUP)
         rawProperties.setProperty("${FFREK}.id.${KEY_ID}", KEY_HEX)
         rawProperties.setProperty("${FFREK}.id.${KEY_ID_2}", KEY_HEX_2)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -412,7 +412,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty("${FFREK}.id.${KEY_ID}", KEY_HEX)
         rawProperties.setProperty(FFREK, KEY_HEX_DUP)
         rawProperties.setProperty(FFREKID, KEY_ID_2)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -445,7 +445,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty("${PREK}.id.${KEY_ID}", KEY_HEX)
         rawProperties.setProperty("${PREK}.id.${KEY_ID_2}", KEY_HEX_2)
         rawProperties.setProperty("${PREK}.id.${KEY_ID_3}", KEY_HEX_3)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -467,7 +467,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
     void testShouldGetProvenanceRepoEncryptionKeysWithNoneDefined() throws Exception {
         // Arrange
         Properties rawProperties = new Properties()
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -492,7 +492,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         final String KEY_ID = "arbitraryKeyId"
 
         rawProperties.setProperty(PREKID, KEY_ID)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -527,7 +527,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty("${PREK}.id.${KEY_ID_3}", KEY_HEX_3)
         rawProperties.setProperty(NiFiProperties.PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_IMPLEMENTATION_CLASS, "some.class.provider")
         rawProperties.setProperty(NiFiProperties.PROVENANCE_REPO_ENCRYPTION_KEY_PROVIDER_LOCATION, "some://url")
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -553,7 +553,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
         rawProperties.setProperty(CREKID, KEY_ID)
         rawProperties.setProperty(CREK, KEY_HEX)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -586,7 +586,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty(CREK, KEY_HEX)
         rawProperties.setProperty("${CREK}.id.${KEY_ID_2}", KEY_HEX_2)
         rawProperties.setProperty("${CREK}.id.${KEY_ID_3}", KEY_HEX_3)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -619,7 +619,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty("${CREK}.id.${KEY_ID}", KEY_HEX)
         rawProperties.setProperty("${CREK}.id.${KEY_ID_2}", KEY_HEX_2)
         rawProperties.setProperty("${CREK}.id.${KEY_ID_3}", KEY_HEX_3)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -641,7 +641,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
     void testShouldGetContentRepositoryEncryptionKeysWithNoneDefined() throws Exception {
         // Arrange
         Properties rawProperties = new Properties()
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -666,7 +666,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         final String KEY_ID = "arbitraryKeyId"
 
         rawProperties.setProperty(CREKID, KEY_ID)
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -701,7 +701,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         rawProperties.setProperty("${CREK}.id.${KEY_ID_3}", KEY_HEX_3)
         rawProperties.setProperty(NiFiProperties.CONTENT_REPOSITORY_ENCRYPTION_KEY_PROVIDER_IMPLEMENTATION_CLASS, "some.class.provider")
         rawProperties.setProperty(NiFiProperties.CONTENT_REPOSITORY_ENCRYPTION_KEY_PROVIDER_LOCATION, "some://url")
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Act
@@ -724,7 +724,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String noLeadingSlash = "some/context/path"
         Properties rawProps = new Properties(["nifi.web.proxy.context.path": noLeadingSlash])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw context path property [${noLeadingSlash}]")
 
         // Act
@@ -740,7 +740,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String leadingSlash = "/some/context/path"
         Properties rawProps = new Properties(["nifi.web.proxy.context.path": leadingSlash])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw context path property [${leadingSlash}]")
 
         // Act
@@ -760,7 +760,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         List<String> paths = [noLeadingSlash, leadingSlash, leadingAndTrailingSlash]
         String combinedPaths = paths.join(",")
         Properties rawProps = new Properties(["nifi.web.proxy.context.path": combinedPaths])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw context path property [${noLeadingSlash}]")
 
         // Act
@@ -780,7 +780,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String leadingSlash = "/some/context/path"
         Properties rawProps = new Properties(["nifi.web.proxy.context.path": leadingSlash])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw context path property [${leadingSlash}]")
 
         // Act
@@ -801,7 +801,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         List<String> paths = [noLeadingSlash, leadingSlash, leadingAndTrailingSlash]
         String combinedPaths = paths.join(",")
         Properties rawProps = new Properties(["nifi.web.proxy.context.path": combinedPaths])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw context path property [${noLeadingSlash}]")
 
         // Act
@@ -818,7 +818,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String empty = ""
         Properties rawProps = new Properties(["nifi.web.proxy.context.path": empty])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw context path property [${empty}]")
 
         // Act
@@ -834,7 +834,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String extraSpaceHostname = "somehost.com  "
         Properties rawProps = new Properties(["nifi.web.proxy.host": extraSpaceHostname])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw proxy host property [${extraSpaceHostname}]")
 
         // Act
@@ -851,7 +851,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String hostname = "somehost.com"
         Properties rawProps = new Properties(["nifi.web.proxy.host": hostname])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw proxy host property [${hostname}]")
 
         // Act
@@ -872,7 +872,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         List<String> hosts = [extraSpaceHostname, normalHostname, hostnameWithPort, extraSpaceHostnameWithPort]
         String combinedHosts = hosts.join(",")
         Properties rawProps = new Properties(["nifi.web.proxy.host": combinedHosts])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw proxy host property [${combinedHosts}]")
 
         // Act
@@ -893,7 +893,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String normalHostname = "someotherhost.com"
         Properties rawProps = new Properties(["nifi.web.proxy.host": normalHostname])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw proxy host property [${normalHostname}]")
 
         // Act
@@ -915,7 +915,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         List<String> hosts = [extraSpaceHostname, normalHostname, hostnameWithPort, extraSpaceHostnameWithPort]
         String combinedHosts = hosts.join(",")
         Properties rawProps = new Properties(["nifi.web.proxy.host": combinedHosts])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw proxy host property [${combinedHosts}]")
 
         // Act
@@ -932,7 +932,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String empty = ""
         Properties rawProps = new Properties(["nifi.web.proxy.host": empty])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw proxy host property [${empty}]")
 
         // Act
@@ -948,7 +948,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
         String empty = ""
         Properties rawProps = new Properties(["nifi.web.proxy.host": empty])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with raw proxy host property [${empty}]")
 
         // Act
@@ -997,7 +997,7 @@ class StandardNiFiPropertiesGroovyTest extends GroovyTestCase {
     void testWebMaxContentSizeShouldDefaultToEmpty() {
         // Arrange
         Properties rawProps = new Properties(["nifi.web.max.content.size": ""])
-        NiFiProperties props = new StandardNiFiProperties(rawProps)
+        NiFiProperties props = new NiFiProperties(rawProps)
         logger.info("Created a NiFiProperties instance with empty web max content size property")
 
         // Act
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy
index ef9ff4b..b73a970 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/NiFiPropertiesLoaderGroovyTest.groovy
@@ -17,6 +17,7 @@
 package org.apache.nifi.properties
 
 import org.apache.commons.lang3.SystemUtils
+import org.apache.nifi.util.NiFiBootstrapUtils
 import org.apache.nifi.util.NiFiProperties
 import org.apache.nifi.util.file.FileUtils
 import org.bouncycastle.jce.provider.BouncyCastleProvider
@@ -89,7 +90,6 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
 //        if (ProtectedNiFiProperties.@localProviderCache) {
 //            ProtectedNiFiProperties.@localProviderCache = [:]
 //        }
-        NiFiPropertiesLoader.@sensitivePropertyProviderFactory = null
     }
 
     @AfterClass
@@ -124,31 +124,6 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
     }
 
     @Test
-    void testShouldGetDefaultProviderKey() throws Exception {
-        // Arrange
-        final String EXPECTED_PROVIDER_KEY = "aes/gcm/${Cipher.getMaxAllowedKeyLength("AES") > 128 ? 256 : 128}"
-        logger.info("Expected provider key: ${EXPECTED_PROVIDER_KEY}")
-
-        // Act
-        String defaultKey = NiFiPropertiesLoader.getDefaultProviderKey()
-        logger.info("Default key: ${defaultKey}")
-        // Assert
-        assert defaultKey == EXPECTED_PROVIDER_KEY
-    }
-
-    @Test
-    void testShouldInitializeSensitivePropertyProviderFactory() throws Exception {
-        // Arrange
-        NiFiPropertiesLoader niFiPropertiesLoader = new NiFiPropertiesLoader()
-
-        // Act
-        niFiPropertiesLoader.initializeSensitivePropertyProviderFactory()
-
-        // Assert
-        assert niFiPropertiesLoader.@sensitivePropertyProviderFactory
-    }
-
-    @Test
     void testShouldLoadUnprotectedPropertiesFromFile() throws Exception {
         // Arrange
         File unprotectedFile = new File("src/test/resources/conf/nifi.properties")
@@ -161,7 +136,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         assert niFiProperties.size() > 0
 
         // Ensure it is not a ProtectedNiFiProperties
-        assert niFiProperties instanceof StandardNiFiProperties
+        assert !(niFiProperties instanceof ProtectedNiFiProperties)
     }
 
     @Test
@@ -180,7 +155,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         assert niFiProperties.size() > 0
 
         // Ensure it is not a ProtectedNiFiProperties
-        assert niFiProperties instanceof StandardNiFiProperties
+        assert !(niFiProperties instanceof ProtectedNiFiProperties)
     }
 
     @Test
@@ -193,13 +168,13 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         logger.info("Set ${NiFiProperties.PROPERTIES_FILE_PATH} to ${protectedFile.absolutePath}")
 
         // Act
-        def msg = shouldFail(IOException) {
+        def msg = shouldFail(SensitivePropertyProtectionException) {
             NiFiProperties niFiProperties = NiFiPropertiesLoader.loadDefaultWithKeyFromBootstrap()
         }
         logger.expected(msg)
 
         // Assert
-        assert msg =~ "Cannot read from bootstrap.conf"
+        assert msg =~ "Could not read root key from bootstrap.conf"
     }
 
     @Test
@@ -308,7 +283,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         assert niFiProperties.size() > 0
 
         // Ensure it is not a ProtectedNiFiProperties
-        assert niFiProperties instanceof StandardNiFiProperties
+        assert !(niFiProperties instanceof ProtectedNiFiProperties)
     }
 
     @Test
@@ -348,7 +323,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         }
 
         // Ensure it is not a ProtectedNiFiProperties
-        assert niFiProperties instanceof StandardNiFiProperties
+        assert !(niFiProperties instanceof ProtectedNiFiProperties)
     }
 
     @Test
@@ -358,7 +333,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath)
 
         // Act
-        String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+        String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile()
 
         // Assert
         assert key == KEY_HEX
@@ -371,7 +346,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath)
 
         // Act
-        String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+        String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile()
 
         // Assert
         assert key == ""
@@ -384,7 +359,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
         System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, defaultNiFiPropertiesFilePath)
 
         // Act
-        String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+        String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile()
 
         // Assert
         assert key == ""
@@ -398,7 +373,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
 
         // Act
         def msg = shouldFail(IOException) {
-            String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+            String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile()
         }
         logger.expected(msg)
 
@@ -419,7 +394,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
 
         // Act
         def msg = shouldFail(IOException) {
-            String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+            String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile()
         }
         logger.expected(msg)
 
@@ -444,7 +419,7 @@ class NiFiPropertiesLoaderGroovyTest extends GroovyTestCase {
 
         // Act
         def msg = shouldFail(IOException) {
-            String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile()
+            String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile()
         }
         logger.expected(msg)
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy
index 034c6ca..f902dbc 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/ProtectedNiFiPropertiesGroovyTest.groovy
@@ -55,6 +55,9 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
 
     private static String originalPropertiesPath = System.getProperty(NiFiProperties.PROPERTIES_FILE_PATH)
 
+    private static SensitivePropertyProviderFactory sensitivePropertyProviderFactory =
+            StandardSensitivePropertyProviderFactory.withKey(KEY_HEX)
+
     @BeforeClass
     static void setUpOnce() throws Exception {
         Assume.assumeTrue("Test only runs on *nix", !SystemUtils.IS_OS_WINDOWS)
@@ -85,8 +88,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         try {
             filePath = ProtectedNiFiPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath()
         } catch (URISyntaxException ex) {
-            throw new RuntimeException("Cannot load properties file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load properties file due to " + ex.getLocalizedMessage(), ex)
         }
 
         File file = new File(filePath)
@@ -109,14 +111,14 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
 
             // If it has protected keys, inject the SPP
             if (protectedNiFiProperties.hasProtectedKeys()) {
-                protectedNiFiProperties.addSensitivePropertyProvider(new AESSensitivePropertyProvider(KEY_HEX))
+                protectedNiFiProperties.addSensitivePropertyProvider(sensitivePropertyProviderFactory
+                        .getProvider(PropertyProtectionScheme.AES_GCM))
             }
 
             return protectedNiFiProperties
         } catch (final Exception ex) {
             logger.error("Cannot load properties file due to " + ex.getLocalizedMessage())
-            throw new RuntimeException("Cannot load properties file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load properties file due to " + ex.getLocalizedMessage(), ex)
         } finally {
             if (null != inStream) {
                 try {
@@ -135,7 +137,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
 
         // Act
-        NiFiProperties niFiProperties = new StandardNiFiProperties()
+        NiFiProperties niFiProperties = new NiFiProperties()
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Assert
@@ -152,7 +154,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         assert rawProperties.size() == 1
 
         // Act
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
 
         // Assert
@@ -166,7 +168,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         Properties rawProperties = new Properties()
         rawProperties.setProperty("key", "value")
         rawProperties.setProperty("key.protected", "value2")
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
         assert niFiProperties.size() == 2
 
@@ -190,9 +192,9 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         assert rawProperties.size() == 1
 
         // Act
-        NiFiProperties niFiProperties = new StandardNiFiProperties(rawProperties)
+        NiFiProperties niFiProperties = new NiFiProperties(rawProperties)
         logger.info("niFiProperties has ${niFiProperties.size()} properties: ${niFiProperties.getPropertyKeys()}")
-        NiFiProperties emptyProperties = new StandardNiFiProperties()
+        NiFiProperties emptyProperties = new NiFiProperties()
         logger.info("emptyProperties has ${emptyProperties.size()} properties: ${emptyProperties.getPropertyKeys()}")
 
         // Assert
@@ -360,7 +362,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
      * @throws Exception
      */
     @Test
-    void testGetValueOfSensitivePropertyShouldHandleUnknownProtectionScheme() throws Exception {
+    void testGetValueOfSensitivePropertyShouldFailOnUnknownProtectionScheme() throws Exception {
         // Arrange
         final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
 
@@ -375,16 +377,18 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
         boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
 
-        // While the value is "protected", the scheme is not registered, so treat it as raw
+        // While the value is "protected", the scheme is not registered, so throw an exception
         logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
 
         // Act
-        NiFiProperties unprotectedProperties = properties.getUnprotectedProperties()
-        String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
-        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        def msg = shouldFail(IllegalStateException) {
+            NiFiProperties unprotectedProperties = properties.getUnprotectedProperties()
+            String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+            logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        }
 
         // Assert
-        assert retrievedKeystorePassword == RAW_KEYSTORE_PASSWORD
+        assert msg == "No provider available for nifi.sensitive.props.key"
         assert isSensitive
         assert isProtected
     }
@@ -439,7 +443,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         ProtectedNiFiProperties properties = loadFromFile("/conf/nifi_with_sensitive_properties_protected_aes_multiple_malformed.properties")
 
         // Iterate over the protected keys and track the ones that fail to decrypt
-        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX)
+        SensitivePropertyProvider spp = sensitivePropertyProviderFactory.getProvider(PropertyProtectionScheme.AES_GCM)
         Set<String> malformedKeys = properties.getProtectedPropertyKeys()
                 .findAll { String key, String scheme -> scheme == spp.identifierKey }
                 .keySet().collect { String key ->
@@ -538,11 +542,11 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
     // TODO: Test getProtected with multiple providers
 
     /**
-     * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the internal cache of providers is empty.
+     * In the protection enabled scenario, a call to retrieve a sensitive property should fail if the internal cache of providers is empty.
      * @throws Exception
      */
     @Test
-    void testGetValueOfSensitivePropertyShouldHandleInvalidatedInternalCache() throws Exception {
+    void testGetValueOfSensitivePropertyShouldFailOnInvalidatedInternalCache() throws Exception {
         // Arrange
         final String KEYSTORE_PASSWORD_KEY = "nifi.security.keystorePasswd"
         final String EXPECTED_KEYSTORE_PASSWORD = "thisIsABadKeystorePassword"
@@ -553,19 +557,21 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Read raw value from properties: ${RAW_PASSWORD}")
 
         // Overwrite the internal cache
-        properties.localProviderCache = [:]
+        properties.getSensitivePropertyProviders().clear()
 
         boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
         boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
         logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
 
         // Act
-        NiFiProperties unprotectedProperties = properties.getUnprotectedProperties()
-        String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
-        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        def msg = shouldFail(IllegalStateException) {
+            NiFiProperties unprotectedProperties = properties.getUnprotectedProperties()
+            String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+            logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        }
 
         // Assert
-        assert retrievedKeystorePassword == RAW_PASSWORD
+        assert msg == "No provider available for nifi.sensitive.props.key"
         assert isSensitive
         assert isProtected
     }
@@ -614,6 +620,10 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         assert !unprotectedPasswordIsProtected
     }
 
+    private static double getPercentOfSensitivePropertiesProtected(final ProtectedNiFiProperties properties) {
+        return (int) Math.round(properties.getProtectedPropertyKeys().size() / ((double) properties.getPopulatedSensitivePropertyKeys().size()) * 100);
+    }
+
     @Test
     void testShouldGetPercentageOfSensitivePropertiesProtected_0() throws Exception {
         // Arrange
@@ -623,7 +633,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
 
         // Act
-        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        double percentProtected = getPercentOfSensitivePropertiesProtected(properties)
         logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
 
         // Assert
@@ -639,7 +649,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
 
         // Act
-        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        double percentProtected = getPercentOfSensitivePropertiesProtected(properties)
         logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
 
         // Assert
@@ -655,7 +665,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
 
         // Act
-        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        double percentProtected = getPercentOfSensitivePropertiesProtected(properties)
         logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
 
         // Assert
@@ -666,13 +676,13 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
     void testInstanceWithNoProtectedPropertiesShouldNotLoadSPP() throws Exception {
         // Arrange
         ProtectedNiFiProperties properties = loadFromFile("/conf/nifi.properties")
-        assert properties.@localProviderCache?.isEmpty()
+        assert properties.getSensitivePropertyProviders().isEmpty()
 
         logger.info("Has protected properties: ${properties.hasProtectedKeys()}")
         assert !properties.hasProtectedKeys()
 
         // Act
-        Map localCache = properties.@localProviderCache
+        Map localCache = properties.getSensitivePropertyProviders()
         logger.info("Internal cache ${localCache} has ${localCache.size()} providers loaded")
 
         // Assert
@@ -706,7 +716,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         assert properties.getSensitivePropertyProviders().isEmpty()
 
         // Act
-        def msg = shouldFail(IllegalArgumentException) {
+        def msg = shouldFail(NullPointerException) {
             properties.addSensitivePropertyProvider(null)
         }
         logger.expected(msg)
@@ -756,7 +766,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         ProtectedNiFiProperties protectedNiFiProperties = loadFromFile(noProtectedPropertiesPath)
         logger.info("Loaded ${protectedNiFiProperties.size()} properties from ${noProtectedPropertiesPath}")
 
-        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        int hashCode = protectedNiFiProperties.getApplicationProperties().hashCode()
         logger.info("Hash code of internal instance: ${hashCode}")
 
         // Act
@@ -766,7 +776,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Assert
         assert unprotectedNiFiProperties.size() == protectedNiFiProperties.size()
         assert unprotectedNiFiProperties.getPropertyKeys().every {
-            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+            !unprotectedNiFiProperties.getProperty(it).endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)
         }
         logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
         assert unprotectedNiFiProperties.hashCode() == hashCode
@@ -781,7 +791,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
 
         int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size()
         int protectionSchemeCount = protectedNiFiProperties
-                .getPropertyKeys().findAll { it.endsWith(".protected") }
+                .getPropertyKeys().findAll { it.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) }
                 .size()
         int expectedUnprotectedPropertyCount = protectedNiFiProperties.size() - protectionSchemeCount
 
@@ -797,7 +807,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
 
         logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}")
 
-        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        int hashCode = protectedNiFiProperties.getApplicationProperties().hashCode()
         logger.info("Hash code of internal instance: ${hashCode}")
 
         // Act
@@ -807,7 +817,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Assert
         assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount
         assert unprotectedNiFiProperties.getPropertyKeys().every {
-            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+            !unprotectedNiFiProperties.getProperty(it).endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)
         }
         logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
         assert unprotectedNiFiProperties.hashCode() != hashCode
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/test/groovy/org/apache/nifi/NiFiGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/test/groovy/org/apache/nifi/NiFiGroovyTest.groovy
index 69dc5b0..5dda4e1 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/test/groovy/org/apache/nifi/NiFiGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-runtime/src/test/groovy/org/apache/nifi/NiFiGroovyTest.groovy
@@ -18,9 +18,11 @@ package org.apache.nifi
 
 import ch.qos.logback.classic.spi.LoggingEvent
 import ch.qos.logback.core.AppenderBase
-import org.apache.nifi.properties.AESSensitivePropertyProvider
+import org.apache.nifi.properties.ApplicationPropertiesProtector
 import org.apache.nifi.properties.NiFiPropertiesLoader
-import org.apache.nifi.properties.StandardNiFiProperties
+import org.apache.nifi.properties.PropertyProtectionScheme
+import org.apache.nifi.properties.SensitivePropertyProvider
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory
 import org.apache.nifi.util.NiFiProperties
 import org.bouncycastle.jce.provider.BouncyCastleProvider
 import org.junit.After
@@ -34,7 +36,6 @@ import org.slf4j.LoggerFactory
 import org.slf4j.bridge.SLF4JBridgeHandler
 
 import javax.crypto.Cipher
-import java.nio.file.Paths
 import java.security.Security
 
 @RunWith(JUnit4.class)
@@ -64,7 +65,6 @@ class NiFiGroovyTest extends GroovyTestCase {
 
     @After
     void tearDown() throws Exception {
-        NiFiPropertiesLoader.@sensitivePropertyProviderFactory = null
         TestAppender.reset()
         System.setIn(System.in)
     }
@@ -166,7 +166,7 @@ class NiFiGroovyTest extends GroovyTestCase {
         System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, testPropertiesPath)
 
         def protectedNiFiProperties = new NiFiPropertiesLoader().readProtectedPropertiesFromDisk(new File(testPropertiesPath))
-        NiFiProperties unprocessedProperties = protectedNiFiProperties.internalNiFiProperties
+        NiFiProperties unprocessedProperties = protectedNiFiProperties.getApplicationProperties()
         def protectedKeys = getProtectedKeys(unprocessedProperties)
         logger.info("Reading from raw properties file gives protected properties: ${protectedKeys}")
 
@@ -198,29 +198,30 @@ class NiFiGroovyTest extends GroovyTestCase {
     }
 
     private static boolean hasProtectedKeys(NiFiProperties properties) {
-        properties.getPropertyKeys().any { it.endsWith(".protected") }
+        properties.getPropertyKeys().any { it.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) }
     }
 
     private static Map<String, String> getProtectedPropertyKeys(NiFiProperties properties) {
         getProtectedKeys(properties).collectEntries { String key ->
-            [(key): properties.getProperty(key + ".protected")]
+            [(key): properties.getProperty(key + ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)]
         }
     }
 
     private static Set<String> getProtectedKeys(NiFiProperties properties) {
-        properties.getPropertyKeys().findAll { it.endsWith(".protected") }.collect { it - ".protected" }
+        properties.getPropertyKeys().findAll { it.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) }.collect { it - ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX }
     }
 
     private static NiFiProperties decrypt(NiFiProperties encryptedProperties, String keyHex) {
-        AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(keyHex)
+        SensitivePropertyProvider spp = StandardSensitivePropertyProviderFactory.withKey(keyHex)
+                .getProvider(PropertyProtectionScheme.AES_GCM)
         def map = encryptedProperties.getPropertyKeys().collectEntries { String key ->
-            if (encryptedProperties.getProperty(key + ".protected") == spp.getIdentifierKey()) {
+            if (encryptedProperties.getProperty(key + ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) == spp.getIdentifierKey()) {
                 [(key): spp.unprotect(encryptedProperties.getProperty(key))]
-            } else if (!key.endsWith(".protected")) {
+            } else if (!key.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)) {
                 [(key): encryptedProperties.getProperty(key)]
             }
         }
-        new StandardNiFiProperties(map as Properties)
+        new NiFiProperties(map as Properties)
     }
 
     @Test
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/groovy/org/apache/nifi/remote/StandardPublicPortGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/groovy/org/apache/nifi/remote/StandardPublicPortGroovyTest.groovy
index e9e9b6c..3a09a78 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/groovy/org/apache/nifi/remote/StandardPublicPortGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/groovy/org/apache/nifi/remote/StandardPublicPortGroovyTest.groovy
@@ -16,12 +16,10 @@
  */
 package org.apache.nifi.remote
 
-
 import org.apache.nifi.authorization.Authorizer
 import org.apache.nifi.connectable.Connectable
 import org.apache.nifi.connectable.ConnectableType
 import org.apache.nifi.controller.ProcessScheduler
-import org.apache.nifi.properties.StandardNiFiProperties
 import org.apache.nifi.remote.protocol.CommunicationsSession
 import org.apache.nifi.remote.protocol.ServerProtocol
 import org.apache.nifi.reporting.BulletinRepository
@@ -87,7 +85,7 @@ class StandardPublicPortGroovyTest extends GroovyTestCase {
                     logger.mock("getProperty(${prop}) -> ${value}")
                     value
                 },
-        ] as StandardNiFiProperties
+        ] as NiFiProperties
 
         StandardPublicPort port = createPublicPort(mockProps)
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestHttpRemoteSiteListener.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestHttpRemoteSiteListener.java
index fbad70f..099efab 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestHttpRemoteSiteListener.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestHttpRemoteSiteListener.java
@@ -22,7 +22,6 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import org.apache.nifi.processor.ProcessSession;
-import org.apache.nifi.properties.StandardNiFiProperties;
 import org.apache.nifi.remote.protocol.FlowFileTransaction;
 import org.apache.nifi.remote.protocol.HandshakeProperties;
 import org.apache.nifi.util.NiFiProperties;
@@ -40,7 +39,7 @@ public class TestHttpRemoteSiteListener {
 
     @Test
     public void testNormalTransactionProgress() {
-        HttpRemoteSiteListener transactionManager = HttpRemoteSiteListener.getInstance(new StandardNiFiProperties());
+        HttpRemoteSiteListener transactionManager = HttpRemoteSiteListener.getInstance(new NiFiProperties());
         String transactionId = transactionManager.createTransaction();
 
         assertTrue("Transaction should be active.", transactionManager.isTransactionActive(transactionId));
@@ -60,7 +59,7 @@ public class TestHttpRemoteSiteListener {
 
     @Test
     public void testDuplicatedTransactionId() {
-        HttpRemoteSiteListener transactionManager = HttpRemoteSiteListener.getInstance(new StandardNiFiProperties());
+        HttpRemoteSiteListener transactionManager = HttpRemoteSiteListener.getInstance(new NiFiProperties());
         String transactionId = transactionManager.createTransaction();
 
         assertTrue("Transaction should be active.", transactionManager.isTransactionActive(transactionId));
@@ -79,7 +78,7 @@ public class TestHttpRemoteSiteListener {
 
     @Test
     public void testNoneExistingTransaction() {
-        HttpRemoteSiteListener transactionManager = HttpRemoteSiteListener.getInstance(new StandardNiFiProperties());
+        HttpRemoteSiteListener transactionManager = HttpRemoteSiteListener.getInstance(new NiFiProperties());
 
         String transactionId = "does-not-exist-1";
         assertFalse("Transaction should not be active.", transactionManager.isTransactionActive(transactionId));
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestPeerDescriptionModifier.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestPeerDescriptionModifier.java
index cf6639f..122cc3f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestPeerDescriptionModifier.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/TestPeerDescriptionModifier.java
@@ -17,7 +17,6 @@
 package org.apache.nifi.remote;
 
 import org.apache.nifi.attribute.expression.language.exception.AttributeExpressionLanguageException;
-import org.apache.nifi.properties.StandardNiFiProperties;
 import org.apache.nifi.remote.protocol.SiteToSiteTransportProtocol;
 import org.apache.nifi.util.NiFiProperties;
 import org.junit.Test;
@@ -37,7 +36,7 @@ public class TestPeerDescriptionModifier {
     @Test
     public void testNoConfiguration() {
         Properties props = new Properties();
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         final PeerDescriptionModifier modifier = new PeerDescriptionModifier(properties);
         assertFalse(modifier.isModificationNeeded(SiteToSiteTransportProtocol.RAW));
         assertFalse(modifier.isModificationNeeded(SiteToSiteTransportProtocol.HTTP));
@@ -47,7 +46,7 @@ public class TestPeerDescriptionModifier {
     public void testInvalidNoHostname() {
         Properties props = new Properties();
         props.put("nifi.remote.route.raw.no-host.when", "true");
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         try {
             new PeerDescriptionModifier(properties);
             fail("Should throw an Exception");
@@ -61,7 +60,7 @@ public class TestPeerDescriptionModifier {
         Properties props = new Properties();
         props.put("nifi.remote.route.raw.no-port.when", "true");
         props.put("nifi.remote.route.raw.no-port.hostname", "proxy.example.com");
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         try {
             new PeerDescriptionModifier(properties);
             fail("Should throw an Exception");
@@ -78,7 +77,7 @@ public class TestPeerDescriptionModifier {
         props.put("nifi.remote.route.raw.invalid-name.port", "8081");
         props.put("nifi.remote.route.raw.invalid-name.secure", "true");
         props.put("nifi.remote.route.raw.invalid-name.unsupported", "true");
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         try {
             new PeerDescriptionModifier(properties);
             fail("Should throw an Exception");
@@ -93,7 +92,7 @@ public class TestPeerDescriptionModifier {
     public void testInvalidPropertyKeyNoProtocol() {
         Properties props = new Properties();
         props.put("nifi.remote.route.", "true");
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         try {
             new PeerDescriptionModifier(properties);
             fail("Should throw an Exception");
@@ -108,7 +107,7 @@ public class TestPeerDescriptionModifier {
     public void testInvalidPropertyKeyNoName() {
         Properties props = new Properties();
         props.put("nifi.remote.route.http.", "true");
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         try {
             new PeerDescriptionModifier(properties);
             fail("Should throw an Exception");
@@ -125,7 +124,7 @@ public class TestPeerDescriptionModifier {
         props.put("nifi.remote.route.raw.invalid-el.when", "${nonExistingFunction()}");
         props.put("nifi.remote.route.raw.invalid-el.hostname", "proxy.example.com");
         props.put("nifi.remote.route.raw.invalid-el.port", "8081");
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         final PeerDescriptionModifier modifier = new PeerDescriptionModifier(properties);
 
         final PeerDescription source = new PeerDescription("client", 12345, true);
@@ -146,7 +145,7 @@ public class TestPeerDescriptionModifier {
         props.put("nifi.remote.route.raw.no-port.when", "true");
         props.put("nifi.remote.route.raw.no-port.hostname", "proxy.example.com");
         props.put("nifi.remote.route.raw.no-port.port", "8443");
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         final PeerDescriptionModifier modifier = new PeerDescriptionModifier(properties);
 
         final PeerDescription source = new PeerDescription("client", 12345, true);
@@ -178,7 +177,7 @@ public class TestPeerDescriptionModifier {
         props.put("nifi.remote.input.socket.port", "8081");
         props.put("nifi.remote.input.http.enabled", "true");
 
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         final PeerDescriptionModifier modifier = new PeerDescriptionModifier(properties);
 
         // For requests coming from the proxy server, modify target description,
@@ -230,7 +229,7 @@ public class TestPeerDescriptionModifier {
         props.put("nifi.remote.input.socket.port", "8081");
         props.put("nifi.remote.input.http.enabled", "true");
 
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         final PeerDescriptionModifier modifier = new PeerDescriptionModifier(properties);
 
         // For requests coming from the proxy server, modify target description,
@@ -284,7 +283,7 @@ public class TestPeerDescriptionModifier {
         props.put("nifi.remote.input.http.enabled", "true");
 
 
-        final NiFiProperties properties = new StandardNiFiProperties(props);
+        final NiFiProperties properties = new NiFiProperties(props);
         final PeerDescriptionModifier modifier = new PeerDescriptionModifier(properties);
 
         // For requests coming from the proxy server, modify target description,
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/protocol/http/TestHttpFlowFileServerProtocol.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/protocol/http/TestHttpFlowFileServerProtocol.java
index 529dfe6..e63e901 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/protocol/http/TestHttpFlowFileServerProtocol.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-site-to-site/src/test/java/org/apache/nifi/remote/protocol/http/TestHttpFlowFileServerProtocol.java
@@ -25,7 +25,6 @@ import org.apache.nifi.processor.ProcessContext;
 import org.apache.nifi.processor.ProcessSession;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.processor.Relationship;
-import org.apache.nifi.properties.StandardNiFiProperties;
 import org.apache.nifi.provenance.ProvenanceEventRecord;
 import org.apache.nifi.provenance.ProvenanceEventType;
 import org.apache.nifi.remote.HttpRemoteSiteListener;
@@ -109,7 +108,7 @@ public class TestHttpFlowFileServerProtocol {
 
     private HttpFlowFileServerProtocol getDefaultHttpFlowFileServerProtocol() {
         final StandardVersionNegotiator versionNegotiator = new StandardVersionNegotiator(5, 4, 3, 2, 1);
-        return new StandardHttpFlowFileServerProtocol(versionNegotiator, new StandardNiFiProperties());
+        return new StandardHttpFlowFileServerProtocol(versionNegotiator, new NiFiProperties());
     }
 
     @Test
@@ -367,7 +366,7 @@ public class TestHttpFlowFileServerProtocol {
             sessionState.getFlowFileQueue().offer(flowFile);
         }
 
-        final HttpRemoteSiteListener remoteSiteListener = HttpRemoteSiteListener.getInstance(new StandardNiFiProperties());
+        final HttpRemoteSiteListener remoteSiteListener = HttpRemoteSiteListener.getInstance(new NiFiProperties());
 
         serverProtocol.handshake(peer);
         assertTrue(serverProtocol.isHandshakeSuccessful());
@@ -522,7 +521,7 @@ public class TestHttpFlowFileServerProtocol {
     }
 
     private void receiveFlowFiles(final HttpFlowFileServerProtocol serverProtocol, final String transactionId, final Peer peer, final DataPacket ... dataPackets) throws IOException {
-        final HttpRemoteSiteListener remoteSiteListener = HttpRemoteSiteListener.getInstance(new StandardNiFiProperties());
+        final HttpRemoteSiteListener remoteSiteListener = HttpRemoteSiteListener.getInstance(new NiFiProperties());
         final HttpServerCommunicationsSession commsSession = (HttpServerCommunicationsSession) peer.getCommunicationsSession();
 
         serverProtocol.handshake(peer);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy
index 6842e4c..d00ed76 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/groovy/org/apache/nifi/web/server/JettyServerGroovyTest.groovy
@@ -23,7 +23,6 @@ import org.apache.nifi.nar.ExtensionManagerHolder
 import org.apache.nifi.nar.ExtensionMapping
 import org.apache.nifi.nar.SystemBundle
 import org.apache.nifi.processor.DataUnit
-import org.apache.nifi.properties.StandardNiFiProperties
 import org.apache.nifi.security.util.StandardTlsConfiguration
 import org.apache.nifi.security.util.TlsConfiguration
 import org.apache.nifi.security.util.TlsPlatform
@@ -50,6 +49,11 @@ import org.junit.contrib.java.lang.system.SystemErrRule
 import org.junit.contrib.java.lang.system.SystemOutRule
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 
@@ -87,7 +91,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
     // These protocol versions should not ever be supported
     static private final List<String> LEGACY_TLS_PROTOCOLS = ["TLS", "TLSv1", "TLSv1.1", "SSL", "SSLv2", "SSLv2Hello", "SSLv3"]
 
-    NiFiProperties httpsProps = new StandardNiFiProperties(rawProperties: new Properties([
+    NiFiProperties httpsProps = new NiFiProperties(new Properties([
             (NiFiProperties.WEB_HTTPS_PORT)            : HTTPS_PORT as String,
             (NiFiProperties.WEB_HTTPS_HOST)            : HTTPS_HOSTNAME,
             (NiFiProperties.SECURITY_KEYSTORE)         : KEYSTORE_PATH,
@@ -134,15 +138,16 @@ class JettyServerGroovyTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_HOST): "secure.host.com",
                 (NiFiProperties.WEB_THREADS)   : NiFiProperties.DEFAULT_WEB_THREADS
         ]
-        NiFiProperties mockProps = [
-                getPort    : { -> 8080 },
-                getSslPort : { -> 8443 },
-                getProperty: { String prop ->
-                    String value = badProps[prop] ?: "no_value"
-                    logger.mock("getProperty(${prop}) -> ${value}")
-                    value
-                },
-        ] as StandardNiFiProperties
+        NiFiProperties mockProps = Mockito.mock(NiFiProperties.class)
+        Mockito.when(mockProps.getPort()).thenReturn(8080)
+        Mockito.when(mockProps.getSslPort()).thenReturn(8443)
+
+        Mockito.when(mockProps.getProperty(ArgumentMatchers.anyString())).thenAnswer(new Answer<Object>() {
+            @Override
+            Object answer(InvocationOnMock invocation) throws Throwable {
+                badProps[(String) invocation.getArgument(0)] ?: "no_value"
+            }
+        })
 
         // Act
         boolean bothConfigsPresent = JettyServer.bothHttpAndHttpsConnectorsConfigured(mockProps)
@@ -170,7 +175,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
                     logger.mock("getProperty(${prop}) -> ${value}")
                     value
                 },
-        ] as StandardNiFiProperties
+        ] as NiFiProperties
 
         Map httpsMap = [
                 (NiFiProperties.WEB_HTTP_HOST) : null,
@@ -184,7 +189,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
                     logger.mock("getProperty(${prop}) -> ${value}")
                     value
                 },
-        ] as StandardNiFiProperties
+        ] as NiFiProperties
 
         // Act
         boolean bothConfigsPresentForHttp = JettyServer.bothHttpAndHttpsConnectorsConfigured(httpProps)
@@ -221,7 +226,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
                 getWebThreads      : { -> NiFiProperties.DEFAULT_WEB_THREADS },
                 getWebMaxHeaderSize: { -> NiFiProperties.DEFAULT_WEB_MAX_HEADER_SIZE },
                 isHTTPSConfigured  : { -> true }
-        ] as StandardNiFiProperties
+        ] as NiFiProperties
 
         // The web server should fail to start and exit Java
         exit.expectSystemExitWithStatus(1)
@@ -253,7 +258,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
         // Arrange
         final String externalHostname = "localhost"
 
-        NiFiProperties httpsProps = new StandardNiFiProperties(rawProperties: new Properties([
+        NiFiProperties httpsProps = new NiFiProperties(new Properties([
                 (NiFiProperties.WEB_HTTPS_PORT): HTTPS_PORT as String,
                 (NiFiProperties.WEB_HTTPS_HOST): externalHostname,
                 (NiFiProperties.SECURITY_KEYSTORE): "src/test/resources/multiple_cert_keystore.p12",
@@ -285,7 +290,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
         // Arrange
         final String externalHostname = "localhost"
 
-        NiFiProperties httpsProps = new StandardNiFiProperties(rawProperties: new Properties([
+        NiFiProperties httpsProps = new NiFiProperties(new Properties([
                 (NiFiProperties.WEB_HTTPS_PORT): HTTPS_PORT as String,
                 (NiFiProperties.WEB_HTTPS_HOST): externalHostname,
                 (NiFiProperties.SECURITY_KEYSTORE): "src/test/resources/multiple_cert_keystore.jks",
@@ -308,7 +313,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
         jetty.stop()
     }
 
-    private static JettyServer createJettyServer(StandardNiFiProperties httpsProps) {
+    private static JettyServer createJettyServer(NiFiProperties httpsProps) {
         Server internalServer = new Server()
         JettyServer jetty = new JettyServer(internalServer, httpsProps)
         jetty.systemBundle = SystemBundle.create(httpsProps)
@@ -323,7 +328,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
         // Arrange
         final String externalHostname = "localhost"
 
-        NiFiProperties httpsProps = new StandardNiFiProperties(rawProperties: new Properties([
+        NiFiProperties httpsProps = new NiFiProperties(new Properties([
                 (NiFiProperties.WEB_HTTPS_PORT): HTTPS_PORT as String,
                 (NiFiProperties.WEB_HTTPS_HOST): externalHostname,
         ]))
@@ -462,7 +467,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTP_HOST)       : "localhost",
                 (NiFiProperties.WEB_MAX_CONTENT_SIZE): "1 MB",
         ]
-        NiFiProperties mockProps = new StandardNiFiProperties(new Properties(defaultProps))
+        NiFiProperties mockProps = new NiFiProperties(new Properties(defaultProps))
 
         List<FilterHolder> filters = []
         def mockWebContext = [
@@ -505,7 +510,7 @@ class JettyServerGroovyTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTP_PORT): "8080",
                 (NiFiProperties.WEB_HTTP_HOST): "localhost",
         ]
-        NiFiProperties mockProps = new StandardNiFiProperties(new Properties(defaultProps))
+        NiFiProperties mockProps = new NiFiProperties(new Properties(defaultProps))
 
         List<FilterHolder> filters = []
         def mockWebContext = [
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/HostHeaderHandlerTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/HostHeaderHandlerTest.groovy
index a08be9d..80a3c58 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/HostHeaderHandlerTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/test/java/org/apache/nifi/web/server/HostHeaderHandlerTest.groovy
@@ -17,7 +17,7 @@
 package org.apache.nifi.web.server
 
 import org.apache.commons.lang3.StringUtils
-import org.apache.nifi.properties.StandardNiFiProperties
+
 import org.apache.nifi.util.NiFiProperties
 import org.junit.After
 import org.junit.Before
@@ -112,7 +112,7 @@ class HostHeaderHandlerTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_HOST): DEFAULT_HOSTNAME,
                 (NiFiProperties.WEB_HTTPS_PORT): "${DEFAULT_PORT}".toString(),
         ])
-        NiFiProperties simpleProperties = new StandardNiFiProperties(rawProps)
+        NiFiProperties simpleProperties = new NiFiProperties(rawProps)
 
         // Act
         HostHeaderHandler handler = new HostHeaderHandler(simpleProperties)
@@ -141,7 +141,7 @@ class HostHeaderHandlerTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_PORT): "${DEFAULT_PORT}".toString(),
                 (NiFiProperties.WEB_PROXY_HOST): concatenatedHosts
         ])
-        NiFiProperties simpleProperties = new StandardNiFiProperties(rawProps)
+        NiFiProperties simpleProperties = new NiFiProperties(rawProps)
 
         HostHeaderHandler handler = new HostHeaderHandler(simpleProperties)
         logger.info("Handler: ${handler}")
@@ -190,7 +190,7 @@ class HostHeaderHandlerTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_PORT): "${DEFAULT_PORT}".toString(),
                 (NiFiProperties.WEB_PROXY_HOST): concatenatedHosts
         ])
-        NiFiProperties simpleProperties = new StandardNiFiProperties(rawProps)
+        NiFiProperties simpleProperties = new NiFiProperties(rawProps)
 
         HostHeaderHandler handler = new HostHeaderHandler(simpleProperties)
         logger.info("Handler: ${handler}")
@@ -236,7 +236,7 @@ class HostHeaderHandlerTest extends GroovyTestCase {
                 (NiFiProperties.WEB_HTTPS_PORT): "${DEFAULT_PORT}".toString(),
                 (NiFiProperties.WEB_PROXY_HOST): concatenatedHosts
         ])
-        NiFiProperties simpleProperties = new StandardNiFiProperties(rawProps)
+        NiFiProperties simpleProperties = new NiFiProperties(rawProps)
 
         HostHeaderHandler handler = new HostHeaderHandler(simpleProperties)
         logger.info("Handler: ${handler}")
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
index 2d4b496..356b067 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
@@ -16,7 +16,7 @@
  */
 package org.apache.nifi.web.api
 
-import org.apache.nifi.properties.StandardNiFiProperties
+
 import org.apache.nifi.util.NiFiProperties
 import org.glassfish.jersey.uri.internal.JerseyUriBuilder
 import org.junit.After
@@ -109,7 +109,7 @@ class ApplicationResourceTest extends GroovyTestCase {
 
         resource.setHttpServletRequest(mockRequest)
         resource.setUriInfo(mockUriInfo)
-        resource.properties = new StandardNiFiProperties()
+        resource.properties = new NiFiProperties()
 
         resource
     }
@@ -136,7 +136,7 @@ class ApplicationResourceTest extends GroovyTestCase {
         // Arrange
         ApplicationResource resource = buildApplicationResource()
         logger.info("Allowed path(s): ${ALLOWED_PATH}")
-        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
+        NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
         resource.properties = niFiProperties
 
         // Act
@@ -153,7 +153,7 @@ class ApplicationResourceTest extends GroovyTestCase {
         ApplicationResource resource = buildApplicationResource()
         String multipleAllowedPaths = [ALLOWED_PATH, "another/path", "a/third/path"].join(",")
         logger.info("Allowed path(s): ${multipleAllowedPaths}")
-        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
+        NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
         resource.properties = niFiProperties
 
         // Act
@@ -203,7 +203,7 @@ class ApplicationResourceTest extends GroovyTestCase {
         // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
         logger.info("Allowed path(s): ${ALLOWED_PATH}")
-        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
+        NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
         resource.properties = niFiProperties
 
         // Act
@@ -219,7 +219,7 @@ class ApplicationResourceTest extends GroovyTestCase {
         // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
         logger.info("Allowed path(s): ${ALLOWED_PATH}")
-        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
+        NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
         resource.properties = niFiProperties
 
         // Act
@@ -236,7 +236,7 @@ class ApplicationResourceTest extends GroovyTestCase {
         ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
         String multipleAllowedPaths = [ALLOWED_PATH, "another/path", "a/third/path"].join(",")
         logger.info("Allowed path(s): ${multipleAllowedPaths}")
-        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
+        NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
         resource.properties = niFiProperties
 
         // Act
@@ -253,7 +253,7 @@ class ApplicationResourceTest extends GroovyTestCase {
         ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
         String multipleAllowedPaths = [ALLOWED_PATH, "another/path", "a/third/path"].join(",")
         logger.info("Allowed path(s): ${multipleAllowedPaths}")
-        NiFiProperties niFiProperties = new StandardNiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
+        NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
         resource.properties = niFiProperties
 
         // Act
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestServer.java
index 1fdab5d..1ef38b5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestServer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/integration/util/NiFiTestServer.java
@@ -168,7 +168,8 @@ public class NiFiTestServer {
     }
 
     public Client getClient() throws TlsException {
-        return WebUtils.createClient(null, org.apache.nifi.security.util.SslContextFactory.createSslContext(StandardTlsConfiguration.fromNiFiProperties(properties)));
+        return WebUtils.createClient(null, org.apache.nifi.security.util.SslContextFactory.createSslContext(StandardTlsConfiguration
+                .fromNiFiProperties(properties)));
     }
 
     /**
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java
index c6e433a..3e581c2 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBean.java
@@ -16,24 +16,6 @@
  */
 package org.apache.nifi.web.security.spring;
 
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import javax.xml.XMLConstants;
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.JAXBElement;
-import javax.xml.bind.JAXBException;
-import javax.xml.bind.Unmarshaller;
-import javax.xml.stream.XMLStreamReader;
-import javax.xml.transform.stream.StreamSource;
-import javax.xml.validation.Schema;
-import javax.xml.validation.SchemaFactory;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authentication.AuthenticationResponse;
 import org.apache.nifi.authentication.LoginCredentials;
@@ -50,11 +32,9 @@ import org.apache.nifi.authentication.generated.Provider;
 import org.apache.nifi.bundle.Bundle;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
-import org.apache.nifi.properties.AESSensitivePropertyProviderFactory;
+import org.apache.nifi.properties.PropertyProtectionScheme;
 import org.apache.nifi.properties.SensitivePropertyProtectionException;
-import org.apache.nifi.properties.SensitivePropertyProvider;
-import org.apache.nifi.properties.SensitivePropertyProviderFactory;
-import org.apache.nifi.security.kms.CryptoUtils;
+import org.apache.nifi.properties.SensitivePropertyProviderFactoryAware;
 import org.apache.nifi.security.xml.XmlUtils;
 import org.apache.nifi.util.NiFiProperties;
 import org.slf4j.Logger;
@@ -63,18 +43,36 @@ import org.springframework.beans.factory.DisposableBean;
 import org.springframework.beans.factory.FactoryBean;
 import org.xml.sax.SAXException;
 
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 /**
  *
  */
-public class LoginIdentityProviderFactoryBean implements FactoryBean, DisposableBean, LoginIdentityProviderLookup {
+public class LoginIdentityProviderFactoryBean extends SensitivePropertyProviderFactoryAware
+        implements FactoryBean, DisposableBean, LoginIdentityProviderLookup {
 
     private static final Logger logger = LoggerFactory.getLogger(LoginIdentityProviderFactoryBean.class);
     private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/login-identity-providers.xsd";
     private static final String JAXB_GENERATED_PATH = "org.apache.nifi.authentication.generated";
     private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
 
-    private static SensitivePropertyProviderFactory SENSITIVE_PROPERTY_PROVIDER_FACTORY;
-    private static SensitivePropertyProvider SENSITIVE_PROPERTY_PROVIDER;
+    private NiFiProperties properties;
 
     /**
      * Load the JAXBContext.
@@ -87,11 +85,14 @@ public class LoginIdentityProviderFactoryBean implements FactoryBean, Disposable
         }
     }
 
-    private NiFiProperties properties;
     private ExtensionManager extensionManager;
     private LoginIdentityProvider loginIdentityProvider;
     private final Map<String, LoginIdentityProvider> loginIdentityProviders = new HashMap<>();
 
+    public void setProperties(NiFiProperties properties) {
+        this.properties = properties;
+    }
+
     @Override
     public LoginIdentityProvider getLoginIdentityProvider(String identifier) {
         return loginIdentityProviders.get(identifier);
@@ -218,26 +219,9 @@ public class LoginIdentityProviderFactoryBean implements FactoryBean, Disposable
         return new StandardLoginIdentityProviderConfigurationContext(provider.getIdentifier(), providerProperties);
     }
 
-    private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException {
-            initializeSensitivePropertyProvider(encryptionScheme);
-        return SENSITIVE_PROPERTY_PROVIDER.unprotect(cipherText);
-    }
-
-    private static void initializeSensitivePropertyProvider(String encryptionScheme) throws SensitivePropertyProtectionException {
-        if (SENSITIVE_PROPERTY_PROVIDER == null || !SENSITIVE_PROPERTY_PROVIDER.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) {
-            try {
-                String keyHex = getRootKey();
-                SENSITIVE_PROPERTY_PROVIDER_FACTORY = new AESSensitivePropertyProviderFactory(keyHex);
-                SENSITIVE_PROPERTY_PROVIDER = SENSITIVE_PROPERTY_PROVIDER_FACTORY.getProvider();
-            } catch (IOException e) {
-                logger.error("Error extracting master key from bootstrap.conf for login identity provider decryption", e);
-                throw new SensitivePropertyProtectionException("Could not read root key from bootstrap.conf");
-            }
-        }
-    }
-
-    private static String getRootKey() throws IOException {
-        return CryptoUtils.extractKeyFromBootstrapFile();
+    private String decryptValue(final String cipherText, final String protectionScheme) throws SensitivePropertyProtectionException {
+        return getSensitivePropertyProviderFactory().getProvider(PropertyProtectionScheme.fromIdentifier(protectionScheme))
+                .unprotect(cipherText);
     }
 
     private void performMethodInjection(final LoginIdentityProvider instance, final Class loginIdentityProviderClass)
@@ -356,10 +340,6 @@ public class LoginIdentityProviderFactoryBean implements FactoryBean, Disposable
         }
     }
 
-    public void setProperties(NiFiProperties properties) {
-        this.properties = properties;
-    }
-
     public void setExtensionManager(ExtensionManager extensionManager) {
         this.extensionManager = extensionManager;
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy
index 5480ad4..011788f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/OidcServiceGroovyTest.groovy
@@ -45,16 +45,15 @@ class OidcServiceGroovyTest extends GroovyTestCase {
 
     private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
     private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
-            isOidcEnabled                 : true,
-            getOidcDiscoveryUrl           : "https://localhost/oidc",
-            isLoginIdentityProviderEnabled: false,
-            isKnoxSsoEnabled              : false,
-            getOidcConnectTimeout         : "1000",
-            getOidcReadTimeout            : "1000",
-            getOidcClientId               : "expected_client_id",
-            getOidcClientSecret           : "expected_client_secret",
-            getOidcClaimIdentifyingUser   : "username",
-            getOidcPreferredJwsAlgorithm  : ""
+            "nifi.security.user.oidc.discovery.url"           : "https://localhost/oidc",
+            "nifi.security.user.login.identity.provider"      : "provider",
+            "nifi.security.user.knox.url"                     : "url",
+            "nifi.security.user.oidc.connect.timeout"         : "1000",
+            "nifi.security.user.oidc.read.timeout"            : "1000",
+            "nifi.security.user.oidc.client.id"               : "expected_client_id",
+            "nifi.security.user.oidc.client.secret"           : "expected_client_secret",
+            "nifi.security.user.oidc.claim.identifying.user"  : "username",
+            "nifi.security.user.oidc.preferred.jwsalgorithm"  : ""
     ]
 
     // Mock collaborators
@@ -89,10 +88,7 @@ class OidcServiceGroovyTest extends GroovyTestCase {
 
     private static NiFiProperties buildNiFiProperties(Map<String, Object> props = [:]) {
         def combinedProps = DEFAULT_NIFI_PROPERTIES + props
-        def mockNFP = combinedProps.collectEntries { String k, def v ->
-            [k, { -> return v }]
-        }
-        mockNFP as NiFiProperties
+        new NiFiProperties(combinedProps)
     }
 
     private static JwtService buildJwtService() {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
index c52d4cd..089f28a 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/oidc/StandardOidcIdentityProviderGroovyTest.groovy
@@ -68,17 +68,15 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
 
     private static final Key SIGNING_KEY = new Key(id: 1, identity: "signingKey", key: "mock-signing-key-value")
     private static final Map<String, Object> DEFAULT_NIFI_PROPERTIES = [
-//            isOidcEnabled                 : false,
-            isOidcEnabled                 : true,
-            getOidcDiscoveryUrl           : "https://localhost/oidc",
-            isLoginIdentityProviderEnabled: false,
-            isKnoxSsoEnabled              : false,
-            getOidcConnectTimeout         : 1000,
-            getOidcReadTimeout            : 1000,
-            getOidcClientId               : "expected_client_id",
-            getOidcClientSecret           : "expected_client_secret",
-            getOidcClaimIdentifyingUser   : "username",
-            getOidcPreferredJwsAlgorithm  : ""
+            "nifi.security.user.oidc.discovery.url"           : "https://localhost/oidc",
+            "nifi.security.user.login.identity.provider"      : "provider",
+            "nifi.security.user.knox.url"                     : "url",
+            "nifi.security.user.oidc.connect.timeout"         : "1000",
+            "nifi.security.user.oidc.read.timeout"            : "1000",
+            "nifi.security.user.oidc.client.id"               : "expected_client_id",
+            "nifi.security.user.oidc.client.secret"           : "expected_client_secret",
+            "nifi.security.user.oidc.claim.identifying.user"  : "username",
+            "nifi.security.user.oidc.preferred.jwsalgorithm"  : ""
     ]
 
     // Mock collaborators
@@ -108,10 +106,7 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
 
     private static NiFiProperties buildNiFiProperties(Map<String, Object> props = [:]) {
         def combinedProps = DEFAULT_NIFI_PROPERTIES + props
-        def mockNFP = combinedProps.collectEntries { String k, def v ->
-            [k, { -> return v }]
-        }
-        mockNFP as NiFiProperties
+        new NiFiProperties(combinedProps)
     }
 
     private static JwtService buildJwtService() {
@@ -414,7 +409,9 @@ class StandardOidcIdentityProviderGroovyTest extends GroovyTestCase {
     @Test
     void testConvertOIDCTokenToLoginAuthenticationTokenShouldHandleNoEmailClaimHasFallbackClaims() {
         // Arrange
-        StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(["getOidcClaimIdentifyingUser": "email", "getOidcFallbackClaimsIdentifyingUser": ["upn"] ])
+        StandardOidcIdentityProvider soip = buildIdentityProviderWithMockTokenValidator(
+                ["nifi.security.user.oidc.claim.identifying.user": "email",
+                 "nifi.security.user.oidc.fallback.claims.identifying.user": "upn" ])
         String expectedUpn = "xxx@aaddomain";
 
         OIDCTokenResponse mockResponse = mockOIDCTokenResponse(["email": null, "upn": expectedUpn])
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy
index 13eab19..eca7efb 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/groovy/org/apache/nifi/web/security/spring/LoginIdentityProviderFactoryBeanTest.groovy
@@ -18,13 +18,9 @@ package org.apache.nifi.web.security.spring
 
 import org.apache.nifi.authentication.generated.Property
 import org.apache.nifi.authentication.generated.Provider
-import org.apache.nifi.properties.AESSensitivePropertyProvider
 import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.junit.After
-import org.junit.AfterClass
 import org.junit.Before
 import org.junit.BeforeClass
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -54,8 +50,10 @@ class LoginIdentityProviderFactoryBeanTest extends GroovyTestCase {
 
     private static final String PASSWORD = "thisIsABadPassword"
 
+    private LoginIdentityProviderFactoryBean bean
+
     @BeforeClass
-    public static void setUpOnce() throws Exception {
+    static void setUpOnce() throws Exception {
         Security.addProvider(new BouncyCastleProvider())
 
         logger.metaClass.methodMissing = { String name, args ->
@@ -63,47 +61,16 @@ class LoginIdentityProviderFactoryBeanTest extends GroovyTestCase {
         }
     }
 
-    @AfterClass
-    public static void tearDownOnce() throws Exception {
-    }
-
     @Before
-    public void setUp() throws Exception {
-        LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER = new AESSensitivePropertyProvider(KEY_HEX)
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER = null
-        LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER_FACTORY = null
+    void setUp() {
+        bean = new LoginIdentityProviderFactoryBean()
+        bean.configureSensitivePropertyProviderFactory(KEY_HEX, null)
     }
 
     private static boolean isUnlimitedStrengthCryptoAvailable() {
         Cipher.getMaxAllowedKeyLength("AES") > 128
     }
 
-    private static int getKeyLength(String keyHex = KEY_HEX) {
-        keyHex?.size() * 4
-    }
-
-    @Ignore("Can't test without overloading static metaClass method")
-    @Test
-    void testShouldInitializeSensitivePropertyProvider() {
-        // Arrange
-        assert !LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER
-        assert !LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER_FACTORY
-
-        logger.info("Encryption scheme: ${ENCRYPTION_SCHEME}")
-
-        // Act
-        LoginIdentityProviderFactoryBean.initializeSensitivePropertyProvider(ENCRYPTION_SCHEME)
-
-        // Assert
-        assert LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER
-        assert LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER_FACTORY
-        assert LoginIdentityProviderFactoryBean.SENSITIVE_PROPERTY_PROVIDER.getIdentifierKey() == ENCRYPTION_SCHEME
-    }
-
     @Test
     void testShouldDecryptValue() {
         // Arrange
@@ -111,7 +78,7 @@ class LoginIdentityProviderFactoryBeanTest extends GroovyTestCase {
         logger.info("Cipher text: ${CIPHER_TEXT}")
 
         // Act
-        String decrypted = new LoginIdentityProviderFactoryBean().decryptValue(CIPHER_TEXT, ENCRYPTION_SCHEME)
+        String decrypted = bean.decryptValue(CIPHER_TEXT, ENCRYPTION_SCHEME)
         logger.info("Decrypted ${CIPHER_TEXT} -> ${decrypted}")
 
         // Assert
@@ -129,7 +96,6 @@ class LoginIdentityProviderFactoryBeanTest extends GroovyTestCase {
         encryptedProvider.property = [managerPasswordProperty]
 
         logger.info("Manager Password property: ${managerPasswordProperty.dump()}")
-        def bean = new LoginIdentityProviderFactoryBean()
 
         // Act
         def context = bean.loadLoginIdentityProviderConfiguration(encryptedProvider)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java
index e1232fa..b4a85f3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtAuthenticationProviderTest.java
@@ -28,7 +28,6 @@ import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.authorization.user.NiFiUserDetails;
 import org.apache.nifi.idp.IdpType;
 import org.apache.nifi.idp.IdpUserGroup;
-import org.apache.nifi.properties.StandardNiFiProperties;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.security.InvalidAuthenticationException;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
@@ -87,7 +86,7 @@ public class JwtAuthenticationProviderTest {
         Properties props = new Properties();
         props.put(properties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX, "^(.*?)@(.*?)$");
         props.put(properties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX, "$1");
-        properties = new StandardNiFiProperties(props);
+        properties = new NiFiProperties(props);
 
         jwtAuthenticationProvider = new JwtAuthenticationProvider(jwtService, properties, authorizer, idpUserGroupService);
     }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
index 513eb0c..a89082f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/jwt/JwtServiceTest.java
@@ -25,7 +25,7 @@ import org.apache.nifi.authorization.user.StandardNiFiUser;
 import org.apache.nifi.authorization.util.IdentityMapping;
 import org.apache.nifi.authorization.util.IdentityMappingUtil;
 import org.apache.nifi.key.Key;
-import org.apache.nifi.properties.StandardNiFiProperties;
+import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.security.token.LoginAuthenticationToken;
 import org.codehaus.jettison.json.JSONObject;
 import org.junit.After;
@@ -244,7 +244,7 @@ public class JwtServiceTest {
         Properties props = new Properties();
         props.setProperty(SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX+"kerb",  "^(.*?)@(.*?)$");
         props.setProperty(SECURITY_IDENTITY_MAPPING_VALUE_PREFIX+"kerb", "$1");
-        identityMappings = IdentityMappingUtil.getIdentityMappings(new StandardNiFiProperties(props));
+        identityMappings = IdentityMappingUtil.getIdentityMappings(new NiFiProperties(props));
     }
 
     @After
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLService.java
index 745bcec..8ad2fc4 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/saml/impl/TestStandardSAMLService.java
@@ -28,6 +28,8 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.io.File;
+import java.util.Arrays;
+import java.util.HashSet;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -72,6 +74,15 @@ public class TestStandardSAMLService {
         when(properties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE)).thenReturn("src/test/resources/saml/truststore.jks");
         when(properties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_PASSWD)).thenReturn("passwordpassword");
         when(properties.getProperty(NiFiProperties.SECURITY_TRUSTSTORE_TYPE)).thenReturn("JKS");
+        when(properties.getPropertyKeys()).thenReturn(new HashSet<>(Arrays.asList(
+                NiFiProperties.SECURITY_KEYSTORE,
+                NiFiProperties.SECURITY_KEYSTORE_PASSWD,
+                NiFiProperties.SECURITY_KEY_PASSWD,
+                NiFiProperties.SECURITY_KEYSTORE_TYPE,
+                NiFiProperties.SECURITY_TRUSTSTORE,
+                NiFiProperties.SECURITY_TRUSTSTORE_PASSWD,
+                NiFiProperties.SECURITY_TRUSTSTORE_TYPE
+        )));
 
         when(properties.isSamlEnabled()).thenReturn(true);
         when(properties.getSamlServiceProviderEntityId()).thenReturn(spEntityId);
diff --git a/nifi-nar-bundles/nifi-framework-bundle/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/pom.xml
index bce4655..452b843 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/pom.xml
@@ -147,6 +147,11 @@
             </dependency>
             <dependency>
                 <groupId>org.apache.nifi</groupId>
+                <artifactId>nifi-sensitive-property-provider</artifactId>
+                <version>1.14.0-SNAPSHOT</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.nifi</groupId>
                 <artifactId>nifi-framework-authorization</artifactId>
                 <version>1.14.0-SNAPSHOT</version>
             </dependency>
diff --git a/nifi-nar-bundles/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/EncryptedWriteAheadProvenanceRepository.java b/nifi-nar-bundles/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/EncryptedWriteAheadProvenanceRepository.java
index 24ee0b5..24d80d6 100644
--- a/nifi-nar-bundles/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/EncryptedWriteAheadProvenanceRepository.java
+++ b/nifi-nar-bundles/nifi-provenance-repository-bundle/nifi-persistent-provenance-repository/src/main/java/org/apache/nifi/provenance/EncryptedWriteAheadProvenanceRepository.java
@@ -16,8 +16,6 @@
  */
 package org.apache.nifi.provenance;
 
-import org.apache.commons.codec.DecoderException;
-import org.apache.commons.codec.binary.Hex;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.events.EventReporter;
 import org.apache.nifi.provenance.serialization.RecordReaders;
@@ -35,7 +33,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
 import java.io.IOException;
 import java.security.KeyManagementException;
 
@@ -89,7 +86,7 @@ public class EncryptedWriteAheadProvenanceRepository extends WriteAheadProvenanc
             try {
                 KeyProvider keyProvider;
                 if (KeyProviderFactory.requiresRootKey(getConfig().getKeyProviderImplementation())) {
-                    SecretKey rootKey = getRootKey();
+                    SecretKey rootKey = CryptoUtils.getRootKey();
                     keyProvider = buildKeyProvider(rootKey);
                 } else {
                     keyProvider = buildKeyProvider();
@@ -151,15 +148,4 @@ public class EncryptedWriteAheadProvenanceRepository extends WriteAheadProvenanc
 
         return KeyProviderFactory.buildKeyProvider(implementationClassName, config.getKeyProviderLocation(), config.getKeyId(), config.getEncryptionKeys(), rootKey);
     }
-
-    private static SecretKey getRootKey() throws KeyManagementException {
-        try {
-            // Get the root encryption key from bootstrap.conf
-            String rootKeyHex = CryptoUtils.extractKeyFromBootstrapFile();
-            return new SecretKeySpec(Hex.decodeHex(rootKeyHex.toCharArray()), "AES");
-        } catch (IOException | DecoderException e) {
-            logger.error("Encountered an error: ", e);
-            throw new KeyManagementException(e);
-        }
-    }
 }
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java
index 3c2a3f4..9a06d59 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java
@@ -17,10 +17,10 @@
 package org.apache.nifi.registry.security.authentication;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.properties.SensitivePropertyProvider;
 import org.apache.nifi.registry.extension.ExtensionManager;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
-import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
-import org.apache.nifi.registry.properties.SensitivePropertyProvider;
 import org.apache.nifi.registry.security.authentication.annotation.IdentityProviderContext;
 import org.apache.nifi.registry.security.authentication.generated.IdentityProviders;
 import org.apache.nifi.registry.security.authentication.generated.Property;
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
index 1fb3d90..3d1e790 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
@@ -18,12 +18,12 @@ package org.apache.nifi.registry.security.authorization;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Validate;
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.properties.SensitivePropertyProvider;
 import org.apache.nifi.registry.extension.ExtensionClassLoader;
 import org.apache.nifi.registry.extension.ExtensionCloseable;
 import org.apache.nifi.registry.extension.ExtensionManager;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
-import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
-import org.apache.nifi.registry.properties.SensitivePropertyProvider;
 import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
 import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
 import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java
index 7859492..055b71e 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java
@@ -16,25 +16,25 @@
  */
 package org.apache.nifi.registry.security.crypto;
 
-import org.apache.nifi.registry.properties.AESSensitivePropertyProvider;
-import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
-import org.apache.nifi.registry.properties.SensitivePropertyProvider;
-import org.apache.nifi.registry.properties.SensitivePropertyProviderFactory;
+import org.apache.nifi.properties.PropertyProtectionScheme;
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.properties.SensitivePropertyProvider;
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory;
+import org.apache.nifi.registry.properties.util.NiFiRegistryBootstrapUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
-import javax.crypto.NoSuchPaddingException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
+import java.io.IOException;
 
 @Configuration
-public class SensitivePropertyProviderConfiguration implements SensitivePropertyProviderFactory {
-
+public class SensitivePropertyProviderConfiguration {
     private static final Logger logger = LoggerFactory.getLogger(SensitivePropertyProviderConfiguration.class);
 
+    private static final PropertyProtectionScheme DEFAULT_SCHEME = PropertyProtectionScheme.AES_GCM;
+
     @Autowired(required = false)
     private CryptoKeyProvider masterKeyProvider;
 
@@ -43,7 +43,6 @@ public class SensitivePropertyProviderConfiguration implements SensitiveProperty
      *         or null if the master key is not present.
      */
     @Bean
-    @Override
     public SensitivePropertyProvider getProvider() {
         if (masterKeyProvider == null || masterKeyProvider.isEmpty()) {
             // This NiFi Registry was not configured with a master key, so the assumption is
@@ -56,11 +55,18 @@ public class SensitivePropertyProviderConfiguration implements SensitiveProperty
             // returned provider, which has a copy of the sensitive master key material
             // to be reaped when it goes out of scope in order to decrease the time
             // key material is held in memory.
-            String key = masterKeyProvider.getKey();
-            return new AESSensitivePropertyProvider(masterKeyProvider.getKey());
-        } catch (MissingCryptoKeyException | NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
-            logger.warn("Error creating AES Sensitive Property Provider", e);
-            throw new SensitivePropertyProtectionException("Error creating AES Sensitive Property Provider", e);
+            return StandardSensitivePropertyProviderFactory
+                    .withKeyAndBootstrapSupplier(masterKeyProvider.getKey(), () -> {
+                        try {
+                            return NiFiRegistryBootstrapUtils.loadBootstrapProperties();
+                        } catch (IOException e) {
+                            throw new SensitivePropertyProtectionException("Error creating Sensitive Property Provider", e);
+                        }
+                    })
+                    .getProvider(DEFAULT_SCHEME);
+        } catch (final MissingCryptoKeyException e) {
+            logger.warn("Error creating Sensitive Property Provider", e);
+            throw new SensitivePropertyProtectionException("Error creating Sensitive Property Provider", e);
         }
     }
 
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
index 7fb8dee..75e10c6 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
@@ -26,6 +26,7 @@ import org.mockito.Mockito;
 
 import javax.sql.DataSource;
 import java.net.URL;
+import java.util.Properties;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -36,8 +37,9 @@ public class TestStandardProviderFactory {
 
     @Test
     public void testGetProvidersSuccess() {
-        final NiFiRegistryProperties props = new NiFiRegistryProperties();
-        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
+        final Properties properties = new Properties();
+        properties.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
+        final NiFiRegistryProperties props = new NiFiRegistryProperties(properties);
 
         final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
         when(extensionManager.getExtensionClassLoader(any(String.class)))
@@ -67,8 +69,9 @@ public class TestStandardProviderFactory {
 
     @Test(expected = ProviderFactoryException.class)
     public void testGetFlowProviderBeforeInitializingShouldThrowException() {
-        final NiFiRegistryProperties props = new NiFiRegistryProperties();
-        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
+        final Properties properties = new Properties();
+        properties.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
+        final NiFiRegistryProperties props = new NiFiRegistryProperties(properties);
 
         final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
         when(extensionManager.getExtensionClassLoader(any(String.class)))
@@ -82,8 +85,9 @@ public class TestStandardProviderFactory {
 
     @Test(expected = ProviderFactoryException.class)
     public void testProvidersConfigDoesNotExist() {
-        final NiFiRegistryProperties props = new NiFiRegistryProperties();
-        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-does-not-exist.xml");
+        final Properties properties = new Properties();
+        properties.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-does-not-exist.xml");
+        final NiFiRegistryProperties props = new NiFiRegistryProperties(properties);
 
         final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
         when(extensionManager.getExtensionClassLoader(any(String.class)))
@@ -97,8 +101,9 @@ public class TestStandardProviderFactory {
 
     @Test(expected = ProviderFactoryException.class)
     public void testFlowProviderClassNotFound() {
-        final NiFiRegistryProperties props = new NiFiRegistryProperties();
-        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-class-not-found.xml");
+        final Properties properties = new Properties();
+        properties.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-class-not-found.xml");
+        final NiFiRegistryProperties props = new NiFiRegistryProperties(properties);
 
         final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
         when(extensionManager.getExtensionClassLoader(any(String.class)))
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
index 39d45ae..0085bba 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
@@ -28,6 +28,7 @@ import org.mockito.Mockito;
 import javax.sql.DataSource;
 
 import java.net.URL;
+import java.util.Properties;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.when;
@@ -36,8 +37,9 @@ public class TestScriptEventHookProvider {
 
     @Test(expected = ProviderCreationException.class)
     public void testBadScriptProvider() {
-        final NiFiRegistryProperties props = new NiFiRegistryProperties();
-        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/hook/bad-script-provider.xml");
+        final Properties properties = new Properties();
+        properties.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/hook/bad-script-provider.xml");
+        final NiFiRegistryProperties props = new NiFiRegistryProperties(properties);
 
         final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
         when(extensionManager.getExtensionClassLoader(any(String.class)))
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseAccessPolicyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseAccessPolicyProvider.java
index 7f522ff..9c2cb60 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseAccessPolicyProvider.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseAccessPolicyProvider.java
@@ -45,6 +45,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
@@ -286,9 +287,11 @@ public class TestDatabaseAccessPolicyProvider extends DatabaseBaseTest {
 
     @Test
     public void testOnConfiguredAppliesIdentityMappings() {
+        final Properties props = new Properties();
         // Set up an identity mapping for kerberos principals
-        properties.setProperty("nifi.registry.security.identity.mapping.pattern.kerb", "^(.*?)@(.*?)$");
-        properties.setProperty("nifi.registry.security.identity.mapping.value.kerb", "$1");
+        props.setProperty("nifi.registry.security.identity.mapping.pattern.kerb", "^(.*?)@(.*?)$");
+        props.setProperty("nifi.registry.security.identity.mapping.value.kerb", "$1");
+        properties = new NiFiRegistryProperties(props);
 
         identityMapper = new DefaultIdentityMapper(properties);
         ((DatabaseAccessPolicyProvider)policyProvider).setIdentityMapper(identityMapper);
@@ -311,10 +314,12 @@ public class TestDatabaseAccessPolicyProvider extends DatabaseBaseTest {
 
     @Test
     public void testOnConfiguredAppliesGroupMappings() {
+        final Properties props = new Properties();
         // Set up an identity mapping for kerberos principals
-        properties.setProperty("nifi.registry.security.group.mapping.pattern.anyGroup", "^(.*)$");
-        properties.setProperty("nifi.registry.security.group.mapping.value.anyGroup", "$1");
-        properties.setProperty("nifi.registry.security.group.mapping.transform.anyGroup", "LOWER");
+        props.setProperty("nifi.registry.security.group.mapping.pattern.anyGroup", "^(.*)$");
+        props.setProperty("nifi.registry.security.group.mapping.value.anyGroup", "$1");
+        props.setProperty("nifi.registry.security.group.mapping.transform.anyGroup", "LOWER");
+        properties = new NiFiRegistryProperties(props);
 
         identityMapper = new DefaultIdentityMapper(properties);
         ((DatabaseAccessPolicyProvider)policyProvider).setIdentityMapper(identityMapper);
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseUserGroupProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseUserGroupProvider.java
index 0f8d432..dc14e66 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseUserGroupProvider.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/authorization/database/TestDatabaseUserGroupProvider.java
@@ -35,6 +35,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
 import javax.sql.DataSource;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 import java.util.UUID;
 
@@ -179,9 +180,11 @@ public class TestDatabaseUserGroupProvider extends DatabaseBaseTest {
 
     @Test
     public void testOnConfiguredAppliesIdentityMappingsToInitialUsers() {
+        final Properties props = new Properties();
         // Set up an identity mapping for kerberos principals
-        properties.setProperty("nifi.registry.security.identity.mapping.pattern.kerb", "^(.*?)@(.*?)$");
-        properties.setProperty("nifi.registry.security.identity.mapping.value.kerb", "$1");
+        props.setProperty("nifi.registry.security.identity.mapping.pattern.kerb", "^(.*?)@(.*?)$");
+        props.setProperty("nifi.registry.security.identity.mapping.value.kerb", "$1");
+        properties = new NiFiRegistryProperties(props);
 
         identityMapper = new DefaultIdentityMapper(properties);
         ((DatabaseUserGroupProvider)userGroupProvider).setIdentityMapper(identityMapper);
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml b/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml
index 6458aec..77c7c14 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/pom.xml
@@ -72,5 +72,11 @@
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-sensitive-property-provider</artifactId>
+            <version>1.14.0-SNAPSHOT</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
deleted file mode 100644
index b7d1d2e..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * 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.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.util.encoders.Base64;
-import org.bouncycastle.util.encoders.DecoderException;
-import org.bouncycastle.util.encoders.EncoderException;
-import org.bouncycastle.util.encoders.Hex;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.charset.StandardCharsets;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.SecureRandom;
-import java.security.Security;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
-    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProvider.class);
-
-    private static final String IMPLEMENTATION_NAME = "AES Sensitive Property Provider";
-    private static final String IMPLEMENTATION_KEY = "aes/gcm/";
-    private static final String ALGORITHM = "AES/GCM/NoPadding";
-    private static final String PROVIDER = "BC";
-    private static final String DELIMITER = "||"; // "|" is not a valid Base64 character, so ensured not to be present in cipher text
-    private static final int IV_LENGTH = 12;
-    private static final int MIN_CIPHER_TEXT_LENGTH = IV_LENGTH * 4 / 3 + DELIMITER.length() + 1;
-
-    private Cipher cipher;
-    private final SecretKey key;
-
-    public AESSensitivePropertyProvider(String keyHex) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
-        byte[] key = validateKey(keyHex);
-
-        try {
-            Security.addProvider(new BouncyCastleProvider());
-            cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
-            // Only store the key if the cipher was initialized successfully
-            this.key = new SecretKeySpec(key, "AES");
-        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
-            logger.error("Encountered an error initializing the {}: {}", IMPLEMENTATION_NAME, e.getMessage());
-            throw new SensitivePropertyProtectionException("Error initializing the protection cipher", e);
-        }
-    }
-
-    private byte[] validateKey(String keyHex) {
-        if (keyHex == null || StringUtils.isBlank(keyHex)) {
-            throw new SensitivePropertyProtectionException("The key cannot be empty");
-        }
-        keyHex = formatHexKey(keyHex);
-        if (!isHexKeyValid(keyHex)) {
-            throw new SensitivePropertyProtectionException("The key must be a valid hexadecimal key");
-        }
-        byte[] key = Hex.decode(keyHex);
-        final List<Integer> validKeyLengths = getValidKeyLengths();
-        if (!validKeyLengths.contains(key.length * 8)) {
-            List<String> validKeyLengthsAsStrings = validKeyLengths.stream().map(i -> Integer.toString(i)).collect(Collectors.toList());
-            throw new SensitivePropertyProtectionException("The key (" + key.length * 8 + " bits) must be a valid length: " + StringUtils.join(validKeyLengthsAsStrings, ", "));
-        }
-        return key;
-    }
-
-    public AESSensitivePropertyProvider(byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
-        this(key == null ? "" : Hex.toHexString(key));
-    }
-
-    private static String formatHexKey(String input) {
-        if (input == null || StringUtils.isBlank(input)) {
-            return "";
-        }
-        return input.replaceAll("[^0-9a-fA-F]", "").toLowerCase();
-    }
-
-    private static boolean isHexKeyValid(String key) {
-        if (key == null || StringUtils.isBlank(key)) {
-            return false;
-        }
-        // Key length is in "nibbles" (i.e. one hex char = 4 bits)
-        return getValidKeyLengths().contains(key.length() * 4) && key.matches("^[0-9a-fA-F]*$");
-    }
-
-    private static List<Integer> getValidKeyLengths() {
-        List<Integer> validLengths = new ArrayList<>();
-        validLengths.add(128);
-
-        try {
-            if (Cipher.getMaxAllowedKeyLength("AES") > 128) {
-                validLengths.add(192);
-                validLengths.add(256);
-            } else {
-                logger.warn("JCE Unlimited Strength Cryptography Jurisdiction policies are not available, so the max key length is 128 bits");
-            }
-        } catch (NoSuchAlgorithmException e) {
-            logger.warn("Encountered an error determining the max key length", e);
-        }
-
-        return validLengths;
-    }
-
-    /**
-     * Returns the name of the underlying implementation.
-     *
-     * @return the name of this sensitive property provider
-     */
-    @Override
-    public String getName() {
-        return IMPLEMENTATION_NAME;
-    }
-
-    /**
-     * 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 IMPLEMENTATION_KEY + getKeySize(Hex.toHexString(key.getEncoded()));
-    }
-
-    private int getKeySize(String key) {
-        if (StringUtils.isBlank(key)) {
-            return 0;
-        } else {
-            // A key in hexadecimal format has one char per nibble (4 bits)
-            return formatHexKey(key).length() * 4;
-        }
-    }
-
-    /**
-     * Returns the encrypted cipher text.
-     *
-     * @param unprotectedValue the sensitive value
-     * @return the value to persist in the {@code nifi.properties} file
-     * @throws SensitivePropertyProtectionException if there is an exception encrypting the value
-     */
-    @Override
-    public String protect(String unprotectedValue) throws SensitivePropertyProtectionException {
-        if (unprotectedValue == null || unprotectedValue.trim().length() == 0) {
-            throw new IllegalArgumentException("Cannot encrypt an empty value");
-        }
-
-        // Generate IV
-        byte[] iv = generateIV();
-        if (iv.length < IV_LENGTH) {
-            throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes");
-        }
-
-        try {
-            // Initialize cipher for encryption
-            cipher.init(Cipher.ENCRYPT_MODE, this.key, new IvParameterSpec(iv));
-
-            byte[] plainBytes = unprotectedValue.getBytes(StandardCharsets.UTF_8);
-            byte[] cipherBytes = cipher.doFinal(plainBytes);
-            logger.info(getName() + " encrypted a sensitive value successfully");
-            return base64Encode(iv) + DELIMITER + base64Encode(cipherBytes);
-            // return Base64.toBase64String(iv) + DELIMITER + Base64.toBase64String(cipherBytes);
-        } catch (BadPaddingException | IllegalBlockSizeException | EncoderException | InvalidAlgorithmParameterException | InvalidKeyException e) {
-            final String msg = "Error encrypting a protected value";
-            logger.error(msg, e);
-            throw new SensitivePropertyProtectionException(msg, e);
-        }
-    }
-
-    private String base64Encode(byte[] input) {
-        return Base64.toBase64String(input).replaceAll("=", "");
-    }
-
-    /**
-     * Generates a new random IV of 12 bytes using {@link SecureRandom}.
-     *
-     * @return the IV
-     */
-    private byte[] generateIV() {
-        byte[] iv = new byte[IV_LENGTH];
-        new SecureRandom().nextBytes(iv);
-        return iv;
-    }
-
-    /**
-     * Returns the decrypted plaintext.
-     *
-     * @param protectedValue the cipher text read from the {@code nifi.properties} file
-     * @return the raw value to be used by the application
-     * @throws SensitivePropertyProtectionException if there is an error decrypting the cipher text
-     */
-    @Override
-    public String unprotect(String protectedValue) throws SensitivePropertyProtectionException {
-        if (protectedValue == null || protectedValue.trim().length() < MIN_CIPHER_TEXT_LENGTH) {
-            throw new IllegalArgumentException("Cannot decrypt a cipher text shorter than " + MIN_CIPHER_TEXT_LENGTH + " chars");
-        }
-
-        if (!protectedValue.contains(DELIMITER)) {
-            throw new IllegalArgumentException("The cipher text does not contain the delimiter " + DELIMITER + " -- it should be of the form Base64(IV) || Base64(cipherText)");
-        }
-
-        protectedValue = protectedValue.trim();
-
-        final String IV_B64 = protectedValue.substring(0, protectedValue.indexOf(DELIMITER));
-        byte[] iv = Base64.decode(IV_B64);
-        if (iv.length < IV_LENGTH) {
-            throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes");
-        }
-
-        String CIPHERTEXT_B64 = protectedValue.substring(protectedValue.indexOf(DELIMITER) + 2);
-
-        // Restore the = padding if necessary to reconstitute the GCM MAC check
-        if (CIPHERTEXT_B64.length() % 4 != 0) {
-            final int paddedLength = CIPHERTEXT_B64.length() + 4 - (CIPHERTEXT_B64.length() % 4);
-            CIPHERTEXT_B64 = StringUtils.rightPad(CIPHERTEXT_B64, paddedLength, '=');
-        }
-
-        try {
-            byte[] cipherBytes = Base64.decode(CIPHERTEXT_B64);
-
-            cipher.init(Cipher.DECRYPT_MODE, this.key, new IvParameterSpec(iv));
-            byte[] plainBytes = cipher.doFinal(cipherBytes);
-            logger.debug(getName() + " decrypted a sensitive value successfully");
-            return new String(plainBytes, StandardCharsets.UTF_8);
-        } catch (BadPaddingException | IllegalBlockSizeException | DecoderException | InvalidAlgorithmParameterException | InvalidKeyException e) {
-            final String msg = "Error decrypting a protected value";
-            logger.error(msg, e);
-            throw new SensitivePropertyProtectionException(msg, e);
-        }
-    }
-
-    public static int getIvLength() {
-        return IV_LENGTH;
-    }
-
-    public static int getMinCipherTextLength() {
-        return MIN_CIPHER_TEXT_LENGTH;
-    }
-
-    public static String getDelimiter() {
-        return DELIMITER;
-    }
-}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java
deleted file mode 100644
index 5c24a73..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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 javax.crypto.NoSuchPaddingException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-
-public class AESSensitivePropertyProviderFactory implements SensitivePropertyProviderFactory {
-    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactory.class);
-
-    private String keyHex;
-
-    public AESSensitivePropertyProviderFactory(String keyHex) {
-        this.keyHex = keyHex;
-    }
-
-    public SensitivePropertyProvider getProvider() throws SensitivePropertyProtectionException {
-        try {
-            if (keyHex != null && !StringUtils.isBlank(keyHex)) {
-                return new AESSensitivePropertyProvider(keyHex);
-            } else {
-                throw new SensitivePropertyProtectionException("The provider factory cannot generate providers without a key");
-            }
-        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
-            String msg = "Error creating AES Sensitive Property Provider";
-            logger.warn(msg, e);
-            throw new SensitivePropertyProtectionException(msg, e);
-        }
-    }
-
-    @Override
-    public String toString() {
-        return "SensitivePropertyProviderFactory for creating AESSensitivePropertyProviders";
-    }
-}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
deleted file mode 100644
index df4047f..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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 java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
-
-public class MultipleSensitivePropertyProtectionException extends SensitivePropertyProtectionException {
-
-    private Set<String> failedKeys;
-
-    /**
-     * 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 MultipleSensitivePropertyProtectionException() {
-    }
-
-    /**
-     * 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 MultipleSensitivePropertyProtectionException(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.)
-     * @since 1.4
-     */
-    public MultipleSensitivePropertyProtectionException(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.)
-     * @since 1.4
-     */
-    public MultipleSensitivePropertyProtectionException(Throwable cause) {
-        super(cause);
-    }
-
-    /**
-     * Constructs a new exception with the provided message and a unique set of the keys that caused the error.
-     *
-     * @param message    the message
-     * @param failedKeys any failed keys
-     */
-    public MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys) {
-        this(message, failedKeys, null);
-    }
-
-    /**
-     * Constructs a new exception with the provided message and a unique set of the keys that caused the error.
-     *
-     * @param message    the message
-     * @param failedKeys any failed keys
-     * @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 MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys, Throwable cause) {
-        super(message, cause);
-        this.failedKeys = new HashSet<>(failedKeys);
-    }
-
-    public Set<String> getFailedKeys() {
-        return this.failedKeys;
-    }
-
-    @Override
-    public String toString() {
-        return "SensitivePropertyProtectionException for [" + StringUtils.join(this.failedKeys, ", ") + "]: " + getLocalizedMessage();
-    }
-}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
index 48b90e5..3f8ec6b 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -17,13 +17,14 @@
 package org.apache.nifi.registry.properties;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.properties.ApplicationProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Enumeration;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -31,11 +32,20 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-public class NiFiRegistryProperties extends Properties {
+public class NiFiRegistryProperties extends ApplicationProperties {
 
     private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryProperties.class);
 
+    public static final String NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY = "nifi.registry.properties.file.path";
+    public static final String NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY = "nifi.registry.bootstrap.config.file.path";
+    public static final String NIFI_REGISTRY_BOOTSTRAP_DOCS_DIR_PROPERTY = "nifi.registry.bootstrap.config.docs.dir";
+
+    public static final String RELATIVE_BOOTSTRAP_FILE_LOCATION = "conf/bootstrap.conf";
+    public static final String RELATIVE_PROPERTIES_FILE_LOCATION = "conf/nifi-registry.properties";
+    public static final String RELATIVE_DOCS_LOCATION = "docs";
+
     // Keys
+    public static final String PROPERTIES_FILE_PATH = "nifi.registry.properties.file.path";
     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";
@@ -119,11 +129,15 @@ public class NiFiRegistryProperties extends Properties {
     public static final String DEFAULT_SECURITY_USER_OIDC_READ_TIMEOUT = "5 secs";
 
     public NiFiRegistryProperties() {
-        super();
+        this(Collections.EMPTY_MAP);
+    }
+
+    public NiFiRegistryProperties(final Map<String, String> props) {
+        super(props);
     }
 
-    public NiFiRegistryProperties(Map<String, String> props) {
-        this.putAll(props);
+    public NiFiRegistryProperties(final Properties props) {
+        super(props);
     }
 
     public int getWebThreads() {
@@ -297,7 +311,7 @@ public class NiFiRegistryProperties extends Properties {
 
     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)));
+        getPropertyKeys().stream().filter(key -> key.startsWith(EXTENSION_DIR_PREFIX)).forEach(key -> extensionDirs.add(getProperty(key)));
         return extensionDirs;
     }
 
@@ -305,21 +319,6 @@ public class NiFiRegistryProperties extends Properties {
         return Boolean.parseBoolean(getPropertyAsTrimmedString(REVISIONS_ENABLED));
     }
 
-    /**
-     * 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) {
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
index 5ceffd1..be9e282 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
@@ -16,25 +16,33 @@
  */
 package org.apache.nifi.registry.properties;
 
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.properties.SensitivePropertyProvider;
+import org.apache.nifi.properties.SensitivePropertyProviderFactory;
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory;
+import org.apache.nifi.registry.properties.util.NiFiRegistryBootstrapUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 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;
+import java.security.Security;
+import java.util.Properties;
 
 public class NiFiRegistryPropertiesLoader {
 
     private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesLoader.class);
 
+    private static final String APPLICATION_PATH = "nifi.registry";
+
     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;
+    private SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
 
     /**
      * Returns an instance of the loader configured with the key.
@@ -46,7 +54,7 @@ public class NiFiRegistryPropertiesLoader {
      * @param keyHex the key used to encrypt any sensitive properties
      * @return the configured loader
      */
-    public static NiFiRegistryPropertiesLoader withKey(String keyHex) {
+    public static NiFiRegistryPropertiesLoader withKey(final String keyHex) {
         NiFiRegistryPropertiesLoader loader = new NiFiRegistryPropertiesLoader();
         loader.setKeyHex(keyHex);
         return loader;
@@ -54,12 +62,12 @@ public class NiFiRegistryPropertiesLoader {
 
     /**
      * Sets the hexadecimal key used to unprotect properties encrypted with
-     * {@link AESSensitivePropertyProvider}. If the key has already been set,
+     * {@link SensitivePropertyProvider}. 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) {
+    public void setKeyHex(final String keyHex) {
         if (this.keyHex == null || this.keyHex.trim().isEmpty()) {
             this.keyHex = keyHex;
         } else {
@@ -67,21 +75,18 @@ public class NiFiRegistryPropertiesLoader {
         }
     }
 
-    private static String getDefaultProviderKey() {
-        try {
-            return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? "256" : "128");
-        } catch (NoSuchAlgorithmException e) {
-            return "aes/gcm/128";
+    private SensitivePropertyProviderFactory getSensitivePropertyProviderFactory() {
+        if (sensitivePropertyProviderFactory == null) {
+            sensitivePropertyProviderFactory = StandardSensitivePropertyProviderFactory
+                    .withKeyAndBootstrapSupplier(keyHex, () -> {
+                        try {
+                            return NiFiRegistryBootstrapUtils.loadBootstrapProperties();
+                        } catch (IOException e) {
+                            throw new SensitivePropertyProtectionException("Could not load bootstrap.conf for sensitive property provider configuration.", e);
+                        }
+                    });
         }
-    }
-
-    private void initializeSensitivePropertyProviderFactory() {
-        sensitivePropertyProviderFactory = new AESSensitivePropertyProviderFactory(keyHex);
-    }
-
-    private SensitivePropertyProvider getSensitivePropertyProvider() {
-        initializeSensitivePropertyProviderFactory();
-        return sensitivePropertyProviderFactory.getProvider();
+        return sensitivePropertyProviderFactory;
     }
 
     /**
@@ -100,11 +105,12 @@ public class NiFiRegistryPropertiesLoader {
             throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable");
         }
 
-        final NiFiRegistryProperties rawProperties = new NiFiRegistryProperties();
+        final Properties rawProperties = new Properties();
         try (final FileReader reader = new FileReader(file)) {
             rawProperties.load(reader);
+            final NiFiRegistryProperties innerProperties = new NiFiRegistryProperties(rawProperties);
             logger.info("Loaded {} properties from {}", rawProperties.size(), file.getAbsolutePath());
-            ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = new ProtectedNiFiRegistryProperties(rawProperties);
+            ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = new ProtectedNiFiRegistryProperties(innerProperties);
             return protectedNiFiRegistryProperties;
         } catch (final IOException ioe) {
             logger.error("Cannot load properties file due to " + ioe.getLocalizedMessage());
@@ -120,13 +126,16 @@ public class NiFiRegistryPropertiesLoader {
      * @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());
+    public NiFiRegistryProperties load(final File file) {
+        final ProtectedNiFiRegistryProperties protectedNiFiProperties = readProtectedPropertiesFromDisk(file);
+        if (protectedNiFiProperties.hasProtectedKeys()) {
+            Security.addProvider(new BouncyCastleProvider());
+            getSensitivePropertyProviderFactory()
+                    .getSupportedSensitivePropertyProviders()
+                    .forEach(protectedNiFiProperties::addSensitivePropertyProvider);
         }
 
-        return protectedNiFiRegistryProperties.getUnprotectedProperties();
+        return protectedNiFiProperties.getUnprotectedProperties();
     }
 
     /**
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
index 5debc4a..57e1af2 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
@@ -17,31 +17,36 @@
 package org.apache.nifi.registry.properties;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.properties.ApplicationPropertiesProtector;
+import org.apache.nifi.properties.ProtectedProperties;
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.properties.SensitivePropertyProtector;
+import org.apache.nifi.properties.SensitivePropertyProvider;
 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.Properties;
 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}.
+ * Decorator class for intermediate phase when {@link NiFiRegistryPropertiesLoader} loads the
+ * raw properties file and performs unprotection activities before returning a clean
+ * implementation of {@link NiFiRegistryProperties}.
+ * This encapsulates the sensitive property access logic from external consumers
+ * of {@code NiFiRegistryProperties}.
  */
-class ProtectedNiFiRegistryProperties {
+class ProtectedNiFiRegistryProperties extends NiFiRegistryProperties implements ProtectedProperties<NiFiRegistryProperties>,
+        SensitivePropertyProtector<ProtectedNiFiRegistryProperties, NiFiRegistryProperties> {
     private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiRegistryProperties.class);
 
-    private NiFiRegistryProperties properties;
+    private SensitivePropertyProtector<ProtectedNiFiRegistryProperties, NiFiRegistryProperties> propertyProtectionDelegate;
 
-    private Map<String, SensitivePropertyProvider> localProviderCache = new HashMap<>();
+    private NiFiRegistryProperties applicationProperties;
 
     // Additional "sensitive" property key
     public static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = "nifi.registry.sensitive.props.additional.keys";
@@ -53,32 +58,34 @@ class ProtectedNiFiRegistryProperties {
             NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD));
 
     public ProtectedNiFiRegistryProperties() {
-        this(null);
+        this(new NiFiRegistryProperties());
     }
 
     /**
      * Creates an instance containing the provided {@link NiFiRegistryProperties}.
      *
-     * @param props the NiFiProperties to contain
+     * @param props the NiFiRegistryProperties 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());
+    public ProtectedNiFiRegistryProperties(final NiFiRegistryProperties props) {
+        this.applicationProperties = props;
+        this.propertyProtectionDelegate = new ApplicationPropertiesProtector<>(this);
+        logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiRegistryProperties", getApplicationProperties()
+                .getPropertyKeys().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);
+    @Override
+    public String getAdditionalSensitivePropertiesKeys() {
+        return getProperty(getAdditionalSensitivePropertiesKeysName());
+    }
+
+    @Override
+    public String getAdditionalSensitivePropertiesKeysName() {
+        return ADDITIONAL_SENSITIVE_PROPERTIES_KEY;
+    }
+
+    @Override
+    public List<String> getDefaultSensitiveProperties() {
+        return DEFAULT_SENSITIVE_PROPERTIES;
     }
 
     /**
@@ -89,382 +96,119 @@ class ProtectedNiFiRegistryProperties {
      *
      * @return the internal properties
      */
-    NiFiRegistryProperties getInternalNiFiProperties() {
-        if (this.properties == null) {
-            this.properties = new NiFiRegistryProperties();
+    @Override
+    public NiFiRegistryProperties getApplicationProperties() {
+        if (this.applicationProperties == null) {
+            this.applicationProperties = new NiFiRegistryProperties();
         }
 
-        return this.properties;
+        return this.applicationProperties;
     }
 
-    /**
-     * 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();
+    @Override
+    public NiFiRegistryProperties createApplicationProperties(final Properties rawProperties) {
+        return new NiFiRegistryProperties(rawProperties);
     }
 
     /**
-     * Returns the complete set of property keys in the NiFiRegistryProperties,
-     * including any protection keys (i.e. 'x.y.z.protected').
+     * Retrieves the property value for the given property key.
      *
-     * @return the set of property keys
+     * @param key the key of property value to lookup
+     * @return value of property at given key or null if not found
      */
-    Set<String> getPropertyKeysIncludingProtectionSchemes() {
-        return getInternalNiFiProperties().getPropertyKeys();
+    @Override
+    public String getProperty(String key) {
+        return getApplicationProperties().getProperty(key);
     }
 
     /**
-     * Returns the set of property keys in the NiFiRegistryProperties,
-     * excluding any protection keys (i.e. 'x.y.z.protected').
+     * Retrieves all known property keys.
      *
-     * @return the set of property keys
+     * @return all known property keys
      */
-    Set<String> getPropertyKeysExcludingProtectionSchemes() {
-        Set<String> filteredKeys = getPropertyKeysIncludingProtectionSchemes();
-        filteredKeys.removeIf(p -> p.endsWith(".protected"));
-        return filteredKeys;
+    @Override
+    public Set<String> getPropertyKeys() {
+        return propertyProtectionDelegate.getPropertyKeys();
     }
 
     /**
-     * Splits a single string containing multiple property keys into a List.
-     *
-     * Delimited by ',' or ';' and ignores leading and trailing whitespace around delimiter.
+     * Returns the number of properties, excluding protection scheme properties.
+     * <p>
+     * Example:
+     * <p>
+     * key: E(value, key)
+     * key.protected: aes/gcm/256
+     * key2: value2
+     * <p>
+     * would return size 2
      *
-     * @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
+     * @return the count of real 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;
-        }
+    @Override
+    public int size() {
+        return propertyProtectionDelegate.size();
     }
 
-    /**
-     * 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
-     */
+    @Override
+    public Set<String> getPropertyKeysIncludingProtectionSchemes() {
+        return propertyProtectionDelegate.getPropertyKeysIncludingProtectionSchemes();
+    }
+
+    @Override
     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;
-        }
+        return propertyProtectionDelegate.getSensitivePropertyKeys();
     }
 
-    /**
-     * 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
-     */
+    @Override
     public List<String> getPopulatedSensitivePropertyKeys() {
-        List<String> allSensitiveKeys = getSensitivePropertyKeys();
-        return allSensitiveKeys.stream().filter(k -> StringUtils.isNotBlank(getProperty(k))).collect(Collectors.toList());
+        return propertyProtectionDelegate.getPopulatedSensitivePropertyKeys();
     }
 
-    /**
-     * Returns true if any sensitive keys are protected.
-     *
-     * @return true if any key is protected; false otherwise
-     */
+    @Override
     public boolean hasProtectedKeys() {
-        List<String> sensitiveKeys = getSensitivePropertyKeys();
-        for (String k : sensitiveKeys) {
-            if (isPropertyProtected(k)) {
-                return true;
-            }
-        }
-        return false;
+        return propertyProtectionDelegate.hasProtectedKeys();
     }
 
-    /**
-     * 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
-     */
+    @Override
     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;
+        return propertyProtectionDelegate.getProtectedPropertyKeys();
     }
 
-    /**
-     * Returns the unique set of all protection schemes currently in use for this instance.
-     *
-     * @return the set of protection schemes
-     */
+    @Override
     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());
+        return propertyProtectionDelegate.getProtectionSchemes();
     }
 
-    /**
-     * 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)));
+    @Override
+    public boolean isPropertySensitive(final String key) {
+        return propertyProtectionDelegate.isPropertySensitive(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";
+    @Override
+    public boolean isPropertyProtected(final String key) {
+        return propertyProtectionDelegate.isPropertyProtected(key);
     }
 
-    /**
-     * 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
-     */
+    @Override
     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;
+        return propertyProtectionDelegate.getUnprotectedProperties();
     }
 
-    /**
-     * 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();
+    @Override
+    public void addSensitivePropertyProvider(final SensitivePropertyProvider sensitivePropertyProvider) {
+        propertyProtectionDelegate.addSensitivePropertyProvider(sensitivePropertyProvider);
     }
 
-    /**
-     * 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 Map<String, SensitivePropertyProvider> getSensitivePropertyProviders() {
+        return propertyProtectionDelegate.getSensitivePropertyProviders();
     }
 
     @Override
     public String toString() {
         final Set<String> providers = getSensitivePropertyProviders().keySet();
-        return new StringBuilder("ProtectedNiFiProperties instance with ")
-                .append(getPropertyKeysIncludingProtectionSchemes().size())
-                .append(" properties (")
+        return new StringBuilder("ProtectedNiFiRegistryProperties instance with ")
+                .append(size()).append(" properties (")
                 .append(getProtectedPropertyKeys().size())
                 .append(" protected) and ")
                 .append(providers.size())
@@ -472,57 +216,4 @@ class ProtectedNiFiRegistryProperties {
                 .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;
-    }
 }
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
deleted file mode 100644
index 2ffa902..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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();
-    }
-}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
deleted file mode 100644
index c0dd43c6..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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;
-}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapPropertiesLoader.java
similarity index 55%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
rename to nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapPropertiesLoader.java
index c800b3a..1a8be9c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapPropertiesLoader.java
@@ -14,10 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.properties;
+package org.apache.nifi.registry.properties.util;
 
-public interface SensitivePropertyProviderFactory {
+import org.apache.nifi.properties.AbstractBootstrapPropertiesLoader;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
 
-    SensitivePropertyProvider getProvider();
+public class NiFiRegistryBootstrapPropertiesLoader extends AbstractBootstrapPropertiesLoader {
+    @Override
+    protected String getApplicationPrefix() {
+        return "nifi.registry";
+    }
 
+    @Override
+    protected String getApplicationPropertiesFilename() {
+        return "nifi-registry.properties";
+    }
+
+    @Override
+    protected String getApplicationPropertiesFilePathSystemProperty() {
+        return NiFiRegistryProperties.PROPERTIES_FILE_PATH;
+    }
 }
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapUtils.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapUtils.java
new file mode 100644
index 0000000..8494915
--- /dev/null
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapUtils.java
@@ -0,0 +1,82 @@
+/*
+ * 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.nifi.properties.AbstractBootstrapPropertiesLoader;
+import org.apache.nifi.properties.BootstrapProperties;
+
+import java.io.IOException;
+
+/**
+ * Encapsulates utility methods for dealing with bootstrap.conf or nifi-registry.properties.
+ */
+public class NiFiRegistryBootstrapUtils {
+    private static final AbstractBootstrapPropertiesLoader BOOTSTRAP_PROPERTIES_LOADER = new NiFiRegistryBootstrapPropertiesLoader();
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties, extracted from
+     * {@code $NIFI_REGISTRY_HOME/conf/bootstrap.conf}.
+     *
+     * @return the key in hexadecimal format
+     * @throws IOException if the file is not readable
+     */
+    public static String extractKeyFromBootstrapFile() throws IOException {
+        return BOOTSTRAP_PROPERTIES_LOADER.extractKeyFromBootstrapFile();
+    }
+
+    /**
+     * Loads the default bootstrap.conf file into a BootstrapProperties object.
+     * @return The default bootstrap.conf as a BootstrapProperties object
+     * @throws IOException If the file is not readable
+     */
+    public static BootstrapProperties loadBootstrapProperties() throws IOException {
+        return loadBootstrapProperties(null);
+    }
+
+    /**
+     * Loads the bootstrap.conf file into a BootstrapProperties object.
+     * @param bootstrapPath the path to the bootstrap file
+     * @return The bootstrap.conf as a BootstrapProperties object
+     * @throws IOException If the file is not readable
+     */
+    public static BootstrapProperties loadBootstrapProperties(final String bootstrapPath) throws IOException {
+        return BOOTSTRAP_PROPERTIES_LOADER.loadBootstrapProperties(bootstrapPath);
+    }
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties, extracted from
+     * {@code $NIFI_REGISTRY_HOME/conf/bootstrap.conf}.
+     *
+     * @param bootstrapPath the path to the bootstrap file (if null, returns the sensitive key
+     *                      found in $NIFI_REGISTRY_HOME/conf/bootstrap.conf)
+     * @return the key in hexadecimal format
+     * @throws IOException if the file is not readable
+     */
+    public static String extractKeyFromBootstrapFile(final String bootstrapPath) throws IOException {
+        return BOOTSTRAP_PROPERTIES_LOADER.extractKeyFromBootstrapFile(bootstrapPath);
+    }
+
+    /**
+     * Returns the default file path to {@code $NIFI_REGISTRY_HOME/conf/nifi-registry.properties}. If the system
+     * property nifi-registry.properties.file.path is not set, it will be set to the relative conf/nifi-registry.properties
+     *
+     * @return the path to the nifi-registry.properties file
+     */
+    public static String getDefaultApplicationPropertiesFilePath() {
+        return BOOTSTRAP_PROPERTIES_LOADER.getDefaultApplicationPropertiesFilePath();
+    }
+}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
index 191b5e2..c277a9c 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.security.crypto;
 
+import org.apache.nifi.registry.properties.util.NiFiRegistryBootstrapUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,7 +63,7 @@ public class BootstrapFileCryptoKeyProvider implements CryptoKeyProvider {
     @Override
     public String getKey() throws MissingCryptoKeyException {
         try {
-            return CryptoKeyLoader.extractKeyFromBootstrapFile(this.bootstrapFile);
+            return NiFiRegistryBootstrapUtils.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);
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
deleted file mode 100644
index d828773..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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;
-    }
-
-}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
deleted file mode 100644
index 0d1d5e2..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
deleted file mode 100644
index bad659f..0000000
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
+++ /dev/null
@@ -1,471 +0,0 @@
-/*
- * 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.bouncycastle.util.encoders.Hex
-import org.junit.*
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-
-import javax.crypto.Cipher
-import javax.crypto.spec.IvParameterSpec
-import javax.crypto.spec.SecretKeySpec
-import java.nio.charset.StandardCharsets
-import java.security.SecureRandom
-import java.security.Security
-
-@RunWith(JUnit4.class)
-class AESSensitivePropertyProviderTest extends GroovyTestCase {
-    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderTest.class)
-
-    private static final String KEY_128_HEX = "0123456789ABCDEFFEDCBA9876543210"
-    private static final String KEY_256_HEX = KEY_128_HEX * 2
-    private static final int IV_LENGTH = AESSensitivePropertyProvider.getIvLength()
-
-    private static final List<Integer> KEY_SIZES = getAvailableKeySizes()
-
-    private static final SecureRandom secureRandom = new SecureRandom()
-
-    private static final Base64.Encoder encoder = Base64.encoder
-    private static final Base64.Decoder decoder = Base64.decoder
-
-    @BeforeClass
-    static void setUpOnce() throws Exception {
-        Security.addProvider(new BouncyCastleProvider())
-
-        logger.metaClass.methodMissing = { String name, args ->
-            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
-        }
-    }
-
-    @Before
-    void setUp() throws Exception {
-
-    }
-
-    @After
-    void tearDown() throws Exception {
-
-    }
-
-    private static Cipher getCipher(boolean encrypt = true, int keySize = 256, byte[] iv = [0x00] * IV_LENGTH) {
-        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding")
-        String key = getKeyOfSize(keySize)
-        cipher.init((encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, new SecretKeySpec(Hex.decode(key), "AES"), new IvParameterSpec(iv))
-        logger.setup("Initialized a cipher in ${encrypt ? "encrypt" : "decrypt"} mode with a key of length ${keySize} bits")
-        cipher
-    }
-
-    private static String getKeyOfSize(int keySize = 256) {
-        switch (keySize) {
-            case 128:
-                return KEY_128_HEX
-            case 192:
-            case 256:
-                if (Cipher.getMaxAllowedKeyLength("AES") < keySize) {
-                    throw new IllegalArgumentException("The JCE unlimited strength cryptographic jurisdiction policies are not installed, so the max key size is 128 bits")
-                }
-                return KEY_256_HEX[0..<keySize.intdiv(4)]
-            default:
-                throw new IllegalArgumentException("Key size ${keySize} bits is not valid")
-        }
-    }
-
-    private static List<Integer> getAvailableKeySizes() {
-        if (Cipher.getMaxAllowedKeyLength("AES") > 128) {
-            [128, 192, 256]
-        } else {
-            [128]
-        }
-    }
-
-    private static String manipulateString(String input, int start = 0, int end = input?.length()) {
-        if ((input[start..end] as List).unique().size() == 1) {
-            throw new IllegalArgumentException("Can't manipulate a String where the entire range is identical [${input[start..end]}]")
-        }
-        List shuffled = input[start..end] as List
-        Collections.shuffle(shuffled)
-        String reconstituted = input[0..<start] + shuffled.join() + input[end + 1..-1]
-        return reconstituted != input ? reconstituted : manipulateString(input, start, end)
-    }
-
-    @Test
-    void testShouldProtectValue() throws Exception {
-        final String PLAINTEXT = "This is a plaintext value"
-
-        // Act
-        Map<Integer, String> CIPHER_TEXTS = KEY_SIZES.collectEntries { int keySize ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            [(keySize): spp.protect(PLAINTEXT)]
-        }
-        CIPHER_TEXTS.each { ks, ct -> logger.info("Encrypted for ${ks} length key: ${ct}") }
-
-        // Assert
-
-        // The IV generation is part of #protect, so the expected cipher text values must be generated after #protect has run
-        Map<Integer, Cipher> decryptionCiphers = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
-            // The 12 byte IV is the first 16 Base64-encoded characters of the "complete" cipher text
-            byte[] iv = decoder.decode(cipherText[0..<16])
-            [(keySize): getCipher(false, keySize, iv)]
-        }
-        Map<Integer, String> plaintexts = decryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
-            String cipherTextWithoutIVAndDelimiter = CIPHER_TEXTS[e.key][18..-1]
-            String plaintext = new String(e.value.doFinal(decoder.decode(cipherTextWithoutIVAndDelimiter)), StandardCharsets.UTF_8)
-            [(e.key): plaintext]
-        }
-        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
-
-        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
-    }
-
-    @Test
-    void testShouldHandleProtectEmptyValue() throws Exception {
-        final List<String> EMPTY_PLAINTEXTS = ["", "    ", null]
-
-        // Act
-        KEY_SIZES.collectEntries { int keySize ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            EMPTY_PLAINTEXTS.each { String emptyPlaintext ->
-                def msg = shouldFail(IllegalArgumentException) {
-                    spp.protect(emptyPlaintext)
-                }
-                logger.expected("${msg} for keySize ${keySize} and plaintext [${emptyPlaintext}]")
-
-                // Assert
-                assert msg == "Cannot encrypt an empty value"
-            }
-        }
-    }
-
-    @Test
-    void testShouldUnprotectValue() throws Exception {
-        // Arrange
-        final String PLAINTEXT = "This is a plaintext value"
-
-        Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { int keySize ->
-            byte[] iv = new byte[IV_LENGTH]
-            secureRandom.nextBytes(iv)
-            [(keySize): getCipher(true, keySize, iv)]
-        }
-
-        Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
-            String iv = encoder.encodeToString(e.value.getIV())
-            String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8)))
-            [(e.key): "${iv}||${cipherText}"]
-        }
-        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
-
-        // Act
-        Map<Integer, String> plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            [(keySize): spp.unprotect(cipherText)]
-        }
-        plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") }
-
-        // Assert
-        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
-    }
-
-    /**
-     * Tests inputs where the entire String is empty/blank space/{@code null}.
-     *
-     * @throws Exception
-     */
-    @Test
-    void testShouldHandleUnprotectEmptyValue() throws Exception {
-        // Arrange
-        final List<String> EMPTY_CIPHER_TEXTS = ["", "    ", null]
-
-        // Act
-        KEY_SIZES.each { int keySize ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
-                def msg = shouldFail(IllegalArgumentException) {
-                    spp.unprotect(emptyCipherText)
-                }
-                logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]")
-
-                // Assert
-                assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
-            }
-        }
-    }
-
-    @Test
-    void testShouldUnprotectValueWithWhitespace() throws Exception {
-        // Arrange
-        final String PLAINTEXT = "This is a plaintext value"
-
-        Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { int keySize ->
-            byte[] iv = new byte[IV_LENGTH]
-            secureRandom.nextBytes(iv)
-            [(keySize): getCipher(true, keySize, iv)]
-        }
-
-        Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
-            String iv = encoder.encodeToString(e.value.getIV())
-            String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8)))
-            [(e.key): "${iv}||${cipherText}"]
-        }
-        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
-
-        // Act
-        Map<Integer, String> plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            [(keySize): spp.unprotect("\t" + cipherText + "\n")]
-        }
-        plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") }
-
-        // Assert
-        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
-    }
-
-    @Test
-    void testShouldHandleUnprotectMalformedValue() throws Exception {
-        // Arrange
-        final String PLAINTEXT = "This is a plaintext value"
-
-        // Act
-        KEY_SIZES.each { int keySize ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            String cipherText = spp.protect(PLAINTEXT)
-            // Swap two characters in the cipher text
-            final String MALFORMED_CIPHER_TEXT = manipulateString(cipherText, 25, 28)
-            logger.info("Manipulated ${cipherText} to\n${MALFORMED_CIPHER_TEXT.padLeft(163)}")
-
-            def msg = shouldFail(SensitivePropertyProtectionException) {
-                spp.unprotect(MALFORMED_CIPHER_TEXT)
-            }
-            logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_CIPHER_TEXT}]")
-
-            // Assert
-            assert msg == "Error decrypting a protected value"
-        }
-    }
-
-    @Test
-    void testShouldHandleUnprotectMissingIV() throws Exception {
-        // Arrange
-        final String PLAINTEXT = "This is a plaintext value"
-
-        // Act
-        KEY_SIZES.each { int keySize ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            String cipherText = spp.protect(PLAINTEXT)
-
-            // Remove the IV from the "complete" cipher text
-            final String MISSING_IV_CIPHER_TEXT = cipherText[18..-1]
-            logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT.padLeft(172)}")
-
-            def msg = shouldFail(IllegalArgumentException) {
-                spp.unprotect(MISSING_IV_CIPHER_TEXT)
-            }
-            logger.expected("${msg} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT}]")
-
-            // Remove the IV from the "complete" cipher text but keep the delimiter
-            final String MISSING_IV_CIPHER_TEXT_WITH_DELIMITER = cipherText[16..-1]
-            logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER.padLeft(172)}")
-
-            def msgWithDelimiter = shouldFail(IllegalArgumentException) {
-                spp.unprotect(MISSING_IV_CIPHER_TEXT_WITH_DELIMITER)
-            }
-            logger.expected("${msgWithDelimiter} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER}]")
-
-            // Assert
-            assert msg == "The cipher text does not contain the delimiter || -- it should be of the form Base64(IV) || Base64(cipherText)"
-
-            // Assert
-            assert msgWithDelimiter == "The IV (0 bytes) must be at least 12 bytes"
-        }
-    }
-
-    /**
-     * Tests inputs which have a valid IV and delimiter but no "cipher text".
-     *
-     * @throws Exception
-     */
-    @Test
-    void testShouldHandleUnprotectEmptyCipherText() throws Exception {
-        // Arrange
-        final String IV_AND_DELIMITER = "${encoder.encodeToString("Bad IV value".getBytes(StandardCharsets.UTF_8))}||"
-        logger.info("IV and delimiter: ${IV_AND_DELIMITER}")
-
-        final List<String> EMPTY_CIPHER_TEXTS = ["", "      ", "\n"].collect { "${IV_AND_DELIMITER}${it}" }
-
-        // Act
-        KEY_SIZES.each { int keySize ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
-                def msg = shouldFail(IllegalArgumentException) {
-                    spp.unprotect(emptyCipherText)
-                }
-                logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]")
-
-                // Assert
-                assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
-            }
-        }
-    }
-
-    @Test
-    void testShouldHandleUnprotectMalformedIV() throws Exception {
-        // Arrange
-        final String PLAINTEXT = "This is a plaintext value"
-
-        // Act
-        KEY_SIZES.each { int keySize ->
-            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
-            logger.info("Initialized ${spp.name} with key size ${keySize}")
-            String cipherText = spp.protect(PLAINTEXT)
-            // Swap two characters in the IV
-            final String MALFORMED_IV_CIPHER_TEXT = manipulateString(cipherText, 8, 11)
-            logger.info("Manipulated ${cipherText} to\n${MALFORMED_IV_CIPHER_TEXT.padLeft(163)}")
-
-            def msg = shouldFail(SensitivePropertyProtectionException) {
-                spp.unprotect(MALFORMED_IV_CIPHER_TEXT)
-            }
-            logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_IV_CIPHER_TEXT}]")
-
-            // Assert
-            assert msg == "Error decrypting a protected value"
-        }
-    }
-
-    @Test
-    void testShouldGetIdentifierKeyWithDifferentMaxKeyLengths() throws Exception {
-        // Arrange
-        def keys = getAvailableKeySizes().collectEntries { int keySize ->
-            [(keySize): getKeyOfSize(keySize)]
-        }
-        logger.info("Keys: ${keys}")
-
-        // Act
-        keys.each { int size, String key ->
-            String identifierKey = new AESSensitivePropertyProvider(key).getIdentifierKey()
-            logger.info("Identifier key: ${identifierKey} for size ${size}")
-
-            // Assert
-            assert identifierKey =~ /aes\/gcm\/${size}/
-        }
-    }
-
-    @Test
-    void testShouldNotAllowEmptyKey() throws Exception {
-        // Arrange
-        final String INVALID_KEY = ""
-
-        // Act
-        def msg = shouldFail(SensitivePropertyProtectionException) {
-            AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
-        }
-
-        // Assert
-        assert msg == "The key cannot be empty"
-    }
-
-    @Test
-    void testShouldNotAllowIncorrectlySizedKey() throws Exception {
-        // Arrange
-        final String INVALID_KEY = "Z" * 31
-
-        // Act
-        def msg = shouldFail(SensitivePropertyProtectionException) {
-            AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
-        }
-
-        // Assert
-        assert msg == "The key must be a valid hexadecimal key"
-    }
-
-    @Test
-    void testShouldNotAllowInvalidKey() throws Exception {
-        // Arrange
-        final String INVALID_KEY = "Z" * 32
-
-        // Act
-        def msg = shouldFail(SensitivePropertyProtectionException) {
-            AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
-        }
-
-        // Assert
-        assert msg == "The key must be a valid hexadecimal key"
-    }
-
-    /**
-     * This test is to ensure internal consistency and allow for encrypting value for various property files
-     */
-    @Test
-    void testShouldEncryptArbitraryValues() {
-        // Arrange
-        def values = ["thisIsABadPassword", "thisIsABadSensitiveKeyPassword", "thisIsABadKeystorePassword", "thisIsABadKeyPassword", "thisIsABadTruststorePassword", "This is an encrypted banner message", "nififtw!"]
-
-        String key = "2C576A9585DB862F5ECBEE5B4FFFCCA1" //getKeyOfSize(128)
-        // key = "0" * 64
-
-        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
-
-        // Act
-        def encryptedValues = values.collect { String v ->
-            def encryptedValue = spp.protect(v)
-            logger.info("${v} -> ${encryptedValue}")
-            def (String iv, String cipherText) = encryptedValue.tokenize("||")
-            logger.info("Normal Base64 encoding would be ${encoder.encodeToString(decoder.decode(iv))}||${encoder.encodeToString(decoder.decode(cipherText))}")
-            encryptedValue
-        }
-
-        // Assert
-        assert values == encryptedValues.collect { spp.unprotect(it) }
-    }
-
-    /**
-     * This test is to ensure external compatibility in case someone encodes the encrypted value with Base64 and does not remove the padding
-     */
-    @Test
-    void testShouldDecryptPaddedValueWith256BitKey() {
-        // Arrange
-        Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
-
-        final String EXPECTED_VALUE = getKeyOfSize(256) // "thisIsABadKeyPassword"
-        String cipherText = "aYDkDKys1ENr3gp+||sTBPpMlIvHcOLTGZlfWct8r9RY8BuDlDkoaYmGJ/9m9af9tZIVzcnDwvYQAaIKxRGF7vI2yrY7Xd6x9GTDnWGiGiRXlaP458BBMMgfzH2O8"
-        String unpaddedCipherText = cipherText.replaceAll("=", "")
-
-        String key = "AAAABBBBCCCCDDDDEEEEFFFF00001111" * 2 // getKeyOfSize(256)
-
-        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
-
-        // Act
-        String rawValue = spp.unprotect(cipherText)
-        logger.info("Decrypted ${cipherText} to ${rawValue}")
-        String rawUnpaddedValue = spp.unprotect(unpaddedCipherText)
-        logger.info("Decrypted ${unpaddedCipherText} to ${rawUnpaddedValue}")
-
-        // Assert
-        assert rawValue == EXPECTED_VALUE
-        assert rawUnpaddedValue == EXPECTED_VALUE
-    }
-}
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
index c7a0a4d..92480d0 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
@@ -52,8 +52,8 @@ class NiFiRegistryPropertiesGroovyTest extends GroovyTestCase {
         try {
             filePath = NiFiRegistryPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath()
         } catch (URISyntaxException ex) {
-            throw new RuntimeException("Cannot load properties file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load properties file due to " +
+                    ex.getLocalizedMessage(), ex)
         }
 
         NiFiRegistryProperties properties = new NiFiRegistryProperties()
@@ -66,8 +66,8 @@ class NiFiRegistryPropertiesGroovyTest extends GroovyTestCase {
             return properties
         } catch (final Exception ex) {
             logger.error("Cannot load properties file due to " + ex.getLocalizedMessage())
-            throw new RuntimeException("Cannot load properties file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load properties file due to " +
+                    ex.getLocalizedMessage(), ex)
         }
     }
 
@@ -106,8 +106,9 @@ class NiFiRegistryPropertiesGroovyTest extends GroovyTestCase {
         // Arrange
 
         // Act
-        NiFiRegistryProperties properties = new NiFiRegistryProperties()
-        properties.setProperty("key", "value")
+        Properties props = new Properties()
+        props.setProperty("key", "value")
+        NiFiRegistryProperties properties = new NiFiRegistryProperties(props)
         logger.info("niFiProperties has ${properties.size()} properties: ${properties.getPropertyKeys()}")
         NiFiRegistryProperties emptyProperties = new NiFiRegistryProperties()
         logger.info("emptyProperties has ${emptyProperties.size()} properties: ${emptyProperties.getPropertyKeys()}")
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
index 58c8087..5fe7eed 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
@@ -17,7 +17,6 @@
 package org.apache.nifi.registry.properties
 
 import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.junit.After
 import org.junit.AfterClass
 import org.junit.Before
 import org.junit.BeforeClass
@@ -46,7 +45,7 @@ class NiFiRegistryPropertiesLoaderGroovyTest extends GroovyTestCase {
     private static final String PASSWORD_KEY_HEX_128 = "2C576A9585DB862F5ECBEE5B4FFFCCA1"
 
     @BeforeClass
-    public static void setUpOnce() throws Exception {
+    static void setUpOnce() throws Exception {
         Security.addProvider(new BouncyCastleProvider())
 
         logger.metaClass.methodMissing = { String name, args ->
@@ -55,21 +54,15 @@ class NiFiRegistryPropertiesLoaderGroovyTest extends GroovyTestCase {
     }
 
     @Before
-    public void setUp() throws Exception {
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        // Clear the sensitive property providers between runs
-        NiFiRegistryPropertiesLoader.@sensitivePropertyProviderFactory = null
+    void setUp() throws Exception {
     }
 
     @AfterClass
-    public static void tearDownOnce() {
+    static void tearDownOnce() {
     }
 
     @Test
-    public void testConstructorShouldCreateNewInstance() throws Exception {
+    void testConstructorShouldCreateNewInstance() throws Exception {
         // Arrange
 
         // Act
@@ -104,31 +97,6 @@ class NiFiRegistryPropertiesLoaderGroovyTest extends GroovyTestCase {
     }
 
     @Test
-    public void testShouldGetDefaultProviderKey() throws Exception {
-        // Arrange
-        final String expectedProviderKey = "aes/gcm/${Cipher.getMaxAllowedKeyLength("AES") > 128 ? 256 : 128}"
-        logger.info("Expected provider key: ${expectedProviderKey}")
-
-        // Act
-        String defaultKey = NiFiRegistryPropertiesLoader.getDefaultProviderKey()
-        logger.info("Default key: ${defaultKey}")
-        // Assert
-        assert defaultKey == expectedProviderKey
-    }
-
-    @Test
-    public void testShouldInitializeSensitivePropertyProviderFactory() throws Exception {
-        // Arrange
-        NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader()
-
-        // Act
-        propertiesLoader.initializeSensitivePropertyProviderFactory()
-
-        // Assert
-        assert propertiesLoader.@sensitivePropertyProviderFactory
-    }
-
-    @Test
     public void testShouldLoadUnprotectedPropertiesFromFile() throws Exception {
         // Arrange
         File unprotectedFile = new File("src/test/resources/conf/nifi-registry.properties")
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiRegistryPropertiesGroovyTest.groovy
similarity index 87%
rename from nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy
rename to nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiRegistryPropertiesGroovyTest.groovy
index 86c7fb4..b1bb526 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiRegistryPropertiesGroovyTest.groovy
@@ -16,6 +16,12 @@
  */
 package org.apache.nifi.registry.properties
 
+import org.apache.nifi.properties.ApplicationPropertiesProtector
+import org.apache.nifi.properties.MultipleSensitivePropertyProtectionException
+import org.apache.nifi.properties.PropertyProtectionScheme
+import org.apache.nifi.properties.SensitivePropertyProtectionException
+import org.apache.nifi.properties.SensitivePropertyProvider
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory
 import org.bouncycastle.jce.provider.BouncyCastleProvider
 import org.junit.After
 import org.junit.AfterClass
@@ -32,8 +38,8 @@ import javax.crypto.Cipher
 import java.security.Security
 
 @RunWith(JUnit4.class)
-class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
-    private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiPropertiesGroovyTest.class)
+class ProtectedNiFiRegistryPropertiesGroovyTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiRegistryPropertiesGroovyTest.class)
 
     private static final String KEYSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD
     private static final String KEY_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEY_PASSWD
@@ -83,39 +89,40 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
             throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable")
         }
 
-        NiFiRegistryProperties properties = new NiFiRegistryProperties()
         FileReader reader = new FileReader(file)
 
         try {
-            properties.load(reader)
+            final Properties props = new Properties()
+            props.load(reader)
+            NiFiRegistryProperties properties = new NiFiRegistryProperties(props)
             logger.info("Loaded {} properties from {}", properties.size(), file.getAbsolutePath())
 
             ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(properties)
 
             // If it has protected keys, inject the SPP
             if (protectedNiFiProperties.hasProtectedKeys()) {
-                protectedNiFiProperties.addSensitivePropertyProvider(new AESSensitivePropertyProvider(keyHex))
+                protectedNiFiProperties.addSensitivePropertyProvider(StandardSensitivePropertyProviderFactory.withKey(keyHex)
+                        .getProvider(PropertyProtectionScheme.AES_GCM))
             }
 
             return protectedNiFiProperties
         } catch (final Exception ex) {
             logger.error("Cannot load properties file due to " + ex.getLocalizedMessage())
-            throw new RuntimeException("Cannot load properties file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load properties file due to " +
+                    ex.getLocalizedMessage(), ex)
         }
     }
 
     private static File fileForResource(String resourcePath) {
         String filePath
         try {
-            URL resourceURL = ProtectedNiFiPropertiesGroovyTest.class.getResource(resourcePath)
+            URL resourceURL = ProtectedNiFiRegistryPropertiesGroovyTest.class.getResource(resourcePath)
             if (!resourceURL) {
                 throw new RuntimeException("File ${resourcePath} not found in class resources, cannot load.")
             }
             filePath = resourceURL.toURI().getPath()
         } catch (URISyntaxException ex) {
-            throw new RuntimeException("Cannot load resource file due to "
-                    + ex.getLocalizedMessage(), ex)
+            throw new RuntimeException("Cannot load resource file due to " + ex.getLocalizedMessage(), ex)
         }
         File file = new File(filePath)
         return file
@@ -287,16 +294,18 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
         boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
 
-        // While the value is "protected", the scheme is not registered, so treat it as raw
+        // While the value is "protected", the scheme is not registered
         logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
 
         // Act
-        NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
-        String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
-        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        def msg = shouldFail(IllegalStateException) {
+            NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+            String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+            logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        }
 
         // Assert
-        assert retrievedKeystorePassword == expectedKeystorePassword
+        assert msg == "No provider available for nifi.registry.security.keyPasswd"
         assert isSensitive
         assert isProtected
     }
@@ -340,7 +349,8 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
                 loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties", KEY_HEX_128)
 
         // Iterate over the protected keys and track the ones that fail to decrypt
-        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX_128)
+        SensitivePropertyProvider spp = StandardSensitivePropertyProviderFactory.withKey(KEY_HEX_128)
+                .getProvider(PropertyProtectionScheme.AES_GCM)
         Set<String> malformedKeys = properties.getProtectedPropertyKeys()
                 .findAll { String key, String scheme -> scheme == spp.identifierKey }
                 .keySet()
@@ -384,19 +394,21 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Read raw value from properties: ${expectedKeystorePassword}")
 
         // Overwrite the internal cache
-        properties.localProviderCache = [:]
+        properties.getSensitivePropertyProviders().clear()
 
         boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
         boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
         logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
 
         // Act
-        NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
-        String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
-        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        def msg = shouldFail(IllegalStateException) {
+            NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+            String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+            logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        }
 
         // Assert
-        assert retrievedKeystorePassword == expectedKeystorePassword
+        assert msg == "No provider available for nifi.registry.security.keyPasswd"
         assert isSensitive
         assert isProtected
     }
@@ -455,13 +467,17 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
 
         // Act
-        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        double percentProtected = getPercentOfSensitivePropertiesProtected(properties)
         logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
 
         // Assert
         assert percentProtected == 0.0
     }
 
+    private static double getPercentOfSensitivePropertiesProtected(final ProtectedNiFiRegistryProperties properties) {
+        return (int) Math.round(properties.getProtectedPropertyKeys().size() / ((double) properties.getPopulatedSensitivePropertyKeys().size()) * 100);
+    }
+
     @Test
     void testShouldGetPercentageOfSensitivePropertiesProtected_75() throws Exception {
         // Arrange
@@ -472,7 +488,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
 
         // Act
-        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        double percentProtected = getPercentOfSensitivePropertiesProtected(properties)
         logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
 
         // Assert
@@ -489,7 +505,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
 
         // Act
-        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        double percentProtected = getPercentOfSensitivePropertiesProtected(properties)
         logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
 
         // Assert
@@ -500,13 +516,13 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
     void testInstanceWithNoProtectedPropertiesShouldNotLoadSPP() throws Exception {
         // Arrange
         ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties")
-        assert properties.@localProviderCache?.isEmpty()
+        assert properties.getSensitivePropertyProviders().isEmpty()
 
         logger.info("Has protected properties: ${properties.hasProtectedKeys()}")
         assert !properties.hasProtectedKeys()
 
         // Act
-        Map localCache = properties.@localProviderCache
+        Map localCache = properties.getSensitivePropertyProviders()
         logger.info("Internal cache ${localCache} has ${localCache.size()} providers loaded")
 
         // Assert
@@ -537,10 +553,10 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
     void testShouldNotAddNullSensitivePropertyProvider() throws Exception {
         // Arrange
         ProtectedNiFiRegistryProperties properties = new ProtectedNiFiRegistryProperties()
-        assert properties.@localProviderCache?.isEmpty()
+        assert properties.getSensitivePropertyProviders().isEmpty()
 
         // Act
-        def msg = shouldFail(IllegalArgumentException) {
+        def msg = shouldFail(NullPointerException) {
             properties.addSensitivePropertyProvider(null)
         }
         logger.info(msg)
@@ -589,7 +605,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         ProtectedNiFiRegistryProperties protectedNiFiProperties = loadFromResourceFile("/conf/nifi-registry.properties")
         logger.info("Loaded ${protectedNiFiProperties.size()} properties from conf/nifi.properties")
 
-        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        int hashCode = protectedNiFiProperties.getApplicationProperties().hashCode()
         logger.info("Hash code of internal instance: ${hashCode}")
 
         // Act
@@ -599,7 +615,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Assert
         assert unprotectedNiFiProperties.size() == protectedNiFiProperties.size()
         assert unprotectedNiFiProperties.getPropertyKeys().every {
-            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+            !unprotectedNiFiProperties.getProperty(it).endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)
         }
         logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
         assert unprotectedNiFiProperties.hashCode() == hashCode
@@ -614,7 +630,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size()
         int protectionSchemeCount = protectedNiFiProperties
                 .getPropertyKeysIncludingProtectionSchemes()
-                .findAll { it.endsWith(".protected") }
+                .findAll { it.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) }
                 .size()
         int expectedUnprotectedPropertyCount = protectedNiFiProperties.size()
 
@@ -630,7 +646,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
 
         logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}")
 
-        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        int hashCode = protectedNiFiProperties.getApplicationProperties().hashCode()
         logger.info("Hash code of internal instance: ${hashCode}")
 
         // Act
@@ -640,7 +656,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Assert
         assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount
         assert unprotectedNiFiProperties.getPropertyKeys().every {
-            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+            !unprotectedNiFiProperties.getProperty(it).endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)
         }
         logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
         assert unprotectedNiFiProperties.hashCode() != hashCode
@@ -656,7 +672,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size()
         int protectionSchemeCount = protectedNiFiProperties
                 .getPropertyKeysIncludingProtectionSchemes()
-                .findAll { it.endsWith(".protected") }
+                .findAll { it.endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX) }
                 .size()
         int expectedUnprotectedPropertyCount = protectedNiFiProperties.size()
 
@@ -672,7 +688,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
 
         logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}")
 
-        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        int hashCode = protectedNiFiProperties.getApplicationProperties().hashCode()
         logger.info("Hash code of internal instance: ${hashCode}")
 
         // Act
@@ -682,7 +698,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
         // Assert
         assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount
         assert unprotectedNiFiProperties.getPropertyKeys().every {
-            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+            !unprotectedNiFiProperties.getProperty(it).endsWith(ApplicationPropertiesProtector.PROTECTED_KEY_SUFFIX)
         }
         logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
         assert unprotectedNiFiProperties.hashCode() != hashCode
@@ -691,14 +707,15 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
     @Test
     void testShouldCalculateSize() {
         // Arrange
-        NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties
+        Properties props = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as Properties
+        NiFiRegistryProperties rawProperties = new NiFiRegistryProperties(props)
         ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties)
-        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}")
+        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.getPropertyKeys().join(", ")}")
 
         // Act
         int protectedSize = protectedNiFiProperties.size()
         logger.info("Protected properties (${protectedNiFiProperties.size()}): " +
-                "${protectedNiFiProperties.getPropertyKeysExcludingProtectionSchemes().join(", ")}")
+                "${protectedNiFiProperties.getPropertyKeys().join(", ")}")
 
         // Assert
         assert protectedSize == rawProperties.size() - 1
@@ -707,25 +724,27 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
     @Test
     void testGetPropertyKeysShouldMatchSize() {
         // Arrange
-        NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties
+        Properties props = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as Properties
+        NiFiRegistryProperties rawProperties = new NiFiRegistryProperties(props)
         ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties)
-        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}")
+        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.getPropertyKeys().join(", ")}")
 
         // Act
-        def filteredKeys = protectedNiFiProperties.getPropertyKeysExcludingProtectionSchemes()
+        def filteredKeys = protectedNiFiProperties.getPropertyKeys()
         logger.info("Protected properties (${protectedNiFiProperties.size()}): ${filteredKeys.join(", ")}")
 
         // Assert
         assert protectedNiFiProperties.size() == rawProperties.size() - 1
-        assert filteredKeys == rawProperties.keySet() - "key.protected"
+        assert filteredKeys == rawProperties.getPropertyKeys() - "key.protected"
     }
 
     @Test
     void testShouldGetPropertyKeysIncludingProtectionSchemes() {
         // Arrange
-        NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties
+        Properties props = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as Properties
+        NiFiRegistryProperties rawProperties = new NiFiRegistryProperties(props)
         ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties)
-        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}")
+        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.getPropertyKeys().join(", ")}")
 
         // Act
         def allKeys = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes()
@@ -733,7 +752,7 @@ class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
 
         // Assert
         assert allKeys.size() == rawProperties.size()
-        assert allKeys == rawProperties.keySet()
+        assert allKeys == rawProperties.getPropertyKeys()
     }
 
 }
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapUtilsGroovyTest.groovy
similarity index 81%
rename from nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy
rename to nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapUtilsGroovyTest.groovy
index 6d8623e..465a122 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy
+++ b/nifi-registry/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/util/NiFiRegistryBootstrapUtilsGroovyTest.groovy
@@ -14,10 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.security.crypto
+package org.apache.nifi.registry.properties.util
 
 import org.apache.commons.lang3.SystemUtils
-import org.apache.nifi.registry.security.crypto.CryptoKeyLoader
 import org.apache.nifi.registry.security.crypto.CryptoKeyProvider
 import org.bouncycastle.jce.provider.BouncyCastleProvider
 import org.junit.Assume
@@ -33,9 +32,9 @@ import java.nio.file.attribute.PosixFilePermission
 import java.security.Security
 
 @RunWith(JUnit4.class)
-class CryptoKeyLoaderGroovyTest extends GroovyTestCase {
+class NiFiRegistryBootstrapUtilsGroovyTest extends GroovyTestCase {
 
-    private static final Logger logger = LoggerFactory.getLogger(CryptoKeyLoaderGroovyTest.class)
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryBootstrapUtilsGroovyTest.class)
 
     private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
     private static final String KEY_HEX_256 = KEY_HEX_128 * 2
@@ -57,7 +56,7 @@ class CryptoKeyLoaderGroovyTest extends GroovyTestCase {
         final String expectedKey = KEY_HEX_256
 
         // Act
-        String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.conf")
+        String key = NiFiRegistryBootstrapUtils.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.conf")
 
         // Assert
         assert key == expectedKey
@@ -68,7 +67,7 @@ class CryptoKeyLoaderGroovyTest extends GroovyTestCase {
         // Arrange
 
         // Act
-        String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key_line.conf")
+        String key = NiFiRegistryBootstrapUtils.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key_line.conf")
 
         // Assert
         assert key == CryptoKeyProvider.EMPTY_KEY
@@ -79,7 +78,7 @@ class CryptoKeyLoaderGroovyTest extends GroovyTestCase {
         // Arrange
 
         // Act
-        String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key.conf")
+        String key = NiFiRegistryBootstrapUtils.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key.conf")
 
         // Assert
         assert key == CryptoKeyProvider.EMPTY_KEY
@@ -91,7 +90,7 @@ class CryptoKeyLoaderGroovyTest extends GroovyTestCase {
 
         // Act
         def msg = shouldFail(IOException) {
-            CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.missing.conf")
+            NiFiRegistryBootstrapUtils.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.missing.conf")
         }
         logger.info(msg)
 
@@ -110,7 +109,7 @@ class CryptoKeyLoaderGroovyTest extends GroovyTestCase {
 
             // Act
             def msg = shouldFail(IOException) {
-                CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.unreadable_file_permissions.conf")
+                NiFiRegistryBootstrapUtils.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.unreadable_file_permissions.conf")
             }
             logger.info(msg)
 
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java
index 455ab9d..0123e39 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java
@@ -16,10 +16,10 @@
  */
 package org.apache.nifi.registry;
 
+import org.apache.nifi.properties.SensitivePropertyProtectionException;
 import org.apache.nifi.registry.jetty.JettyServer;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
 import org.apache.nifi.registry.properties.NiFiRegistryPropertiesLoader;
-import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
 import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider;
 import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
 import org.apache.nifi.registry.security.crypto.MissingCryptoKeyException;
@@ -42,13 +42,6 @@ public class NiFiRegistry {
 
     public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.registry.bootstrap.listen.port";
 
-    public static final String NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY = "nifi.registry.properties.file.path";
-    public static final String NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY = "nifi.registry.bootstrap.config.file.path";
-    public static final String NIFI_REGISTRY_BOOTSTRAP_DOCS_DIR_PROPERTY = "nifi.registry.bootstrap.config.docs.dir";
-
-    public static final String RELATIVE_BOOTSTRAP_FILE_LOCATION = "conf/bootstrap.conf";
-    public static final String RELATIVE_PROPERTIES_FILE_LOCATION = "conf/nifi-registry.properties";
-    public static final String RELATIVE_DOCS_LOCATION = "docs";
 
     private final JettyServer server;
     private final BootstrapListener bootstrapListener;
@@ -106,7 +99,8 @@ public class NiFiRegistry {
         SLF4JBridgeHandler.removeHandlersForRootLogger();
         SLF4JBridgeHandler.install();
 
-        final String docsDir = System.getProperty(NIFI_REGISTRY_BOOTSTRAP_DOCS_DIR_PROPERTY, RELATIVE_DOCS_LOCATION);
+        final String docsDir = System.getProperty(NiFiRegistryProperties.NIFI_REGISTRY_BOOTSTRAP_DOCS_DIR_PROPERTY,
+                NiFiRegistryProperties.RELATIVE_DOCS_LOCATION);
 
         final long startTime = System.nanoTime();
         server = new JettyServer(properties, masterKeyProvider, docsDir);
@@ -168,7 +162,8 @@ public class NiFiRegistry {
     }
 
     public static CryptoKeyProvider getMasterKeyProvider() {
-        final String bootstrapConfigFilePath = System.getProperty(NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY, RELATIVE_BOOTSTRAP_FILE_LOCATION);
+        final String bootstrapConfigFilePath = System.getProperty(NiFiRegistryProperties.NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY,
+                NiFiRegistryProperties.RELATIVE_BOOTSTRAP_FILE_LOCATION);
         CryptoKeyProvider masterKeyProvider = new BootstrapFileCryptoKeyProvider(bootstrapConfigFilePath);
         LOGGER.info("Read property protection key from {}", bootstrapConfigFilePath);
         return masterKeyProvider;
@@ -186,7 +181,8 @@ public class NiFiRegistry {
         try {
             try {
                 // Load properties using key. If properties are protected and key missing, throw RuntimeException
-                final String nifiRegistryPropertiesFilePath = System.getProperty(NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY, RELATIVE_PROPERTIES_FILE_LOCATION);
+                final String nifiRegistryPropertiesFilePath = System.getProperty(NiFiRegistryProperties.NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY,
+                        NiFiRegistryProperties.RELATIVE_PROPERTIES_FILE_LOCATION);
                 final NiFiRegistryProperties properties = NiFiRegistryPropertiesLoader.withKey(key).load(nifiRegistryPropertiesFilePath);
                 LOGGER.info("Loaded {} properties", properties.size());
                 return properties;
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
index 0fe0538..7a30d37 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
@@ -40,6 +40,7 @@ import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
 import java.io.FileReader;
 import java.io.IOException;
+import java.util.Properties;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
@@ -176,17 +177,17 @@ public abstract class IntegrationTestBase {
      * @param propertiesFilePath The location of the properties file
      * @return A NiFIRegistryProperties instance based on the properties file contents
      */
-    static NiFiRegistryProperties loadNiFiRegistryProperties(String propertiesFilePath) {
-        NiFiRegistryProperties properties = new NiFiRegistryProperties();
+    static NiFiRegistryProperties loadNiFiRegistryProperties(final String propertiesFilePath) {
+        final Properties props = new Properties();
         try (final FileReader reader = new FileReader(propertiesFilePath)) {
-            properties.load(reader);
+            props.load(reader);
+            return new NiFiRegistryProperties(props);
         } catch (final IOException ioe) {
             throw new RuntimeException("Unable to load properties: " + ioe, ioe);
         }
-        return properties;
     }
 
-    private static Client createClientFromConfig(NiFiRegistryClientConfig registryClientConfig) {
+    private static Client createClientFromConfig(final NiFiRegistryClientConfig registryClientConfig) {
 
         final ClientConfig clientConfig = new ClientConfig();
         clientConfig.register(jacksonJaxbJsonProvider());
diff --git a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
index f6d1248..2bbefc3 100644
--- a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
+++ b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -17,6 +17,9 @@
 package org.apache.nifi.registry.web.api;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.properties.PropertyProtectionScheme;
+import org.apache.nifi.properties.SensitivePropertyProvider;
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory;
 import org.apache.nifi.registry.SecureLdapTestApiApplication;
 import org.apache.nifi.registry.authorization.AccessPolicy;
 import org.apache.nifi.registry.authorization.AccessPolicySummary;
@@ -33,9 +36,7 @@ import org.apache.nifi.registry.client.UserClient;
 import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
 import org.apache.nifi.registry.client.impl.request.BearerTokenRequestConfig;
 import org.apache.nifi.registry.extension.ExtensionManager;
-import org.apache.nifi.registry.properties.AESSensitivePropertyProvider;
 import org.apache.nifi.registry.properties.NiFiRegistryProperties;
-import org.apache.nifi.registry.properties.SensitivePropertyProvider;
 import org.apache.nifi.registry.revision.entity.RevisionInfo;
 import org.apache.nifi.registry.security.authorization.Authorizer;
 import org.apache.nifi.registry.security.authorization.AuthorizerFactory;
@@ -140,7 +141,8 @@ public class SecureLdapIT extends IntegrationTestBase {
         @Primary
         @Bean
         public static SensitivePropertyProvider sensitivePropertyProvider() throws Exception {
-            return new AESSensitivePropertyProvider(getNiFiRegistryMasterKeyProvider().getKey());
+            return StandardSensitivePropertyProviderFactory.withKey(getNiFiRegistryMasterKeyProvider().getKey())
+                    .getProvider(PropertyProtectionScheme.AES_GCM);
         }
 
         private static CryptoKeyProvider getNiFiRegistryMasterKeyProvider() {
diff --git a/nifi-registry/nifi-registry-toolkit/nifi-registry-toolkit-persistence/src/main/java/org/apache/nifi/registry/toolkit/persistence/FlowPersistenceProviderMigrator.java b/nifi-registry/nifi-registry-toolkit/nifi-registry-toolkit-persistence/src/main/java/org/apache/nifi/registry/toolkit/persistence/FlowPersistenceProviderMigrator.java
index a510281..266d248 100644
--- a/nifi-registry/nifi-registry-toolkit/nifi-registry-toolkit-persistence/src/main/java/org/apache/nifi/registry/toolkit/persistence/FlowPersistenceProviderMigrator.java
+++ b/nifi-registry/nifi-registry-toolkit/nifi-registry-toolkit-persistence/src/main/java/org/apache/nifi/registry/toolkit/persistence/FlowPersistenceProviderMigrator.java
@@ -41,6 +41,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.jdbc.core.JdbcTemplate;
 
 import javax.sql.DataSource;
+import java.util.Properties;
 
 public class FlowPersistenceProviderMigrator {
     private static final Logger log = LoggerFactory.getLogger(FlowPersistenceProviderMigrator.class);
@@ -91,12 +92,13 @@ public class FlowPersistenceProviderMigrator {
         new FlowPersistenceProviderMigrator().doMigrate(fromMetadataService, fromPersistenceProvider, toPersistenceProvider);
     }
 
-    private static NiFiRegistryProperties createToProperties(CommandLine commandLine, NiFiRegistryProperties fromProperties) {
-        NiFiRegistryProperties toProperties = new NiFiRegistryProperties();
-        for (String propertyKey : fromProperties.getPropertyKeys()) {
-            toProperties.setProperty(propertyKey, fromProperties.getProperty(propertyKey));
+    private static NiFiRegistryProperties createToProperties(final CommandLine commandLine, final NiFiRegistryProperties fromProperties) {
+        final Properties props = new Properties();
+        for (final String propertyKey : fromProperties.getPropertyKeys()) {
+            props.setProperty(propertyKey, fromProperties.getProperty(propertyKey));
         }
-        toProperties.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, commandLine.getOptionValue('t'));
+        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, commandLine.getOptionValue('t'));
+        final NiFiRegistryProperties toProperties = new NiFiRegistryProperties(props);
         return toProperties;
     }
 
diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy
index de5aeb4..f9a2f9f 100644
--- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy
+++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/nodemanager/NodeManagerTool.groovy
@@ -28,6 +28,7 @@ import org.apache.nifi.toolkit.admin.client.ClientFactory
 import org.apache.nifi.toolkit.admin.client.NiFiClientFactory
 import org.apache.nifi.toolkit.admin.client.NiFiClientUtil
 import org.apache.nifi.toolkit.admin.util.AdminUtil
+import org.apache.nifi.util.NiFiBootstrapUtils
 import org.apache.nifi.util.NiFiProperties
 import org.apache.nifi.util.StringUtils
 import org.apache.nifi.web.api.dto.NodeDTO
@@ -271,7 +272,7 @@ public class NodeManagerTool extends AbstractAdminTool {
                 String nifiConfDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("conf.dir"), bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath())
                 String nifiLibDir = AdminUtil.getRelativeDirectory(bootstrapProperties.getProperty("lib.dir"), bootstrapConf.getCanonicalFile().getParentFile().getParentFile().getCanonicalPath())
                 String nifiPropertiesFileName = nifiConfDir + File.separator +"nifi.properties"
-                final String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfFileName)
+                final String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile(bootstrapConfFileName)
                 final NiFiProperties niFiProperties = NiFiPropertiesLoader.withKey(key).load(nifiPropertiesFileName)
                 final String operation = commandLine.getOptionValue(OPERATION)
 
diff --git a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy
index df538e8..136bcd8 100644
--- a/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy
+++ b/nifi-toolkit/nifi-toolkit-admin/src/main/groovy/org/apache/nifi/toolkit/admin/notify/NotificationTool.groovy
@@ -29,6 +29,7 @@ import org.apache.nifi.toolkit.admin.client.ClientFactory
 import org.apache.nifi.toolkit.admin.client.NiFiClientFactory
 import org.apache.nifi.toolkit.admin.client.NiFiClientUtil
 import org.apache.nifi.toolkit.admin.util.AdminUtil
+import org.apache.nifi.util.NiFiBootstrapUtils
 import org.apache.nifi.util.NiFiProperties
 import org.apache.nifi.web.api.dto.BulletinDTO
 import org.apache.nifi.web.api.entity.BulletinEntity
@@ -88,7 +89,7 @@ public class NotificationTool extends AbstractAdminTool {
             logger.info("Loading nifi properties for host information")
         }
 
-        final String key = NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfFile)
+        final String key = NiFiBootstrapUtils.extractKeyFromBootstrapFile(bootstrapConfFile)
         final NiFiProperties niFiProperties = NiFiPropertiesLoader.withKey(key).load(nifiPropertiesFile)
         final Client client =  clientFactory.getClient(niFiProperties,nifiInstallDir)
         final String url = NiFiClientUtil.getUrl(niFiProperties,NOTIFICATION_ENDPOINT)
diff --git a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy
index ddb445f..e469980 100644
--- a/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy
+++ b/nifi-toolkit/nifi-toolkit-admin/src/test/groovy/org/apache/nifi/toolkit/admin/client/NiFiClientFactorySpec.groovy
@@ -23,6 +23,7 @@ import org.apache.nifi.properties.NiFiPropertiesLoader
 import org.apache.nifi.security.util.CertificateUtils
 import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandalone
 import org.apache.nifi.toolkit.tls.standalone.TlsToolkitStandaloneCommandLine
+import org.apache.nifi.util.NiFiBootstrapUtils
 import org.apache.nifi.util.NiFiProperties
 import org.bouncycastle.asn1.x500.X500Name
 import org.bouncycastle.asn1.x500.X500NameBuilder
@@ -90,7 +91,7 @@ class NiFiClientFactorySpec extends Specification {
 
         def bootstrapConfFile = "src/test/resources/notify/conf/bootstrap.conf"
         def nifiPropertiesFile = "src/test/resources/notify/conf/nifi-secured.properties"
-        def key = NiFiPropertiesLoader.extractKeyFromBootstrapFile(bootstrapConfFile)
+        def key = NiFiBootstrapUtils.extractKeyFromBootstrapFile(bootstrapConfFile)
         def NiFiProperties niFiProperties = NiFiPropertiesLoader.withKey(key).load(nifiPropertiesFile)
         def clientFactory = new NiFiClientFactory()
 
diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/pom.xml b/nifi-toolkit/nifi-toolkit-encrypt-config/pom.xml
index 319e2d9..606664a 100644
--- a/nifi-toolkit/nifi-toolkit-encrypt-config/pom.xml
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/pom.xml
@@ -35,6 +35,11 @@
             <version>1.14.0-SNAPSHOT</version>
         </dependency>
         <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-properties</artifactId>
+            <version>1.14.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-properties-loader</artifactId>
             <version>1.14.0-SNAPSHOT</version>
@@ -168,4 +173,4 @@
             </plugin>
         </plugins>
     </build>
-</project>
\ No newline at end of file
+</project>
diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
index f6f4453..9f48d6c 100644
--- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/properties/ConfigEncryptionTool.groovy
@@ -31,9 +31,10 @@ import org.apache.nifi.encrypt.PropertyEncryptor
 import org.apache.nifi.encrypt.PropertyEncryptorFactory
 import org.apache.nifi.flow.encryptor.FlowEncryptor
 import org.apache.nifi.flow.encryptor.StandardFlowEncryptor
-import org.apache.nifi.security.kms.CryptoUtils
+import org.apache.nifi.registry.properties.util.NiFiRegistryBootstrapUtils
 import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
 import org.apache.nifi.toolkit.tls.commandLine.ExitCode
+import org.apache.nifi.util.NiFiBootstrapUtils
 import org.apache.nifi.util.NiFiProperties
 import org.apache.nifi.util.console.TextDevice
 import org.apache.nifi.util.console.TextDevices
@@ -46,16 +47,17 @@ import org.xml.sax.SAXException
 import javax.crypto.BadPaddingException
 import javax.crypto.Cipher
 import java.nio.charset.StandardCharsets
+import java.nio.file.Files
 import java.nio.file.Path
 import java.nio.file.Paths
 import java.nio.file.StandardCopyOption
 import java.security.KeyException
 import java.security.Security
+import java.util.function.Supplier
 import java.util.regex.Matcher
 import java.util.zip.GZIPInputStream
 import java.util.zip.GZIPOutputStream
 import java.util.zip.ZipException
-import java.nio.file.Files
 
 class ConfigEncryptionTool {
     private static final Logger logger = LoggerFactory.getLogger(ConfigEncryptionTool.class)
@@ -70,6 +72,10 @@ class ConfigEncryptionTool {
     public static flowXmlPath
     public String outputFlowXmlPath
 
+    static final PropertyProtectionScheme DEFAULT_PROTECTION_SCHEME = PropertyProtectionScheme.AES_GCM
+
+    private PropertyProtectionScheme protectionScheme = DEFAULT_PROTECTION_SCHEME
+    private PropertyProtectionScheme migrationProtectionScheme = DEFAULT_PROTECTION_SCHEME
     private String keyHex
     private String migrationKeyHex
     private String password
@@ -77,6 +83,7 @@ class ConfigEncryptionTool {
 
     // This is the raw value used in nifi.sensitive.props.key
     private String flowPropertiesPassword
+    private String existingFlowPropertiesPassword
 
     private String newFlowAlgorithm
     private String newFlowProvider
@@ -109,9 +116,11 @@ class ConfigEncryptionTool {
     private static final String FLOW_XML_ARG = "flowXml"
     private static final String OUTPUT_FLOW_XML_ARG = "outputFlowXml"
     private static final String KEY_ARG = "key"
+    private static final String PROTECTION_SCHEME_ARG = "protectionScheme"
     private static final String PASSWORD_ARG = "password"
     private static final String KEY_MIGRATION_ARG = "oldKey"
     private static final String PASSWORD_MIGRATION_ARG = "oldPassword"
+    private static final String PROTECTION_SCHEME_MIGRATION_ARG = "oldProtectionScheme"
     private static final String USE_KEY_ARG = "useRawKey"
     private static final String MIGRATION_ARG = "migrate"
     private static final String PROPS_KEY_ARG = "propsKey"
@@ -120,6 +129,9 @@ class ConfigEncryptionTool {
     private static final String NEW_FLOW_PROVIDER_ARG = "newFlowProvider"
     private static final String TRANSLATE_CLI_ARG = "translateCli"
 
+    private static final String PROTECTION_SCHEME_DESC = String.format("Selects the protection scheme for encrypted properties.  " +
+            "Valid values are: [%s] (default is %s)", PropertyProtectionScheme.values().join(", "), DEFAULT_PROTECTION_SCHEME.name())
+
     // Static holder to avoid re-generating the options object multiple times in an invocation
     private static Options staticOptions
 
@@ -230,13 +242,15 @@ class ConfigEncryptionTool {
         options.addOption(Option.builder("u").longOpt(OUTPUT_AUTHORIZERS_ARG).hasArg(true).argName("file").desc("The destination authorizers.xml file containing protected config values (will not modify input authorizers.xml)").build())
         options.addOption(Option.builder("f").longOpt(FLOW_XML_ARG).hasArg(true).argName("file").desc("The flow.xml.gz file currently protected with old password (will be overwritten unless -g is specified)").build())
         options.addOption(Option.builder("g").longOpt(OUTPUT_FLOW_XML_ARG).hasArg(true).argName("file").desc("The destination flow.xml.gz file containing protected config values (will not modify input flow.xml.gz)").build())
-        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_CONF_ARG).hasArg(true).argName("file").desc("The bootstrap.conf file to persist root key").build())
+        options.addOption(Option.builder("b").longOpt(BOOTSTRAP_CONF_ARG).hasArg(true).argName("file").desc("The bootstrap.conf file to persist root key and to optionally provide any configuration for the protection scheme.").build())
+        options.addOption(Option.builder("S").longOpt(PROTECTION_SCHEME_ARG).hasArg(true).argName("protectionScheme").desc(PROTECTION_SCHEME_DESC).build())
         options.addOption(Option.builder("k").longOpt(KEY_ARG).hasArg(true).argName("keyhex").desc("The raw hexadecimal key to use to encrypt the sensitive properties").build())
         options.addOption(Option.builder("e").longOpt(KEY_MIGRATION_ARG).hasArg(true).argName("keyhex").desc("The old raw hexadecimal key to use during key migration").build())
+        options.addOption(Option.builder("H").longOpt(PROTECTION_SCHEME_MIGRATION_ARG).hasArg(true).argName("protectionScheme").desc("The old protection scheme to use during encryption migration (see --protectionScheme for possible values).  Default is " + DEFAULT_PROTECTION_SCHEME.name()).build())
         options.addOption(Option.builder("p").longOpt(PASSWORD_ARG).hasArg(true).argName("password").desc("The password from which to derive the key to use to encrypt the sensitive properties").build())
         options.addOption(Option.builder("w").longOpt(PASSWORD_MIGRATION_ARG).hasArg(true).argName("password").desc("The old password from which to derive the key during migration").build())
         options.addOption(Option.builder("r").longOpt(USE_KEY_ARG).hasArg(false).desc("If provided, the secure console will prompt for the raw key value in hexadecimal form").build())
-        options.addOption(Option.builder("m").longOpt(MIGRATION_ARG).hasArg(false).desc("If provided, the nifi.properties and/or login-identity-providers.xml sensitive properties will be re-encrypted with a new key").build())
+        options.addOption(Option.builder("m").longOpt(MIGRATION_ARG).hasArg(false).desc("If provided, the nifi.properties and/or login-identity-providers.xml sensitive properties will be re-encrypted with the new scheme").build())
         options.addOption(Option.builder("x").longOpt(DO_NOT_ENCRYPT_NIFI_PROPERTIES_ARG).hasArg(false).desc("If provided, the properties in flow.xml.gz will be re-encrypted with a new key but the nifi.properties and/or login-identity-providers.xml files will not be modified").build())
         options.addOption(Option.builder("s").longOpt(PROPS_KEY_ARG).hasArg(true).argName("password|keyhex").desc("The password or key to use to encrypt the sensitive processor properties in flow.xml.gz").build())
         options.addOption(Option.builder("A").longOpt(NEW_FLOW_ALGORITHM_ARG).hasArg(true).argName("algorithm").desc("The algorithm to use to encrypt the sensitive processor properties in flow.xml.gz").build())
@@ -312,6 +326,10 @@ class ConfigEncryptionTool {
                 }
             }
 
+            if (commandLine.hasOption(PROTECTION_SCHEME_ARG)) {
+                protectionScheme = PropertyProtectionScheme.valueOf(commandLine.getOptionValue(PROTECTION_SCHEME_ARG))
+            }
+
             // If translating nifi.properties to CLI format, none of the remaining parsing is necessary
             if (translatingCli) {
 
@@ -410,17 +428,23 @@ class ConfigEncryptionTool {
                 if (isVerbose) {
                     logger.info("Key migration mode activated")
                 }
-                if (commandLine.hasOption(PASSWORD_MIGRATION_ARG)) {
-                    usingPasswordMigration = true
-                    if (commandLine.hasOption(KEY_MIGRATION_ARG)) {
-                        printUsageAndThrow("Only one of '-w'/'--${PASSWORD_MIGRATION_ARG}' and '-e'/'--${KEY_MIGRATION_ARG}' can be used", ExitCode.INVALID_ARGS)
+                if (commandLine.hasOption(PROTECTION_SCHEME_MIGRATION_ARG)) {
+                    migrationProtectionScheme = PropertyProtectionScheme.valueOf(commandLine.getOptionValue(PROTECTION_SCHEME_MIGRATION_ARG))
+                }
+
+                if (migrationProtectionScheme.requiresSecretKey()) {
+                    if (commandLine.hasOption(PASSWORD_MIGRATION_ARG)) {
+                        usingPasswordMigration = true
+                        if (commandLine.hasOption(KEY_MIGRATION_ARG)) {
+                            printUsageAndThrow("Only one of '-w'/'--${PASSWORD_MIGRATION_ARG}' and '-e'/'--${KEY_MIGRATION_ARG}' can be used", ExitCode.INVALID_ARGS)
+                        } else {
+                            migrationPassword = commandLine.getOptionValue(PASSWORD_MIGRATION_ARG)
+                        }
                     } else {
-                        migrationPassword = commandLine.getOptionValue(PASSWORD_MIGRATION_ARG)
+                        migrationKeyHex = commandLine.getOptionValue(KEY_MIGRATION_ARG)
+                        // Use the "migration password" value if the migration key hex is absent
+                        usingPasswordMigration = !migrationKeyHex
                     }
-                } else {
-                    migrationKeyHex = commandLine.getOptionValue(KEY_MIGRATION_ARG)
-                    // Use the "migration password" value if the migration key hex is absent
-                    usingPasswordMigration = !migrationKeyHex
                 }
             } else {
                 if (commandLine.hasOption(PASSWORD_MIGRATION_ARG) || commandLine.hasOption(KEY_MIGRATION_ARG)) {
@@ -428,23 +452,25 @@ class ConfigEncryptionTool {
                 }
             }
 
-            if (commandLine.hasOption(PASSWORD_ARG)) {
-                usingPassword = true
-                if (commandLine.hasOption(KEY_ARG)) {
-                    printUsageAndThrow("Only one of '-p'/'--${PASSWORD_ARG}' and '-k'/'--${KEY_ARG}' can be used", ExitCode.INVALID_ARGS)
+            if (protectionScheme.requiresSecretKey()) {
+                if (commandLine.hasOption(PASSWORD_ARG)) {
+                    usingPassword = true
+                    if (commandLine.hasOption(KEY_ARG)) {
+                        printUsageAndThrow("Only one of '-p'/'--${PASSWORD_ARG}' and '-k'/'--${KEY_ARG}' can be used", ExitCode.INVALID_ARGS)
+                    } else {
+                        password = commandLine.getOptionValue(PASSWORD_ARG)
+                    }
                 } else {
-                    password = commandLine.getOptionValue(PASSWORD_ARG)
+                    keyHex = commandLine.getOptionValue(KEY_ARG)
+                    usingPassword = !keyHex
                 }
-            } else {
-                keyHex = commandLine.getOptionValue(KEY_ARG)
-                usingPassword = !keyHex
-            }
 
-            if (commandLine.hasOption(USE_KEY_ARG)) {
-                if (keyHex || password) {
-                    logger.warn("If the key or password is provided in the arguments, '-r'/'--${USE_KEY_ARG}' is ignored")
-                } else {
-                    usingPassword = false
+                if (commandLine.hasOption(USE_KEY_ARG)) {
+                    if (keyHex || password) {
+                        logger.warn("If the key or password is provided in the arguments, '-r'/'--${USE_KEY_ARG}' is ignored")
+                    } else {
+                        usingPassword = false
+                    }
                 }
             }
 
@@ -555,8 +581,10 @@ class ConfigEncryptionTool {
     private static String parseKey(String rawKey) throws KeyException {
         String hexKey = rawKey.replaceAll("[^0-9a-fA-F]", "")
         def validKeyLengths = getValidKeyLengths()
+        List<Integer> validHexCharLengths = validKeyLengths.collect {it / 4 }
         if (!validKeyLengths.contains(hexKey.size() * 4)) {
-            throw new KeyException("The key (${hexKey.size()} hex chars) must be of length ${validKeyLengths} bits (${validKeyLengths.collect { it / 4 }} hex characters)")
+            throw new KeyException("The key (${hexKey.size()} hex chars) must be of length ${validKeyLengths} " +
+                    "bits (${validHexCharLengths} hex characters)")
         }
         hexKey.toUpperCase()
     }
@@ -570,6 +598,10 @@ class ConfigEncryptionTool {
         Cipher.getMaxAllowedKeyLength("AES") > 128 ? [128, 192, 256] : [128]
     }
 
+    private NiFiPropertiesLoader getNiFiPropertiesLoader(final String keyHex) {
+        return protectionScheme.requiresSecretKey() ? NiFiPropertiesLoader.withKey(keyHex) : new NiFiPropertiesLoader()
+    }
+
     /**
      * Loads the {@link NiFiProperties} instance from the provided file path (restoring the original value of the System property {@code nifi.properties.file.path} after loading this instance).
      *
@@ -581,7 +613,7 @@ class ConfigEncryptionTool {
         if (niFiPropertiesPath && (niFiPropertiesFile = new File(niFiPropertiesPath)).exists()) {
             NiFiProperties properties
             try {
-                properties = NiFiPropertiesLoader.withKey(existingKeyHex).load(niFiPropertiesFile)
+                properties = getNiFiPropertiesLoader(existingKeyHex).load(niFiPropertiesFile)
                 logger.info("Loaded NiFiProperties instance with ${properties.size()} properties")
                 return properties
             } catch (RuntimeException e) {
@@ -725,14 +757,18 @@ class ConfigEncryptionTool {
         Paths.get(originalOutputFlowXmlPath).resolveSibling(migratedFileName)
     }
 
+    private SensitivePropertyProviderFactory getSensitivePropertyProviderFactory(final String keyHex) {
+        StandardSensitivePropertyProviderFactory.withKeyAndBootstrapSupplier(keyHex, getBootstrapSupplier(bootstrapConfPath))
+    }
+
     String decryptLoginIdentityProviders(String encryptedXml, String existingKeyHex = keyHex) {
-        AESSensitivePropertyProvider sensitivePropertyProvider = new AESSensitivePropertyProvider(existingKeyHex)
+        final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(existingKeyHex)
 
         try {
             def doc = getXmlSlurper().parseText(encryptedXml)
             // Find the provider element by class even if it has been renamed
             def passwords = doc.provider.find { it.'class' as String == LDAP_PROVIDER_CLASS }.property.findAll {
-                it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}"
+                it.@name =~ "Password" && it.@encryption != ""
             }
 
             if (passwords.isEmpty()) {
@@ -743,8 +779,10 @@ class ConfigEncryptionTool {
             }
 
             passwords.each { password ->
+                final SensitivePropertyProvider sensitivePropertyProvider = providerFactory
+                        .getProvider(PropertyProtectionScheme.fromIdentifier((String) password.@encryption))
                 if (isVerbose) {
-                    logger.info("Attempting to decrypt ${password.text()}")
+                    logger.info("Attempting to decrypt ${password.text()} using protection scheme ${password.@encryption}")
                 }
                 String decryptedValue = sensitivePropertyProvider.unprotect(password.text().trim())
                 password.replaceNode {
@@ -762,7 +800,7 @@ class ConfigEncryptionTool {
     }
 
     String decryptAuthorizers(String encryptedXml, String existingKeyHex = keyHex) {
-        AESSensitivePropertyProvider sensitivePropertyProvider = new AESSensitivePropertyProvider(existingKeyHex)
+        final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(existingKeyHex)
 
         try {
             def filename = "authorizers.xml"
@@ -771,7 +809,7 @@ class ConfigEncryptionTool {
             def passwords = doc.userGroupProvider.find {
                 it.'class' as String == LDAP_USER_GROUP_PROVIDER_CLASS
             }.property.findAll {
-                it.@name =~ "Password" && it.@encryption =~ "aes/gcm/\\d{3}"
+                it.@name =~ "Password" && it.@encryption != ""
             }
 
             if (passwords.isEmpty()) {
@@ -784,8 +822,10 @@ class ConfigEncryptionTool {
             passwords.each { password ->
                 // TODO: Capture the raw password, and only display it in the log if the decrypted value is different (to avoid possibly printing an incorrectly provided plaintext password)
                 if (isVerbose) {
-                    logger.info("Attempting to decrypt ${password.text()}")
+                    logger.info("Attempting to decrypt ${password.text()} using protection scheme ${password.@encryption}")
                 }
+                final SensitivePropertyProvider sensitivePropertyProvider = providerFactory
+                        .getProvider(PropertyProtectionScheme.fromIdentifier((String) password.@encryption))
                 String decryptedValue = sensitivePropertyProvider.unprotect(password.text().trim())
                 password.replaceNode {
                     property(name: password.@name, encryption: "none", decryptedValue)
@@ -803,8 +843,8 @@ class ConfigEncryptionTool {
         }
     }
 
-    String encryptLoginIdentityProviders(String plainXml, String newKeyHex = keyHex) {
-        AESSensitivePropertyProvider sensitivePropertyProvider = new AESSensitivePropertyProvider(newKeyHex)
+    String encryptLoginIdentityProviders(final String plainXml, final String newKeyHex = keyHex, final PropertyProtectionScheme newProtectionScheme = protectionScheme) {
+        final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(newKeyHex)
 
         // TODO: Switch to XmlParser & XmlNodePrinter to maintain "empty" element structure
         try {
@@ -822,10 +862,11 @@ class ConfigEncryptionTool {
                 }
                 return plainXml
             }
+            final SensitivePropertyProvider sensitivePropertyProvider = providerFactory.getProvider(newProtectionScheme)
 
             passwords.each { password ->
                 if (isVerbose) {
-                    logger.info("Attempting to encrypt ${password.name()}")
+                    logger.info("Attempting to encrypt ${password.name()} using protection scheme ${sensitivePropertyProvider.identifierKey}")
                 }
                 String encryptedValue = sensitivePropertyProvider.protect(password.text().trim())
                 password.replaceNode {
@@ -845,8 +886,8 @@ class ConfigEncryptionTool {
         }
     }
 
-    String encryptAuthorizers(String plainXml, String newKeyHex = keyHex) {
-        AESSensitivePropertyProvider sensitivePropertyProvider = new AESSensitivePropertyProvider(newKeyHex)
+    String encryptAuthorizers(final String plainXml, final String newKeyHex = keyHex, final PropertyProtectionScheme newProtectionScheme = protectionScheme) {
+        final SensitivePropertyProviderFactory providerFactory = getSensitivePropertyProviderFactory(newKeyHex)
 
         // TODO: Switch to XmlParser & XmlNodePrinter to maintain "empty" element structure
         try {
@@ -865,10 +906,11 @@ class ConfigEncryptionTool {
                 }
                 return plainXml
             }
+            final SensitivePropertyProvider sensitivePropertyProvider = providerFactory.getProvider(newProtectionScheme)
 
             passwords.each { password ->
                 if (isVerbose) {
-                    logger.info("Attempting to encrypt ${password.name()}")
+                    logger.info("Attempting to encrypt ${password.name()} using protection scheme ${sensitivePropertyProvider.identifierKey}")
                 }
                 String encryptedValue = sensitivePropertyProvider.protect(password.text().trim())
                 password.replaceNode {
@@ -912,7 +954,8 @@ class ConfigEncryptionTool {
         // Holder for encrypted properties and protection schemes
         Properties encryptedProperties = new Properties()
 
-        AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(keyHex)
+        final SensitivePropertyProviderFactory sensitivePropertyProviderFactory = getSensitivePropertyProviderFactory(keyHex)
+        final SensitivePropertyProvider spp = sensitivePropertyProviderFactory.getProvider(protectionScheme)
         protectedWrapper.addSensitivePropertyProvider(spp)
 
         List<String> keysToSkip = []
@@ -929,7 +972,7 @@ class ConfigEncryptionTool {
                 logger.info("Protected ${key} with ${spp.getIdentifierKey()} -> \t${protectedValue}")
 
                 // Add the protection key ("x.y.z.protected" -> "aes/gcm/{128,256}")
-                String protectionKey = protectedWrapper.getProtectionKey(key)
+                String protectionKey = ApplicationPropertiesProtector.getProtectionKey(key)
                 encryptedProperties.setProperty(protectionKey, spp.getIdentifierKey())
                 logger.info("Updated protection key ${protectionKey}")
 
@@ -943,7 +986,7 @@ class ConfigEncryptionTool {
         nonSensitiveKeys.each { String key ->
             encryptedProperties.setProperty(key, plainProperties.getProperty(key))
         }
-        NiFiProperties mergedProperties = new StandardNiFiProperties(encryptedProperties)
+        NiFiProperties mergedProperties = new NiFiProperties(encryptedProperties)
         logger.info("Final result: ${mergedProperties.size()} keys including ${ProtectedNiFiProperties.countProtectedProperties(mergedProperties)} protected keys")
 
         mergedProperties
@@ -1129,7 +1172,8 @@ class ConfigEncryptionTool {
         // Only need to replace the keys that have been protected AND nifi.sensitive.props.key
         Map<String, String> protectedKeys = protectedNiFiProperties.getProtectedPropertyKeys()
         if (!protectedKeys.containsKey(NiFiProperties.SENSITIVE_PROPS_KEY)) {
-            protectedKeys.put(NiFiProperties.SENSITIVE_PROPS_KEY, protectedNiFiProperties.getProperty(ProtectedNiFiProperties.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY)))
+            protectedKeys.put(NiFiProperties.SENSITIVE_PROPS_KEY, protectedNiFiProperties.getProperty(ApplicationPropertiesProtector
+                    .getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY)))
         }
 
         protectedKeys.each { String key, String protectionScheme ->
@@ -1139,8 +1183,8 @@ class ConfigEncryptionTool {
             }
             // Get the index of the following line (or cap at max)
             int p = l + 1 > lines.size() ? lines.size() : l + 1
-            String protectionLine = "${protectedNiFiProperties.getProtectionKey(key)}=${protectionScheme ?: ""}"
-            if (p < lines.size() && lines.get(p).startsWith("${protectedNiFiProperties.getProtectionKey(key)}=")) {
+            String protectionLine = "${ApplicationPropertiesProtector.getProtectionKey(key)}=${protectionScheme ?: ""}"
+            if (p < lines.size() && lines.get(p).startsWith("${ApplicationPropertiesProtector.getProtectionKey(key)}=")) {
                 lines.set(p, protectionLine)
             } else {
                 lines.add(p, protectionLine)
@@ -1260,7 +1304,7 @@ class ConfigEncryptionTool {
     boolean niFiPropertiesAreEncrypted() {
         if (niFiPropertiesPath) {
             try {
-                def nfp = NiFiPropertiesLoader.withKey(keyHex).readProtectedPropertiesFromDisk(new File(niFiPropertiesPath))
+                def nfp = getNiFiPropertiesLoader(keyHex).readProtectedPropertiesFromDisk(new File(niFiPropertiesPath))
                 return nfp.hasProtectedKeys()
             } catch (SensitivePropertyProtectionException | IOException e) {
                 return true
@@ -1299,7 +1343,7 @@ class ConfigEncryptionTool {
                 if (tool.translatingCli) {
                     if (tool.bootstrapConfPath) {
                         // Check to see if bootstrap.conf has a root key
-                        tool.keyHex = CryptoUtils.extractKeyFromBootstrapFile(tool.bootstrapConfPath)
+                        tool.keyHex = NiFiBootstrapUtils.extractKeyFromBootstrapFile(tool.bootstrapConfPath)
                     }
 
                     if (!tool.keyHex) {
@@ -1319,7 +1363,7 @@ class ConfigEncryptionTool {
                 if (!tool.ignorePropertiesFiles || (tool.handlingFlowXml && existingNiFiPropertiesAreEncrypted)) {
                     // If we are handling the flow.xml.gz and nifi.properties is already encrypted, try getting the key from bootstrap.conf rather than the console
                     if (tool.ignorePropertiesFiles) {
-                        tool.keyHex = CryptoUtils.extractKeyFromBootstrapFile(tool.bootstrapConfPath)
+                        tool.keyHex = NiFiBootstrapUtils.extractKeyFromBootstrapFile(tool.bootstrapConfPath)
                     } else {
                         tool.keyHex = tool.getKey()
                     }
@@ -1338,7 +1382,7 @@ class ConfigEncryptionTool {
                         tool.printUsageAndThrow(e.getMessage(), ExitCode.INVALID_ARGS)
                     }
 
-                    if (tool.migration) {
+                    if (tool.migration && tool.migrationProtectionScheme.requiresSecretKey()) {
                         String migrationKeyHex = tool.getMigrationKey()
 
                         if (!migrationKeyHex) {
@@ -1397,6 +1441,9 @@ class ConfigEncryptionTool {
                 }
 
                 if (tool.handlingNiFiProperties) {
+                    // If the flow password was not set in nifi.properties, use the hard-coded default
+                    tool.existingFlowPropertiesPassword = tool.getExistingFlowPassword()
+
                     tool.niFiProperties = tool.encryptSensitiveProperties(tool.niFiProperties)
                 }
             } catch (CommandLineParseException e) {
@@ -1444,8 +1491,7 @@ class ConfigEncryptionTool {
     }
 
     void handleFlowXml(boolean existingNiFiPropertiesAreEncrypted = false) {
-        // If the flow password was not set in nifi.properties, use the hard-coded default
-        String existingFlowPassword = getExistingFlowPassword()
+        String existingFlowPassword = existingFlowPropertiesPassword ?: getExistingFlowPassword()
 
         // If the new password was not provided in the arguments, read from the console. If that is empty, use the same value (essentially a copy no-op)
         String newFlowPassword = flowPropertiesPassword ?: getFlowPassword()
@@ -1481,20 +1527,22 @@ class ConfigEncryptionTool {
                 rawProperties.put(k, nfp.getProperty(k))
             }
 
-            // If the tool is not going to encrypt NiFiProperties and the existing file is already encrypted, encrypt and update the new sensitive props key
-            if (!handlingNiFiProperties && existingNiFiPropertiesAreEncrypted) {
-                AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(keyHex)
+            // If the tool is supposed to encrypt NiFiProperties or the existing file is already encrypted, encrypt and update the new sensitive props key
+            if (handlingNiFiProperties || existingNiFiPropertiesAreEncrypted) {
+                final SensitivePropertyProviderFactory sensitivePropertyProviderFactory = getSensitivePropertyProviderFactory(keyHex)
+                SensitivePropertyProvider spp = sensitivePropertyProviderFactory.getProvider(protectionScheme)
                 String encryptedSPK = spp.protect(newFlowPassword)
                 rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, encryptedSPK)
                 // Manually update the protection scheme or it will be lost
-                rawProperties.put(ProtectedNiFiProperties.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY), spp.getIdentifierKey())
+                rawProperties.put(ApplicationPropertiesProtector.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY), spp.getIdentifierKey())
                 if (isVerbose) {
                     logger.info("Tool is not configured to encrypt nifi.properties, but the existing nifi.properties is encrypted and flow.xml.gz was migrated, so manually persisting the new encrypted value to nifi.properties")
                 }
             } else {
                 rawProperties.put(NiFiProperties.SENSITIVE_PROPS_KEY, newFlowPassword)
+                rawProperties.put(ApplicationPropertiesProtector.getProtectionKey(NiFiProperties.SENSITIVE_PROPS_KEY), "")
             }
-            niFiProperties = new StandardNiFiProperties(rawProperties)
+            niFiProperties = new NiFiProperties(rawProperties)
         }
     }
 
@@ -1513,6 +1561,19 @@ class ConfigEncryptionTool {
         cliOutput.join("\n")
     }
 
+    static Supplier<BootstrapProperties> getBootstrapSupplier(final String bootstrapConfPath) {
+        new Supplier<BootstrapProperties>() {
+            @Override
+            BootstrapProperties get() {
+                try {
+                    NiFiRegistryBootstrapUtils.loadBootstrapProperties(bootstrapConfPath)
+                } catch (final IOException e) {
+                    throw new SensitivePropertyProtectionException(e.getCause(), e)
+                }
+            }
+        }
+    }
+
     static String determineBaseUrl(NiFiProperties niFiProperties) {
         String protocol = niFiProperties.isHTTPSConfigured() ? "https" : "http"
         String host = niFiProperties.isHTTPSConfigured() ? niFiProperties.getProperty(NiFiProperties.WEB_HTTPS_HOST) : niFiProperties.getProperty(NiFiProperties.WEB_HTTP_HOST)
diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/DecryptMode.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/DecryptMode.groovy
index b2b901b..52289c7 100644
--- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/DecryptMode.groovy
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/DecryptMode.groovy
@@ -19,8 +19,10 @@ package org.apache.nifi.toolkit.encryptconfig
 import groovy.cli.commons.CliBuilder
 import groovy.cli.commons.OptionAccessor
 import org.apache.commons.cli.HelpFormatter
-import org.apache.nifi.properties.AESSensitivePropertyProvider
+import org.apache.nifi.properties.ConfigEncryptionTool
+import org.apache.nifi.properties.PropertyProtectionScheme
 import org.apache.nifi.properties.SensitivePropertyProvider
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory
 import org.apache.nifi.toolkit.encryptconfig.util.BootstrapUtil
 import org.apache.nifi.toolkit.encryptconfig.util.PropertiesEncryptor
 import org.apache.nifi.toolkit.encryptconfig.util.ToolUtilities
@@ -208,6 +210,7 @@ class DecryptMode implements ToolMode {
         OptionAccessor rawOptions
 
         Configuration.KeySource keySource
+        PropertyProtectionScheme protectionScheme = ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME
         String key
         SensitivePropertyProvider decryptionProvider
         String inputBootstrapPath
@@ -228,11 +231,17 @@ class DecryptMode implements ToolMode {
             validateOptions()
             determineInputFileFromRemainingArgs()
 
-            determineKey()
-            if (!key) {
-                throw new RuntimeException("Failed to configure tool, could not determine key.")
+            determineProtectionScheme()
+            determineBootstrapProperties()
+            if (protectionScheme.requiresSecretKey()) {
+                determineKey()
+                if (!key) {
+                    throw new RuntimeException("Failed to configure tool, could not determine key.")
+                }
             }
-            decryptionProvider = new AESSensitivePropertyProvider(key)
+            decryptionProvider = StandardSensitivePropertyProviderFactory
+                    .withKeyAndBootstrapSupplier(key, ConfigEncryptionTool.getBootstrapSupplier(inputBootstrapPath))
+                    .getProvider(protectionScheme)
 
             if (rawOptions.t) {
                 fileType = FileType.valueOf(rawOptions.t)
@@ -244,6 +253,12 @@ class DecryptMode implements ToolMode {
             }
         }
 
+        private void determineBootstrapProperties() {
+            if (rawOptions.b) {
+                inputBootstrapPath = rawOptions.b
+            }
+        }
+
         private void validateOptions() {
 
             String validationFailedMessage = null
@@ -268,6 +283,13 @@ class DecryptMode implements ToolMode {
             this.inputFilePath = remainingArgs[0]
         }
 
+        private void determineProtectionScheme() {
+
+            if (rawOptions.S) {
+                protectionScheme = PropertyProtectionScheme.valueOf(rawOptions.S)
+            }
+        }
+
         private void determineKey() {
 
             boolean usingPassword = false
@@ -302,7 +324,6 @@ class DecryptMode implements ToolMode {
                 }
                 key = ToolUtilities.determineKey(TextDevices.defaultTextDevice(), keyHex, password, usingPassword)
             } else if (usingBootstrapKey) {
-                inputBootstrapPath = rawOptions.b
                 logger.debug("Looking in bootstrap conf file ${inputBootstrapPath} for root key for decryption.")
 
                 // first, try to treat the bootstrap file as a NiFi bootstrap.conf
diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptMode.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptMode.groovy
index f30b011..919e2b0 100644
--- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptMode.groovy
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryDecryptMode.groovy
@@ -17,7 +17,9 @@
 package org.apache.nifi.toolkit.encryptconfig
 
 import groovy.cli.commons.CliBuilder
-import org.apache.nifi.properties.AESSensitivePropertyProvider
+import org.apache.nifi.properties.ConfigEncryptionTool
+import org.apache.nifi.properties.PropertyProtectionScheme
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory
 import org.apache.nifi.toolkit.encryptconfig.util.BootstrapUtil
 import org.apache.nifi.toolkit.encryptconfig.util.ToolUtilities
 import org.slf4j.Logger
@@ -72,6 +74,10 @@ class NiFiRegistryDecryptMode extends DecryptMode {
             config.inputFilePath = options.r
             config.fileType = FileType.properties  // disables auto-detection, which is still experimental
 
+            if (options.S) {
+                config.protectionScheme = PropertyProtectionScheme.valueOf((String) options.S)
+            }
+
             // one of [-p, -k, -b]
             String keyHex = null
             String password = null
@@ -110,7 +116,9 @@ class NiFiRegistryDecryptMode extends DecryptMode {
                 }
             }
 
-            config.decryptionProvider = new AESSensitivePropertyProvider(config.key)
+            config.decryptionProvider = StandardSensitivePropertyProviderFactory
+                    .withKeyAndBootstrapSupplier(config.key, ConfigEncryptionTool.getBootstrapSupplier(config.inputBootstrapPath))
+                    .getProvider(config.protectionScheme)
 
             run(config)
 
diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryMode.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryMode.groovy
index 4a43c21..3b5bbce 100644
--- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryMode.groovy
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/NiFiRegistryMode.groovy
@@ -20,8 +20,10 @@ import groovy.cli.commons.CliBuilder
 import groovy.cli.commons.OptionAccessor
 import org.apache.commons.cli.HelpFormatter
 import org.apache.commons.cli.Options
-import org.apache.nifi.properties.AESSensitivePropertyProvider
+import org.apache.nifi.properties.ConfigEncryptionTool
+import org.apache.nifi.properties.PropertyProtectionScheme
 import org.apache.nifi.properties.SensitivePropertyProvider
+import org.apache.nifi.properties.StandardSensitivePropertyProviderFactory
 import org.apache.nifi.toolkit.encryptconfig.util.BootstrapUtil
 import org.apache.nifi.toolkit.encryptconfig.util.NiFiRegistryAuthorizersXmlEncryptor
 import org.apache.nifi.toolkit.encryptconfig.util.NiFiRegistryIdentityProvidersXmlEncryptor
@@ -186,6 +188,10 @@ class NiFiRegistryMode implements ToolMode {
                 argName: 'keyhex',
                 optionalArg: true,
                 'Protect the files using a raw hexadecimal key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the key.')
+        cli.S(longOpt: 'protectionScheme',
+                args: 1,
+                argName: 'protectionScheme',
+                "Selects the protection scheme for encrypted properties.  Valid values are: [${PropertyProtectionScheme.values().join(", ")}] (default is ${ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME.name()})")
 
         // Options for the old password or key, if running the tool to migrate keys
         cli._(longOpt: 'oldPassword',
@@ -196,12 +202,16 @@ class NiFiRegistryMode implements ToolMode {
                 args: 1,
                 argName: 'keyhex',
                 'If the input files are already protected using a key, this specifies the raw hexadecimal key so that the files can be unprotected before re-protecting.')
+        cli.H(longOpt: 'oldProtectionScheme',
+                args: 1,
+                argName: 'protectionScheme',
+                "The old protection scheme to use during encryption migration (see --protectionScheme for possible values).  Default is ${ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME.name()}.")
 
         // Options for output bootstrap.conf file
         cli.b(longOpt: 'bootstrapConf',
                 args: 1,
                 argName: 'file',
-                'The bootstrap.conf file containing no root key or an existing root key. If a new password or key is specified (using -p or -k) and no output bootstrap.conf file is specified, then this file will be overwritten to persist the new master key.')
+                'The bootstrap.conf file containing no root key or an existing root key, and any other protection scheme configuration properties. If a new password or key is specified (using -p or -k) and no output bootstrap.conf file is specified, then this file will be overwritten to persist the new master key.')
         cli.B(longOpt: 'outputBootstrapConf',
                 args: 1,
                 argName: 'file',
@@ -249,7 +259,9 @@ class NiFiRegistryMode implements ToolMode {
         boolean usingPassword
         boolean usingBootstrapKey
 
+        PropertyProtectionScheme protectionScheme
         String encryptionKey
+        PropertyProtectionScheme oldProtectionScheme
         String decryptionKey
 
         SensitivePropertyProvider encryptionProvider
@@ -285,32 +297,43 @@ class NiFiRegistryMode implements ToolMode {
             // Set input bootstrap.conf path
             inputBootstrapPath = rawOptions.b
 
-            // Determine key for encryption (required)
-            determineEncryptionKey()
-            if (!encryptionKey) {
-                throw new RuntimeException("Failed to configure tool, could not determine encryption key. Must provide -p, -k, or -b. If using -b, bootstrap.conf argument must already contain root key.")
-            }
-            encryptionProvider = new AESSensitivePropertyProvider(encryptionKey)
-
+            determineOldProtectionScheme()
             // Determine key for decryption (if migrating)
             determineDecryptionKey()
             if (!decryptionKey) {
                 logger.debug("No decryption key specified via options, so if any input files require decryption prior to re-encryption (i.e., migration), this tool will fail.")
             }
-            decryptionProvider = decryptionKey ? new AESSensitivePropertyProvider(decryptionKey) : null
-
-            writingKeyToBootstrap = (usingPassword || usingRawKeyHex || rawOptions.B)
-            if (writingKeyToBootstrap) {
-                outputBootstrapPath = rawOptions.B ?: inputBootstrapPath
-            }
 
             handlingNiFiRegistryProperties = rawOptions.r
             if (handlingNiFiRegistryProperties) {
                 inputNiFiRegistryPropertiesPath = rawOptions.r
                 outputNiFiRegistryPropertiesPath = rawOptions.R ?: inputNiFiRegistryPropertiesPath
+            }
+
+            determineProtectionScheme()
+
+            // Determine key for encryption (required)
+            determineEncryptionKey()
+            if (!encryptionKey) {
+                throw new RuntimeException("Failed to configure tool, could not determine encryption key. Must provide -p, -k, or -b. If using -b, bootstrap.conf argument must already contain root key.")
+            }
+            encryptionProvider = StandardSensitivePropertyProviderFactory
+                    .withKeyAndBootstrapSupplier(encryptionKey, ConfigEncryptionTool.getBootstrapSupplier(inputBootstrapPath))
+                    .getProvider(oldProtectionScheme)
+
+            decryptionProvider = decryptionKey ? StandardSensitivePropertyProviderFactory
+                    .withKeyAndBootstrapSupplier(decryptionKey, ConfigEncryptionTool.getBootstrapSupplier(inputBootstrapPath))
+                    .getProvider(protectionScheme) : null
+
+            if (handlingNiFiRegistryProperties) {
                 propertiesEncryptor = new NiFiRegistryPropertiesEncryptor(encryptionProvider, decryptionProvider)
             }
 
+            writingKeyToBootstrap = (usingPassword || usingRawKeyHex || rawOptions.B)
+            if (writingKeyToBootstrap) {
+                outputBootstrapPath = rawOptions.B ?: inputBootstrapPath
+            }
+
             handlingIdentityProviders = rawOptions.i
             if (handlingIdentityProviders) {
                 inputIdentityProvidersPath = rawOptions.i
@@ -327,6 +350,23 @@ class NiFiRegistryMode implements ToolMode {
 
         }
 
+        private void determineProtectionScheme() {
+
+            if (rawOptions.S) {
+                protectionScheme = PropertyProtectionScheme.valueOf(rawOptions.S)
+            } else {
+                protectionScheme = ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME
+            }
+        }
+        private void determineOldProtectionScheme() {
+
+            if (rawOptions.H) {
+                oldProtectionScheme = PropertyProtectionScheme.valueOf(rawOptions.H)
+            } else {
+                oldProtectionScheme = ConfigEncryptionTool.DEFAULT_PROTECTION_SCHEME
+            }
+        }
+
         private void validateOptions() {
 
             String validationFailedMessage = null
diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy
index 102ad9e..30639a8 100644
--- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/main/groovy/org/apache/nifi/toolkit/encryptconfig/util/NiFiRegistryAuthorizersXmlEncryptor.groovy
@@ -42,7 +42,7 @@ class NiFiRegistryAuthorizersXmlEncryptor extends XmlEncryptor {
      *   .*?</userGroupProvider>          -> find everything as needed up until and including occurrence of '</userGroupProvider>'
      */
 
-    NiFiRegistryAuthorizersXmlEncryptor(SensitivePropertyProvider encryptionProvider, SensitivePropertyProvider decryptionProvider) {
+    NiFiRegistryAuthorizersXmlEncryptor(final SensitivePropertyProvider encryptionProvider, final SensitivePropertyProvider decryptionProvider) {
         super(encryptionProvider, decryptionProvider)
     }
 
diff --git a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
index acb118b..57be63b 100644
--- a/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
+++ b/nifi-toolkit/nifi-toolkit-encrypt-config/src/test/groovy/org/apache/nifi/properties/ConfigEncryptionToolTest.groovy
@@ -19,11 +19,10 @@ package org.apache.nifi.properties
 import org.apache.commons.cli.CommandLine
 import org.apache.commons.cli.CommandLineParser
 import org.apache.commons.cli.DefaultParser
+import org.apache.commons.io.IOUtils
 import org.apache.commons.lang3.SystemUtils
 import org.apache.log4j.AppenderSkeleton
 import org.apache.log4j.spi.LoggingEvent
-import org.apache.nifi.encrypt.PropertyEncryptor
-import org.apache.nifi.encrypt.PropertyEncryptorFactory
 import org.apache.nifi.security.util.EncryptionMethod
 import org.apache.nifi.toolkit.tls.commandLine.CommandLineParseException
 import org.apache.nifi.util.NiFiProperties
@@ -34,7 +33,6 @@ import org.junit.After
 import org.junit.AfterClass
 import org.junit.Assume
 import org.junit.BeforeClass
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.contrib.java.lang.system.Assertion
@@ -50,20 +48,13 @@ import org.xmlunit.diff.DefaultNodeMatcher
 import org.xmlunit.diff.Diff
 import org.xmlunit.diff.ElementSelectors
 
-import javax.crypto.BadPaddingException
 import javax.crypto.Cipher
-import javax.crypto.SecretKey
-import javax.crypto.SecretKeyFactory
-import javax.crypto.spec.PBEKeySpec
-import javax.crypto.spec.PBEParameterSpec
+import java.nio.charset.StandardCharsets
 import java.nio.file.Files
 import java.nio.file.attribute.PosixFilePermission
 import java.security.KeyException
 import java.security.Security
 
-import org.apache.commons.io.IOUtils
-import java.nio.charset.StandardCharsets
-
 @RunWith(JUnit4.class)
 class ConfigEncryptionToolTest extends GroovyTestCase {
     private static final Logger logger = LoggerFactory.getLogger(ConfigEncryptionToolTest.class)
@@ -82,6 +73,8 @@ class ConfigEncryptionToolTest extends GroovyTestCase {
     public static final String KEY_HEX = isUnlimitedStrengthCryptoAvailable() ? KEY_HEX_256 : KEY_HEX_128
     private static final String PASSWORD = "thisIsABadPassword"
     private static final String ANOTHER_PASSWORD = "thisIsAnotherBadPassword"
+    
+    private static final SensitivePropertyProviderFactory DEFAULT_PROVIDER_FACTORY = StandardSensitivePropertyProviderFactory.withKey(KEY_HEX)
 
     // From ConfigEncryptionTool.deriveKeyFromPassword("thisIsABadPassword")
... 481 lines suppressed ...