DEV Community

Kairox
Kairox

Posted on

Como Proteger a Privacidade do Usuário Durante a Verificação por Número de Telefone

Uma conversa técnica e honesta sobre um fluxo que todo dev implementa — mas que poucos param pra questionar do ponto de vista de privacidade.

Tem um momento no desenvolvimento de praticamente qualquer aplicativo moderno onde você chega num ponto de decisão que parece simples, mas não é.

O usuário vai criar uma conta. Você precisa verificar que ele é uma pessoa real, com acesso a um dispositivo específico. A solução mais óbvia — a que todo tutorial mostra, a que todas as APIs facilitam — é pedir o número de telefone e mandar um código por SMS.

Você implementa. Funciona. Segue em frente.

O que a maioria dos devs não para pra pensar: você acabou de coletar um dos identificadores mais sensíveis que existem na vida digital de uma pessoa. E agora você é responsável por ele.

Esse artigo é sobre o que fazer com essa responsabilidade — e como construir fluxos de verificação por número de telefone que respeitam a privacidade do usuário de verdade, não só no papel.

O Problema que Começa na Coleta

Vamos começar pelo básico que muita gente pula.

Quando você pede o número de telefone de um usuário para verificação, você está coletando um dado que:

1. Está vinculado à identidade física. No Brasil, número de telefone está associado a CPF no cadastro da operadora. Isso não é um dado anônimo — é uma ligação direta a uma pessoa real.

2. Serve como fator de autenticação em dezenas de outros serviços. O número que seu app coleta para verificação é provavelmente o mesmo que o usuário usa como 2FA no banco, no e-mail, no WhatsApp. Comprometer esse dado tem efeito cascata.

3. É um ponto de correlação entre plataformas. Se você armazena número de telefone e cruzar com outros dados (comportamento, compras, localização), você tem um identificador poderoso que une o usuário entre contextos que ele não necessariamente quis conectar.

4. Tem vida longa. E-mails podem ser trocados com relativa facilidade. Número de telefone, não. Um número coletado hoje pode identificar o mesmo usuário daqui a dez anos.

// O que parece ser apenas uma coleta simples:
const user = {
  phone: "+55 11 9xxxx-xxxx", // <- muito mais do que parece
  email: "user@example.com",
  name: "João Silva"
};

// O que você realmente está coletando:
// - Identificador vinculado ao CPF do usuário
// - Chave de recuperação de conta em múltiplos serviços
// - Ponto de correlação entre plataformas
// - Dado que vai viver no seu banco por anos
Enter fullscreen mode Exit fullscreen mode

Com isso em mente, as decisões de implementação mudam de figura.

Princípio 1: Colete Apenas o Que é Necessário — e só pelo Tempo Necessário

O princípio de minimização de dados — presente tanto na LGPD brasileira quanto no GDPR europeu — diz que você deve coletar apenas o mínimo necessário para a finalidade declarada.

Aplicado à verificação por telefone, isso significa perguntar: você precisa armazenar o número após a verificação?

Na maioria dos casos, a resposta honesta é não.

Se o objetivo é confirmar que o usuário tem acesso ao dispositivo/número, você precisa do número para enviar o SMS. Mas depois que a verificação é concluída com sucesso, você precisa guardar o número completo indefinidamente? Ou basta guardar que a verificação foi concluída — e talvez um hash do número para verificações futuras?

// ❌ Padrão comum — armazena número completo indefinidamente
await db.users.update({
  id: userId,
  phone: phoneNumber, // armazenado em plaintext, para sempre
  phoneVerified: true,
  phoneVerifiedAt: new Date()
});

// ✅ Abordagem de minimização — dependendo do caso de uso
const crypto = require('crypto');

// Se você não precisa do número para contato futuro,
// apenas para verificar "este usuário já verificou este número antes":
const phoneHash = crypto
  .createHmac('sha256', process.env.PHONE_HASH_SECRET)
  .update(phoneNumber)
  .digest('hex');

await db.users.update({
  id: userId,
  phoneHash,          // verificável, mas não reversível sem a chave
  phoneVerified: true,
  phoneVerifiedAt: new Date()
});

// O número em plaintext não precisa persistir
Enter fullscreen mode Exit fullscreen mode

