DEV Community

Cover image for Evoluindo a Segurança de Transações em Apps Mobile: Da Senha ao Device Binding
Fabricio_Gonçalves
Fabricio_Gonçalves

Posted on

Evoluindo a Segurança de Transações em Apps Mobile: Da Senha ao Device Binding

Salve, pessoal! Depois de iniciar minha trilha de gestão, tem me faltado tempo pra experimentar algumas coisas no mundo de desenvolvimento, mas essa semana me deparei com um problema que para alguns deve ser algo rotineiro, mas que para mim não é.

Estou falando simplesmente de garantir uma transação segura por meio de aparelhos móveis. Fácil, não é mesmo? 😅

Me dei conta que poderia não ser... por isso resolvi fazer um POC e entender melhor como podemos fazer uma transação de forma segura. Claro, não é um problema do meu dia a dia, posso no final estar falando groselha, e se for, fique à vontade para aparecer nos comentários ;)

Pois bem. Ainda que seja para falar de um tema cabeludo aqui, o objetivo não é entrar nas profundezas desse tema falando de criptografia quântica e o escambau, mas somente ilustrar um fluxo evolutivo de uma forma que, pra mim, parece aceitável de se lidar com esse problema.

Disclaimer: Este post foca em conceitos didáticos para iniciantes. Em produção, sempre consulte especialistas em segurança e siga as melhores práticas da sua organização.

🎯 O Que Vamos Abordar

  • Por que senhas simples não bastam
  • Evolução progressiva de segurança (do jeito que fui entendendo)
  • JWT, biometria e device binding na prática
  • Implementação de múltiplos fatores de autenticação
  • Fallbacks para dispositivos sem biometria

❌ Tentativa 1: O Básico Inseguro (Meus Tempos de Inocência)

No passado muito distante, quando nem sabia o que era um JWT, era muito comum pensar que um fluxo onde o app faz a autenticação pra logar no aparelho e depois, no máximo, colocando novamente a senha, já estaria apto a fazer uma transação.

O cenário que eu achava suficiente:

🔓 Usuário faz login
🔓 Para cada transação: digita senha novamente
💸 Executa transação
Enter fullscreen mode Exit fullscreen mode

Mas isso tem alguns riscos bem graves que só fui perceber depois:

  • Alguém com acesso ao aparelho pode facilmente reutilizar a sessão
  • Nenhuma prova é exigida na hora da transação de que quem está enviando é realmente o dono da conta
  • Vulnerabilidade a shoulder surfing: Senhas podem ser observadas
  • Sem contexto: Nenhuma informação sobre dispositivo ou localização

⚠️ Tentativa 2: Melhorando com JWT (Descobrindo Tokens)

Não muito diferente da primeira, mas já com a evolução de usar o JWT. Então o usuário loga no sistema, recebe seu JWT, e usa esse JWT (mais a senha, talvez) para efetuar a transação.

O fluxo que pensei ser mais esperto:

🔐 Login → recebe JWT
🔐 Transação → envia JWT + (opcional) senha
💸 Servidor valida e executa
Enter fullscreen mode Exit fullscreen mode

Ganhos em relação à tentativa 1:

  • ✅ Token curto, com validade controlada
  • ✅ Possibilidade de usar claims no JWT para indicar escopos ou expiração específica para transações
  • ✅ Stateless (servidor não precisa armazenar sessões)

Ainda assim, há problemas sérios:

  • O JWT pode ser interceptado se não houver HTTPS (ou se tiver alguma falha na implementação)
  • Não há fator de prova adicional no momento da transação
  • Não está claro se quem usa o JWT é realmente o dono do aparelho
  • Falta de contexto transacional: JWT não está vinculado à transação específica

🛡️ Tentativa 3: Adicionando Biometria (Achando que Resolvi Tudo)

A abordagem agora, um pouco mais elaborada, pode ser dividida em 2 frentes, que depois podem se completar:

3.1 Biometria

Os aparelhos móveis evoluíram bastante. Não é difícil encontrar aparelhos com a capacidade de usar biometria para reconhecer o "dono" do aparelho. Adicionando isso no fluxo, garantimos que só o dono do aparelho possa autorizar uma transação.

O que achei que seria a solução final:

🔐 Login → recebe JWT
👤 Biometria local confirma identidade
🔐 Transação → envia JWT
💸 Servidor executa
Enter fullscreen mode Exit fullscreen mode

Melhorias:

  • ✅ Prova local de identidade
  • ✅ Experiência do usuário melhorada
  • ✅ Segurança adicional contra uso não autorizado

Mas, calma... 🤔

A biometria por si só não protege contra sequestro de token (JWT), por exemplo. O servidor não tem como saber se a transação realmente passou pela verificação biométrica. Ou seja, a autenticação local não garante integridade da transação.

