Skip to content

Commit a97aa38

Browse files
tcarmelveilleuxpull[bot]
authored andcommitted
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
1 parent 913acf1 commit a97aa38

File tree

9 files changed

+385
-69
lines changed

9 files changed

+385
-69
lines changed

examples/chip-tool/config/PersistentStorage.cpp

+26-56
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
#include "PersistentStorage.h"
1919

2020
#include <lib/core/CHIPEncoding.h>
21-
#include <lib/support/Base64.h>
22-
#include <protocols/secure_channel/PASESession.h>
21+
#include <lib/support/IniEscaping.h>
2322

2423
#include <fstream>
2524
#include <memory>
@@ -30,6 +29,7 @@ using Sections = std::map<String, Section>;
3029

3130
using namespace ::chip;
3231
using namespace ::chip::Controller;
32+
using namespace ::chip::IniEscaping;
3333
using namespace ::chip::Logging;
3434

3535
constexpr const char kDefaultSectionName[] = "Default";
@@ -48,60 +48,6 @@ std::string GetFilename(const char * name)
4848
return "/tmp/chip_tool_config." + std::string(name) + ".ini";
4949
}
5050

51-
namespace {
52-
53-
std::string EscapeKey(const std::string & key)
54-
{
55-
std::string escapedKey;
56-
escapedKey.reserve(key.size());
57-
58-
for (char c : key)
59-
{
60-
// Replace spaces, non-printable chars, `=` and the escape itself with hex-escaped (C-style) characters.
61-
if ((c <= 0x20) || (c == '=') || (c == '\\') || (c >= 0x7F))
62-
{
63-
char escaped[5] = { 0 };
64-
snprintf(escaped, sizeof(escaped), "\\x%02x", (static_cast<unsigned>(c) & 0xff));
65-
escapedKey += escaped;
66-
}
67-
else
68-
{
69-
escapedKey += c;
70-
}
71-
}
72-
73-
return escapedKey;
74-
}
75-
76-
std::string StringToBase64(const std::string & value)
77-
{
78-
std::unique_ptr<char[]> buffer(new char[BASE64_ENCODED_LEN(value.length())]);
79-
80-
uint32_t len =
81-
chip::Base64Encode32(reinterpret_cast<const uint8_t *>(value.data()), static_cast<uint32_t>(value.length()), buffer.get());
82-
if (len == UINT32_MAX)
83-
{
84-
return "";
85-
}
86-
87-
return std::string(buffer.get(), len);
88-
}
89-
90-
std::string Base64ToString(const std::string & b64Value)
91-
{
92-
std::unique_ptr<uint8_t[]> buffer(new uint8_t[BASE64_MAX_DECODED_LEN(b64Value.length())]);
93-
94-
uint32_t len = chip::Base64Decode32(b64Value.data(), static_cast<uint32_t>(b64Value.length()), buffer.get());
95-
if (len == UINT32_MAX)
96-
{
97-
return "";
98-
}
99-
100-
return std::string(reinterpret_cast<const char *>(buffer.get()), len);
101-
}
102-
103-
} // namespace
104-
10551
CHIP_ERROR PersistentStorage::Init(const char * name)
10652
{
10753
CHIP_ERROR err = CHIP_NO_ERROR;
@@ -118,6 +64,12 @@ CHIP_ERROR PersistentStorage::Init(const char * name)
11864
mName = name;
11965
mConfig.parse(ifs);
12066
ifs.close();
67+
68+
// To audit the contents at init, uncomment the following:
69+
#if 0
70+
DumpKeys();
71+
#endif
72+
12173
exit:
12274
return err;
12375
}
@@ -189,6 +141,24 @@ bool PersistentStorage::SyncDoesKeyExist(const char * key)
189141
return (it != section.end());
190142
}
191143

