Autenticação de dois fatores (2FA): implementando TOTP

1. Fundamentos do TOTP (Time-based One-Time Password)

TOTP (Time-based One-Time Password) é um mecanismo de autenticação de dois fatores que gera senhas temporárias baseadas no tempo. Diferentemente do HOTP (HMAC-based One-Time Password), que usa um contador incremental, o TOTP utiliza o timestamp Unix dividido por um intervalo fixo (geralmente 30 segundos) como entrada para o algoritmo.

Os componentes essenciais do TOTP são:

  • Chave secreta: valor criptográfico compartilhado entre servidor e cliente
  • Timestamp: tempo atual do sistema (Unix epoch)
  • Intervalo de tempo: janela padrão de 30 segundos para validade do código

O TOTP é definido pela RFC 6238, que estende o algoritmo HOTP (RFC 4226) substituindo o contador incremental por um contador baseado no tempo. Ambos utilizam HMAC-SHA1 como função hash subjacente.

2. Geração Segura da Chave Secreta

A segurança do TOTP começa com a geração da chave secreta. É obrigatório utilizar um gerador criptograficamente seguro (CSPRNG - Cryptographically Secure Pseudo-Random Number Generator).

// Geração segura da chave em Node.js
const crypto = require('crypto');

function generateSecretKey(length = 20) {
    // CSPRNG: crypto.randomBytes é seguro para uso criptográfico
    const randomBytes = crypto.randomBytes(length);
    return randomBytes;
}

A chave gerada deve ser codificada em Base32 para exibição ao usuário e armazenamento:

// Codificação Base32 da chave secreta
function encodeBase32(secretKey) {
    // Buffer para Base32 (usando biblioteca como 'base32-encode')
    const base32 = require('base32-encode');
    return base32(secretKey, 'RFC4648', { padding: false });
}

No servidor, a chave secreta deve ser armazenada de forma segura:
- Criptografada com AES-256 antes de persistir no banco de dados
- Em módulos de segurança de hardware (HSM) para ambientes críticos
- Associada exclusivamente ao usuário, sem exposição em logs

3. Implementação do Lado do Servidor

O cálculo do código TOTP segue etapas precisas definidas pela RFC 6238:

// Implementação completa do TOTP no servidor
function generateTOTP(secretKey, timeStep = 30, digits = 6) {
    // 1. Cálculo do contador de tempo
    const timestamp = Math.floor(Date.now() / 1000);
    const counter = Math.floor(timestamp / timeStep);

    // 2. Converter contador para buffer de 8 bytes (big-endian)
    const counterBuffer = Buffer.alloc(8);
    counterBuffer.writeBigUInt64BE(BigInt(counter));

    // 3. Aplicar HMAC-SHA1
    const hmac = crypto.createHmac('sha1', secretKey);
    hmac.update(counterBuffer);
    const hmacResult = hmac.digest();

    // 4. Extrair offset (últimos 4 bits do último byte)
    const offset = hmacResult[hmacResult.length - 1] & 0x0f;

    // 5. Extrair 4 bytes a partir do offset
    const binaryCode = (hmacResult[offset] & 0x7f) << 24 |
                       (hmacResult[offset + 1] & 0xff) << 16 |
                       (hmacResult[offset + 2] & 0xff) << 8 |
                       (hmacResult[offset + 3] & 0xff);

    // 6. Gerar código de 6 dígitos
    const otp = binaryCode % Math.pow(10, digits);
    return otp.toString().padStart(digits, '0');
}

4. Provisionamento do Dispositivo do Usuário

Para configurar o autenticador no dispositivo do usuário, é necessário gerar um URI no formato otpauth://:

// Geração do URI de configuração
function generateOTPAuthURI(issuer, user, secret, algorithm = 'SHA1', digits = 6, period = 30) {
    const encodedIssuer = encodeURIComponent(issuer);
    const encodedUser = encodeURIComponent(user);
    const encodedSecret = encodeBase32(secret);

    return `otpauth://totp/${encodedIssuer}:${encodedUser}` +
           `?secret=${encodedSecret}` +
           `&issuer=${encodedIssuer}` +
           `&algorithm=${algorithm}` +
           `&digits=${digits}` +
           `&period=${period}`;
}

O QR Code pode ser gerado com bibliotecas como qrcode:

// Geração de QR Code para escaneamento
const QRCode = require('qrcode');

async function displayQRCode(otpauthURI) {
    const qrCodeDataURL = await QRCode.toDataURL(otpauthURI);
    // Exibir qrCodeDataURL como imagem para o usuário
    return qrCodeDataURL;
}

Como alternativa, exiba a chave secreta em Base32 para digitação manual:

