Protección de la hora BLE frente a la falsificación

Por qué hoy el propietario no entrega la hora

En la versión 6.12, de la que se tomó el registro del controlador, el código de Android no tenía nada parecido: no había sincronización de hora en segundo plano desde el teléfono del propietario. La hora salía solo como efecto secundario de un fire del propietario (fase 3), y ni así llegaba: el controlador cortaba la conexión justo después de disparar el relé.

En la 6.13 apareció una conexión de fondo aparte: en cada detección pasiva del controlador, el teléfono del propietario abre su propia sesión GATT y escribe TIME. Pero la única fuente de hora sigue siendo el propietario.

Cualquier cliente de confianza debería poder entregar la hora

Si no, el cuadro es este: el propietario está de vacaciones, el controlador se queda sin corriente, arranca con rtc=0 y los invitados quedan fuera hasta que vuelva. Así no se construye.

A un invitado con un token válido hay que permitirle firmar TIME con su clave de invitado.

Cómo impedir la falsificación: un trinquete monótono, solo para invitados

La idea es sencilla: para un invitado la hora solo puede avanzar. Del propietario nos fiamos: la mueve en cualquier sentido y sin límites, algo necesario si alguien puso por error el año 2099 y hay que revertirlo.

El controlador guarda la última hora conocida en la NVS (time_ratchet). Ante una escritura TIME de invitado:

Ante una escritura TIME del propietario, el trinquete se sobrescribe sin más con su valor, en cualquier sentido.

Tras reiniciarse, el controlador arranca desde rtc = ratchet y no desde 0. El RTC sigue contando desde ahí. Un reinicio de fábrica borra también el trinquete, pero el emparejamiento le escribe enseguida la hora actual.

Por qué esto detiene a un invitado malicioso

El invitado atacante quiere prolongar un token cuyo valid_until = X). ¿Qué puede intentar?

Opción 1: atrasar la hora (poner el reloj en ayer para que su token parezca fresco):
→ el trinquete no lo permite. Rechazado.

Opción 2: adelantar la hora (adelantar el reloj un año):
→ el controlador lo acepta, porque la hora avanzó de verdad.
→ PERO ahora el controlador compara valid_until=X con now = a year aheadX < now → y su propio token ha caducado.
→ El atacante solo se ha perjudicado a sí mismo.

El único movimiento honesto de un invitado es poner la hora real. Cualquier falsificación se rechaza o se vuelve en su contra.

Quién firma TIME

El controlador debe aceptar una escritura TIME firmada con:

A la carga útil de TIME se añaden dos bytes: bleId. Luego el controlador prueba:

  1. el HMAC con owner_secret: si cuadra, se acepta.
  2. Si no, derivar guest_key para ese bleId y comprobar de nuevo.

Sin un paquete de invitado legítimo emitido por el servidor no se puede firmar TIME: el atacante no tiene la clave.

Protección adicional: una puerta fire primero dentro de la ventana fresca

Si la última sincronización correcta fue hace menos de una hora, el controlador considera fresco su reloj y no ve motivo para actualizarlo porque sí. Dentro de esa ventana una escritura TIME se acepta solo si el cliente hizo un FIRE correcto en los últimos 10 segundos: prueba de que tiene un token válido y no caducado.

Si han pasado más de una hora la puerta del fire se desactiva y TIME se acepta sin más, siempre con el tope de +24 h para invitados.

Qué cierra esto: un atacante con un paquete robado pero caducado de invitado no puede hacer nada. Su FIRE se rechaza por caducidad, así que la escritura TIME dentro de la ventana fresca queda fuera de su alcance. Fuera de la ventana el desplazamiento sigue limitado a 24 horas y el propietario pone la hora correcta en su siguiente visita.

Un extra: sin colisiones

Si el propietario y tres invitados escriben horas distintas a la vez, cada escritura avanza el trinquete si es mayor o se rechaza si es menor. Aquí no hay disputa ni carrera.

Qué hay que hacer

Firmware del controlador:

Android:

Después de eso:

NTP como fuente principal (opcional)

Si el controlador está dentro del alcance del Wi-Fi, es más robusto sacar del todo la sincronización de hora por BLE de la ruta crítica y tomar la hora directamente de un servidor NTP.

Implementación en la firmware (NTP_ENABLED=1):

NTP junto con el sueño profundo

Con pilas, el controlador duerme casi todo el tiempo y no puede mantener el Wi-Fi. La solución: los despertares normales trabajan solo por BLE, en una ventana corta de unos 2,5 s, mientras que cada DEEP_SLEEP_NTP_EVERY_N_WAKESdespertar se alarga a 20 s y levanta el Wi-Fi con SNTP. Con wake=3 s y N=7200 la sincronización NTP ocurre cada 6 horas aproximadamente y el consumo sigue siendo aceptable.

Constantes configurables de la firmware

Todos los parámetros de la política de hora, el sueño y NTP están en el bloque CONFIG al principio del sketch. Se pueden ajustar por instalación: