← Voltar aos projetos

Projeto

Aditus: abrir uma porta com o telemóvel ou o relógio

Um protótipo de controlo de acessos em Flutter e Wear OS, com chaves RSA por dispositivo, autenticação por Bluetooth, um controlador ESP32 e um backend Flask.

Código-fonte ↗
Aditus num ecrã redondo com Wear OS, com a Sala do GLUA selecionada e um botão para abrir a porta.

O Aditus é um protótipo de controlo de acessos que desenvolvi entre o final de 2025 e o início de 2026. Fiz a aplicação Android, a aplicação Wear OS, o firmware do ESP32 e o backend Flask. A ideia era usar o telemóvel ou o relógio que uma pessoa já traz consigo para pedir acesso a uma sala, com uma forma de gerir os dispositivos e as permissões por trás desse pedido.

Uma pessoa pode ter um telemóvel, um tablet e um relógio. Perder o relógio não deveria obrigar a substituir as credenciais dos outros dois. Uma turma pode precisar de acesso a um laboratório, com uma exceção para um dos alunos. Estes casos influenciaram a base de dados e o processo de registo tanto como o botão de abrir a porta.

A demonstração original entre o relógio e o ESP32. Quando a autenticação é bem-sucedida, o LED da placa pisca durante três segundos; este protótipo não aciona uma fechadura física. Abrir o vídeo diretamente.

Quatro componentes, duas formas de comunicar

O telemóvel e o relógio comunicam com a porta por Bluetooth Low Energy. O ESP32 tem a sua própria ligação Wi-Fi ao backend, onde consulta permissões e obtém chaves públicas. Estes pedidos não passam pelo telemóvel.

Esta divisão é especialmente útil no relógio: depois de emparelhado, consegue completar a troca por BLE sem o telemóvel por perto nem uma ligação própria à Internet. O controlador continua a precisar de contactar o Flask. O telemóvel também verifica a sessão da conta online ao arrancar, pelo que o sistema não funciona inteiramente offline.

Ao arrancar, o ESP32 identifica-se junto do backend pelo seu endereço MAC de BLE. Depois de registado, recebe o identificador da porta e o nome a apresentar, e passa a anunciar-se com esse nome. Mudar o nome de uma sala não exige, por isso, recompilar o firmware.

ComponentePrincipais responsabilidades
Aplicação Flutter para telemóvelConfiguração da conta, registo de dispositivos, descoberta, abertura e administração
Aplicação Flutter para Wear OSEmparelhamento, seleção da porta com sinal mais forte e abertura
ESP32 / C++Características BLE, pedidos ao backend, verificação de assinaturas e estado do LED
Flask / SQLAlchemyContas, chaves, permissões, sessões de emparelhamento e registos de acesso em SQLite

Uma conta não é um dispositivo

Na primeira utilização, o telemóvel inicia sessão, configura um PIN de quatro a seis dígitos e regista-se com um nome. O registo gera um par de chaves RSA-2048 com o PointyCastle. A chave privada fica no armazenamento local cifrado; a chave pública passa a fazer parte de um registo de dispositivo associado ao seu proprietário.

Ecrã de registo de dispositivo que explica a geração local das chaves e o envio da chave pública.
O registo cria um par de chaves para este telemóvel em particular.
Um tablet, um Pixel Watch e um telemóvel listados separadamente, cada um com a sua opção de revogação.
É possível remover um dispositivo da conta e manter os restantes registos.

Há três mecanismos com funções diferentes. Os tokens JWT de acesso e de renovação autenticam os pedidos à API da conta. O PIN local ou a verificação biométrica controla a entrada na aplicação. A chave RSA assina o desafio enviado pelo controlador. Registar outro dispositivo não exige copiar uma chave privada ou um token de sessão do primeiro.

A tabela de dispositivos do backend guarda o proprietário, o nome e a chave pública. Apagar esse registo impede que um controlador obtenha a chave num pedido posterior. As chaves são geradas em Dart e guardadas como texto em formato PEM; não são chaves não exportáveis geradas no hardware do dispositivo Android.

Seguir um pedido de abertura por Bluetooth

A aplicação procura o serviço BLE do Aditus e ordena as portas encontradas por RSSI. Assim, é fácil encontrar o sinal mais forte, embora este seja apenas uma indicação aproximada da distância.

Lista de portas próximas com nomes, endereços Bluetooth e intensidade do sinal recebido.
O UUID do serviço identifica os controladores Aditus; os anúncios BLE fornecem os nomes das portas.
Janela de progresso da autenticação sobre a lista de portas próximas.
A interface acompanha a ligação, a autenticação, a assinatura e a abertura.

O protocolo usa quatro características GATT. Duas aceitam escritas do cliente; as outras duas permitem ao controlador enviar-lhe notificações:

CaracterísticaSentidoConteúdo
IdentidadeAplicação → ESP32JSON com user_id e device_id
DesafioESP32 → aplicaçãoDesafio de seis dígitos em texto
AssinaturaAplicação → ESP32Assinatura RSA codificada em Base64
EstadoESP32 → aplicaçãoAUTHORIZED ou um motivo DENIED_...

