PDM · DAW 2

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

OWASP Mobile Top 10: os que mais aparecem

Risco (lista de 2024)Como aparece numa app FlutterDefesa
M1 Credenciais mal usadasClient-id e segredo no código; password guardada em claroSecure storage, tokens curtos, segredos fora do binário
M3 Autenticação e autorização insegurasVerificar "é admin" só no cliente; confiar num campo do JWT sem o servidor validarO servidor decide sempre; o cliente só mostra ou esconde
M5 Comunicação inseguraHTTP em claro; aceitar qualquer certificado (badCertificateCallback a devolver true)HTTPS sempre; pinning quando se justifica
M8 Configuração inseguraBackup a incluir tokens; logs com dados pessoais; debuggable em releaseRelease limpo, backup filtrado, logs só em debug
M9 Armazenamento inseguroToken em shared_preferences; base de dados com dados pessoais em claroKeychain/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

app.apk é um zip unzip app.apk classes.dex Kotlin compilado: o jadx descompila lib/arm64/libapp.so Dart AOT: as strings continuam legíveis assets/ imagens, JSON e um .env esquecido API_KEY = "sk_live_…" chave em texto: pública Tudo o que vai no binário é público. apktool / strings em 5 minutos Atacante lê a chave e usa-a como se fosse a app Em vez disso Segredo no servidor a app pede; a chave nunca sai de lá --dart-define no build .env.json fora do git; no CI, Secrets Nunca no repositório commitado? está comprometido: roda-o
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.

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

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

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

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

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

Isolate principal (UI) event loop: build, toques, animações Memória própria widgets, estado, BuildContext, controllers Isolate de trabalho compute(_parsePrevisoes, corpo) Memória própria só o que recebeu por mensagem: a String corpo (cópia) resultado (cópia) memória separada Linha do tempo a UI continua a 60 fps enquanto o worker trabalha UI · 60 fps Worker compute(_parse, corpo) compute() chamado Future completa → setState
Cada isolate tem a sua memória; o que passa entre eles é copiado por mensagem, e a UI nunca espera pelo cálculo.
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

Para os testes: build correr muitas vezes é normal; o problema é reconstruir muitos widgets que não mudaram.

Listas e imagens

ProblemaSintomaSolução
ListView(children: [...]) com 2000 itensArranque lento; constrói tudo de uma vezListView.builder: só constrói o que está visível
Itens com altura variávelScroll a saltar, layout caro por frameitemExtent ou prototypeItem quando a altura é fixa
Scroll rápido mostra itens em brancoConstrói tardecacheExtent maior (pré-constrói fora do ecrã)
Foto de 4000×3000 num cartão de 300 px48 MB de memória por imagem; a app morre em Android com pouca RAMcacheWidth/cacheHeight; servidor com miniaturas
Imagens de rede recarregadas a cada scrollRede e bateria gastas, flickercached_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

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

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:

  1. O que está merged em main e corre no telemóvel neste momento.
  2. O que está em PR aberto e quem está a rever.
  3. 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.
  4. 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

  1. Corre em profile com o telemóvel ligado por cabo:
    flutter run --profile
  2. Liga o overlay: carrega em P no terminal. Faz scroll na lista de localidades, abre um detalhe, volta. Procura barras vermelhas.
  3. Abre o DevTools (v no terminal) → Inspector → ativa Track widget builds. Interage outra vez e vê que widgets se reconstroem a cada toque.
  4. 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.
  5. 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

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

  1. Segredos no histórico e no código. Corre no repositório do grupo:
    git log -p | grep -inE 'secret|token|password|api_key' | head -20
    grep -rniE 'secret|password' lib/ android/app/src/main/ ios/Runner/
    Qualquer resultado com um valor real é um achado. Um nome de variável não é.
  2. .gitignore tem de incluir .env.json, *.jks, key.properties e *.keystore. Confirma com git check-ignore -v .env.json.
  3. AndroidManifest.xml: lista cada <uses-permission>. Para cada um, há uma funcionalidade que o usa? Verifica também android:allowBackup e usesCleartextTraffic.
  4. Info.plist: cada chave NS…UsageDescription tem um texto real e específico? Não há NSAllowsArbitraryLoads?
  5. 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