#include "ClientACL.h" #include "ClientACLFileTransaction.h" #include "ClientACLFileIntegrity.h" #include "ClientLoginPersistence.h" #include "ClientPathPersistence.h" #include "FileRead.h" #include "FilePresence.h" #include #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 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 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; }