* refactor: moved ACL out of SensorMesh -> ClientACL

This commit is contained in:
Scott Powell 2025-09-12 15:35:31 +10:00
parent c8a10cc3b3
commit 281591f147
6 changed files with 232 additions and 210 deletions

View file

@ -92,7 +92,7 @@ void LocalIdentity::sign(uint8_t* sig, const uint8_t* message, int msg_len) cons
ed25519_sign(sig, message, msg_len, pub_key, prv_key);
}
void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) {
void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const {
ed25519_key_exchange(secret, other_pub_key, prv_key);
}

View file

@ -71,7 +71,7 @@ public:
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
* \param other_pub_key IN - the public key of second party in the exchange (must be PUB_KEY_SIZE bytes)
*/
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key);
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const;
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;

128
src/helpers/ClientACL.cpp Normal file
View file

@ -0,0 +1,128 @@
#include "ClientACL.h"
static File openWrite(FILESYSTEM* _fs, const char* filename) {
#if defined(NRF52_PLATFORM) || 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
}
void ClientACL::load(FILESYSTEM* _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[6];
bool success = (file.read(pub_key, 32) == 32);
success = success && (file.read((uint8_t *) &c.permissions, 1) == 1);
success = success && (file.read(unused, 6) == 6);
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);
c.last_timestamp = 0; // transient
c.last_activity = 0;
if (!success) break; // EOF
c.id = mesh::Identity(pub_key);
if (num_clients < MAX_CLIENTS) {
clients[num_clients++] = c;
} else {
full = true;
}
}
file.close();
}
}
}
void ClientACL::save(FILESYSTEM* _fs) {
File file = openWrite(_fs, "/s_contacts");
if (file) {
uint8_t unused[6];
memset(unused, 0, sizeof(unused));
for (int i = 0; i < num_clients; i++) {
auto c = &clients[i];
if (c->permissions == 0) continue; // skip deleted entries
bool success = (file.write(c->id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *) &c->permissions, 1) == 1);
success = success && (file.write(unused, 6) == 6);
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);
if (!success) break; // write failed
}
file.close();
}
}
ClientInfo* ClientACL::getClient(const uint8_t* pubkey, int key_len) {
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 = &clients[MAX_CLIENTS - 1];
for (int i = 0; i < num_clients; i++) {
if (id.matches(clients[i].id)) return &clients[i]; // already known
if (!clients[i].isAdmin() && 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 {
c = oldest; // evict least active contact
}
memset(c, 0, sizeof(*c));
c->permissions = init_perms;
c->id = id;
c->out_path_len = -1; // initially out_path is unknown
return c;
}
bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) {
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);
c->permissions = perms; // update their permissions
self_id.calcSharedSecret(c->shared_secret, pubkey);
}
return true;
}

47
src/helpers/ClientACL.h Normal file
View file

@ -0,0 +1,47 @@
#pragma once
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityStore.h>
#define PERM_ACL_ROLE_MASK 3 // lower 2 bits
#define PERM_ACL_GUEST 0
#define PERM_ACL_READ_ONLY 1
#define PERM_ACL_READ_WRITE 2
#define PERM_ACL_ADMIN 3
struct ClientInfo {
mesh::Identity id;
uint8_t permissions;
int8_t out_path_len;
uint8_t out_path[MAX_PATH_SIZE];
uint8_t shared_secret[PUB_KEY_SIZE];
uint32_t last_timestamp; // by THEIR clock (transient)
uint32_t last_activity; // by OUR clock (transient)
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }
};
#ifndef MAX_CLIENTS
#define MAX_CLIENTS 20
#endif
class ClientACL {
ClientInfo clients[MAX_CLIENTS];
int num_clients;
public:
ClientACL() {
memset(clients, 0, sizeof(clients));
num_clients = 0;
}
void load(FILESYSTEM* _fs);
void save(FILESYSTEM* _fs);
ClientInfo* getClient(const uint8_t* pubkey, int key_len);
ClientInfo* putClient(const mesh::Identity& id, uint8_t init_perms);
bool applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms);
int getNumClients() const { return num_clients; }
ClientInfo* getClientByIdx(int idx) { return &clients[idx]; }
};