Dans la version 6.12, celle dont provient le journal du contrôleur, le code Android n’avait rien de tel — aucune synchronisation d’heure en arrière-plan depuis le téléphone du propriétaire. L’heure ne partait qu’en effet secondaire d’un fire du propriétaire (phase 3), et encore n’arrivait-elle pas : le contrôleur coupait la connexion juste après le déclenchement du relais.
La 6.13 a ajouté une connexion d’arrière-plan distincte : à chaque détection passive du contrôleur, le téléphone du propriétaire ouvre sa propre session GATT et écrit TIME. Mais la seule source d’heure reste le propriétaire.
Sinon voici le tableau : le propriétaire est en vacances, le contrôleur perd l’alimentation, redémarre avec rtc=0 — et les invités sont exclus jusqu’à son retour. On ne construit pas ainsi.
Un invité muni d’un jeton valide doit pouvoir signer TIME avec sa clé invité.
L’idée est simple : pour un invité, l’heure ne peut qu’avancer. Le propriétaire est de confiance : il la déplace dans les deux sens sans limite — utile si quelqu’un a mis 2099 par erreur et qu’il faut revenir en arrière.
Le contrôleur garde la dernière heure connue en NVS (time_ratchet). Lors d’une écriture TIME d’invité :
new_time < ratchet → rejet en tant que retour en arrière.new_time ≥ ratchet → acceptation, avancée du cliquet et écriture en NVS.Lors d’une écriture TIME du propriétaire, le cliquet est simplement écrasé par sa valeur, dans les deux sens.
Après un redémarrage, le contrôleur part de rtc = ratchet et non de 0. Le RTC compte à partir de là. Une réinitialisation d’usine efface aussi le cliquet, mais l’appairage y écrit aussitôt l’heure courante.
L’invité attaquant veut prolonger un jeton dont valid_until = X). Que peut-il tenter ?
Option 1 — reculer l’heure (mettre l’horloge à hier pour que son jeton paraisse frais) :
→ le cliquet l’en empêche. Refusé.
Option 2 — avancer l’heure (avancer l’horloge d’un an) :
→ le contrôleur l’accepte, car l’heure a bien avancé.
→ MAIS le contrôleur compare désormais valid_until=X avec now = a year ahead → X < now → et son propre jeton a expiré.
→ L’attaquant ne s’est nui qu’à lui-même.
Le seul geste honnête d’un invité est de mettre l’heure réelle. Toute falsification est soit rejetée, soit se retourne contre lui.
Le contrôleur doit accepter une écriture TIME signée avec :
Deux octets sont ajoutés à la charge utile TIME : bleId. Ensuite le contrôleur essaie :
Sans lot invité légitime délivré par le serveur, impossible de signer TIME — l’attaquant n’a pas la clé.
Si la dernière synchronisation réussie date de moins d’une heure, le contrôleur juge son horloge fraîche et ne voit aucune raison de la mettre à jour sans motif. Dans cette fenêtre, une écriture TIME n’est acceptée que si le client a réalisé un FIRE réussi dans les 10 dernières secondes — preuve qu’il détient un jeton valide non expiré.
S’il s’est écoulé plus d’une heure le verrou fire se désactive et TIME est accepté librement, toujours plafonné à +24 h pour les invités.
Ce que cela ferme : un attaquant avec un lot volé mais expiré invité ne peut rien faire. Son FIRE est rejeté pour cause de validité, l’écriture TIME dans la fenêtre fraîche lui est donc inaccessible. Hors de cette fenêtre le décalage reste plafonné à 24 heures, et le propriétaire remet l’heure juste à sa prochaine visite.
Si le propriétaire et trois invités écrivent des heures différentes en même temps, chaque écriture avance le cliquet si elle est plus grande, ou est rejetée si elle est plus petite. Ni contention ni course ici.
Micrologiciel du contrôleur :
TimeCallback : tenter la vérification avec owner_secret ; en cas d’échec, prendre le bleId dans la charge utile, dériver guest_key et réessayer.new_time ≥ time_ratchet.time_ratchet en NVS et le mettre à jour à chaque TIME accepté.settimeofday(ratchet), g_rtcValid = (ratchet > 0).Android:
BleCrypto.buildTimeSync — ajouter le bleId et le choix de la clé : owner_secret ou guest_key.maybeSyncTime par analogie avec celui du propriétaire, déclenché par le balayage passif et au plus une fois toutes les 30 minutes.Après cela :
Si le contrôleur est à portée du Wi-Fi, il est plus robuste de retirer complètement la synchronisation d’heure BLE du chemin critique et de prendre l’heure directement d’un serveur NTP.
Implémentation dans le micrologiciel (NTP_ENABLED=1):
CHAR_WIFI — le propriétaire y écrit le SSID et le mot de passe signés. Ils sont conservés en NVS.configTime() vers pool.ntp.org, time.google.com ou Cloudflare.NTP_RESYNC_INTERVAL_MS (6 heures par défaut).Sur piles, le contrôleur dort presque tout le temps et ne peut pas maintenir le Wi-Fi. La solution : les réveils ordinaires ne font que du BLE, dans une courte fenêtre d’environ 2,5 s, tandis que chaque DEEP_SLEEP_NTP_EVERY_N_WAKESréveil s’étire à 20 s et lève le Wi-Fi avec SNTP. Avec wake=3 s et N=7200, une synchronisation NTP a lieu environ toutes les 6 heures et le budget énergétique reste acceptable.
Tous les paramètres de la politique d’heure, de la veille et de NTP se trouvent dans le bloc CONFIG en tête du sketch. Ils s’ajustent selon le site :
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 (alimenté sur secteur), NTP_ENABLED = 1.NTP_ENABLED = 0, tout le reste uniquement en BLE.