tl; dr: En general, no lo recomendaría, pero puede ser útil en circunstancias específicas
Ventajas de SCRAM
- La contraseña nunca se envía al servidor. Las únicas cosas enviadas en el cable son:
- El nonce
- La sal
- El recuento de iteraciones
-
HMAC(PBKDF2(password), "Client Key") XOR HMAC(H(HMAC(PBKDF2(password), "Client Key")), AuthMessage)
(en serio, ¡no lo estoy inventando!)
-
HMAC(HMAC(PBKDF2(password), "Server Key"), AuthMessage)
- El servidor nunca conoce la contraseña y, por lo tanto, no puede hacerse pasar por el usuario en otros servidores (siempre que la sal en esos servidores sea distinta de la sal de este servidor)
Desventajas de SCRAM
- Requiere el uso de PBKDF2. Si bien PBKDF2 es significativamente mejor que un hash rápido, bcrypt es mejor (y hoy en día también deberías considerar Argon2).
- Requiere que toda la complejidad computacional sea manejada por el cliente
El primer punto es desafortunado, pero el segundo es el verdadero asesino. Ahora, con los teléfonos inteligentes y las tabletas en todas partes, simplemente no puede asumir que el cliente podrá calcular rápidamente PBKDF2 con suficientes iteraciones para estar seguro.
¿Cuándo sería útil SCRAM?
El uso que veo para SCRAM es donde el cliente es de confianza pero el servidor no. Esto nunca es cierto para una aplicación web (ya que el "cliente" es el JavaScript enviado por ... el servidor), y solo a veces es cierto para las aplicaciones locales. Los mejores ejemplos que se me ocurren son los Wikipedia da : SMTP, IMAP y XMPP, donde confía en su correo electrónico o cliente de chat, pero se están autenticando en un servidor de terceros potencialmente malintencionado.
Si el cliente impone un alto recuento de iteraciones y sal única, puede mitigar el riesgo de un secuestro de DNS combinado con una CA deshonesta, pero ya lo ha hecho con la fijación de certificados. Lo único que queda por defender es que el servidor sea malicioso, lo que puede ser útil en algunos casos, pero solo si las ganancias superan las desventajas de SCRAM.
SCRAM podría ser útil para evitar que el servidor obtenga la contraseña si sabe que se usará una contraseña segura, pero que la contraseña también se reutilizará en otro lugar (si no se reutiliza, ¿por qué? ¿Te importa si el servidor puede saberlo?). Lamentablemente, esto realmente no se puede hacer cumplir, y el ideal sería utilizar una contraseña única de todos modos.
¿Cómo funciona SCRAM?
En SCRAM, el servidor almacena:
- La sal
- El recuento de iteraciones
-
ServerKey = HMAC(PBKDF2(password), "Server Key")
(donde "Server Key"
es una clave HMAC constante conocida) *
-
StoredKey = H(HMAC(PBKDF2(password), "Client Key"))
(ídem para "Client Key"
)
Para autenticar:
- El cliente envía el nombre de usuario y una fuente aleatoria
- El servidor agrega su propia fuente aleatoria al cliente y responde con el recuento de nonce, salt e iteración para el usuario
- El cliente calcula lo siguiente:
-
ClientKey = HMAC(PBKDF2(password), "Client Key")
-
StoredKey = H(ClientKey)
-
ClientSignature = HMAC(StoredKey, AuthMessage)
(donde AuthMessage
es la concatenación de todos los mensajes intercambiados previamente y el nonce, delimitado por comas)
-
ClientProof = ClientKey XOR ClientSignature
- El cliente envía el nonce y
ClientProof
al servidor
- El servidor calcula lo siguiente:
-
ClientSignature = HMAC(StoredKey, AuthMessage)
-
ClientKey = ClientProof XOR ClientSignature
-
ServerSignature = HMAC(ServerKey, AuthMessage)
- El servidor verifica
H(ClientKey) == StoredKey
, lo que demuestra que el cliente conoce la contraseña (o al menos ClientKey
)
- El servidor responde con
ServerSignature
- El cliente calcula
ServerSignature
y lo compara con el valor devuelto por el servidor, verificando que el servidor conoce el ServerKey
Esto está bastante simplificado, ya que excluye algunas cosas como la selección de hash, la codificación, el formato del mensaje y el enlace del canal, pero debería dar una buena idea de cómo funciona.
* Todos los usos de PBKDF2 incluyen obviamente el conteo de sal y iteración, SCRAM establece dkLen
a la longitud de salida del hash seleccionado