(Nessuno degli esempi forniti deve essere utilizzato per l'hashing della password. Sono troppo veloci!)
Il primo principio generale che vorrei menzionare qui è che vogliamo che le nostre operazioni di crittografia siano interfacce di alto livello ; dovrebbe esserci una astrazione comune e riusabile che definisce:
- Quali precondizioni deve soddisfare il chiamante prima di chiamarli;
- Quali argomenti richiede l'operazione;
- Quali valori restituisce o quali postcondizioni soddisfa quando viene chiamato correttamente;
- Quali proprietà di sicurezza deve offrire il concetto.
In questo caso stiamo parlando di hashing password . Una funzione di hashing della password, nella sua più semplice incarnazione, dovrebbe:
- Accetta due argomenti, una salt e una password;
- Restituisce un codice hash per quella password / combinazione di sale;
- consuma molte risorse in modo da rallentare notevolmente l'attacco di un utente malintenzionato, ma non tanto da un utente onesto
Quindi SHA-256 fallisce il primo e il terzo punto. Accetta solo un argomento ed è troppo veloce!
E in realtà, l'interfaccia sopra descritta non è forse nemmeno la migliore per l'hashing delle password. Un'interfaccia migliore è una coppia di funzioni scritte sopra l'hash della password (in pseudocodice 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
Quindi un'astrazione appropriata dice che per la verifica della password dovremmo usare due funzioni come queste, scritte sopra una funzione password_hash
intensiva delle risorse, che a sua volta verrebbe probabilmente scritta su una funzione di basso livello come SHA -256.
Si noti che l'interfaccia di hashing della password di PHP segue effettivamente quest'ultimo design:
- La funzione
password_hash
deve essere chiamata con una password ma senza sale ; seleziona saltato in modo casuale, e come dice la pagina: "L'algoritmo utilizzato, costo e sale vengono restituiti come parte dell'hash. Pertanto, tutte le informazioni necessarie per verificare l'hash sono incluse in esso."
- La funzione
password_verify
viene utilizzata per verificare se una password putativa corrisponde alla stringa di verifica .
Secondo problema: un'idea chiave nella progettazione di protocolli di sicurezza o altri costrutti di sicurezza è che gli autori di attacchi spesso infrangono le regole e fanno in modo che il codice venga richiamato in modi e contesti inattesi . Ad esempio, quando le persone scrivono qualcosa del genere:
hash($salt.$pass)
hash($pass.$salt)
... spesso assumono implicitamente che salt
sarà sempre la stessa lunghezza fissa, e non si fermerà a considerare se qualche attacker potrebbe essere in grado di:
- Trova un modo per "infrangere le regole" in modo che possano determinare la lunghezza di
salt
da variare tra più chiamate al codice;
- Trova un modo inaspettato per sfruttarlo a proprio vantaggio.
Ad esempio, potresti pensare che sia impossibile per un utente malintenzionato trovare una collisione per l'hash delle password in questo modo, ma in effetti se possono controllare salt
e pass
è banale farlo:
hash("0001"."Passsword1!")
hash("0001P"."asssword1!")
Questo potrebbe sembrare inverosimile, e beh, a dirti la verità, non posso davvero pensare a uno scenario in cui ciò potrebbe essere sfruttabile. Ma vorremmo davvero che la nostra sicurezza si fondasse su basi più solide di "Non riesco a pensare a un modo per rompere questo". Quindi, come filosofia generale, è più sicuro progettare le cose in modo tale che non possano sorgere ambiguità. In questo caso, vorremmo che fosse mantenuta la seguente regola:
- Se abbiamo due password diverse con due diversi sali, gli input che forniamo alla funzione di hash di basso livello dovrebbero essere diversi.
Quindi questo significa che per produrre l'input che nutriamo con l'hash sottostante, dovremmo preferire combine
con una iniettivo funzione - una funzione tale che combine(salt1, password1) == combine(salt2, password2)
se e solo se salt1 == salt2
e password1 == password2
.
hash(combine($salt, $password))
Alcuni modi per farlo:
- Applica uno degli input a una dimensione fissa e antepone l'altro. HMAC lo fa internamente per le chiavi.
- Hash uno dei due input e anteponilo all'altro. HMAC esegue questa operazione per le chiavi più lunghe della dimensione di blocco della funzione di hash sottostante.
- Inserisci un delimitatore univoco tra gli input (probabilmente richiede di evitare le occorrenze del delimitatore nei valori di input).
Funzioni di hashing della password dedicate come PBKDF2, bcrypt, scrypt e Argon2 aderiscono alla maggior parte di queste idee.