Il problema qui è fondamentalmente un problema di entropia. Quindi iniziamo a cercare lì:
Entropia per carattere
Il numero di bit di entropia per byte sono:
- Caratteri esadecimali
- Bit: 4
- Valori: 16
- Entropia in 72 caratteri: 288 bit
- Alfanumerico
- Bit: 6
- Valori: 62
- Entropia in 72 caratteri: 432 bit
- Simboli "comuni"
- Punte: 6.5
- Valori: 94
- Entropia in 72 caratteri: 468 bit
- Byte completi
- Bit: 8
- Valori: 255
- Entropia in 72 caratteri: 576 bit
Quindi, il modo in cui agiamo dipende dal tipo di personaggi che ci aspettiamo.
Il primo problema
Il primo problema con il codice è che il passaggio dell'hash "pepe" è l'output di caratteri esadecimali (poiché il quarto parametro su hash_hmac()
non è impostato).
Pertanto, eseguendo l'hashing del tuo pepe, stai effettivamente riducendo la massima entropia disponibile per la password di un fattore 2 (da 576 a 288 bit possibili ).
Il secondo problema
Tuttavia, sha256
fornisce solo 256
bit di entropia in primo luogo. Quindi stai effettivamente riducendo un possibile 576 bit fino a 256 bit. Il tuo passaggio di hash * immediatamente *, per definizione, perde
almeno il 50% della possibile entropia nella password.
Potresti risolvere parzialmente questo problema passando a SHA512
, dove ridurrai l'entropia disponibile solo di circa il 12%. Ma questa è ancora una differenza non insignificante. Quel 12% riduce il numero di permutazioni di un fattore 1.8e19
. È un numero elevato ... e questo è il fattore che lo riduce di ...
Il problema sottostante
Il problema di fondo è che esistono tre tipi di password con più di 72 caratteri. L'impatto che questo sistema di stile avrà su di loro sarà molto diverso:
Nota: da qui in poi presumo che ci stiamo confrontando con un sistema pepper che utilizza SHA512
con output grezzo (non esadecimale).
Password casuali ad alta entropia
Questi sono i tuoi utenti che utilizzano generatori di password che generano una quantità di chiavi grandi per le password. Sono casuali (generati, non scelti dall'uomo) e hanno un'entropia elevata per personaggio. Questi tipi utilizzano byte elevati (caratteri> 127) e alcuni caratteri di controllo.
Per questo gruppo, la tua funzione di hashing ridurrà significativamente la loro entropia disponibile in bcrypt
.
Lasciatemelo dire di nuovo. Per gli utenti che utilizzano password lunghe e ad alta entropia, la tua soluzione riduce significativamente la forza della loro password di una quantità misurabile. (62 bit di entropia persi per una password di 72 caratteri e di più per password più lunghe)
Password casuali di media entropia
Questo gruppo utilizza password contenenti simboli comuni, ma non byte alti o caratteri di controllo. Queste sono le tue password digitabili.
Per questo gruppo, lo farai leggermente sbloccherai più entropia (non la creerai, ma consentirai a più entropia di adattarsi alla password bcrypt). Quando dico leggermente, intendo leggermente. Il pareggio si verifica quando si massimizzano i 512 bit di SHA512. Pertanto, il picco è di 78 caratteri.
Lasciatemelo dire di nuovo. Per questa classe di password, è possibile memorizzare solo altri 6 caratteri prima di esaurire l'entropia.
Password non casuali a bassa entropia
Questo è il gruppo che utilizza caratteri alfanumerici che probabilmente non sono generati in modo casuale. Qualcosa di simile a una citazione della Bibbia o simile. Queste frasi hanno circa 2,3 bit di entropia per carattere.
Per questo gruppo, puoi sbloccare in modo significativo più entropia (non crearla, ma consentire a più di entrare nella password di bcrypt) tramite hashing. Il pareggio è di circa 223 caratteri prima che si esaurisca l'entropia.
Diciamolo di nuovo. Per questa classe di password, il pre-hashing aumenta decisamente la sicurezza in modo significativo.
Ritorno al mondo reale
Questi tipi di calcoli di entropia non hanno molta importanza nel mondo reale. Ciò che conta è indovinare l'entropia. Questo è ciò che influenza direttamente ciò che gli aggressori possono fare. Questo è ciò che vuoi massimizzare.
Sebbene ci siano poche ricerche per indovinare l'entropia, ci sono alcuni punti che vorrei sottolineare.
Le possibilità di indovinare a caso 72 caratteri corretti di seguito sono estremamente basse. Hai più probabilità di vincere alla lotteria Powerball 21 volte, piuttosto che avere questa collisione ... Ecco quanto è grande il numero di cui stiamo parlando.
Ma potremmo non inciampare su di esso statisticamente. Nel caso delle frasi, la probabilità che i primi 72 caratteri siano gli stessi è molto più alta rispetto a una password casuale. Ma è ancora banalmente basso (è più probabile che vincerai la lotteria Powerball 5 volte, sulla base di 2,3 bit per personaggio).
In pratica
In pratica, non importa davvero. Le possibilità che qualcuno indovini correttamente i primi 72 caratteri, dove questi ultimi fanno una differenza significativa, sono così basse che non vale la pena preoccuparsi. Perché?
Bene, diciamo che stai prendendo una frase. Se la persona riesce a interpretare correttamente i primi 72 caratteri, è davvero fortunata (non probabile) o è una frase comune. Se è una frase comune, l'unica variabile è il tempo necessario per realizzarla.
Facciamo un esempio. Prendiamo una citazione dalla Bibbia (solo perché è una fonte comune di testo lungo, non per nessun altro motivo):
Non desiderare la casa del tuo prossimo. Non desiderare la moglie del tuo vicino, il suo servo o la sua schiava, il suo bue o l'asino o qualsiasi cosa che appartenga al tuo prossimo.
Sono 180 caratteri. Il 73 ° personaggio è g
il secondo neighbor's
. Se hai indovinato così tanto, probabilmente non ti fermerai a nei
, ma continuerai con il resto del verso (poiché è così che è probabile che venga utilizzata la password). Pertanto, il tuo "hash" non ha aggiunto molto.
BTW: ASSOLUTAMENTE NON sto sostenendo l'uso di una citazione della Bibbia. In effetti, l'esatto contrario.
Conclusione
Non aiuterai molto le persone che usano password lunghe eseguendo prima l'hashing. Alcuni gruppi puoi sicuramente aiutare. Alcuni possono sicuramente ferire.
Ma alla fine, niente di tutto ciò è eccessivamente significativo. I numeri con cui abbiamo a che fare sono SEMPRE troppo alti. La differenza di entropia non sarà molto.
È meglio lasciare bcrypt così com'è. È più probabile che rovini l'hashing (letteralmente, l'hai già fatto e non sei il primo o l'ultimo a commettere quell'errore) di quanto l'attacco che stai cercando di prevenire accadrà.
Concentrati sulla protezione del resto del sito. E aggiungi un misuratore di entropia della password alla casella della password al momento della registrazione per indicare la forza della password (e indicare se una password è troppo lunga che l'utente potrebbe desiderare di cambiarla) ...
Questo è almeno il mio $ 0,02 (o forse molto più di $ 0,02) ...
Per quanto riguarda l'utilizzo di un pepe "segreto":
Non c'è letteralmente alcuna ricerca sull'alimentazione di una funzione hash in bcrypt. Pertanto, nella migliore delle ipotesi non è chiaro se inserire un hash "pepato" in bcrypt causerà mai vulnerabilità sconosciute (sappiamo che ciò hash1(hash2($value))
può esporre vulnerabilità significative intorno alla resistenza alle collisioni e agli attacchi preimage).
Considerando che stai già pensando di conservare una chiave segreta (il "pepe"), perché non usarla in un modo ben studiato e compreso? Perché non crittografare l'hash prima di archiviarlo?
Fondamentalmente, dopo aver hash la password, inserisci l'intero output hash in un algoritmo di crittografia potente. Quindi memorizzare il risultato crittografato.
Ora, un attacco SQL-Injection non farà trapelare nulla di utile, perché non hanno la chiave di cifratura. E se la chiave è trapelata, gli aggressori non stanno meglio che se usassi un semplice hash (che è dimostrabile, qualcosa con il pepe "pre-hash" non fornisce).
Nota: se scegli di farlo, usa una libreria. Per PHP, consiglio vivamente il Zend\Crypt
pacchetto di Zend Framework 2 . In realtà è l'unico che consiglierei in questo momento. È stato fortemente rivisto e prende tutte le decisioni per te (il che è molto positivo) ...
Qualcosa di simile a:
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);
}
Ed è vantaggioso perché stai utilizzando tutti gli algoritmi in modi che sono ben compresi e ben studiati (almeno relativamente). Ricorda:
Chiunque, dal dilettante più incapace al miglior crittografo, può creare un algoritmo che lui stesso non può rompere.