#include "ClientACL.h" #if defined(NRF52_PLATFORM) #include "AtomicFileWriter.h" #endif static const uint8_t CONTACT_RECORD_VERSION_ALT_PATH = 1; #if !defined(NRF52_PLATFORM) static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(STM32_PLATFORM) _fs->remove(filename); return _fs->open(filename, FILE_O_WRITE); #elif defined(RP2040_PLATFORM) return _fs->open(filename, "w"); #else return _fs->open(filename, "w", true); #endif } #endif void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) { _fs = fs; num_clients = 0; if (_fs->exists("/s_contacts")) { #if defined(RP2040_PLATFORM) File file = _fs->open("/s_contacts", "r"); #else File file = _fs->open("/s_contacts"); #endif if (file) { bool full = false; while (!full) { ClientInfo c; uint8_t pub_key[32]; uint8_t unused[2]; memset(&c, 0, sizeof(c)); c.alt_path_len = OUT_PATH_UNKNOWN; 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 if (success && unused[0] >= CONTACT_RECORD_VERSION_ALT_PATH) { success = success && (file.read((uint8_t *)&c.alt_path_len, 1) == 1); success = success && (file.read(c.alt_path, 64) == 64); } if (!success) break; // EOF c.id = mesh::Identity(pub_key); self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate shared secrets in case our private key changed if (num_clients < MAX_CLIENTS) { clients[num_clients++] = c; } else { full = true; } } file.close(); } } } void ClientACL::save(FILESYSTEM* fs, bool (*filter)(ClientInfo*)) { _fs = fs; #if defined(NRF52_PLATFORM) mesh::AtomicFileWriter file(_fs, "/s_contacts"); #else File file = openWrite(_fs, "/s_contacts"); #endif if (file) { uint8_t unused[2]; unused[0] = CONTACT_RECORD_VERSION_ALT_PATH; unused[1] = 0; bool success = true; for (int i = 0; success && i < num_clients; i++) { auto c = &clients[i]; if (c->permissions == 0 || (filter && !filter(c))) continue; // skip deleted entries, or by filter function success = (file.write(c->id.pub_key, 32) == 32); success = success && (file.write((uint8_t *) &c->permissions, 1) == 1); success = success && (file.write((uint8_t *) &c->extra.room.sync_since, 4) == 4); success = success && (file.write(unused, 2) == 2); success = success && (file.write((uint8_t *)&c->out_path_len, 1) == 1); success = success && (file.write(c->out_path, 64) == 64); success = success && (file.write(c->shared_secret, PUB_KEY_SIZE) == PUB_KEY_SIZE); success = success && (file.write((uint8_t *)&c->alt_path_len, 1) == 1); success = success && (file.write(c->alt_path, 64) == 64); if (!success) break; // write failed } #if defined(NRF52_PLATFORM) if (!file.commit(success)) { MESH_DEBUG_PRINTLN("ERROR: ClientACL::save atomic commit failed"); } #else file.close(); #endif } } bool ClientACL::clear() { if (!_fs) return false; // no filesystem, nothing to clear if (_fs->exists("/s_contacts")) { _fs->remove("/s_contacts"); } #if defined(NRF52_PLATFORM) if (_fs->exists("/s_contacts.tmp")) { _fs->remove("/s_contacts.tmp"); } #endif memset(clients, 0, sizeof(clients)); num_clients = 0; 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) { 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 < MAX_CLIENTS) { 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->alt_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 (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; }