Quando você SÍ precisa armazenar o número completo:

  • Para enviar notificações ou comunicações futuras por SMS
  • Para autenticação contínua via SMS (2FA em logins futuros)
  • Para recuperação de conta via SMS

Quando você NÃO precisa:

  • Se a verificação é só para confirmar humanidade no cadastro
  • Se você usa outro canal para comunicações (e-mail, push)
  • Se você vai usar TOTP para 2FA depois do cadastro inicial

Princípio 2: Transmita e Armazene com Criptografia Séria

Se você decidiu que precisa armazenar o número, ele precisa de proteção adequada.

// ❌ Nunca faça isso
const user = {
  phone: "11999999999", // plaintext no banco — um dump expõe tudo
};

// ✅ Criptografia em repouso com chave gerenciada separadamente
const { createCipheriv, createDecipheriv, randomBytes } = require('crypto');

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); // 32 bytes, gerenciada externamente

function encryptPhone(phone) {
  const iv = randomBytes(16);
  const cipher = createCipheriv(ALGORITHM, KEY, iv);

  const encrypted = Buffer.concat([
    cipher.update(phone, 'utf8'),
    cipher.final()
  ]);

  const authTag = cipher.getAuthTag();

  // Armazena: iv + authTag + encrypted
  return Buffer.concat([iv, authTag, encrypted]).toString('base64');
}

function decryptPhone(encryptedData) {
  const buffer = Buffer.from(encryptedData, 'base64');
  const iv = buffer.slice(0, 16);
  const authTag = buffer.slice(16, 32);
  const encrypted = buffer.slice(32);

  const decipher = createDecipheriv(ALGORITHM, KEY, iv);
  decipher.setAuthTag(authTag);

  return Buffer.concat([
    decipher.update(encrypted),
    decipher.final()
  ]).toString('utf8');
}
Enter fullscreen mode Exit fullscreen mode

Pontos críticos de segurança:

  • Chave de criptografia gerenciada externamente — AWS KMS, Google Cloud KMS, HashiCorp Vault. Nunca no mesmo lugar que os dados.
  • Rotação de chaves — implemente desde o início. Rotacionar depois é muito mais trabalhoso.
  • HTTPS obrigatório — o número nunca deve transitar em plaintext pela rede.
  • Logs com cuidado — nunca logue o número de telefone completo. Se precisar de rastreabilidade, logue os últimos 4 dígitos ou um identificador opaco.
// ❌ Risco em logs
console.log(`SMS enviado para ${phoneNumber}`); // expõe o número nos logs

// ✅ Logs seguros
const maskedPhone = phoneNumber.replace(/(\d{2})(\d+)(\d{4})/, '$1****$3');
console.log(`SMS enviado para ${maskedPhone}`); // "11****9999"
logger.info('sms_sent', { 
  userId, 
  maskedPhone,
  timestamp: new Date().toISOString() 
});
Enter fullscreen mode Exit fullscreen mode

Princípio 3: Proteja o Fluxo de Verificação Contra Abuso

Um fluxo de verificação por SMS que não tem proteções adequadas é um vetor de ataque em dois sentidos: contra seus usuários (enumeração de números) e contra você (custo de SMS por força bruta).

Rate Limiting por IP e por Número

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// Rate limit por IP — evita força bruta de números
const smsRequestLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // máximo 5 tentativas por IP por janela
  store: new RedisStore({ client: redisClient }),
  message: { error: 'Muitas tentativas. Aguarde antes de tentar novamente.' },
  standardHeaders: true,
  legacyHeaders: false,
});

// Rate limit por número — evita spam para o mesmo número
const phoneSpecificLimiter = async (phoneNumber) => {
  const key = `sms_limit:${phoneHash(phoneNumber)}`;
  const attempts = await redis.incr(key);

  if (attempts === 1) {
    await redis.expire(key, 3600); // 1 hora de janela
  }

  if (attempts > 3) {
    throw new Error('RATE_LIMIT_EXCEEDED');
  }
};
Enter fullscreen mode Exit fullscreen mode

Proteção Contra Enumeração

Enumeração é quando um atacante consegue descobrir quais números já estão cadastrados na sua plataforma simplesmente tentando verificar vários números e observando as respostas diferentes.

