mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-01 17:56:12 +00:00
feat(companion): on-device room login with saved passwords
Log in to a room server from the device UI with no phone app: picking a room prompts for its password (blank allowed for open rooms), and a context-menu "Login..." allows re-login. Successful passwords are persisted to a dedicated /room_pw file so a previously-used room logs back in after reboot without retyping; a failed login forgets the (now-stale) saved password so the next attempt prompts again. The room-password file is written via a temp file + atomic rename (new DataStore::commitFile helper), matching the crash-safety of contacts/channels persistence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
57774d41f3
commit
be9f3db8c6
@@ -870,6 +870,24 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
i += 6; // pub_key_prefix
|
||||
}
|
||||
_serial->writeFrame(out_frame, i);
|
||||
} else if (ui_pending_login && memcmp(&ui_pending_login, contact.id.pub_key, 4) == 0) { // check for on-device UI login response
|
||||
ui_pending_login = 0;
|
||||
|
||||
bool success;
|
||||
uint8_t permissions = 0;
|
||||
if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
|
||||
success = true;
|
||||
} else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response
|
||||
uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16;
|
||||
if (keep_alive_secs > 0) {
|
||||
startConnection(contact, keep_alive_secs);
|
||||
}
|
||||
success = true;
|
||||
permissions = data[7]; // ACL permissions
|
||||
} else {
|
||||
success = false;
|
||||
}
|
||||
_ui->onRoomLoginResult(contact.id.pub_key, success, permissions);
|
||||
} else if (len > 4 && // check for status response
|
||||
pending_status &&
|
||||
memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme
|
||||
@@ -910,6 +928,104 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
||||
}
|
||||
}
|
||||
|
||||
#define ROOM_PW_FILE "/room_pw"
|
||||
#define ROOM_PW_TMP "/room_pw.tmp"
|
||||
#define MAX_SAVED_ROOM_PASSWORDS 16
|
||||
|
||||
namespace {
|
||||
struct RoomPwRec {
|
||||
uint8_t key[4]; // pub-key prefix
|
||||
char pw[16]; // up to 15 chars + NUL
|
||||
};
|
||||
}
|
||||
|
||||
bool MyMesh::getRoomPassword(const uint8_t* pub_key, char* out_password, uint8_t max_len) {
|
||||
File f = _store->openRead(ROOM_PW_FILE);
|
||||
if (!f) return false;
|
||||
|
||||
RoomPwRec rec;
|
||||
bool found = false;
|
||||
while (f.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
||||
if (memcmp(rec.key, pub_key, 4) == 0) {
|
||||
strncpy(out_password, rec.pw, max_len - 1);
|
||||
out_password[max_len - 1] = 0;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
return found;
|
||||
}
|
||||
|
||||
bool MyMesh::saveRoomPassword(const uint8_t* pub_key, const char* password) {
|
||||
// The table is tiny (<= MAX_SAVED_ROOM_PASSWORDS * 20 bytes), so just load
|
||||
// it whole, update/append/evict in RAM, then rewrite -- simpler and just
|
||||
// as crash-safe as a record seek given how rarely this runs (once per new
|
||||
// room login).
|
||||
RoomPwRec recs[MAX_SAVED_ROOM_PASSWORDS];
|
||||
int count = 0;
|
||||
File rf = _store->openRead(ROOM_PW_FILE);
|
||||
if (rf) {
|
||||
RoomPwRec rec;
|
||||
while (count < MAX_SAVED_ROOM_PASSWORDS && rf.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
||||
if (memcmp(rec.key, pub_key, 4) != 0) { // drop stale entry for this key -- replaced below
|
||||
recs[count++] = rec;
|
||||
}
|
||||
}
|
||||
rf.close();
|
||||
}
|
||||
|
||||
RoomPwRec new_rec;
|
||||
memcpy(new_rec.key, pub_key, 4);
|
||||
strncpy(new_rec.pw, password, sizeof(new_rec.pw) - 1);
|
||||
new_rec.pw[sizeof(new_rec.pw) - 1] = 0;
|
||||
|
||||
if (count < MAX_SAVED_ROOM_PASSWORDS) {
|
||||
recs[count++] = new_rec;
|
||||
} else { // table full and not already present -- evict oldest (front)
|
||||
memmove(&recs[0], &recs[1], sizeof(RoomPwRec) * (MAX_SAVED_ROOM_PASSWORDS - 1));
|
||||
recs[MAX_SAVED_ROOM_PASSWORDS - 1] = new_rec;
|
||||
}
|
||||
|
||||
// Write to a temp file and atomically swap it over /room_pw, so an
|
||||
// interrupted save leaves the previous good table intact rather than a
|
||||
// truncated mix (mirrors how contacts/channels are persisted).
|
||||
File wf = _store->openWrite(ROOM_PW_TMP);
|
||||
if (!wf) return false;
|
||||
size_t want = sizeof(RoomPwRec) * count;
|
||||
bool ok = (wf.write((uint8_t *)recs, want) == want);
|
||||
wf.close();
|
||||
if (!ok) { _store->removeFile(ROOM_PW_TMP); return false; } // keep previous good file
|
||||
return _store->commitFile(ROOM_PW_TMP, ROOM_PW_FILE);
|
||||
}
|
||||
|
||||
void MyMesh::forgetRoomPassword(const uint8_t* pub_key) {
|
||||
RoomPwRec recs[MAX_SAVED_ROOM_PASSWORDS];
|
||||
int count = 0;
|
||||
File rf = _store->openRead(ROOM_PW_FILE);
|
||||
if (!rf) return;
|
||||
|
||||
RoomPwRec rec;
|
||||
bool removed = false;
|
||||
while (count < MAX_SAVED_ROOM_PASSWORDS && rf.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
||||
if (memcmp(rec.key, pub_key, 4) == 0) {
|
||||
removed = true;
|
||||
} else {
|
||||
recs[count++] = rec;
|
||||
}
|
||||
}
|
||||
rf.close();
|
||||
if (!removed) return; // nothing to do, avoid a pointless rewrite
|
||||
|
||||
File wf = _store->openWrite(ROOM_PW_TMP);
|
||||
if (!wf) return;
|
||||
size_t want = sizeof(RoomPwRec) * count;
|
||||
bool ok = (wf.write((uint8_t *)recs, want) == want);
|
||||
wf.close();
|
||||
if (!ok) { _store->removeFile(ROOM_PW_TMP); return; } // keep previous good file
|
||||
_store->commitFile(ROOM_PW_TMP, ROOM_PW_FILE);
|
||||
}
|
||||
|
||||
bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
|
||||
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
|
||||
uint32_t tag;
|
||||
|
||||
Reference in New Issue
Block a user