mirror of https://github.com/meshcore-dev/MeshCore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
155 lines
5.8 KiB
155 lines
5.8 KiB
#pragma once
|
|
|
|
#include <MeshCore.h>
|
|
#include <string.h>
|
|
|
|
#define SESSION_STATE_NONE 0
|
|
#define SESSION_STATE_INIT_SENT 1 // initiator: INIT sent, waiting for ACCEPT
|
|
#define SESSION_STATE_DUAL_DECODE 2 // responder: new key active, old key still valid
|
|
#define SESSION_STATE_ACTIVE 3 // session key confirmed and in use
|
|
|
|
|
|
struct SessionKeyEntry {
|
|
uint8_t peer_pub_prefix[4]; // first 4 bytes of peer's public key
|
|
uint8_t session_key[SESSION_KEY_SIZE];
|
|
uint8_t prev_session_key[SESSION_KEY_SIZE];
|
|
uint16_t nonce; // session key nonce counter (starts at 1)
|
|
uint8_t state; // SESSION_STATE_*
|
|
uint8_t sends_since_last_recv; // RAM-only counter, threshold SESSION_KEY_STALE_THRESHOLD
|
|
uint8_t retries_left; // remaining INIT retries this round
|
|
unsigned long timeout_at; // millis timestamp for INIT timeout
|
|
uint8_t ephemeral_prv[PRV_KEY_SIZE]; // initiator-only: ephemeral private key (zeroed after use)
|
|
uint8_t ephemeral_pub[PUB_KEY_SIZE]; // initiator-only: ephemeral public key
|
|
uint32_t last_used; // LRU counter (higher = more recent)
|
|
};
|
|
|
|
class SessionKeyPool {
|
|
SessionKeyEntry entries[MAX_SESSION_KEYS_RAM];
|
|
int count;
|
|
uint32_t lru_counter;
|
|
|
|
// Track prefixes removed since last save, so merge-save doesn't resurrect them
|
|
uint8_t removed_prefixes[MAX_SESSION_KEYS_RAM][4];
|
|
int removed_count;
|
|
|
|
void touch(SessionKeyEntry* entry) {
|
|
entry->last_used = ++lru_counter;
|
|
}
|
|
|
|
public:
|
|
SessionKeyPool() : count(0), lru_counter(0), removed_count(0) {
|
|
memset(entries, 0, sizeof(entries));
|
|
memset(removed_prefixes, 0, sizeof(removed_prefixes));
|
|
}
|
|
|
|
bool isFull() const { return count >= MAX_SESSION_KEYS_RAM; }
|
|
|
|
SessionKeyEntry* findByPrefix(const uint8_t* pub_key) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) {
|
|
touch(&entries[i]);
|
|
return &entries[i];
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Lookup without updating LRU — use during save/merge to avoid perturbing eviction order
|
|
bool hasPrefix(const uint8_t* pub_key) const {
|
|
for (int i = 0; i < count; i++) {
|
|
if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
SessionKeyEntry* allocate(const uint8_t* pub_key) {
|
|
// Check if already exists
|
|
auto existing = findByPrefix(pub_key);
|
|
if (existing) return existing;
|
|
|
|
// Find free slot
|
|
if (count < MAX_SESSION_KEYS_RAM) {
|
|
auto e = &entries[count++];
|
|
memset(e, 0, sizeof(*e));
|
|
memcpy(e->peer_pub_prefix, pub_key, 4);
|
|
touch(e);
|
|
return e;
|
|
}
|
|
// Pool full — LRU eviction, skip INIT_SENT entries (ephemeral keys are RAM-only)
|
|
int evict_idx = -1;
|
|
uint32_t min_used = 0xFFFFFFFF;
|
|
for (int i = 0; i < MAX_SESSION_KEYS_RAM; i++) {
|
|
if (entries[i].state == SESSION_STATE_INIT_SENT) continue;
|
|
if (entries[i].last_used < min_used) {
|
|
min_used = entries[i].last_used;
|
|
evict_idx = i;
|
|
}
|
|
}
|
|
if (evict_idx < 0) evict_idx = 0; // all INIT_SENT — shouldn't happen, fall back to [0]
|
|
memset(&entries[evict_idx], 0, sizeof(entries[evict_idx]));
|
|
memcpy(entries[evict_idx].peer_pub_prefix, pub_key, 4);
|
|
touch(&entries[evict_idx]);
|
|
return &entries[evict_idx];
|
|
}
|
|
|
|
void remove(const uint8_t* pub_key) {
|
|
for (int i = 0; i < count; i++) {
|
|
if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) {
|
|
// Track removed prefix for merge-save
|
|
if (removed_count < MAX_SESSION_KEYS_RAM) {
|
|
memcpy(removed_prefixes[removed_count++], entries[i].peer_pub_prefix, 4);
|
|
}
|
|
// Shift remaining entries down
|
|
count--;
|
|
for (int j = i; j < count; j++) {
|
|
entries[j] = entries[j + 1];
|
|
}
|
|
memset(&entries[count], 0, sizeof(entries[count]));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool isRemoved(const uint8_t* pub_key_prefix) const {
|
|
for (int i = 0; i < removed_count; i++) {
|
|
if (memcmp(removed_prefixes[i], pub_key_prefix, 4) == 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void clearRemoved() { removed_count = 0; }
|
|
|
|
int getCount() const { return count; }
|
|
SessionKeyEntry* getByIdx(int idx) { return (idx >= 0 && idx < count) ? &entries[idx] : nullptr; }
|
|
|
|
// Persistence helpers — variable-length records: [pub_prefix:4][flags:1][nonce:2][session_key:32][prev_session_key:32 if flags & PREV_VALID]
|
|
// Returns false when idx is past end or entry is not persistable
|
|
bool getEntryForSave(int idx, uint8_t* pub_key_prefix, uint8_t* flags, uint16_t* nonce,
|
|
uint8_t* session_key, uint8_t* prev_session_key) {
|
|
if (idx >= count) return false;
|
|
auto& e = entries[idx];
|
|
if (e.state == SESSION_STATE_NONE || e.state == SESSION_STATE_INIT_SENT) return false; // don't persist pending negotiations
|
|
memcpy(pub_key_prefix, e.peer_pub_prefix, 4);
|
|
*flags = (e.state == SESSION_STATE_DUAL_DECODE) ? SESSION_FLAG_PREV_VALID : 0;
|
|
*nonce = e.nonce;
|
|
memcpy(session_key, e.session_key, SESSION_KEY_SIZE);
|
|
memcpy(prev_session_key, e.prev_session_key, SESSION_KEY_SIZE);
|
|
return true;
|
|
}
|
|
|
|
bool applyLoaded(const uint8_t* pub_key_prefix, uint8_t flags, uint16_t nonce,
|
|
const uint8_t* session_key, const uint8_t* prev_session_key) {
|
|
auto e = allocate(pub_key_prefix);
|
|
if (!e) return false;
|
|
e->nonce = nonce;
|
|
e->state = (flags & SESSION_FLAG_PREV_VALID) ? SESSION_STATE_DUAL_DECODE : SESSION_STATE_ACTIVE;
|
|
e->sends_since_last_recv = 0;
|
|
e->retries_left = 0;
|
|
e->timeout_at = 0;
|
|
memcpy(e->session_key, session_key, SESSION_KEY_SIZE);
|
|
memcpy(e->prev_session_key, prev_session_key, SESSION_KEY_SIZE);
|
|
memset(e->ephemeral_prv, 0, sizeof(e->ephemeral_prv));
|
|
memset(e->ephemeral_pub, 0, sizeof(e->ephemeral_pub));
|
|
return true;
|
|
}
|
|
};
|
|
|