Harden marathon-tested OTA and hardware paths
Add durable TempRadio, ACL, clock, bridge, GPS/I2C, USB logging, and Indicator recovery safeguards together with expanded automation, build/profile contracts, release metadata, and the hardware validation ledger.
This commit is contained in:
1 parent
e26d48e4ca
commit
9fd580c89e
109 files changed
+18697
-1094
No files matched your search
+583
-38
@@ -1,11 +1,110 @@
|
||||
#include "ClientACL.h"
|
||||
#include "ClientACLFileTransaction.h"
|
||||
#include "ClientACLFileIntegrity.h"
|
||||
#include "ClientLoginPersistence.h"
|
||||
#include "ClientPathPersistence.h"
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include "AtomicFileWriter.h"
|
||||
#endif
|
||||
|
||||
static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1;
|
||||
static const uint8_t EMPTY_OUT_PATH[MAX_PATH_SIZE] = {};
|
||||
|
||||
static File openRead(FILESYSTEM* fs, const char* filename) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
return fs->open(filename, "r");
|
||||
#else
|
||||
return fs->open(filename);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
static const size_t CONTACT_RECORD_SIZE =
|
||||
32 + 1 + 4 + 2 + 1 + 64 + PUB_KEY_SIZE + 1 + 64;
|
||||
static const size_t LEGACY_CONTACT_RECORD_SIZE =
|
||||
32 + 1 + 4 + 2 + 1 + 64 + PUB_KEY_SIZE;
|
||||
#endif
|
||||
|
||||
static bool readPersistedClientPath(FILESYSTEM* fs,
|
||||
const uint8_t pubkey[PUB_KEY_SIZE],
|
||||
uint8_t* path_len,
|
||||
uint8_t path[MAX_PATH_SIZE]) {
|
||||
if (!fs->exists(mesh::CLIENT_ACL_PRIMARY_PATH)) return false;
|
||||
File file = openRead(fs, mesh::CLIENT_ACL_PRIMARY_PATH);
|
||||
if (!file) return false;
|
||||
|
||||
bool found = false;
|
||||
while (!found) {
|
||||
uint8_t record_pubkey[PUB_KEY_SIZE];
|
||||
uint8_t permissions;
|
||||
uint32_t sync_since;
|
||||
uint8_t version[2];
|
||||
uint8_t record_path_len;
|
||||
uint8_t record_path[MAX_PATH_SIZE];
|
||||
uint8_t shared_secret[PUB_KEY_SIZE];
|
||||
|
||||
bool success = file.read(record_pubkey, sizeof(record_pubkey))
|
||||
== (int)sizeof(record_pubkey);
|
||||
success = success && file.read(&permissions, sizeof(permissions))
|
||||
== (int)sizeof(permissions);
|
||||
success = success
|
||||
&& file.read((uint8_t*)&sync_since, sizeof(sync_since))
|
||||
== (int)sizeof(sync_since);
|
||||
success = success && file.read(version, sizeof(version))
|
||||
== (int)sizeof(version);
|
||||
success = success && file.read(&record_path_len, sizeof(record_path_len))
|
||||
== (int)sizeof(record_path_len);
|
||||
success = success && file.read(record_path, sizeof(record_path))
|
||||
== (int)sizeof(record_path);
|
||||
success = success && file.read(shared_secret, sizeof(shared_secret))
|
||||
== (int)sizeof(shared_secret);
|
||||
if (!success) break;
|
||||
|
||||
if (version[0] >= CONTACT_RECORD_VERSION_ALT_PATH) {
|
||||
uint8_t alt_path_len;
|
||||
uint8_t alt_path[MAX_PATH_SIZE];
|
||||
success = file.read(&alt_path_len, sizeof(alt_path_len))
|
||||
== (int)sizeof(alt_path_len);
|
||||
success = success && file.read(alt_path, sizeof(alt_path))
|
||||
== (int)sizeof(alt_path);
|
||||
if (!success) break;
|
||||
}
|
||||
|
||||
if (memcmp(record_pubkey, pubkey, PUB_KEY_SIZE) == 0
|
||||
&& (record_path_len == OUT_PATH_UNKNOWN
|
||||
|| record_path_len == OUT_PATH_FORCE_FLOOD
|
||||
|| mesh::isValidEncodedClientPathLength(
|
||||
record_path_len, MAX_PATH_SIZE))) {
|
||||
*path_len = record_path_len;
|
||||
memcpy(path, record_path, MAX_PATH_SIZE);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
return found;
|
||||
}
|
||||
|
||||
static mesh::StoredClientPathView storedClientPathForSave(
|
||||
FILESYSTEM* fs,
|
||||
const ClientInfo* client,
|
||||
uint8_t prior_path[MAX_PATH_SIZE],
|
||||
uint8_t* prior_path_len) {
|
||||
const bool prior_exists = !client->out_path_is_persistable
|
||||
&& readPersistedClientPath(
|
||||
fs, client->id.pub_key, prior_path_len, prior_path);
|
||||
return mesh::selectStoredClientPath(
|
||||
client->out_path_is_persistable,
|
||||
client->out_path_len,
|
||||
client->out_path,
|
||||
prior_exists,
|
||||
*prior_path_len,
|
||||
prior_path,
|
||||
OUT_PATH_UNKNOWN,
|
||||
EMPTY_OUT_PATH);
|
||||
}
|
||||
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
|
||||
static File openWrite(FILESYSTEM* _fs, const char* filename) {
|
||||
#if defined(STM32_PLATFORM)
|
||||
_fs->remove(filename);
|
||||
@@ -16,11 +115,337 @@ static File openWrite(FILESYSTEM* _fs, const char* filename) {
|
||||
return _fs->open(filename, "w", true);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool readMatches(File& file, const uint8_t* expected, size_t length) {
|
||||
uint8_t buffer[32];
|
||||
while (length > 0) {
|
||||
const size_t chunk = length < sizeof(buffer) ? length : sizeof(buffer);
|
||||
if (file.read(buffer, chunk) != (int)chunk
|
||||
|| memcmp(buffer, expected, chunk) != 0) {
|
||||
return false;
|
||||
}
|
||||
expected += chunk;
|
||||
length -= chunk;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool validateContactsFileIntegrity(FILESYSTEM* fs,
|
||||
const char* filename) {
|
||||
File file = openRead(fs, filename);
|
||||
if (!file) return false;
|
||||
const size_t size = file.size();
|
||||
const bool has_crc = size >= 8 && (size - 8) % CONTACT_RECORD_SIZE == 0;
|
||||
// A backup means publication was interrupted or is being finalized. In that
|
||||
// state a primary without its CRC trailer is a torn new image, not a legacy
|
||||
// image, even when truncation lands exactly on a record-size multiple.
|
||||
const bool crc_required = strcmp(filename, mesh::CLIENT_ACL_PRIMARY_PATH) == 0
|
||||
&& fs->exists(mesh::CLIENT_ACL_BACKUP_PATH);
|
||||
if (!has_crc && size % CONTACT_RECORD_SIZE != 0
|
||||
&& size % LEGACY_CONTACT_RECORD_SIZE != 0) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
if (!has_crc) {
|
||||
file.close();
|
||||
return !crc_required; // standalone legacy fixed-record image
|
||||
}
|
||||
uint32_t crc = 0xFFFFFFFFUL;
|
||||
size_t remaining = size - 8;
|
||||
uint8_t buffer[32];
|
||||
while (remaining != 0) {
|
||||
const size_t chunk = remaining < sizeof(buffer) ? remaining : sizeof(buffer);
|
||||
if (file.read(buffer, chunk) != (int)chunk) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
crc = mesh::updateClientACLCRC(crc, buffer, chunk);
|
||||
remaining -= chunk;
|
||||
}
|
||||
uint8_t magic[4];
|
||||
uint32_t stored_crc;
|
||||
const bool valid = file.read(magic, sizeof(magic)) == (int)sizeof(magic)
|
||||
&& file.read((uint8_t*)&stored_crc, sizeof(stored_crc))
|
||||
== (int)sizeof(stored_crc)
|
||||
&& memcmp(magic, mesh::CLIENT_ACL_CRC_MAGIC, sizeof(magic)) == 0
|
||||
&& stored_crc == (crc ^ 0xFFFFFFFFUL);
|
||||
file.close();
|
||||
return valid;
|
||||
}
|
||||
|
||||
static bool verifyContactsFile(
|
||||
FILESYSTEM* fs,
|
||||
const char* filename,
|
||||
ClientInfo* clients,
|
||||
int num_clients,
|
||||
bool (*filter)(ClientInfo*),
|
||||
size_t expected_size) {
|
||||
File file = openRead(fs, filename);
|
||||
if (!file || file.size() != expected_size + 8) {
|
||||
if (file) file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t unused[2] = {CONTACT_RECORD_VERSION_ALT_PATH, 0};
|
||||
bool matches = true;
|
||||
for (int i = 0; matches && i < num_clients; i++) {
|
||||
ClientInfo* c = &clients[i];
|
||||
if (!mesh::clientRoleIsPersisted(
|
||||
c->permissions, PERM_ACL_ROLE_MASK, PERM_ACL_GUEST)
|
||||
|| (filter && !filter(c))) continue;
|
||||
uint8_t prior_path_len = OUT_PATH_UNKNOWN;
|
||||
uint8_t prior_path[MAX_PATH_SIZE] = {};
|
||||
const mesh::StoredClientPathView persisted_path =
|
||||
storedClientPathForSave(
|
||||
fs, c, prior_path, &prior_path_len);
|
||||
matches = readMatches(file, c->id.pub_key, 32)
|
||||
&& readMatches(file, &c->permissions, 1)
|
||||
&& readMatches(file, (uint8_t*)&c->extra.room.sync_since, 4)
|
||||
&& readMatches(file, unused, sizeof(unused))
|
||||
&& readMatches(file, &persisted_path.encoded_path_len, 1)
|
||||
&& readMatches(file, persisted_path.path, 64)
|
||||
&& readMatches(file, c->shared_secret, PUB_KEY_SIZE)
|
||||
&& readMatches(file, &c->alt_path_len, 1)
|
||||
&& readMatches(file, c->alt_path, 64);
|
||||
}
|
||||
uint8_t magic[4];
|
||||
uint32_t stored_crc;
|
||||
matches = matches
|
||||
&& file.read(magic, sizeof(magic)) == (int)sizeof(magic)
|
||||
&& file.read((uint8_t*)&stored_crc, sizeof(stored_crc))
|
||||
== (int)sizeof(stored_crc)
|
||||
&& memcmp(magic, mesh::CLIENT_ACL_CRC_MAGIC, sizeof(magic)) == 0;
|
||||
file.close();
|
||||
return matches && validateContactsFileIntegrity(fs, filename);
|
||||
}
|
||||
#endif
|
||||
|
||||
static bool validateLoginReplayFileIntegrity(FILESYSTEM* fs,
|
||||
const char* filename) {
|
||||
File file = openRead(fs, filename);
|
||||
if (!file) return false;
|
||||
const size_t size = file.size();
|
||||
if (size < mesh::CLIENT_LOGIN_REPLAY_TRAILER_SIZE) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
const size_t payload_size =
|
||||
size - mesh::CLIENT_LOGIN_REPLAY_TRAILER_SIZE;
|
||||
const size_t record_count =
|
||||
payload_size / mesh::CLIENT_LOGIN_REPLAY_RECORD_SIZE;
|
||||
if (payload_size % mesh::CLIENT_LOGIN_REPLAY_RECORD_SIZE != 0
|
||||
|| record_count > mesh::MAX_CLIENT_LOGIN_REPLAY_IDENTITIES) {
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t crc = 0xFFFFFFFFUL;
|
||||
bool valid = true;
|
||||
for (size_t i = 0; valid && i < record_count; i++) {
|
||||
uint8_t pubkey[PUB_KEY_SIZE];
|
||||
uint32_t ceiling;
|
||||
valid = file.read(pubkey, sizeof(pubkey)) == (int)sizeof(pubkey)
|
||||
&& file.read((uint8_t*)&ceiling, sizeof(ceiling))
|
||||
== (int)sizeof(ceiling)
|
||||
&& ceiling != 0;
|
||||
if (valid) {
|
||||
crc = mesh::updateClientLoginReplayCRC(
|
||||
crc, pubkey, sizeof(pubkey));
|
||||
crc = mesh::updateClientLoginReplayCRC(
|
||||
crc, (const uint8_t*)&ceiling, sizeof(ceiling));
|
||||
}
|
||||
}
|
||||
uint8_t magic[sizeof(mesh::CLIENT_LOGIN_REPLAY_MAGIC)];
|
||||
uint32_t stored_crc;
|
||||
valid = valid
|
||||
&& file.read(magic, sizeof(magic)) == (int)sizeof(magic)
|
||||
&& file.read((uint8_t*)&stored_crc, sizeof(stored_crc))
|
||||
== (int)sizeof(stored_crc)
|
||||
&& memcmp(magic, mesh::CLIENT_LOGIN_REPLAY_MAGIC,
|
||||
sizeof(magic)) == 0
|
||||
&& stored_crc == (crc ^ 0xFFFFFFFFUL);
|
||||
file.close();
|
||||
return valid;
|
||||
}
|
||||
|
||||
// The image is validated before this scan. Choose the maximum if a legacy or
|
||||
// manually recovered image somehow contains a duplicate identity; taking the
|
||||
// strongest boundary remains fail-safe.
|
||||
static bool readClientLoginReplayCeiling(
|
||||
FILESYSTEM* fs,
|
||||
const uint8_t pubkey[PUB_KEY_SIZE],
|
||||
uint32_t* ceiling,
|
||||
bool* found) {
|
||||
*ceiling = 0;
|
||||
*found = false;
|
||||
if (!fs->exists(mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH)) return true;
|
||||
if (!validateLoginReplayFileIntegrity(
|
||||
fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH)) {
|
||||
return false;
|
||||
}
|
||||
File file = openRead(fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
|
||||
if (!file) return false;
|
||||
const size_t record_count =
|
||||
(file.size() - mesh::CLIENT_LOGIN_REPLAY_TRAILER_SIZE)
|
||||
/ mesh::CLIENT_LOGIN_REPLAY_RECORD_SIZE;
|
||||
bool success = true;
|
||||
for (size_t i = 0; success && i < record_count; i++) {
|
||||
uint8_t record_pubkey[PUB_KEY_SIZE];
|
||||
uint32_t record_ceiling;
|
||||
success = file.read(record_pubkey, sizeof(record_pubkey))
|
||||
== (int)sizeof(record_pubkey);
|
||||
success = success
|
||||
&& file.read((uint8_t*)&record_ceiling, sizeof(record_ceiling))
|
||||
== (int)sizeof(record_ceiling);
|
||||
if (success && memcmp(record_pubkey, pubkey, PUB_KEY_SIZE) == 0
|
||||
&& (!*found || record_ceiling > *ceiling)) {
|
||||
*ceiling = record_ceiling;
|
||||
*found = true;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
return success;
|
||||
}
|
||||
|
||||
template <typename Writer>
|
||||
static bool writeClientLoginReplayRecord(
|
||||
Writer& writer,
|
||||
const uint8_t pubkey[PUB_KEY_SIZE],
|
||||
uint32_t ceiling,
|
||||
uint32_t* crc) {
|
||||
const bool success = writer.write(pubkey, PUB_KEY_SIZE) == PUB_KEY_SIZE
|
||||
&& writer.write((const uint8_t*)&ceiling, sizeof(ceiling))
|
||||
== sizeof(ceiling);
|
||||
if (success) {
|
||||
*crc = mesh::updateClientLoginReplayCRC(
|
||||
*crc, pubkey, PUB_KEY_SIZE);
|
||||
*crc = mesh::updateClientLoginReplayCRC(
|
||||
*crc, (const uint8_t*)&ceiling, sizeof(ceiling));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
static bool writeClientLoginReplayCeiling(
|
||||
FILESYSTEM* fs,
|
||||
const uint8_t pubkey[PUB_KEY_SIZE],
|
||||
uint32_t new_ceiling,
|
||||
mesh::ClientLoginReplayReservationAction action) {
|
||||
if (action == mesh::ClientLoginReplayReservationAction::None) return true;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (fs->exists(mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH)
|
||||
&& !validateLoginReplayFileIntegrity(
|
||||
fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH)) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!mesh::recoverClientLoginReplayFiles(
|
||||
fs, validateLoginReplayFileIntegrity)) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
File source = openRead(fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
|
||||
const size_t record_count = source
|
||||
? (source.size() - mesh::CLIENT_LOGIN_REPLAY_TRAILER_SIZE)
|
||||
/ mesh::CLIENT_LOGIN_REPLAY_RECORD_SIZE
|
||||
: 0;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
mesh::AtomicFileWriter destination(
|
||||
fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
|
||||
#else
|
||||
File destination = openWrite(
|
||||
fs, mesh::CLIENT_LOGIN_REPLAY_TEMP_PATH);
|
||||
#endif
|
||||
if (!destination) {
|
||||
if (source) source.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t crc = 0xFFFFFFFFUL;
|
||||
bool found = false;
|
||||
bool success = true;
|
||||
for (size_t i = 0; success && i < record_count; i++) {
|
||||
uint8_t record_pubkey[PUB_KEY_SIZE];
|
||||
uint32_t record_ceiling;
|
||||
success = source.read(record_pubkey, sizeof(record_pubkey))
|
||||
== (int)sizeof(record_pubkey);
|
||||
success = success
|
||||
&& source.read((uint8_t*)&record_ceiling, sizeof(record_ceiling))
|
||||
== (int)sizeof(record_ceiling);
|
||||
if (!success) break;
|
||||
if (memcmp(record_pubkey, pubkey, PUB_KEY_SIZE) == 0) {
|
||||
found = true;
|
||||
if (record_ceiling < new_ceiling) record_ceiling = new_ceiling;
|
||||
}
|
||||
success = writeClientLoginReplayRecord(
|
||||
destination, record_pubkey, record_ceiling, &crc);
|
||||
}
|
||||
if (source) source.close();
|
||||
|
||||
if (success
|
||||
&& action == mesh::ClientLoginReplayReservationAction::UpdateExisting
|
||||
&& !found) {
|
||||
// Do not turn a low-trust update into insertion if the image changed
|
||||
// between its admission read and this transaction.
|
||||
success = false;
|
||||
}
|
||||
if (success && !found
|
||||
&& action == mesh::ClientLoginReplayReservationAction::CreateNew) {
|
||||
success = mesh::clientLoginReplayCanInsert(record_count);
|
||||
}
|
||||
if (success && !found) {
|
||||
success = writeClientLoginReplayRecord(
|
||||
destination, pubkey, new_ceiling, &crc);
|
||||
}
|
||||
if (success) {
|
||||
const uint32_t final_crc = crc ^ 0xFFFFFFFFUL;
|
||||
success = destination.write(
|
||||
mesh::CLIENT_LOGIN_REPLAY_MAGIC,
|
||||
sizeof(mesh::CLIENT_LOGIN_REPLAY_MAGIC))
|
||||
== sizeof(mesh::CLIENT_LOGIN_REPLAY_MAGIC)
|
||||
&& destination.write(
|
||||
(const uint8_t*)&final_crc, sizeof(final_crc))
|
||||
== sizeof(final_crc);
|
||||
}
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
return destination.commit(success);
|
||||
#else
|
||||
destination.close();
|
||||
const bool verified = success && validateLoginReplayFileIntegrity(
|
||||
fs, mesh::CLIENT_LOGIN_REPLAY_TEMP_PATH);
|
||||
return mesh::publishClientLoginReplayTemp(
|
||||
fs, verified, validateLoginReplayFileIntegrity);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
|
||||
_fs = fs;
|
||||
num_clients = 0;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// AtomicFileWriter may leave only a harmless temp image when reset before
|
||||
// rename. The live image remains authoritative.
|
||||
mesh::removeClientLoginReplayArtifact(
|
||||
_fs, mesh::CLIENT_LOGIN_REPLAY_TEMP_PATH);
|
||||
login_replay_store_available =
|
||||
!_fs->exists(mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH)
|
||||
|| validateLoginReplayFileIntegrity(
|
||||
_fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
|
||||
#else
|
||||
login_replay_store_available = mesh::recoverClientLoginReplayFiles(
|
||||
_fs, validateLoginReplayFileIntegrity);
|
||||
#endif
|
||||
if (!login_replay_store_available) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"ERROR: ClientACL::load could not recover login replay state");
|
||||
}
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
if (!mesh::recoverClientACLFilesVerified(
|
||||
_fs, validateContactsFileIntegrity)) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: ClientACL::load could not recover contacts files");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (_fs->exists("/s_contacts")) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = _fs->open("/s_contacts", "r");
|
||||
@@ -52,6 +477,22 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
|
||||
if (!success) break; // EOF
|
||||
|
||||
c.id = mesh::Identity(pub_key);
|
||||
c.out_path_is_persistable = true;
|
||||
if (login_replay_store_available) {
|
||||
uint32_t replay_ceiling;
|
||||
bool replay_entry_found;
|
||||
if (!readClientLoginReplayCeiling(
|
||||
_fs, pub_key, &replay_ceiling, &replay_entry_found)) {
|
||||
login_replay_store_available = false;
|
||||
c.last_timestamp = UINT32_MAX;
|
||||
} else if (replay_entry_found) {
|
||||
// On boot, skip the unused reservation tail. During this boot a
|
||||
// successful login replaces this with its exact accepted value.
|
||||
c.last_timestamp = replay_ceiling;
|
||||
}
|
||||
} else {
|
||||
c.last_timestamp = UINT32_MAX;
|
||||
}
|
||||
self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate shared secrets in case our private key changed
|
||||
if (num_clients < MAX_CLIENTS) {
|
||||
clients[num_clients++] = c;
|
||||
@@ -64,43 +505,146 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
|
||||
}
|
||||
}
|
||||
|
||||
void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
|
||||
bool ClientACL::authorizeLoginTimestamp(
|
||||
const uint8_t* pubkey,
|
||||
uint32_t sender_timestamp,
|
||||
uint32_t runtime_last_timestamp,
|
||||
uint8_t login_permissions) {
|
||||
if (_fs == NULL || pubkey == NULL || !login_replay_store_available) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t persisted_ceiling;
|
||||
bool replay_entry_found;
|
||||
if (!readClientLoginReplayCeiling(
|
||||
_fs, pubkey, &persisted_ceiling, &replay_entry_found)) {
|
||||
login_replay_store_available = false;
|
||||
return false;
|
||||
}
|
||||
if (!replay_entry_found) persisted_ceiling = 0;
|
||||
|
||||
const bool role_needs_durable_replay =
|
||||
mesh::clientLoginRoleNeedsDurableReplay(
|
||||
login_permissions, PERM_ACL_ROLE_MASK,
|
||||
PERM_ACL_GUEST, PERM_ACL_READ_ONLY);
|
||||
const mesh::ClientLoginReplayPlan plan = mesh::planClientLoginReplay(
|
||||
runtime_last_timestamp, persisted_ceiling, sender_timestamp);
|
||||
if (!plan.fresh) return false;
|
||||
const mesh::ClientLoginReplayReservationAction action =
|
||||
mesh::selectClientLoginReplayReservation(
|
||||
plan.reservation_needed, replay_entry_found,
|
||||
role_needs_durable_replay);
|
||||
if (action != mesh::ClientLoginReplayReservationAction::None
|
||||
&& !writeClientLoginReplayCeiling(
|
||||
_fs, pubkey, plan.reservation_ceiling, action)) {
|
||||
// The old valid image remains usable after an ordinary write failure, but
|
||||
// this login is not safe to accept until its new boundary is durable.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) {
|
||||
_fs = fs;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
mesh::AtomicFileWriter file(_fs, "/s_contacts");
|
||||
#else
|
||||
File file = openWrite(_fs, "/s_contacts");
|
||||
#endif
|
||||
if (file) {
|
||||
uint8_t unused[2];
|
||||
unused[0] = CONTACT_RECORD_VERSION_ALT_PATH;
|
||||
unused[1] = 0;
|
||||
|
||||
bool success = true;
|
||||
for (int i = 0; success && i < num_clients; i++) {
|
||||
auto c = &clients[i];
|
||||
if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function
|
||||
|
||||
success = (file.write(c->id.pub_key, 32) == 32);
|
||||
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
|
||||
success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4);
|
||||
success = success && (file.write(unused, 2) == 2);
|
||||
success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1);
|
||||
success = success && (file.write(c->out_path, 64) == 64);
|
||||
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
|
||||
success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1);
|
||||
success = success && (file.write(c->alt_path, 64) == 64);
|
||||
|
||||
if (!success) break; // write failed
|
||||
}
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (!file.commit(success)) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: ClientACL::save atomic commit failed");
|
||||
}
|
||||
#else
|
||||
file.close();
|
||||
#endif
|
||||
if (!mesh::recoverClientACLFilesVerified(
|
||||
_fs, validateContactsFileIntegrity)) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: ClientACL::save recovery is incomplete");
|
||||
return false;
|
||||
}
|
||||
File file = openWrite(_fs, mesh::CLIENT_ACL_TEMP_PATH);
|
||||
#endif
|
||||
if (!file) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: ClientACL::save could not open contacts file");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t unused[2];
|
||||
unused[0] = CONTACT_RECORD_VERSION_ALT_PATH;
|
||||
unused[1] = 0;
|
||||
|
||||
bool success = true;
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
size_t expected_size = 0;
|
||||
uint32_t contacts_crc = 0xFFFFFFFFUL;
|
||||
#endif
|
||||
for (int i = 0; success && i < num_clients; i++) {
|
||||
auto c = &clients[i];
|
||||
if (!mesh::clientRoleIsPersisted(
|
||||
c->permissions, PERM_ACL_ROLE_MASK, PERM_ACL_GUEST)
|
||||
|| (filter && !filter(c))) continue; // skip guests/deleted entries or filtered clients
|
||||
|
||||
success = (file.write(c->id.pub_key, 32) == 32);
|
||||
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
|
||||
success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4);
|
||||
success = success && (file.write(unused, 2) == 2);
|
||||
uint8_t prior_path_len = OUT_PATH_UNKNOWN;
|
||||
uint8_t prior_path[MAX_PATH_SIZE] = {};
|
||||
const mesh::StoredClientPathView persisted_path =
|
||||
storedClientPathForSave(
|
||||
_fs, c, prior_path, &prior_path_len);
|
||||
success = success
|
||||
&& (file.write(&persisted_path.encoded_path_len, 1) == 1);
|
||||
success = success
|
||||
&& (file.write(persisted_path.path, MAX_PATH_SIZE) == MAX_PATH_SIZE);
|
||||
success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE);
|
||||
success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1);
|
||||
success = success && (file.write(c->alt_path, 64) == 64);
|
||||
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
if (success) {
|
||||
expected_size += 32 + 1 + 4 + 2 + 1 + 64 + PUB_KEY_SIZE + 1 + 64;
|
||||
contacts_crc = mesh::updateClientACLCRC(contacts_crc, c->id.pub_key, 32);
|
||||
contacts_crc = mesh::updateClientACLCRC(contacts_crc, &c->permissions, 1);
|
||||
contacts_crc = mesh::updateClientACLCRC(
|
||||
contacts_crc, (uint8_t*)&c->extra.room.sync_since, 4);
|
||||
contacts_crc = mesh::updateClientACLCRC(contacts_crc, unused, sizeof(unused));
|
||||
contacts_crc = mesh::updateClientACLCRC(
|
||||
contacts_crc, &persisted_path.encoded_path_len, 1);
|
||||
contacts_crc = mesh::updateClientACLCRC(
|
||||
contacts_crc, persisted_path.path, MAX_PATH_SIZE);
|
||||
contacts_crc = mesh::updateClientACLCRC(contacts_crc, c->shared_secret, PUB_KEY_SIZE);
|
||||
contacts_crc = mesh::updateClientACLCRC(contacts_crc, &c->alt_path_len, 1);
|
||||
contacts_crc = mesh::updateClientACLCRC(contacts_crc, c->alt_path, 64);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if !defined(NRF52_PLATFORM)
|
||||
if (success) {
|
||||
const uint32_t final_crc = contacts_crc ^ 0xFFFFFFFFUL;
|
||||
success = file.write(mesh::CLIENT_ACL_CRC_MAGIC,
|
||||
sizeof(mesh::CLIENT_ACL_CRC_MAGIC))
|
||||
== sizeof(mesh::CLIENT_ACL_CRC_MAGIC)
|
||||
&& file.write((const uint8_t*)&final_crc, sizeof(final_crc))
|
||||
== sizeof(final_crc);
|
||||
}
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM)
|
||||
success = file.commit(success);
|
||||
if (!success) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: ClientACL::save atomic commit failed");
|
||||
}
|
||||
#else
|
||||
file.close();
|
||||
if (success) {
|
||||
success = verifyContactsFile(
|
||||
_fs, mesh::CLIENT_ACL_TEMP_PATH, clients, num_clients,
|
||||
filter, expected_size);
|
||||
}
|
||||
if (!success) {
|
||||
mesh::publishVerifiedClientACLTemp(_fs, false);
|
||||
MESH_DEBUG_PRINTLN("ERROR: ClientACL::save close/readback failed");
|
||||
return false;
|
||||
}
|
||||
success = mesh::publishVerifiedClientACLTemp(
|
||||
_fs, true, validateContactsFileIntegrity);
|
||||
if (!success) {
|
||||
MESH_DEBUG_PRINTLN("ERROR: ClientACL::save atomic publish failed");
|
||||
}
|
||||
#endif
|
||||
return success;
|
||||
}
|
||||
|
||||
bool ClientACL::clear() {
|
||||
@@ -108,14 +652,14 @@ bool ClientACL::clear() {
|
||||
if (_fs->exists("/s_contacts")) {
|
||||
_fs->remove("/s_contacts");
|
||||
}
|
||||
#if defined(NRF52_PLATFORM)
|
||||
if (_fs->exists("/s_contacts.tmp")) {
|
||||
_fs->remove("/s_contacts.tmp");
|
||||
}
|
||||
#endif
|
||||
if (_fs->exists("/s_contacts.tmp")) _fs->remove("/s_contacts.tmp");
|
||||
if (_fs->exists("/s_contacts.bak")) _fs->remove("/s_contacts.bak");
|
||||
const bool files_cleared = !_fs->exists("/s_contacts")
|
||||
&& !_fs->exists("/s_contacts.tmp")
|
||||
&& !_fs->exists("/s_contacts.bak");
|
||||
memset(clients, 0, sizeof(clients));
|
||||
num_clients = 0;
|
||||
return true;
|
||||
return files_cleared;
|
||||
}
|
||||
|
||||
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
|
||||
@@ -148,6 +692,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
c->permissions = init_perms;
|
||||
c->id = id;
|
||||
c->out_path_len = OUT_PATH_UNKNOWN;
|
||||
c->out_path_is_persistable = true;
|
||||
c->alt_path_len = OUT_PATH_UNKNOWN;
|
||||
return c;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user