Calcular SHA-256 en PostgreSQL: digest, pgcrypto y cuándo hashear en BD
PostgreSQL tiene una función MD5 builtin y una familia de hashes más completa en la extensión pgcrypto. La elección entre ambos va de si el hash es para seguridad o solo para huella, y de si la base de datos es el sitio adecuado para calcularlo.
MD5 viene de serie, el resto necesita pgcrypto
md5(text) devuelve una cadena hex de 32 caracteres en minúsculas y viene en el core de Postgres sin extensión.
SELECT md5('hello world');
-- 5eb63bbbe01eeed093cb22bb8f5acdc3
Para SHA-1, SHA-224, SHA-256, SHA-384 y SHA-512 necesitas la extensión pgcrypto y su función digest:
```sql CREATE EXTENSION IF NOT EXISTS pgcrypto;
SELECT digest('hello world', 'sha256'); -- bytea: \xb94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 ```
digest siempre devuelve bytea (bytes crudos). Para sacar un string hex, envuelve con encode.
encode y el round-trip de bytea
encode(bytea, 'hex') convierte bytes crudos en string hex en minúsculas, la forma que esperan la mayoría de APIs y capas de almacenamiento:
SELECT encode(digest('hello world', 'sha256'), 'hex');
-- b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
Para almacenamiento binario, déjalo como bytea y guarda los 16 o 32 bytes directamente. Un SHA-256 en hex son 64 caracteres; el bytea crudo son 32 bytes. Es la mitad de almacenamiento e idéntica velocidad de lookup una vez indexado. Convierte a hex solo en la frontera con la API.
encode también acepta 'base64' y 'escape' si tu consumidor prefiere esas formas. decode(text, 'hex') es el inverso: parsea un string hex de vuelta a bytea para comparaciones.
MD5 vale para huellas, no para seguridad
MD5 está roto como hash criptográfico. Las colisiones son computacionalmente baratas. No lo uses para hashing de contraseñas, verificación de firmas, ni nada donde un atacante pueda fabricar una entrada que colisione.
MD5 sigue valiendo para huella no adversarial: detectar si dos filas son idénticas byte a byte, deduplicar líneas de log, montar una columna checksum para cortocircuitar comparaciones. La probabilidad de colisión accidental sobre cualquier dataset realista sigue siendo efectivamente cero.
Para contraseñas, ni MD5 ni SHA-256 son la respuesta. Usa crypt(password, gen_salt('bf', 12)) de pgcrypto, que es bcrypt con factor de coste. O mejor, haz el hashing en la aplicación con argon2 y guarda solo el digest resultante. Hacerlo en la base implica que la contraseña en claro llega a la base, lo cual amplía tu radio de explosión si la BD se ve comprometida o si el log de queries se filtra.
Cuándo hashear en la base de datos
Hashea en la base cuando el hash es parte del dato: una columna de lookup direccionada por contenido, una clave de dedupe determinista, un CHECK que verifica que el cuerpo de la fila coincide con una huella guardada.
Hashea en la aplicación cuando el hash es parte de un flujo de autenticación o firma: contraseñas, firmas JWT, HMAC del cuerpo de una request, contenido de un mensaje cifrado extremo a extremo. La base de datos no debería ver nunca el texto plano, y calcular el hash allí significa que sí lo ve.
Regla útil: si la entrada del hash también se guarda en la misma tabla, calcularlo en la BD vale. Si la entrada no se guarda nunca, la BD no tiene por qué verla.
gen_random_uuid también está en pgcrypto, pero no es un hash. Genera un UUID v4 aleatorio. No lo confundas con digest cuando leas migraciones de otra gente.
Ejemplo completo
sql-- Activar pgcrypto para la familia SHA
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Añadir columna de huella SHA-256 a una tabla de documentos
ALTER TABLE documents ADD COLUMN body_sha256 bytea;
-- Backfill: hashear los bodies existentes
UPDATE documents
SET body_sha256 = digest(body, 'sha256');
-- Indexar para lookup de igualdad rápido
CREATE INDEX documents_body_sha256_idx ON documents (body_sha256);
-- Encontrar duplicados por contenido
SELECT body_sha256, count(*)
FROM documents
GROUP BY body_sha256
HAVING count(*) > 1;
-- Devolver un hash como hex para una respuesta de API
SELECT id, encode(body_sha256, 'hex') AS body_sha256_hex
FROM documents
WHERE id = $1; ¿Solo necesitas el resultado?
Cuando tienes un string en el portapapeles y solo quieres comparar su SHA-256 contra un valor en un manifest de despliegue, abrir psql es exagerado. El generador de hash en navegador de aldeacode.com calcula MD5, SHA-1, SHA-256 y SHA-512 con la Web Crypto API, no manda la entrada al servidor, y te da el digest hex en un solo pegado.
Abrir Generador de Hashes SHA →Preguntas frecuentes
¿Por qué digest devuelve algo que empieza por \x?
Es la representación por defecto de bytea en psql. El valor guardado son los bytes crudos; el prefijo \x es solo cómo los pinta el cliente. Envuelve la llamada en encode(digest(...), 'hex') para tener un hex limpio, o en encode(..., 'base64') para base64.
¿Puedo usar MD5 para hashear contraseñas de usuario?
No. MD5 es rápido y propenso a colisiones, exactamente las propiedades opuestas a las que quieres en un hash de contraseña. Usa crypt(password, gen_salt('bf', 12)) de pgcrypto si tienes que hashear en la BD, o hashea con argon2 en la aplicación antes de que el valor llegue a la base.
¿Calcular SHA-256 en la base es más lento que en la app?
Por fila, parecido. La diferencia de coste la domina el round-trip a la aplicación, no el hash en sí. Calcula en la BD cuando ahorras un round-trip (una columna de huella en INSERT, un CHECK), en la app cuando evitas mandar texto en claro por la red (contraseñas, secretos, firmas de request).