fingerprint 5. Hash de Senhas com bcrypt

Armazenar senhas em texto plano é o erro mais grave que um desenvolvedor pode cometer. Se seu banco for invadido, todas as senhas ficarão expostas. E pior: como usuários reutilizam senhas, você estaria comprometendo contas em outros sistemas (email, bancos, redes sociais).

O bcrypt é um algoritmo de hash projetado especificamente para senhas. Ele possui três características essenciais: é unidirecional (impossível reverter), adiciona salt automático (cada hash é único mesmo para senhas iguais) e é computacionalmente caro (dificulta ataques de força bruta).

O PHP oferece as funções password_hash() e password_verify() que implementam bcrypt com configuração segura por padrão. Você nunca precisa gerenciar salt manualmente ou escolher algoritmos - o PHP faz tudo automaticamente, seguindo as melhores práticas atuais.

block
❌ NUNCA USE: MD5, SHA1 ou SHA256 para senhas. Estes algoritmos são rápidos demais e vulneráveis a rainbow tables. Também nunca use password_hash() sem especificar PASSWORD_BCRYPT ou PASSWORD_DEFAULT.

compare Hash vs Criptografia: Qual a Diferença?

lock Criptografia (Reversível)

  • Propósito: Proteger dados em trânsito
  • Direção: Bidirecional (encrypt + decrypt)
  • Uso: HTTPS, arquivos confidenciais
  • Exemplo: AES, RSA
  • Velocidade: Muito rápida

tag Hash (Irreversível)

  • Propósito: Verificar autenticidade
  • Direção: Unidirecional (não pode reverter)
  • Uso: Senhas, checksums
  • Exemplo: bcrypt, Argon2
  • Velocidade: Intencionalmente lenta
info
💡 Conceito: Senhas são hasheadas porque você NUNCA precisa conhecer a senha original. Apenas precisa verificar se a senha fornecida gera o mesmo hash.

code Implementando bcrypt no PHP

1️⃣ Criando Hash ao Registrar Usuário

 false, 'message' => 'Senha deve ter no mínimo 8 caracteres']);
    exit;
}

// Criar hash (AUTOMÁTICO: salt + custo otimizado)
$passwordHash = password_hash($password, PASSWORD_DEFAULT);

// O hash gerado tem este formato:
// $2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
//  │  │  │                                                        │
//  │  │  └── Salt (22 caracteres)                                │
//  │  └── Custo (10 = 2^10 iterações = 1024x)                   │
//  └── Algoritmo (2y = bcrypt)                                    │
//                                                 Hash final (31 chars) ──┘

try {
    // Inserir no banco
    $stmt = $pdo->prepare('
        INSERT INTO users (email, password_hash, created_at) 
        VALUES (?, ?, NOW())
    ');
    $stmt->execute([$email, $passwordHash]);
    
    $userId = $pdo->lastInsertId();
    
    echo json_encode([
        'success' => true,
        'message' => 'Usuário criado com sucesso',
        'user_id' => $userId
    ]);
    
} catch (PDOException $e) {
    if ($e->getCode() == 23000) { // Duplicate entry
        http_response_code(409);
        echo json_encode(['success' => false, 'message' => 'Email já cadastrado']);
    } else {
        error_log("Register Error: " . $e->getMessage());
        http_response_code(500);
        echo json_encode(['success' => false, 'message' => 'Erro ao criar usuário']);
    }
}
?>

2️⃣ Verificando Senha no Login

prepare('SELECT id, email, password_hash FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch();

// Verificar senha
if (!$user || !password_verify($password, $user['password_hash'])) {
    http_response_code(401);
    echo json_encode(['success' => false, 'message' => 'Credenciais inválidas']);
    exit;
}

// password_verify() faz DUAS coisas:
// 1. Extrai o salt e algoritmo do hash armazenado
// 2. Hasheia a senha fornecida com o mesmo salt
// 3. Compara os dois hashes em tempo constante (previne timing attacks)

// Se chegou aqui, senha está correta
$token = generateJWT($user['id']);

echo json_encode([
    'success' => true,
    'token' => $token,
    'user' => ['id' => $user['id'], 'email' => $user['email']]
]);
?>
verified
✅ Segurança Garantida: password_verify() usa comparação em tempo constante, prevenindo timing attacks onde atacantes medem o tempo de resposta para deduzir caracteres corretos.

tune Ajustando o Custo Computacional

O parâmetro cost define quantas vezes o algoritmo será executado. Quanto maior, mais seguro, mas mais lento. O padrão é 10 (2^10 = 1024 iterações).

 $cost]);
        $end = microtime(true);
        
        $elapsed = $end - $start;
        $cost++;
        
    } while ($elapsed < $targetTime && $cost < 14);
    
    return $cost - 1;
}

