Proteção da hora BLE contra falsificação

Porque hoje o proprietário não entrega a hora

Na versão 6.12, de onde foi tirado o registo do controlador, o código Android não tinha nada disso — não havia sincronização de hora em segundo plano a partir do telefone do proprietário. A hora só saía como efeito secundário de um fire do proprietário (fase 3), e mesmo assim não chegava: o controlador cortava a ligação logo após o relé disparar.

Na 6.13 surgiu uma ligação de fundo separada: em cada deteção passiva do controlador, o telefone do proprietário abre a sua própria sessão GATT e escreve TIME. Mas a única fonte de hora continua a ser o proprietário.

Qualquer cliente de confiança deveria poder entregar a hora

Caso contrário o quadro é este: o proprietário está de férias, o controlador fica sem energia, reinicia com rtc=0 — e os convidados ficam de fora até ele voltar. Assim não se constrói.

A um convidado com um token válido deve permitir-se assinar TIME com a sua chave de convidado.

Como impedir a falsificação — um roquete monótono, só para convidados

A ideia é simples: para um convidado a hora só pode avançar. No proprietário confia-se: ele move-a em qualquer sentido e sem limites — necessário se alguém pôs por engano o ano 2099 e é preciso reverter.

O controlador guarda a última hora conhecida na NVS (time_ratchet). Numa escrita TIME de convidado:

Numa escrita TIME do proprietário, o roquete é simplesmente substituído pelo valor dele, em qualquer sentido.

Depois de reiniciar, o controlador arranca a partir de rtc = ratchet e não de 0. O RTC continua a contar a partir daí. Uma reposição de fábrica apaga também o roquete, mas o emparelhamento escreve-lhe logo a hora atual.

Porque isto trava um convidado mal-intencionado

O convidado atacante quer prolongar um token cujo valid_until = X). O que pode tentar?

Opção 1 — atrasar a hora (pôr o relógio em ontem para o token parecer fresco):
→ o roquete não deixa. Recusado.

Opção 2 — adiantar a hora (adiantar o relógio um ano):
→ o controlador aceita, porque a hora avançou mesmo.
→ MAS o controlador compara agora valid_until=X com now = a year aheadX < now → e o seu próprio token expirou.
→ O atacante só prejudicou a si próprio.

A única jogada honesta de um convidado é pôr a hora real. Qualquer falsificação é rejeitada ou vira-se contra ele.

Quem assina TIME

O controlador deve aceitar uma escrita TIME assinada com:

À carga útil do TIME acrescentam-se dois bytes: bleId. Depois o controlador tenta:

  1. o HMAC com owner_secret — se conferir, aceita-se.
  2. Caso contrário, derivar guest_key para esse bleId e verificar de novo.

Sem um pacote de convidado legítimo emitido pelo servidor não se pode assinar TIME — o atacante não tem a chave.

Proteção adicional: um portão fire primeiro dentro da janela fresca

Se a última sincronização bem-sucedida foi há menos de uma hora, o controlador considera o seu relógio fresco e não vê motivo para o atualizar do nada. Dentro dessa janela uma escrita TIME só é aceite se o cliente tiver feito um FIRE bem-sucedido nos últimos 10 segundos — prova de que tem um token válido e não expirado.

Se passou mais de uma hora o portão do fire desliga-se e o TIME é aceite livremente, sempre com o limite de +24 h para convidados.

O que isto fecha: um atacante com um pacote roubado mas expirado de convidado não consegue nada. O seu FIRE é rejeitado por validade, por isso a escrita TIME dentro da janela fresca fica fora de alcance. Fora dessa janela o desvio continua limitado a 24 horas e o proprietário acerta a hora na visita seguinte.

Um extra: sem colisões

Se o proprietário e três convidados escreverem horas diferentes ao mesmo tempo, cada escrita avança o roquete se for maior ou é rejeitada se for menor. Não há disputa nem corrida.

O que é preciso fazer

Firmware do controlador:

Android:

Depois disso:

NTP como fonte principal (opcional)

Se o controlador estiver ao alcance do Wi-Fi, é mais robusto tirar por completo a sincronização de hora por BLE do caminho crítico e obter a hora diretamente de um servidor NTP.

Implementação no firmware (NTP_ENABLED=1):

NTP em conjunto com o sono profundo

A pilhas, o controlador dorme quase sempre e não consegue manter o Wi-Fi. A solução: os despertares normais funcionam só por BLE, numa janela curta de cerca de 2,5 s, enquanto cada DEEP_SLEEP_NTP_EVERY_N_WAKESdespertar se estende a 20 s e levanta o Wi-Fi com SNTP. Com wake=3 s e N=7200 a sincronização NTP acontece cerca de cada 6 horas e o consumo mantém-se aceitável.

Constantes configuráveis do firmware

Todos os parâmetros da política de hora, do sono e do NTP estão no bloco CONFIG no início do sketch. Podem ser ajustados por instalação: