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.
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.
| Componente | Principais responsabilidades |
|---|---|
| Aplicação Flutter para telemóvel | Configuração da conta, registo de dispositivos, descoberta, abertura e administração |
| Aplicação Flutter para Wear OS | Emparelhamento, 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 / SQLAlchemy | Contas, 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.
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.
O protocolo usa quatro características GATT. Duas aceitam escritas do cliente; as outras duas permitem ao controlador enviar-lhe notificações:
| Característica | Sentido | Conteúdo |
|---|---|---|
| Identidade | Aplicação → ESP32 | JSON com user_id e device_id |
| Desafio | ESP32 → aplicação | Desafio de seis dígitos em texto |
| Assinatura | Aplicação → ESP32 | Assinatura RSA codificada em Base64 |
| Estado | ESP32 → aplicação | AUTHORIZED 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 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 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.
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.
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.