144+
void PersistentStorage::DumpKeys() const
145+
{
146+
#if CHIP_PROGRESS_LOGGING
147+
for (const auto & section : mConfig.sections)
148+
{
149+
const std::string & sectionName = section.first;
150+
const auto & sectionContent = section.second;
151+
152+
ChipLogProgress(chipTool, "[%s]", sectionName.c_str());
153+
for (const auto & entry : sectionContent)
154+
{
155+
const std::string & keyName = entry.first;
156+
ChipLogProgress(chipTool, " => %s", UnescapeKey(keyName).c_str());
157+
}
158+
}
159+
#endif // CHIP_PROGRESS_LOGGING
160+
}
161+
192162
CHIP_ERROR PersistentStorage::SyncClearAll()
193163
{
194164
ChipLogProgress(chipTool, "Clearing %s storage", kDefaultSectionName);

examples/chip-tool/config/PersistentStorage.h

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class PersistentStorage : public chip::PersistentStorageDelegate
3434
CHIP_ERROR SyncDeleteKeyValue(const char * key) override;
3535
bool SyncDoesKeyExist(const char * key) override;
3636

37+
void DumpKeys() const;
38+
3739
uint16_t GetListenPort();
3840
chip::Logging::LogCategory GetLoggingLevel();
3941

scripts/tools/check_includes_config.py

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@
106106
# Not intended for embedded clients (#11705).
107107
'src/app/ClusterStateCache.h': {'list', 'map', 'set', 'vector', 'queue'},
108108
'src/app/BufferedReadCallback.h': {'vector'},
109+
'src/lib/support/IniEscaping.cpp': {'string'},
110+
'src/lib/support/IniEscaping.h': {'string'},
109111

110112
# Itself in DENY.
111113
'src/lib/support/CHIPListUtils.h': {'set'},

src/lib/support/BUILD.gn

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ static_library("support") {
107107
"FibonacciUtils.h",
108108
"FixedBufferAllocator.cpp",
109109
"FixedBufferAllocator.h",
110+
"IniEscaping.cpp",
111+
"IniEscaping.h",
110112
"Iterators.h",
111113
"LifetimePersistedCounter.h",
112114
"ObjectLifeCycle.h",

src/lib/support/IniEscaping.cpp

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
*
3+
* Copyright (c) 2022 Project CHIP Authors
4+
* All rights reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
#include <memory>
20+
#include <string>
21+
22+
#include "IniEscaping.h"
23+
#include <lib/support/Base64.h>
24+
#include <lib/support/BytesToHex.h>
25+
26+
namespace chip {
27+
namespace IniEscaping {
28+
29+
namespace {
30+
31+
constexpr size_t kEscapeChunkSize = 4; // "\x12" --> 4 chars
32+
33+
constexpr bool NeedsEscape(char c)
34+
{
35+
return (c <= 0x20) || (c == '=') || (c == '\\') || (c >= 0x7F);
36+
}
37+
38+
constexpr bool IsLowercaseHex(char c)
39+
{
40+
return ((c >= '0') && (c <= '9')) || ((c >= 'a') && (c <= 'f'));
41+
}
42+
43+
bool IsValidEscape(const std::string & s)
44+
{
45+
return (s.size() >= kEscapeChunkSize) && (s[0] == '\\') && (s[1] == 'x') && IsLowercaseHex(s[2]) && IsLowercaseHex(s[3]);
46+
}
47+
48+
} // namespace
49+
50+
std::string EscapeKey(const std::string & key)
51+
{
52+
std::string escapedKey;
53+
escapedKey.reserve(key.size());
54+
55+
for (char c : key)
56+
{
57+
// Replace spaces, non-printable chars, `=` and the escape itself with hex-escaped (C-style) characters.
58+
if (NeedsEscape(c))
59+
{
60+
char escaped[kEscapeChunkSize + 1] = { 0 };
61+
snprintf(escaped, sizeof(escaped), "\\x%02x", (static_cast<unsigned>(c) & 0xff));
62+
escapedKey += escaped;
63+
}
64+
else
65+
{
66+
escapedKey += c;
67+
}
68+
}
69+
70+
return escapedKey;
71+
}
72+
73+
std::string UnescapeKey(const std::string & key)
74+
{
75+
std::string unescaped;
76+
unescaped.reserve(key.size());
77+
78+
size_t idx = 0;
79+
size_t remaining = key.size();
80+
while (remaining > 0)
81+
{
82+
char c = key[idx];
83+
if (c == '\\')
84+
{
85+
// Don't process invalid escapes.
86+
if (remaining < kEscapeChunkSize)
87+
{
88+
return "";
89+
}
90+
91+
auto escapeChunk = key.substr(idx, kEscapeChunkSize);
92+
if (!IsValidEscape(escapeChunk))
93+
{
94+
return "";
95+
}
96+
97+
// We validated format, now extract the last two chars as hex
98+
auto hexDigits = escapeChunk.substr(2, 2);
99+
uint8_t charByte = 0;
100+
if ((chip::Encoding::HexToBytes(hexDigits.data(), 2, &charByte, 1) != 1) || !NeedsEscape(static_cast<char>(charByte)))
101+
{
102+
return "";
103+
}
104+
105+
unescaped += static_cast<char>(charByte);
106+
idx += kEscapeChunkSize;
107+
}
108+
else
109+
{
110+
unescaped += c;
111+
idx += 1;
112+
}
113+
114+
remaining = key.size() - idx;
115+
}
116+
117+
return unescaped;
118+
}
119+
120+
std::string StringToBase64(const std::string & value)
121+
{
122+
std::unique_ptr<char[]> buffer(new char[BASE64_ENCODED_LEN(value.length())]);
123+
124+
uint32_t len =
125+
chip::Base64Encode32(reinterpret_cast<const uint8_t *>(value.data()), static_cast<uint32_t>(value.length()), buffer.get());
126+
if (len == UINT32_MAX)
127+
{
128+
return "";
129+
}
130+
131+
return std::string(buffer.get(), len);
132+
}
133+
134+
std::string Base64ToString(const std::string & b64Value)
135+
{
136+
std::unique_ptr<uint8_t[]> buffer(new uint8_t[BASE64_MAX_DECODED_LEN(b64Value.length())]);
137+
138+
uint32_t len = chip::Base64Decode32(b64Value.data(), static_cast<uint32_t>(b64Value.length()), buffer.get());
139+
if (len == UINT32_MAX)
140+
{
141+
return "";
142+
}
143+
144+
return std::string(reinterpret_cast<const char *>(buffer.get()), len);
145+
}
146+
147+
} // namespace IniEscaping
148+
} // namespace chip

src/lib/support/IniEscaping.h

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
*
3+
* Copyright (c) 2022 Project CHIP Authors
4+
* All rights reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
#pragma once
20+
21+
#include <string>
22+
23+
namespace chip {
24+
namespace IniEscaping {
25+
26+
/**
27+
* @brief Escape a storage key to be INI-safe.
28+
*
29+
* All characters <= 0x20, >= 0x7F and `\` and `=` are
30+
* escaped as `\xYY` where `YY` is a 2-digit lowercase hex value.
31+
*
32+
* @param key - key to escape
33+
* @return the escaped key
34+
*/
35+
std::string EscapeKey(const std::string & key);
36+
37+
/**
38+
* @brief Unescape a storage key escaped by `EscapeKey`
39+
*
40+
* If any character not expected to be escaped is found, or
41+
* if any escape sequences are partial, or if uppercase hex is seen
42+
* in an escape sequence, the empty string is returned.
43+
*
44+
* @param key - key to unescape
45+
* @return the original key that was provided to EscapeKey or empty string on error.
46+
*/
47+
std::string UnescapeKey(const std::string & escapedKey);
48+
49+
/**
50+
* @brief Takes an octet string passed into a std::string and converts it to base64
51+
*
52+
* There may be `\0` characters in the data of std::string.
53+
*
54+
* @param value - Value to convert to base64.
55+
* @return the base64 encoding of the `value` input
56+
*/
57+
std::string StringToBase64(const std::string & value);
58+
59+
/**
60+
* @brief Takes a base64 buffer and converts it to an octet string buffer
61+
* within a std::string.
62+
*
63+
* @param b64Value - Buffer of base64 to decode
64+
* @return an std::string with the bytes, or empty string on decoding errors
65+
*/
66+
std::string Base64ToString(const std::string & b64Value);
67+
68+
} // namespace IniEscaping
69+
} // namespace chip

src/lib/support/tests/BUILD.gn

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ chip_test_suite("tests") {
3434
"TestErrorStr.cpp",
3535
"TestFixedBufferAllocator.cpp",
3636
"TestFold.cpp",
37+
"TestIniEscaping.cpp",
3738
"TestIntrusiveList.cpp",
3839
"TestOwnerOf.cpp",
3940
"TestPersistedCounter.cpp",

0 commit comments

Comments
 (0)