// ❌ Resposta que vaza informação
app.post('/verify/send', async (req, res) => {
  const user = await db.users.findByPhone(phone);

  if (user) {
    return res.status(400).json({ 
      error: 'Este número já está cadastrado' // revela que o número existe
    });
  }

  await sendSMS(phone);
  res.json({ success: true });
});

// ✅ Resposta que não vaza informação
app.post('/verify/send', async (req, res) => {
  // Sempre retorna a mesma resposta, independente de o número existir ou não
  // A lógica de "número já existe" é tratada internamente ou no login

  await sendVerificationSMS(phone); // internamente ignora se já existe

  // Tempo de resposta constante — evita timing attacks
  res.json({ 
    message: 'Se este número for válido, você receberá um código em breve.' 
  });
});
Enter fullscreen mode Exit fullscreen mode

Expiração e Invalidação de Código

// Código com tempo de vida curto e uso único
async function createVerificationCode(phone) {
  const code = crypto.randomInt(100000, 999999).toString();
  const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutos

  // Armazena hash do código, não o código em plaintext
  const codeHash = crypto
    .createHash('sha256')
    .update(code + process.env.CODE_SALT)
    .digest('hex');

  await redis.setex(
    `verification:${phoneHash(phone)}`,
    300, // TTL de 5 minutos no Redis
    JSON.stringify({ codeHash, attempts: 0 })
  );

  return code; // retorna o código original apenas para envio no SMS
}

