(Ninguno de los ejemplos que proporcione debe usarse para el hashing de contraseñas. ¡Son demasiado rápidos!)
El primer principio general que mencionaría aquí es que queremos que nuestras operaciones criptográficas sean interfaces de alto nivel ; debe haber una abstracción reutilizable y común que defina:
- Qué condiciones previas debe cumplir la persona que llama antes de llamarla;
- Qué argumentos toma la operación;
- qué valores devuelve o qué condiciones posteriores cumple si se llama correctamente;
- Qué propiedades de seguridad debe ofrecer el concepto.
En este caso, estamos hablando de hash de contraseña . Una función de hashing de contraseñas, en su encarnación más simple, debería:
- Acepta dos argumentos, un salt y una contraseña;
- Devuelva un código hash para esa combinación de contraseña / sal;
- Inherentemente, consume una gran cantidad de recursos para que ralentice sustancialmente a un atacante que adivina la contraseña, pero no tanto como un usuario honesto
Entonces SHA-256 falla el primer y tercer punto. ¡Solo acepta un argumento, y es demasiado rápido!
Y, de hecho, la interfaz descrita anteriormente no es la mejor para el hashing de contraseñas. Una mejor interfaz es como un par de funciones escritas encima del hash de contraseña (en pseudocódigo Python-ish):
def generate_new_verification_code(password):
salt = crypto_random(16) # 16 byte random salt
hash = password_hash(salt, password)
verification_code = salt + hash
return verification_code
def verify_password(putative_password, actual_verification_code):
actual_salt = actual_verification_code[0:16]
actual_hash = actual_verification_code[16:-1]
putative_hash = password_hash(actual_salt, putative_password)
return putative_hash == actual_hash
Por lo tanto, la abstracción adecuada dice que para la verificación de la contraseña deberíamos estar usando dos funciones como estas, escritas sobre una función password_hash
de uso intensivo de recursos, que a su vez probablemente se escribiría sobre una función de bajo nivel como SHA -256.
Tenga en cuenta que la interfaz de hash de contraseña de PHP sigue este último diseño:
- La función
password_hash
debe llamarse con una contraseña pero sin sal ; escoge una sal al azar, y como dice la página: "El algoritmo, el costo y la sal utilizados se devuelven como parte del hash. Por lo tanto, toda la información que se necesita para verificar el hash se incluye en ella".
- La función
password_verify
se usa para verificar si una contraseña putativa coincide con la cadena de verificación .
Segundo problema: una idea clave en el diseño de protocolos de seguridad u otras construcciones de seguridad es que los atacantes a menudo romperán las reglas y harán que su código sea llamado de maneras y contextos inesperados . Por ejemplo, cuando la gente escribe algo como esto:
hash($salt.$pass)
hash($pass.$salt)
... a menudo asumen implícitamente que salt
siempre tendrá la misma longitud fija, y no se detienen a considerar si algún atacante podría:
- Encuentre una manera de "romper las reglas" para que puedan causar que la longitud de
salt
varíe a lo largo de varias llamadas al código;
- Encuentre alguna forma inesperada de explotar esto para su ventaja.
Por ejemplo, podrías pensar que es imposible que un atacante encuentre una colisión para las contraseñas de esta manera, pero de hecho, si pueden controlar el salt
y el pass
, es trivial hacerlo:
hash("0001"."Passsword1!")
hash("0001P"."asssword1!")
Esto puede sonar descabellado, y bueno, para decirte la verdad, no puedo pensar en un escenario en el que esto pueda explotarse. Pero realmente nos gustaría que nuestra seguridad se fundara por razones más sólidas que "No se me ocurre ninguna manera de romper esto". Entonces, como filosofía general, es más seguro diseñar las cosas de tal manera que no puedan surgir ambigüedades. En este caso, nos gustaría que se cumpla la siguiente regla:
- Si hash dos contraseñas diferentes con dos sales diferentes, las entradas que proporcionamos a la función hash de bajo nivel deberían ser diferentes.
Esto significa que para generar la entrada que alimentamos al hash subyacente, deberíamos preferir combine
con un inyectable función : una función tal que combine(salt1, password1) == combine(salt2, password2)
si y solo si salt1 == salt2
y password1 == password2
.
hash(combine($salt, $password))
Algunas formas de hacer esto:
- Ponga una de las entradas en un tamaño fijo y antepóngala a la otra. HMAC hace esto internamente para las llaves.
- Hash una de las dos entradas y antepónelo a la otra. HMAC hace esto para las claves que son más largas que el tamaño de bloque de la función hash subyacente.
- Coloque un delimitador no ambiguo entre las entradas (posiblemente requiera que escape las incidencias del delimitador en los valores de entrada).
Funciones de hash de contraseña dedicadas como PBKDF2, bcrypt, scrypt y Argon2 se adhieren a la mayoría de estas ideas.