diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index ee714f89..ab53ec5b 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -1,3 +1,20 @@ +/* + * File: DataStore.cpp + * Project: companion_radio + * Created Date: 2026-09-26 12:03:18 + * Author: 3urobeat + * + * Last Modified: 2026-09-26 12:16:49 + * Modified By: 3urobeat + * + * Copyright (c) 2026 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + */ + + #include #include "DataStore.h" #include "Features.h" // FEAT_JOYSTICK_ROTATION_SETTING (else `#if !FEAT_…` is always true) @@ -624,8 +641,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no // append the lock-screen password. Should be empty by default // since struct was zero initialized in begin(), meaning password is disabled + // → 0xC0DE002F: append the lock-screen password + // → 0xC0DE0030: append the per-device password salt rd(_prefs.lock_screen_password, sizeof(_prefs.lock_screen_password)); - _prefs.lock_screen_password[sizeof(_prefs.lock_screen_password) - 1] = '\0'; + rd(_prefs.lock_screen_password_salt, sizeof(_prefs.lock_screen_password_salt)); // Schema sentinel: bumped on layout changes. Mismatch means an older file // (or a different schema); rd() and the clamps above already keep every @@ -825,6 +844,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.loc_share_scope, sizeof(_prefs.loc_share_scope)); file.write((uint8_t *)&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx)); file.write((uint8_t *)_prefs.lock_screen_password, sizeof(_prefs.lock_screen_password)); + file.write((uint8_t *)_prefs.lock_screen_password_salt, sizeof(_prefs.lock_screen_password_salt)); // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is // the one we check: once the flash fills, writes return 0, so a good diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 8dbc7ba6..d426b8ef 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -539,7 +539,9 @@ struct NodePrefs { // persisted to file // Lock-screen password, empty by default. If set, a password is required to // unlock the lock screen. static const uint8_t LOCK_PASSWORD_MAX_LEN = 32; - char lock_screen_password[LOCK_PASSWORD_MAX_LEN]; + static const uint8_t lock_screen_password_salt_LEN = 16; + uint8_t lock_screen_password[LOCK_PASSWORD_MAX_LEN]; + uint8_t lock_screen_password_salt[lock_screen_password_salt_LEN]; // Single source of truth for the live-share option tables (shared by the Map // UI labels and the auto-send engine in UITask). @@ -628,7 +630,7 @@ struct NodePrefs { // persisted to file // repeat_* fields) instead of at the tail, which shifted every field after // them by 25 bytes when loading an older file. Never released, but a dev // build wrote it, so the number must not be reused for anything else. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE002F; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE0030; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage @@ -784,7 +786,8 @@ struct NodePrefs { // persisted to file // (ESP32) builds, sizeof unchanged at 2824. loc_share_duration_idx (0xC0DE002E) // likewise (sim build; see the check below). // 0xC0DE002F 32 byte bump for lock_screen_password -static_assert(sizeof(NodePrefs) == 2856, +// 0xC0DE0030 16 byte bump for lock_screen_password_salt +static_assert(sizeof(NodePrefs) == 2872, "NodePrefs layout changed — sync DataStore save/load + clamp, bump " "SCHEMA_SENTINEL, then update this size (see steps above)."); diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 2ae38c42..3b74b680 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -873,8 +873,7 @@ public: auto res = _kb->handleInput(c); if (res == KeyboardWidget::DONE) { if (p) { - strncpy(p->lock_screen_password, _kb->buf, sizeof(p->lock_screen_password) - 1); - p->lock_screen_password[sizeof(p->lock_screen_password) - 1] = '\0'; + _task->setNodeLockPassword(_kb->buf); _dirty = true; // savePrefsIfDirty persists new password } _edit_lock_pass = false; @@ -1137,7 +1136,7 @@ public: // LockPass: Clear password if defined or get input from keyboard if (_selected == LOCK_PASSWORD && p && enter) { if (p->lock_screen_password[0]) { - p->lock_screen_password[0] = '\0'; + _task->setNodeLockPassword(""); _dirty = true; } else { _edit_lock_pass = true; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 7171e2fb..9ba6da76 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2224,6 +2224,39 @@ bool UITask::passwordLockEnabled() const { return _node_prefs && _node_prefs->lock_screen_password[0] != '\0'; } +void UITask::setNodeLockPassword(const char* plain) { + if (!_node_prefs || !plain) return; + if (plain[0] == '\0') { // Clear the password + memset(_node_prefs->lock_screen_password, 0, sizeof(_node_prefs->lock_screen_password)); + memset(_node_prefs->lock_screen_password_salt, 0, sizeof(_node_prefs->lock_screen_password_salt)); + return; + } + // Generate a fresh random salt and store salted SHA-256 digest + mesh::RNG* rng = the_mesh.getRNG(); + if (rng) { + rng->random(_node_prefs->lock_screen_password_salt, sizeof(_node_prefs->lock_screen_password_salt)); + } else { + // Time as fallback entropy source + uint32_t t = (uint32_t)millis() ^ (uint32_t)rtc_clock.getCurrentTime(); + memcpy(_node_prefs->lock_screen_password_salt, &t, sizeof(t)); + } + mesh::Utils::sha256((uint8_t*)_node_prefs->lock_screen_password, + sizeof(_node_prefs->lock_screen_password), + _node_prefs->lock_screen_password_salt, + sizeof(_node_prefs->lock_screen_password_salt), + (const uint8_t*)plain, (int)strlen(plain)); +} + +bool UITask::checkNodeLockPassword(const char* entered) const { + if (!_node_prefs || !entered) return false; + uint8_t digest[NodePrefs::LOCK_PASSWORD_MAX_LEN]; + mesh::Utils::sha256(digest, sizeof(digest), + _node_prefs->lock_screen_password_salt, + sizeof(_node_prefs->lock_screen_password_salt), + (const uint8_t*)entered, (int)strlen(entered)); + return memcmp(digest, _node_prefs->lock_screen_password, sizeof(digest)) == 0; +} + void UITask::beginUnlockPrompt() { _unlock_kb = true; // Track that keyboard is visible and is waiting for input int max_len = _node_prefs ? (int)sizeof(_node_prefs->lock_screen_password) - 1 : 32; @@ -2241,7 +2274,7 @@ void UITask::cancelUnlockPrompt() { void UITask::handleUnlockKey(char c) { auto res = _kb.handleInput(c); if (res == KeyboardWidget::DONE) { // Process input on submit - if (_node_prefs && strcmp(_kb.buf, _node_prefs->lock_screen_password) == 0) { + if (_node_prefs && checkNodeLockPassword(_kb.buf)) { // Match: Unlock _unlock_kb = false; _locked = false; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 8fe7056d..964842fe 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -306,6 +306,11 @@ private: void renderAlertOverlay(); public: + // Stores new lock screen salted SHA-256 password + // `plain` is the raw user input, passing "" clears currently set password + void setNodeLockPassword(const char* plain); + // Verifies entered against the stored password hash. Returns match as bool + bool checkNodeLockPassword(const char* entered) const; UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) { next_batt_chck = _next_refresh = 0;