// Usar custo customizado
$optimalCost = findOptimalCost(); // Exemplo: 12
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => $optimalCost]);

// Resultado com cost=12:
// $2y$12$... (mais seguro, ~400ms por hash)
?>
speed

Cost = 8 (Fraco)

~50ms por hash
256 iterações
Vulnerável a força bruta

check_circle

Cost = 10 (Padrão)

~100-200ms por hash
1024 iterações
Recomendado

shield

Cost = 12 (Forte)

~400-800ms por hash
4096 iterações
Alta segurança

warning
⚠️ Balanceamento: Custo muito alto pode causar DoS acidental em servidores com pouco recurso. Múltiplos registros simultâneos podem sobrecarregar a CPU.

update Rehashing: Atualizando Algoritmo

Com o tempo, algoritmos se tornam obsoletos. O PHP permite verificar se um hash precisa ser atualizado e fazer isso transparentemente no login do usuário.

fetch();

if (!$user || !password_verify($password, $user['password_hash'])) {
    http_response_code(401);
    echo json_encode(['success' => false, 'message' => 'Credenciais inválidas']);
    exit;
}

// Verificar se precisa atualizar o hash
if (password_needs_rehash($user['password_hash'], PASSWORD_DEFAULT)) {
    // Gerar novo hash com algoritmo/custo atualizado
    $newHash = password_hash($password, PASSWORD_DEFAULT);
    
    // Atualizar no banco
    $stmt = $pdo->prepare('UPDATE users SET password_hash = ? WHERE id = ?');
    $stmt->execute([$newHash, $user['id']]);
    
    error_log("Password rehashed for user ID: " . $user['id']);
}

// Continuar com login normal...
?>
auto_fix_high
💡 Migração Transparente: Este código permite que você atualize de MD5 para bcrypt, ou de bcrypt cost=10 para cost=12, sem forçar usuários a resetarem senhas. A atualização acontece automaticamente no próximo login.

rule Validação de Força de Senha

 bool, 'errors' => array]
 */
function validatePasswordStrength($password) {
    $errors = [];
    
    // Comprimento mínimo
    if (strlen($password) < 8) {
        $errors[] = 'Senha deve ter no mínimo 8 caracteres';
    }
    
    // Letra maiúscula
    if (!preg_match('/[A-Z]/', $password)) {
        $errors[] = 'Senha deve conter ao menos uma letra maiúscula';
    }
    
    // Letra minúscula
    if (!preg_match('/[a-z]/', $password)) {
        $errors[] = 'Senha deve conter ao menos uma letra minúscula';
    }
    
    // Número
    if (!preg_match('/[0-9]/', $password)) {
        $errors[] = 'Senha deve conter ao menos um número';
    }
    
    // Caractere especial
    if (!preg_match('/[^A-Za-z0-9]/', $password)) {
        $errors[] = 'Senha deve conter ao menos um caractere especial';
    }
    
    // Senhas comuns (lista reduzida, use biblioteca completa em produção)
    $commonPasswords = ['password', '12345678', 'qwerty', 'abc123', 'password123'];
    if (in_array(strtolower($password), $commonPasswords)) {
        $errors[] = 'Senha muito comum, escolha outra';
    }
    
    return [
        'valid' => empty($errors),
        'errors' => $errors
    ];
}

// Uso
$validation = validatePasswordStrength($data['password']);
if (!$validation['valid']) {
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'message' => 'Senha inválida',
        'errors' => $validation['errors']
    ]);
    exit;
}
?>
arrow_forward
✅ Próxima Seção: Com senhas seguras implementadas, vamos ao CRUD Avançado - CREATE, aprendendo validação, sanitização e proteção contra SQL Injection.