Merge PR #37: lock screen PIN (3urobeat), adapted

The contributor's feature -- a PIN asked before the screen unlocks, on
boot, and when the magnet cover opens, stored as a salted SHA-256 -- on
top of this branch, with:

- ui-core/ScreenLock.h: set / check / isSet and the tries limit, shared by
  L1 and L2. isSet looks at every byte (the PR tested only the first, so one
  PIN in 256 silently disabled the lock). 5 misses pause entry for 30 s.
- NodePrefs: the fields at the tail after quiet hours, sentinel 0xC0DE0031,
  sizeof 2880; an older file's bytes there are cleared. A sentinel bump no
  longer resets display_brightness_pct from a file that already has it.
- DataStore: no AGPL header (the project is MIT), no whitespace churn.
- Keyboard: the number pad is a widget flag (pin_mode) with a prompt in the
  preview ("New PIN", "Again", "PIN"), not keyboard_type = 2 written into the
  prefs and restored after.
- L1 Settings: "Lock PIN" right after the schema's "Lock screen" row, typed
  twice, at least 4 characters, saved at once (a power-off right after
  setting it kept no PIN). Home is told about a boot-time lock once it
  exists (also fixes booting with the cover closed).
- L2: the screen PIN moves from plain text in NVS to the same hash in
  NodePrefs; the old one is migrated on first boot and removed. The pad
  unlocks as soon as the hash matches from 4 digits on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-29 00:06:01 +02:00
co-authored by Claude Opus 5.5
13 changed files with 540 additions and 71 deletions
+11 -2
View File
@@ -548,6 +548,14 @@ struct NodePrefs { // persisted to file
// ── Custom messages ────────────────────────────────────────────────────
char custom_msgs[10][140]; // user-defined quick messages (supports {loc}, {time})
// ── Screen lock PIN (0xC0DE0031) ───────────────────────────────────────
// SHA-256 of salt + PIN (see ScreenLock.h); all zeros = no PIN. It locks the
// screen only: the radio and the app link keep working.
static const uint8_t LOCK_HASH_LEN = 32;
static const uint8_t LOCK_SALT_LEN = 16;
uint8_t lock_screen_password[LOCK_HASH_LEN];
uint8_t lock_screen_password_salt[LOCK_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).
static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
@@ -635,7 +643,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 = 0xC0DE0030;
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0031;
// 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
@@ -819,7 +827,8 @@ static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) {
// quiet_hours / quiet_from / quiet_to (0xC0DE0030) added 8 bytes, not 3 (next
// to buzzer_auto, rounded up to the alignment) -- sizeof 2832, confirmed via real
// WioTrackerL1_companion_solo_dual (nRF52/ARM) and L2 (ESP32) builds.
static_assert(sizeof(NodePrefs) == 2832,
// lock_screen_password / _salt (0xC0DE0031) added 48 bytes -- sizeof 2880.
static_assert(sizeof(NodePrefs) == 2880,
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
"SCHEMA_SENTINEL, then update this size (see steps above).");