Files
MeshCore/src/helpers/ClientACL.cpp
T

960 lines
35 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,
bool* metadata_ok = nullptr) {
#if defined(ESP32_PLATFORM)
bool present = false;
if (!mesh::filePresence(fs, filename, present)) {
if (metadata_ok) *metadata_ok = false;
return mesh::emptyFile(fs);
}
if (!present) return mesh::emptyFile(fs);
File file = fs->open(filename);
if (!file && metadata_ok) *metadata_ok = false;
if (file && file.isDirectory()) file.close();
return file;
#else
(void)metadata_ok;
return mesh::openFileRead(fs, 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],
bool* metadata_ok = nullptr) {
#if !defined(ESP32_PLATFORM)
if (!fs->exists(mesh::CLIENT_ACL_PRIMARY_PATH)) return false;
#endif
File file = openRead(fs, mesh::CLIENT_ACL_PRIMARY_PATH, metadata_ok);
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,
bool* metadata_ok = nullptr) {
const bool prior_exists = !client->out_path_is_persistable
&& readPersistedClientPath(
fs, client->id.pub_key, prior_path_len, prior_path, metadata_ok);
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 mesh::ClientACLFileValidation validateContactsFileIntegrityChecked(
FILESYSTEM* fs, const char* filename) {
bool metadata_ok = true;
File file = openRead(fs, filename, &metadata_ok);
if (!metadata_ok) return mesh::ClientACLFileValidation::MetadataError;
if (!file) return mesh::ClientACLFileValidation::Invalid;
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.
bool crc_required = false;
if (strcmp(filename, mesh::CLIENT_ACL_PRIMARY_PATH) == 0
&& !mesh::clientACLFilePresence(fs, mesh::CLIENT_ACL_BACKUP_PATH,
crc_required)) {
file.close();
return mesh::ClientACLFileValidation::MetadataError;
}
if (!has_crc && size % CONTACT_RECORD_SIZE != 0
&& size % LEGACY_CONTACT_RECORD_SIZE != 0) {
file.close();
return mesh::ClientACLFileValidation::Invalid;
}
if (!has_crc) {
file.close();
return mesh::clientACLValidation(!crc_required); // standalone legacy 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 mesh::ClientACLFileValidation::Invalid;
}
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 mesh::clientACLValidation(valid);
}
static bool validateContactsFileIntegrity(FILESYSTEM* fs, const char* filename) {
return validateContactsFileIntegrityChecked(fs, filename)
== mesh::ClientACLFileValidation::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] = {};
bool metadata_ok = true;
const mesh::StoredClientPathView persisted_path =
storedClientPathForSave(
fs, c, prior_path, &prior_path_len, &metadata_ok);
matches = metadata_ok && 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, validateContactsFileIntegrityChecked)) {
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, validateContactsFileIntegrityChecked)) {
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] = {};
bool metadata_ok = true;
const mesh::StoredClientPathView persisted_path =
storedClientPathForSave(
_fs, c, prior_path, &prior_path_len, &metadata_ok);
success = success && metadata_ok
&& (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, validateContactsFileIntegrityChecked);
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;
}