Um atacante pode simplesmente interceptar o JWT e fazer a requisição diretamente, contornando completamente a biometria.


🔐 Tentativa 4: Device Binding + Desafio (Agora Sim Ficou Sério!)

3.2 Device Binding + Desafio

Uma nova etapa é adicionada no fluxo: a autorização do aparelho (device binding).

Aqui começamos a ficar sérios! Introduzimos o conceito de device binding com challenge-response.

Aqui, um "desafio" (challenge) é gerado pelo servidor e enviado para o app. Esse desafio é:

  • Assinado pelo dispositivo (com uma chave privada que está segura no aparelho e protegida por biometria)
  • Devolvido para o servidor, que valida com a chave pública

O que isso traz de ganho?

  • O desafio é algo que expira, é único e não reutilizável
  • Somente o aparelho autorizado consegue responder corretamente
  • A transação fica atrelada a um contexto: quem, quando, de qual dispositivo, e com qual fator de autenticação

Como Funciona:

  1. Registro do Dispositivo:
   📱 App gera par de chaves (pública/privada)
   🔑 Chave privada fica protegida no dispositivo (TEE/Secure Enclave)
   📤 Chave pública é enviada e registrada no servidor
Enter fullscreen mode Exit fullscreen mode
  1. Fluxo de Transação:
   🔐 Login normal → recebe JWT
   💸 Usuário inicia transação
   🎲 Servidor gera challenge único
   👤 App solicita biometria para liberar chave privada
   ✍️ App assina o challenge com chave privada
   📤 Envia JWT + challenge assinado + dados da transação
   ✅ Servidor valida assinatura com chave pública registrada
   💸 Transação aprovada
Enter fullscreen mode Exit fullscreen mode

Ganhos Significativos:

  • Prova criptográfica de que a requisição veio do dispositivo específico
  • Challenge único previne replay attacks
  • Biometria integrada ao processo de assinatura
  • Contexto completo: quem, quando, de onde e com qual fator

Implementação Prática:

React Native - Gerando chaves seguras:

import Keychain from 'react-native-keychain';
import TouchID from 'react-native-touch-id';

// Gera e armazena chaves do dispositivo
const generateDeviceKeys = async () => {
  const biometryType = await TouchID.isSupported();

  if (biometryType) {
    const { publicKey, privateKey } = await generateKeyPair();

    // Armazena chave privada protegida por biometria
    await Keychain.setInternetCredentials(
      'transaction_key', 'device_key', privateKey,
      { accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY }
    );

    return publicKey; // Enviar ao servidor
  }
};

// Assina challenge com biometria
const signChallenge = async (challenge: string) => {
  await TouchID.authenticate('Confirme a transação');

  const credentials = await Keychain.getInternetCredentials('transaction_key');
  return await signWithPrivateKey(challenge, credentials.password);
};
Enter fullscreen mode Exit fullscreen mode

🏆 Tentativa 5: Juntando Tudo (e Mais Um Pouco)

Agora sim, o fluxo começa a ganhar cara de algo minimamente robusto!

Antes de realizar a transação, introduzimos um novo tipo de token: o token de autorização. Até agora, o JWT que usamos era um token de autenticação, usado para identificar quem está fazendo a requisição. Mas ele não diz muita coisa sobre a intenção de fazer uma transação específica.

O token de autorização vem para preencher essa lacuna. A versão mais robusta adiciona uma camada extra: tokens de autorização específicos para transação.

Diferença Entre Tokens (Que Eu Finalmente Entendi):

  • JWT de Autenticação: "Eu sou o usuário X"
  • Token de Autorização: "Eu tenho permissão para fazer ESTA transação específica AGORA"

Ele pode ter:

  • Um tempo de expiração curtíssimo (ex: 30 segundos)
  • Ser vinculado a uma transação específica (ex: ID do pagamento ou valor envolvido)
  • Ser liberado somente após a autenticação biométrica + assinatura do desafio, e gerado no lado do servidor

O app envia esse token ao backend como parte da requisição da transação, e o backend só valida a operação se esse token de autorização estiver presente, válido e íntegro.

Assim, conseguimos aplicar os três pilares da segurança:

  • Autenticação (JWT de login)
  • Autorização (token específico para a transação)
  • Prova de posse (biometria + assinatura criptográfica do desafio)

Fluxo Completo:

1. 🔐 Login → JWT de autenticação
2. 💸 Usuário inicia transação (valor: R$ 1000)
3. 🎲 Servidor gera challenge + token de autorização temporário
   - Válido por 30 segundos
   - Vinculado ao valor exato e destinatário
   - Só é liberado após validação completa
