You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ab...@apache.org on 2020/10/20 16:36:14 UTC

[nifi-minifi-cpp] branch main updated: MINIFICPP-1323 Add libsodium dependency

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

aboda pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-minifi-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 4d89f2c  MINIFICPP-1323 Add libsodium dependency
4d89f2c is described below

commit 4d89f2c96a5123fb037d862c863ae50505863d80
Author: Ferenc Gerlits <fg...@gmail.com>
AuthorDate: Thu Sep 24 13:37:23 2020 +0200

    MINIFICPP-1323 Add libsodium dependency
    
    MINIFICPP-1323 Create the encrypt-config binary
    
    Note: this uses cxxopts, which uses <regex> internally, which does not work
    in gcc 4.8 (it does not allow [] in regular expressions), so I have disabled
    this binary in the gcc 4.8 CI build.
    
    Signed-off-by: Arpad Boda <ab...@apache.org>
    
    This closes #914
---
 .github/workflows/ci.yml                           |   2 +-
 CMakeLists.txt                                     |  16 +-
 LICENSE                                            |  23 +-
 NOTICE                                             |   1 +
 cmake/BundledLibSodium.cmake                       |  95 ++++++++
 encrypt-config/CMakeLists.txt                      |  25 +++
 encrypt-config/ConfigFile.cpp                      | 177 +++++++++++++++
 encrypt-config/ConfigFile.h                        |  87 ++++++++
 encrypt-config/ConfigFileEncryptor.cpp             |  64 ++++++
 encrypt-config/ConfigFileEncryptor.h               |  34 +++
 encrypt-config/EncryptConfig.cpp                   | 158 ++++++++++++++
 encrypt-config/EncryptConfig.h                     |  53 +++++
 encrypt-config/EncryptConfigMain.cpp               |  33 +++
 encrypt-config/tests/CMakeLists.txt                |  41 ++++
 encrypt-config/tests/ConfigFileEncryptorTests.cpp  | 128 +++++++++++
 encrypt-config/tests/ConfigFileTests.cpp           | 238 +++++++++++++++++++++
 encrypt-config/tests/resources/minifi.properties   | 101 +++++++++
 ...th-additional-sensitive-props.minifi.properties | 103 +++++++++
 libminifi/CMakeLists.txt                           |   2 +-
 .../properties/{Configure.h => Configuration.h}    |  12 +-
 libminifi/include/properties/Configure.h           | 105 ++-------
 libminifi/include/properties/Decryptor.h           |  55 +++++
 libminifi/include/properties/Properties.h          |  23 +-
 libminifi/include/utils/EncryptionUtils.h          | 109 ++++++++++
 libminifi/src/Configuration.cpp                    |  77 +++++++
 libminifi/src/Configure.cpp                        |  88 ++++----
 libminifi/src/Decryptor.cpp                        |  54 +++++
 libminifi/src/Properties.cpp                       |  21 +-
 libminifi/src/core/logging/LoggerConfiguration.cpp |  16 +-
 libminifi/src/utils/EncryptionUtils.cpp            | 127 +++++++++++
 libminifi/src/utils/Id.cpp                         |   4 +-
 libminifi/test/resources/conf/bootstrap.conf       |   1 +
 .../test/resources/encrypted.minifi.properties     | 105 +++++++++
 libminifi/test/unit/DecryptorTests.cpp             | 124 +++++++++++
 libminifi/test/unit/EncryptionUtilsTests.cpp       |  86 ++++++++
 main/MainHelper.h                                  |   3 +-
 main/MiNiFiMain.cpp                                |  24 ++-
 msi/WixWin.wsi                                     |   4 +-
 msi/WixWinMergeModules.wsi                         |   4 +-
 nanofi/ecu/log_aggregator.c                        |   2 +-
 nanofi/ecu/tailfile_chunk.c                        |   2 +-
 nanofi/ecu/tailfile_delimited.c                    |   2 +-
 nanofi/examples/generate_flow.c                    |   2 +-
 nanofi/examples/monitor_directory.c                |   2 +-
 nanofi/examples/terminate_handler.c                |   2 +-
 nanofi/examples/transmit_flow.c                    |   2 +-
 nanofi/include/api/nanofi.h                        |   2 +-
 nanofi/src/api/nanofi.cpp                          |   4 +-
 nanofi/tests/CAPITests.cpp                         |  18 +-
 nanofi/tests/CTestsBase.h                          |   2 +-
 thirdparty/libsodium/libsodium.patch               | 154 +++++++++++++
 win_build_vs.bat                                   |   2 +-
 52 files changed, 2391 insertions(+), 228 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4175b4e..88ff635 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -103,7 +103,7 @@ jobs:
           sudo unlink /usr/bin/gcc && sudo ln -s /usr/bin/gcc-4.8 /usr/bin/gcc
           sudo unlink /usr/bin/g++ && sudo ln -s /usr/bin/g++-4.8 /usr/bin/g++
       - id: build
-        run: ./bootstrap.sh -e -t && cd build  && cmake -DUSE_SHARED_LIBS= -DCMAKE_BUILD_TYPE=Release -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES=OFF -DSTRICT_GSL_CHECKS=AUDIT .. && cmake --build . --parallel 4   && make test ARGS="--timeout 300 -j2 --output-on-failure"
+        run: ./bootstrap.sh -e -t && cd build  && cmake -DUSE_SHARED_LIBS= -DCMAKE_BUILD_TYPE=Release -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_RULE_MESSAGES=OFF -DSTRICT_GSL_CHECKS=AUDIT .. && cmake --build . --parallel 4 && make test ARGS="--timeout 300 -j2 --output-on-failure"
   ubuntu_20_04:
     name: "ubuntu-20.04"
     runs-on: ubuntu-20.04
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 13b682c..c123700 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -245,6 +245,12 @@ if (NOT OPENSSL_OFF)
 	set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DOPENSSL_SUPPORT")
 endif()
 
+# libsodium
+include(BundledLibSodium)
+use_bundled_libsodium("${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSODIUM_STATIC=1")
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSODIUM_STATIC=1")
+
 # zlib
 include(BundledZLIB)
 use_bundled_zlib(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR})
@@ -544,10 +550,13 @@ endif()
 
 ## NOW WE CAN ADD LIBRARIES AND EXTENSIONS TO MAIN
 add_subdirectory(main)
-add_subdirectory(nanofi)
 
+add_subdirectory(nanofi)
 add_dependencies(nanofi minifiexe)
 
+add_subdirectory(encrypt-config)
+add_dependencies(encrypt-config minifi)
+
 if (NOT DISABLE_CURL AND NOT DISABLE_CONTROLLER)
 	add_subdirectory(controller)
 	add_dependencies(minificontroller minifiexe)
@@ -781,6 +790,8 @@ if (NOT DISABLE_ROCKSDB AND NOT DISABLE_LIBARCHIVE)
 	registerTest("${TEST_DIR}/persistence-tests")
 endif()
 
+registerTest("encrypt-config/tests")
+
 include(BuildDocs)
 
 include(DockerConfig)
@@ -795,6 +806,9 @@ add_custom_target(linter
     COMMAND ${CMAKE_SOURCE_DIR}/thirdparty/google-styleguide/run_linter.sh
             ${CMAKE_SOURCE_DIR}/libminifi/include/ --
             ${CMAKE_SOURCE_DIR}/libminifi/test/
+    COMMAND ${CMAKE_SOURCE_DIR}/thirdparty/google-styleguide/run_linter.sh
+            ${CMAKE_SOURCE_DIR}/encrypt-config/ --
+            ${CMAKE_SOURCE_DIR}/encrypt-config/
     DEPENDS ${extensions})
 endif(NOT WIN32)
 
diff --git a/LICENSE b/LICENSE
index 68acca9..59beaf7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2712,7 +2712,6 @@ bzip2/libbzip2 version 1.0.8 of 13 July 2019
 
 --------------------------------------------------------------------------
 
-
 This product bundles 'gsl-lite' under the MIT license.
 
 The MIT License (MIT)
@@ -2766,3 +2765,25 @@ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 DEALINGS IN THE SOFTWARE.
+
+--------------------------------------------------------------------------
+
+This product bundles 'libsodium' which is available under the ISC software license.
+libsodium - Copyright (c) 2013 - 2018 Frank Denis
+
+ISC License
+
+Copyright (c) 2013-2019
+Frank Denis <j at pureftpd dot org>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/NOTICE b/NOTICE
index 75b86fc..41506fe 100644
--- a/NOTICE
+++ b/NOTICE
@@ -50,6 +50,7 @@ This software includes third party software subject to the following copyrights:
 - Android tool chain cmake build files - Copyright (c) 2010-2011, Ethan Rublee and Copyright (c) 2011-2014, Andrey Kamaev
 - gsl-lite - Copyright (c) 2015 Martin Moene and Copyright (c) 2015 Microsoft Corporation. All rights reserved.
 - optional-lite - Copyright (c) 2015-2018 Martin Moene under the Boost Software License
+- libsodium - Copyright (c) 2013 - 2018 Frank Denis under the ISC software license
 
 The licenses for these third party components are included in LICENSE.txt
 
