feat: Migrate binary settings to json (#920)

## Summary

* This PR introduces a migration from binary file storage to JSON-based
storage for application settings, state, and various credential stores.
This improves readability, maintainability, and allows for easier manual
configuration editing.
* Benefits:
  - Settings files are now JSON and can be easily read/edited manually
  - Easier to inspect application state and settings during development
  - JSON structure is more flexible for future changes
* Drawback: around 15k of additional flash usage
* Compatibility: Seamless migration preserves existing user data

## Additional Context
1. New JSON I/O Infrastructure files:
- JsonSettingsIO: Core JSON serialization/deserialization logic using
ArduinoJson library
- ObfuscationUtils: XOR-based password obfuscation for sensitive data
2. Migrated Components (now use JSON storage with automatic binary
migration):
     - CrossPointSettings (settings.json): Main application settings
- CrossPointState (state.json): Application state (open book, sleep
mode, etc.)
- WifiCredentialStore (wifi.json): WiFi network credentials (Password
Obfuscation: Sensitive data like WiFi passwords, uses XOR encryption
with fixed keys. Note: This is obfuscation, not cryptographic security -
passwords can be recovered with the key)
- KOReaderCredentialStore (koreader.json): KOReader sync credentials
     - RecentBooksStore (recent.json): Recently opened books list
3. Migration Logic
     - Forward Compatibility: New installations use JSON format
- Backward Compatibility: Existing binary files are automatically
migrated to JSON on first load
- Backup Safety: Original binary files are renamed with .bak extension
after successful migration
- Fallback Handling: If JSON parsing fails, system falls back to binary
loading
4. Infrastructure Updates
     - HalStorage: Added rename() method for backup operations

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _** YES**_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
jpirnay
2026-02-22 07:18:25 +01:00
committed by GitHub
parent f62529ad91
commit 6e4d0e534d
16 changed files with 713 additions and 253 deletions

View File

@@ -1,70 +1,77 @@
#include "WifiCredentialStore.h"
#include <HalStorage.h>
#include <JsonSettingsIO.h>
#include <Logging.h>
#include <ObfuscationUtils.h>
#include <Serialization.h>
// Initialize the static instance
WifiCredentialStore WifiCredentialStore::instance;
namespace {
// File format version
constexpr uint8_t WIFI_FILE_VERSION = 2; // Increased version
// File format version (for binary migration)
constexpr uint8_t WIFI_FILE_VERSION = 2;
// WiFi credentials file path
constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin";
// File paths
constexpr char WIFI_FILE_BIN[] = "/.crosspoint/wifi.bin";
constexpr char WIFI_FILE_JSON[] = "/.crosspoint/wifi.json";
constexpr char WIFI_FILE_BAK[] = "/.crosspoint/wifi.bin.bak";
// Obfuscation key - "CrossPoint" in ASCII
// This is NOT cryptographic security, just prevents casual file reading
constexpr uint8_t OBFUSCATION_KEY[] = {0x43, 0x72, 0x6F, 0x73, 0x73, 0x50, 0x6F, 0x69, 0x6E, 0x74};
constexpr size_t KEY_LENGTH = sizeof(OBFUSCATION_KEY);
} // namespace
// Legacy obfuscation key - "CrossPoint" in ASCII (only used for binary migration)
constexpr uint8_t LEGACY_OBFUSCATION_KEY[] = {0x43, 0x72, 0x6F, 0x73, 0x73, 0x50, 0x6F, 0x69, 0x6E, 0x74};
constexpr size_t LEGACY_KEY_LENGTH = sizeof(LEGACY_OBFUSCATION_KEY);
void WifiCredentialStore::obfuscate(std::string& data) const {
LOG_DBG("WCS", "Obfuscating/deobfuscating %zu bytes", data.size());
void legacyDeobfuscate(std::string& data) {
for (size_t i = 0; i < data.size(); i++) {
data[i] ^= OBFUSCATION_KEY[i % KEY_LENGTH];
data[i] ^= LEGACY_OBFUSCATION_KEY[i % LEGACY_KEY_LENGTH];
}
}
} // namespace
bool WifiCredentialStore::saveToFile() const {
// Make sure the directory exists
Storage.mkdir("/.crosspoint");
FsFile file;
if (!Storage.openFileForWrite("WCS", WIFI_FILE, file)) {
return false;
}
// Write header
serialization::writePod(file, WIFI_FILE_VERSION);
serialization::writeString(file, lastConnectedSsid); // Save last connected SSID
serialization::writePod(file, static_cast<uint8_t>(credentials.size()));
// Write each credential
for (const auto& cred : credentials) {
// Write SSID (plaintext - not sensitive)
serialization::writeString(file, cred.ssid);
LOG_DBG("WCS", "Saving SSID: %s, password length: %zu", cred.ssid.c_str(), cred.password.size());
// Write password (obfuscated)
std::string obfuscatedPwd = cred.password;
obfuscate(obfuscatedPwd);
serialization::writeString(file, obfuscatedPwd);
}
file.close();
LOG_DBG("WCS", "Saved %zu WiFi credentials to file", credentials.size());
return true;
return JsonSettingsIO::saveWifi(*this, WIFI_FILE_JSON);
}
bool WifiCredentialStore::loadFromFile() {
// Try JSON first
if (Storage.exists(WIFI_FILE_JSON)) {
String json = Storage.readFile(WIFI_FILE_JSON);
if (!json.isEmpty()) {
bool resave = false;
bool result = JsonSettingsIO::loadWifi(*this, json.c_str(), &resave);
if (result && resave) {
LOG_DBG("WCS", "Resaving JSON with obfuscated passwords");
saveToFile();
}
return result;
}
}
// Fall back to binary migration
if (Storage.exists(WIFI_FILE_BIN)) {
if (loadFromBinaryFile()) {
if (saveToFile()) {
Storage.rename(WIFI_FILE_BIN, WIFI_FILE_BAK);
LOG_DBG("WCS", "Migrated wifi.bin to wifi.json");
return true;
} else {
LOG_ERR("WCS", "Failed to save wifi during migration");
return false;
}
}
}
return false;
}
bool WifiCredentialStore::loadFromBinaryFile() {
FsFile file;
if (!Storage.openFileForRead("WCS", WIFI_FILE, file)) {
if (!Storage.openFileForRead("WCS", WIFI_FILE_BIN, file)) {
return false;
}
// Read and verify version
uint8_t version;
serialization::readPod(file, version);
if (version > WIFI_FILE_VERSION) {
@@ -79,29 +86,20 @@ bool WifiCredentialStore::loadFromFile() {
lastConnectedSsid.clear();
}
// Read credential count
uint8_t count;
serialization::readPod(file, count);
// Read credentials
credentials.clear();
for (uint8_t i = 0; i < count && i < MAX_NETWORKS; i++) {
WifiCredential cred;
// Read SSID
serialization::readString(file, cred.ssid);
// Read and deobfuscate password
serialization::readString(file, cred.password);
LOG_DBG("WCS", "Loaded SSID: %s, obfuscated password length: %zu", cred.ssid.c_str(), cred.password.size());
obfuscate(cred.password); // XOR is symmetric, so same function deobfuscates
LOG_DBG("WCS", "After deobfuscation, password length: %zu", cred.password.size());
legacyDeobfuscate(cred.password);
credentials.push_back(cred);
}
file.close();
LOG_DBG("WCS", "Loaded %zu WiFi credentials from file", credentials.size());
// LOG_DBG("WCS", "Loaded %zu WiFi credentials from binary file", credentials.size());
return true;
}