4. 👤 App solicita biometria
5. ✍️ App assina challenge + dados da transação
6. 📤 Envia: JWT + token de autorização + challenge assinado
7. ✅ Servidor valida TUDO:
   - JWT válido?
   - Token de autorização válido e não expirado?
   - Assinatura do challenge correta?
   - Dados da transação batem?
8. 💸 Transação aprovada
Enter fullscreen mode Exit fullscreen mode

Implementação do Token de Autorização:



// Gera token específico para transação
func generateTransactionToken(userID string, data TransactionData, challengeID string) (string, error) {
    claims := TransactionClaims{
        UserID:      userID,
        Amount:      data.Amount,
        Recipient:   data.Recipient,
        ChallengeID: challengeID,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(30 * time.Second)),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(secretKey)
}

// Valida transação completa
func validateTransaction(authJWT, authToken, signature string, data TransactionData) error {
    // 1. Valida usuário
    user, err := validateAuthJWT(authJWT)
    if err != nil {
        return err
    }

    // 2. Valida token de autorização
    claims, err := validateAuthToken(authToken)
    if err != nil {
        return err
    }

    // 3. Verifica dados
    if claims.Amount != data.Amount || claims.Recipient != data.Recipient {
        return errors.New("dados não conferem")
    }

    // 4. Valida assinatura do challenge
    return verifyChallenge(claims.ChallengeID, signature, user.PublicKey)
}
Enter fullscreen mode Exit fullscreen mode

🔄 Fallbacks e Considerações Práticas

Para Dispositivos Sem Biometria:

  1. Senha de Transação: Como bancos tradicionais
  2. SMS/Email OTP: Segundo fator via comunicação externa
  3. App Authenticator: TOTP (Time-based One-Time Password)
// React Native - Escolha do método de autenticação
import TouchID from 'react-native-touch-id';

const getAuthenticationMethod = async () => {
  const biometrySupported = await TouchID.isSupported();

  if (biometrySupported && userPreferences.biometry) {
    return 'biometric_challenge';
  } else if (userPreferences.transactionPassword) {
    return 'transaction_password';
  } else {
    return 'sms_otp';
  }
};
Enter fullscreen mode Exit fullscreen mode

🛡️ Os Três Pilares Implementados

Nossa solução final atende aos três pilares fundamentais da segurança:

1. Autenticação (Quem é?)

  • JWT de login válido
  • Identidade confirmada

2. Autorização (Pode fazer?)

  • Token específico para a transação
  • Escopo limitado e temporal

3. Prova de Posse (Está realmente aqui?)

  • Biometria + assinatura criptográfica
  • Device binding comprovado

⚡ Resumo dos Benefícios

Múltiplas camadas de segurança
Proteção contra replay attacks

Device binding criptográfico
Tokens com escopo limitado
Fallbacks para diferentes cenários
Experiência do usuário fluida
Detecção de anomalias


🚀 Próximos Passos

Para quem quer se aprofundar:

  1. Estude criptografia aplicada (curvas elípticas, RSA)
  2. Implemente rate limiting e throttling
  3. Adicione logging e auditoria detalhados
  4. Considere WebAuthn/FIDO2 para web
  5. Teste com pen testing profissional

📚 Referências Técnicas

Segurança Geral

React Native

Golang


💬 Conclusão

Juntando tudo, parece que agora temos uma transação minimamente segura:

  • Temos a identidade do usuário (login)
  • Temos a confirmação de que é ele mesmo, e que está no aparelho certo (biometria + device binding)
  • Temos a autorização contextual, com tempo curto e uso único (token da transação)

Em cenários onde não há suporte à biometria, usar uma senha específica para transações (como já é feito em bancos) também é totalmente aceitável - desde que seja tratada com o mesmo cuidado de um fator sensível.

Esse post não pretende esgotar o assunto - longe disso - mas sim mostrar que, mesmo para quem não lida com isso todo dia, é possível montar soluções coerentes, seguras e evolutivas.

Este guia apresenta uma evolução didática para implementar transações seguras em mobile. Em produção, sempre:

  • Consulte especialistas em segurança
  • Faça pen testing regular
  • Monitore logs de segurança
  • Mantenha-se atualizado com vulnerabilidades
  • Implemente gradualmente e teste muito

Segurança não é um destino, é uma jornada contínua de melhorias e adaptações.

Se você já implementou algo parecido ou tem sugestões, manda nos comentários! Estou aprendendo também. 😉


💡 Observação Importante

Na prática, considere soluções como OAuth 2.0, Auth0, Firebase Auth ou AWS Cognito que já implementam esses fluxos de forma segura e testada.

O aprendizado aqui é válido para entender os conceitos, mas em produção, "não reinvente a roda" 🛞


Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.