Files
MeshCore/src/helpers/ClientACL.cpp
T
mikecarper b847b46ef7 Retry identity startup and harden replay state and permissions
Read identities up to three times before allowing replacement, publish only complete reads, and require new startup keys to be saved with bounded retries.

Use authoritative replay-file metadata throughout persistence and clock recovery. Validate setperm input before narrowing or mutating roles, and add production-path regression coverage.
2026-09-15 17:33:50 -07:00

927 lines
34 KiB
C++

#include "ClientACL.h"
#include "ClientACLFileTransaction.h"
#include "ClientACLFileIntegrity.h"
#include "ClientLoginPersistence.h"
#include "ClientPathPersistence.h"
#include "FileRead.h"
#include "FilePresence.h"
#include <initializer_list>
#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) {
return mesh::openFileRead(fs, filename);
}
#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 (_fs == NULL) return mesh::emptyFile(_fs);
#if defined(STM32_PLATFORM)
_fs->remove(filename);
File file = _fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
File file = _fs->open(filename, "w");
#else
File file = _fs->open(filename, "w", true);
#endif
if (file && file.isDirectory()) file.close();
return file;
}
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 loginReplayRecordCount(File& file, size_t* count) {
if (!file || file.isDirectory()) return false;
const size_t size = file.size();
if (size < mesh::CLIENT_LOGIN_REPLAY_TRAILER_SIZE) return false;
const size_t payload_size =
size - mesh::CLIENT_LOGIN_REPLAY_TRAILER_SIZE;
if (payload_size % mesh::CLIENT_LOGIN_REPLAY_RECORD_SIZE != 0
|| payload_size / mesh::CLIENT_LOGIN_REPLAY_RECORD_SIZE
> mesh::MAX_CLIENT_LOGIN_REPLAY_IDENTITIES) return false;
*count = payload_size / mesh::CLIENT_LOGIN_REPLAY_RECORD_SIZE;
return true;
}
static bool validateLoginReplayFileIntegrity(FILESYSTEM* fs,
const char* filename) {
File file = openRead(fs, filename);
size_t record_count = 0;
if (!loginReplayRecordCount(file, &record_count)) {
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;
bool present = false;
if (!mesh::filePresence(fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH, present)) return false;
if (!present) return true;
if (!validateLoginReplayFileIntegrity(
fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH)) {
return false;
}
File file = openRead(fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
size_t record_count = 0;
if (!loginReplayRecordCount(file, &record_count)) {
file.close();
return 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 = 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)
bool primary_exists = false;
if (!mesh::filePresence(fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH, primary_exists)) return false;
if (primary_exists
&& !validateLoginReplayFileIntegrity(
fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH)) {
return false;
}
#else
if (!mesh::recoverClientLoginReplayFiles(
fs, validateLoginReplayFileIntegrity)) {
return false;
}
#endif
File source = mesh::emptyFile(fs);
size_t record_count = 0;
bool source_exists = false;
if (!mesh::filePresence(fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH, source_exists)) return false;
if (source_exists) {
source = openRead(fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
// A previously present image becoming unreadable is not a first login.
// Check again after opening so disappearance/truncation cannot underflow
// the trailer subtraction or replace historical replay boundaries.
if (!loginReplayRecordCount(source, &record_count)) {
source.close();
return false;
}
}
#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
}
// Scan the complete source, including its CRC, even when no record is selected.
// The optional destination preserves every record in its original order. This
// also handles duplicate historical identities without dropping tombstones.
template <typename Writer>
static bool copyClampedLoginReplay(
File& source, Writer* destination, const uint8_t* selected_pubkey,
uint32_t now, ClientLoginReplayClampResult& result, uint32_t& source_crc) {
size_t record_count = 0;
if (!loginReplayRecordCount(source, &record_count)) return false;
uint32_t original_crc = 0xFFFFFFFFUL;
uint32_t output_crc = 0xFFFFFFFFUL;
for (size_t i = 0; i < record_count; i++) {
uint8_t pubkey[PUB_KEY_SIZE];
uint32_t ceiling;
if (source.read(pubkey, sizeof(pubkey)) != (int)sizeof(pubkey)
|| source.read((uint8_t*)&ceiling, sizeof(ceiling))
!= (int)sizeof(ceiling)
|| ceiling == 0) return false;
original_crc = mesh::updateClientLoginReplayCRC(
original_crc, pubkey, sizeof(pubkey));
original_crc = mesh::updateClientLoginReplayCRC(
original_crc, (const uint8_t*)&ceiling, sizeof(ceiling));
if (selected_pubkey == NULL
|| memcmp(pubkey, selected_pubkey, PUB_KEY_SIZE) == 0) {
result.stored_matched++;
if (ceiling > now) {
ceiling = now;
result.stored_changed++;
}
}
if (destination != NULL && !writeClientLoginReplayRecord(
*destination, pubkey, ceiling, &output_crc)) return false;
}
uint8_t magic[sizeof(mesh::CLIENT_LOGIN_REPLAY_MAGIC)];
uint32_t stored_crc;
if (source.read(magic, sizeof(magic)) != (int)sizeof(magic)
|| source.read((uint8_t*)&stored_crc, sizeof(stored_crc))
!= (int)sizeof(stored_crc)
|| memcmp(magic, mesh::CLIENT_LOGIN_REPLAY_MAGIC, sizeof(magic)) != 0
|| stored_crc != (original_crc ^ 0xFFFFFFFFUL)) return false;
source_crc = stored_crc;
if (destination == NULL) return true;
const uint32_t final_crc = output_crc ^ 0xFFFFFFFFUL;
return destination->write(magic, sizeof(magic)) == sizeof(magic)
&& destination->write((const uint8_t*)&final_crc, sizeof(final_crc))
== sizeof(final_crc);
}
bool ClientACL::clampLoginReplayTimestamps(
const uint8_t* pubkey, uint32_t now,
ClientLoginReplayClampResult& result) {
result = {};
if (_fs == NULL || !login_replay_store_available || now == 0) return false;
ClientLoginReplayClampResult pending = {};
bool primary = false, backup = false, temp = false;
if (!mesh::filePresence(_fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH, primary)
|| !mesh::filePresence(_fs, mesh::CLIENT_LOGIN_REPLAY_BACKUP_PATH, backup)
|| !mesh::filePresence(_fs, mesh::CLIENT_LOGIN_REPLAY_TEMP_PATH, temp)) return false;
if (primary) {
File source = openRead(_fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
uint32_t original_crc = 0;
const bool valid = copyClampedLoginReplay(
source, (File*)NULL, pubkey, now, pending, original_crc);
source.close();
if (!valid) {
login_replay_store_available = false;
return false;
}
if (pending.stored_changed != 0) {
#if !defined(NRF52_PLATFORM)
// A validated live image is authoritative. Only discard a stale backup
// after validation, and never enter publication with an uncleared one.
if (!mesh::removeClientLoginReplayArtifact(
_fs, mesh::CLIENT_LOGIN_REPLAY_BACKUP_PATH)) return false;
#endif
source = openRead(_fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
#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) {
source.close();
return false;
}
ClientLoginReplayClampResult copied = {};
uint32_t copied_crc = 0;
const bool copied_ok = copyClampedLoginReplay(
source, &destination, pubkey, now, copied, copied_crc)
&& copied_crc == original_crc
&& copied.stored_matched == pending.stored_matched
&& copied.stored_changed == pending.stored_changed;
source.close();
#if defined(NRF52_PLATFORM)
const bool published = destination.commit(copied_ok);
#else
destination.close();
const bool verified = copied_ok && validateLoginReplayFileIntegrity(
_fs, mesh::CLIENT_LOGIN_REPLAY_TEMP_PATH);
const bool published = mesh::publishClientLoginReplayTemp(
_fs, verified, validateLoginReplayFileIntegrity);
#endif
if (!published) {
// Ordinary failed writes retain the old store. If rollback or a read
// also failed, do not let a missing/corrupt image become a first login.
File retained = openRead(_fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH);
ClientLoginReplayClampResult ignored = {};
uint32_t retained_crc = 0;
const bool retained_ok = copyClampedLoginReplay(
retained, (File*)NULL, pubkey, now, ignored, retained_crc)
&& retained_crc == original_crc;
retained.close();
if (!retained_ok) login_replay_store_available = false;
return false;
}
}
} else if (backup || temp) {
// Recovery is performed by load(). Do not mistake an interrupted/corrupt
// transaction for a never-created store during this explicit operation.
login_replay_store_available = false;
return false;
}
for (int i = 0; i < num_clients; i++) {
ClientInfo& client = clients[i];
if (pubkey != NULL && memcmp(client.id.pub_key, pubkey, PUB_KEY_SIZE) != 0)
continue;
pending.live_matched++;
if (client.last_timestamp > now) {
client.last_timestamp = now;
pending.live_changed++;
}
}
result = pending;
return true;
}
void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
_fs = fs;
num_clients = 0;
login_replay_store_available = false;
acl_load_complete = false;
if (_fs == NULL) return;
bool primary_exists = false, backup_exists = false;
if (!mesh::filePresence(_fs, mesh::CLIENT_ACL_PRIMARY_PATH, primary_exists)
|| !mesh::filePresence(_fs, mesh::CLIENT_ACL_BACKUP_PATH, backup_exists)) {
return;
}
const bool durable_acl_expected = primary_exists || backup_exists;
#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);
bool replay_exists = false;
login_replay_store_available =
mesh::filePresence(_fs, mesh::CLIENT_LOGIN_REPLAY_PRIMARY_PATH, replay_exists)
&& (!replay_exists || 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 (!mesh::filePresence(_fs, mesh::CLIENT_ACL_PRIMARY_PATH, primary_exists)
|| (durable_acl_expected && !primary_exists)) return;
if (primary_exists) {
File file = openRead(_fs, "/s_contacts");
if (file) {
size_t remaining = file.size();
#if !defined(NRF52_PLATFORM)
// Versioned SPIFFS images finish with a verified CRC trailer, not a
// partially readable extra contact. The data pass must consume every
// byte before it, even if the earlier integrity pass succeeded.
if (remaining >= 8 && (remaining - 8) % CONTACT_RECORD_SIZE == 0) {
remaining -= 8;
}
#endif
bool complete = true;
while (remaining != 0) {
ClientInfo c;
uint8_t pub_key[32];
uint8_t unused[2];
memset(&c, 0, sizeof(c));
c.alt_path_len = OUT_PATH_UNKNOWN;
c.observed_path_len = OUT_PATH_UNKNOWN;
static const size_t base_size = 32 + 1 + 4 + 2 + 1 + 64 + PUB_KEY_SIZE;
if (remaining < base_size || num_clients >= capacity) {
complete = false;
break;
}
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
success = success && (file.read((uint8_t *) &c.extra.room.sync_since, 4) == 4);
success = success && (file.read(unused, 2) == 2);
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.read(c.out_path, 64) == 64);
success = success && (file.read(c.shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); // will be recalculated below
size_t record_size = base_size;
if (success && unused[0] > CONTACT_RECORD_VERSION_ALT_PATH) success = false;
if (success && unused[0] == CONTACT_RECORD_VERSION_ALT_PATH) {
record_size += 1 + 64;
success = remaining >= record_size;
success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1);
success = success && (file.read(c.alt_path, 64) == 64);
}
if (!success) {
complete = false;
break;
}
remaining -= record_size;
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
clients[num_clients++] = c;
}
file.close();
if (!complete) {
// A prefix is not an authoritative ACL. Keep flash untouched and
// refuse admission/mutations until an explicit reload or reboot can
// read the complete image; never save the prefix over its source.
num_clients = 0;
return;
}
} else {
return;
}
}
acl_load_complete = true;
}
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
|| !acl_load_complete) {
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*)) {
// A failed allocation is not an empty ACL. Preserve the stored managers.
if (capacity == 0 || fs == NULL || !acl_load_complete) return false;
_fs = fs;
#if defined(NRF52_PLATFORM)
mesh::AtomicFileWriter file(_fs, "/s_contacts");
#else
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() {
if (!_fs) return false; // no filesystem, nothing to clear
for (const char* path : {mesh::CLIENT_ACL_PRIMARY_PATH,
mesh::CLIENT_ACL_TEMP_PATH,
mesh::CLIENT_ACL_BACKUP_PATH}) {
bool present = false;
if (!mesh::filePresence(_fs, path, present)) return false;
if (present && !_fs->remove(path)) return false;
if (!mesh::filePresence(_fs, path, present) || present) return false;
}
if (clients) memset(clients, 0, sizeof(ClientInfo) * (size_t)capacity);
num_clients = 0;
acl_load_complete = true;
return true;
}
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return NULL;
for (int i = 0; i < num_clients; i++) {
if (memcmp(pubkey, clients[i].id.pub_key, key_len) == 0) return &clients[i]; // already known
}
return NULL; // not found
}
ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
if (!acl_load_complete) return NULL;
uint32_t min_time = 0xFFFFFFFF;
ClientInfo* oldest = NULL;
for (int i = 0; i < num_clients; i++) {
if (id.matches(clients[i].id)) return &clients[i]; // already known
if (!clients[i].isProtectedManager() && clients[i].last_activity < min_time) {
oldest = &clients[i];
min_time = oldest->last_activity;
}
}
ClientInfo* c;
if (num_clients < capacity) {
c = &clients[num_clients++];
} else {
if (oldest == NULL) return NULL; // every entry is protected
c = oldest; // evict least active contact
}
memset(c, 0, sizeof(*c));
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;
c->observed_path_len = OUT_PATH_UNKNOWN;
return c;
}
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
if (!acl_load_complete) return false;
if (pubkey == NULL || key_len <= 0 || key_len > PUB_KEY_SIZE) return false;
ClientInfo* c;
if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts
c = getClient(pubkey, key_len);
if (c == NULL) return false; // partial pubkey not found
num_clients--; // delete from contacts[]
int i = c - clients;
while (i < num_clients) {
clients[i] = clients[i + 1];
i++;
}
} else {
if (key_len != PUB_KEY_SIZE) return false; // need complete pubkey when adding/modifying
mesh::Identity id(pubkey);
c = putClient(id, 0);
if (c == NULL) return false;
c->permissions = perms; // update their permissions
self_id.calcSharedSecret(c->shared_secret, pubkey);
}
return true;
}