salted password online hashed and security hash passwords cryptography password-hash

security - password - ¿Es el "hash doble" una contraseña menos segura que solo una vez?



salt hash password c# (16)

Hashear una contraseña una vez es inseguro

No, los hashes múltiples no son menos seguros; Son una parte esencial del uso seguro de la contraseña.

La iteración del hash aumenta el tiempo que tarda un atacante en probar cada contraseña en su lista de candidatos. Puede aumentar fácilmente el tiempo que lleva atacar una contraseña de horas a años.

La simple iteración no es suficiente

El simple hecho de encadenar la salida de hash a la entrada no es suficiente para la seguridad. La iteración debe tener lugar en el contexto de un algoritmo que preserva la entropía de la contraseña. Afortunadamente, hay varios algoritmos publicados que han tenido el escrutinio suficiente para dar confianza en su diseño.

Un buen algoritmo de derivación de claves como PBKDF2 inyecta la contraseña en cada ronda de hash, mitigando las preocupaciones sobre las colisiones en la salida de hash. PBKDF2 se puede usar para la autenticación de contraseña tal como está. Bcrypt sigue la derivación de la clave con un paso de cifrado; de esa manera, si se descubre una manera rápida de revertir la derivación de la clave, un atacante aún tiene que completar un ataque de texto simple conocido.

Cómo romper una contraseña

Las contraseñas almacenadas necesitan protección contra un ataque sin conexión. Si las contraseñas no tienen fecha de sal, se pueden romper con un ataque de diccionario precalculado (por ejemplo, utilizando una tabla Rainbow). De lo contrario, el atacante debe dedicar tiempo a calcular un hash para cada contraseña y ver si coincide con el hash almacenado.

Todas las contraseñas no son igualmente probables. Los atacantes pueden buscar exhaustivamente todas las contraseñas cortas, pero saben que sus posibilidades de éxito de fuerza bruta disminuyen drásticamente con cada personaje adicional. En su lugar, utilizan una lista ordenada de las contraseñas más probables. Comienzan con "password123" y pasan a las contraseñas de uso menos frecuente.

Digamos que una lista de atacantes es larga, con 10 mil millones de candidatos; Supongamos también que un sistema de escritorio puede calcular 1 millón de hashes por segundo. El atacante puede probar que toda su lista dura menos de tres horas si solo se usa una iteración. Pero si solo se usan 2000 iteraciones, ese tiempo se extiende a casi 8 meses. Para vencer a un atacante más sofisticado, uno capaz de descargar un programa que puede aprovechar el poder de su GPU, por ejemplo, necesita más iteraciones.

¿Cuánto es suficiente?

El número de iteraciones a usar es un equilibrio entre la seguridad y la experiencia del usuario. El hardware especializado que pueden usar los atacantes es barato, pero aún puede realizar cientos de millones de iteraciones por segundo. El rendimiento del sistema del atacante determina cuánto tiempo lleva romper una contraseña dada una serie de iteraciones. Pero es probable que su aplicación no utilice este hardware especializado. La cantidad de iteraciones que puede realizar sin agravar a los usuarios depende de su sistema.

Probablemente pueda dejar que los usuarios esperen un ¾ segundo adicional durante la autenticación. Cree un perfil de su plataforma de destino y utilice todas las iteraciones que pueda pagar. Las plataformas que he probado (un usuario en un dispositivo móvil o muchos usuarios en una plataforma de servidor) pueden admitir cómodamente PBKDF2 con entre 60,000 y 120,000 iteraciones, o bcrypt con un factor de costo de 12 o 13.

Más fondo

Lea PKCS # 5 para obtener información autorizada sobre el papel de la sal y las iteraciones en el hash. Aunque PBKDF2 estaba destinado a generar claves de cifrado a partir de contraseñas, funciona bien como un hash de una sola vía para la autenticación de contraseñas. Cada iteración de bcrypt es más costosa que un hash SHA-2, por lo que puede usar menos iteraciones, pero la idea es la misma. Bcrypt también va un paso más allá de la mayoría de las soluciones basadas en PBKDF2 mediante el uso de la clave derivada para cifrar un texto plano conocido. El texto de cifrado resultante se almacena como el "hash", junto con algunos metadatos. Sin embargo, nada le impide hacer lo mismo con PBKDF2.

Aquí hay otras respuestas que he escrito sobre este tema:

¿El hashing de una contraseña dos veces antes del almacenamiento es más o menos seguro que solo una vez?

De lo que estoy hablando es de hacer esto:

$hashed_password = hash(hash($plaintext_password));

en lugar de sólo esto:

$hashed_password = hash($plaintext_password);

Si es menos seguro, ¿puede proporcionar una buena explicación (o un enlace a una)?

Además, ¿la función hash utilizada hace una diferencia? ¿Hay alguna diferencia si mezclas md5 y sha1 (por ejemplo) en lugar de repetir la misma función hash?

Nota 1: Cuando digo "hash doble", estoy hablando de hash una contraseña dos veces en un intento de hacerlo más oculto. No estoy hablando de la técnica para resolver colisiones .

Nota 2: Sé que necesito agregar una sal aleatoria para que sea realmente segura. La pregunta es si el hash dos veces con el mismo algoritmo ayuda o perjudica al hash.


Sí.

Absolutamente no utilice múltiples iteraciones de una función hash convencional, como md5(md5(md5(password))). En el mejor de los casos , obtendrás un aumento marginal en la seguridad (un esquema como este apenas ofrece protección contra un ataque de GPU; solo canaliza.) En el peor, estás reduciendo el espacio de hash (y por lo tanto la seguridad) con cada iteración que agregas . En seguridad, es prudente asumir lo peor.

