In Version 6.12, aus der das Controller-Log stammt, gab es im Android-Code nichts dergleichen — keine Hintergrund-Zeitsynchronisation vom Telefon des Besitzers. Die Zeit ging nur als Nebeneffekt eines Besitzer-FIRE hinaus (Phase 3), und selbst dann kam sie nicht an: Der Controller trennte die Verbindung direkt nach dem Schalten des Relais.
In 6.13 kam eine eigene Hintergrundverbindung hinzu: Bei jedem passiven Erkennen des Controllers öffnet das Telefon des Besitzers eine eigene GATT-Sitzung und schreibt TIME. Doch die Zeitquelle bleibt allein der Besitzer.
Sonst sieht es so aus: Der Besitzer ist im Urlaub, der Controller verliert die Stromversorgung, startet mit rtc=0 neu — und die Gäste bleiben bis zu seiner Rückkehr ausgesperrt. So darf man das nicht bauen.
Einem Gast mit gültigem Token muss erlaubt sein, TIME mit seinem Gastschlüssel zu signieren.
Die Idee ist einfach: Beim Gast kann die Zeit nur vorwärts gehen. Dem Besitzer wird vertraut: Er verschiebt sie ohne Grenzen in beide Richtungen — nötig, wenn jemand versehentlich das Jahr 2099 gesetzt hat und es zurückgedreht werden muss.
Der Controller hält die zuletzt bekannte Zeit im NVS (time_ratchet). Bei einem Gast-TIME-Schreibvorgang:
new_time < ratchet → als Rückschritt abweisen.new_time ≥ ratchet → annehmen, die Ratsche weiterdrehen und ins NVS schreiben.Bei einem Besitzer-TIME-Schreibvorgang wird die Ratsche einfach mit dessen Wert überschrieben, in beide Richtungen.
Nach einem Neustart startet der Controller von rtc = ratchet statt von 0. Die RTC zählt von dort weiter. Ein Werksreset löscht auch die Ratsche, doch die Kopplung schreibt sofort die aktuelle Zeit hinein.
Der angreifende Gast will ein Token verlängern, dessen valid_until = X). Was kann er versuchen?
Variante 1 — die Zeit zurückdrehen (die Uhr auf gestern stellen, damit das Token frisch wirkt):
→ die Ratsche lässt es nicht zu. Abgelehnt.
Variante 2 — die Zeit vorstellen (die Uhr ein Jahr vorstellen):
→ der Controller nimmt es an, denn die Zeit ist tatsächlich vorgerückt.
→ ABER der Controller vergleicht nun valid_until=X mit now = a year ahead → X < now → und sein eigenes Token ist abgelaufen.
→ Der Angreifer hat nur sich selbst geschadet.
Der einzige ehrliche Zug eines Gastes ist, die echte aktuelle Zeit zu setzen. Jede Fälschung wird entweder abgelehnt oder fällt auf ihn zurück.
Der Controller soll einen TIME-Schreibvorgang annehmen, der signiert ist mit:
Der TIME-Nutzlast werden zwei Bytes hinzugefügt: bleId. Danach versucht der Controller:
Ohne ein legitimes, über den Server ausgestelltes Gast-Bundle lässt sich TIME nicht signieren — der Angreifer hat keinen Schlüssel.
Liegt die letzte erfolgreiche Synchronisation weniger als eine Stunde zurück, hält der Controller seine Uhr für frisch und sieht keinen Grund, sie grundlos zu aktualisieren. In diesem Fenster wird ein TIME-Schreibvorgang nur angenommen, wenn der Client in den letzten 10 Sekunden ein erfolgreiches FIRE ausgeführt hat — Beweis für ein gültiges Token mit nicht abgelaufener Frist.
Ist es mehr als eine Stunde her ist das FIRE-Gate aus und TIME wird frei angenommen, weiterhin mit der Obergrenze +24 h für Gäste.
Was das schließt: Ein Angreifer mit einem gestohlenen, aber abgelaufen Gast-Bundle kann nichts ausrichten. Sein FIRE scheitert an der Frist, ein TIME-Schreibvorgang im frischen Fenster bleibt also unerreichbar. Außerhalb des Fensters ist die Verschiebung ohnehin auf 24 Stunden begrenzt, und der Besitzer stellt beim nächsten Besuch die richtige Zeit.
Schreiben der Besitzer und drei Gäste gleichzeitig verschiedene Zeiten, so schiebt jeder Schreibvorgang die Ratsche weiter, wenn er größer ist, oder wird abgelehnt, wenn er kleiner ist. Es gibt hier weder Streit noch Wettlauf.
Controller-Firmware:
TimeCallback: mit owner_secret prüfen; schlägt das fehl, die bleId aus der Nutzlast nehmen, guest_key ableiten und erneut versuchen.new_time ≥ time_ratchet.time_ratchet im NVS und bei jedem angenommenen TIME aktualisieren.settimeofday(ratchet), g_rtcValid = (ratchet > 0).Android:
BleCrypto.buildTimeSync — die bleId und die Schlüsselwahl ergänzen: owner_secret oder guest_key.maybeSyncTime analog zum Besitzerpfad — ausgelöst vom passiven Scannen und höchstens alle 30 Minuten.Danach:
Steht der Controller in Wi-Fi-Reichweite, ist es robuster, die BLE-Zeitsynchronisation ganz vom kritischen Pfad zu nehmen und die Zeit direkt von einem NTP-Server zu holen.
Umsetzung in der Firmware (NTP_ENABLED=1):
CHAR_WIFI — der Besitzer schreibt dort signierte SSID und Passwort hinein. Sie liegen im NVS.configTime() gegen pool.ntp.org, time.google.com oder Cloudflare.NTP_RESYNC_INTERVAL_MS (standardmäßig 6 Stunden).Im Batteriebetrieb schläft der Controller fast durchgehend und kann kein WLAN halten. Die Lösung: gewöhnliche Weckzyklen laufen nur über BLE, in einem kurzen Fenster von etwa 2,5 s, während jeder DEEP_SLEEP_NTP_EVERY_N_WAKES-te Weckzyklus auf 20 s verlängert wird und WLAN mit SNTP hochfährt. Bei wake=3 s und N=7200 erfolgt eine NTP-Synchronisation etwa alle 6 Stunden, und der Energiehaushalt bleibt vertretbar.
Alle Parameter der Zeitpolitik, des Schlafs und von NTP stehen im CONFIG-Block am Anfang des Sketches. Sie lassen sich je Standort anpassen:
FRESH_SYNC_WINDOW = 1 h, GUEST_FWD_CAP = 24 h, DEEP_SLEEP = on, NTP_EVERY_N = 7200.FRESH_SYNC_WINDOW = 5 min, GUEST_FWD_CAP = 15 min, DEEP_SLEEP = off (Netzbetrieb), NTP_ENABLED = 1.NTP_ENABLED = 0, alles Übrige nur über BLE.