diff --git a/cmake/BundledLibSodium.cmake b/cmake/BundledLibSodium.cmake
new file mode 100644
index 0000000..6e3b7e4
--- /dev/null
+++ b/cmake/BundledLibSodium.cmake
@@ -0,0 +1,95 @@
+# 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.
+
+function(use_bundled_libsodium SOURCE_DIR BINARY_DIR)
+    message("Using bundled libsodium")
+
+    # Define patch step
+    if (WIN32)
+        set(PC "${Patch_EXECUTABLE}" -p1 -i "${SOURCE_DIR}/thirdparty/libsodium/libsodium.patch")
+    endif()
+
+    # Define byproduct
+    if (WIN32)
+        set(BYPRODUCT "lib/sodium.lib")
+    else()
+        set(BYPRODUCT "lib/libsodium.a")
+    endif()
+
+    # Set build options
+    set(LIBSODIUM_BIN_DIR "${BINARY_DIR}/thirdparty/libsodium-install" CACHE STRING "" FORCE)
+
+    if (WIN32)
+        set(LIBSODIUM_CMAKE_ARGS ${PASSTHROUGH_CMAKE_ARGS}
+                "-DCMAKE_INSTALL_PREFIX=${LIBSODIUM_BIN_DIR}"
+                "-DSODIUM_LIBRARY_MINIMAL=1")
+    endif()
+
+    # Build project
+    set(LIBSODIUM_URL https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz)
+    set(LIBSODIUM_URL_HASH "SHA256=6f504490b342a4f8a4c4a02fc9b866cbef8622d5df4e5452b46be121e46636c1")
+
+    if (WIN32)
+        ExternalProject_Add(
+                libsodium-external
+                URL ${LIBSODIUM_URL}
+                URL_HASH ${LIBSODIUM_URL_HASH}
+                SOURCE_DIR "${BINARY_DIR}/thirdparty/libsodium-src"
+                LIST_SEPARATOR % # This is needed for passing semicolon-separated lists
+                CMAKE_ARGS ${LIBSODIUM_CMAKE_ARGS}
+                PATCH_COMMAND ${PC}
+                BUILD_BYPRODUCTS "${LIBSODIUM_BIN_DIR}/${BYPRODUCT}"
+                EXCLUDE_FROM_ALL TRUE
+        )
+    else()
+        set(CONFIGURE_COMMAND ./configure --disable-pie --enable-minimal "--prefix=${LIBSODIUM_BIN_DIR}")
+
+        ExternalProject_Add(
+                libsodium-external
+                URL ${LIBSODIUM_URL}
+                URL_HASH ${LIBSODIUM_URL_HASH}
+                BUILD_IN_SOURCE true
+                SOURCE_DIR "${BINARY_DIR}/thirdparty/libsodium-src"
+                BUILD_COMMAND make
+                CMAKE_COMMAND ""
+                UPDATE_COMMAND ""
+                INSTALL_COMMAND make install
+                BUILD_BYPRODUCTS "${LIBSODIUM_BIN_DIR}/${BYPRODUCT}"
+                CONFIGURE_COMMAND "${CONFIGURE_COMMAND}"
+                PATCH_COMMAND ""
+                STEP_TARGETS build
+                EXCLUDE_FROM_ALL TRUE
+        )
+    endif()
+
+    # Set variables
+    set(LIBSODIUM_FOUND "YES" CACHE STRING "" FORCE)
+    set(LIBSODIUM_INCLUDE_DIRS "${LIBSODIUM_BIN_DIR}/include" CACHE STRING "" FORCE)
+    set(LIBSODIUM_LIBRARIES "${LIBSODIUM_BIN_DIR}/${BYPRODUCT}" CACHE STRING "" FORCE)
+
+    # Set exported variables for FindPackage.cmake
+    set(PASSTHROUGH_VARIABLES ${PASSTHROUGH_VARIABLES} "-DEXPORTED_LIBSODIUM_INCLUDE_DIRS=${LIBSODIUM_INCLUDE_DIRS}" CACHE STRING "" FORCE)
+    set(PASSTHROUGH_VARIABLES ${PASSTHROUGH_VARIABLES} "-DEXPORTED_LIBSODIUM_LIBRARIES=${LIBSODIUM_LIBRARIES}" CACHE STRING "" FORCE)
+
+    # Create imported targets
+    file(MAKE_DIRECTORY ${LIBSODIUM_INCLUDE_DIRS})
+
+    add_library(libsodium STATIC IMPORTED)
+    set_target_properties(libsodium PROPERTIES IMPORTED_LOCATION "${LIBSODIUM_LIBRARIES}")
+    add_dependencies(libsodium libsodium-external)
+    set_property(TARGET libsodium APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${LIBSODIUM_INCLUDE_DIRS}")
+endfunction(use_bundled_libsodium)
diff --git a/encrypt-config/CMakeLists.txt b/encrypt-config/CMakeLists.txt
new file mode 100644
index 0000000..f818dd4
--- /dev/null
+++ b/encrypt-config/CMakeLists.txt
@@ -0,0 +1,25 @@
+#
+# 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.
+
+file(GLOB ENCRYPT_CONFIG_FILES  "*.cpp")
+add_executable(encrypt-config "${ENCRYPT_CONFIG_FILES}")
+target_include_directories(encrypt-config PRIVATE ../libminifi/include)
+target_wholearchive_library(encrypt-config minifi)
+target_link_libraries(encrypt-config libsodium)
+set_target_properties(encrypt-config PROPERTIES OUTPUT_NAME encrypt-config)
+install(TARGETS encrypt-config RUNTIME DESTINATION bin COMPONENT bin)
diff --git a/encrypt-config/ConfigFile.cpp b/encrypt-config/ConfigFile.cpp
new file mode 100644
index 0000000..4d6d0ca
--- /dev/null
+++ b/encrypt-config/ConfigFile.cpp
@@ -0,0 +1,177 @@
+/**
+ * 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.
+ */
+
+#include "ConfigFile.h"
+
+#include <algorithm>
+#include <fstream>
+#include <utility>
+
+#include "utils/StringUtils.h"
+
+namespace {
+constexpr std::array<const char*, 2> DEFAULT_SENSITIVE_PROPERTIES{"nifi.security.client.pass.phrase",
+                                                                  "nifi.rest.api.password"};
+constexpr const char* ADDITIONAL_SENSITIVE_PROPS_PROPERTY_NAME = "nifi.sensitive.props.additional.keys";
+}  // namespace
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+ConfigLine::ConfigLine(std::string line) : line_(line) {
+  line = utils::StringUtils::trim(line);
+  if (line.empty() || line[0] == '#') { return; }
+
+  size_t index_of_first_equals_sign = line.find('=');
+  if (index_of_first_equals_sign == std::string::npos) { return; }
+
+  std::string key = utils::StringUtils::trim(line.substr(0, index_of_first_equals_sign));
+  if (key.empty()) { return; }
+
+  key_ = key;
+  value_ = utils::StringUtils::trim(line.substr(index_of_first_equals_sign + 1));
+}
+
+ConfigLine::ConfigLine(std::string key, std::string value)
+  : line_{utils::StringUtils::join_pack(key, "=", value)}, key_{std::move(key)}, value_{std::move(value)} {
+}
+
+void ConfigLine::updateValue(const std::string& value) {
+  auto pos = line_.find('=');
+  if (pos != std::string::npos) {
+    line_.replace(pos + 1, std::string::npos, value);
+    value_ = value;
+  } else {
+    throw std::invalid_argument{"Cannot update value in config line: it does not contain an = sign!"};
+  }
+}
+
+ConfigFile::ConfigFile(std::istream& input_stream) {
+  std::string line;
+  while (std::getline(input_stream, line)) {
+    config_lines_.push_back(ConfigLine{line});
+  }
+}
+
+ConfigFile::Lines::const_iterator ConfigFile::findKey(const std::string& key) const {
+  return std::find_if(config_lines_.cbegin(), config_lines_.cend(), [&key](const ConfigLine& config_line) {
+    return config_line.getKey() == key;
+  });
+}
+
+ConfigFile::Lines::iterator ConfigFile::findKey(const std::string& key) {
+  return std::find_if(config_lines_.begin(), config_lines_.end(), [&key](const ConfigLine& config_line) {
+    return config_line.getKey() == key;
+  });
+}
+
+bool ConfigFile::hasValue(const std::string& key) const {
+  const auto it = findKey(key);
+  return (it != config_lines_.end());
+}
+
+utils::optional<std::string> ConfigFile::getValue(const std::string& key) const {
+  const auto it = findKey(key);
+  if (it != config_lines_.end()) {
+    return it->getValue();
+  } else {
+    return utils::nullopt;
+  }
+}
+
+void ConfigFile::update(const std::string& key, const std::string& value) {
+  auto it = findKey(key);
+  if (it != config_lines_.end()) {
+    it->updateValue(value);
+  } else {
+    throw std::invalid_argument{"Key " + key + " not found in the config file!"};
+  }
+}
+
+void ConfigFile::insertAfter(const std::string& after_key, const std::string& key, const std::string& value) {
+  auto it = findKey(after_key);
+  if (it != config_lines_.end()) {
+    ++it;
+    config_lines_.emplace(it, key, value);
+  } else {
+    throw std::invalid_argument{"Key " + after_key + " not found in the config file!"};
+  }
+}
+
+void ConfigFile::append(const std::string& key, const std::string& value) {
+  config_lines_.emplace_back(key, value);
+}
+
+int ConfigFile::erase(const std::string& key) {
+  auto has_this_key = [&key](const ConfigLine& line) { return line.getKey() == key; };
+  auto new_end = std::remove_if(config_lines_.begin(), config_lines_.end(), has_this_key);
+  auto num_removed = std::distance(new_end, config_lines_.end());
+  config_lines_.erase(new_end, config_lines_.end());
+  return gsl::narrow<int>(num_removed);
+}
+
+void ConfigFile::writeTo(const std::string& file_path) const {
+  try {
+    std::ofstream file{file_path};
+    file.exceptions(std::ios::failbit | std::ios::badbit);
+
+    for (const auto& config_line : config_lines_) {
+      file << config_line.getLine() << '\n';
+    }
+  } catch (const std::exception&) {
+    throw std::runtime_error{"Could not write to file " + file_path};
+  }
+}
+
+std::vector<std::string> ConfigFile::getSensitiveProperties() const {
+  std::vector<std::string> sensitive_properties(DEFAULT_SENSITIVE_PROPERTIES.begin(), DEFAULT_SENSITIVE_PROPERTIES.end());
+  const utils::optional<std::string> additional_sensitive_props_list = getValue(ADDITIONAL_SENSITIVE_PROPS_PROPERTY_NAME);
+  if (additional_sensitive_props_list) {
+    std::vector<std::string> additional_sensitive_properties = utils::StringUtils::split(*additional_sensitive_props_list, ",");
+    sensitive_properties = mergeProperties(sensitive_properties, additional_sensitive_properties);
+  }
+
+  const auto not_found = [this](const std::string& property_name) { return !hasValue(property_name); };
+  const auto new_end = std::remove_if(sensitive_properties.begin(), sensitive_properties.end(), not_found);
+  sensitive_properties.erase(new_end, sensitive_properties.end());
+
+  return sensitive_properties;
+}
+
+std::vector<std::string> ConfigFile::mergeProperties(std::vector<std::string> properties,
+                                                     const std::vector<std::string>& additional_properties) {
+  for (const auto& property_name : additional_properties) {
+    std::string property_name_trimmed = utils::StringUtils::trim(property_name);
+    if (!property_name_trimmed.empty()) {
+      properties.push_back(std::move(property_name_trimmed));
+    }
+  }
+
+  std::sort(properties.begin(), properties.end());
+  auto new_end = std::unique(properties.begin(), properties.end());
+  properties.erase(new_end, properties.end());
+  return properties;
+}
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/encrypt-config/ConfigFile.h b/encrypt-config/ConfigFile.h
new file mode 100644
index 0000000..8eba793
--- /dev/null
+++ b/encrypt-config/ConfigFile.h
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <istream>
+#include <string>
+#include <vector>
+
+#include "utils/EncryptionUtils.h"
+#include "utils/OptionalUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+class ConfigLine {
+ public:
+  explicit ConfigLine(std::string line);
+  ConfigLine(std::string key, std::string value);
+
+  void updateValue(const std::string& value);
+
+  std::string getLine() const { return line_; }
+  std::string getKey() const { return key_; }
+  std::string getValue() const { return value_; }
+
+ private:
+  // NOTE(fgerlits): having both line_ and { key_, value } is redundant in many cases, but
+  // * we need the original line_ in order to preserve formatting, comments and blank lines
+  // * we could get rid of key_ and value_ and parse them each time from line_, but I think the code is clearer this way
+  std::string line_;
+  std::string key_;
+  std::string value_;
+};
+
+class ConfigFile {
+ public:
+  explicit ConfigFile(std::istream& input_stream);
+  explicit ConfigFile(std::istream&& input_stream) : ConfigFile{input_stream} {}
+
+  bool hasValue(const std::string& key) const;
+  utils::optional<std::string> getValue(const std::string& key) const;
+  void update(const std::string& key, const std::string& value);
+  void insertAfter(const std::string& after_key, const std::string& key, const std::string& value);
+  void append(const std::string& key, const std::string& value);
+  int erase(const std::string& key);
+
+  void writeTo(const std::string& file_path) const;
+
+  size_t size() const { return config_lines_.size(); }
+
+  std::vector<std::string> getSensitiveProperties() const;
+
+ private:
+  friend class ConfigFileTestAccessor;
+  friend bool operator==(const ConfigFile&, const ConfigFile&);
+  using Lines = std::vector<ConfigLine>;
+
+  Lines::const_iterator findKey(const std::string& key) const;
+  Lines::iterator findKey(const std::string& key);
+  static std::vector<std::string> mergeProperties(std::vector<std::string> properties,
+                                                  const std::vector<std::string>& additional_properties);
+
+  Lines config_lines_;
+};
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/encrypt-config/ConfigFileEncryptor.cpp b/encrypt-config/ConfigFileEncryptor.cpp
new file mode 100644
index 0000000..a62d2ad
--- /dev/null
+++ b/encrypt-config/ConfigFileEncryptor.cpp
@@ -0,0 +1,64 @@
+/**
+ * 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.
+ */
+
+#include "ConfigFileEncryptor.h"
+
+#include <iostream>
+#include <string>
+
+#include "utils/StringUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+uint32_t encryptSensitivePropertiesInFile(ConfigFile& config_file, const utils::crypto::Bytes& encryption_key) {
+  int num_properties_encrypted = 0;
+
+  for (const auto& property_key : config_file.getSensitiveProperties()) {
+    utils::optional<std::string> property_value = config_file.getValue(property_key);
+    if (!property_value) { continue; }
+
+    std::string encryption_type_key = property_key + ".protected";
+    utils::optional<std::string> encryption_type = config_file.getValue(encryption_type_key);
+
+    if (!encryption_type || encryption_type->empty() || *encryption_type == "plaintext") {
+      std::string encrypted_property_value = utils::crypto::encrypt(*property_value, encryption_key);
+
+      config_file.update(property_key, encrypted_property_value);
+
+      if (encryption_type) {
+        config_file.update(encryption_type_key, utils::crypto::EncryptionType::name());
+      } else {
+        config_file.insertAfter(property_key, encryption_type_key, utils::crypto::EncryptionType::name());
+      }
+
+      std::cout << "Encrypted property: " << property_key << '\n';
+      ++num_properties_encrypted;
+    }
+  }
+
+  return num_properties_encrypted;
+}
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/encrypt-config/ConfigFileEncryptor.h b/encrypt-config/ConfigFileEncryptor.h
new file mode 100644
index 0000000..169edc1
--- /dev/null
+++ b/encrypt-config/ConfigFileEncryptor.h
@@ -0,0 +1,34 @@
+/**
+ * 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.
+ */
+#pragma once
+
+#include "ConfigFile.h"
+#include "utils/EncryptionUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+uint32_t encryptSensitivePropertiesInFile(ConfigFile& config_file, const utils::crypto::Bytes& encryption_key);
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/encrypt-config/EncryptConfig.cpp b/encrypt-config/EncryptConfig.cpp
new file mode 100644
index 0000000..2d6dea6
--- /dev/null
+++ b/encrypt-config/EncryptConfig.cpp
@@ -0,0 +1,158 @@
+/**
+ * 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.
+ */
+
+#include "EncryptConfig.h"
+
+#include <sodium.h>
+
+#include <stdexcept>
+
+#include "ConfigFile.h"
+#include "ConfigFileEncryptor.h"
+#include "utils/file/FileUtils.h"
+#include "utils/OptionalUtils.h"
+
+namespace {
+
+constexpr const char* CONF_DIRECTORY_NAME = "conf";
+constexpr const char* BOOTSTRAP_FILE_NAME = "bootstrap.conf";
+constexpr const char* MINIFI_PROPERTIES_FILE_NAME = "minifi.properties";
+constexpr const char* ENCRYPTION_KEY_PROPERTY_NAME = "nifi.bootstrap.sensitive.key";
+constexpr const char* USAGE_STRING = "Usage: encrypt-config --minifi-home <your-minifi-home>";
+
+}  // namespace
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+EncryptConfig::EncryptConfig(int argc, char* argv[]) : minifi_home_(parseMinifiHomeFromTheOptions(argc, argv)) {
+  if (sodium_init() < 0) {
+    throw std::runtime_error{"Could not initialize the libsodium library!"};
+  }
+}
+
+std::string EncryptConfig::parseMinifiHomeFromTheOptions(int argc, char* argv[]) {
+  if (argc >= 2) {
+    for (int i = 1; i < argc; ++i) {
+      std::string argstr(argv[i]);
+      if ((argstr == "-h") || (argstr == "--help")) {
+        std::cout << USAGE_STRING << std::endl;
+        std::exit(0);
+      }
+    }
+  }
+
+  if (argc >= 3) {
+    for (int i = 1; i < argc; ++i) {
+      std::string argstr(argv[i]);
+      if ((argstr == "-m") || (argstr == "--minifi-home")) {
+        if (i+1 < argc) {
+          return std::string(argv[i+1]);
+        }
+      }
+    }
+  }
+
+  throw std::runtime_error{USAGE_STRING};
+}
+
+void EncryptConfig::encryptSensitiveProperties() const {
+  utils::crypto::Bytes encryption_key = getEncryptionKey();
+  encryptSensitiveProperties(encryption_key);
+}
+
+std::string EncryptConfig::bootstrapFilePath() const {
+  return utils::file::concat_path(
+      utils::file::concat_path(minifi_home_, CONF_DIRECTORY_NAME),
+      BOOTSTRAP_FILE_NAME);
+}
+
+std::string EncryptConfig::propertiesFilePath() const {
+  return utils::file::concat_path(
+      utils::file::concat_path(minifi_home_, CONF_DIRECTORY_NAME),
+      MINIFI_PROPERTIES_FILE_NAME);
+}
+
+utils::crypto::Bytes EncryptConfig::getEncryptionKey() const {
+  encrypt_config::ConfigFile bootstrap_file{std::ifstream{bootstrapFilePath()}};
+  utils::optional<std::string> key_from_bootstrap_file = bootstrap_file.getValue(ENCRYPTION_KEY_PROPERTY_NAME);
+
+  if (key_from_bootstrap_file && !key_from_bootstrap_file->empty()) {
+    std::string binary_key = hexDecodeAndValidateKey(*key_from_bootstrap_file);
+    std::cout << "Using the existing encryption key found in " << bootstrapFilePath() << '\n';
+    return utils::crypto::stringToBytes(binary_key);
+  } else {
+    std::cout << "Generating a new encryption key...\n";
+    utils::crypto::Bytes encryption_key = utils::crypto::generateKey();
+    writeEncryptionKeyToBootstrapFile(encryption_key);
+    std::cout << "Wrote the new encryption key to " << bootstrapFilePath() << '\n';
+    return encryption_key;
+  }
+}
+
+std::string EncryptConfig::hexDecodeAndValidateKey(const std::string& key) const {
+  // Note: from_hex() allows [and skips] non-hex characters
+  std::string binary_key = utils::StringUtils::from_hex(key);
+  if (binary_key.size() == utils::crypto::EncryptionType::keyLength()) {
+    return binary_key;
+  } else {
+    std::stringstream error;
+    error << "The encryption key " << ENCRYPTION_KEY_PROPERTY_NAME << " in the bootstrap file\n"
+        << "    " << bootstrapFilePath() << '\n'
+        << "is invalid; delete it to generate a new key.";
+    throw std::runtime_error{error.str()};
+  }
+}
+
+void EncryptConfig::writeEncryptionKeyToBootstrapFile(const utils::crypto::Bytes& encryption_key) const {
+  std::string key_encoded = utils::StringUtils::to_hex(utils::crypto::bytesToString(encryption_key));
+  encrypt_config::ConfigFile bootstrap_file{std::ifstream{bootstrapFilePath()}};
+
+  if (bootstrap_file.hasValue(ENCRYPTION_KEY_PROPERTY_NAME)) {
+    bootstrap_file.update(ENCRYPTION_KEY_PROPERTY_NAME, key_encoded);
+  } else {
+    bootstrap_file.append(ENCRYPTION_KEY_PROPERTY_NAME, key_encoded);
+  }
+
+  bootstrap_file.writeTo(bootstrapFilePath());
+}
+
+void EncryptConfig::encryptSensitiveProperties(const utils::crypto::Bytes& encryption_key) const {
+  encrypt_config::ConfigFile properties_file{std::ifstream{propertiesFilePath()}};
+  if (properties_file.size() == 0) {
+    throw std::runtime_error{"Properties file " + propertiesFilePath() + " not found!"};
+  }
+
+  uint32_t num_properties_encrypted = encryptSensitivePropertiesInFile(properties_file, encryption_key);
+  if (num_properties_encrypted == 0) {
+    std::cout << "Could not find any (new) sensitive properties to encrypt in " << propertiesFilePath() << '\n';
+    return;
+  }
+
+  properties_file.writeTo(propertiesFilePath());
+  std::cout << "Encrypted " << num_properties_encrypted << " sensitive "
+      << (num_properties_encrypted == 1 ? "property" : "properties") << " in " << propertiesFilePath() << '\n';
+}
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/encrypt-config/EncryptConfig.h b/encrypt-config/EncryptConfig.h
new file mode 100644
index 0000000..dd8b351
--- /dev/null
+++ b/encrypt-config/EncryptConfig.h
@@ -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.
+ */
+#pragma once
+
+#include <string>
+
+#include "utils/EncryptionUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+class EncryptConfig {
+ public:
+  EncryptConfig(int argc, char* argv[]);
+  void encryptSensitiveProperties() const;
+
+ private:
+  std::string bootstrapFilePath() const;
+  std::string propertiesFilePath() const;
+
+  static std::string parseMinifiHomeFromTheOptions(int argc, char* argv[]);
+
+  utils::crypto::Bytes getEncryptionKey() const;
+  std::string hexDecodeAndValidateKey(const std::string& key) const;
+  void writeEncryptionKeyToBootstrapFile(const utils::crypto::Bytes& encryption_key) const;
+
+  void encryptSensitiveProperties(const utils::crypto::Bytes& encryption_key) const;
+
+  const std::string minifi_home_;
+};
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/encrypt-config/EncryptConfigMain.cpp b/encrypt-config/EncryptConfigMain.cpp
new file mode 100644
index 0000000..6622e51
--- /dev/null
+++ b/encrypt-config/EncryptConfigMain.cpp
@@ -0,0 +1,33 @@
+/**
+ * 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.
+ */
+
+#include <iostream>
+#include <typeinfo>
+
+#include "EncryptConfig.h"
+
+int main(int argc, char* argv[]) try {
+  org::apache::nifi::minifi::encrypt_config::EncryptConfig encrypt_config{argc, argv};
+  encrypt_config.encryptSensitiveProperties();
+  return 0;
+} catch (const std::exception& ex) {
+  std::cerr << ex.what() << "\n(" << typeid(ex).name() << ")\n";
+  return 1;
+} catch (...) {
+  std::cerr << "Unknown error\n";
+  return 2;
+}
diff --git a/encrypt-config/tests/CMakeLists.txt b/encrypt-config/tests/CMakeLists.txt
new file mode 100644
index 0000000..f5305d1
--- /dev/null
+++ b/encrypt-config/tests/CMakeLists.txt
@@ -0,0 +1,41 @@
+#
+# 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.
+
+file(GLOB ENCRYPT_CONFIG_TESTS  "*.cpp")
+
+file(GLOB ENCRYPT_CONFIG_SOURCES  "${CMAKE_SOURCE_DIR}/encrypt-config/*.cpp")
+list(REMOVE_ITEM ENCRYPT_CONFIG_SOURCES "${CMAKE_SOURCE_DIR}/encrypt-config/EncryptConfigMain.cpp")
+
+set(ENCRYPT_CONFIG_TEST_COUNT 0)
+foreach(testfile ${ENCRYPT_CONFIG_TESTS})
+  get_filename_component(testfilename "${testfile}" NAME_WE)
+  add_executable(${testfilename} "${testfile}" ${ENCRYPT_CONFIG_SOURCES})
+
+  target_include_directories(${testfilename} BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/thirdparty/catch")
+  target_include_directories(${testfilename} BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/encrypt-config")
+  target_include_directories(${testfilename} BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/libminifi/include")
+  target_include_directories(${testfilename} BEFORE PRIVATE "${CMAKE_SOURCE_DIR}/libminifi/test")
+
+  target_wholearchive_library(${testfilename} minifi)
+  target_link_libraries(${testfilename} ${CATCH_MAIN_LIB})
+  add_test(NAME ${testfilename} COMMAND ${testfilename} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
+
+  math(EXPR ENCRYPT_CONFIG_TEST_COUNT "${ENCRYPT_CONFIG_TEST_COUNT}+1")
+endforeach()
+
+message("-- Finished building ${ENCRYPT_CONFIG_TEST_COUNT} encrypt-config test file(s)...")
diff --git a/encrypt-config/tests/ConfigFileEncryptorTests.cpp b/encrypt-config/tests/ConfigFileEncryptorTests.cpp
new file mode 100644
index 0000000..e20791a
--- /dev/null
+++ b/encrypt-config/tests/ConfigFileEncryptorTests.cpp
@@ -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.
+ */
+
+#include "ConfigFileEncryptor.h"
+
+#include "TestBase.h"
+#include "utils/RegexUtils.h"
+
+using org::apache::nifi::minifi::encrypt_config::ConfigFile;
+using org::apache::nifi::minifi::encrypt_config::encryptSensitivePropertiesInFile;
+
+namespace {
+size_t base64_length(size_t unencoded_length) {
+  return (unencoded_length + 2) / 3 * 4;
+}
+
+bool check_encryption(const ConfigFile& test_file, const std::string& property_name, size_t original_value_length) {
+    utils::optional<std::string> encrypted_value = test_file.getValue(property_name);
+    if (!encrypted_value) { return false; }
+
+    utils::optional<std::string> encryption_type = test_file.getValue(property_name + ".protected");
+    if (!encryption_type || *encryption_type != utils::crypto::EncryptionType::name()) { return false; }
+
+    auto length = base64_length(utils::crypto::EncryptionType::nonceLength()) +
+        utils::crypto::EncryptionType::separator().size() +
+        base64_length(original_value_length + utils::crypto::EncryptionType::macLength());
+    return utils::Regex::matchesFullInput("[0-9A-Za-z/+=|]{" + std::to_string(length) + "}", *encrypted_value);
+}
+}  // namespace
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+// NOTE(fgerlits): these ==/!= operators are in the test file on purpose, and should not be part of production code,
+// as they take a varying amount of time depending on which character in the line differs, so they would open up
+// our code to timing attacks.  If you need == in production code, make sure to compare all pairs of chars/lines.
+bool operator==(const ConfigLine& left, const ConfigLine& right) { return left.getLine() == right.getLine(); }
+bool operator!=(const ConfigLine& left, const ConfigLine& right) { return !(left == right); }
+bool operator==(const ConfigFile& left, const ConfigFile& right) { return left.config_lines_ == right.config_lines_; }
+bool operator!=(const ConfigFile& left, const ConfigFile& right) { return !(left == right); }
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
+
+TEST_CASE("ConfigFileEncryptor can encrypt the sensitive properties", "[encrypt-config][encryptSensitivePropertiesInFile]") {
+  utils::crypto::Bytes KEY = utils::crypto::stringToBytes(utils::StringUtils::from_base64(
+      "6q9u8LEDy1/CdmSBm8oSqPS/Ds5UOD2nRouP8yUoK10="));
+
+  SECTION("default properties") {
+    ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+    std::string original_password = test_file.getValue("nifi.rest.api.password").value();
+
+    uint32_t num_properties_encrypted = encryptSensitivePropertiesInFile(test_file, KEY);
+
+    REQUIRE(num_properties_encrypted == 1);
+    REQUIRE(test_file.size() == 102);
+    REQUIRE(check_encryption(test_file, "nifi.rest.api.password", original_password.length()));
+
+    SECTION("calling encryptSensitiveProperties a second time does nothing") {
+      ConfigFile test_file_copy = test_file;
+
+      uint32_t num_properties_encrypted = encryptSensitivePropertiesInFile(test_file, KEY);
+
+      REQUIRE(num_properties_encrypted == 0);
+      REQUIRE(test_file == test_file_copy);
+    }
+
+    SECTION("if you reset the password, it will get encrypted again") {
+      test_file.update("nifi.rest.api.password", original_password);
+
+      SECTION("remove the .protected property") {
+        int num_lines_removed = test_file.erase("nifi.rest.api.password.protected");
+        REQUIRE(num_lines_removed == 1);
+      }
+      SECTION("change the value of the .protected property to blank") {
+        test_file.update("nifi.rest.api.password.protected", "");
+      }
+      SECTION("change the value of the .protected property to 'plaintext'") {
+        test_file.update("nifi.rest.api.password.protected", "plaintext");
+      }
+
+      uint32_t num_properties_encrypted = encryptSensitivePropertiesInFile(test_file, KEY);
+
+      REQUIRE(num_properties_encrypted == 1);
+      REQUIRE(check_encryption(test_file, "nifi.rest.api.password", original_password.length()));
+    }
+  }
+
+  SECTION("with additional properties") {
+    ConfigFile test_file{std::ifstream{"resources/with-additional-sensitive-props.minifi.properties"}};
+    size_t original_file_size = test_file.size();
+
+    std::string original_c2_enable = test_file.getValue("nifi.c2.enable").value();
+    std::string original_flow_config_file = test_file.getValue("nifi.flow.configuration.file").value();
+    std::string original_password = test_file.getValue("nifi.rest.api.password").value();
+    std::string original_pass_phrase = test_file.getValue("nifi.security.client.pass.phrase").value();
+
+    uint32_t num_properties_encrypted = encryptSensitivePropertiesInFile(test_file, KEY);
+
+    REQUIRE(num_properties_encrypted == 4);
+    REQUIRE(test_file.size() == original_file_size + 4);
+
+    REQUIRE(check_encryption(test_file, "nifi.c2.enable", original_c2_enable.length()));
+    REQUIRE(check_encryption(test_file, "nifi.flow.configuration.file", original_flow_config_file.length()));
+    REQUIRE(check_encryption(test_file, "nifi.rest.api.password", original_password.length()));
+    REQUIRE(check_encryption(test_file, "nifi.security.client.pass.phrase", original_pass_phrase.length()));
+  }
+}
diff --git a/encrypt-config/tests/ConfigFileTests.cpp b/encrypt-config/tests/ConfigFileTests.cpp
new file mode 100644
index 0000000..36cc214
--- /dev/null
+++ b/encrypt-config/tests/ConfigFileTests.cpp
@@ -0,0 +1,238 @@
+/**
+ * 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.
+ */
+
+#include "ConfigFile.h"
+
+#include "gsl/gsl-lite.hpp"
+
+#include "TestBase.h"
+#include "utils/file/FileUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace encrypt_config {
+
+class ConfigFileTestAccessor {
+ public:
+  static std::vector<std::string> mergeProperties(std::vector<std::string> left, const std::vector<std::string>& right) {
+    return ConfigFile::mergeProperties(left, right);
+  }
+};
+
+}  // namespace encrypt_config
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
+
+using org::apache::nifi::minifi::encrypt_config::ConfigFile;
+using org::apache::nifi::minifi::encrypt_config::ConfigFileTestAccessor;
+using org::apache::nifi::minifi::encrypt_config::ConfigLine;
+
+TEST_CASE("ConfigLine can be constructed from a line", "[encrypt-config][constructor]") {
+  auto line_is_parsed_correctly = [](const std::string& line, const std::string& expected_key, const std::string& expected_value) {
+    ConfigLine config_line{line};
+    return config_line.getKey() == expected_key && config_line.getValue() == expected_value;
+  };
+
+  REQUIRE(line_is_parsed_correctly("", "", ""));
+  REQUIRE(line_is_parsed_correctly("    \t  \r", "", ""));
+  REQUIRE(line_is_parsed_correctly("#disabled.setting=foo", "", ""));
+  REQUIRE(line_is_parsed_correctly("some line without an equals sign", "", ""));
+  REQUIRE(line_is_parsed_correctly("=value_without_key", "", ""));
+  REQUIRE(line_is_parsed_correctly("\t  =value_without_key", "", ""));
+
+  REQUIRE(line_is_parsed_correctly("nifi.some.key=", "nifi.some.key", ""));
+  REQUIRE(line_is_parsed_correctly("nifi.some.key=some_value", "nifi.some.key", "some_value"));
+  REQUIRE(line_is_parsed_correctly("nifi.some.key = some_value", "nifi.some.key", "some_value"));
+  REQUIRE(line_is_parsed_correctly("\tnifi.some.key\t=\tsome_value", "nifi.some.key", "some_value"));
+  REQUIRE(line_is_parsed_correctly("nifi.some.key=some_value  \r", "nifi.some.key", "some_value"));
+  REQUIRE(line_is_parsed_correctly("nifi.some.key=some value", "nifi.some.key", "some value"));
+  REQUIRE(line_is_parsed_correctly("nifi.some.key=value=with=equals=signs=", "nifi.some.key", "value=with=equals=signs="));
+}
+
+TEST_CASE("ConfigLine can be constructed from a key-value pair", "[encrypt-config][constructor]") {
+  auto can_construct_from_kv = [](const std::string& key, const std::string& value, const std::string& expected_line) {
+    ConfigLine config_line{key, value};
+    return config_line.getLine() == expected_line;
+  };
+
+  REQUIRE(can_construct_from_kv("nifi.some.key", "", "nifi.some.key="));
+  REQUIRE(can_construct_from_kv("nifi.some.key", "some_value", "nifi.some.key=some_value"));
+}
+
+TEST_CASE("ConfigLine can update the value", "[encrypt-config][updateValue]") {
+  auto can_update_value = [](const std::string& original_line, const std::string& new_value, const std::string& expected_line) {
+    ConfigLine config_line{original_line};
+    config_line.updateValue(new_value);
+    return config_line.getLine() == expected_line;
+  };
+
+  REQUIRE(can_update_value("nifi.some.key=some_value", "new_value", "nifi.some.key=new_value"));
+  REQUIRE(can_update_value("nifi.some.key=", "new_value", "nifi.some.key=new_value"));
+  REQUIRE(can_update_value("nifi.some.key=some_value", "", "nifi.some.key="));
+  REQUIRE(can_update_value("nifi.some.key=some_value", "very_long_new_value", "nifi.some.key=very_long_new_value"));
+
+  // whitespace is preserved in the key, but not in the value
+  REQUIRE(can_update_value("nifi.some.key= some_value", "some_value", "nifi.some.key=some_value"));
+  REQUIRE(can_update_value("nifi.some.key = some_value  ", "some_value", "nifi.some.key =some_value"));
+  REQUIRE(can_update_value("  nifi.some.key=some_value", "some_value", "  nifi.some.key=some_value"));
+  REQUIRE(can_update_value("  nifi.some.key =\tsome_value\r", "some_value", "  nifi.some.key =some_value"));
+}
+
+TEST_CASE("ConfigFile creates an empty object from a nonexistent file", "[encrypt-config][constructor]") {
+  ConfigFile test_file{std::ifstream{"resources/nonexistent-minifi.properties"}};
+  REQUIRE(test_file.size() == 0);
+}
+
+TEST_CASE("ConfigFile can parse a simple config file", "[encrypt-config][constructor]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+  REQUIRE(test_file.size() == 101);
+}
+
+TEST_CASE("ConfigFile can test whether a key is present", "[encrypt-config][hasValue]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+  REQUIRE(test_file.hasValue("nifi.version"));
+  REQUIRE(test_file.hasValue("nifi.c2.flow.id"));  // present but blank
+  REQUIRE(!test_file.hasValue("nifi.remote.input.secure"));  // commented out
+  REQUIRE(!test_file.hasValue("nifi.this.property.does.not.exist"));
+}
+
+TEST_CASE("ConfigFile can read empty properties correctly", "[encrypt-config][constructor]") {
+  ConfigFile test_file{std::ifstream{"resources/with-additional-sensitive-props.minifi.properties"}};
+  REQUIRE(test_file.size() == 103);
+
+  auto empty_property = test_file.getValue("nifi.security.need.ClientAuth");
+  REQUIRE(empty_property);
+  REQUIRE(empty_property->empty());
+
+  auto whitespace_property = test_file.getValue("nifi.security.client.certificate");  // value = " \t\r"
+  REQUIRE(whitespace_property);
+  REQUIRE(whitespace_property->empty());
+}
+
+TEST_CASE("ConfigFile can find the value for a key", "[encrypt-config][getValue]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+
+  SECTION("valid key") {
+    REQUIRE(test_file.getValue("nifi.bored.yield.duration") == utils::optional<std::string>{"10 millis"});
+  }
+
+  SECTION("nonexistent key") {
+    REQUIRE(test_file.getValue("nifi.bored.panda") == utils::nullopt);
+  }
+}
+
+TEST_CASE("ConfigFile can update the value for a key", "[encrypt-config][update]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+
+  SECTION("valid key") {
+    test_file.update("nifi.bored.yield.duration", "20 millis");
+    REQUIRE(test_file.getValue("nifi.bored.yield.duration") == utils::optional<std::string>{"20 millis"});
+  }
+
+  SECTION("nonexistent key") {
+    REQUIRE_THROWS(test_file.update("nifi.bored.panda", "cat video"));
+  }
+}
+
+TEST_CASE("ConfigFile can add a new setting after an existing setting", "[encrypt-config][insertAfter]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+
+  SECTION("valid key") {
+    test_file.insertAfter("nifi.rest.api.password", "nifi.rest.api.password.protected", "my-cipher-name");
+    REQUIRE(test_file.size() == 102);
+    REQUIRE(test_file.getValue("nifi.rest.api.password.protected") == utils::optional<std::string>{"my-cipher-name"});
+  }
+
+  SECTION("nonexistent key") {
+    REQUIRE_THROWS(test_file.insertAfter("nifi.toil.api.password", "key", "value"));
+  }
+}
+
+TEST_CASE("ConfigFile can add a new setting at the end", "[encrypt-config][append]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+
+  const std::string KEY = "nifi.bootstrap.sensitive.key";
+  const std::string VALUE = "aa411f289c91685ef9d5a9e5a4fad9393ff4c7a78ab978484323488caed7a9ab";
+  test_file.append(KEY, VALUE);
+  REQUIRE(test_file.size() == 102);
+  REQUIRE(test_file.getValue(KEY) == utils::make_optional(VALUE));
+}
+
+TEST_CASE("ConfigFile can write to a new file", "[encrypt-config][writeTo]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+  test_file.update("nifi.bored.yield.duration", "20 millis");
+
+  char format[] = "/tmp/ConfigFileTests.tmp.XXXXXX";
+  std::string temp_dir = utils::file::create_temp_directory(format);
+  auto remove_directory = gsl::finally([&temp_dir]() { utils::file::delete_dir(temp_dir); });
+  std::string file_path = utils::file::concat_path(temp_dir, "minifi.properties");
+
+  test_file.writeTo(file_path);
+
+  ConfigFile test_file_copy{std::ifstream{file_path}};
+  REQUIRE(test_file.size() == test_file_copy.size());
+  REQUIRE(test_file_copy.getValue("nifi.bored.yield.duration") == utils::optional<std::string>{"20 millis"});
+}
+
+TEST_CASE("ConfigFile will throw if we try to write to an invalid file name", "[encrypt-config][writeTo]") {
+  ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+  const char* file_path = "/tmp/3915913c-b37d-4adc-b6a8-b8e36e44c639/6ede949c-12b3-4a91-8956-71bc6ab6f73e/some.file";
+  REQUIRE_THROWS(test_file.writeTo(file_path));
+}
+
+TEST_CASE("ConfigFile can merge lists of property names", "[encrypt-config][mergeProperties]") {
+  using vector = std::vector<std::string>;
+
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{}, vector{}) == vector{});
+
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a"}, vector{}) == vector{"a"});
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a"}, vector{"a"}) == vector{"a"});
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a"}, vector{"b"}) == (vector{"a", "b"}));
+
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"c"}) == (vector{"a", "b", "c"}));
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"a", "b"}) == (vector{"a", "b"}));
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"a", "c"}) == (vector{"a", "b", "c"}));
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"b", "c"}) == (vector{"a", "b", "c"}));
+
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a"}, vector{" a"}) == vector{"a"});
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a"}, vector{"a "}) == vector{"a"});
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a"}, vector{" a "}) == vector{"a"});
+
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"\tc"}) == (vector{"a", "b", "c"}));
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"a\n", "b"}) == (vector{"a", "b"}));
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"a", "c\r\n"}) == (vector{"a", "b", "c"}));
+  REQUIRE(ConfigFileTestAccessor::mergeProperties(vector{"a", "b"}, vector{"b\n", "\t c"}) == (vector{"a", "b", "c"}));
+}
+
+TEST_CASE("ConfigFile can find the list of sensitive properties", "[encrypt-config][getSensitiveProperties]") {
+  SECTION("default properties") {
+    ConfigFile test_file{std::ifstream{"resources/minifi.properties"}};
+    std::vector<std::string> expected_properties{"nifi.rest.api.password"};
+    REQUIRE(test_file.getSensitiveProperties() == expected_properties);
+  }
+
+  SECTION("with additional properties") {
+    ConfigFile test_file{std::ifstream{"resources/with-additional-sensitive-props.minifi.properties"}};
+    std::vector<std::string> expected_properties{
+        "nifi.c2.enable", "nifi.flow.configuration.file", "nifi.rest.api.password", "nifi.security.client.pass.phrase"};
+    REQUIRE(test_file.getSensitiveProperties() == expected_properties);
+  }
+}
diff --git a/encrypt-config/tests/resources/minifi.properties b/encrypt-config/tests/resources/minifi.properties
new file mode 100644
index 0000000..f1ca0a2
--- /dev/null
+++ b/encrypt-config/tests/resources/minifi.properties
@@ -0,0 +1,101 @@
+# 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.
+
+# Core Properties #
+nifi.version=0.7.0
+nifi.flow.configuration.file=./conf/config.yml
+nifi.administrative.yield.duration=30 sec
+# If a component has no work to do (is "bored"), how long should we wait before checking again for work?
+nifi.bored.yield.duration=10 millis
+
+# Provenance Repository #
+nifi.provenance.repository.directory.default=${MINIFI_HOME}/provenance_repository
+nifi.provenance.repository.max.storage.time=1 MIN
+nifi.provenance.repository.max.storage.size=1 MB
+nifi.flowfile.repository.directory.default=${MINIFI_HOME}/flowfile_repository
+nifi.database.content.repository.directory.default=${MINIFI_HOME}/content_repository
+
+#nifi.remote.input.secure=true
+#nifi.security.need.ClientAuth=
+#nifi.security.client.certificate=
+#nifi.security.client.private.key=
+#nifi.security.client.pass.phrase=
+#nifi.security.client.ca.certificate=
+
+nifi.rest.api.user.name=admin
+nifi.rest.api.password=password
+
+# State storage configuration #
+## The default state storage can be overridden by specifying a controller service instance
+## that implements CoreComponentStateManagementProvider
+## (e.g. an instance of RocksDbPersistableKeyValueStoreService or UnorderedMapPersistableKeyValueStoreService)
+#nifi.state.management.provider.local=
+## To make the default state storage persist every state change, set this to true
+## this comes at a performance penalty, but makes sure no state is lost even on unclean shutdowns
+#nifi.state.management.provider.local.always.persist=true
+## To change the frequency at which the default state storage is persisted, modify the following
+#nifi.state.management.provider.local.auto.persistence.interval=1 min
+
+## Enabling C2 Uncomment each of the following options
+## define those with missing options
+nifi.c2.enable=true
+## define protocol parameters
+## The default is CoAP, if that extension is built.
+## Alternatively, you may use RESTSender if http-curl is built
+#nifi.c2.agent.protocol.class=CoapProtocol
+nifi.c2.agent.protocol.class=RESTSender
+#nifi.c2.agent.coap.host=
+#nifi.c2.agent.coap.port=
+## base URL of the c2 server,
+## very likely the same base url of rest urls
+nifi.c2.flow.base.url=http://localhost:10080/efm/api
+nifi.c2.rest.url=http://localhost:10080/efm/api/c2-protocol/heartbeat
+nifi.c2.rest.url.ack=http://localhost:10080/efm/api/c2-protocol/acknowledge
+nifi.c2.root.classes=DeviceInfoNode,AgentInformation,FlowInformation
+## Minimize heartbeat payload size by excluding agent manifest from the heartbeat
+#nifi.c2.full.heartbeat=false
+## heartbeat 4 times a second
+#nifi.c2.agent.heartbeat.period=250
+## define parameters about your agent
+nifi.c2.agent.class=EncryptConfigTester
+nifi.c2.agent.identifier=EncryptConfigTester-001
+
+## define metrics reported
+nifi.c2.root.class.definitions=metrics
+nifi.c2.root.class.definitions.metrics.name=metrics
+nifi.c2.root.class.definitions.metrics.metrics=typedmetrics
+nifi.c2.root.class.definitions.metrics.metrics.typedmetrics.name=RuntimeMetrics
+nifi.c2.root.class.definitions.metrics.metrics.queuemetrics.name=QueueMetrics
+nifi.c2.root.class.definitions.metrics.metrics.queuemetrics.classes=QueueMetrics
+nifi.c2.root.class.definitions.metrics.metrics.typedmetrics.classes=ProcessMetrics,SystemInformation
+nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.name=ProcessorMetric
+nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.classes=GetFileMetrics
+
+## enable the controller socket provider on port 9998
+## off by default. C2 must be enabled to support these
+#controller.socket.host=localhost
+#controller.socket.port=9998
+
+
+#JNI properties
+nifi.framework.dir=${MINIFI_HOME}/minifi-jni/lib
+nifi.nar.directory=${MINIFI_HOME}/minifi-jni/nars
+nifi.nar.deploy.directory=${MINIFI_HOME}/minifi-jni/nardeploy
+nifi.nar.docs.directory=${MINIFI_HOME}/minifi-jni/nardocs
+# must be comma separated
+nifi.jvm.options=-Xmx1G
+nifi.python.processor.dir=${MINIFI_HOME}/minifi-python/
+nifi.c2.flow.id=
+nifi.c2.flow.url=
diff --git a/encrypt-config/tests/resources/with-additional-sensitive-props.minifi.properties b/encrypt-config/tests/resources/with-additional-sensitive-props.minifi.properties
new file mode 100644
index 0000000..d021f0f
--- /dev/null
+++ b/encrypt-config/tests/resources/with-additional-sensitive-props.minifi.properties
@@ -0,0 +1,103 @@
+# 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.
+
+# Core Properties #
+nifi.version=0.7.0
+nifi.flow.configuration.file=./conf/config.yml
+nifi.administrative.yield.duration=30 sec
+# If a component has no work to do (is "bored"), how long should we wait before checking again for work?
+nifi.bored.yield.duration=10 millis
+
+# Provenance Repository #
+nifi.provenance.repository.directory.default=${MINIFI_HOME}/provenance_repository
+nifi.provenance.repository.max.storage.time=1 MIN
+nifi.provenance.repository.max.storage.size=1 MB
+nifi.flowfile.repository.directory.default=${MINIFI_HOME}/flowfile_repository
+nifi.database.content.repository.directory.default=${MINIFI_HOME}/content_repository
+
+nifi.remote.input.secure=true
+nifi.security.need.ClientAuth=
+nifi.security.client.certificate= 	
+nifi.security.client.private.key=
+nifi.security.client.pass.phrase=correct_horse_battery_staple
+nifi.security.client.ca.certificate=
+
+nifi.sensitive.props.additional.keys=nifi.flow.configuration.file ,nifi.rest.api.password, nifi.c2.enable,
+
+nifi.rest.api.user.name=admin
+nifi.rest.api.password=password
+
+# State storage configuration #
+## The default state storage can be overridden by specifying a controller service instance
+## that implements CoreComponentStateManagementProvider
+## (e.g. an instance of RocksDbPersistableKeyValueStoreService or UnorderedMapPersistableKeyValueStoreService)
+#nifi.state.management.provider.local=
+## To make the default state storage persist every state change, set this to true
+## this comes at a performance penalty, but makes sure no state is lost even on unclean shutdowns
+#nifi.state.management.provider.local.always.persist=true
+## To change the frequency at which the default state storage is persisted, modify the following
+#nifi.state.management.provider.local.auto.persistence.interval=1 min
+
+## Enabling C2 Uncomment each of the following options
+## define those with missing options
+nifi.c2.enable=true
+## define protocol parameters
+## The default is CoAP, if that extension is built.
+## Alternatively, you may use RESTSender if http-curl is built
+#nifi.c2.agent.protocol.class=CoapProtocol
+nifi.c2.agent.protocol.class=RESTSender
+#nifi.c2.agent.coap.host=
+#nifi.c2.agent.coap.port=
+## base URL of the c2 server,
+## very likely the same base url of rest urls
+nifi.c2.flow.base.url=http://localhost:10080/efm/api
+nifi.c2.rest.url=http://localhost:10080/efm/api/c2-protocol/heartbeat
+nifi.c2.rest.url.ack=http://localhost:10080/efm/api/c2-protocol/acknowledge
+nifi.c2.root.classes=DeviceInfoNode,AgentInformation,FlowInformation
+## Minimize heartbeat payload size by excluding agent manifest from the heartbeat
+#nifi.c2.full.heartbeat=false
+## heartbeat 4 times a second
+#nifi.c2.agent.heartbeat.period=250
+## define parameters about your agent
+nifi.c2.agent.class=EncryptConfigTester
+nifi.c2.agent.identifier=EncryptConfigTester-001
+
+## define metrics reported
+nifi.c2.root.class.definitions=metrics
+nifi.c2.root.class.definitions.metrics.name=metrics
+nifi.c2.root.class.definitions.metrics.metrics=typedmetrics
+nifi.c2.root.class.definitions.metrics.metrics.typedmetrics.name=RuntimeMetrics
+nifi.c2.root.class.definitions.metrics.metrics.queuemetrics.name=QueueMetrics
+nifi.c2.root.class.definitions.metrics.metrics.queuemetrics.classes=QueueMetrics
+nifi.c2.root.class.definitions.metrics.metrics.typedmetrics.classes=ProcessMetrics,SystemInformation
+nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.name=ProcessorMetric
+nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.classes=GetFileMetrics
+
+## enable the controller socket provider on port 9998
+## off by default. C2 must be enabled to support these
+#controller.socket.host=localhost
+#controller.socket.port=9998
+
+
+#JNI properties
+nifi.framework.dir=${MINIFI_HOME}/minifi-jni/lib
+nifi.nar.directory=${MINIFI_HOME}/minifi-jni/nars
+nifi.nar.deploy.directory=${MINIFI_HOME}/minifi-jni/nardeploy
+nifi.nar.docs.directory=${MINIFI_HOME}/minifi-jni/nardocs
+# must be comma separated
+nifi.jvm.options=-Xmx1G
+nifi.python.processor.dir=${MINIFI_HOME}/minifi-python/
+nifi.c2.flow.id=
+nifi.c2.flow.url=
diff --git a/libminifi/CMakeLists.txt b/libminifi/CMakeLists.txt
index 656da27..333fb4c 100644
--- a/libminifi/CMakeLists.txt
+++ b/libminifi/CMakeLists.txt
@@ -113,7 +113,7 @@ if(WIN32)
         target_include_directories(core-minifi PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include")
 endif()
 
-list(APPEND LIBMINIFI_LIBRARIES yaml-cpp ZLIB::ZLIB concurrentqueue RapidJSON spdlog cron Threads::Threads gsl-lite optional-lite)
+list(APPEND LIBMINIFI_LIBRARIES yaml-cpp ZLIB::ZLIB concurrentqueue RapidJSON spdlog cron Threads::Threads gsl-lite optional-lite libsodium)
 if(NOT WIN32)
 	list(APPEND LIBMINIFI_LIBRARIES OSSP::libuuid++)
 endif()
diff --git a/libminifi/include/properties/Configure.h b/libminifi/include/properties/Configuration.h
similarity index 95%
copy from libminifi/include/properties/Configure.h
copy to libminifi/include/properties/Configuration.h
index e1ad1f5..c15df6c 100644
--- a/libminifi/include/properties/Configure.h
+++ b/libminifi/include/properties/Configuration.h
@@ -1,7 +1,4 @@
 /**
- * @file Configure.h
- * Configure class declaration
- *
  * 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.
@@ -17,8 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#ifndef LIBMINIFI_INCLUDE_PROPERTIES_CONFIGURE_H_
-#define LIBMINIFI_INCLUDE_PROPERTIES_CONFIGURE_H_
+
+#pragma once
 
 #include <string>
 #include <mutex>
@@ -29,9 +26,9 @@ namespace apache {
 namespace nifi {
 namespace minifi {
 
-class Configure : public Properties {
+class Configuration : public Properties {
  public:
-  Configure() : Properties("MiNiFi configuration") {}
+  Configuration() : Properties("MiNiFi configuration") {}
 
   void setAgentIdentifier(const std::string &identifier) {
     std::lock_guard<std::mutex> lock(mutex_);
@@ -121,4 +118,3 @@ class Configure : public Properties {
 }  // namespace nifi
 }  // namespace apache
 }  // namespace org
-#endif  // LIBMINIFI_INCLUDE_PROPERTIES_CONFIGURE_H_
diff --git a/libminifi/include/properties/Configure.h b/libminifi/include/properties/Configure.h
index e1ad1f5..8b26bef 100644
--- a/libminifi/include/properties/Configure.h
+++ b/libminifi/include/properties/Configure.h
@@ -1,7 +1,4 @@
 /**
- * @file Configure.h
- * Configure class declaration
- *
  * 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.
@@ -17,108 +14,36 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#ifndef LIBMINIFI_INCLUDE_PROPERTIES_CONFIGURE_H_
-#define LIBMINIFI_INCLUDE_PROPERTIES_CONFIGURE_H_
+#pragma once
 
 #include <string>
-#include <mutex>
-#include "properties/Properties.h"
+#include <utility>
+
+#include "properties/Configuration.h"
+#include "properties/Decryptor.h"
+#include "utils/OptionalUtils.h"
 
 namespace org {
 namespace apache {
 namespace nifi {
 namespace minifi {
 
-class Configure : public Properties {
+class Configure : public Configuration {
  public:
-  Configure() : Properties("MiNiFi configuration") {}
-
-  void setAgentIdentifier(const std::string &identifier) {
-    std::lock_guard<std::mutex> lock(mutex_);
-    agent_identifier_ = identifier;
-  }
-  std::string getAgentIdentifier() const {
-    std::lock_guard<std::mutex> lock(mutex_);
-    return agent_identifier_;
-  }
-
-  void setAgentClass(const std::string& agentClass) {
-    std::lock_guard<std::mutex> lock(mutex_);
-    agent_class_ = agentClass;
-  }
-
-  std::string getAgentClass() const {
-    std::lock_guard<std::mutex> lock(mutex_);
-    return agent_class_;
-  }
+  explicit Configure(utils::optional<Decryptor> decryptor = utils::nullopt)
+      : Configuration{}, decryptor_(std::move(decryptor)) {}
 
-  // nifi.flow.configuration.file
-  static constexpr const char *nifi_default_directory = "nifi.default.directory";
-  static constexpr const char *nifi_flow_configuration_file = "nifi.flow.configuration.file";
-  static constexpr const char *nifi_flow_configuration_file_exit_failure = "nifi.flow.configuration.file.exit.onfailure";
-  static constexpr const char *nifi_flow_configuration_file_backup_update = "nifi.flow.configuration.backup.on.update";
-  static constexpr const char *nifi_flow_engine_threads = "nifi.flow.engine.threads";
-  static constexpr const char *nifi_flow_engine_alert_period = "nifi.flow.engine.alert.period";
-  static constexpr const char *nifi_flow_engine_event_driven_time_slice = "nifi.flow.engine.event.driven.time.slice";
-  static constexpr const char *nifi_administrative_yield_duration = "nifi.administrative.yield.duration";
-  static constexpr const char *nifi_bored_yield_duration = "nifi.bored.yield.duration";
-  static constexpr const char *nifi_graceful_shutdown_seconds = "nifi.flowcontroller.graceful.shutdown.period";
-  static constexpr const char *nifi_flowcontroller_drain_timeout = "nifi.flowcontroller.drain.timeout";
-  static constexpr const char *nifi_log_level = "nifi.log.level";
-  static constexpr const char *nifi_server_name = "nifi.server.name";
-  static constexpr const char *nifi_configuration_class_name = "nifi.flow.configuration.class.name";
-  static constexpr const char *nifi_flow_repository_class_name = "nifi.flowfile.repository.class.name";
-  static constexpr const char *nifi_content_repository_class_name = "nifi.content.repository.class.name";
-  static constexpr const char *nifi_volatile_repository_options = "nifi.volatile.repository.options.";
-  static constexpr const char *nifi_provenance_repository_class_name = "nifi.provenance.repository.class.name";
-  static constexpr const char *nifi_server_port = "nifi.server.port";
-  static constexpr const char *nifi_server_report_interval = "nifi.server.report.interval";
-  static constexpr const char *nifi_provenance_repository_max_storage_size = "nifi.provenance.repository.max.storage.size";
-  static constexpr const char *nifi_provenance_repository_max_storage_time = "nifi.provenance.repository.max.storage.time";
-  static constexpr const char *nifi_provenance_repository_directory_default = "nifi.provenance.repository.directory.default";
-  static constexpr const char *nifi_flowfile_repository_max_storage_size = "nifi.flowfile.repository.max.storage.size";
-  static constexpr const char *nifi_flowfile_repository_max_storage_time = "nifi.flowfile.repository.max.storage.time";
-  static constexpr const char *nifi_flowfile_repository_directory_default = "nifi.flowfile.repository.directory.default";
-  static constexpr const char *nifi_dbcontent_repository_directory_default = "nifi.database.content.repository.directory.default";
-  static constexpr const char *nifi_remote_input_secure = "nifi.remote.input.secure";
-  static constexpr const char *nifi_remote_input_http = "nifi.remote.input.http.enabled";
-  static constexpr const char *nifi_security_need_ClientAuth = "nifi.security.need.ClientAuth";
-  // site2site security config
-  static constexpr const char *nifi_security_client_certificate = "nifi.security.client.certificate";
-  static constexpr const char *nifi_security_client_private_key = "nifi.security.client.private.key";
-  static constexpr const char *nifi_security_client_pass_phrase = "nifi.security.client.pass.phrase";
-  static constexpr const char *nifi_security_client_ca_certificate = "nifi.security.client.ca.certificate";
-
-  // nifi rest api user name and password
-  static constexpr const char *nifi_rest_api_user_name = "nifi.rest.api.user.name";
-  static constexpr const char *nifi_rest_api_password = "nifi.rest.api.password";
-  // c2 options
-  static constexpr const char *nifi_c2_enable = "nifi.c2.enable";
-  static constexpr const char *nifi_c2_file_watch = "nifi.c2.file.watch";
-  static constexpr const char *nifi_c2_flow_id = "nifi.c2.flow.id";
-  static constexpr const char *nifi_c2_flow_url = "nifi.c2.flow.url";
-  static constexpr const char *nifi_c2_flow_base_url = "nifi.c2.flow.base.url";
-  static constexpr const char *nifi_c2_full_heartbeat = "nifi.c2.full.heartbeat";
-
-  // state management options
-  static constexpr const char *nifi_state_management_provider_local = "nifi.state.management.provider.local";
-  static constexpr const char *nifi_state_management_provider_local_always_persist = "nifi.state.management.provider.local.always.persist";
-  static constexpr const char *nifi_state_management_provider_local_auto_persistence_interval = "nifi.state.management.provider.local.auto.persistence.interval";
-
-  // disk space watchdog options
-  static constexpr const char *minifi_disk_space_watchdog_enable = "minifi.disk.space.watchdog.enable";
-  static constexpr const char *minifi_disk_space_watchdog_interval = "minifi.disk.space.watchdog.interval";
-  static constexpr const char *minifi_disk_space_watchdog_stop_threshold = "minifi.disk.space.watchdog.stop.threshold";
-  static constexpr const char *minifi_disk_space_watchdog_restart_threshold = "minifi.disk.space.watchdog.restart.threshold";
+  bool get(const std::string& key, std::string& value) const;
+  bool get(const std::string& key, const std::string& alternate_key, std::string& value) const;
+  utils::optional<std::string> get(const std::string& key) const;
 
  private:
-  std::string agent_identifier_;
-  std::string agent_class_;
-  mutable std::mutex mutex_;
+  bool isEncrypted(const std::string& key) const;
+
+  utils::optional<Decryptor> decryptor_;
 };
 
 }  // namespace minifi
 }  // namespace nifi
 }  // namespace apache
 }  // namespace org
-#endif  // LIBMINIFI_INCLUDE_PROPERTIES_CONFIGURE_H_
diff --git a/libminifi/include/properties/Decryptor.h b/libminifi/include/properties/Decryptor.h
new file mode 100644
index 0000000..06bf830
--- /dev/null
+++ b/libminifi/include/properties/Decryptor.h
@@ -0,0 +1,55 @@
+/**
+ * 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.
+ */
+#pragma once
+
+#include <string>
+
+#include "utils/EncryptionUtils.h"
+#include "utils/OptionalUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+
+class Decryptor {
+ public:
+  explicit Decryptor(const utils::crypto::Bytes& encryption_key);
+
+  static bool isValidEncryptionMarker(const utils::optional<std::string>& encryption_marker);
+  std::string decrypt(const std::string& encrypted_text) const;
+
+  static utils::optional<Decryptor> create(const std::string& minifi_home);
+
+ private:
+  const utils::crypto::Bytes encryption_key_;
+};
+
+inline Decryptor::Decryptor(const utils::crypto::Bytes& encryption_key) : encryption_key_(encryption_key) {}
+
+inline bool Decryptor::isValidEncryptionMarker(const utils::optional<std::string>& encryption_marker) {
+  return encryption_marker && *encryption_marker == utils::crypto::EncryptionType::name();
+}
+
+inline std::string Decryptor::decrypt(const std::string& encrypted_text) const {
+  return utils::crypto::decrypt(encrypted_text, encryption_key_);
+}
+
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/libminifi/include/properties/Properties.h b/libminifi/include/properties/Properties.h
index 94d2bca..33f8a3a 100644
--- a/libminifi/include/properties/Properties.h
+++ b/libminifi/include/properties/Properties.h
@@ -65,18 +65,7 @@ class Properties {
    * @param value value in which to place the map's stored property value
    * @returns true if found, false otherwise.
    */
-  bool get(const std::string &key, std::string &value) const;
-
-  /**
-   * Returns the config value by placing it into the referenced param value
-   * Uses alternate_key if key is not found within the map.
-   *
-   * @param key key to look up
-   * @param alternate_key is the secondary lookup key if key is not found
-   * @param value value in which to place the map's stored property value
-   * @returns true if found, false otherwise.
-   */
-  bool get(const std::string &key, const std::string &alternate_key, std::string &value) const;
+  bool getString(const std::string &key, std::string &value) const;
 
   /**
    * Returns the configuration value or an empty string.
@@ -84,9 +73,15 @@ class Properties {
    */
   int getInt(const std::string &key, int default_value) const;
 
-  utils::optional<std::string> get(const std::string& key) const {
+  /**
+   * Returns the config value.
+   *
+   * @param key key to look up
+   * @returns the value if found, nullopt otherwise.
+   */
+  utils::optional<std::string> getString(const std::string& key) const {
     std::string result;
-    const bool found = get(key, result);
+    const bool found = getString(key, result);
     if (found) {
       return result;
     } else {
diff --git a/libminifi/include/utils/EncryptionUtils.h b/libminifi/include/utils/EncryptionUtils.h
new file mode 100644
index 0000000..c691937
--- /dev/null
+++ b/libminifi/include/utils/EncryptionUtils.h
@@ -0,0 +1,109 @@
+/**
+ * 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.
+ */
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace utils {
+namespace crypto {
+
+using Bytes = std::vector<unsigned char>;
+
+Bytes stringToBytes(const std::string& text);
+
+std::string bytesToString(const Bytes& bytes);
+
+Bytes generateKey();
+
+Bytes randomBytes(size_t num_bytes);
+
+struct EncryptionType {
+  static std::string name();
+  static size_t keyLength();
+  static size_t nonceLength();
+  static size_t macLength();
+  static std::string separator();
+};
+
+/**
+ * Encrypt the input (raw version).
+ *
+ * Uses libsodium's secretbox encryption.
+ * Takes the plaintext as input, and returns the ciphertext (encrypted plaintext) plus the MAC (message authentication
+ * code), in binary form.
+ * The ciphertext has the same length as the plaintext, and the MAC is always EncryptionType::macLength() bytes long,
+ * so the output is EncryptionType::macLength() bytes longer than the input.
+ *
+ * @param plaintext plaintext
+ * @param key secret key of EncryptionType::keyLength() bytes, should be generated by a CSPRNG
+ * @param nonce random data freshly generated for each encryption to break rainbow table-type attacks;
+ *              not encrypted and not secret, but included when computing the ciphertext;
+ *              the length is EncryptionType::nonceLength() bytes, should be generated by a CSPRNG
+ * @return ciphertext plus MAC
+ * @throws std::exception if the input is incorrect (wrong lengths)
+ */
+Bytes encryptRaw(const Bytes& plaintext, const Bytes& key, const Bytes& nonce);
+
+/**
+ * Encrypt the plaintext using a randomly-generated nonce.
+ *
+ * * Generates a random nonce,
+ * * calls encryptRaw(),
+ * * base64-encodes the nonce and the ciphertext-plus-MAC, and
+ * * returns <encoded nonce><EncryptionType::separator()><encoded ciphertext-plus-MAC>.
+ */
+std::string encrypt(const std::string& plaintext, const Bytes& key);
+
+/**
+ * Decrypt the input (raw version).
+ *
+ * Uses libsodium's secretbox decryption.
+ * Takes the (binary) ciphertext plus MAC as input, and returns the plaintext.  It also authenticates the input by
+ * checking the MAC.
+ * The plaintext has the same length as the ciphertext, and the MAC is always EncryptionType::macLength() bytes long,
+ * so the output is EncryptionType::macLength() bytes shorter than the input.
+ *
+ * @param input ciphertext plus MAC
+ * @param key secret key of EncryptionType::keyLength() bytes, must be the same as used when encrypting
+ * @param nonce random data freshly generated for each encryption to break rainbow table-type attacks;
+ *              EncryptionType::nonceLength() bytes, must be the same as used when encrypting
+ * @return plaintext
+ * @throws std::exception if either the decryption or the authentication fails
+ */
+Bytes decryptRaw(const Bytes& input, const Bytes& key, const Bytes& nonce);
+
+/**
+ * Decrypt an input of the form nonce + EncryptionType::separator() + ciphertext_plus_MAC.
+ *
+ * * Splits the input at EncryptionType::separator(),
+ * * base64-decodes the two parts of the input (nonce and ciphertext-plus-MAC),
+ * * calls deryptRaw(),
+ * * returns the decrypted plaintext.
+ */
+std::string decrypt(const std::string& input, const Bytes& key);
+
+}  // namespace crypto
+}  // namespace utils
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/libminifi/src/Configuration.cpp b/libminifi/src/Configuration.cpp
new file mode 100644
index 0000000..35b7462
--- /dev/null
+++ b/libminifi/src/Configuration.cpp
@@ -0,0 +1,77 @@
+/**
+ * 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.
+ */
+#include "properties/Configuration.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+
+constexpr const char *Configuration::nifi_default_directory;
+constexpr const char *Configuration::nifi_c2_enable;
+constexpr const char *Configuration::nifi_flow_configuration_file;
+constexpr const char *Configuration::nifi_flow_configuration_file_exit_failure;
+constexpr const char *Configuration::nifi_flow_configuration_file_backup_update;
+constexpr const char *Configuration::nifi_flow_engine_threads;
+constexpr const char *Configuration::nifi_flow_engine_alert_period;
+constexpr const char *Configuration::nifi_flow_engine_event_driven_time_slice;
+constexpr const char *Configuration::nifi_administrative_yield_duration;
+constexpr const char *Configuration::nifi_bored_yield_duration;
+constexpr const char *Configuration::nifi_graceful_shutdown_seconds;
+constexpr const char *Configuration::nifi_flowcontroller_drain_timeout;
+constexpr const char *Configuration::nifi_log_level;
+constexpr const char *Configuration::nifi_server_name;
+constexpr const char *Configuration::nifi_configuration_class_name;
+constexpr const char *Configuration::nifi_flow_repository_class_name;
+constexpr const char *Configuration::nifi_content_repository_class_name;
+constexpr const char *Configuration::nifi_volatile_repository_options;
+constexpr const char *Configuration::nifi_provenance_repository_class_name;
+constexpr const char *Configuration::nifi_server_port;
+constexpr const char *Configuration::nifi_server_report_interval;
+constexpr const char *Configuration::nifi_provenance_repository_max_storage_size;
+constexpr const char *Configuration::nifi_provenance_repository_max_storage_time;
+constexpr const char *Configuration::nifi_provenance_repository_directory_default;
+constexpr const char *Configuration::nifi_flowfile_repository_max_storage_size;
+constexpr const char *Configuration::nifi_flowfile_repository_max_storage_time;
+constexpr const char *Configuration::nifi_flowfile_repository_directory_default;
+constexpr const char *Configuration::nifi_dbcontent_repository_directory_default;
+constexpr const char *Configuration::nifi_remote_input_secure;
+constexpr const char *Configuration::nifi_remote_input_http;
+constexpr const char *Configuration::nifi_security_need_ClientAuth;
+constexpr const char *Configuration::nifi_security_client_certificate;
+constexpr const char *Configuration::nifi_security_client_private_key;
+constexpr const char *Configuration::nifi_security_client_pass_phrase;
+constexpr const char *Configuration::nifi_security_client_ca_certificate;
+constexpr const char *Configuration::nifi_rest_api_user_name;
+constexpr const char *Configuration::nifi_rest_api_password;
+constexpr const char *Configuration::nifi_c2_file_watch;
+constexpr const char *Configuration::nifi_c2_flow_id;
+constexpr const char *Configuration::nifi_c2_flow_url;
+constexpr const char *Configuration::nifi_c2_flow_base_url;
+constexpr const char *Configuration::nifi_c2_full_heartbeat;
+constexpr const char *Configuration::nifi_state_management_provider_local;
+constexpr const char *Configuration::nifi_state_management_provider_local_always_persist;
+constexpr const char *Configuration::nifi_state_management_provider_local_auto_persistence_interval;
+constexpr const char *Configuration::minifi_disk_space_watchdog_enable;
+constexpr const char *Configuration::minifi_disk_space_watchdog_interval;
+constexpr const char *Configuration::minifi_disk_space_watchdog_stop_threshold;
+constexpr const char *Configuration::minifi_disk_space_watchdog_restart_threshold;
+
+} /* namespace minifi */
+} /* namespace nifi */
+} /* namespace apache */
+} /* namespace org */
diff --git a/libminifi/src/Configure.cpp b/libminifi/src/Configure.cpp
index 956ca4a..28de0c8 100644
--- a/libminifi/src/Configure.cpp
+++ b/libminifi/src/Configure.cpp
@@ -15,62 +15,52 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 #include "properties/Configure.h"
 
+#include "utils/gsl.h"
+
+#include "core/logging/LoggerConfiguration.h"
+
 namespace org {
 namespace apache {
 namespace nifi {
 namespace minifi {
 
-constexpr const char *Configure::nifi_default_directory;
-constexpr const char *Configure::nifi_c2_enable;
-constexpr const char *Configure::nifi_flow_configuration_file;
-constexpr const char *Configure::nifi_flow_configuration_file_exit_failure;
-constexpr const char *Configure::nifi_flow_configuration_file_backup_update;
-constexpr const char *Configure::nifi_flow_engine_threads;
-constexpr const char *Configure::nifi_flow_engine_alert_period;
-constexpr const char *Configure::nifi_flow_engine_event_driven_time_slice;
-constexpr const char *Configure::nifi_administrative_yield_duration;
-constexpr const char *Configure::nifi_bored_yield_duration;
-constexpr const char *Configure::nifi_graceful_shutdown_seconds;
-constexpr const char *Configure::nifi_flowcontroller_drain_timeout;
-constexpr const char *Configure::nifi_log_level;
-constexpr const char *Configure::nifi_server_name;
-constexpr const char *Configure::nifi_configuration_class_name;
-constexpr const char *Configure::nifi_flow_repository_class_name;
-constexpr const char *Configure::nifi_content_repository_class_name;
-constexpr const char *Configure::nifi_volatile_repository_options;
-constexpr const char *Configure::nifi_provenance_repository_class_name;
-constexpr const char *Configure::nifi_server_port;
-constexpr const char *Configure::nifi_server_report_interval;
-constexpr const char *Configure::nifi_provenance_repository_max_storage_size;
-constexpr const char *Configure::nifi_provenance_repository_max_storage_time;
-constexpr const char *Configure::nifi_provenance_repository_directory_default;
-constexpr const char *Configure::nifi_flowfile_repository_max_storage_size;
-constexpr const char *Configure::nifi_flowfile_repository_max_storage_time;
-constexpr const char *Configure::nifi_flowfile_repository_directory_default;
-constexpr const char *Configure::nifi_dbcontent_repository_directory_default;
-constexpr const char *Configure::nifi_remote_input_secure;
-constexpr const char *Configure::nifi_remote_input_http;
-constexpr const char *Configure::nifi_security_need_ClientAuth;
-constexpr const char *Configure::nifi_security_client_certificate;
-constexpr const char *Configure::nifi_security_client_private_key;
-constexpr const char *Configure::nifi_security_client_pass_phrase;
-constexpr const char *Configure::nifi_security_client_ca_certificate;
-constexpr const char *Configure::nifi_rest_api_user_name;
-constexpr const char *Configure::nifi_rest_api_password;
-constexpr const char *Configure::nifi_c2_file_watch;
-constexpr const char *Configure::nifi_c2_flow_id;
-constexpr const char *Configure::nifi_c2_flow_url;
-constexpr const char *Configure::nifi_c2_flow_base_url;
-constexpr const char *Configure::nifi_c2_full_heartbeat;
-constexpr const char *Configure::nifi_state_management_provider_local;
-constexpr const char *Configure::nifi_state_management_provider_local_always_persist;
-constexpr const char *Configure::nifi_state_management_provider_local_auto_persistence_interval;
-constexpr const char *Configure::minifi_disk_space_watchdog_enable;
-constexpr const char *Configure::minifi_disk_space_watchdog_interval;
-constexpr const char *Configure::minifi_disk_space_watchdog_stop_threshold;
-constexpr const char *Configure::minifi_disk_space_watchdog_restart_threshold;
+bool Configure::get(const std::string& key, std::string& value) const {
+  bool found = getString(key, value);
+  if (decryptor_ && found && isEncrypted(key)) {
+    value = decryptor_->decrypt(value);
+  }
+  return found;
+}
+
+bool Configure::get(const std::string& key, const std::string& alternate_key, std::string& value) const {
+  if (get(key, value)) {
+    return true;
+  } else if (get(alternate_key, value)) {
+    const auto logger = logging::LoggerFactory<Configure>::getLogger();
+    logger->log_warn("%s is an alternate property that may not be supported in future releases. Please use %s instead.", alternate_key, key);
+    return true;
+  } else {
+    return false;
+  }
+}
+
+utils::optional<std::string> Configure::get(const std::string& key) const {
+  utils::optional<std::string> value = getString(key);
+  if (decryptor_ && value && isEncrypted(key)) {
+    return decryptor_->decrypt(*value);
+  } else {
+    return value;
+  }
+}
+
+bool Configure::isEncrypted(const std::string& key) const {
+  gsl_Expects(decryptor_);
+  utils::optional<std::string> encryption_marker = getString(key + ".protected");
+  return decryptor_->isValidEncryptionMarker(encryption_marker);
+}
 
 } /* namespace minifi */
 } /* namespace nifi */
diff --git a/libminifi/src/Decryptor.cpp b/libminifi/src/Decryptor.cpp
new file mode 100644
index 0000000..313bc61
--- /dev/null
+++ b/libminifi/src/Decryptor.cpp
@@ -0,0 +1,54 @@
+/**
+ *
+ * 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.
+ */
+#include "properties/Decryptor.h"
+
+#include "properties/Properties.h"
+#include "utils/OptionalUtils.h"
+#include "utils/StringUtils.h"
+
+namespace {
+
+#ifdef WIN32
+constexpr const char* DEFAULT_NIFI_BOOTSTRAP_FILE = "\\conf\\bootstrap.conf";
+#else
+constexpr const char* DEFAULT_NIFI_BOOTSTRAP_FILE = "./conf/bootstrap.conf";
+#endif  // WIN32
+
+constexpr const char* CONFIG_ENCRYPTION_KEY_PROPERTY_NAME = "nifi.bootstrap.sensitive.key";
+
+}  // namespace
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+
+utils::optional<minifi::Decryptor> Decryptor::create(const std::string& minifi_home) {
+  minifi::Properties bootstrap_conf;
+  bootstrap_conf.setHome(minifi_home);
+  bootstrap_conf.loadConfigureFile(DEFAULT_NIFI_BOOTSTRAP_FILE);
+  return bootstrap_conf.getString(CONFIG_ENCRYPTION_KEY_PROPERTY_NAME)
+      | utils::map([](const std::string& encryption_key_hex) { return utils::StringUtils::from_hex(encryption_key_hex); })
+      | utils::map(&utils::crypto::stringToBytes)
+      | utils::map([](const utils::crypto::Bytes& encryption_key_bytes) { return minifi::Decryptor{encryption_key_bytes}; });
+}
+
+} /* namespace minifi */
+} /* namespace nifi */
+} /* namespace apache */
+} /* namespace org */
diff --git a/libminifi/src/Properties.cpp b/libminifi/src/Properties.cpp
index 88f19d5..0e615f7 100644
--- a/libminifi/src/Properties.cpp
+++ b/libminifi/src/Properties.cpp
@@ -36,7 +36,7 @@ Properties::Properties(const std::string& name)
 }
 
 // Get the config value
