TL;DR: Hashear contraseñas no sirve si el caso de uso requiere que un administrador pueda visualizar y recuperar dichas claves (ej. profesores gestionando PINs de alumnos menores de edad). En estos escenarios, usar cifrado simétrico (AES) con un Vector de Inicialización (IV) y un gestor de secretos ofrece el equilibrio perfecto entre la seguridad de los datos y las necesidades reales de UX del producto.
El problema de UX al usar Hashing para contraseñas
En una auditoría de seguridad detecté que las contraseñas de los alumnos no estaban en puro texto plano. Es decir, la verificación de la contraseña era algo tal que así:
if (student == null || !login.Password.Equals(student.Password))
Y por supuesto, saltaron todas las alarmas, ¿por qué demonios no estaba hasheada la contraseña de los estudiantes? Y más cuando las contraseñas de los profesores sí que estaban utilizando IPasswordHasher para las suyas.
Tras consultar con los diseñadores del producto me comentaron que la contraseña no era tanto una contraseña sino más bien un PIN de acceso que los profesores proporcionaban a los alumnos, junto con el nombre de usuario de cada alumno, para que pudieran hacer login en el juego y realizar los entrenamientos, desafíos, tests, tutoriales, etcétera que el profesor fuese proponiendo durante sus lecciones o deberes.
Así que la respuesta obvia de hashear la contraseña con bcrypt o PBKDF2 no era una opción puesto que imposibilitaría que el profesor pudiera consultar, exportar o modificar el PIN de acceso de sus alumnos en cualquier momento. Sin embargo, dejarlas en texto plano tampoco era la mejor de las opciones... cualquier fuga de la base de datos expondría directamente las claves, y podría suplantar a cualquier alumno sin necesidad de descifrar nada.
El trade-off: Cifrado Simétrico (AES) vs Hashing
El conflicto estaba en cómo mantener la usabilidad de la aplicación y al mismo tiempo, aumentar la seguridad de las contraseñas. Hashear sería la máxima seguridad pero como mucho podría consultar la contraseña la primera vez que crease a sus alumnos, luego sería irrecuperable. Tan solo se podría resetear. Esto parecía algo incómodo para los profesores y desde diseño de producto me seguían diciendo que el profesor tenía que poder acceder a las contraseñas en cualquier momento.
"48291" → hash → "AQAAAAIAAYag..." → IRRECUPERABLE ❌
Hashing (unidireccional) descartado, es el estándar cuando es el propio usuario el que gestiona su contraseña. Ocurre en la mayoría de aplicaciones, pero no es el caso de los estudiantes de esta aplicación.
El cifrado simétrico (bidireccional)
"48291" → cifrar con clave → "x7Bk9mQ=..." → RECUPERABLE con clave del servidor ✅
De este modo es posible:
- Descifrar la contraseña usando la clave de servidor
- Proteger los datos contra fuga directa de la DB, sin la clave de servidor no se puede descifrar. La peor parte, si un atacante obtiene la DB y la clave, lo podría descifrar todo, por lo que hay que proteger fuertemente la clave con Azure Key Vault o similares.
No es una decisión universal. Es una decisión basada en el contexto concreto de la aplicación y los requerimientos de usabilidad.
La solución: Cifrado AES en .NET para contraseñas
En este caso elegí un Cifrado AES porque:
- Encajaba bien con los requerimientos y el flujo principal de uso en el aula por parte del profesor. Permite consultar y exportar las contraseñas cuando sea necesario.
- Son PINs de acceso no contraseñas personales.
- Son alumnos menores que no gestionan sus credenciales sino que se las proporciona el docente, que es la autoridad que administra las cuentas.
- El cifrado protege contra fugas de DB, sql injection, acceso sin autorización, etc.
- El hashing no encaja con varios casos de uso del producto.
Además, cada operación de cifrado utiliza un Initialization Vector (IV), un valor aleatorio para que el cifrado sea diferente incluso cuando la contraseña sea la misma, evitando que los atacantes puedan detectar contraseñas repetidas comparando los textos cifrados.
Algunos apuntes de la implementación
En primer lugar creé la interfaz del servicio IStudentPasswordService. Aunque sea vox populi bien merece la pena recordar por qué utilicé un servicio. Los servicios nos permiten testear con mucha más facilidad, desacoplan y nos ajustamos a los principios de inversión de dependencias y de responsabilidad única y, no menos importante, nos permiten reutilizar el código donde sea necesario.
Implementé el StudentPasswordService y solo señalaré en este paso que es importante que la comparación de componentes criptográficos debe realizarse utilizando:
public class StudentPasswordService : IStudentPasswordService
{
//...
var decrypted = Decrypt(storedPassword);
var decryptedBytes = Encoding.UTF8.GetBytes(decrypted);
return CryptographicOperations.FixedTimeEquals(inputBytes, decryptedBytes);
//...
}
Si utilizáramos algo como input == decrypted estaríamos expuestos a Timing attacks en los que el atacante utiliza los ms que tarda en rechazar la comparación (en el primer carácter diferente) para deducir la contraseña. Es decir, si el atacante puede realizar múltiples peticiones y medir el tiempo que tarda la petición en ser rechazada para averiguar cuántos caracteres son correctos. CryptographicOperations.FixedTimeEquals evita este problema realizando la comparación en tiempo constante.
También modifiqué el login, la creación y edición de estudiantes y las vistas del profesor para que utilizaran este servicio para cifrar o descifrar.
¿Y las contraseñas existentes?
Pues una migración en caliente, automática y sin downtime.
Login de alumno:
-> Intentamos `Decrypt` como AES
-> Si "OK", el alumno se loguea, ya tenía su contraseña cifrada en AES ✅
-> Si falla, comparamos en texto plano con su contraseña
-> Si "Ok" , ciframos su contraseña y la guardamos en DB. ✅
-> Su próximo login será correcto con `Decrypt
¿Y si un alumno no hace login y su contraseña no se cifra?
Esta aplicación tiene ciclos de uso anuales, por lo que no sería un problema "duradero". Todos los alumnos se crean nuevos cada año para ajustar a las clases de cada curso de los profesores y la creación de nuevos alumnos ya se ajusta a la nueva política de seguridad.
¿Había otras alternativas?
Alternativa 1
Se podría haber implementado un sistema en el que fuera el alumno el que gestionase su contraseña. Pero son menores, en un contexto académico y el producto estaba diseñado para que fuera el profesor quien lo gestionase y controlase el flujo educativo.
Alternativa 2
Mostrar el PIN una sola vez, al crear cada alumno y ofrecer un botón para resetear la contraseña. Esto lo hacen muchas plataformas pero la creación del alumno no solía coincidir en el tiempo con el momento en el que se le daban las credenciales al estudiante. Esto era una opción pero no facilitaba el trabajo a los profesores y consideramos que empeoraba la experiencia de uso.
La lección
Aunque la auditoría tenía toda la razón en el diagnóstico, la solución estándar no encajaba directamente con el flujo de uso de los profesores por lo que había que buscar otras soluciones que sí que se amoldasen a él. No hay que aplicar cualquier solución estandarizada sin entender el contexto del sistema ni las necesidades del producto y usuarios.