Skip to content

Commit c42895c

Browse files
committed
feat(nvs): cryptographic hash with key derivation
Use the hardware and a configurable salt to encrypt sensitive datas
1 parent af0651d commit c42895c

4 files changed

Lines changed: 69 additions & 10 deletions

File tree

include/config/SecureStorage.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ class SecureStorage {
3131
bool remove(const char* key);
3232
String get(const char* key, const char* defaultValue = nullptr);
3333

34+
// Set the public salt (should be called before begin())
35+
static void setSalt(const String& salt);
36+
3437
private:
3538
size_t _eepromSize;
3639
bool loadToMemory();

readme.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,25 @@ cp data/config-{hellocubic|smalltv}.example data/config.json
209209

210210
You can edit this JSON file to configure your firmware, for example by modifying `wifi_ssid` and `wifi_password` so that your device connects to your network
211211

212+
Note: The Wi-Fi credentials in config.json are migrated to EEPROM “secure storage” on first boot. After that, the Wi-Fi credentials are erased from config.json
213+
214+
Security of stored secrets:
215+
216+
- All sensitive values (API keys, wifi credentials, tokens, etc.) are stored in EEPROM using a device-unique obfuscation scheme
217+
- The obfuscation key is derived from the ESP8266's MAC address, chip ID, and a salt (configurable [here](./src/main.cpp#L37) in code) using SHA-256
218+
- The JSON payload is XORed with this derived key before being written to EEPROM, and de-obfuscated on read
219+
- This makes it much harder to recover secrets from a raw flash dump on another device, or with only partial knowledge of the hardware
220+
221+
Limitations:
222+
223+
- This is not true encryption and does not provide hardware-backed security or protection against a determined attacker with full device access
224+
- The public salt is not secret; if an attacker knows the salt, MAC, and chip ID, they can reconstruct the key
225+
- There is no secure element or tamper resistance
226+
227+
**Do not assume confidentiality against a determined attacker**
228+
229+
Warning: If the salt, the chip ID or the mac address change, the eeprom secure storage is clear and reset to ensure no data leak is possible
230+
212231
### 3. Build the firmware and filesystem
213232

214233
To build the firmware you can use decontainer, docker or build it by yourself using [PlateformIO](https://docs.platformio.org/en/latest/core/installation/methods/installer-script.html)
@@ -275,15 +294,6 @@ From there, select and flash the filesystem using the `littlefs.bin` file
275294

276295
Once the device reboots, the setup is complete!
277296

278-
Note: The Wi-Fi credentials in config.json are migrated to EEPROM “secure storage” on first boot. After that, the Wi-Fi credentials are erased from config.json
279-
280-
Data stored in EEPROM (or flash-based EEPROM emulation) is not secure by default
281-
All values (API keys, Wi-Fi credentials, tokens, etc.) are currently stored in clear text and can be recovered by anyone with physical access to the device or the ability to read the flash memory
282-
283-
This project does not provide hardware-backed security
284-
285-
Do not assume confidentiality against a determined attacker
286-
287297
## License
288298

289299
This project is licensed under the **GPLv3 License** - see the [LICENSE](LICENSE) file for details

src/config/SecureStorage.cpp

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
#include <array>
2121
#include <EEPROM.h>
2222
#include <Logger.h>
23+
#include <ESP8266WiFi.h>
24+
#include <user_interface.h>
25+
#include <BearSSLHelpers.h>
2326

2427
#include "config/SecureStorage.h"
2528

@@ -28,6 +31,35 @@ static constexpr uint8_t LEN_SHIFT = 8;
2831
static constexpr uint8_t LEN_HIGH_IDX = 4;
2932
static constexpr uint8_t LEN_LOW_IDX = 5;
3033
static constexpr uint8_t LEN_MASK = 0xFF;
34+
static constexpr size_t KEY_LEN = 32;
35+
36+
static String kvSalt;
37+
38+
/**
39+
* @brief Set the public salt used in key derivation
40+
*
41+
* @param salt The public salt string
42+
*/
43+
void SecureStorage::setSalt(const String& salt) { kvSalt = salt; }
44+
45+
/**
46+
* @brief Derive the obfuscation key from device-unique parameters and public salt
47+
*
48+
* @param out32 Output buffer for the 32-byte derived key
49+
*
50+
* @returns void
51+
*/
52+
static void deriveKey(uint8_t* out32) {
53+
String mac = WiFi.macAddress();
54+
uint32_t chip = system_get_chip_id();
55+
56+
String input = mac + String(chip) + kvSalt;
57+
58+
br_sha256_context ctx;
59+
br_sha256_init(&ctx);
60+
br_sha256_update(&ctx, (const unsigned char*)input.c_str(), input.length());
61+
br_sha256_out(&ctx, out32);
62+
}
3163

3264
SecureStorage::SecureStorage(size_t eepromSize) : _eepromSize(eepromSize), _doc() {}
3365

@@ -105,6 +137,13 @@ auto const SecureStorage::loadToMemory() -> bool {
105137

106138
buf[len] = '\0';
107139

140+
// De-obfuscate using derived key
141+
std::array<uint8_t, KEY_LEN> key;
142+
deriveKey(key.data());
143+
for (uint16_t i = 0; i < len; ++i) {
144+
buf[i] ^= key[static_cast<size_t>(i) % KEY_LEN];
145+
}
146+
108147
DeserializationError err = deserializeJson(_doc, buf);
109148

110149
if (err) {
@@ -150,8 +189,12 @@ auto const SecureStorage::flushToEEPROM() -> bool {
150189
EEPROM.write(LEN_HIGH_IDX, static_cast<uint8_t>((len >> LEN_SHIFT) & LEN_MASK));
151190
EEPROM.write(LEN_LOW_IDX, static_cast<uint8_t>(len & LEN_MASK));
152191

192+
// Obfuscate using derived key
193+
std::array<uint8_t, KEY_LEN> key;
194+
deriveKey(key.data());
153195
for (uint16_t i = 0; i < len; ++i) {
154-
EEPROM.write(static_cast<int>(headerSize + i), static_cast<uint8_t>(out.charAt(i)));
196+
uint8_t obfuscatedString = out.charAt(i) ^ key[static_cast<size_t>(i) % KEY_LEN];
197+
EEPROM.write(static_cast<int>(headerSize + i), obfuscatedString);
155198
}
156199

157200
if (!EEPROM.commit()) {

src/main.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ ConfigManager configManager;
3434
const char* AP_SSID = "GeekMagic";
3535
const char* AP_PASSWORD = "$str0ngPa$$w0rd";
3636
WiFiManager* wifiManager = nullptr;
37+
static String KV_SALT = "GeekMagicOpenFirmwareIsAwesome";
3738

3839
static constexpr uint32_t SERIAL_BAUD_RATE = 115200;
3940
static constexpr uint32_t BOOT_DELAY_MS = 200;
@@ -67,6 +68,8 @@ void setup() {
6768

6869
step++;
6970

71+
SecureStorage::setSalt(KV_SALT);
72+
7073
if (configManager.secure.begin()) {
7174
Logger::info("SecureStorage initialized successfully", "ConfigManager");
7275
}

0 commit comments

Comments
 (0)