Introducción
Las aplicaciones que manejan cuentas de usuario necesitan autenticar (establecer la identidad de) el usuario antes de permitir (autorizar) que el usuario realice diferentes tareas dentro de la aplicación. Las contraseñas son un método probado y común que se utiliza para autenticar a los usuarios. En la autenticación típica basada en contraseña, las credenciales de usuario consisten en una ID de inicio de sesión (nombre de usuario, correo electrónico, etc.) y una contraseña. Estas credenciales se almacenan en la base de datos. Para cada intento de inicio de sesión, las credenciales ingresadas por el usuario se comparan con las almacenadas en la base de datos.
Al almacenar las credenciales de usuario en la base de datos, nunca (nunca) almacene las contraseñas como texto sin formato (texto legible sin cifrar). Lo opuesto al texto sin formato es el texto cifrado.
-
Todos los sistemas informáticos son hackeables. En el desafortunado caso de que su servidor se vea comprometido y un atacante obtenga incluso acceso de solo lectura a la base de datos (o a un volcado de la base de datos), puede obtener las credenciales de inicio de sesión de los usuarios, incluidos los usuarios privilegiados.
-
Los usuarios a menudo ignoran las mejores prácticas de seguridad y usan la misma contraseña para múltiples servicios. Exponer la contraseña de un usuario en su servicio puede hacer que la cuenta del usuario se vea comprometida en otros servicios.
-
Si el proveedor de servicios puede leer las contraseñas, el usuario puede culpar al proveedor de servicios en caso de que suceda algo no deseado. Además, los desarrolladores y administradores de sistemas sin escrúpulos pueden hacer un mal uso de las contraseñas de los usuarios.
-
Muchas regulaciones nacionales y supranacionales (por ejemplo, GDPR) hacen que sea ilegal almacenar contraseñas como texto sin formato.
Alcance
El alcance de este artículo se limita al almacenamiento seguro de las contraseñas de los usuarios. Este artículo no cubre el cifrado de datos general en PostgreSQL, ni cubre el cifrado de las credenciales de la base de datos (es decir, las contraseñas utilizadas para iniciar sesión en la propia base de datos) o las conexiones.
requisitos previos
Para beneficiarse de esta guía, es necesario tener una exposición previa a PostgreSQL. Para probar los ejemplos, se supone que ya tiene PostgreSQL ejecutándose en un servidor independiente o como una instancia de Vultr Managed Databases para PostgreSQL.
Es útil, pero no obligatorio, estar familiarizado con los conceptos básicos de criptografía, como cifrado y hashing.
Tenga en cuenta que todos los ejemplos de código de esta guía son instrucciones SQL. Tenga en cuenta también que las cadenas de texto en las sentencias SQL están entre comillas simples.
Primeros principios – Hashing de contraseñas
Si las contraseñas de los usuarios no deben almacenarse como texto sin formato, deben almacenarse en forma cifrada. Sin embargo, los algoritmos de cifrado tienen algoritmos de descifrado correspondientes que se utilizan para recuperar los datos cifrados. El objetivo del cifrado es ofuscar los datos temporalmente y descifrarlos al original cuando sea necesario. Pero esto no es deseable en el caso de las contraseñas. Almacenar una contraseña cifrada que se pueda descifrar fácilmente no supone ningún beneficio adicional. Por lo tanto, las contraseñas no están encriptadas sino codificadas.
A hash
es una cadena de aspecto aleatorio que se genera a partir de una cadena de entrada. El algoritmo que genera el hash se llama función hash. Es fácil calcular el hash de una cadena de entrada, pero es casi imposible calcular el valor de una cadena dado su hash. Se dice que hashing es un cálculo unidireccional.
Por lo tanto, el vector de ataque más práctico contra las contraseñas hash es la fuerza bruta. Los intentos de fuerza bruta generalmente se basan en diccionarios. La idea general es comenzar con una lista (diccionario) de las contraseñas más utilizadas y calcular secuencialmente el hash de cada contraseña posible hasta que haya una coincidencia.
Hashing es una tarea computacionalmente intensiva; esto hace que la fuerza bruta sea difícil. Además, las funciones de hashing de contraseñas utilizan técnicas como estiramiento clave para ralentizar aún más los intentos de fuerza bruta. Un example Una de las técnicas de estiramiento de claves es aplicar hash repetidamente a una cadena (primero calcular el hash, luego el hash del hash, y así sucesivamente).
Hashing de contraseñas en PostgreSQL
El pgcrypto
Extensión
En PostgreSQL, el pgcrypto
La extensión tiene las funciones necesarias para cifrar contraseñas. Desde pgcrypto
es una extensión incorporada, no necesita descargar ni instalar ningún software adicional. Habilitar la extensión:
CREATE EXTENSION pgcrypto ;
Sal
Una función hash siempre genera el mismo hash para una cadena de entrada determinada. Esto conduce a dos problemas principales:
-
Si dos usuarios tienen la misma contraseña, sus hashes son los mismos. Es preferible que todos los hashes sean únicos.
-
La fuerza bruta es computacionalmente costosa. Por lo tanto, los atacantes suelen utilizar mesas de arcoiris en lugar de fuerza bruta. Estas tablas contienen valores hash calculados previamente de contraseñas de uso común.
Ambos problemas se resuelven calculando el hash utilizando dos valores de entrada: la cadena de entrada (la contraseña) y la sal. La sal es una cadena generada pseudoaleatoriamente que ayuda a garantizar la unicidad de los valores hash. Incluso si dos usuarios tienen la misma contraseña, sus valores de sal serán diferentes. Por lo tanto, todos los hashes serán únicos.
El gen_salt()
Función
La sal se genera utilizando el gen_salt()
función. Por lo general, esta función toma un argumento: type
. La sintaxis es:
-- pseudocode
gen_salt(type) ;
El type
El argumento denota el tipo de algoritmo criptográfico que se usará para el hashing. Puede tomar uno de cuatro valores posibles:
-
des
– para el algoritmo Estándar de cifrado de datos (DES) -
xdes
– para el algoritmo DES extendido -
md5
– para el algoritmo de resumen de mensajes MD5 -
bf
– para el algoritmo Blowfish
Para examplepara generar una sal usando el algoritmo Extended DES:
SELECT gen_salt('xdes') ;
De manera similar, para generar una sal usando el algoritmo MD5:
SELECT gen_salt('md5') ;
Tenga en cuenta que el salt también codifica información sobre el tipo de algoritmo criptográfico que se utilizará para el hash, así como (si corresponde) los parámetros de hash. Intente generar el mismo tipo de sal repetidamente; observe que las sales del mismo tipo siguen un patrón constante.
Guardar nueva contraseña
Cuando un usuario crea una nueva contraseña (o cambia su contraseña existente), la base de datos necesita almacenar (el hash de) la contraseña. El hash se genera utilizando el crypt()
función.
El crypt()
Función
El crypt()
función se basa en la Biblioteca de criptas de Unix . Genera un hash basado en los siguientes argumentos:
-
la cadena de contraseña
-
un valor de sal
La sintaxis de la crypt()
función es:
-- pseudocode
crypt(password_string, salt_string) ;
El salt_string
El argumento se genera usando el gen_salt()
función. Para exampleusar el md5
algoritmo para calcular el hash:
-- pseudocode
SELECT crypt(password_string, gen_salt('md5')) ;
Hashing sin Sal
Para simular el uso del crypt()
función sin una sal aleatoria, use una cadena constante, salt
, para la sal. Generar el hash para la contraseña, supersecurepassword
:
SELECT crypt('supersecurepassword', 'salt') ;
El comando anterior genera el texto cifrado saUkChKIZTKFs
. Una sal no aleatorizada (o sin sal) conduce a hashes predecibles y hace que el sistema sea vulnerable a los ataques de la tabla del arco iris. Por lo tanto, es necesario utilizar sales aleatorias.
Hashing de contraseña con sal aleatoria
Generar un hash para la cadena de contraseña supersecurepassword
usando, para exampleel algoritmo MD5:
SELECT crypt('supersecurepassword', gen_salt('md5')) ;
Copie el valor hash generado por el comando anterior. Lo utilizará en la siguiente sección.
Compruebe si la contraseña ingresada es correcta
Cuando un usuario existente inicia sesión, ingresa su nombre de usuario y contraseña. El servidor necesita verificar si la contraseña ingresada coincide con la contraseña almacenada. Esto se hace llamando al mismo crypt()
función con los siguientes argumentos:
-
La contraseña ingresada
-
El hash almacenado de la contraseña real (en lugar de una sal generada)
Si la contraseña ingresada es la misma que la contraseña real, el hash de la contraseña ingresada coincide con el hash almacenado (de la contraseña real).
La sintaxis general es:
--pseudocode
crypt(entered_password, stored_hash_of_actual_password) ;
Supongamos que el usuario ingresa la contraseña correcta, supersecurepassword
. Para probar esto, llame al crypt()
funcionar de la siguiente manera:
SELECT crypt('supersecurepassword', 'generated_hash_value_from_actual_password') ;
El segundo argumento anterior es el hash generado por el comando anterior: pegue el valor hash que había copiado anteriormente. Este comando genera el mismo hash que generó la contraseña real.
Ahora, supongamos que el usuario ingresa la contraseña incorrecta, wrong_password
. Llama a crypt()
funcionar de la siguiente manera:
SELECT crypt('wrong_password', 'generated_hash_value_from_actual_password') ;
El valor hash de salida es diferente del hash de la contraseña real.
En otras palabras, llamar a la crypt()
función con una cadena ( string1
) y el hash ( hash1
) de esa cadena ( string1
) genera el mismo hash ( hash1
).
Uso práctico
En el uso real, los valores hash se almacenan y consultan desde tablas. Los ejemplos de esta sección muestran cómo hacerlo:
Crear un user_account
tabla con tres columnas: una ID generada automáticamente, el nombre de usuario y el hash de la contraseña del usuario:
CREATE table user_account (user_id SERIAL, user_name VARCHAR(10), password_hash VARCHAR(100)) ;
Inserte una fila de datos de prueba en la tabla:
INSERT INTO user_account (user_name, password_hash)
VALUES ('user1', crypt('user1_password', gen_salt('md5'))) ;
El comando anterior inserta el nombre de usuario user1
y el hash MD5 de la contraseña, user1_password
.
Para cambiar el (hash de) la contraseña del usuario, use el SQL UPDATE
dominio:
UPDATE user_account
SET password_hash = crypt('user1_new_password', gen_salt('md5'))
WHERE user_name="user1" ;
Para hacer coincidir la contraseña ingresada con la contraseña correcta, llame al crypt()
función con la contraseña ingresada y el hash de la contraseña correcta como la sal.
Si el usuario intenta iniciar sesión con la contraseña correcta, user1_new_password
:
SELECT (password_hash = crypt('user1_new_password', password_hash))
AS password_match
FROM user_account
WHERE user_name="user1" ;
Esto debería dar salida t
para true
como el valor de password_match
.
Si el intento de inicio de sesión utiliza una contraseña incorrecta, user1_wrong_password
:
SELECT (password_hash = crypt('user1_wrong_password', password_hash))
AS password_match
FROM user_account
WHERE user_name="user1" ;
Esto debería dar salida f
para false
como el valor de password_match
.
Uso avanzado
Cuando el hash se calcula utilizando los algoritmos Extended DES o Blowfish, es posible personalizar (afinar) el número de iteraciones que experimenta el algoritmo para generar el hash. En este caso, el gen_salt()
función puede aceptar un argumento adicional, iter_count
, para el número de iteraciones. La sintaxis es:
-- pseudocode
gen_salt(type, iter_count)
En la declaración anterior, type
es cualquiera xdes
o bf
.
-
El recuento de iteraciones para el DES extendido (
xdes
) el algoritmo puede ser un número impar entre 1 y 16777215. El valor predeterminado es 725. -
El recuento de iteraciones para el Blowfish (
bf
) el algoritmo puede ser un número entero entre 4 y 31. El valor predeterminado es 6.
Si el hash se ha generado después de N iteraciones, cualquier intento de fuerza bruta también debe generar hash de conjeturas de contraseña N veces. Cuanto mayor sea el valor de N, más difícil será aplicar fuerza bruta al hash de la contraseña. Sin embargo, hacer que el cálculo del hash sea demasiado lento no es práctico para el uso regular. Como demostración, hash la contraseña supersecurepassword
utilizando el algoritmo Blowfish con el número predeterminado de iteraciones, 6:
SELECT crypt('supersecurepassword', gen_salt('bf', 6)) ;
Fíjese aproximadamente cuánto tiempo lleva (usando el reloj de su computadora o teléfono). Ahora ejecute la misma función hash con un mayor número de iteraciones:
SELECT crypt('supersecurepassword', gen_salt('bf', 30)) ;
Lleva mucho más tiempo. Si tarda demasiado, cancele la operación usando CTRL
+ C
y vuelva a intentarlo con un número menor de iteraciones.
Si ingresa un número no válido de iteraciones, arroja un error como este:
ERROR: gen_salt: Incorrect number of rounds
Ajuste de la función hash
Elegir el número de iteraciones es un equilibrio entre usabilidad y seguridad. Cuanto mayor sea el número de iteraciones, más tiempo llevará calcular el hash. Esto lo hace menos fácil de usar, pero también aumenta la seguridad al frustrar los intentos de fuerza bruta. Una recomendación común es elegir el número de iteraciones de modo que el hardware de servidor estándar pueda calcular entre 4 y 100 hashes por segundo.
Conclusión
En realidad, las contraseñas son una solución subóptima. El almacenamiento de contraseñas en el servidor (incluso en formato hash) coloca toda la responsabilidad en el desarrollador y los administradores de la aplicación. La autenticación es un tema complejo y es recomendable utilizar proveedores de servicios dedicados que se especialicen en ella, como Open ID . Permitir que los usuarios inicien sesión con un conjunto estándar de credenciales (como su cuenta de Google) es ventajoso tanto para los usuarios como para los desarrolladores de aplicaciones.
No obstante, la autenticación basada en contraseña es conveniente en muchos casos y se usa ampliamente. Por lo tanto, se debe tener el debido cuidado para almacenar de forma segura las contraseñas de los usuarios y salvaguardar tanto la seguridad del usuario como la integridad de la aplicación.
Advertencias
-
Este artículo solo analiza cómo almacenar contraseñas de forma segura en la base de datos. Esto también se denomina protección de datos en reposo. Igualmente importante es asegurar los datos en tránsito. Al transmitir contraseñas a través de Internet (por exampledesde un front-end web) siempre asegúrese de 1) usar HTTPS y 2) enviar las credenciales de inicio de sesión como datos de formulario, no como parámetros de URL.
-
Antes de que la contraseña haya sido codificada, está disponible como texto sin formato. Esto lo hace vulnerable a empleados sin escrúpulos con acceso a la base de datos y/o al servidor web. Por lo tanto, el sistema descrito anteriormente se basa en confiar en los desarrolladores y administradores de la aplicación. Si no se puede establecer esta confianza, considere usar métodos de encriptación en el lado del cliente. De esta manera, las contraseñas de texto sin formato nunca aparecen en el servidor.
-
Además, el hashing de contraseñas solo es efectivo contra un atacante que ha obtenido acceso de solo lectura a la base de datos (o un volcado de la base de datos). A un atacante que tenga acceso de escritura a la base de datos le resultará más fácil simplemente sobrescribir los hash de la contraseña.
En última instancia, ninguna seguridad es verdaderamente infalible. Mayores medidas de seguridad implican un diseño de sistema más complejo o sistemas menos fáciles de usar. Debe lograr un equilibrio según las necesidades de seguridad de su aplicación en particular.
Título del artículo Nombre (opcional) Correo electrónico (opcional) Descripción
Enviar sugerencia