No use una contraseña ha sido que la diseñada por un criptógrafo competente para ser un hash de contraseña eficaz y resistente tanto a la fuerza bruta y ataques de tiempo-espacio. Estos incluyen bcrypt, scrypt, y en algunas situaciones PBKDF2. El hash basado en glibc SHA-256 también es aceptable.


Para aquellos que dicen que es seguro, son correctos en general . El hash "doble" (o la expansión lógica de eso, iterar una función hash) es absolutamente seguro si se hace correctamente , para una preocupación específica.

Para aquellos que dicen que es inseguro, tienen razón en este caso . El código que se publica en la pregunta es inseguro. Hablemos de por qué:

$hashed_password1 = md5( md5( plaintext_password ) ); $hashed_password2 = md5( plaintext_password );

Hay dos propiedades fundamentales de una función hash que nos preocupan:

  1. Resistencia previa a la imagen : dado un hash $h , debería ser difícil encontrar un mensaje $m tal que $h === hash($m)

  2. Resistencia de la segunda imagen previa : dado un mensaje $m1 , debería ser difícil encontrar un mensaje diferente $m2 tal que hash($m1) === hash($m2)

  3. Resistencia a colisiones : debería ser difícil encontrar un par de mensajes ($m1, $m2) tales que hash($m1) === hash($m2) (tenga en cuenta que esto es similar a la resistencia de la Segunda Pre-Imagen, pero diferente en que aquí el atacante tiene control sobre ambos mensajes) ...

Para el almacenamiento de contraseñas , todo lo que realmente nos importa es la resistencia previa a la imagen . Los otros dos serían discutibles, porque $m1 es la contraseña del usuario que estamos tratando de mantener a salvo. Así que si el atacante ya lo tiene, el hash no tiene nada que proteger ...

RENUNCIA

Todo lo que sigue se basa en la premisa de que todo lo que nos importa es la resistencia a la Pre-imagen . Las otras dos propiedades fundamentales de las funciones hash pueden no (y típicamente no) mantenerse de la misma manera. Por lo tanto, las conclusiones de esta publicación solo son aplicables cuando se utilizan funciones hash para el almacenamiento de contraseñas. No son aplicables en general ...

Empecemos

Por el bien de esta discusión, inventemos nuestra propia función hash:

function ourHash($input) { $result = 0; for ($i = 0; $i < strlen($input); $i++) { $result += ord($input[$i]); } return (string) ($result % 256); }

Ahora debería ser bastante obvio lo que hace esta función hash. Suma los valores ASCII de cada carácter de entrada y luego toma el módulo de ese resultado con 256.

Así que vamos a probarlo:

