From 7f8dc0df5e7e30f98a391fbc822db9e282b32496 Mon Sep 17 00:00:00 2001 From: Tennessee Carmel-Veilleux Date: Mon, 4 Jul 2022 21:30:45 -0400 Subject: [PATCH] Fix escaping in Linux app storage (#20289) * Fix escaping in Linux app storage - Linux apps (like all-clusters-app) use a different storage backend than chip-tool, but the backend eventually goes to the same INI format. - The Linux app did not properly manage keys with `=` and `\n` in the key, when attempting reload of value. Fixes #20188 This PR: - Adds the same escaping to Linux apps as for chip-tool - Adds unescaping - Adds unit tests for escaping and unescaping - Adds debug dumping of all keys to chip-tool storage init Testing done: - Added unit tests pass - Existing unit tests pass - Cert tests pass - Manual validation of restart with colliding keys in INI backend no longer shows collision after change * Fix lint * Fix CI * Address review comment --- .../chip-tool/config/PersistentStorage.cpp | 82 +++------- examples/chip-tool/config/PersistentStorage.h | 2 + scripts/tools/check_includes_config.py | 2 + src/lib/support/BUILD.gn | 2 + src/lib/support/IniEscaping.cpp | 148 ++++++++++++++++++ src/lib/support/IniEscaping.h | 69 ++++++++ src/lib/support/tests/BUILD.gn | 1 + src/lib/support/tests/TestIniEscaping.cpp | 111 +++++++++++++ src/platform/Linux/CHIPLinuxStorageIni.cpp | 37 +++-- 9 files changed, 385 insertions(+), 69 deletions(-) create mode 100644 src/lib/support/IniEscaping.cpp create mode 100644 src/lib/support/IniEscaping.h create mode 100644 src/lib/support/tests/TestIniEscaping.cpp diff --git a/examples/chip-tool/config/PersistentStorage.cpp b/examples/chip-tool/config/PersistentStorage.cpp index bd3d1e4e96e843..f8ceb248095709 100644 --- a/examples/chip-tool/config/PersistentStorage.cpp +++ b/examples/chip-tool/config/PersistentStorage.cpp @@ -18,8 +18,7 @@ #include "PersistentStorage.h" #include -#include -#include +#include #include #include @@ -30,6 +29,7 @@ using Sections = std::map; using namespace ::chip; using namespace ::chip::Controller; +using namespace ::chip::IniEscaping; using namespace ::chip::Logging; constexpr const char kDefaultSectionName[] = "Default"; @@ -48,60 +48,6 @@ std::string GetFilename(const char * name) return "/tmp/chip_tool_config." + std::string(name) + ".ini"; } -namespace { - -std::string EscapeKey(const std::string & key) -{ - std::string escapedKey; - escapedKey.reserve(key.size()); - - for (char c : key) - { - // Replace spaces, non-printable chars, `=` and the escape itself with hex-escaped (C-style) characters. - if ((c <= 0x20) || (c == '=') || (c == '\\') || (c >= 0x7F)) - { - char escaped[5] = { 0 }; - snprintf(escaped, sizeof(escaped), "\\x%02x", (static_cast(c) & 0xff)); - escapedKey += escaped; - } - else - { - escapedKey += c; - } - } - - return escapedKey; -} - -std::string StringToBase64(const std::string & value) -{ - std::unique_ptr buffer(new char[BASE64_ENCODED_LEN(value.length())]); - - uint32_t len = - chip::Base64Encode32(reinterpret_cast(value.data()), static_cast(value.length()), buffer.get()); - if (len == UINT32_MAX) - { - return ""; - } - - return std::string(buffer.get(), len); -} - -std::string Base64ToString(const std::string & b64Value) -{ - std::unique_ptr buffer(new uint8_t[BASE64_MAX_DECODED_LEN(b64Value.length())]); - - uint32_t len = chip::Base64Decode32(b64Value.data(), static_cast(b64Value.length()), buffer.get()); - if (len == UINT32_MAX) - { - return ""; - } - - return std::string(reinterpret_cast(buffer.get()), len); -} - -} // namespace - CHIP_ERROR PersistentStorage::Init(const char * name) { CHIP_ERROR err = CHIP_NO_ERROR; @@ -118,6 +64,12 @@ CHIP_ERROR PersistentStorage::Init(const char * name) mName = name; mConfig.parse(ifs); ifs.close(); + + // To audit the contents at init, uncomment the following: +#if 0 + DumpKeys(); +#endif + exit: return err; } @@ -189,6 +141,24 @@ bool PersistentStorage::SyncDoesKeyExist(const char * key) return (it != section.end()); } +void PersistentStorage::DumpKeys() const +{ +#if CHIP_PROGRESS_LOGGING + for (const auto & section : mConfig.sections) + { + const std::string & sectionName = section.first; + const auto & sectionContent = section.second; + + ChipLogProgress(chipTool, "[%s]", sectionName.c_str()); + for (const auto & entry : sectionContent) + { + const std::string & keyName = entry.first; + ChipLogProgress(chipTool, " => %s", UnescapeKey(keyName).c_str()); + } + } +#endif // CHIP_PROGRESS_LOGGING +} + CHIP_ERROR PersistentStorage::SyncClearAll() { ChipLogProgress(chipTool, "Clearing %s storage", kDefaultSectionName); diff --git a/examples/chip-tool/config/PersistentStorage.h b/examples/chip-tool/config/PersistentStorage.h index 106dcd64949a87..f2afde99355bf7 100644 --- a/examples/chip-tool/config/PersistentStorage.h +++ b/examples/chip-tool/config/PersistentStorage.h @@ -34,6 +34,8 @@ class PersistentStorage : public chip::PersistentStorageDelegate CHIP_ERROR SyncDeleteKeyValue(const char * key) override; bool SyncDoesKeyExist(const char * key) override; + void DumpKeys() const; + uint16_t GetListenPort(); chip::Logging::LogCategory GetLoggingLevel(); diff --git a/scripts/tools/check_includes_config.py b/scripts/tools/check_includes_config.py index 5f74d1e64adce8..2dffe78715f760 100644 --- a/scripts/tools/check_includes_config.py +++ b/scripts/tools/check_includes_config.py @@ -106,6 +106,8 @@ # Not intended for embedded clients (#11705). 'src/app/ClusterStateCache.h': {'list', 'map', 'set', 'vector', 'queue'}, 'src/app/BufferedReadCallback.h': {'vector'}, + 'src/lib/support/IniEscaping.cpp': {'string'}, + 'src/lib/support/IniEscaping.h': {'string'}, # Itself in DENY. 'src/lib/support/CHIPListUtils.h': {'set'}, diff --git a/src/lib/support/BUILD.gn b/src/lib/support/BUILD.gn index fb7c63411155c6..103555cde30b27 100644 --- a/src/lib/support/BUILD.gn +++ b/src/lib/support/BUILD.gn @@ -107,6 +107,8 @@ static_library("support") { "FibonacciUtils.h", "FixedBufferAllocator.cpp", "FixedBufferAllocator.h", + "IniEscaping.cpp", + "IniEscaping.h", "Iterators.h", "LifetimePersistedCounter.h", "ObjectLifeCycle.h", diff --git a/src/lib/support/IniEscaping.cpp b/src/lib/support/IniEscaping.cpp new file mode 100644 index 00000000000000..595316fe475e86 --- /dev/null +++ b/src/lib/support/IniEscaping.cpp @@ -0,0 +1,148 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed 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 +#include + +#include "IniEscaping.h" +#include +#include + +namespace chip { +namespace IniEscaping { + +namespace { + +constexpr size_t kEscapeChunkSize = 4; // "\x12" --> 4 chars + +constexpr bool NeedsEscape(char c) +{ + return (c <= 0x20) || (c == '=') || (c == '\\') || (c >= 0x7F); +} + +constexpr bool IsLowercaseHex(char c) +{ + return ((c >= '0') && (c <= '9')) || ((c >= 'a') && (c <= 'f')); +} + +bool IsValidEscape(const std::string & s) +{ + return (s.size() >= kEscapeChunkSize) && (s[0] == '\\') && (s[1] == 'x') && IsLowercaseHex(s[2]) && IsLowercaseHex(s[3]); +} + +} // namespace + +std::string EscapeKey(const std::string & key) +{ + std::string escapedKey; + escapedKey.reserve(key.size()); + + for (char c : key) + { + // Replace spaces, non-printable chars, `=` and the escape itself with hex-escaped (C-style) characters. + if (NeedsEscape(c)) + { + char escaped[kEscapeChunkSize + 1] = { 0 }; + snprintf(escaped, sizeof(escaped), "\\x%02x", (static_cast(c) & 0xff)); + escapedKey += escaped; + } + else + { + escapedKey += c; + } + } + + return escapedKey; +} + +std::string UnescapeKey(const std::string & key) +{ + std::string unescaped; + unescaped.reserve(key.size()); + + size_t idx = 0; + size_t remaining = key.size(); + while (remaining > 0) + { + char c = key[idx]; + if (c == '\\') + { + // Don't process invalid escapes. + if (remaining < kEscapeChunkSize) + { + return ""; + } + + auto escapeChunk = key.substr(idx, kEscapeChunkSize); + if (!IsValidEscape(escapeChunk)) + { + return ""; + } + + // We validated format, now extract the last two chars as hex + auto hexDigits = escapeChunk.substr(2, 2); + uint8_t charByte = 0; + if ((chip::Encoding::HexToBytes(hexDigits.data(), 2, &charByte, 1) != 1) || !NeedsEscape(static_cast(charByte))) + { + return ""; + } + + unescaped += static_cast(charByte); + idx += kEscapeChunkSize; + } + else + { + unescaped += c; + idx += 1; + } + + remaining = key.size() - idx; + } + + return unescaped; +} + +std::string StringToBase64(const std::string & value) +{ + std::unique_ptr buffer(new char[BASE64_ENCODED_LEN(value.length())]); + + uint32_t len = + chip::Base64Encode32(reinterpret_cast(value.data()), static_cast(value.length()), buffer.get()); + if (len == UINT32_MAX) + { + return ""; + } + + return std::string(buffer.get(), len); +} + +std::string Base64ToString(const std::string & b64Value) +{ + std::unique_ptr buffer(new uint8_t[BASE64_MAX_DECODED_LEN(b64Value.length())]); + + uint32_t len = chip::Base64Decode32(b64Value.data(), static_cast(b64Value.length()), buffer.get()); + if (len == UINT32_MAX) + { + return ""; + } + + return std::string(reinterpret_cast(buffer.get()), len); +} + +} // namespace IniEscaping +} // namespace chip diff --git a/src/lib/support/IniEscaping.h b/src/lib/support/IniEscaping.h new file mode 100644 index 00000000000000..f0b689d4e0a9ef --- /dev/null +++ b/src/lib/support/IniEscaping.h @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed 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 + +namespace chip { +namespace IniEscaping { + +/** + * @brief Escape a storage key to be INI-safe. + * + * All characters <= 0x20, >= 0x7F and `\` and `=` are + * escaped as `\xYY` where `YY` is a 2-digit lowercase hex value. + * + * @param key - key to escape + * @return the escaped key + */ +std::string EscapeKey(const std::string & key); + +/** + * @brief Unescape a storage key escaped by `EscapeKey` + * + * If any character not expected to be escaped is found, or + * if any escape sequences are partial, or if uppercase hex is seen + * in an escape sequence, the empty string is returned. + * + * @param key - key to unescape + * @return the original key that was provided to EscapeKey or empty string on error. + */ +std::string UnescapeKey(const std::string & escapedKey); + +/** + * @brief Takes an octet string passed into a std::string and converts it to base64 + * + * There may be `\0` characters in the data of std::string. + * + * @param value - Value to convert to base64. + * @return the base64 encoding of the `value` input + */ +std::string StringToBase64(const std::string & value); + +/** + * @brief Takes a base64 buffer and converts it to an octet string buffer + * within a std::string. + * + * @param b64Value - Buffer of base64 to decode + * @return an std::string with the bytes, or empty string on decoding errors + */ +std::string Base64ToString(const std::string & b64Value); + +} // namespace IniEscaping +} // namespace chip diff --git a/src/lib/support/tests/BUILD.gn b/src/lib/support/tests/BUILD.gn index 116e023ac0bef1..99cc2801355119 100644 --- a/src/lib/support/tests/BUILD.gn +++ b/src/lib/support/tests/BUILD.gn @@ -34,6 +34,7 @@ chip_test_suite("tests") { "TestErrorStr.cpp", "TestFixedBufferAllocator.cpp", "TestFold.cpp", + "TestIniEscaping.cpp", "TestIntrusiveList.cpp", "TestOwnerOf.cpp", "TestPersistedCounter.cpp", diff --git a/src/lib/support/tests/TestIniEscaping.cpp b/src/lib/support/tests/TestIniEscaping.cpp new file mode 100644 index 00000000000000..3c8890bb4d60be --- /dev/null +++ b/src/lib/support/tests/TestIniEscaping.cpp @@ -0,0 +1,111 @@ +/* + * + * Copyright (c) 2022 Project CHIP Authors + * All rights reserved. + * + * Licensed 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 +#include +#include + +using namespace chip; +using namespace chip::IniEscaping; + +namespace { + +struct TestCase +{ + const char * input; + const char * expectedOutput; +}; + +void TestEscaping(nlTestSuite * inSuite, void * inContext) +{ + NL_TEST_ASSERT(inSuite, EscapeKey("") == ""); + NL_TEST_ASSERT(inSuite, EscapeKey("abcd1234,!") == "abcd1234,!"); + NL_TEST_ASSERT(inSuite, EscapeKey("ab\ncd =12\\34\x7f") == "ab\\x0acd\\x20\\x3d12\\x5c34\\x7f"); + NL_TEST_ASSERT(inSuite, EscapeKey(" ") == "\\x20"); + NL_TEST_ASSERT(inSuite, EscapeKey("===") == "\\x3d\\x3d\\x3d"); +} + +void TestUnescaping(nlTestSuite * inSuite, void * inContext) +{ + // Test valid cases + NL_TEST_ASSERT(inSuite, UnescapeKey("") == ""); + NL_TEST_ASSERT(inSuite, UnescapeKey("abcd1234,!") == "abcd1234,!"); + std::string out = UnescapeKey("abcd1234,!"); + NL_TEST_ASSERT(inSuite, UnescapeKey("ab\\x0acd\\x20\\x3d12\\x5c34\\x7f") == "ab\ncd =12\\34\x7f"); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x20") == " "); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x3d\\x3d\\x3d") == "==="); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x0d") == "\r"); + + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x01\\x02\\x03\\x04\\x05\\x06\\x07") == "\x01\x02\x03\x04\x05\x06\x07"); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x08\\x09\\x0a\\x0b\\x0c\\x0d\\x0e") == "\x08\x09\x0a\x0b\x0c\x0d\x0e"); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x0f\\x10\\x11\\x12\\x13\\x14\\x15") == "\x0f\x10\x11\x12\x13\x14\x15"); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x16\\x17\\x18\\x19\\x1a\\x1b\\x1c") == "\x16\x17\x18\x19\x1a\x1b\x1c"); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x1d\\x1e\\x1f\\x20\\x7f\\x3d\\x5c") == "\x1d\x1e\x1f \x7f=\\"); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x81\\x82\\xff") == "\x81\x82\xff"); + + // Test invalid cases + + // letters should never be escaped + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x5a\55") != "ZU"); + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x5a\55") == ""); + + // Capitalized hex forbidden + NL_TEST_ASSERT(inSuite, UnescapeKey("\\x0D") == ""); + + // Partial escapes forbidden + NL_TEST_ASSERT(inSuite, UnescapeKey("1\\x0") == ""); +} + +void TestRoundTrip(nlTestSuite * inSuite, void * inContext) +{ + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("")) == ""); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("abcd1234,!")) == "abcd1234,!"); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("ab\ncd =12\\34\x7f")) == "ab\ncd =12\\34\x7f"); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey(" ")) == " "); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("===")) == "==="); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("\r")) == "\r"); + + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("\x01\x02\x03\x04\x05\x06\x07")) == "\x01\x02\x03\x04\x05\x06\x07"); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("\x08\x09\x0a\x0b\x0c\x0d\x0e")) == "\x08\x09\x0a\x0b\x0c\x0d\x0e"); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("\x0f\x10\x11\x12\x13\x14\x15")) == "\x0f\x10\x11\x12\x13\x14\x15"); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("\x16\x17\x18\x19\x1a\x1b\x1c")) == "\x16\x17\x18\x19\x1a\x1b\x1c"); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("\x1d\x1e\x1f \x7f=\\")) == "\x1d\x1e\x1f \x7f=\\"); + NL_TEST_ASSERT(inSuite, UnescapeKey(EscapeKey("\x81\x82\xff")) == "\x81\x82\xff"); + + // Make sure entire range is escapable + for (int c = 0; c <= 255; c++) + { + std::string s(5, static_cast(c)); + NL_TEST_ASSERT_LOOP(inSuite, c, UnescapeKey(EscapeKey(s)) == s); + } +} + +const nlTest sTests[] = { NL_TEST_DEF("Test escaping API", TestEscaping), NL_TEST_DEF("Test unescaping API", TestUnescaping), + NL_TEST_DEF("Test escaping API round-tripping with itself", TestRoundTrip), NL_TEST_SENTINEL() }; + +} // namespace + +int TestIniEscaping(void) +{ + nlTestSuite theSuite = { "IniEscaping tests", &sTests[0], nullptr, nullptr }; + + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestIniEscaping); diff --git a/src/platform/Linux/CHIPLinuxStorageIni.cpp b/src/platform/Linux/CHIPLinuxStorageIni.cpp index 1f089f092e6458..21dbefa2493c2e 100644 --- a/src/platform/Linux/CHIPLinuxStorageIni.cpp +++ b/src/platform/Linux/CHIPLinuxStorageIni.cpp @@ -30,10 +30,13 @@ #include #include #include +#include #include #include #include +using namespace chip::IniEscaping; + namespace chip { namespace DeviceLayer { namespace Internal { @@ -131,11 +134,12 @@ CHIP_ERROR ChipLinuxStorageIni::GetUInt16Value(const char * key, uint16_t & val) if (retval == CHIP_NO_ERROR) { - auto it = section.find(key); + std::string escapedKey = EscapeKey(key); + auto it = section.find(escapedKey); if (it != section.end()) { - if (!inipp::extract(section[key], val)) + if (!inipp::extract(section[escapedKey], val)) { retval = CHIP_ERROR_INVALID_ARGUMENT; } @@ -158,11 +162,12 @@ CHIP_ERROR ChipLinuxStorageIni::GetUIntValue(const char * key, uint32_t & val) if (retval == CHIP_NO_ERROR) { - auto it = section.find(key); + std::string escapedKey = EscapeKey(key); + auto it = section.find(escapedKey); if (it != section.end()) { - if (!inipp::extract(section[key], val)) + if (!inipp::extract(section[escapedKey], val)) { retval = CHIP_ERROR_INVALID_ARGUMENT; } @@ -185,11 +190,12 @@ CHIP_ERROR ChipLinuxStorageIni::GetUInt64Value(const char * key, uint64_t & val) if (retval == CHIP_NO_ERROR) { - auto it = section.find(key); + std::string escapedKey = EscapeKey(key); + auto it = section.find(escapedKey); if (it != section.end()) { - if (!inipp::extract(section[key], val)) + if (!inipp::extract(section[escapedKey], val)) { retval = CHIP_ERROR_INVALID_ARGUMENT; } @@ -212,12 +218,13 @@ CHIP_ERROR ChipLinuxStorageIni::GetStringValue(const char * key, char * buf, siz if (retval == CHIP_NO_ERROR) { - auto it = section.find(key); + std::string escapedKey = EscapeKey(key); + auto it = section.find(escapedKey); if (it != section.end()) { std::string value; - if (inipp::extract(section[key], value)) + if (inipp::extract(section[escapedKey], value)) { size_t len = value.size(); @@ -258,7 +265,8 @@ CHIP_ERROR ChipLinuxStorageIni::GetBinaryBlobDataAndLengths(const char * key, return err; } - auto it = section.find(key); + std::string escapedKey = EscapeKey(key); + auto it = section.find(escapedKey); if (it == section.end()) { return CHIP_ERROR_KEY_NOT_FOUND; @@ -267,7 +275,7 @@ CHIP_ERROR ChipLinuxStorageIni::GetBinaryBlobDataAndLengths(const char * key, std::string value; // Compute the expectedDecodedLen - if (!inipp::extract(section[key], value)) + if (!inipp::extract(section[escapedKey], value)) { return CHIP_ERROR_INVALID_ARGUMENT; } @@ -338,7 +346,8 @@ bool ChipLinuxStorageIni::HasValue(const char * key) if (GetDefaultSection(section) != CHIP_NO_ERROR) return false; - auto it = section.find(key); + std::string escapedKey = EscapeKey(key); + auto it = section.find(escapedKey); return it != section.end(); } @@ -349,8 +358,9 @@ CHIP_ERROR ChipLinuxStorageIni::AddEntry(const char * key, const char * value) if ((key != nullptr) && (value != nullptr)) { + std::string escapedKey = EscapeKey(key); std::map & section = mConfigStore.sections["DEFAULT"]; - section[key] = std::string(value); + section[escapedKey] = std::string(value); } else { @@ -367,7 +377,8 @@ CHIP_ERROR ChipLinuxStorageIni::RemoveEntry(const char * key) std::map & section = mConfigStore.sections["DEFAULT"]; - auto it = section.find(key); + std::string escapedKey = EscapeKey(key); + auto it = section.find(escapedKey); if (it != section.end()) {