202 lines
8.2 KiB
C++
202 lines
8.2 KiB
C++
#include "IdentityStore.h"
|
|
|
|
#include "FilePresence.h"
|
|
#if defined(ESP32_PLATFORM)
|
|
#include <Preferences.h>
|
|
#include <nvs.h>
|
|
#endif
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
#include "AtomicFileWriter.h"
|
|
#else
|
|
#include "ContactFileTransaction.h"
|
|
#endif
|
|
|
|
bool IdentityStore::recover(const char* name) {
|
|
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
|
char filename[40], backup[48];
|
|
if (snprintf(filename, sizeof(filename), "%s/%s.id", _dir, name)
|
|
>= (int)sizeof(filename)) return false;
|
|
snprintf(backup, sizeof(backup), "%s.bak", filename);
|
|
bool primary_exists = false, backup_exists = false;
|
|
if (!mesh::filePresence(_fs, filename, primary_exists)
|
|
|| !mesh::filePresence(_fs, backup, backup_exists)) return false;
|
|
if (!primary_exists && backup_exists) return _fs->rename(backup, filename);
|
|
#else
|
|
(void)name;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) {
|
|
return loadResult(name, id) == IdentityLoadResult::Loaded;
|
|
}
|
|
|
|
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id, char display_name[], int max_name_sz) {
|
|
return loadResult(name, id, display_name, max_name_sz)
|
|
== IdentityLoadResult::Loaded;
|
|
}
|
|
|
|
IdentityLoadResult IdentityStore::loadResult(
|
|
const char *name, mesh::LocalIdentity& id) {
|
|
return loadResult(name, id, nullptr, 0);
|
|
}
|
|
|
|
IdentityLoadResult IdentityStore::loadResult(
|
|
const char *name, mesh::LocalIdentity& id, char display_name[],
|
|
int max_name_sz) {
|
|
char filename[40];
|
|
if (snprintf(filename, sizeof(filename), "%s/%s.id", _dir, name)
|
|
>= (int)sizeof(filename)) return IdentityLoadResult::Unreadable;
|
|
#if defined(ESP32_PLATFORM)
|
|
// A 4 MiB LoRa partition migration must boot the old app0 receiver before
|
|
// the new Full image can run. The bridge staged the original key in stable
|
|
// NVS; that old receiver may have formatted the newly sized SPIFFS and
|
|
// generated a temporary key. Restore the original before normal startup
|
|
// accepts either that temporary key or a missing identity.
|
|
if (strcmp(_dir, "/identity") == 0 && strcmp(name, "_main") == 0) {
|
|
// Probe without creating an empty migration namespace on ordinary boots.
|
|
// Direct NVS distinguishes a clean absence (without Preferences logging)
|
|
// from storage/type failures that must not accept a temporary/new key.
|
|
nvs_handle_t migration_handle;
|
|
const esp_err_t opened = nvs_open("mesh-pt-migrate", NVS_READONLY,
|
|
&migration_handle);
|
|
if (opened != ESP_OK && opened != ESP_ERR_NVS_NOT_FOUND)
|
|
return IdentityLoadResult::Unreadable;
|
|
if (opened == ESP_OK) {
|
|
uint8_t pending_value = 0;
|
|
const esp_err_t flag = nvs_get_u8(migration_handle, "id-pending",
|
|
&pending_value);
|
|
if ((flag != ESP_OK && flag != ESP_ERR_NVS_NOT_FOUND)
|
|
|| (flag == ESP_OK && pending_value > 1)) {
|
|
nvs_close(migration_handle);
|
|
return IdentityLoadResult::Unreadable;
|
|
}
|
|
const bool pending = flag == ESP_OK && pending_value == 1;
|
|
uint8_t file_bytes[PUB_KEY_SIZE + PRV_KEY_SIZE];
|
|
size_t staged = sizeof(file_bytes);
|
|
const esp_err_t identity_result = pending
|
|
? nvs_get_blob(migration_handle, "identity", file_bytes, &staged) : ESP_OK;
|
|
nvs_close(migration_handle);
|
|
if (pending) {
|
|
if (identity_result != ESP_OK || staged != sizeof(file_bytes))
|
|
return IdentityLoadResult::Unreadable;
|
|
uint8_t export_bytes[sizeof(file_bytes)];
|
|
memcpy(export_bytes, file_bytes + PUB_KEY_SIZE, PRV_KEY_SIZE);
|
|
memcpy(export_bytes + PRV_KEY_SIZE, file_bytes, PUB_KEY_SIZE);
|
|
mesh::LocalIdentity original;
|
|
original.readFrom(export_bytes, sizeof(export_bytes));
|
|
if (!saveWithRetry(name, original)) return IdentityLoadResult::Unreadable;
|
|
File restored = _fs->open(filename, "r");
|
|
uint8_t verified[sizeof(file_bytes)];
|
|
const bool matches = restored
|
|
&& restored.read(verified, sizeof(verified)) == sizeof(verified)
|
|
&& memcmp(file_bytes, verified, sizeof(file_bytes)) == 0;
|
|
if (restored) restored.close();
|
|
if (!matches) return IdentityLoadResult::Unreadable;
|
|
Preferences migration_nvs;
|
|
if (!migration_nvs.begin("mesh-pt-migrate", false))
|
|
return IdentityLoadResult::Unreadable;
|
|
// Clear the pending flag first: a power cut cannot then leave a
|
|
// pending flag pointing at already removed key bytes.
|
|
const bool cleared = migration_nvs.remove("id-pending");
|
|
if (cleared) migration_nvs.remove("identity");
|
|
migration_nvs.end();
|
|
if (!cleared) return IdentityLoadResult::Unreadable;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
for (unsigned attempt = 0; attempt < IO_ATTEMPTS; ++attempt) {
|
|
if (!recover(name)) continue;
|
|
bool present = false;
|
|
if (!mesh::filePresence(_fs, filename, present)) continue;
|
|
if (!present) return IdentityLoadResult::Missing;
|
|
#if defined(RP2040_PLATFORM)
|
|
File file = _fs->open(filename, "r");
|
|
#else
|
|
File file = _fs->open(filename);
|
|
#endif
|
|
if (!file || file.isDirectory()) {
|
|
file.close();
|
|
continue;
|
|
}
|
|
mesh::LocalIdentity loaded;
|
|
if (!loaded.readFrom(file)) {
|
|
file.close();
|
|
continue;
|
|
}
|
|
// The legacy display name is optional. A key-only image remains valid;
|
|
// neither a short name nor a partial key may leak into the caller's state.
|
|
if (display_name != nullptr && max_name_sz > 0) {
|
|
const int n = max_name_sz > 32 ? 32 : max_name_sz;
|
|
char loaded_name[32];
|
|
if (file.read(reinterpret_cast<uint8_t*>(loaded_name), n) == n) {
|
|
loaded_name[n - 1] = 0;
|
|
memcpy(display_name, loaded_name, n);
|
|
}
|
|
}
|
|
file.close();
|
|
id = loaded;
|
|
return IdentityLoadResult::Loaded;
|
|
}
|
|
// An existing file which cannot be read is materially different from a
|
|
// fresh device. Startup callers must preserve it instead of replacing the
|
|
// radio's private key after a transient or localized flash failure.
|
|
return IdentityLoadResult::Unreadable;
|
|
}
|
|
|
|
bool IdentityStore::saveWithRetry(const char* name, const mesh::LocalIdentity& id) {
|
|
for (unsigned attempt = 0; attempt < IO_ATTEMPTS; ++attempt) {
|
|
if (save(name, id)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
|
|
char filename[40];
|
|
sprintf(filename, "%s/%s.id", _dir, name);
|
|
|
|
if (!recover(name)) return false;
|
|
uint8_t key_data[PRV_KEY_SIZE + PUB_KEY_SIZE];
|
|
if (id.writeTo(key_data, sizeof(key_data)) != sizeof(key_data)) return false;
|
|
|
|
// LocalIdentity's byte-buffer export is private-key then public-key, while
|
|
// the historical file format is public-key then private-key.
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
mesh::AtomicFileWriter writer(_fs, filename);
|
|
#else
|
|
mesh::ContactFileTransaction writer(_fs, filename, mesh::filePresence<FILESYSTEM>);
|
|
#endif
|
|
const bool wrote = writer
|
|
&& writer.write(&key_data[PRV_KEY_SIZE], PUB_KEY_SIZE) == PUB_KEY_SIZE
|
|
&& writer.write(key_data, PRV_KEY_SIZE) == PRV_KEY_SIZE;
|
|
const bool success = writer.commit(wrote);
|
|
MESH_DEBUG_PRINTLN("IdentityStore::save() atomic write - %s", success ? "OK" : "Err");
|
|
return success;
|
|
}
|
|
|
|
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id, const char display_name[]) {
|
|
char filename[40];
|
|
sprintf(filename, "%s/%s.id", _dir, name);
|
|
|
|
if (!recover(name)) return false;
|
|
uint8_t key_data[PRV_KEY_SIZE + PUB_KEY_SIZE];
|
|
if (id.writeTo(key_data, sizeof(key_data)) != sizeof(key_data)) return false;
|
|
uint8_t display_data[32];
|
|
memset(display_data, 0, sizeof(display_data));
|
|
size_t display_len = strlen(display_name);
|
|
if (display_len > sizeof(display_data) - 1) display_len = sizeof(display_data) - 1;
|
|
memcpy(display_data, display_name, display_len);
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
mesh::AtomicFileWriter writer(_fs, filename);
|
|
#else
|
|
mesh::ContactFileTransaction writer(_fs, filename, mesh::filePresence<FILESYSTEM>);
|
|
#endif
|
|
const bool wrote = writer
|
|
&& writer.write(&key_data[PRV_KEY_SIZE], PUB_KEY_SIZE) == PUB_KEY_SIZE
|
|
&& writer.write(key_data, PRV_KEY_SIZE) == PRV_KEY_SIZE
|
|
&& writer.write(display_data, sizeof(display_data)) == sizeof(display_data);
|
|
return writer.commit(wrote);
|
|
}
|