var_dump( ourHash(''abc''), // string(2) "38" ourHash(''def''), // string(2) "47" ourHash(''hij''), // string(2) "59" ourHash(''klm'') // string(2) "68" );

Ahora, veamos qué sucede si lo ejecutamos varias veces alrededor de una función:

$tests = array( "abc", "def", "hij", "klm", ); foreach ($tests as $test) { $hash = $test; for ($i = 0; $i < 100; $i++) { $hash = ourHash($hash); } echo "Hashing $test => $hash/n"; }

Eso produce:

Hashing abc => 152 Hashing def => 152 Hashing hij => 155 Hashing klm => 155

Hrm guau Hemos generado colisiones !!! Vamos a tratar de ver por qué:

Aquí está la salida de hashing una cadena de cada salida hash posible:

Hashing 0 => 48 Hashing 1 => 49 Hashing 2 => 50 Hashing 3 => 51 Hashing 4 => 52 Hashing 5 => 53 Hashing 6 => 54 Hashing 7 => 55 Hashing 8 => 56 Hashing 9 => 57 Hashing 10 => 97 Hashing 11 => 98 Hashing 12 => 99 Hashing 13 => 100 Hashing 14 => 101 Hashing 15 => 102 Hashing 16 => 103 Hashing 17 => 104 Hashing 18 => 105 Hashing 19 => 106 Hashing 20 => 98 Hashing 21 => 99 Hashing 22 => 100 Hashing 23 => 101 Hashing 24 => 102 Hashing 25 => 103 Hashing 26 => 104 Hashing 27 => 105 Hashing 28 => 106 Hashing 29 => 107 Hashing 30 => 99 Hashing 31 => 100 Hashing 32 => 101 Hashing 33 => 102 Hashing 34 => 103 Hashing 35 => 104 Hashing 36 => 105 Hashing 37 => 106 Hashing 38 => 107 Hashing 39 => 108 Hashing 40 => 100 Hashing 41 => 101 Hashing 42 => 102 Hashing 43 => 103 Hashing 44 => 104 Hashing 45 => 105 Hashing 46 => 106 Hashing 47 => 107 Hashing 48 => 108 Hashing 49 => 109 Hashing 50 => 101 Hashing 51 => 102 Hashing 52 => 103 Hashing 53 => 104 Hashing 54 => 105 Hashing 55 => 106 Hashing 56 => 107 Hashing 57 => 108 Hashing 58 => 109 Hashing 59 => 110 Hashing 60 => 102 Hashing 61 => 103 Hashing 62 => 104 Hashing 63 => 105 Hashing 64 => 106 Hashing 65 => 107 Hashing 66 => 108 Hashing 67 => 109 Hashing 68 => 110 Hashing 69 => 111 Hashing 70 => 103 Hashing 71 => 104 Hashing 72 => 105 Hashing 73 => 106 Hashing 74 => 107 Hashing 75 => 108 Hashing 76 => 109 Hashing 77 => 110 Hashing 78 => 111 Hashing 79 => 112 Hashing 80 => 104 Hashing 81 => 105 Hashing 82 => 106 Hashing 83 => 107 Hashing 84 => 108 Hashing 85 => 109 Hashing 86 => 110 Hashing 87 => 111 Hashing 88 => 112 Hashing 89 => 113 Hashing 90 => 105 Hashing 91 => 106 Hashing 92 => 107 Hashing 93 => 108 Hashing 94 => 109 Hashing 95 => 110 Hashing 96 => 111 Hashing 97 => 112 Hashing 98 => 113 Hashing 99 => 114 Hashing 100 => 145 Hashing 101 => 146 Hashing 102 => 147 Hashing 103 => 148 Hashing 104 => 149 Hashing 105 => 150 Hashing 106 => 151 Hashing 107 => 152 Hashing 108 => 153 Hashing 109 => 154 Hashing 110 => 146 Hashing 111 => 147 Hashing 112 => 148 Hashing 113 => 149 Hashing 114 => 150 Hashing 115 => 151 Hashing 116 => 152 Hashing 117 => 153 Hashing 118 => 154 Hashing 119 => 155 Hashing 120 => 147 Hashing 121 => 148 Hashing 122 => 149 Hashing 123 => 150 Hashing 124 => 151 Hashing 125 => 152 Hashing 126 => 153 Hashing 127 => 154 Hashing 128 => 155 Hashing 129 => 156 Hashing 130 => 148 Hashing 131 => 149 Hashing 132 => 150 Hashing 133 => 151 Hashing 134 => 152 Hashing 135 => 153 Hashing 136 => 154 Hashing 137 => 155 Hashing 138 => 156 Hashing 139 => 157 Hashing 140 => 149 Hashing 141 => 150 Hashing 142 => 151 Hashing 143 => 152 Hashing 144 => 153 Hashing 145 => 154 Hashing 146 => 155 Hashing 147 => 156 Hashing 148 => 157 Hashing 149 => 158 Hashing 150 => 150 Hashing 151 => 151 Hashing 152 => 152 Hashing 153 => 153 Hashing 154 => 154 Hashing 155 => 155 Hashing 156 => 156 Hashing 157 => 157 Hashing 158 => 158 Hashing 159 => 159 Hashing 160 => 151 Hashing 161 => 152 Hashing 162 => 153 Hashing 163 => 154 Hashing 164 => 155 Hashing 165 => 156 Hashing 166 => 157 Hashing 167 => 158 Hashing 168 => 159 Hashing 169 => 160 Hashing 170 => 152 Hashing 171 => 153 Hashing 172 => 154 Hashing 173 => 155 Hashing 174 => 156 Hashing 175 => 157 Hashing 176 => 158 Hashing 177 => 159 Hashing 178 => 160 Hashing 179 => 161 Hashing 180 => 153 Hashing 181 => 154 Hashing 182 => 155 Hashing 183 => 156 Hashing 184 => 157 Hashing 185 => 158 Hashing 186 => 159 Hashing 187 => 160 Hashing 188 => 161 Hashing 189 => 162 Hashing 190 => 154 Hashing 191 => 155 Hashing 192 => 156 Hashing 193 => 157 Hashing 194 => 158 Hashing 195 => 159 Hashing 196 => 160 Hashing 197 => 161 Hashing 198 => 162 Hashing 199 => 163 Hashing 200 => 146 Hashing 201 => 147 Hashing 202 => 148 Hashing 203 => 149 Hashing 204 => 150 Hashing 205 => 151 Hashing 206 => 152 Hashing 207 => 153 Hashing 208 => 154 Hashing 209 => 155 Hashing 210 => 147 Hashing 211 => 148 Hashing 212 => 149 Hashing 213 => 150 Hashing 214 => 151 Hashing 215 => 152 Hashing 216 => 153 Hashing 217 => 154 Hashing 218 => 155 Hashing 219 => 156 Hashing 220 => 148 Hashing 221 => 149 Hashing 222 => 150 Hashing 223 => 151 Hashing 224 => 152 Hashing 225 => 153 Hashing 226 => 154 Hashing 227 => 155 Hashing 228 => 156 Hashing 229 => 157 Hashing 230 => 149 Hashing 231 => 150 Hashing 232 => 151 Hashing 233 => 152 Hashing 234 => 153 Hashing 235 => 154 Hashing 236 => 155 Hashing 237 => 156 Hashing 238 => 157 Hashing 239 => 158 Hashing 240 => 150 Hashing 241 => 151 Hashing 242 => 152 Hashing 243 => 153 Hashing 244 => 154 Hashing 245 => 155 Hashing 246 => 156 Hashing 247 => 157 Hashing 248 => 158 Hashing 249 => 159 Hashing 250 => 151 Hashing 251 => 152 Hashing 252 => 153 Hashing 253 => 154 Hashing 254 => 155 Hashing 255 => 156

Note la tendencia hacia números más altos. Eso resulta ser nuestra caída muerta. Ejecutar el hash 4 veces ($ hash = ourHash ($ hash) `, para cada elemento) termina dándonos:

Hashing 0 => 153 Hashing 1 => 154 Hashing 2 => 155 Hashing 3 => 156 Hashing 4 => 157 Hashing 5 => 158 Hashing 6 => 150 Hashing 7 => 151 Hashing 8 => 152 Hashing 9 => 153 Hashing 10 => 157 Hashing 11 => 158 Hashing 12 => 150 Hashing 13 => 154 Hashing 14 => 155 Hashing 15 => 156 Hashing 16 => 157 Hashing 17 => 158 Hashing 18 => 150 Hashing 19 => 151 Hashing 20 => 158 Hashing 21 => 150 Hashing 22 => 154 Hashing 23 => 155 Hashing 24 => 156 Hashing 25 => 157 Hashing 26 => 158 Hashing 27 => 150 Hashing 28 => 151 Hashing 29 => 152 Hashing 30 => 150 Hashing 31 => 154 Hashing 32 => 155 Hashing 33 => 156 Hashing 34 => 157 Hashing 35 => 158 Hashing 36 => 150 Hashing 37 => 151 Hashing 38 => 152 Hashing 39 => 153 Hashing 40 => 154 Hashing 41 => 155 Hashing 42 => 156 Hashing 43 => 157 Hashing 44 => 158 Hashing 45 => 150 Hashing 46 => 151 Hashing 47 => 152 Hashing 48 => 153 Hashing 49 => 154 Hashing 50 => 155 Hashing 51 => 156 Hashing 52 => 157 Hashing 53 => 158 Hashing 54 => 150 Hashing 55 => 151 Hashing 56 => 152 Hashing 57 => 153 Hashing 58 => 154 Hashing 59 => 155 Hashing 60 => 156 Hashing 61 => 157 Hashing 62 => 158 Hashing 63 => 150 Hashing 64 => 151 Hashing 65 => 152 Hashing 66 => 153 Hashing 67 => 154 Hashing 68 => 155 Hashing 69 => 156 Hashing 70 => 157 Hashing 71 => 158 Hashing 72 => 150 Hashing 73 => 151 Hashing 74 => 152 Hashing 75 => 153 Hashing 76 => 154 Hashing 77 => 155 Hashing 78 => 156 Hashing 79 => 157 Hashing 80 => 158 Hashing 81 => 150 Hashing 82 => 151 Hashing 83 => 152 Hashing 84 => 153 Hashing 85 => 154 Hashing 86 => 155 Hashing 87 => 156 Hashing 88 => 157 Hashing 89 => 158 Hashing 90 => 150 Hashing 91 => 151 Hashing 92 => 152 Hashing 93 => 153 Hashing 94 => 154 Hashing 95 => 155 Hashing 96 => 156 Hashing 97 => 157 Hashing 98 => 158 Hashing 99 => 150 Hashing 100 => 154 Hashing 101 => 155 Hashing 102 => 156 Hashing 103 => 157 Hashing 104 => 158 Hashing 105 => 150 Hashing 106 => 151 Hashing 107 => 152 Hashing 108 => 153 Hashing 109 => 154 Hashing 110 => 155 Hashing 111 => 156 Hashing 112 => 157 Hashing 113 => 158 Hashing 114 => 150 Hashing 115 => 151 Hashing 116 => 152 Hashing 117 => 153 Hashing 118 => 154 Hashing 119 => 155 Hashing 120 => 156 Hashing 121 => 157 Hashing 122 => 158 Hashing 123 => 150 Hashing 124 => 151 Hashing 125 => 152 Hashing 126 => 153 Hashing 127 => 154 Hashing 128 => 155 Hashing 129 => 156 Hashing 130 => 157 Hashing 131 => 158 Hashing 132 => 150 Hashing 133 => 151 Hashing 134 => 152 Hashing 135 => 153 Hashing 136 => 154 Hashing 137 => 155 Hashing 138 => 156 Hashing 139 => 157 Hashing 140 => 158 Hashing 141 => 150 Hashing 142 => 151 Hashing 143 => 152 Hashing 144 => 153 Hashing 145 => 154 Hashing 146 => 155 Hashing 147 => 156 Hashing 148 => 157 Hashing 149 => 158 Hashing 150 => 150 Hashing 151 => 151 Hashing 152 => 152 Hashing 153 => 153 Hashing 154 => 154 Hashing 155 => 155 Hashing 156 => 156 Hashing 157 => 157 Hashing 158 => 158 Hashing 159 => 159 Hashing 160 => 151 Hashing 161 => 152 Hashing 162 => 153 Hashing 163 => 154 Hashing 164 => 155 Hashing 165 => 156 Hashing 166 => 157 Hashing 167 => 158 Hashing 168 => 159 Hashing 169 => 151 Hashing 170 => 152 Hashing 171 => 153 Hashing 172 => 154 Hashing 173 => 155 Hashing 174 => 156 Hashing 175 => 157 Hashing 176 => 158 Hashing 177 => 159 Hashing 178 => 151 Hashing 179 => 152 Hashing 180 => 153 Hashing 181 => 154 Hashing 182 => 155 Hashing 183 => 156 Hashing 184 => 157 Hashing 185 => 158 Hashing 186 => 159 Hashing 187 => 151 Hashing 188 => 152 Hashing 189 => 153 Hashing 190 => 154 Hashing 191 => 155 Hashing 192 => 156 Hashing 193 => 157 Hashing 194 => 158 Hashing 195 => 159 Hashing 196 => 151 Hashing 197 => 152 Hashing 198 => 153 Hashing 199 => 154 Hashing 200 => 155 Hashing 201 => 156 Hashing 202 => 157 Hashing 203 => 158 Hashing 204 => 150 Hashing 205 => 151 Hashing 206 => 152 Hashing 207 => 153 Hashing 208 => 154 Hashing 209 => 155 Hashing 210 => 156 Hashing 211 => 157 Hashing 212 => 158 Hashing 213 => 150 Hashing 214 => 151 Hashing 215 => 152 Hashing 216 => 153 Hashing 217 => 154 Hashing 218 => 155 Hashing 219 => 156 Hashing 220 => 157 Hashing 221 => 158 Hashing 222 => 150 Hashing 223 => 151 Hashing 224 => 152 Hashing 225 => 153 Hashing 226 => 154 Hashing 227 => 155 Hashing 228 => 156 Hashing 229 => 157 Hashing 230 => 158 Hashing 231 => 150 Hashing 232 => 151 Hashing 233 => 152 Hashing 234 => 153 Hashing 235 => 154 Hashing 236 => 155 Hashing 237 => 156 Hashing 238 => 157 Hashing 239 => 158 Hashing 240 => 150 Hashing 241 => 151 Hashing 242 => 152 Hashing 243 => 153 Hashing 244 => 154 Hashing 245 => 155 Hashing 246 => 156 Hashing 247 => 157 Hashing 248 => 158 Hashing 249 => 159 Hashing 250 => 151 Hashing 251 => 152 Hashing 252 => 153 Hashing 253 => 154 Hashing 254 => 155 Hashing 255 => 156

Nos hemos reducido a 8 valores ... Eso es malo ... Nuestra función original asignó S(∞) a S(256) . Es decir, hemos creado una función Surjective que mapea $input a $output .

Dado que tenemos una función Surjective, no tenemos garantía de que la asignación para cualquier subconjunto de la entrada no tendrá colisiones (de hecho, en la práctica lo harán).

¡Eso es lo que pasó aquí! Nuestra función era mala, pero no es por eso que funcionó (por eso funcionó tan rápida y completamente).

Lo mismo sucede con MD5 . Mapea S(∞) sobre S(2^128) . Ya que no hay garantía de que la ejecución de MD5(S(output)) sea Injective , lo que significa que no tendrá colisiones.

Sección TL / DR

Por lo tanto, dado que la alimentación de vuelta a md5 directamente puede generar colisiones, cada iteración aumentará la posibilidad de colisiones. Sin embargo, este es un aumento lineal, lo que significa que mientras el conjunto de resultados de 2^128 se reduce, no se reduce significativamente lo suficientemente rápido como para ser un defecto crítico.

Asi que,

$output = md5($input); // 2^128 possibilities $output = md5($output); // < 2^128 possibilities $output = md5($output); // < 2^128 possibilities $output = md5($output); // < 2^128 possibilities $output = md5($output); // < 2^128 possibilities

Cuantas más veces itere, más se reducirá la reducción.

La solución

Afortunadamente para nosotros, hay una manera trivial de arreglar esto: retroalimentar algo en las siguientes iteraciones:

$output = md5($input); // 2^128 possibilities $output = md5($input . $output); // 2^128 possibilities $output = md5($input . $output); // 2^128 possibilities $output = md5($input . $output); // 2^128 possibilities $output = md5($input . $output); // 2^128 possibilities

Tenga en cuenta que las iteraciones adicionales no son 2 ^ 128 para cada valor individual para $input . Lo que significa que podemos generar $input valores de $input que aún colisionan en la línea (y por lo tanto, se establecerán o resonarán en mucho menos que 2^128 salidas posibles). Pero el caso general para $input sigue siendo tan fuerte como lo fue para una sola ronda.

Espera, ¿fue? Probemos esto con nuestra función ourHash() . Cambiar a $hash = ourHash($input . $hash); , para 100 iteraciones:

Hashing 0 => 201 Hashing 1 => 212 Hashing 2 => 199 Hashing 3 => 201 Hashing 4 => 203 Hashing 5 => 205 Hashing 6 => 207 Hashing 7 => 209 Hashing 8 => 211 Hashing 9 => 204 Hashing 10 => 251 Hashing 11 => 147 Hashing 12 => 251 Hashing 13 => 148 Hashing 14 => 253 Hashing 15 => 0 Hashing 16 => 1 Hashing 17 => 2 Hashing 18 => 161 Hashing 19 => 163 Hashing 20 => 147 Hashing 21 => 251 Hashing 22 => 148 Hashing 23 => 253 Hashing 24 => 0 Hashing 25 => 1 Hashing 26 => 2 Hashing 27 => 161 Hashing 28 => 163 Hashing 29 => 8 Hashing 30 => 251 Hashing 31 => 148 Hashing 32 => 253 Hashing 33 => 0 Hashing 34 => 1 Hashing 35 => 2 Hashing 36 => 161 Hashing 37 => 163 Hashing 38 => 8 Hashing 39 => 4 Hashing 40 => 148 Hashing 41 => 253 Hashing 42 => 0 Hashing 43 => 1 Hashing 44 => 2 Hashing 45 => 161 Hashing 46 => 163 Hashing 47 => 8 Hashing 48 => 4 Hashing 49 => 9 Hashing 50 => 253 Hashing 51 => 0 Hashing 52 => 1 Hashing 53 => 2 Hashing 54 => 161 Hashing 55 => 163 Hashing 56 => 8 Hashing 57 => 4 Hashing 58 => 9 Hashing 59 => 11 Hashing 60 => 0 Hashing 61 => 1 Hashing 62 => 2 Hashing 63 => 161 Hashing 64 => 163 Hashing 65 => 8 Hashing 66 => 4 Hashing 67 => 9 Hashing 68 => 11 Hashing 69 => 4 Hashing 70 => 1 Hashing 71 => 2 Hashing 72 => 161 Hashing 73 => 163 Hashing 74 => 8 Hashing 75 => 4 Hashing 76 => 9 Hashing 77 => 11 Hashing 78 => 4 Hashing 79 => 3 Hashing 80 => 2 Hashing 81 => 161 Hashing 82 => 163 Hashing 83 => 8 Hashing 84 => 4 Hashing 85 => 9 Hashing 86 => 11 Hashing 87 => 4 Hashing 88 => 3 Hashing 89 => 17 Hashing 90 => 161 Hashing 91 => 163 Hashing 92 => 8 Hashing 93 => 4 Hashing 94 => 9 Hashing 95 => 11 Hashing 96 => 4 Hashing 97 => 3 Hashing 98 => 17 Hashing 99 => 13 Hashing 100 => 246 Hashing 101 => 248 Hashing 102 => 49 Hashing 103 => 44 Hashing 104 => 255 Hashing 105 => 198 Hashing 106 => 43 Hashing 107 => 51 Hashing 108 => 202 Hashing 109 => 2 Hashing 110 => 248 Hashing 111 => 49 Hashing 112 => 44 Hashing 113 => 255 Hashing 114 => 198 Hashing 115 => 43 Hashing 116 => 51 Hashing 117 => 202 Hashing 118 => 2 Hashing 119 => 51 Hashing 120 => 49 Hashing 121 => 44 Hashing 122 => 255 Hashing 123 => 198 Hashing 124 => 43 Hashing 125 => 51 Hashing 126 => 202 Hashing 127 => 2 Hashing 128 => 51 Hashing 129 => 53 Hashing 130 => 44 Hashing 131 => 255 Hashing 132 => 198 Hashing 133 => 43 Hashing 134 => 51 Hashing 135 => 202 Hashing 136 => 2 Hashing 137 => 51 Hashing 138 => 53 Hashing 139 => 55 Hashing 140 => 255 Hashing 141 => 198 Hashing 142 => 43 Hashing 143 => 51 Hashing 144 => 202 Hashing 145 => 2 Hashing 146 => 51 Hashing 147 => 53 Hashing 148 => 55 Hashing 149 => 58 Hashing 150 => 198 Hashing 151 => 43 Hashing 152 => 51 Hashing 153 => 202 Hashing 154 => 2 Hashing 155 => 51 Hashing 156 => 53 Hashing 157 => 55 Hashing 158 => 58 Hashing 159 => 0 Hashing 160 => 43 Hashing 161 => 51 Hashing 162 => 202 Hashing 163 => 2 Hashing 164 => 51 Hashing 165 => 53 Hashing 166 => 55 Hashing 167 => 58 Hashing 168 => 0 Hashing 169 => 209 Hashing 170 => 51 Hashing 171 => 202 Hashing 172 => 2 Hashing 173 => 51 Hashing 174 => 53 Hashing 175 => 55 Hashing 176 => 58 Hashing 177 => 0 Hashing 178 => 209 Hashing 179 => 216 Hashing 180 => 202 Hashing 181 => 2 Hashing 182 => 51 Hashing 183 => 53 Hashing 184 => 55 Hashing 185 => 58 Hashing 186 => 0 Hashing 187 => 209 Hashing 188 => 216 Hashing 189 => 219 Hashing 190 => 2 Hashing 191 => 51 Hashing 192 => 53 Hashing 193 => 55 Hashing 194 => 58 Hashing 195 => 0 Hashing 196 => 209 Hashing 197 => 216 Hashing 198 => 219 Hashing 199 => 220 Hashing 200 => 248 Hashing 201 => 49 Hashing 202 => 44 Hashing 203 => 255 Hashing 204 => 198 Hashing 205 => 43 Hashing 206 => 51 Hashing 207 => 202 Hashing 208 => 2 Hashing 209 => 51 Hashing 210 => 49 Hashing 211 => 44 Hashing 212 => 255 Hashing 213 => 198 Hashing 214 => 43 Hashing 215 => 51 Hashing 216 => 202 Hashing 217 => 2 Hashing 218 => 51 Hashing 219 => 53 Hashing 220 => 44 Hashing 221 => 255 Hashing 222 => 198 Hashing 223 => 43 Hashing 224 => 51 Hashing 225 => 202 Hashing 226 => 2 Hashing 227 => 51 Hashing 228 => 53 Hashing 229 => 55 Hashing 230 => 255 Hashing 231 => 198 Hashing 232 => 43 Hashing 233 => 51 Hashing 234 => 202 Hashing 235 => 2 Hashing 236 => 51 Hashing 237 => 53 Hashing 238 => 55 Hashing 239 => 58 Hashing 240 => 198 Hashing 241 => 43 Hashing 242 => 51 Hashing 243 => 202 Hashing 244 => 2 Hashing 245 => 51 Hashing 246 => 53 Hashing 247 => 55 Hashing 248 => 58 Hashing 249 => 0 Hashing 250 => 43 Hashing 251 => 51 Hashing 252 => 202 Hashing 253 => 2 Hashing 254 => 51 Hashing 255 => 53

Todavía hay un patrón aproximado, pero tenga en cuenta que no es más un patrón que nuestra función subyacente (que ya era bastante débil).

Sin embargo, tenga en cuenta que 0 y 3 convirtieron en colisiones, a pesar de que no estaban en la única carrera. Esa es una aplicación de lo que dije antes (que la resistencia a la colisión permanece igual para el conjunto de todas las entradas, pero las rutas de colisión específicas pueden abrirse debido a fallas en el algoritmo subyacente).

Sección TL / DR

Al realimentar la entrada en cada iteración, efectivamente rompemos cualquier colisión que pueda haber ocurrido en la iteración anterior.

Por lo tanto, md5($input . md5($input)); debería ser (al menos teóricamente ) tan fuerte como md5($input) .

¿Esto es importante?

Sí. Esta es una de las razones por las que PBKDF2 reemplazó a PBKDF1 en RFC 2898 . Considere los bucles internos de los dos:

PBKDF1:

T_1 = Hash (P || S) , T_2 = Hash (T_1) , ... T_c = Hash (T_{c-1})

Donde c es el recuento de iteraciones, P es la contraseña y S es la sal

PBKDF2:

U_1 = PRF (P, S || INT (i)) , U_2 = PRF (P, U_1) , ... U_c = PRF (P, U_{c-1})

Donde PRF es realmente solo un HMAC. Pero para nuestros propósitos aquí, solo digamos que PRF(P, S) = Hash(P || S) (es decir, el PRF de 2 entradas es el mismo, más o menos, como hash con los dos concatenados juntos). Es mucho no , pero para nuestros propósitos lo es.

Por lo tanto, PBKDF2 mantiene la resistencia a la colisión de la función Hash subyacente, donde PBKDF1 no lo hace.

Atándolo todo juntos:

Sabemos de formas seguras de iterar un hash. De hecho:

$hash = $input; $i = 10000; do { $hash = hash($input . $hash); } while ($i-- > 0);

Es típicamente seguro

Ahora, para adentrarnos en por qué querríamos hacerlo, analicemos el movimiento de la entropía.

Un hash toma el conjunto infinito: S(∞) y produce un conjunto S(n) más pequeño y de tamaño consistente. La siguiente iteración (suponiendo que la entrada se pasa de nuevo) asigna S(∞) a S(n) nuevo:

S(∞) -> S(n) S(∞) -> S(n) S(∞) -> S(n) S(∞) -> S(n) S(∞) -> S(n) S(∞) -> S(n)

Observe que la salida final tiene exactamente la misma cantidad de entropía que la primera . La iteración no "lo hará más oculto". La entropía es idéntica. No hay una fuente mágica de imprevisibilidad (es una función pseudoaleatoria, no una función aleatoria).

Sin embargo, hay una ganancia para iterar. Hace que el proceso de hash sea artificialmente más lento. Y es por eso que iterar puede ser una buena idea. De hecho, es el principio básico de la mayoría de los algoritmos modernos de hash de contraseñas (el hecho de que hacer algo una y otra vez lo hace más lento).

Lento es bueno, porque está combatiendo la principal amenaza para la seguridad: la fuerza bruta. Cuanto más lentos sea nuestro algoritmo de hash, más duro tienen que trabajar los atacantes para atacar los hashes de contraseña que nos han robado. Y eso es algo bueno !!!


Sí, volver a aplicar hash reduce el espacio de búsqueda, pero no, no importa, la reducción efectiva es insignificante.

Re-hashing aumenta el tiempo que toma la fuerza bruta, pero hacerlo solo dos veces es subóptimo.

Lo que realmente desea es codificar la contraseña con PBKDF2 , un método comprobado de usar un hash seguro con salt e iteraciones. Echa un vistazo a esta respuesta SO .

EDITAR : Casi lo olvido - ¡¡¡NO USE MD5 !!!! Utilice un hash criptográfico moderno como la familia SHA-2 (SHA-256, SHA-384 y SHA-512).


El hash doble solo tiene sentido si tengo la contraseña en el cliente y luego guardo el hash (con sal diferente) de ese hash en el servidor.

De esa manera, incluso si alguien hackeó su camino en el servidor (ignorando así la seguridad que proporciona SSL), aún no puede obtener las contraseñas claras.

Sí, tendrá los datos necesarios para ingresar al sistema, pero no podrá utilizar esos datos para comprometer las cuentas externas que tiene el usuario. Y se sabe que las personas usan la misma contraseña para prácticamente cualquier cosa.

La única forma en que podría obtener las contraseñas claras es instalar un keygen en el cliente, y ese ya no es su problema.

Así que en resumen:

  1. El primer hash en el cliente protege a sus usuarios en un escenario de "violación del servidor".
  2. El segundo hash en el servidor sirve para proteger su sistema si alguien consiguió una copia de seguridad de su base de datos, por lo que no puede usar esas contraseñas para conectarse a sus servicios.

Por lo que he leído, se puede recomendar volver a cifrar la contraseña cientos o miles de veces.

La idea es que si puede hacer que le lleve más tiempo codificar la contraseña, es más trabajo para un atacante realizar varias conjeturas para descifrar la contraseña. Esa parece ser la ventaja de volver a aplicar hash, no es que sea más criptográficamente segura, sino que simplemente lleva más tiempo generar un ataque de diccionario.

Por supuesto, las computadoras se vuelven más rápidas todo el tiempo, por lo que esta ventaja disminuye con el tiempo (o requiere que aumentes las iteraciones).


Sí, reduce el número de cadenas posibles que coinciden con la cadena.

Como ya has mencionado, los hashes salados son mucho mejores.

Un artículo aquí: http://websecurity.ro/blog/2007/11/02/md5md5-vs-md5/ , intenta una prueba de por qué es equivalente, pero no estoy seguro con la lógica. En parte, suponen que no hay software disponible para analizar md5 (md5 (texto)), pero obviamente es bastante trivial producir las tablas del arco iris.

Sigo con mi respuesta de que hay un número menor de hashes de tipo md5 (md5 (texto)) que de hash md5 (texto), lo que aumenta la posibilidad de colisión (incluso si es probable) y reduce el espacio de búsqueda.


Voy a arriesgarme y diré que es más seguro en ciertas circunstancias ... ¡no me rechaces todavía!

Desde un punto de vista matemático / criptográfico, es menos seguro, por razones que estoy seguro de que alguien más le dará una explicación más clara de lo que yo podría.

Sin embargo , existen grandes bases de datos de hashes MD5, que tienen más probabilidades de contener el texto de "contraseña" que el MD5 de la misma. Entonces, al hacer doble hash, está reduciendo la efectividad de esas bases de datos.

Por supuesto, si usa sal, esta ventaja (¿desventaja?) Desaparece.


Acabo de ver esto desde un punto de vista práctico. ¿Qué es el hacker después? Por qué, la combinación de caracteres que, cuando se pone a través de la función hash, genera el hash deseado.

Solo está guardando el último hash, por lo tanto, el pirata informático solo tiene que aplicar fuerza bruta a uno. Suponiendo que tiene aproximadamente las mismas probabilidades de encontrar el hash deseado con cada paso de fuerza bruta, el número de hashes es irrelevante. Podría hacer un millón de iteraciones de hash, y esto no aumentaría ni reduciría la seguridad un poco, ya que al final de la línea todavía hay un solo hash que romper, y las probabilidades de romperlo son las mismas que cualquier otro hash.

Tal vez los carteles anteriores piensen que la entrada es relevante; no es. Siempre que lo que pongas en la función de hash genere el hash deseado, te ayudará a ingresar la entrada correcta o incorrecta.

Ahora, las tablas del arco iris son otra historia. Dado que una tabla de arco iris solo transporta contraseñas sin procesar, el hash dos veces puede ser una buena medida de seguridad, ya que una tabla de arco iris que contiene cada hash de cada hash sería demasiado grande.

Por supuesto, solo estoy considerando el ejemplo que dio el OP, en el que solo se trata de una contraseña de texto sin formato que se oculta. Si incluye el nombre de usuario o una sal en el hash, es una historia diferente; hacer hash dos veces es completamente innecesario, ya que la tabla del arco iris ya sería demasiado grande para ser práctica y contener el hash correcto.

De todos modos, no soy un experto en seguridad aquí, pero eso es lo que he descubierto a partir de mi experiencia.


Como lo sugieren varias respuestas en este artículo, hay algunos casos en los que puede mejorar la seguridad y otros donde definitivamente la perjudica. Hay una mejor solución que definitivamente mejorará la seguridad. En lugar de duplicar la cantidad de veces que calcula el hash, doble el tamaño de su sal, o doble la cantidad de bits utilizados en el hash, o haga ambas cosas. En lugar de SHA-245, salta a SHA-512.


El doble hash es feo porque es muy probable que un atacante haya construido una tabla para crear la mayoría de los hashes. Mejor es poner sal a los hashes y mezclarlos. También hay nuevos esquemas para "firmar" hashes (básicamente salazón), pero de una manera más segura.


En general, no proporciona seguridad adicional para hacer doble hash o cifrar doble algo. Si puedes romper el hash una vez, puedes romperlo nuevamente. Sin embargo, generalmente no hace daño a la seguridad hacer esto.

En su ejemplo de uso de MD5, como probablemente sepa, hay algunos problemas de colisión. "Double Hashing" no ayuda realmente a protegerse contra esto, ya que las mismas colisiones darán como resultado el mismo primer hash, que luego puede MD5 nuevamente para obtener el segundo hash.

Esto protege contra ataques de diccionario, como los "bases de datos MD5 inversas", pero también lo hace la salazón.

En una tangente, el cifrado doble de algo no proporciona ninguna seguridad adicional, ya que todo lo que hace es dar como resultado una clave diferente que es una combinación de las dos claves que realmente se utilizan. Por lo tanto, el esfuerzo por encontrar la "clave" no se duplica porque en realidad no es necesario encontrar dos claves. Esto no es cierto para el hash, porque el resultado del hash no suele ser la misma longitud que la entrada original.


La mayoría de las respuestas son de personas sin antecedentes en criptografía o seguridad. Y están equivocados. Utilice una sal, si es posible única por registro. MD5 / SHA / etc son demasiado rápidos, lo contrario de lo que quieres. PBKDF2 y bcrypt son más lentos (lo que es bueno) pero pueden ser derrotados con ASIC / FPGA / GPU (muy asequible en la actualidad). Así que se necesita un algoritmo de memoria dura: ingrese scrypt .

Aquí hay una explicación laica sobre las sales y la velocidad (pero no sobre los algoritmos de memoria dura).


La preocupación por reducir el espacio de búsqueda es matemáticamente correcta, aunque el espacio de búsqueda sigue siendo lo suficientemente grande como para todos los propósitos prácticos (suponiendo que use sales), en 2 ^ 128. Sin embargo, dado que estamos hablando de contraseñas, el número de posibles cadenas de 16 caracteres (alfanumérico, mayúsculas, algunos símbolos incorporados) es de aproximadamente 2 ^ 98, de acuerdo con mis cálculos en el reverso del sobre. Por lo tanto, la disminución percibida en el espacio de búsqueda no es realmente relevante.

Aparte de eso, realmente no hay diferencia, criptográficamente hablando.

Aunque existe una primitiva criptográfica llamada "cadena hash", una técnica que le permite hacer algunos trucos geniales, como revelar una clave de firma después de que se haya utilizado, sin sacrificar la integridad del sistema, dado que la sincronización del tiempo es mínima, esto le permite eludir limpiamente el problema de la distribución de la clave inicial. Básicamente, usted calcula un conjunto grande de hashes de hashes - h (h (h (h .... (h (k)) ...)), use el valor nth para firmar, después de un intervalo establecido, envíe salga de la llave y firme con la tecla (n-1). Los destinatarios ahora pueden verificar que enviaste todos los mensajes anteriores, y nadie puede falsificar tu firma ya que ha pasado el período de tiempo por el cual es válida.

Re-hashing cientos de miles de veces, como sugiere Bill, es solo un desperdicio de su cpu ... use una tecla más larga si le preocupa que las personas rompan 128 bits.


Personalmente, no me molestaría con múltiples hashses, pero me aseguraría de que también incluyera el nombre de usuario (u otro campo de ID de usuario) y la contraseña para que dos usuarios con la misma contraseña no terminen con el mismo hash. También probablemente lanzaría alguna otra cadena constante en la cadena de entrada también para una buena medida.

$hashed_password = md5( "xxx" + "|" + user_name + "|" + plaintext_password);


Supongamos que utiliza el algoritmo de hashing: compute rot13, tome los primeros 10 caracteres. Si lo hace dos veces (o incluso 2000 veces), es posible hacer una función que sea más rápida, pero que dé el mismo resultado (es decir, solo tome los primeros 10 caracteres).

Asimismo, puede ser posible realizar una función más rápida que ofrezca la misma salida que una función de hashing repetida. Por lo tanto, su elección de la función de hashing es muy importante: como en el ejemplo de rot13, no se tiene en cuenta que el hashing repetido mejorará la seguridad. Si no hay investigaciones que indiquen que el algoritmo está diseñado para un uso recursivo, entonces es más seguro asumir que no le brindará mayor protección.

Dicho esto: para todas las funciones de hash más sencillas, lo más probable es que los expertos en criptografía calculen las funciones más rápidas, por lo que si está protegiéndose contra los atacantes que no tienen acceso a los expertos en criptografía, probablemente sea más seguro en la práctica utilizar una función de hashing repetida. .