Hashing, HMAC y el manejo seguro de contraseñas en Go

Cuando necesitas verificar que un archivo no se ha corrompido, utilizas un hash como crypto/sha256. Sin embargo, el hash por sí solo no garantiza que un atacante no haya sustituido el archivo y su hash simultáneamente; para asegurar la autenticidad (saber quién envió el mensaje) además de la integridad, necesitas un HMAC (Hash-based Message Authentication Code) que utilice una clave secreta. Por otro lado, cuando guardas credenciales, el objetivo cambia: no buscas velocidad, sino lentitud extrema. Aquí es donde entran algoritmos como bcrypt o argon2, que están diseñados para ser computacionalmente costosos.

Esto funciona así porque los algoritmos de hashing estándar (SHA256, SHA512) están optimizados para la velocidad, lo cual es ideal para verificar grandes volúches de datos rápidamente, pero es un desastre para la seguridad de contraseñas. Un atacante con una GPU moderna puede probar miles de millones de combinaciones de SHA256 por segundo. bcrypt soluciona esto introduciendo un factor de coste (work factor) y un salt (sal) único para cada usuario de forma automática, lo que hace que el ataque de fuerza bruta sea prohibitivamente caro y evita el uso de tablas de arcoíris. Debes usar crypto/sha256 para integridades de archivos o firmas digitales, crypto/hmac para autenticar mensajes entre servicios (como webhooks) y bcrypt (o argon2 para mayor resistencia a ataques por hardware especializado) para cualquier cosa que sea una contraseña. Si usas un hash rápido para passwords, un atacante con una base de datos filtrada podrá crackear casi todas tus cuentas en cuestión de horas.

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"log"

	"golang.org/x/crypto/bcrypt"
)

// ChecksumFile simula la verificación de integridad de un archivo usando SHA256.
func ChecksumFile(data []byte) string {
	hash := sha256.Sum256(data) // Devuelve un [32]byte
	return hex.EncodeToString(hash[:])
}

// SignPayload genera un HMAC-SHA256 para asegurar que un mensaje no fue alterado.
func SignPayload(message []byte, secret []byte) []byte {
	h := hmac.New(sha256.New, secret)
	h.Write(message)
	return h.Sum(nil)
}

// HashPassword utiliza bcrypt para proteger una contraseña con un costo adaptativo.
func HashPassword(password string) (string, error) {
	// DefaultCost es 10; si el hardware mejora, puedes subir este valor.
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	return string(bytes), err
}

// VerifyPassword compara una contraseña con su hash de bcrypt.
func VerifyPassword(password, hash string) error {
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}

func main() {
	// 1. Integridad (SHA256)
	content := []byte("importante: el contenido del archivo")
	fmt.Printf("SHA256 Checksum: %s\n", ChecksumFile(content))

	// 2. Autenticidad (HMAC)
	secretKey := []byte("clave-secreta-de-api-muy-segura")
	payload := []byte(`{"user_id": 123, "action": "transfer"}`)
	signature := SignPayload(payload, secretKey)
	fmt.Printf("HMAC Signature: %x\n", signature)

	// 3. Seguridad de Passwords (bcrypt)
	userPass := "MiContraseñaSegura123!"
	hashedPass, err := HashPassword(userPass)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Bcrypt Hash: %s\n", hashedPass)

	// Verificación exitosa
	err = VerifyPassword(userPass, hashedPass)
	if err != nil {
		fmt.Println("Error: Contraseña incorrecta")
	} else {
		fmt.Println("Password validado correctamente")
	}

	// Verificación fallida
	err = VerifyPassword("incorrecto", hashedPass)
	if err != nil && errors.Is(err, bcrypt.ErrMismatchedHashAndPass) {
		fmt.Println("Error esperado: Contraseña inválida")
	}
}

Desglose del ejemplo

En el flujo de la función main, vemos cómo se separan las responsabilidades criptográficas.

Primero, ChecksumFile utiliza sha256.Sum256. Fíjate que esta función devuelve un array de tipo [32]byte. Esto es puramente para integridad: si un solo bit del content cambia, el hash resultante será completamente distinto. Es una operación extremadamente rápida.

Luego, en SignPayload, implementamos un HMAC. A diferencia de un hash simple, aquí pasamos una secretKey. Esto es vital en sistemas distribuidos; si el receptor tiene la misma clave, puede usar hmac.New(sha256.New, secret) para regenerar la firma y compararla con la recibida. Si coinciden, sabemos que el payload no fue modificado por un tercero.

Para la gestión de usuarios, HashPassword utiliza bcrypt.GenerateFromPassword. Lo más importante aquí no es solo que el hash sea irreversible, sino el bcrypt.DefaultCost. Este parámetro controla cuántas iteraciones de la función de expansión se realizan. Si mañana los ataques por fuerza bruta se vuelven más baratos, solo tienes que subir este valor. Además, el string resultante de hashedPass ya contiene el salt incrustado en su estructura, por lo que bcrypt.CompareHashAndPassword sabe exactamente qué salt usar para la comparación sin que nosotros tengamos que gestionarlo manualmente.

El error frecuente

Un error clásico es intentar implementar tu propio sistema de “protección” para contraseñas usando SHA256 de forma manual:

// ¡NUNCA HAGAS ESTO PARA PASSWORDS!
hash := sha256.Sum256([]byte("mi_password"))
fmt.Printf("%x", hash)

Este enfoque es vulnerable por dos razones:
1. Es demasiado rápido: Un atacante puede probar miles de millones de combinaciones por segundo.
2. No tiene salt: Si dos usuarios tienen la misma contraseña ("123456"), sus hashes serán idénticos. Un atacante puede usar tablas precalculadas (rainbow tables) para obtener todas las contraseñas comunes de tu base de datos instantáneamente. bcrypt o argon2 solucionan ambos problemas de raíz.

216

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio