Aula 18 · Semana 8 · Quarta, 28 de outubro de 2026 · 16:00 às 18:00
Segurança, performance e sprint 1
Teoria em duas partes. Segurança: o que um APK expõe, OWASP Mobile Top 10, segredos, pinning, permissões mínimas e privacidade. Desempenho em Flutter: isolates, rebuilds, listas e imagens, medições em modo profile. Prática: sprint 1, com um exercício de medição na app de cada grupo; a auditoria de segredos e permissões fica para casa. A sprint 1 fecha na aula seguinte.
Sprint 1 · último dia
No fim desta aula consegues
Identificar os riscos do OWASP Mobile Top 10 que mais aparecem em apps Flutter.
Tirar os segredos do repositório e passá-los com --dart-define ou ficheiro ignorado pelo git.
Explicar o que o certificate pinning protege e o que custa manter.
Pedir só as permissões necessárias, no momento certo, e cumprir o mínimo de privacidade: RGPD, App Tracking Transparency e formulários das lojas.
Explicar o que é um isolate e quando usar compute() em vez de async/await.
Identificar rebuilds desnecessários e reduzi-los com const, divisão de widgets e Selector.
Medir a tua app em modo profile com o performance overlay e o DevTools, e dizer o que é jank.
Fechar a sprint 1 com PRs merged, app a correr e demo preparada.
OWASP Mobile Top 10: os que mais aparecem
Risco (lista de 2024)
Como aparece numa app Flutter
Defesa
M1 Credenciais mal usadas
Client-id e segredo no código; password guardada em claro
Secure storage, tokens curtos, segredos fora do binário
M3 Autenticação e autorização inseguras
Verificar "é admin" só no cliente; confiar num campo do JWT sem o servidor validar
O servidor decide sempre; o cliente só mostra ou esconde
M5 Comunicação insegura
HTTP em claro; aceitar qualquer certificado (badCertificateCallback a devolver true)
HTTPS sempre; pinning quando se justifica
M8 Configuração insegura
Backup a incluir tokens; logs com dados pessoais; debuggable em release
Release limpo, backup filtrado, logs só em debug
M9 Armazenamento inseguro
Token em shared_preferences; base de dados com dados pessoais em claro
Keychain/Keystore; cifrar o que importa
Para os testes: os três que mais aparecem são segredos no código, armazenamento inseguro e comunicação insegura.
Segredos: o que um atacante tira de um APK em 5 minutos
Tudo o que está no APK é público: o segredo fica no servidor ou entra por dart-define no build, e nunca vai para o git.
Um APK é um zip: unzip app.apk, strings libapp.so | grep -i secret. O jadx descompila o Kotlin; o Dart compilado em AOT continua a ter as strings legíveis.
Consequência: tudo o que vai no binário é público. Um "segredo" no cliente não é segredo, é ofuscação.
Nunca no repositório:.env.json no .gitignore; --dart-define-from-file=.env.json em desenvolvimento; no CI vêm de GitHub Secrets.
Se já foi commitado, está comprometido: rodar o segredo (pedir outro ao docente). Apagar do histórico não chega: alguém já clonou.
O que pode ir no cliente: chaves públicas com restrição (Maps limitada ao package), identificadores sem poder. O que não pode: chaves de API com escrita, passwords, client secrets de OAuth.
Para os testes: tudo o que está no APK é público; segredos vivem no servidor ou entram por dart-define e nunca no git.
Analogia Um APK é um zip: qualquer pessoa o abre e vê o que está lá dentro. Pôr um segredo no código é escrever a password num papel e colá-lo à porta. Ofuscar é escrever o papel com letra pequena: continua na porta. O único sítio onde a password não está à vista é do lado de dentro, no servidor.
HTTPS sempre; certificate pinning quando vale a pena
HTTPS é obrigatório: o Android bloqueia tráfego em claro desde a versão 9; o iOS exige TLS 1.2+ via App Transport Security. Desligar isto para "testar" é o erro M5.
HTTPS protege de quem está no meio (Wi-Fi do café), mas confia em qualquer CA instalada no sistema. Um proxy com certificado instalado (Charles, mitmproxy) vê tudo.
Pinning: a app só aceita o certificado ou a chave pública que conhece. O proxy deixa de funcionar; uma CA comprometida também.
Custo: quando o certificado roda, as versões antigas da app deixam de ligar. Precisa de pin de reserva, plano de rotação e forced update (aula 30).
Quem faz: bancos, saúde, apps com ataque dirigido. Uma app de meteorologia não precisa. Em Flutter: SecurityContext ou http_certificate_pinning.
Para os testes: pinning protege contra CAs comprometidas e proxies, ao custo de manutenção sempre que o certificado roda.
Permissões mínimas, pedidas no momento certo
Cada permissão é uma pergunta ao utilizador e uma linha na revisão da loja. Pede só o que a funcionalidade precisa: localização aproximada chega para "localidade mais próxima".
No momento certo: quando toca em "Perto de mim", não no arranque. Um ecrã a explicar antes do diálogo do sistema (pre-prompt) sobe a taxa de aceitação.
Justificação obrigatória:NSLocationWhenInUseUsageDescription no iOS e a declaração de uso no Google Play. Texto vago ("precisamos da localização") é motivo de rejeição.
Negação: a app continua a funcionar (lista completa em vez de "perto de mim"). Negação permanente: botão que abre as Definições, sem insistir.
O que as lojas rejeitam: permissões sem funcionalidade visível, localização em background sem razão forte, SMS e registo de chamadas fora das apps por defeito.
Para os testes: permissão mínima, pedida em contexto, com justificação, e a app funciona se for negada.
Privacidade: RGPD, ATT, labels e data safety
RGPD: base legal para cada dado recolhido, minimização, direito a apagar. Consentimento explícito e não pré-marcado para analytics e marketing.
App Tracking Transparency (iOS 14.5+): diálogo obrigatório antes de rastrear o utilizador entre apps. Sem "Permitir", sem IDFA. A Apple rejeita quem tenta contornar.
Privacy nutrition labels (App Store) e Data safety form (Play): declaras o que recolhes e porquê. Uma declaração falsa é motivo de remoção.
Na prática: política de privacidade com URL público, mesmo para uma app de tempo. Logs e crash reports sem dados pessoais.
No Tempo Açores: localização usada uma vez para a localidade mais próxima e não guardada; favoritos ligados ao grupo, não a pessoas.
Para os testes: o ATT pede-se antes de rastrear; as labels e o data safety form têm de bater certo com o que a app faz.
Ofuscação e deteção de root em uma frase cada
flutter build apk --obfuscate --split-debug-info=build/symbols renomeia os símbolos Dart. Dificulta ler o binário; não esconde strings nem segredos.
Guarda a pasta split-debug-info por versão: sem ela os stack traces dos crashes em produção são ilegíveis (aula 26).
A minificação Android (R8) está ativa por defeito em release. Alguns plugins precisam de regras ProGuard; o flutter build avisa.
Root e jailbreak: detetar dispositivo comprometido e limitar funcionalidades. Os bancos fazem-no; é contornável; não substitui nenhuma das defesas anteriores.
Para os testes: ofuscação dificulta a engenharia inversa mas não protege segredos; a deteção de root é uma camada extra, não uma garantia.
Isolates: memória separada, trabalho pesado fora da UI
Cada isolate tem a sua memória; o que passa entre eles é copiado por mensagem, e a UI nunca espera pelo cálculo.
Dart corre numa thread com um event loop. async/await liberta a thread enquanto espera (rede, disco), mas não liberta nada enquanto calcula.
Um isolate é outra thread com a sua própria memória. Não partilha objetos: o que entra e sai é copiado por mensagem.
compute(funcao, argumento) (ou Isolate.run) cria um isolate, corre a função e devolve o resultado como Future.
Usa-o para: JSON grande (centenas de KB), ordenar ou filtrar milhares de itens, processar imagens, cifrar.
Não pode passar entre isolates: BuildContext, controllers, sockets, instâncias de plugins. Passa dados simples e modelos.
import 'dart:convert';
import 'package:flutter/foundation.dart';
List<Previsao> _parsePrevisoes(String corpo) {
final lista = jsonDecode(corpo) as List<dynamic>;
return lista.map((e) => Previsao.fromJson(e as Map<String, dynamic>)).toList();
}
// A função tem de ser top-level ou static: vai correr noutro isolate.
Future<List<Previsao>> parsePrevisoes(String corpo) => compute(_parsePrevisoes, corpo);
Para os testes:async/await resolve espera; isolates resolvem cálculo. Um parse de 5 MB com await continua a bloquear a interface.
Analogia Dois cozinheiros em cozinhas separadas, cada um com os seus tachos (a memória). Não partilham nada; passam pratos pela janela, e o que passa é uma cópia (as mensagens entre isolates). O cozinheiro da sala (o isolate principal) nunca deixa a sala vazia: enquanto o outro descasca o JSON de 5 MB, ele continua a atender os clientes, que são os frames.
Rebuilds: baratos, mas não grátis
setState reconstrói o widget inteiro e tudo o que está abaixo. Se o ecrã inteiro é um StatefulWidget, um contador a mudar reconstrói a lista toda.
const: um widget const é criado uma vez e reutilizado; o framework salta-o no rebuild. O lint prefer_const_constructors aponta os candidatos.
Dividir widgets: o que muda fica num widget pequeno com o seu próprio estado; o que não muda fica fora.
Selector e context.select: reconstruir só quando o campo que interessa muda, e não a cada notifyListeners().
Chaves: ValueKey(localidade.id) em itens de lista que se reordenam ou removem, para o framework reutilizar o estado certo.
Para os testes:build correr muitas vezes é normal; o problema é reconstruir muitos widgets que não mudaram.
Listas e imagens
Problema
Sintoma
Solução
ListView(children: [...]) com 2000 itens
Arranque lento; constrói tudo de uma vez
ListView.builder: só constrói o que está visível
Itens com altura variável
Scroll a saltar, layout caro por frame
itemExtent ou prototypeItem quando a altura é fixa
Scroll rápido mostra itens em branco
Constrói tarde
cacheExtent maior (pré-constrói fora do ecrã)
Foto de 4000×3000 num cartão de 300 px
48 MB de memória por imagem; a app morre em Android com pouca RAM
cacheWidth/cacheHeight; servidor com miniaturas
Imagens de rede recarregadas a cada scroll
Rede e bateria gastas, flicker
cached_network_image: cache em disco e memória, placeholder
Para os testes: a memória de uma imagem é largura × altura × 4 bytes, descodificada, e não o tamanho do ficheiro.
Frames: medir em profile, nunca em debug
60 fps dá 16,6 ms por frame; 120 fps dá 8,3 ms. Cada frame passa pela thread de UI (Dart, build, layout) e pela thread de raster (pintar). Se uma delas passa do orçamento, o frame salta: jank.
Performance overlay (P no terminal do flutter run, ou showPerformanceOverlay: true): dois gráficos, raster em cima e UI em baixo. Barra vermelha = frame lento.
Modo debug corre JIT, com asserts e verificações extra: é 2 a 5 vezes mais lento e não representa nada. Mede com flutter run --profile num telemóvel real.
Shaders: com Impeller (por defeito em iOS e Android atuais) o jank da primeira animação praticamente desapareceu; o antigo SkSL warm-up era um remendo do Skia.
DevTools → Performance: timeline de frames, o que demorou em cada um, e o botão Enhance tracing para ver builds e layouts.
Para os testes: só se mede desempenho em modo profile ou release, num dispositivo físico.
Analogia O ecrã pede uma imagem nova 60 vezes por segundo, e cada pedido tem 16 ms para ser servido. Um jsonDecode de 80 ms é um cliente que ocupa o balcão durante cinco pedidos seguidos: os outros ficam à espera e o ecrã encrava. É esse o jank que o overlay mostra a vermelho.
Bateria e rede
O rádio (Wi-Fi, 4G/5G) é dos maiores consumidores. Cada pedido acorda-o e mantém-no ligado alguns segundos: agrupa pedidos em vez de fazer dez seguidos.
Sem polling. Perguntar ao servidor "há novidades?" de 30 em 30 segundos esgota a bateria. Alternativas: push (aula 19), sincronização em background com restrições (aula 23), atualizar ao abrir.
Localização só quando precisa, com a precisão mínima que chega. O GPS ligado em contínuo esgota um telemóvel em poucas horas (aula 21).
Respeita o modo de poupança e o Data Saver: menos animações, menos imagens, sem pré-carregar.
Exemplos: o Instagram pré-carrega imagens em lote enquanto o rádio já está acordado; o Google Maps só liga o GPS de alta precisão em navegação.
Para os testes: o custo de um pedido não é só os bytes; é acordar o rádio.
Sprint 1: ponto de situação
Cada grupo, um minuto, em pé, sem ligar o portátil:
O que está merged em main e corre no telemóvel neste momento.
O que está em PR aberto e quem está a rever.
O que não vai entrar na sprint 1 e passa para a sprint 2. Dizer isto agora é melhor do que na segunda, na review.
Um bloqueio, se houver. O docente resolve primeiro os bloqueios, depois circula.
Segunda é review Demo de 5 minutos por grupo, no telemóvel, a partir de main. Não se mostra código em branch.
Exercício de 20 minutos: mede a tua app
Corre em profile com o telemóvel ligado por cabo:
flutter run --profile
Liga o overlay: carrega em P no terminal. Faz scroll na lista de localidades, abre um detalhe, volta. Procura barras vermelhas.
Abre o DevTools (v no terminal) → Inspector → ativa Track widget builds. Interage outra vez e vê que widgets se reconstroem a cada toque.
Encontra um rebuild desnecessário: um widget que se reconstrói sem os seus dados mudarem. Corrige com const, extraindo um widget, ou com Selector.
Encontra um widget que pode ser const e ainda não é. Corre flutter analyze: o lint prefer_const_constructors ajuda.
Exercício 1 Abre um PR com as duas correções e, na descrição, uma captura do overlay antes e depois. Um colega de outro grupo revê. Se a tua app não tem lista com dados reais ainda, mede a solucao/ da aula 14.
O que interessa quando estás a medir
Uma barra vermelha isolada ao abrir um ecrã é normal (primeira construção). Barras vermelhas seguidas durante scroll são o que procuras.
Se o gráfico de cima (raster) está alto, o problema é pintar: imagens grandes, sombras, Opacity em listas, ClipRRect em cada item.
Se o gráfico de baixo (UI) está alto, o problema é Dart: build pesado, parse na thread principal, lógica dentro de build.
Um ecrã inteiro a reconstruir quando só mudou um texto quer dizer que o setState ou o watch está alto demais na árvore.
Não otimizes o que não mediste. Um const a mais não faz mal; uma arquitetura torcida por um ganho imaginário faz.
Fechar a sprint 1 até segunda às 16:00
Definição de pronto
Todos os PRs da sprint merged ou fechados com justificação.
main compila e corre no telemóvel de cada membro.
flutter analyze sem avisos.
Estados de loading, erro e vazio tratados nos ecrãs entregues.
Nenhum segredo no repositório (a auditoria do slide seguinte).
Demo de 5 minutos
1 min: qual era o objetivo da sprint.
3 min: a app a funcionar, no telemóvel, projetada.
1 min: o que ficou por fazer e porquê.
Cada membro fala da parte que fez. Ensaiem uma vez.
Para casa: auditoria de segredos e permissões
Segredos no histórico e no código. Corre no repositório do grupo:
Qualquer resultado com um valor real é um achado. Um nome de variável não é.
.gitignore tem de incluir .env.json, *.jks, key.properties e *.keystore. Confirma com git check-ignore -v .env.json.
AndroidManifest.xml: lista cada <uses-permission>. Para cada um, há uma funcionalidade que o usa? Verifica também android:allowBackup e usesCleartextTraffic.
Info.plist: cada chave NS…UsageDescription tem um texto real e específico? Não há NSAllowsArbitraryLoads?
Regista o resultado na issue "Auditoria de segurança": o que foi encontrado, o que foi corrigido, quem corrigiu. Fecha-a por PR.
São 15 minutos por grupo. Faz-se em casa, ou no fim da aula se o exercício de medição acabar cedo.
Exercício 2 Até segunda, antes da review: auditoria feita, issue preenchida e, se houve segredo, rodado (pede um novo ao docente) e movido para .env.json com --dart-define-from-file.
Até segunda
Para fazer
Sprint 1 fecha segunda: PRs merged, app a correr em main, demo de 5 minutos preparada e ensaiada.
PR do exercício de medição aberto e revisto por alguém de fora do grupo.
Auditoria de segredos e permissões feita no repositório do grupo: qualquer segredo encontrado fica fora do repositório e é rodado; issue "Auditoria de segurança" fechada por PR.
Instala o scrcpy ou ativa o espelhamento do telemóvel para a demo não depender do cabo do docente.
Para ler
OWASP Mobile Top 10: a lista completa de 2024, com exemplos e defesas por risco; M1, M5 e M9 são os desta aula.
Flutter: Security: como o Flutter trata vulnerabilidades, e as recomendações oficiais para uma app em produção.
Obfuscate Dart code: o que a ofuscação faz e não faz, e como guardar os símbolos para ler crashes.