O cliente ativa as notificações de desafio e estado antes de escrever a sua identidade. Um pedido bem-sucedido segue esta sequência:

O percurso implementado para um pedido bem-sucedido. As consultas de permissões e chaves vão diretamente do controlador para o Flask.

O PointyCastle assina os bytes UTF-8 do desafio com SHA-256/RSA. O ESP32 descodifica a assinatura, interpreta a chave pública PEM e verifica-a com o mbedTLS. As duas implementações têm de concordar nos bytes e nos formatos, não apenas no nome do algoritmo.

Uma assinatura RSA-2048 ocupa 256 bytes, ou 344 caracteres em Base64. O cliente ativa explicitamente as escritas longas de BLE:

await _bleService.writeToCharacteristic(
  signatureUuid,
  signature,
  allowLongWrite: true,
);

Este excerto vem do DoorUnlockBloc, que coordena a troca de mensagens separadamente do ecrã. A espera pelo desafio e a espera pelo estado final têm, cada uma, um timeout de trinta segundos. Tanto a conclusão como os caminhos de erro terminam a ligação BLE. O ESP32 também acompanha o estado atual do pedido, rejeita assinaturas fora do estado de espera e regressa ao estado de repouso quando a ligação termina.

Emparelhar sem copiar as chaves do telemóvel

O telemóvel pede um código de emparelhamento de seis dígitos, válido durante cinco minutos. O Flask guarda-o numa PairingSession com o ID do utilizador, a hora de expiração e um indicador de utilização. O relógio gera o seu próprio par de chaves e envia o código, o nome e a chave pública para o endpoint de emparelhamento.

O telemóvel a apresentar um código temporário de emparelhamento do relógio e a contagem decrescente até à expiração.
O telemóvel inicia a sessão autenticada de emparelhamento.
Um ecrã redondo Wear OS com um campo para o código de seis dígitos e um botão de emparelhamento.
O relógio envia o código com a sua própria chave pública. Estas capturas mostram códigos de exemplo de sessões diferentes.

O backend verifica se o código existe, se ainda é válido e se não foi usado. Cria um dispositivo na mesma conta e marca a sessão como utilizada. A resposta dá ao relógio os IDs do dispositivo e do proprietário; este processo não lhe atribui JWTs como os usados pelo telemóvel.

No pulso, o ecrã principal apresenta a porta com o sinal mais forte e um botão para a abrir. Repor o relógio apaga as chaves e o registo locais. Revogar o registo no backend continua a ser uma ação separada, feita na lista de dispositivos do telemóvel.

Regras de acesso que se conseguem gerir

O telemóvel também permite gerir utilizadores, grupos e portas. O SQLAlchemy representa a pertença a grupos, as permissões diretas de utilizadores, as permissões de grupos e as respetivas exceções através de tabelas de associação. A propriedade do dispositivo é independente dessas permissões: uma chave pública permite provar a posse de um dispositivo registado, enquanto a consulta de permissões determina se o utilizador indicado pode entrar.

Detalhes da porta da Sala do GLUA, com estado, identificador Bluetooth e acesso à gestão de permissões.
A configuração da porta e as permissões são geridas aqui. O atalho para os registos de acesso ficou por implementar.
Janela com pesquisa para selecionar utilizadores a adicionar a um grupo.
Os grupos evitam ter de atribuir a mesma porta a cada pessoa individualmente.

A ordem de avaliação é concreta. Uma exceção de utilizador nega o acesso logo no início. Caso contrário, uma permissão direta autoriza-o. Na ausência dessa permissão, basta pertencer a um grupo autorizado que não tenha uma exceção para a porta. Uma exceção de grupo não anula uma permissão direta independente nem uma permissão atribuída através de outro grupo. Ser administrador dá privilégios de gestão, não acesso automático a todas as portas.

Os limites do protótipo

Os registos de acesso incluem o utilizador, o dispositivo, a porta, a data e hora, o resultado e o motivo de falha. O ecrã de histórico pessoal carrega vinte registos de cada vez. O Flask também disponibiliza filtros administrativos por utilizador, porta, dispositivo, data e resultado.

Histórico de acessos com entradas bem-sucedidas, uma falha por timeout e vinte registos carregados.
O ecrã original de histórico apresenta as tentativas guardadas. É preciso corrigir a diferença entre as rotas do firmware e do backend para receber novos registos do controlador de forma fiável.

A demonstração gravada liga o registo, a consulta de permissões e a verificação de assinaturas, mas instalar o sistema numa porta exige mais trabalho. O firmware desativa atualmente a verificação dos certificados TLS, usa um desafio pseudoaleatório curto e não associa explicitamente o ID de utilizador recebido ao proprietário do dispositivo. Também envia os registos para a rota errada. O relatório original descreve bloqueios intermitentes do controlador durante pedidos feitos pelo relógio, sem uma causa confirmada.

Estas limitações estão documentadas na referência técnica. O repositório inclui as quatro implementações, o relatório original e o material de demonstração.