feat: Hash & salt lock screen password

This commit is contained in:
3urobeat
2026-09-06 19:49:07 +02:00
parent 6404622ebf
commit f2209c338d
5 changed files with 68 additions and 8 deletions
+21 -1
View File
@@ -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 <https://github.com/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 <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#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
+6 -3
View File
@@ -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).");
@@ -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;
+34 -1
View File
@@ -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;
+5
View File
@@ -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;