Skip to content

Latest commit

 

History

History
127 lines (102 loc) · 6.29 KB

File metadata and controls

127 lines (102 loc) · 6.29 KB

Boot Sequence

Source: setup() in src/main.cpp.

This walks through setup() in order, focusing on where encryption keys become available and exactly when each data category gets wired up. Line numbers will drift as the file changes; look for the named calls instead.

1. M5Cardputer.begin() / display / keyboard / hotkeys init
   (no encryption involved — pure hardware bring-up)

2. flash.begin()                          — mount LittleFS
3. Boot-loop detection                    — NVS "ratcom"/"bootc" counter (unrelated namespace)
4. radio.begin()                          — SX1262 bring-up
5. sdStore.begin()                        — mount SD card, ensure base dirs

   ── Identity-at-rest gate ──────────────────────────────────────────────
6. rns.probeIdentityState()
     → NONE              : no identity anywhere, first boot
     → ENCRYPTED         : RID1-magic blob found, needs password to open
     → LEGACY_PLAINTEXT  : pre-Crypto-Edition plaintext identity found

7. runPasswordGate(state)
     SETUP   path: type + confirm a new password (≥6 chars)
     UNLOCK  path: type once, up to 10 attempts, IdentityCrypto::unwrap()
                   verifies the MAC before any decryption is attempted
                   → on each failed attempt, also checked against the
                     optional duress password (if configured); a match
                     wipes the device and reboots instead of counting as
                     a failed attempt (see "Duress password" below)
     → returns { password, unlockedKey }

8. rns.setPassword(password)
   rns.preloadIdentityKey(unlockedKey)     — if we already unwrapped externally

9. rns.begin(&radio, &flash)
     - Adopts the preloaded key, or loads/creates the identity
     - rns.identity() is now valid and holds the private key in RAM
   ── Everything below this line can derive at-rest keys from the identity ──

10. messageStore.begin(&flash, &sdStore)
    messageStore.setIdentity(&rns.identity())   ← message encryption enabled

11. if (rns.legacyIdentityLoaded()):
        runLegacyIdentityMigration()
          - identity already re-wrapped & encrypted by saveIdentityToAll()
          - user is asked: migrate messages too? (opt-in, may take a while)
          - if yes: messageStore.migratePlaintextMessages() with progress UI

12. announceManager = new AnnounceManager(...)
    announceManager->setIdentity(&rns.identity())   ← contacts encryption enabled
    announceManager->loadContacts()
    announceManager->loadNameCache()
    if (announceManager->encryptionEnabled()):
        announceManager->saveContacts()     ← forces immediate re-encrypt of
        announceManager->saveNameCache()      any pre-upgrade plaintext files

13. userConfig.setIdentity(&rns.identity())          ← settings encryption enabled
    userConfig.load(sdStore, flash)
    if (userConfig.encryptionEnabled()):
        userConfig.save(sdStore, flash)     ← forces immediate re-encrypt of
                                               a pre-upgrade plaintext settings file

14. ... WiFi/AutoInterface bring-up, GPS, power/audio settings applied,
        home screen shown

Why contacts/settings migration is unconditional but message migration isn't

Message history can be arbitrarily large — re-encrypting thousands of files at boot would stall startup, so that migration is opt-in with a progress screen (step 11). Contacts are capped at 50 entries and settings is a single file; re-encrypting them is sub-millisecond, so steps 12 and 13 just do it unconditionally with no user-visible delay or prompt. There is no scenario where waiting for confirmation would be worth the UX cost.

Why this ordering is safe

The identity must exist in plaintext in RAM before any domain key can be derived (every domain's key traces back to identity.encryptionPrivateKey()). Steps 10, 12, and 13 are all placed after step 9 specifically because that's the earliest point the identity is guaranteed valid. Nothing earlier in setup() reads contacts or settings — the boot screen, keyboard, hotkeys, radio defaults, and SD mount all use hardcoded values or don't need persisted state at all, so there was no pre-existing code path that would have broken by requiring identity-first ordering.

Password lifetime

The plaintext password string only exists in RAM during steps 7-9; it is explicitly zeroed (IdentityCrypto::secureZero) once rns.begin() has adopted the identity. From that point on, only the identity's private key lives in RAM (which Reticulum itself requires for normal operation — that's not something this firmware can avoid), not the password that unlocked it.

Duress password

Step 7's UNLOCK path checks each failed attempt against an optional second password before counting it as wrong. A match wipes the device and reboots — see duress-password.md for the full mechanism (verifier storage, wipe scope, and why the reboot intentionally looks identical to a fresh device rather than showing any "wiped" state).

Known one-time side effects when migrating from upstream

UserConfig::parseJson() reads every field as doc["key"] | default, so a config written by upstream ratspeak/rsCardputer (missing any key this fork added later) always loads cleanly instead of failing — but two fields fall back to a default that produces a one-time, user-visible prompt rather than silently carrying over prior intent:

  • Timezone: upstream's utc_offset/tz_idx/tz_set have no equivalent in the fork's tz_manual/tz_manual_offset fields, so a migrated device falls back to GPS-estimated local time (manualTimezoneEnabled = false) instead of the old manual offset.
  • LoRa setup wizard: radio_configured doesn't exist in upstream's config at all, so it defaults to false and the wizard (main.cpp, gated on radioConfigured) runs once on first boot — even though the migrated lora_freq/lora_sf/lora_bw/ lora_cr/lora_txp values are correct and preserved. Re-running the wizard just re-confirms already-correct values; nothing is lost.

Both are one-time prompts on the first boot after migration, not data loss — the user re-picks a timezone and re-confirms radio settings once.