El problema aquí es básicamente un problema de entropía. Así que comencemos a buscar allí:
Entropía por carácter
El número de bits de entropía por byte es:
- Personajes Hex
- Bits: 4
- Valores: 16
- Entropía en 72 caracteres: 288 bits
- Alfanumérico
- Bits: 6
- Valores: 62
- Entropía en 72 caracteres: 432 bits
- Símbolos "comunes"
- Bits: 6.5
- Valores: 94
- Entropía en 72 caracteres: 468 bits
- Bytes completos
- Bits: 8
- Valores: 255
- Entropía en 72 caracteres: 576 bits
Entonces, cómo actuamos depende del tipo de personajes que esperamos.
El primer problema
El primer problema con su código es que su paso de hash "pimienta" está generando caracteres hexadecimales (ya que el cuarto parámetro hash_hmac()
no está configurado).
Por lo tanto, al aplicar hash a su pimienta, está efectivamente reduciendo la entropía máxima disponible para la contraseña en un factor de 2 (de 576 a 288 bits posibles ).
El segundo problema
Sin embargo, sha256
solo proporciona 256
bits de entropía en primer lugar. De modo que está reduciendo efectivamente 576 bits a 256 bits. Su paso hash * inmediatamente *, por definición, pierde
al menos el 50% de la posible entropía en la contraseña.
Podría resolver esto parcialmente cambiando a SHA512
, donde solo reduciría la entropía disponible en aproximadamente un 12%. Pero esa sigue siendo una diferencia significativa. Ese 12% reduce el número de permutaciones en un factor de 1.8e19
. Ese es un gran número ... Y ese es el factor que lo reduce por ...
El problema subyacente
El problema subyacente es que hay tres tipos de contraseñas de más de 72 caracteres. El impacto que este sistema de estilo tiene en ellos será muy diferente:
Nota: de aquí en adelante, supongo que estamos comparando con un sistema de pimienta que se usa SHA512
con salida sin procesar (no hexadecimal).
Contraseñas aleatorias de alta entropía
Estos son sus usuarios que utilizan generadores de contraseñas que generan claves grandes para contraseñas. Son aleatorios (generados, no elegidos por humanos) y tienen una alta entropía por carácter. Estos tipos utilizan bytes altos (caracteres> 127) y algunos caracteres de control.
Para este grupo, su función hash reducirá significativamente su entropía disponible en bcrypt
.
Déjame decirlo de nuevo. Para los usuarios que utilizan contraseñas largas de alta entropía, su solución reduce significativamente la seguridad de su contraseña en una cantidad mensurable. (62 bits de entropía perdidos para una contraseña de 72 caracteres y más para contraseñas más largas)
Contraseñas aleatorias de entropía media
Este grupo utiliza contraseñas que contienen símbolos comunes, pero no bytes altos ni caracteres de control. Estas son sus contraseñas que se pueden escribir.
Para este grupo, va a desbloquear un poco más entropía (no crearla, pero permitir que más entropía quepa en la contraseña de bcrypt). Cuando digo un poco, me refiero a un poco. El punto de equilibrio se produce cuando maximiza los 512 bits que tiene SHA512. Por lo tanto, el pico es de 78 caracteres.
Déjame decirlo de nuevo. Para esta clase de contraseñas, solo puede almacenar 6 caracteres adicionales antes de quedarse sin entropía.
Contraseñas no aleatorias de baja entropía
Este es el grupo que utiliza caracteres alfanuméricos que probablemente no se generen al azar. Algo como una cita bíblica o algo así. Estas frases tienen aproximadamente 2,3 bits de entropía por carácter.
Para este grupo, puede desbloquear significativamente más entropía (no crearla, pero permitir que más se ajuste a la entrada de contraseña de bcrypt) mediante hash. El punto de equilibrio es de alrededor de 223 caracteres antes de que te quedes sin entropía.
Digámoslo de nuevo. Para esta clase de contraseñas, el pre-hash definitivamente aumenta la seguridad de manera significativa.
De vuelta al mundo real
Este tipo de cálculos de entropía realmente no importan mucho en el mundo real. Lo que importa es adivinar la entropía. Eso es lo que afecta directamente lo que pueden hacer los atacantes. Eso es lo que quieres maximizar.
Si bien se ha realizado poca investigación para adivinar la entropía, hay algunos puntos que me gustaría señalar.
Las posibilidades de adivinar al azar 72 caracteres correctos seguidos son extremadamente bajas. Es más probable que ganes la lotería Powerball 21 veces, que tener esta colisión ... Así de grande es el número del que estamos hablando.
Pero es posible que no lo encontremos estadísticamente. En el caso de las frases, la probabilidad de que los primeros 72 caracteres sean iguales es mucho mayor que para una contraseña aleatoria. Pero sigue siendo trivialmente bajo (es más probable que ganes la lotería Powerball 5 veces, según 2.3 bits por carácter).
Prácticamente
Prácticamente, realmente no importa. Las posibilidades de que alguien adivine correctamente los primeros 72 caracteres, donde los últimos marcan una diferencia significativa, son tan bajas que no vale la pena preocuparse. ¿Por qué?
Bueno, digamos que estás tomando una frase. Si la persona puede acertar con los primeros 72 caracteres, es muy afortunado (no es probable) o es una frase común. Si es una frase común, la única variable es cuánto tiempo debe hacerse.
Pongamos un ejemplo. Tomemos una cita de la Biblia (solo porque es una fuente común de texto extenso, no por ninguna otra razón):
No codiciarás la casa de tu prójimo. No codiciarás a la mujer de tu prójimo, ni a su siervo, ni a su sierva, ni a su buey, ni a su asno, ni nada que sea de tu prójimo.
Eso es 180 caracteres. El carácter 73 es el g
del segundo neighbor's
. Si adivinó tanto, es probable que no se detenga en nei
, sino que continúe con el resto del versículo (ya que así es como es probable que se use la contraseña). Por lo tanto, su "hash" no agregó mucho.
Por cierto: Absolutamente NO estoy abogando por el uso de una cita bíblica. De hecho, todo lo contrario.
Conclusión
Realmente no vas a ayudar mucho a las personas que usan contraseñas largas usando hash primero. Algunos grupos definitivamente pueden ayudar. Algunos definitivamente puedes lastimarlos.
Pero al final, nada de eso es demasiado significativo. Los números con los que estamos lidiando son MUY demasiado altos. La diferencia de entropía no será mucha.
Es mejor dejar bcrypt como está. Es más probable que arruines el hash (literalmente, ya lo has hecho y no eres el primero ni el último en cometer ese error) que el ataque que estás tratando de prevenir.
Concéntrese en asegurar el resto del sitio. Y agregue un medidor de entropía de contraseña al cuadro de contraseña al registrarse para indicar la fuerza de la contraseña (e indicar si una contraseña es demasiado larga y el usuario puede desear cambiarla) ...
Ese es mi $ 0.02 al menos (o posiblemente mucho más de $ 0.02) ...
En cuanto al uso de un pimiento "secreto":
Literalmente, no hay ninguna investigación sobre la introducción de una función hash en bcrypt. Por lo tanto, no está claro en el mejor de los casos si introducir un hash "salpicado" en bcrypt alguna vez causará vulnerabilidades desconocidas (sabemos que hacerlo hash1(hash2($value))
puede exponer vulnerabilidades significativas en torno a la resistencia a colisiones y los ataques de preimagen).
Teniendo en cuenta que ya está considerando almacenar una clave secreta (el "pimiento"), ¿por qué no usarla de una manera bien estudiada y comprendida? ¿Por qué no cifrar el hash antes de almacenarlo?
Básicamente, después de aplicar el hash a la contraseña, introduzca toda la salida del hash en un algoritmo de cifrado sólido. Luego almacene el resultado encriptado.
Ahora, un ataque de inyección de SQL no filtrará nada útil, porque no tienen la clave de cifrado. Y si se filtra la clave, los atacantes no estarán mejor que si usas un hash simple (lo cual es demostrable, algo con el pimiento "pre-hash" no proporciona).
Nota: si elige hacer esto, use una biblioteca. Para PHP, recomiendo encarecidamente el Zend\Crypt
paquete Zend Framework 2 . De hecho, es el único que recomendaría en este momento. Ha sido revisado enérgicamente y toma todas las decisiones por usted (lo cual es algo muy bueno) ...
Algo como:
use Zend\Crypt\BlockCipher;
public function createHash($password) {
$hash = password_hash($password, PASSWORD_BCRYPT, ["cost"=>$this->cost]);
$blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
$blockCipher->setKey($this->key);
return $blockCipher->encrypt($hash);
}
public function verifyHash($password, $hash) {
$blockCipher = BlockCipher::factory('mcrypt', array('algo' => 'aes'));
$blockCipher->setKey($this->key);
$hash = $blockCipher->decrypt($hash);
return password_verify($password, $hash);
}
Y es beneficioso porque está utilizando todos los algoritmos de formas que se comprenden y se estudian bien (al menos relativamente). Recuerda:
Cualquiera, desde el aficionado más despistado hasta el mejor criptógrafo, puede crear un algoritmo que él mismo no pueda romper.