http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
----------------------------------------------------------------------
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-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
new file mode 100644
index 0000000..cab2b0d
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProvider.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.properties;
+
+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.util.ArrayList;
+import java.util.Collections;
+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 
{
+    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 {
+            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 + 
Collections.max(getValidKeyLengths()).toString();
+    }
+
+    /**
+     * 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 
java.security.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)");
+        }
+
+        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.info(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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProviderFactory.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..56a1cc0
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/AESSensitivePropertyProviderFactory.java
@@ -0,0 +1,53 @@
+/*
+ * 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";
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/MultipleSensitivePropertyProtectionException.java
----------------------------------------------------------------------
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-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/MultipleSensitivePropertyProtectionException.java
new file mode 100644
index 0000000..3b6f3cd
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/MultipleSensitivePropertyProtectionException.java
@@ -0,0 +1,128 @@
+/*
+ * 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.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+
+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();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..831374c
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/NiFiPropertiesLoader.java
@@ -0,0 +1,254 @@
+/*
+ * 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.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.stream.Stream;
+import javax.crypto.Cipher;
+import org.apache.nifi.util.NiFiProperties;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NiFiPropertiesLoader {
+    private static final Logger logger = 
LoggerFactory.getLogger(NiFiPropertiesLoader.class);
+
+    private static final String RELATIVE_PATH = "conf/nifi.properties";
+
+    private static final String BOOTSTRAP_KEY_PREFIX = 
"nifi.bootstrap.sensitive.key=";
+
+    private NiFiProperties instance;
+    private String keyHex;
+
+    // Future enhancement: allow for external registration of new providers
+    private static SensitivePropertyProviderFactory 
sensitivePropertyProviderFactory;
+
+    public NiFiPropertiesLoader() {
+    }
+
+    /**
+     * Returns an instance of the loader configured with the key.
+     *
+     * @param keyHex the key used to encrypt any sensitive properties
+     * @return the configured loader
+     */
+    public static NiFiPropertiesLoader withKey(String keyHex) {
+        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,
+     * calling this method will throw a {@link RuntimeException}.
+     *
+     * @param keyHex the key in hexadecimal format
+     */
+    public void setKeyHex(String keyHex) {
+        if (this.keyHex == null || this.keyHex.trim().isEmpty()) {
+            this.keyHex = keyHex;
+        } else {
+            throw new RuntimeException("Cannot overwrite an existing key");
+        }
+    }
+
+    /**
+     * Returns a {@link NiFiProperties} instance with any encrypted properties
+     * decrypted using the key from the {@code conf/bootstrap.conf} file. This
+     * method is exposed to allow Spring factory-method loading at application
+     * startup.
+     *
+     * @return the populated and decrypted NiFiProperties instance
+     * @throws IOException if there is a problem reading from the 
bootstrap.conf or nifi.properties files
+     */
+    public static NiFiProperties loadDefaultWithKeyFromBootstrap() throws 
IOException {
+        try {
+            String keyHex = 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", 
getDefaultFilePath(), e);
+            throw e;
+        }
+    }
+
+    private static String extractKeyFromBootstrapFile() throws IOException {
+        // 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()) {
+            File expectedBootstrapFile = new File(confDir, "bootstrap.conf");
+            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");
+            }
+        } 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");
+        }
+    }
+
+    private 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_PATH);
+            System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, 
RELATIVE_PATH);
+            systemPath = RELATIVE_PATH;
+        }
+
+        logger.info("Determined default nifi.properties path to be '{}'", 
systemPath);
+        return systemPath;
+    }
+
+    private NiFiProperties loadDefault() {
+        return load(getDefaultFilePath());
+    }
+
+    private static String getDefaultProviderKey() {
+        try {
+            return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? 
"256" : "128");
+        } catch (NoSuchAlgorithmException e) {
+            return "aes/gcm/128";
+        }
+    }
+
+    private void initializeSensitivePropertyProviderFactory() {
+        if (sensitivePropertyProviderFactory == null) {
+            sensitivePropertyProviderFactory = new 
AESSensitivePropertyProviderFactory(keyHex);
+        }
+    }
+
+    private SensitivePropertyProvider getSensitivePropertyProvider() {
+        initializeSensitivePropertyProviderFactory();
+        return sensitivePropertyProviderFactory.getProvider();
+    }
+
+    /**
+     * Returns a {@link ProtectedNiFiProperties} instance loaded from the 
serialized
+     * form in the file. Responsible for actually reading from disk and 
deserializing
+     * the properties. Returns a protected instance to allow for decryption 
operations.
+     *
+     * @param file the file containing serialized properties
+     * @return the ProtectedNiFiProperties instance
+     */
+    ProtectedNiFiProperties readProtectedPropertiesFromDisk(File file) {
+        if (file == null || !file.exists() || !file.canRead()) {
+            String path = (file == null ? "missing file" : 
file.getAbsolutePath());
+            logger.error("Cannot read from '{}' -- file is missing or not 
readable", path);
+            throw new IllegalArgumentException("NiFi properties file missing 
or unreadable");
+        }
+
+        Properties rawProperties = new Properties();
+
+        InputStream inStream = null;
+        try {
+            inStream = new BufferedInputStream(new FileInputStream(file));
+            rawProperties.load(inStream);
+            logger.info("Loaded {} properties from {}", rawProperties.size(), 
file.getAbsolutePath());
+
+            ProtectedNiFiProperties protectedNiFiProperties = new 
ProtectedNiFiProperties(rawProperties);
+            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);
+        } finally {
+            if (null != inStream) {
+                try {
+                    inStream.close();
+                } catch (final Exception ex) {
+                    /**
+                     * do nothing *
+                     */
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns an instance of {@link NiFiProperties} loaded from the provided
+     * {@link File}. If any properties are protected, will attempt to use the
+     * appropriate {@link SensitivePropertyProvider} to unprotect them 
transparently.
+     *
+     * @param file the File containing the serialized properties
+     * @return the NiFiProperties instance
+     */
+    public NiFiProperties load(File file) {
+        ProtectedNiFiProperties protectedNiFiProperties = 
readProtectedPropertiesFromDisk(file);
+        if (protectedNiFiProperties.hasProtectedKeys()) {
+            Security.addProvider(new BouncyCastleProvider());
+            
protectedNiFiProperties.addSensitivePropertyProvider(getSensitivePropertyProvider());
+        }
+
+        return protectedNiFiProperties.getUnprotectedProperties();
+    }
+
+    /**
+     * Returns an instance of {@link NiFiProperties}. If the path is empty, 
this
+     * will load the default properties file as specified by
+     * {@code NiFiProperties.PROPERTY_FILE_PATH}.
+     *
+     * @param path the path of the serialized properties file
+     * @return the NiFiProperties instance
+     * @see NiFiPropertiesLoader#load(File)
+     */
+    public NiFiProperties load(String path) {
+        if (path != null && !path.trim().isEmpty()) {
+            return load(new File(path));
+        } else {
+            return loadDefault();
+        }
+    }
+
+    /**
+     * Returns the loaded {@link NiFiProperties} instance. If none is 
currently loaded, attempts to load the default instance.
+     *
+     * @return the current NiFiProperties instance
+     */
+    public NiFiProperties get() {
+        if (instance == null) {
+            instance = loadDefault();
+        }
+
+        return instance;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/ProtectedNiFiProperties.java
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..83320a0
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/ProtectedNiFiProperties.java
@@ -0,0 +1,521 @@
+/*
+ * 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 static java.util.Arrays.asList;
+
+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 org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.util.NiFiProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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}.
+ * This encapsulates the sensitive property access logic from external 
consumers
+ * of {@code NiFiProperties}.
+ */
+class ProtectedNiFiProperties extends StandardNiFiProperties {
+    private static final Logger logger = 
LoggerFactory.getLogger(ProtectedNiFiProperties.class);
+
+    private NiFiProperties niFiProperties;
+
+    private Map<String, SensitivePropertyProvider> localProviderCache = new 
HashMap<>();
+
+    // Additional "sensitive" property key
+    public static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = 
"nifi.sensitive.props.additional.keys";
+
+    // Default list of "sensitive" property keys
+    public static final List<String> DEFAULT_SENSITIVE_PROPERTIES = new 
ArrayList<>(asList(SECURITY_KEY_PASSWD,
+            SECURITY_KEYSTORE_PASSWD, SECURITY_TRUSTSTORE_PASSWD, 
SENSITIVE_PROPS_KEY));
+
+    public ProtectedNiFiProperties() {
+        this(new StandardNiFiProperties());
+    }
+
+    /**
+     * Creates an instance containing the provided {@link NiFiProperties}.
+     *
+     * @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());
+    }
+
+    /**
+     * Creates an instance containing the provided raw {@link Properties}.
+     *
+     * @param rawProps the Properties to contain
+     */
+    public ProtectedNiFiProperties(Properties rawProps) {
+        this(new StandardNiFiProperties(rawProps));
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * 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"));
+        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;
+    }
+
+    /**
+     * 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
+     */
+    @Override
+    public int size() {
+        return getPropertyKeys().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;
+        }
+    }
+
+    /**
+     * Returns a list of the keys identifying "sensitive" properties. There is 
a default list,
+     * and additional keys can be provided in the {@code 
nifi.sensitive.props.additional.keys} property in {@code nifi.properties}.
+     *
+     * @return the list of sensitive property keys
+     */
+    public List<String> getSensitivePropertyKeys() {
+        String additionalPropertiesString = 
getProperty(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+        if (additionalPropertiesString == null || 
additionalPropertiesString.trim().isEmpty()) {
+            return DEFAULT_SENSITIVE_PROPERTIES;
+        } else {
+            List<String> additionalProperties = 
splitMultipleProperties(additionalPropertiesString);
+            /* Remove this key if it was accidentally provided as a sensitive 
key
+             * because we cannot protect it and read from it
+            */
+            if 
(additionalProperties.contains(ADDITIONAL_SENSITIVE_PROPERTIES_KEY)) {
+                logger.warn("The key '{}' contains itself. This is poor 
practice and should be removed", ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+                
additionalProperties.remove(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+            }
+            additionalProperties.addAll(DEFAULT_SENSITIVE_PROPERTIES);
+            return additionalProperties;
+        }
+    }
+
+    /**
+     * Returns true if any sensitive keys are protected.
+     *
+     * @return true if any key is protected; false otherwise
+     */
+    public boolean hasProtectedKeys() {
+        List<String> sensitiveKeys = getSensitivePropertyKeys();
+        for (String k : sensitiveKeys) {
+            if (isPropertyProtected(k)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a Map of the keys identifying "sensitive" properties that are 
currently protected and the "protection" key for each. This may or may not 
include all properties marked as sensitive.
+     *
+     * @return the Map of protected property keys and the protection 
identifier for each
+     */
+    public Map<String, String> getProtectedPropertyKeys() {
+        List<String> sensitiveKeys = getSensitivePropertyKeys();
+
+        // 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.isBlank(protection)) {
+                traditionalProtectedProperties.put(key, protection);
+            }
+        }
+
+        return traditionalProtectedProperties;
+    }
+
+    /**
+     * Returns the unique set of all protection schemes currently in use for 
this instance.
+     *
+     * @return the set of protection schemes
+     */
+    public Set<String> getProtectionSchemes() {
+        return new HashSet<>(getProtectedPropertyKeys().values());
+    }
+
+    /**
+     * Returns a percentage of the total number of 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) 
getSensitivePropertyKeys().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());
+    }
+
+    /**
+     * 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)));
+    }
+
+    /**
+     * 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 String getProtectionKey(String key) {
+        if (key == null || key.isEmpty()) {
+            throw new IllegalArgumentException("Cannot find protection key for 
null key");
+        }
+
+        return key + ".protected";
+    }
+
+    /**
+     * Returns the unprotected {@link 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
+     */
+    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();
+        }
+    }
+
+    /**
+     * 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);
+        }
+    }
+
+    /**
+     * 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()) {
+                protectedProperties.setProperty(key, plainValue);
+            } else {
+                final String protectedValue = spp.protect(plainValue);
+                protectedProperties.setProperty(key, protectedValue);
+                protectedProperties.setProperty(getProtectionKey(key), 
protectionScheme);
+            }
+        }
+
+        return new StandardNiFiProperties(protectedProperties);
+    }
+
+    /**
+     * Returns the number of properties that are marked as protected in the 
provided {@link NiFiProperties} instance without requiring external creation of 
a {@link ProtectedNiFiProperties} instance.
+     *
+     * @param plainProperties the instance to count protected properties
+     * @return the number of protected properties
+     */
+    public static int countProtectedProperties(NiFiProperties plainProperties) 
{
+        return new 
ProtectedNiFiProperties(plainProperties).getProtectedPropertyKeys().size();
+    }
+
+    /**
+     * Returns the number of properties that are marked as sensitive in the 
provided {@link NiFiProperties} instance without requiring external creation of 
a {@link ProtectedNiFiProperties} instance.
+     *
+     * @param plainProperties the instance to count sensitive properties
+     * @return the number of sensitive properties
+     */
+    public static int countSensitiveProperties(NiFiProperties plainProperties) 
{
+        return new 
ProtectedNiFiProperties(plainProperties).getSensitivePropertyKeys().size();
+    }
+
+    @Override
+    public String toString() {
+        final Set<String> providers = getSensitivePropertyProviders().keySet();
+        return new StringBuilder("ProtectedNiFiProperties instance with ")
+                .append(size()).append(" properties (")
+                .append(getProtectedPropertyKeys().size())
+                .append(" protected) and ")
+                .append(providers.size())
+                .append(" sensitive property providers: ")
+                .append(StringUtils.join(providers, ", "))
+                .toString();
+    }
+
+    /**
+     * Returns the local provider cache (null-safe) as a Map of protection 
schemes -> implementations.
+     *
+     * @return the map
+     */
+    private Map<String, SensitivePropertyProvider> 
getSensitivePropertyProviders() {
+        if (localProviderCache == null) {
+            localProviderCache = new HashMap<>();
+        }
+
+        return localProviderCache;
+    }
+
+    private SensitivePropertyProvider getSensitivePropertyProvider(String 
protectionScheme) {
+        if (isProviderAvailable(protectionScheme)) {
+            return getSensitivePropertyProviders().get(protectionScheme);
+        } else {
+            throw new SensitivePropertyProtectionException("No provider 
available for " + protectionScheme);
+        }
+    }
+
+    private boolean isProviderAvailable(String protectionScheme) {
+        return getSensitivePropertyProviders().containsKey(protectionScheme);
+    }
+
+    /**
+     * If the value is protected, unprotects it and returns it. If not, 
returns the original value.
+     *
+     * @param key            the retrieved property key
+     * @param retrievedValue the retrieved property value
+     * @return the unprotected value
+     */
+    private String unprotectValue(String key, String retrievedValue) {
+        // Checks if the key is sensitive and marked as protected
+        if (isPropertyProtected(key)) {
+            final String protectionScheme = getProperty(getProtectionKey(key));
+
+            // No provider registered for this scheme, so just return the value
+            if (!isProviderAvailable(protectionScheme)) {
+                logger.warn("No provider available for {} so passing the 
protected {} value back", protectionScheme, key);
+                return retrievedValue;
+            }
+
+            try {
+                SensitivePropertyProvider sensitivePropertyProvider = 
getSensitivePropertyProvider(protectionScheme);
+                return sensitivePropertyProvider.unprotect(retrievedValue);
+            } catch (SensitivePropertyProtectionException e) {
+                throw new SensitivePropertyProtectionException("Error 
unprotecting value for " + key, e.getCause());
+            }
+        }
+        return retrievedValue;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProtectionException.java
----------------------------------------------------------------------
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-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProtectionException.java
new file mode 100644
index 0000000..2870c2a
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProtectionException.java
@@ -0,0 +1,91 @@
+/*
+ * 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 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.)
+     * @since 1.4
+     */
+    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.)
+     * @since 1.4
+     */
+    public SensitivePropertyProtectionException(Throwable cause) {
+        super(cause);
+    }
+
+    @Override
+    public String toString() {
+        return "SensitivePropertyProtectionException: " + 
getLocalizedMessage();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
----------------------------------------------------------------------
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-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
new file mode 100644
index 0000000..b0c0be2
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProvider.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.properties;
+
+public interface SensitivePropertyProvider {
+
+    /**
+     * Returns the name of the underlying implementation.
+     *
+     * @return the name of this sensitive property provider
+     */
+    String getName();
+
+    /**
+     * Returns the key used to identify the provider implementation in {@code 
nifi.properties}.
+     *
+     * @return the key to persist in the sibling property
+     */
+    String getIdentifierKey();
+
+    /**
+     * Returns the "protected" form of this value. This is a form which can 
safely be persisted in the {@code nifi.properties} file without compromising 
the value.
+     * An encryption-based provider would return a cipher text, while a 
remote-lookup provider could return a unique ID to retrieve the secured value.
+     *
+     * @param unprotectedValue the sensitive value
+     * @return the value to persist in the {@code nifi.properties} file
+     */
+    String protect(String unprotectedValue) throws 
SensitivePropertyProtectionException;
+
+    /**
+     * Returns the "unprotected" form of this value. This is the raw sensitive 
value which is used by the application logic.
+     * An encryption-based provider would decrypt a cipher text and return the 
plaintext, while a remote-lookup provider could retrieve the secured value.
+     *
+     * @param protectedValue the protected value read from the {@code 
nifi.properties} file
+     * @return the raw value to be used by the application
+     */
+    String unprotect(String protectedValue) throws 
SensitivePropertyProtectionException;
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
----------------------------------------------------------------------
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-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..c800b3a
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/SensitivePropertyProviderFactory.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.properties;
+
+public interface SensitivePropertyProviderFactory {
+
+    SensitivePropertyProvider getProvider();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/StandardNiFiProperties.java
----------------------------------------------------------------------
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-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/StandardNiFiProperties.java
new file mode 100644
index 0000000..b7561ed
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/main/java/org/apache/nifi/properties/StandardNiFiProperties.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.properties;
+
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import org.apache.nifi.util.NiFiProperties;
+
+public class StandardNiFiProperties extends NiFiProperties {
+
+    private Properties rawProperties = new Properties();
+
+    public StandardNiFiProperties() {
+        this(null);
+    }
+
+    public StandardNiFiProperties(Properties props) {
+        this.rawProperties = props == null ? new Properties() : props;
+    }
+
+    /**
+     * 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 rawProperties.getProperty(key);
+    }
+
+    /**
+     * Retrieves all known property keys.
+     *
+     * @return all known property keys
+     */
+    @Override
+    public Set<String> getPropertyKeys() {
+        Set<String> propertyNames = new HashSet<>();
+        Enumeration e = getRawProperties().propertyNames();
+        for (; e.hasMoreElements(); ){
+            propertyNames.add((String) e.nextElement());
+        }
+
+        return propertyNames;
+    }
+
+    Properties getRawProperties() {
+        if (this.rawProperties == null) {
+            this.rawProperties = new Properties();
+        }
+
+        return this.rawProperties;
+    }
+
+    @Override
+    public int size() {
+        return getRawProperties().size();
+    }
+
+    @Override
+    public String toString() {
+        return "StandardNiFiProperties instance with " + size() + " 
properties";
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/c638191a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy
----------------------------------------------------------------------
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
new file mode 100644
index 0000000..b899ad2
--- /dev/null
+++ 
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-properties-loader/src/test/groovy/org/apache/nifi/properties/AESSensitivePropertyProviderFactoryTest.groovy
@@ -0,0 +1,97 @@
+/*
+ * 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.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class 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 {
+        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 testShouldGetProviderWithoutKey() throws Exception {
+        // Arrange
+        SensitivePropertyProviderFactory factory = new 
AESSensitivePropertyProviderFactory()
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert !provider.@key
+        assert !provider.@cipher
+    }
+
+    @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
+    }
+
+    @Test
+    public void testGetProviderShouldHandleEmptyKey() throws Exception {
+        // Arrange
+        SensitivePropertyProviderFactory factory = new 
AESSensitivePropertyProviderFactory("")
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert !provider.@key
+        assert !provider.@cipher
+    }
+}
\ No newline at end of file

Reply via email to