-bool Properties::get(const std::string &key, std::string &value) const {
+bool Properties::getString(const std::string &key, std::string &value) const {
   std::lock_guard<std::mutex> lock(mutex_);
   auto it = properties_.find(key);
 
@@ -48,25 +48,6 @@ bool Properties::get(const std::string &key, std::string &value) const {
   }
 }
 
-bool Properties::get(const std::string &key, const std::string &alternate_key, std::string &value) const {
-  std::lock_guard<std::mutex> lock(mutex_);
-  auto it = properties_.find(key);
-
-  if (it == properties_.end()) {
-    it = properties_.find(alternate_key);
-    if (it != properties_.end()) {
-      logger_->log_warn("%s is an alternate property that may not be supported in future releases. Please use %s instead.", alternate_key, key);
-    }
-  }
-
-  if (it != properties_.end()) {
-    value = it->second;
-    return true;
-  } else {
-    return false;
-  }
-}
-
 int Properties::getInt(const std::string &key, int default_value) const {
   std::lock_guard<std::mutex> lock(mutex_);
   auto it = properties_.find(key);
diff --git a/libminifi/src/core/logging/LoggerConfiguration.cpp b/libminifi/src/core/logging/LoggerConfiguration.cpp
index ae9c2fa..0b9ef46 100644
--- a/libminifi/src/core/logging/LoggerConfiguration.cpp
+++ b/libminifi/src/core/logging/LoggerConfiguration.cpp
@@ -86,7 +86,7 @@ void LoggerConfiguration::initialize(const std::shared_ptr<LoggerProperties> &lo
   std::lock_guard<std::mutex> lock(mutex);
   root_namespace_ = initialize_namespaces(logger_properties);
   std::string spdlog_pattern;
-  if (!logger_properties->get("spdlog.pattern", spdlog_pattern)) {
+  if (!logger_properties->getString("spdlog.pattern", spdlog_pattern)) {
     spdlog_pattern = spdlog_default_pattern;
   }
 
@@ -94,7 +94,7 @@ void LoggerConfiguration::initialize(const std::shared_ptr<LoggerProperties> &lo
    * There is no need to shorten names per spdlog sink as this is a per log instance.
    */
   std::string shorten_names_str;
-  if (logger_properties->get("spdlog.shorten_names", shorten_names_str)) {
+  if (logger_properties->getString("spdlog.shorten_names", shorten_names_str)) {
     utils::StringUtils::StringToBool(shorten_names_str, shorten_names_);
   }
 
@@ -137,7 +137,7 @@ std::shared_ptr<internal::LoggerNamespace> LoggerConfiguration::initialize_names
   for (auto const & appender_key : logger_properties->get_keys_of_type(appender_type)) {
     std::string appender_name = appender_key.substr(appender_type.length() + 1);
     std::string appender_type;
-    if (!logger_properties->get(appender_key, appender_type)) {
+    if (!logger_properties->getString(appender_key, appender_type)) {
       appender_type = "stderr";
     }
     std::transform(appender_type.begin(), appender_type.end(), appender_type.begin(), ::tolower);
@@ -146,11 +146,11 @@ std::shared_ptr<internal::LoggerNamespace> LoggerConfiguration::initialize_names
       sink_map[appender_name] = std::make_shared<spdlog::sinks::null_sink_st>();
     } else if ("rollingappender" == appender_type || "rolling appender" == appender_type || "rolling" == appender_type) {
       std::string file_name;
-      if (!logger_properties->get(appender_key + ".file_name", file_name)) {
+      if (!logger_properties->getString(appender_key + ".file_name", file_name)) {
         file_name = "minifi-app.log";
       }
       std::string directory;
-      if (!logger_properties->get(appender_key + ".directory", directory)) {
+      if (!logger_properties->getString(appender_key + ".directory", directory)) {
         // The below part assumes logger_properties->getHome() is existing
         // Cause minifiHome must be set at MiNiFiMain.cpp?
         directory = logger_properties->getHome() + utils::file::FileUtils::get_separator() + "logs";
@@ -164,7 +164,7 @@ std::shared_ptr<internal::LoggerNamespace> LoggerConfiguration::initialize_names
 
       int max_files = 3;
       std::string max_files_str = "";
-      if (logger_properties->get(appender_key + ".max_files", max_files_str)) {
+      if (logger_properties->getString(appender_key + ".max_files", max_files_str)) {
         try {
           max_files = std::stoi(max_files_str);
         } catch (const std::invalid_argument &) {
@@ -174,7 +174,7 @@ std::shared_ptr<internal::LoggerNamespace> LoggerConfiguration::initialize_names
 
       int max_file_size = 5 * 1024 * 1024;
       std::string max_file_size_str = "";
-      if (logger_properties->get(appender_key + ".max_file_size", max_file_size_str)) {
+      if (logger_properties->getString(appender_key + ".max_file_size", max_file_size_str)) {
         try {
           max_file_size = std::stoi(max_file_size_str);
         } catch (const std::invalid_argument &) {
@@ -197,7 +197,7 @@ std::shared_ptr<internal::LoggerNamespace> LoggerConfiguration::initialize_names
   std::string logger_type = "logger";
   for (auto const & logger_key : logger_properties->get_keys_of_type(logger_type)) {
     std::string logger_def;
-    if (!logger_properties->get(logger_key, logger_def)) {
+    if (!logger_properties->getString(logger_key, logger_def)) {
       continue;
     }
     bool first = true;
diff --git a/libminifi/src/utils/EncryptionUtils.cpp b/libminifi/src/utils/EncryptionUtils.cpp
new file mode 100644
index 0000000..f102606
--- /dev/null
+++ b/libminifi/src/utils/EncryptionUtils.cpp
@@ -0,0 +1,127 @@
+/**
+ * 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.
+ */
+
+#include "utils/EncryptionUtils.h"
+
+#include <sodium.h>
+
+#include <stdexcept>
+#include <string>
+
+#include "utils/StringUtils.h"
+
+namespace org {
+namespace apache {
+namespace nifi {
+namespace minifi {
+namespace utils {
+namespace crypto {
+
+Bytes stringToBytes(const std::string& text) {
+  return Bytes(text.begin(), text.end());
+}
+
+std::string bytesToString(const Bytes& bytes) {
+  return std::string(reinterpret_cast<const char*>(bytes.data()), bytes.size());
+}
+
+Bytes generateKey() {
+  Bytes key(EncryptionType::keyLength());
+  crypto_secretbox_keygen(key.data());
+  return key;
+}
+
+Bytes randomBytes(size_t num_bytes) {
+  Bytes random_bytes(num_bytes);
+  randombytes_buf(random_bytes.data(), num_bytes);
+  return random_bytes;
+}
+
+std::string EncryptionType::name() { return crypto_secretbox_primitive(); }
+
+size_t EncryptionType::keyLength() { return crypto_secretbox_keybytes(); }
+
+size_t EncryptionType::nonceLength() { return crypto_secretbox_noncebytes(); }
+
+size_t EncryptionType::macLength() { return crypto_secretbox_macbytes(); }
+
+std::string EncryptionType::separator() { return "||"; }
+
+Bytes encryptRaw(const Bytes& plaintext, const Bytes& key, const Bytes& nonce) {
+  if (key.size() != EncryptionType::keyLength()) {
+    throw std::invalid_argument{"Expected key of " + std::to_string(EncryptionType::keyLength()) +
+        " bytes, but got " + std::to_string(key.size()) + " bytes during encryption"};
+  }
+  if (nonce.size() != EncryptionType::nonceLength()) {
+    throw std::invalid_argument{"Expected nonce of " + std::to_string(EncryptionType::nonceLength()) +
+        " bytes, but got " + std::to_string(nonce.size()) + " bytes during encryption"};
+  }
+
+  Bytes ciphertext_plus_mac(plaintext.size() + EncryptionType::macLength());
+  crypto_secretbox_easy(ciphertext_plus_mac.data(), plaintext.data(), plaintext.size(), nonce.data(), key.data());
+  return ciphertext_plus_mac;
+}
+
+std::string encrypt(const std::string& plaintext, const Bytes& key) {
+  Bytes nonce = randomBytes(EncryptionType::nonceLength());
+  Bytes ciphertext_plus_mac = encryptRaw(stringToBytes(plaintext), key, nonce);
+
+  std::string nonce_base64 = utils::StringUtils::to_base64(nonce);
+  std::string ciphertext_plus_mac_base64 = utils::StringUtils::to_base64(ciphertext_plus_mac);
+  return nonce_base64 + EncryptionType::separator() + ciphertext_plus_mac_base64;
+}
+
+Bytes decryptRaw(const Bytes& input, const Bytes& key, const Bytes& nonce) {
+  if (key.size() != EncryptionType::keyLength()) {
+    throw std::invalid_argument{"Expected key of " + std::to_string(EncryptionType::keyLength()) +
+        " bytes, but got " + std::to_string(key.size()) + " bytes during decryption"};
+  }
+  if (nonce.size() != EncryptionType::nonceLength()) {
+    throw std::invalid_argument{"Expected a nonce of " + std::to_string(EncryptionType::nonceLength()) +
+        " bytes, but got " + std::to_string(nonce.size()) + " bytes during decryption"};
+  }
+  if (input.size() < EncryptionType::macLength()) {
+    throw std::invalid_argument{"Input is too short: expected at least " + std::to_string(EncryptionType::macLength()) +
+        " bytes, but got " + std::to_string(input.size()) + " bytes during decryption"};
+  }
+
+  Bytes plaintext(input.size() - EncryptionType::macLength());
+  if (crypto_secretbox_open_easy(plaintext.data(), input.data(), input.size(), nonce.data(), key.data())) {
+    throw std::runtime_error{"Decryption failed; the input may be forged!"};
+  }
+  return plaintext;
+}
+
+std::string decrypt(const std::string& input, const Bytes& key) {
+  std::vector<std::string> nonce_and_rest = utils::StringUtils::split(input, EncryptionType::separator());
+  if (nonce_and_rest.size() != 2) {
+    throw std::invalid_argument{"Incorrect input; expected '<nonce>" + EncryptionType::separator() + "<ciphertext_plus_mac>'"};
+  }
+
+  Bytes nonce = utils::StringUtils::from_base64(nonce_and_rest[0].data(), nonce_and_rest[0].size());
+  Bytes ciphertext_plus_mac = utils::StringUtils::from_base64(nonce_and_rest[1].data(), nonce_and_rest[1].size());
+
+  Bytes plaintext = decryptRaw(ciphertext_plus_mac, key, nonce);
+  return bytesToString(plaintext);
+}
+
+}  // namespace crypto
+}  // namespace utils
+}  // namespace minifi
+}  // namespace nifi
+}  // namespace apache
+}  // namespace org
diff --git a/libminifi/src/utils/Id.cpp b/libminifi/src/utils/Id.cpp
index cf98da0..0264833 100644
--- a/libminifi/src/utils/Id.cpp
+++ b/libminifi/src/utils/Id.cpp
@@ -211,7 +211,7 @@ uint64_t IdGenerator::getRandomDeviceSegment(int numBits) const {
 void IdGenerator::initialize(const std::shared_ptr<Properties>& properties) {
   std::string implementation_str;
   implementation_ = UUID_TIME_IMPL;
-  if (properties->get("uid.implementation", implementation_str)) {
+  if (properties->getString("uid.implementation", implementation_str)) {
     std::transform(implementation_str.begin(), implementation_str.end(), implementation_str.begin(), ::tolower);
     if (UUID_RANDOM_STR == implementation_str || UUID_WINDOWS_RANDOM_STR == implementation_str) {
       logging::LOG_DEBUG(logger_) << "Using uuid_generate_random for uids.";
@@ -228,7 +228,7 @@ void IdGenerator::initialize(const std::shared_ptr<Properties>& properties) {
       std::string device_segment;
       uint64_t prefix = timestamp;
       if (device_bits > 0) {
-        if (properties->get("uid.minifi.device.segment", device_segment)) {
+        if (properties->getString("uid.minifi.device.segment", device_segment)) {
           prefix = getDeviceSegmentFromString(device_segment, device_bits);
         } else {
           logging::LOG_WARN(logger_) << "uid.minifi.device.segment not specified, generating random device segment";
diff --git a/libminifi/test/resources/conf/bootstrap.conf b/libminifi/test/resources/conf/bootstrap.conf
new file mode 100644
index 0000000..965d4c2
--- /dev/null
+++ b/libminifi/test/resources/conf/bootstrap.conf
@@ -0,0 +1 @@
+nifi.bootstrap.sensitive.key=5506c28d0fe265299e294a4c766b723a48986764953e93d38b3c627176fd10ed
diff --git a/libminifi/test/resources/encrypted.minifi.properties b/libminifi/test/resources/encrypted.minifi.properties
new file mode 100644
index 0000000..6ddb95f
--- /dev/null
+++ b/libminifi/test/resources/encrypted.minifi.properties
@@ -0,0 +1,105 @@
+# 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.
+
+# Core Properties #
+nifi.version=0.7.0
+nifi.flow.configuration.file=./conf/config.yml
+nifi.administrative.yield.duration=30 sec
+# If a component has no work to do (is "bored"), how long should we wait before checking again for work?
+nifi.bored.yield.duration=10 millis
+
+# Provenance Repository #
+nifi.provenance.repository.directory.default=${MINIFI_HOME}/provenance_repository
+nifi.provenance.repository.max.storage.time=1 MIN
+nifi.provenance.repository.max.storage.size=1 MB
+nifi.flowfile.repository.directory.default=${MINIFI_HOME}/flowfile_repository
+nifi.database.content.repository.directory.default=${MINIFI_HOME}/content_repository
+
+#nifi.remote.input.secure=true
+#nifi.security.need.ClientAuth=
+#nifi.security.client.certificate=
+#nifi.security.client.private.key=
+nifi.security.client.pass.phrase=HvbPejGT3ur9/00gXQK/dJCYwaNqhopf||CiXKiNaljSN7VkLXP5zfJnb4+4UcKIG3ddwuVfSPpkRRfT4=
+nifi.security.client.pass.phrase.protected=xsalsa20poly1305
+#nifi.security.client.ca.certificate=
+
+nifi.rest.api.user.name=admin
+nifi.rest.api.password=5gIgzDLsk8gHusvcXO08kx92iSMtQ8wM||pcKy/wDY6JDR9nJ8DRfvrDNWyK9C+S01vFM=
+nifi.rest.api.password.protected=xsalsa20poly1305
+
+# State storage configuration #
+## The default state storage can be overridden by specifying a controller service instance
+## that implements CoreComponentStateManagementProvider
+## (e.g. an instance of RocksDbPersistableKeyValueStoreService or UnorderedMapPersistableKeyValueStoreService)
+#nifi.state.management.provider.local=
+## To make the default state storage persist every state change, set this to true
+## this comes at a performance penalty, but makes sure no state is lost even on unclean shutdowns
+#nifi.state.management.provider.local.always.persist=true
+## To change the frequency at which the default state storage is persisted, modify the following
+#nifi.state.management.provider.local.auto.persistence.interval=1 min
+
+## Enabling C2 Uncomment each of the following options
+## define those with missing options
+nifi.c2.enable=true
+## define protocol parameters
+## The default is CoAP, if that extension is built.
+## Alternatively, you may use RESTSender if http-curl is built
+#nifi.c2.agent.protocol.class=CoapProtocol
+nifi.c2.agent.protocol.class=RESTSender
+#nifi.c2.agent.coap.host=
+#nifi.c2.agent.coap.port=
+## base URL of the c2 server,
+## very likely the same base url of rest urls
+nifi.c2.flow.base.url=http://localhost:10080/efm/api
+nifi.c2.rest.url=http://localhost:10080/efm/api/c2-protocol/heartbeat
+nifi.c2.rest.url.ack=http://localhost:10080/efm/api/c2-protocol/acknowledge
+nifi.c2.root.classes=DeviceInfoNode,AgentInformation,FlowInformation
+## Minimize heartbeat payload size by excluding agent manifest from the heartbeat
+#nifi.c2.full.heartbeat=false
+## heartbeat 4 times a second
+#nifi.c2.agent.heartbeat.period=250
+## define parameters about your agent
+nifi.c2.agent.class=TailFileTester
+c2.agent.identifier=lZL2phnmPWP4s7k8LzzONTNh/2Nhgyty||OLyo7FtKOZ5M1DbiVCEMrlch8D643MKCtw3T7iouvLHeSA==
+c2.agent.identifier.protected=xsalsa20poly1305
+
+## define metrics reported
+nifi.c2.root.class.definitions=metrics
+nifi.c2.root.class.definitions.metrics.name=metrics
+nifi.c2.root.class.definitions.metrics.metrics=typedmetrics
+nifi.c2.root.class.definitions.metrics.metrics.typedmetrics.name=RuntimeMetrics
+nifi.c2.root.class.definitions.metrics.metrics.queuemetrics.name=QueueMetrics
+nifi.c2.root.class.definitions.metrics.metrics.queuemetrics.classes=QueueMetrics
+nifi.c2.root.class.definitions.metrics.metrics.typedmetrics.classes=ProcessMetrics,SystemInformation
+nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.name=ProcessorMetric
+nifi.c2.root.class.definitions.metrics.metrics.processorMetrics.classes=GetFileMetrics
+
+## enable the controller socket provider on port 9998
+## off by default. C2 must be enabled to support these
+#controller.socket.host=localhost
+#controller.socket.port=9998
+
+#JNI properties
+nifi.framework.dir=${MINIFI_HOME}/minifi-jni/lib
+nifi.nar.directory=${MINIFI_HOME}/minifi-jni/nars
+nifi.nar.deploy.directory=${MINIFI_HOME}/minifi-jni/nardeploy
+nifi.nar.docs.directory=${MINIFI_HOME}/minifi-jni/nardocs
+# must be comma separated
+nifi.jvm.options=-Xmx1G
+nifi.python.processor.dir=${MINIFI_HOME}/minifi-python/
+nifi.c2.flow.id=
+nifi.c2.flow.url=
+
+nifi.sensitive.props.additional.keys=c2.agent.identifier
diff --git a/libminifi/test/unit/DecryptorTests.cpp b/libminifi/test/unit/DecryptorTests.cpp
new file mode 100644
index 0000000..3582be7
--- /dev/null
+++ b/libminifi/test/unit/DecryptorTests.cpp
@@ -0,0 +1,124 @@
+/**
+ * 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.
+ */
+
+#include "properties/Decryptor.h"
+#include "TestUtils.h"
+
+namespace minifi = org::apache::nifi::minifi;
+namespace utils = org::apache::nifi::minifi::utils;
+
+TEST_CASE("Decryptor can decide whether a property is encrypted", "[isValidEncryptionMarker]") {
+  utils::crypto::Bytes encryption_key;
+  minifi::Decryptor decryptor{encryption_key};
+
+  REQUIRE(minifi::Decryptor::isValidEncryptionMarker(utils::nullopt) == false);
+  REQUIRE(minifi::Decryptor::isValidEncryptionMarker(utils::optional<std::string>{""}) == false);
+  REQUIRE(minifi::Decryptor::isValidEncryptionMarker(utils::optional<std::string>{"plaintext"}) == false);
+  REQUIRE(minifi::Decryptor::isValidEncryptionMarker(utils::optional<std::string>{"AES256-GCM"}) == false);
+  REQUIRE(
+      minifi::Decryptor::isValidEncryptionMarker(utils::optional<std::string>{utils::crypto::EncryptionType::name()}) == true);
+}
+
+TEST_CASE("Decryptor can decrypt a property", "[decrypt]") {
+  utils::crypto::Bytes encryption_key = utils::crypto::stringToBytes(utils::StringUtils::from_hex(
+      "4024b327fdc987ce3eb43dd1f690b9987e4072e0020e3edf4349ce1ad91a4e38"));
+  minifi::Decryptor decryptor{encryption_key};
+
+  std::string encrypted_value = "l3WY1V27knTiPa6jVX0jrq4qjmKsySOu||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo=";
+  REQUIRE(decryptor.decrypt(encrypted_value) == "CorrectHorseBatteryStaple");
+}
+
+TEST_CASE("Decryptor will throw if the value is incorrect", "[decrypt]") {
+  utils::crypto::Bytes encryption_key = utils::crypto::stringToBytes(utils::StringUtils::from_hex(
+      "4024b327fdc987ce3eb43dd1f690b9987e4072e0020e3edf4349ce1ad91a4e38"));
+  minifi::Decryptor decryptor{encryption_key};
+
+  // correct nonce + ciphertext and mac: "l3WY1V27knTiPa6jVX0jrq4qjmKsySOu||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo="
+
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // this is not even close
+      "some totally incorrect value"),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // separator missing
+      "l3WY1V27knTiPa6jVX0jrq4qjmKsySOuErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // separator wrong
+      "l3WY1V27knTiPa6jVX0jrq4qjmKsySOu__ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // more than one separator
+      "l3WY1V27knTiPa6jVX0jrq4qjmKsySOu||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo=||extra+stuff"),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // nonce is off by one char
+      "L3WY1V27knTiPa6jVX0jrq4qjmKsySOu||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // ciphertext is off by one char
+      "l3WY1V27knTiPa6jVX0jrq4qjmKsySOu||erntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // nonce is too short
+      "l3WY1V27knTiPa6jVX0rq4qjmKsySOu||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // nonce is too long
+      "l3WY1V27knTiPa6jVX0jrq4qjmKsySOup||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytKk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // ciphertext-and-mac is too short
+      "l3WY1V27knTiPa6jVX0jrq4qjmKsySOu||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUU5EyMloTtSytk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // ciphertext-and-mac is too long
+      "l3WY1V27knTiPa6jVX0jrq4qjmKsySOu||ErntqZpHP1M+6OkA14p5sdnqJhuNHWHDVUUU5EyMloTtSytKk9a5xNKo="),
+      std::exception);
+  REQUIRE_THROWS_AS(decryptor.decrypt(  // correct format but random value
+      "81hf/4bHIRVd2pYglniBW3zOUcaLe+Cw||mkN2sKHS+nepRTcBhOJ5tFW4GXvaywYLD8xzIEbCP0lgUA6Qf3jZ/oMi"),
+      std::exception);
+}
+
+TEST_CASE("Decryptor can decrypt a configuration file", "[decryptSensitiveProperties]") {
+  utils::crypto::Bytes encryption_key = utils::crypto::stringToBytes(utils::StringUtils::from_hex(
+      "5506c28d0fe265299e294a4c766b723a48986764953e93d38b3c627176fd10ed"));
+  minifi::Decryptor decryptor{encryption_key};
+
+  minifi::Configure configuration{decryptor};
+  configuration.setHome("resources");
+  configuration.loadConfigureFile("encrypted.minifi.properties");
+  REQUIRE(configuration.getConfiguredKeys().size() > 0);
+
+  utils::optional<std::string> passphrase = configuration.get(minifi::Configure::nifi_security_client_pass_phrase);
+  REQUIRE(passphrase);
+  REQUIRE(*passphrase == "SpeakFriendAndEnter");
+
+  utils::optional<std::string> password = configuration.get(minifi::Configure::nifi_rest_api_password);
+  REQUIRE(password);
+  REQUIRE(*password == "OpenSesame");
+
+  std::string agent_identifier;
+  REQUIRE(configuration.get("nifi.c2.agent.identifier", "c2.agent.identifier", agent_identifier));
+  REQUIRE(agent_identifier == "TailFileTester-001");
+
+  utils::optional<std::string> unencrypted_property = configuration.get(minifi::Configure::nifi_bored_yield_duration);
+  REQUIRE(unencrypted_property);
+  REQUIRE(*unencrypted_property == "10 millis");
+
+  utils::optional<std::string> nonexistent_property = configuration.get("this.property.does.not.exist");
+  REQUIRE_FALSE(nonexistent_property);
+}
+
+TEST_CASE("Decryptor can be created from a bootstrap file", "[create]") {
+  utils::optional<minifi::Decryptor> valid_decryptor = minifi::Decryptor::create("resources");
+  REQUIRE(valid_decryptor);
+  REQUIRE(valid_decryptor->decrypt("HvbPejGT3ur9/00gXQK/dJCYwaNqhopf||CiXKiNaljSN7VkLXP5zfJnb4+4UcKIG3ddwuVfSPpkRRfT4=") == "SpeakFriendAndEnter");
+
+  utils::optional<minifi::Decryptor> invalid_decryptor = minifi::Decryptor::create("there.is.no.such.directory");
+  REQUIRE_FALSE(invalid_decryptor);
+}
diff --git a/libminifi/test/unit/EncryptionUtilsTests.cpp b/libminifi/test/unit/EncryptionUtilsTests.cpp
new file mode 100644
index 0000000..977be35
--- /dev/null
+++ b/libminifi/test/unit/EncryptionUtilsTests.cpp
@@ -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.
+ */
+
+#include "utils/EncryptionUtils.h"
+#include "utils/StringUtils.h"
+#include "../TestBase.h"
+
+namespace utils = org::apache::nifi::minifi::utils;
+
+namespace {
+const utils::crypto::Bytes SECRET_KEY = utils::crypto::stringToBytes(utils::StringUtils::from_hex(
+    "aa411f289c91685ef9d5a9e5a4fad9393ff4c7a78ab978484323488caed7a9ab"));
+
+const utils::crypto::Bytes NONCE = utils::crypto::stringToBytes(utils::StringUtils::from_base64(
+    "RBrWo9lv7xNA6JJWHCa9avnT42CCr1bn"));
+}  // namespace
+
+TEST_CASE("EncryptionUtils can do a simple encryption", "[encryptRaw]") {
+  utils::crypto::Bytes plaintext = utils::crypto::stringToBytes("the attack begins at two");
+
+  utils::crypto::Bytes output = utils::crypto::encryptRaw(plaintext, SECRET_KEY, NONCE);
+
+  REQUIRE(output.size() == plaintext.size() + utils::crypto::EncryptionType::macLength());
+  REQUIRE(utils::StringUtils::to_base64(output) == "x3WIHJGb+7hGlfIQd3gz8zw11EP0uFh9Ml1XBEAPCX5OTKqWcY+o+Q==");
+}
+
+TEST_CASE("EncryptionUtils can do a simple decryption", "[decryptRaw]") {
+  utils::crypto::Bytes ciphertext_plus_mac = utils::crypto::stringToBytes(utils::StringUtils::from_base64(
+      "x3WIHJGb+7hGlfIQd3gz8zw11EP0uFh9Ml1XBEAPCX5OTKqWcY+o+Q=="));
+
+  utils::crypto::Bytes output = utils::crypto::decryptRaw(ciphertext_plus_mac, SECRET_KEY, NONCE);
+
+  REQUIRE(utils::crypto::bytesToString(output) == "the attack begins at two");
+}
+
+TEST_CASE("EncryptionUtils can generate random bytes", "[randomBytes][generateKey]") {
+  std::function<utils::crypto::Bytes()> randomFunction;
+
+  SECTION("generateKey() can generate random bytes") {
+    randomFunction = utils::crypto::generateKey;
+  }
+  SECTION("randomBytes() can generate random bytes") {
+    randomFunction = [](){ return utils::crypto::randomBytes(32); };
+  }
+
+  utils::crypto::Bytes random_bytes = randomFunction();
+  REQUIRE(random_bytes.size() == 32);
+
+  // the following assertions will fail about once in every hundred and fifteen quattuorvigintillion runs,
+  // which is much less likely than a test failure caused by a meteor strike destroying your computer
+  auto is_zero = [](unsigned char byte) { return byte == 0; };
+  REQUIRE_FALSE(std::all_of(random_bytes.begin(), random_bytes.end(), is_zero));
+
+  utils::crypto::Bytes different_random_bytes = randomFunction();
+  REQUIRE(random_bytes != different_random_bytes);
+}
+
+TEST_CASE("EncryptionUtils can encrypt and decrypt strings using the simplified interface", "[encrypt][decrypt]") {
+  utils::crypto::Bytes key = utils::crypto::generateKey();
+  std::string plaintext = "my social security number is 914-52-5373";
+
+  const auto base64_length = [](int raw_length) { return (raw_length + 2) / 3 * 4; };
+
+  std::string encrypted_text = utils::crypto::encrypt(plaintext, key);
+  REQUIRE(encrypted_text.size() ==
+      base64_length(utils::crypto::EncryptionType::nonceLength()) +
+      utils::crypto::EncryptionType::separator().size() +
+      base64_length(plaintext.size() + utils::crypto::EncryptionType::macLength()));
+
+  std::string decrypted_text = utils::crypto::decrypt(encrypted_text, key);
+  REQUIRE(decrypted_text == plaintext);
+}
diff --git a/main/MainHelper.h b/main/MainHelper.h
index b9d3560..872714b 100644
--- a/main/MainHelper.h
+++ b/main/MainHelper.h
@@ -40,15 +40,14 @@ extern "C" {
 //! Main thread stop wait time
 #define STOP_WAIT_TIME_MS 30*1000
 //! Default YAML location
+
 #ifdef WIN32
 #define DEFAULT_NIFI_CONFIG_YML "\\conf\\config.yml"
-//! Default properties file paths
 #define DEFAULT_NIFI_PROPERTIES_FILE "\\conf\\minifi.properties"
 #define DEFAULT_LOG_PROPERTIES_FILE "\\conf\\minifi-log.properties"
 #define DEFAULT_UID_PROPERTIES_FILE "\\conf\\minifi-uid.properties"
 #else
 #define DEFAULT_NIFI_CONFIG_YML "./conf/config.yml"
-//! Default properties file paths
 #define DEFAULT_NIFI_PROPERTIES_FILE "./conf/minifi.properties"
 #define DEFAULT_LOG_PROPERTIES_FILE "./conf/minifi-log.properties"
 #define DEFAULT_UID_PROPERTIES_FILE "./conf/minifi-uid.properties"
diff --git a/main/MiNiFiMain.cpp b/main/MiNiFiMain.cpp
index d90a37f..e615ab3 100644
--- a/main/MiNiFiMain.cpp
+++ b/main/MiNiFiMain.cpp
@@ -39,21 +39,21 @@
 
 #include <fcntl.h>
 #include <stdio.h>
-#include <cstdlib>
 #include <semaphore.h>
 #include <signal.h>
+#include <sodium.h>
+
+#include <cstdlib>
 #include <vector>
-#include <queue>
-#include <map>
 #include <iostream>
-#include <utils/file/FileUtils.h>
+
 #include "ResourceClaim.h"
 #include "core/Core.h"
-
 #include "core/FlowConfiguration.h"
 #include "core/ConfigurationFactory.h"
 #include "core/RepositoryFactory.h"
 #include "DiskSpaceWatchdog.h"
+#include "properties/Decryptor.h"
 #include "utils/file/PathUtils.h"
 #include "utils/file/FileUtils.h"
 #include "utils/Environment.h"
@@ -138,6 +138,11 @@ int main(int argc, char **argv) {
   }
 #endif
 
+  if (sodium_init() < 0) {
+    logger->log_error("Could not initialize the libsodium library!");
+    return -1;
+  }
+
   uint16_t stop_wait_time = STOP_WAIT_TIME_MS;
 
   // initialize static functions that were defined apriori
@@ -205,7 +210,14 @@ int main(int argc, char **argv) {
   // Make a record of minifi home in the configured log file.
   logger->log_info("MINIFI_HOME=%s", minifiHome);
 
-  const std::shared_ptr<minifi::Configure> configure = std::make_shared<minifi::Configure>();
+  utils::optional<minifi::Decryptor> decryptor = minifi::Decryptor::create(minifiHome);
+  if (decryptor) {
+    logger->log_info("Found encryption key, will decrypt sensitive properties in the configuration");
+  } else {
+    logger->log_info("No encryption key found, will not decrypt sensitive properties in the configuration");
+  }
+
+  const std::shared_ptr<minifi::Configure> configure = std::make_shared<minifi::Configure>(decryptor);
   configure->setHome(minifiHome);
   configure->loadConfigureFile(DEFAULT_NIFI_PROPERTIES_FILE);
 
diff --git a/msi/WixWin.wsi b/msi/WixWin.wsi
index 1738194..336e020 100644
--- a/msi/WixWin.wsi
+++ b/msi/WixWin.wsi
@@ -304,8 +304,8 @@ Licensed to the Apache Software Foundation (ASF) under one or more
                 <IniFile Id="ConfigFileE" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.enable" Value="true" />
                 <IniFile Id="ConfigFileP" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.agent.protocol.class" Value="[AGENT_PROTOCOL]" />
                 <IniFile Id="ConfigFileT" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.agent.heartbeat.period" Value="[AGENT_HEARTBEAT]" />
-                <IniFile Id="ConfigFileH" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="c2.rest.url" Value="[SERVER_HEARTBEAT]" />
-                <IniFile Id="ConfigFileAck" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="c2.rest.url.ack" Value="[SERVER_ACK]" />
+                <IniFile Id="ConfigFileH" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.rest.url" Value="[SERVER_HEARTBEAT]" />
+                <IniFile Id="ConfigFileAck" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.rest.url.ack" Value="[SERVER_ACK]" />
                 <Condition><![CDATA[ENABLEC2="1"]]></Condition>
               </Component>
               <Component Id="UpdateConfigNotExist" Guid="87658309-0339-425c-8633-f54ffaaa4946">
diff --git a/msi/WixWinMergeModules.wsi b/msi/WixWinMergeModules.wsi
index 9af35c8..b293acc 100644
--- a/msi/WixWinMergeModules.wsi
+++ b/msi/WixWinMergeModules.wsi
@@ -308,8 +308,8 @@ Licensed to the Apache Software Foundation (ASF) under one or more
                 <IniFile Id="ConfigFileE" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.enable" Value="true" />
                 <IniFile Id="ConfigFileP" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.agent.protocol.class" Value="[AGENT_PROTOCOL]" />
                 <IniFile Id="ConfigFileT" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.agent.heartbeat.period" Value="[AGENT_HEARTBEAT]" />
-                <IniFile Id="ConfigFileH" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="c2.rest.url" Value="[SERVER_HEARTBEAT]" />
-                <IniFile Id="ConfigFileAck" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="c2.rest.url.ack" Value="[SERVER_ACK]" />
+                <IniFile Id="ConfigFileH" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.rest.url" Value="[SERVER_HEARTBEAT]" />
+                <IniFile Id="ConfigFileAck" Action="addLine" Name="minifi.properties" Directory="CONFIGDIR" Section="c2props" Key="nifi.c2.rest.url.ack" Value="[SERVER_ACK]" />
                 <Condition><![CDATA[ENABLEC2="1"]]></Condition>
               </Component>
               <Component Id="UpdateConfigNotExist" Guid="87658309-0339-425c-8633-f54ffaaa4946">
diff --git a/nanofi/ecu/log_aggregator.c b/nanofi/ecu/log_aggregator.c
index 52d4f23..eb447cf 100644
--- a/nanofi/ecu/log_aggregator.c
+++ b/nanofi/ecu/log_aggregator.c
@@ -76,7 +76,7 @@ int main(int argc, char** argv) {
     clear_content_repo(params.instance);
     delete_all_flow_files_from_proc(uuid_str);
     free_standalone_processor(params.processor);
-    free_instance(params.instance);
+    free_nanofi_instance(params.instance);
     free_proc_params(uuid_str);
     return 0;
 }
diff --git a/nanofi/ecu/tailfile_chunk.c b/nanofi/ecu/tailfile_chunk.c
index 26b9966..0a34eab 100644
--- a/nanofi/ecu/tailfile_chunk.c
+++ b/nanofi/ecu/tailfile_chunk.c
@@ -71,7 +71,7 @@ int main(int argc, char** argv) {
     clear_content_repo(params.instance);
     delete_all_flow_files_from_proc(uuid_str);
     free_standalone_processor(params.processor);
-    free_instance(params.instance);
+    free_nanofi_instance(params.instance);
     free_proc_params(uuid_str);
     return 0;
 }
diff --git a/nanofi/ecu/tailfile_delimited.c b/nanofi/ecu/tailfile_delimited.c
index 3495231..5351fae 100644
--- a/nanofi/ecu/tailfile_delimited.c
+++ b/nanofi/ecu/tailfile_delimited.c
@@ -71,7 +71,7 @@ int main(int argc, char** argv) {
     clear_content_repo(params.instance);
     delete_all_flow_files_from_proc(uuid_str);
     free_standalone_processor(params.processor);
-    free_instance(params.instance);
+    free_nanofi_instance(params.instance);
     free_proc_params(uuid_str);
     return 0;
 }
diff --git a/nanofi/examples/generate_flow.c b/nanofi/examples/generate_flow.c
index a63f882..5323b6b 100644
--- a/nanofi/examples/generate_flow.c
+++ b/nanofi/examples/generate_flow.c
@@ -62,5 +62,5 @@ int main(int argc, char **argv) {
 
   free_flow(new_flow);
 
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
diff --git a/nanofi/examples/monitor_directory.c b/nanofi/examples/monitor_directory.c
index 2283b35..6491b11 100644
--- a/nanofi/examples/monitor_directory.c
+++ b/nanofi/examples/monitor_directory.c
@@ -91,5 +91,5 @@ int main(int argc, char **argv) {
 
   free_flow(new_flow);
 
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
diff --git a/nanofi/examples/terminate_handler.c b/nanofi/examples/terminate_handler.c
index 9bd3b58..45405d1 100644
--- a/nanofi/examples/terminate_handler.c
+++ b/nanofi/examples/terminate_handler.c
@@ -57,5 +57,5 @@ int main(int argc, char **argv) {
   fprintf(stderr, "Dragons!!!");
   free_flowfile(record);
   free_flow(new_flow);
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
diff --git a/nanofi/examples/transmit_flow.c b/nanofi/examples/transmit_flow.c
index f98c8c2..7739e54 100644
--- a/nanofi/examples/transmit_flow.c
+++ b/nanofi/examples/transmit_flow.c
@@ -100,7 +100,7 @@ int main(int argc, char **argv) {
 
   free_flowfile(record);
 
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
 
 
diff --git a/nanofi/include/api/nanofi.h b/nanofi/include/api/nanofi.h
index bec0f83..ba971d8 100644
--- a/nanofi/include/api/nanofi.h
+++ b/nanofi/include/api/nanofi.h
@@ -92,7 +92,7 @@ void initialize_instance(nifi_instance * instance);
  * It's recommended to free all flows before freeing the instance.
  * @param instance instance to be freed
  **/
-void free_instance(nifi_instance * instance);
+void free_nanofi_instance(nifi_instance * instance);
 
 /****
  * ##################################################################
diff --git a/nanofi/src/api/nanofi.cpp b/nanofi/src/api/nanofi.cpp
index 046b2ff..f021013 100644
--- a/nanofi/src/api/nanofi.cpp
+++ b/nanofi/src/api/nanofi.cpp
@@ -184,7 +184,7 @@ void free_standalone_processor(standalone_processor* proc) {
 
   if (ExecutionPlan::getProcWithPlanQty() == 0) {
     // The instance is not needed any more as there are no standalone processors in the system
-    free_instance(standalone_instance);
+    free_nanofi_instance(standalone_instance);
     standalone_instance = nullptr;
   }
 }
@@ -227,7 +227,7 @@ int set_instance_property(nifi_instance *instance, const char *key, const char *
  * Reclaims memory associated with a nifi instance structure.
  * @param instance nifi instance.
  */
-void free_instance(nifi_instance* instance) {
+void free_nanofi_instance(nifi_instance* instance) {
   NULL_CHECK(, instance);
   delete ((minifi::Instance*) instance->instance_ptr);
   free(instance->port.port_id);
diff --git a/nanofi/tests/CAPITests.cpp b/nanofi/tests/CAPITests.cpp
index 0610c3a..ad8f594 100644
--- a/nanofi/tests/CAPITests.cpp
+++ b/nanofi/tests/CAPITests.cpp
@@ -98,7 +98,7 @@ TEST_CASE("Test Creation of instance, one processor", "[createInstanceAndFlow]")
   processor *test_proc = add_processor(test_flow, "GenerateFlowFile");
   REQUIRE(test_proc != nullptr);
   free_flow(test_flow);
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
 
 TEST_CASE("Invalid processor returns null", "[addInvalidProcessor]") {
@@ -110,7 +110,7 @@ TEST_CASE("Invalid processor returns null", "[addInvalidProcessor]") {
   processor *no_proc = add_processor(test_flow, "");
   REQUIRE(no_proc == nullptr);
   free_flow(test_flow);
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
 
 TEST_CASE("Set valid and invalid properties", "[setProcesssorProperties]") {
@@ -128,7 +128,7 @@ TEST_CASE("Set valid and invalid properties", "[setProcesssorProperties]") {
   REQUIRE(set_property(test_proc, nullptr, "Blah") != 0);  // Empty attribute
   REQUIRE(set_property(nullptr, "Invalid Attribute", "Blah") != 0);  // Invalid processor
   free_flow(test_flow);
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
 
 TEST_CASE("get file and put file", "[getAndPutFile]") {
@@ -177,7 +177,7 @@ TEST_CASE("get file and put file", "[getAndPutFile]") {
 
   free_flow(test_flow);
 
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
 
 TEST_CASE("Test manipulation of attributes", "[testAttributes]") {
@@ -253,7 +253,7 @@ TEST_CASE("Test manipulation of attributes", "[testAttributes]") {
   free_flowfile(record);
 
   free_flow(test_flow);
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
 
 TEST_CASE("Test error handling callback", "[errorHandling]") {
@@ -298,7 +298,7 @@ TEST_CASE("Test error handling callback", "[errorHandling]") {
   failure_count = 0;
 
   free_flow(test_flow);
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
 
 TEST_CASE("Test standalone processors", "[testStandalone]") {
@@ -388,7 +388,7 @@ TEST_CASE("Test interaction of flow and standlone processors", "[testStandaloneW
   REQUIRE(test_file_content == put_data);
 
   free_flow(test_flow);
-  free_instance(instance);
+  free_nanofi_instance(instance);
   free_standalone_processor(putfile_proc);
 }
 
@@ -454,7 +454,7 @@ TEST_CASE("Test custom processor", "[TestCutomProcessor]") {
 TEST_CASE("C API robustness test", "[TestRobustness]") {
   free_flow(nullptr);
   free_standalone_processor(nullptr);
-  free_instance(nullptr);
+  free_nanofi_instance(nullptr);
 
   REQUIRE(create_processor(nullptr, nullptr) == nullptr);
 
@@ -536,5 +536,5 @@ TEST_CASE("C API robustness test", "[TestRobustness]") {
 
   free_flow(test_flow);
 
-  free_instance(instance);
+  free_nanofi_instance(instance);
 }
diff --git a/nanofi/tests/CTestsBase.h b/nanofi/tests/CTestsBase.h
index 3608ac1..ac9cddc 100644
--- a/nanofi/tests/CTestsBase.h
+++ b/nanofi/tests/CTestsBase.h
@@ -109,7 +109,7 @@ public:
             free(pp);
         }
         free_standalone_processor(processor_);
-        free_instance(instance_);
+        free_nanofi_instance(instance_);
     }
 
     standalone_processor * getProcessor() const {
diff --git a/thirdparty/libsodium/libsodium.patch b/thirdparty/libsodium/libsodium.patch
new file mode 100644
index 0000000..5ac1b7c
--- /dev/null
+++ b/thirdparty/libsodium/libsodium.patch
@@ -0,0 +1,154 @@
+We can't run the configure script on Windows, and the only thing libsodium provides
+for Windows are msbuild files which do not integrate well into our CMake build system,
+so we create a Windows-specific CMakeLists.txt for the project.
+
+diff -rupN original/CMakeLists.txt patched/CMakeLists.txt
+--- original/CMakeLists.txt	1970-01-01 01:00:00.000000000 +0100
++++ patched/CMakeLists.txt	2020-09-24 13:32:12.368714100 +0200
+@@ -0,0 +1,146 @@
++cmake_minimum_required(VERSION 3.7)
++
++project(libsodium)
++
++set(SOURCES src/libsodium/crypto_aead/aes256gcm/aesni/aead_aes256gcm_aesni.c
++            src/libsodium/crypto_aead/chacha20poly1305/sodium/aead_chacha20poly1305.c
++            src/libsodium/crypto_aead/xchacha20poly1305/sodium/aead_xchacha20poly1305.c
++            src/libsodium/crypto_auth/crypto_auth.c
++            src/libsodium/crypto_auth/hmacsha256/auth_hmacsha256.c
++            src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c
++            src/libsodium/crypto_auth/hmacsha512256/auth_hmacsha512256.c
++            src/libsodium/crypto_box/crypto_box.c
++            src/libsodium/crypto_box/crypto_box_easy.c
++            src/libsodium/crypto_box/crypto_box_seal.c
++            src/libsodium/crypto_box/curve25519xchacha20poly1305/box_curve25519xchacha20poly1305.c
++            src/libsodium/crypto_box/curve25519xchacha20poly1305/box_seal_curve25519xchacha20poly1305.c
++            src/libsodium/crypto_box/curve25519xsalsa20poly1305/box_curve25519xsalsa20poly1305.c
++            src/libsodium/crypto_core/ed25519/core_ed25519.c
++            src/libsodium/crypto_core/ed25519/core_ristretto255.c
++            src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c
++            src/libsodium/crypto_core/hchacha20/core_hchacha20.c
++            src/libsodium/crypto_core/hsalsa20/core_hsalsa20.c
++            src/libsodium/crypto_core/hsalsa20/ref2/core_hsalsa20_ref2.c
++            src/libsodium/crypto_core/salsa/ref/core_salsa_ref.c
++            src/libsodium/crypto_generichash/blake2b/generichash_blake2.c
++            src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-avx2.c
++            src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ref.c
++            src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-sse41.c
++            src/libsodium/crypto_generichash/blake2b/ref/blake2b-compress-ssse3.c
++            src/libsodium/crypto_generichash/blake2b/ref/blake2b-ref.c
++            src/libsodium/crypto_generichash/blake2b/ref/generichash_blake2b.c
++            src/libsodium/crypto_generichash/crypto_generichash.c
++            src/libsodium/crypto_hash/crypto_hash.c
++            src/libsodium/crypto_hash/sha256/cp/hash_sha256_cp.c
++            src/libsodium/crypto_hash/sha256/hash_sha256.c
++            src/libsodium/crypto_hash/sha512/cp/hash_sha512_cp.c
++            src/libsodium/crypto_hash/sha512/hash_sha512.c
++            src/libsodium/crypto_kdf/blake2b/kdf_blake2b.c
++            src/libsodium/crypto_kdf/crypto_kdf.c
++            src/libsodium/crypto_kx/crypto_kx.c
++            src/libsodium/crypto_onetimeauth/crypto_onetimeauth.c
++            src/libsodium/crypto_onetimeauth/poly1305/donna/poly1305_donna.c
++            src/libsodium/crypto_onetimeauth/poly1305/onetimeauth_poly1305.c
++            src/libsodium/crypto_onetimeauth/poly1305/sse2/poly1305_sse2.c
++            src/libsodium/crypto_pwhash/argon2/argon2-core.c
++            src/libsodium/crypto_pwhash/argon2/argon2-encoding.c
++            src/libsodium/crypto_pwhash/argon2/argon2-fill-block-avx2.c
++            src/libsodium/crypto_pwhash/argon2/argon2-fill-block-avx512f.c
++            src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ref.c
++            src/libsodium/crypto_pwhash/argon2/argon2-fill-block-ssse3.c
++            src/libsodium/crypto_pwhash/argon2/argon2.c
++            src/libsodium/crypto_pwhash/argon2/blake2b-long.c
++            src/libsodium/crypto_pwhash/argon2/pwhash_argon2i.c
++            src/libsodium/crypto_pwhash/argon2/pwhash_argon2id.c
++            src/libsodium/crypto_pwhash/crypto_pwhash.c
++            src/libsodium/crypto_pwhash/scryptsalsa208sha256/crypto_scrypt-common.c
++            src/libsodium/crypto_pwhash/scryptsalsa208sha256/nosse/pwhash_scryptsalsa208sha256_nosse.c
++            src/libsodium/crypto_pwhash/scryptsalsa208sha256/pbkdf2-sha256.c
++            src/libsodium/crypto_pwhash/scryptsalsa208sha256/pwhash_scryptsalsa208sha256.c
++            src/libsodium/crypto_pwhash/scryptsalsa208sha256/scrypt_platform.c
++            src/libsodium/crypto_pwhash/scryptsalsa208sha256/sse/pwhash_scryptsalsa208sha256_sse.c
++            src/libsodium/crypto_scalarmult/crypto_scalarmult.c
++            src/libsodium/crypto_scalarmult/curve25519/ref10/x25519_ref10.c
++            src/libsodium/crypto_scalarmult/curve25519/sandy2x/curve25519_sandy2x.c
++            src/libsodium/crypto_scalarmult/curve25519/sandy2x/fe51_invert.c
++            src/libsodium/crypto_scalarmult/curve25519/sandy2x/fe_frombytes_sandy2x.c
++            src/libsodium/crypto_scalarmult/curve25519/scalarmult_curve25519.c
++            src/libsodium/crypto_scalarmult/ed25519/ref10/scalarmult_ed25519_ref10.c
++            src/libsodium/crypto_scalarmult/ristretto255/ref10/scalarmult_ristretto255_ref10.c
++            src/libsodium/crypto_secretbox/crypto_secretbox.c
++            src/libsodium/crypto_secretbox/crypto_secretbox_easy.c
++            src/libsodium/crypto_secretbox/xchacha20poly1305/secretbox_xchacha20poly1305.c
++            src/libsodium/crypto_secretbox/xsalsa20poly1305/secretbox_xsalsa20poly1305.c
++            src/libsodium/crypto_secretstream/xchacha20poly1305/secretstream_xchacha20poly1305.c
++            src/libsodium/crypto_shorthash/crypto_shorthash.c
++            src/libsodium/crypto_shorthash/siphash24/ref/shorthash_siphash24_ref.c
++            src/libsodium/crypto_shorthash/siphash24/ref/shorthash_siphashx24_ref.c
++            src/libsodium/crypto_shorthash/siphash24/shorthash_siphash24.c
++            src/libsodium/crypto_shorthash/siphash24/shorthash_siphashx24.c
++            src/libsodium/crypto_sign/crypto_sign.c
++            src/libsodium/crypto_sign/ed25519/ref10/keypair.c
++            src/libsodium/crypto_sign/ed25519/ref10/obsolete.c
++            src/libsodium/crypto_sign/ed25519/ref10/open.c
++            src/libsodium/crypto_sign/ed25519/ref10/sign.c
++            src/libsodium/crypto_sign/ed25519/sign_ed25519.c
++            src/libsodium/crypto_stream/chacha20/dolbeau/chacha20_dolbeau-avx2.c
++            src/libsodium/crypto_stream/chacha20/dolbeau/chacha20_dolbeau-ssse3.c
++            src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c
++            src/libsodium/crypto_stream/chacha20/stream_chacha20.c
++            src/libsodium/crypto_stream/crypto_stream.c
++            src/libsodium/crypto_stream/salsa20/ref/salsa20_ref.c
++            src/libsodium/crypto_stream/salsa20/stream_salsa20.c
++            src/libsodium/crypto_stream/salsa20/xmm6/salsa20_xmm6.c
++            src/libsodium/crypto_stream/salsa20/xmm6int/salsa20_xmm6int-avx2.c
++            src/libsodium/crypto_stream/salsa20/xmm6int/salsa20_xmm6int-sse2.c
++            src/libsodium/crypto_stream/salsa2012/ref/stream_salsa2012_ref.c
++            src/libsodium/crypto_stream/salsa2012/stream_salsa2012.c
++            src/libsodium/crypto_stream/salsa208/ref/stream_salsa208_ref.c
++            src/libsodium/crypto_stream/salsa208/stream_salsa208.c
++            src/libsodium/crypto_stream/xchacha20/stream_xchacha20.c
++            src/libsodium/crypto_stream/xsalsa20/stream_xsalsa20.c
++            src/libsodium/crypto_verify/sodium/verify.c
++            src/libsodium/randombytes/internal/randombytes_internal_random.c
++            src/libsodium/randombytes/randombytes.c
++            src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c
++            src/libsodium/sodium/codecs.c
++            src/libsodium/sodium/core.c
++            src/libsodium/sodium/runtime.c
++            src/libsodium/sodium/utils.c
++            src/libsodium/sodium/version.c)
++
++if(WIN32)
++  configure_file(builds/msvc/version.h version.h COPYONLY)
++  configure_file(builds/msvc/resource.h resource.h COPYONLY)
++  configure_file(builds/msvc/resource.rc resource.rc COPYONLY)
++endif()
++
++add_library(sodium STATIC ${SOURCES})
++
++set_property(TARGET sodium PROPERTY POSITION_INDEPENDENT_CODE ON)
++
++target_include_directories(sodium
++                            PRIVATE
++                              ${CMAKE_BINARY_DIR}
++                              src/libsodium/include/sodium
++
++			      )
++
++if(WIN32)
++  target_compile_definitions(sodium
++                              PRIVATE
++                                WIN32
++                                HAVE_CONFIG_H
++                                SODIUM_STATIC
++                                NDEBUG
++                                _LIB)
++endif()
++
++install(TARGETS sodium
++    ARCHIVE DESTINATION lib
++)
++
++install(DIRECTORY src/libsodium/include/sodium DESTINATION include
++          FILES_MATCHING PATTERN "*.h")
++install(FILES src/libsodium/include/sodium.h DESTINATION include)
++install(FILES builds/msvc/version.h builds/msvc/resource.h DESTINATION include/sodium)
diff --git a/win_build_vs.bat b/win_build_vs.bat
index 3a24778..b1893e0 100644
--- a/win_build_vs.bat
+++ b/win_build_vs.bat
@@ -78,7 +78,7 @@ pushd %builddir%\
 cmake -G %generator% -DINSTALLER_MERGE_MODULES=%installer_merge_modules% -DENABLE_SQL=%build_SQL% -DCMAKE_BUILD_TYPE_INIT=%cmake_build_type% -DCMAKE_BUILD_TYPE=%cmake_build_type% -DWIN32=WIN32 -DENABLE_LIBRDKAFKA=%build_kafka% -DENABLE_JNI=%build_jni% -DOPENSSL_OFF=OFF -DENABLE_COAP=%build_coap% -DUSE_SHARED_LIBS=OFF -DDISABLE_CONTROLLER=ON  -DBUILD_ROCKSDB=ON -DFORCE_WINDOWS=ON -DUSE_SYSTEM_UUID=OFF -DDISABLE_LIBARCHIVE=OFF -DDISABLE_SCRIPTING=ON -DEXCLUDE_BOOST=ON -DENABLE_WEL=TRUE -DF [...]
 IF %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL%
 if [%cpack%] EQU [ON] (
-    cpack
+    cpack -C %cmake_build_type%
     IF !ERRORLEVEL! NEQ 0 ( popd & exit /b !ERRORLEVEL! )
 )
 if [%skiptests%] NEQ [ON] (