Protecting BLE time against forgery

Why the owner does not deliver the time today

In 6.12, the version the ESP log was taken from, the Android code had nothing of the sort — no background time sync from the owner's phone. Time went out only as a side effect of an owner fire (phase 3), and even then it did not arrive: the ESP dropped the connection right after the relay fired.

6.13 added a separate background connection: on every passive scan of the ESP, the owner's phone opens its own GATT session and writes TIME. But the owner is still the only source of time.

Any trusted client should be able to deliver the time

Otherwise the picture is this: the owner is on holiday, the ESP loses power, the controller reboots with rtc=0 — and guests are locked out until the owner returns. That is no way to build it.

A guest holding a valid token should be allowed to sign TIME with their guest key.

How to prevent forgery — a monotonic ratchet, for guests only

The idea is simple: for a guest, time can only move forward. The owner is trusted and may move it either way without limits — needed if someone set the year to 2099 by mistake and it has to be rolled back.

The ESP keeps the last known time in NVS (time_ratchet). On a guest TIME write:

On an owner TIME write the ratchet is simply overwritten with the owner's value, in either direction.

After a reboot the ESP starts from rtc = ratchet rather than 0. The RTC counts on from there. A factory reset clears the ratchet too, but pairing immediately writes the current time into it.

Why this stops a malicious guest

The attacking guest wants to extend a token whose valid_until = X). What can they try?

Option 1 — wind time back (set the clock to yesterday so their token looks fresh):
→ the ratchet blocks it. Refused.

Option 2 — wind time forward (set the clock a year ahead):
→ the ESP accepts it, since time genuinely moved forward.
→ BUT the ESP now compares valid_until=X with now = a year aheadX < now → and their own token has expired.
→ The attacker has hurt only themselves.

The only honest move for a guest is to set the real current time. Any forgery is either rejected or backfires.

Who signs TIME

The ESP should accept a TIME write signed with:

Two bytes are added to the TIME payload: bleId. The ESP then tries:

  1. the HMAC with owner_secret — if it matches, accept.
  2. Otherwise derive guest_key for that bleId and check again.

Without a legitimate guest bundle issued through the server, TIME cannot be signed — the attacker has no key.

Extra protection: a fire-first gate inside the fresh window

If the last successful sync was less than an hour ago, the ESP treats its clock as fresh and sees no reason to update it out of the blue. Inside that window a TIME write is accepted only if the client performed a successful FIRE within the last 10 seconds — proving it holds a valid token with an unexpired TTL.

If it has been more than an hour the fire gate is off and TIME is accepted freely, still capped at +24 h for guests.

What this closes off: an attacker with a stolen but expired guest bundle can do nothing. Their FIRE is rejected on TTL, so a TIME write inside the fresh window is out of reach. Outside that window the shift is still capped at 24 hours, and the owner sets the right time on their next visit.

A bonus: no collisions

If the owner and three guests write different times at once, each write either advances the ratchet, if it is greater, or is rejected, if it is smaller. There is no contention and no race here.

What needs doing

ESP firmware:

Android:

After that:

NTP as the primary source (optional)

If the ESP sits within Wi-Fi range, it is more robust to take BLE time sync off the critical path entirely and get the time straight from an NTP server.

Firmware implementation (NTP_ENABLED=1):

NTP together with deep sleep

On an AA-powered device the ESP sleeps most of the time and cannot hold Wi-Fi up. The answer: ordinary wake-ups run BLE only, in a short window of about 2.5 s, while every DEEP_SLEEP_NTP_EVERY_N_WAKES-th wake-up stretches to 20 s and brings up Wi-Fi with SNTP. At wake=3 s and N=7200 an NTP sync happens roughly every 6 hours, and the energy budget stays acceptable.

Configurable constants in the firmware

Every parameter of the time policy, sleep and NTP lives in the CONFIG block at the top of the sketch. They can be tuned per site: