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.
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.
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:
new_time < ratchet → rejeita-se como retrocesso.new_time ≥ ratchet → aceita-se, avança-se o roquete e escreve-se na NVS.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.
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 ahead → X < 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.
O controlador deve aceitar uma escrita TIME assinada com:
À carga útil do TIME acrescentam-se dois bytes: bleId. Depois o controlador tenta:
Sem um pacote de convidado legítimo emitido pelo servidor não se pode assinar TIME — o atacante não tem a chave.
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.
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.
Firmware do controlador:
TimeCallback: tentar verificar com owner_secret; se falhar, tirar o bleId da carga útil, derivar guest_key e tentar de novo.new_time ≥ time_ratchet.time_ratchet na NVS e atualizá-lo a cada TIME aceite.settimeofday(ratchet), g_rtcValid = (ratchet > 0).Android:
BleCrypto.buildTimeSync — acrescentar o bleId e a escolha da chave: owner_secret ou guest_key.maybeSyncTime por analogia com o do proprietário, acionado pela varredura passiva e não mais de uma vez a cada 30 minutos.Depois disso:
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):
CHAR_WIFI — o proprietário escreve aí o SSID e a palavra-passe assinados. Ficam guardados na NVS.configTime() contra pool.ntp.org, time.google.com ou Cloudflare.NTP_RESYNC_INTERVAL_MS (6 horas por omissão).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.
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:
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 (alimentado pela rede), NTP_ENABLED = 1.NTP_ENABLED = 0, todo o resto apenas por BLE.