async function verifyCode(phone, submittedCode) {
  const key = `verification:${phoneHash(phone)}`;
  const stored = await redis.get(key);

  if (!stored) {
    throw new Error('VERIFICATION_EXPIRED');
  }

  const { codeHash, attempts } = JSON.parse(stored);

  // Máximo 3 tentativas
  if (attempts >= 3) {
    await redis.del(key);
    throw new Error('TOO_MANY_ATTEMPTS');
  }

  const submittedHash = crypto
    .createHash('sha256')
    .update(submittedCode + process.env.CODE_SALT)
    .digest('hex');

  // Comparação em tempo constante — evita timing attacks
  const isValid = crypto.timingSafeEqual(
    Buffer.from(codeHash),
    Buffer.from(submittedHash)
  );

  if (!isValid) {
    await redis.set(key, JSON.stringify({ codeHash, attempts: attempts + 1 }), 'KEEPTTL');
    throw new Error('INVALID_CODE');
  }

  // Invalida o código após uso bem-sucedido — uso único
  await redis.del(key);
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Princípio 4: Ofereça Alternativas ao SMS Sempre que Possível

SMS tem vulnerabilidades estruturais — SS7 exploitation e SIM swapping são reais e crescentes. Para fluxos onde segurança é crítica, oferecer alternativas é uma prática responsável.

// Suporte a múltiplos métodos de verificação
const VERIFICATION_METHODS = {
  SMS: 'sms',
  TOTP: 'totp',          // Google Authenticator, Authy
  EMAIL: 'email',        // mais fácil de implementar com segurança
  PASSKEY: 'passkey'     // WebAuthn — o futuro da autenticação
};

// UI que deixa o usuário escolher
const verificationOptions = [
  { method: 'sms',     label: 'SMS para seu celular',           security: 'média' },
  { method: 'totp',    label: 'App autenticador (mais seguro)', security: 'alta'  },
  { method: 'email',   label: 'Link por e-mail',               security: 'média' },
  { method: 'passkey', label: 'Chave de acesso (mais seguro)',  security: 'alta'  },
];
Enter fullscreen mode Exit fullscreen mode

Quando o TOTP faz mais sentido que SMS:

  • Aplicativos financeiros ou com dados sensíveis
  • Plataformas onde SIM swapping seria um risco real para o usuário
  • Quando você quer eliminar o custo de envio de SMS a longo prazo

Princípio 5: Seja Transparente com o Usuário

A privacidade não é só sobre implementação técnica — é sobre honestidade com quem usa o seu produto.

// Na interface, seja claro sobre o que você vai fazer com o número
const privacyNotice = {
  purpose: "Verificar que você tem acesso a este número de telefone",
  storage: "Armazenado com criptografia — não compartilhado com terceiros",
  usage: "Usado apenas para autenticação e recuperação de conta",
  deletion: "Você pode solicitar exclusão nas configurações a qualquer momento",
  legal_basis: "Consentimento (LGPD Art. 7º, I)"
};
Enter fullscreen mode Exit fullscreen mode

E na prática, cumpra o que diz. Se o número é "só para verificação", não use para marketing. Se o usuário pediu exclusão, de fato exclua — e documente o processo.


O Que Considerar Quando Você Usa Provedores Externos

Muitos projetos usam provedores de SMS como Twilio, AWS SNS ou similares. Isso adiciona uma camada que precisa ser considerada:

Verifique o que o provedor armazena. Alguns provedores guardam histórico de mensagens enviadas, incluindo número de destino. Leia os termos e configure a retenção de dados adequadamente.

Trate chaves de API como credenciais críticas. Uma chave de API de SMS exposta pode ser usada para enviar mensagens para qualquer número no seu nome — e a conta é sua.

# ❌ Nunca no código
const TWILIO_AUTH_TOKEN = "abc123xyz"; // exposto no repositório

# ✅ Sempre em variáveis de ambiente gerenciadas
const TWILIO_AUTH_TOKEN = process.env.TWILIO_AUTH_TOKEN;
// Gerenciadas via AWS Secrets Manager, Vault, ou similar
Enter fullscreen mode Exit fullscreen mode

Para quem está desenvolvendo e testando: em vez de usar números pessoais em ambientes de staging, considere serviços de numero virtual para sms dedicados a desenvolvimento. O NumeroVirtual.net é uma opção brasileira que funciona bem nesse contexto — números com reputação limpa, entrega de SMS confiável e sem a necessidade de expor o número pessoal de ninguém da equipe em ambientes de teste. Mantém a identidade pessoal fora do pipeline de desenvolvimento onde ela não precisa estar.


Checklist de Privacidade para Fluxos de Verificação por SMS

Antes de fazer o deploy, passe por essa lista:

Coleta e armazenamento:

  • [ ] Estou coletando apenas o que preciso para a finalidade declarada?
  • [ ] Número de telefone está criptografado em repouso?
  • [ ] A chave de criptografia está gerenciada externamente (KMS/Vault)?
  • [ ] Tenho processo definido de exclusão quando o usuário solicitar?
  • [ ] Os logs não contêm número de telefone em plaintext?

Fluxo de verificação:

  • [ ] Rate limiting por IP implementado?
  • [ ] Rate limiting por número implementado?
  • [ ] Respostas de erro não vazam informação sobre existência do número?
  • [ ] Código de verificação tem TTL curto (5-10 min)?
  • [ ] Código é invalidado após uso bem-sucedido?
  • [ ] Comparação de código usa tempo constante (timingSafeEqual)?
  • [ ] Máximo de tentativas por código implementado?

Segurança geral:

  • [ ] HTTPS obrigatório em toda a comunicação?
  • [ ] Chaves de API do provedor de SMS em variáveis de ambiente gerenciadas?
  • [ ] Ofereço alternativa ao SMS (TOTP, passkey)?
  • [ ] Usuário é informado claramente sobre uso do número?

Conformidade:

  • [ ] Base legal para coleta documentada (LGPD Art. 7º)?
  • [ ] Política de privacidade menciona uso do número de telefone?
  • [ ] Processo de exercício de direitos do usuário implementado?

Conclusão: Verificação Responsável não é Mais Difícil — É Diferente

A maioria dos problemas de privacidade em fluxos de verificação por SMS não vem de negligência — vem de automação. Você segue o tutorial, implementa o que funciona, segue em frente.

Mas "funciona" e "é responsável com os dados do usuário" são coisas diferentes.

O número de telefone que seu app coleta amanhã vai continuar existindo no seu banco daqui a anos. Vai potencialmente aparecer num relatório de auditoria, numa solicitação de acesso do usuário, numa investigação de incidente. As decisões que você toma agora na implementação definem como essa situação vai parecer naquele momento.

Implementar minimização de dados, criptografia adequada, rate limiting, transparência com o usuário e alternativas ao SMS não é complexidade desnecessária. É engenharia responsável.

E no contexto brasileiro, com a LGPD em vigor e a ANPD cada vez mais ativa, é também mitigação de risco legal muito concreta.

Vale o cuidado.

Implementa verificação por SMS no seu projeto? Compartilha nos comentários o que você já tem e o que ainda falta implementar — sempre rola aprendizado nas trocas da comunidade.

Top comments (0)