// Exibição da chave secreta para configuração manual
const manualKey = encodeBase32(secretKey);
console.log(`Chave secreta (digite manualmente no autenticador): ${manualKey}`);

5. Validação e Tratamento de Janelas de Tempo

A validação deve considerar possíveis dessincronizações de relógio entre servidor e cliente:

// Validação com janela de tolerância (skew)
function validateTOTP(token, secretKey, windowSize = 1) {
    const timeStep = 30;
    const currentTime = Math.floor(Date.now() / 1000);
    const currentCounter = Math.floor(currentTime / timeStep);

    // Verificar códigos dentro da janela de tolerância
    for (let i = -windowSize; i <= windowSize; i++) {
        const counter = currentCounter + i;
        const expectedToken = generateTOTPFromCounter(secretKey, counter);

        if (expectedToken === token) {
            return {
                valid: true,
                counter: counter,
                timeUsed: currentTime
            };
        }
    }

    return { valid: false };
}

Para prevenir reutilização do mesmo código, implemente tracking do timestamp do último uso:

// Prevenção de reutilização
function validateWithReusePrevention(token, secretKey, userRecord) {
    const result = validateTOTP(token, secretKey);

    if (result.valid) {
        // Verificar se o código não foi usado anteriormente
        if (result.timeUsed <= userRecord.lastTOTPTime) {
            return { valid: false, reason: 'Código já utilizado' };
        }

        // Atualizar timestamp do último uso
        userRecord.lastTOTPTime = result.timeUsed;
        return { valid: true };
    }

    return { valid: false, reason: 'Código inválido' };
}

6. Fluxo Completo de Autenticação com TOTP

O fluxo completo de autenticação de dois fatores segue estas etapas:

  1. Login primário: usuário fornece usuário e senha
  2. Verificação de 2FA: se o 2FA está ativo, solicitar código TOTP
  3. Validação do código: servidor valida o token TOTP
  4. Concessão de acesso: se válido, criar sessão autenticada
// Fluxo completo de autenticação
async function authenticateWith2FA(username, password, totpToken) {
    // Etapa 1: Validar credenciais primárias
    const user = await validateCredentials(username, password);
    if (!user) {
        return { success: false, error: 'Credenciais inválidas' };
    }

    // Etapa 2: Verificar se 2FA está ativo
    if (user.twoFactorEnabled) {
        // Etapa 3: Validar código TOTP
        const totpResult = validateWithReusePrevention(totpToken, user.totpSecret, user);

        if (!totpResult.valid) {
            return { success: false, error: 'Código 2FA inválido' };
        }
    }

    // Etapa 4: Atualizar metadados e criar sessão
    user.lastLoginAt = new Date();
    await user.save();

    return { 
        success: true, 
        session: createSession(user),
        metadata: {
            twoFactorEnabled: user.twoFactorEnabled,
            activatedAt: user.twoFactorActivatedAt
        }
    };
}

Códigos de recuperação (backup codes) devem ser gerados e armazenados durante a ativação do 2FA:

// Geração de códigos de recuperação
function generateBackupCodes(count = 10) {
    const codes = [];
    for (let i = 0; i < count; i++) {
        const code = crypto.randomBytes(4).toString('hex').toUpperCase();
        codes.push({
            code: code,
            used: false,
            hashedCode: crypto.createHash('sha256').update(code).digest('hex')
        });
    }
    return codes;
}

7. Considerações de Segurança e Boas Práticas

Proteção contra timing attacks: Utilize comparação de strings em tempo constante para evitar vazamento de informações:

// Comparação segura contra timing attacks
function secureCompare(a, b) {
    if (a.length !== b.length) {
        return false;
    }

    let result = 0;
    for (let i = 0; i < a.length; i++) {
        result |= a.charCodeAt(i) ^ b.charCodeAt(i);
    }
    return result === 0;
}

Rate limiting: Implemente limitação de tentativas no endpoint de validação 2FA:

// Rate limiting para endpoint 2FA
const rateLimiter = new Map();

function checkRateLimit(userId) {
    const now = Date.now();
    const attempts = rateLimiter.get(userId) || [];

    // Remover tentativas mais antigas que 5 minutos
    const recentAttempts = attempts.filter(time => now - time < 300000);

    if (recentAttempts.length >= 5) {
        return { allowed: false, retryAfter: 300 };
    }

    recentAttempts.push(now);
    rateLimiter.set(userId, recentAttempts);
    return { allowed: true };
}

Riscos de engenharia social e phishing: Códigos TOTP são descartáveis e não devem ser compartilhados. Eduque os usuários sobre:
- Nunca fornecer códigos TOTP por telefone, email ou chat
- Verificar sempre o domínio do site antes de inserir o código
- Utilizar autenticadores oficiais e confiáveis

Referências