Quanti round di hashing sono sufficienti per un gestore di password?

15

Attualmente sto scrivendo il mio piccolo gestore di password che memorizza la chiave in un hash SHA256 , con salt. Creo l'hash effettuando le seguenti operazioni:

def sha256_rounds(raw, rounds=100001):
    obj = hashlib.sha256()
    for _ in xrange(rounds):
        obj.update(raw)
        raw = obj.digest()
    return obj.digest()

Dopo che è stato creato, viene memorizzato con il seguente:

    key = base64.urlsafe_b64encode(provided_key)
    length = len(key)
    with open(key_file, "a+") as key_:
        front_salt, back_salt = os.urandom(16), os.urandom(16)
        key_.write("{}{}{}:{}".format(front_salt, key, back_salt, length))

Le domande / i dubbi che ho sono:

  • È un modo accettabile per memorizzare una chiave con hash?
  • Dovrei usare più iterazioni?
  • La mia tecnica di salatura è sufficiente? O dovrei usare una tecnica di salatura diversa?

Se questo non è accettabile per memorizzare una password / chiave con hash, quali altri passaggi posso prendere per renderlo più sicuro?

UPDATE:

Ho preso molti dei consigli del tuo ragazzo, e se vuoi vedere il risultato finora del mio piccolo gestore di password puoi trovarlo here . Il consiglio di tutti è molto apprezzato e continuerò a cercare di renderlo il più eccellente possibile. (se quel link non funziona usa questo link )

    
posta CertifcateJunky 28.06.2018 - 15:39
fonte

5 risposte

80

I'm currently writing my own little password manager

Questo è il tuo primo errore. Qualcosa di questo complesso ha molte insidie che a volte anche gli esperti cadono, senza molta esperienza in quest'area non hai la possibilità di fare qualcosa di simile alla sicurezza.

stores the key in an SHA256 hash

Uh oh ...

Questo non significa necessariamente che stai facendo qualcosa di sbagliato, ma nutro forti dubbi sul fatto che tu stia andando bene. Immagino tu stia parlando di una password principale che viene sottoposta a hashing qui? La password principale deve essere trasformata in una chiave utilizzando una KDF come PBKDF2, bcrypt o Argon2, quindi questa chiave viene utilizzata per crittografare le password memorizzate.

Se vuoi avere un modo per verificare che la password sia corretta, la memorizzazione di un hash della chiave dovrebbe andare bene, ma NON DEVE memorizzare la chiave stessa ... se si memorizza il chiave chiunque abbia accesso al tuo spazio di archiviazione ha tutto ciò di cui hanno bisogno per decifrare tutte le password!

Se non stai parlando di hashing di una password principale e intendi una vera chiave generata casualmente allora non ho idea di cosa stai provando a realizzare qui, ma non dovresti usare un KDF lento con un grande numero di iterazioni.

In alternativa è possibile eseguire il hashing della password master due volte, una volta per memorizzare come hash per verificare successivamente che la password inserita dall'utente sia corretta e utilizzarla nuovamente come chiave per la crittografia. A seconda di come è fatto, potrebbe andare da un difetto di progettazione a dare via completamente la chiave.

Modifica: dopo aver visto il codice completo sembra essere una quarta opzione: memorizzi un hash della password per verificare in seguito se la password inserita è corretta, quindi hai hash questo hash da usare come la chiave, che è quasi pessima quanto memorizzare la chiave stessa.

I create the hash by doing the following:

def sha256_rounds(raw, rounds=100001):
    obj = hashlib.sha256()
    for _ in xrange(rounds):
        obj.update(raw)
        raw = obj.digest()
    return obj.digest()

Non è chiaro quale sia raw qui, ma suppongo che sia la password. Quello che stai facendo è un hash non salato usando SHA256. Non provare a creare il tuo KDF!

After it is created it is stored with the following:

key = base64.urlsafe_b64encode(provided_key)
length = len(key)
with open(key_file, "a+") as key_:
    front_salt, back_salt = os.urandom(16), os.urandom(16)
    key_.write("{}{}{}:{}".format(front_salt, key, back_salt, length))

Quindi, stai creando la chiave taggando la password, quindi aggiungendo un salt in modo casuale alla parte anteriore e posteriore? Non solo concatenare 2 diversi sali davanti e dietro non standard, non sta facendo nulla qui perché è fatto dopo che il KDF è già finito! Stai solo aggiungendo alcuni valori casuali per il gusto di averli lì.

Per mostrare quanto sia pessimo (a partire dal commit 609fdb5ce976c7e5aa1832670505da60012b73bc), tutto ciò che serve per scaricare tutte le password memorizzate senza che richiedono una password principale è questa:

from encryption.aes_encryption import AESCipher
from lib.settings import store_key, MAIN_DIR, DATABASE_FILE, display_formatted_list_output
from sql.sql import create_connection, select_all_data

conn, cursor = create_connection(DATABASE_FILE)
display_formatted_list_output(select_all_data(cursor, "encrypted_data"), store_key(MAIN_DIR))

Anche se può essere una buona esperienza di apprendimento provare a creare un gestore di password, per favore per favore non usarlo mai per qualcosa da remoto importante. Come suggerisce @Xenos, non sembra che tu abbia abbastanza esperienza che la creazione di un proprio gestore di password sarebbe davvero utile in ogni caso, sarebbe probabilmente una migliore opportunità di apprendimento per dare un'occhiata a un gestore di password open source esistente.

    
risposta data 28.06.2018 - 16:38
fonte
19

Innanzitutto fammi sapere che lavoro per 1Password, un gestore di password, e sebbene possa sembrare autonomo, devo aggiungere la mia voce a coloro che ti dicono che scrivere un gestore di password sicuro è più difficile di quanto possa sembrare prima. D'altra parte, è bene che tu ci stia provando e chiedendo in pubblico. Questo è un buon modo per imparare che è più difficile di quanto possa apparire prima.

Non autenticare. Encrypt invece

Ho dato una rapida occhiata alla fonte a cui ti colleghi nel tuo aggiornamento, e sono lieto di vedere che hai seguito un consiglio di alcuni che è stato offerto finora. Ma a meno che non legga male il tuo codice, hai ancora un errore di progettazione fondamentale che lo rende gravemente insicuro .

Sembra che si tratti la voce della password principale come domanda di autenticazione anziché come derivazione della chiave di crittografia. Cioè, stai controllando la password inserita (dopo l'hashing) con qualcosa che è stato memorizzato, e se quel controllo passa stai usando quella chiave memorizzata per decrittografare i dati.

Quello che dovresti fare è archiviare una chiave di crittografia crittografata. Chiamiamola la chiave principale. Questo dovrebbe essere generato casualmente quando si imposta per la prima volta una nuova istanza. Questa chiave principale è ciò che utilizzi per crittografare e decrittografare i dati effettivi.

La chiave master non viene mai salvata in chiaro. Dovrebbe essere crittografato con ciò che a volte viene chiamato Key Encryption Key (KEK). Questo KEK è derivato tramite la funzione di derivazione chiave (KDF) dalla password principale dell'utente e un valore salt.

Quindi l'output del tuo uso di PBKDF2 (grazie per averlo usato invece dell'hash appena ripetuto) sarà il tuo KEK, e utilizzerai il KEK per decrittografare la chiave master, e quindi utilizzerai la chiave master decrittografata per decrittografare i tuoi dati .

Ora forse questo è quello che stai facendo e ho letto male il tuo codice. Ma se stai solo confrontando ciò che ottieni attraverso il tuo KDF in qualcosa memorizzato e poi decidi se decifrare, allora il tuo sistema è enormemente e terribilmente insicuro.

Per favore, fai la più grande prima linea del progetto README urlando al fatto che è gravemente insicuro. E aggiungilo anche a tutti i file sorgente.

Alcuni altri punti

Qui ci sono solo alcune altre cose che ho notato guardando all'origine

  • Utilizza una modalità di crittografia autenticata come GCM anziché CBC.

  • Il tuo approccio di crittografia del tutto funzionerà per piccole quantità di dati, ma non verrà ridimensionato una volta che avrai set di dati più grandi.

    Quando arriva il momento di crittografare (parti di) record separatamente, tieni presente che avrai bisogno di un unico nonce (per GCM) o vettori di inizializzazione univoci (se non tieni d'occhio con CBC) per ogni record.

  • La distruzione dei dati dopo 3 fallimenti è pericolosa e non aggiunge sicurezza.

    Un hacker sofisticato copia semplicemente il tuo file di dati e scrive il proprio script per provare a decifrare la password. Faranno anche la propria copia dei dati acquisiti. La distruzione dei dati rende solo facile per qualcuno distruggere accidentalmente o intenzionalmente i tuoi dati.

Quanti round di PBKDF2

Quindi alla tua domanda iniziale. La risposta è che dipende. Da un lato, dipende da quanto sia strong la master password e dal tipo di risorse che un utente malintenzionato genererà sul problema. E dipende anche da quali risorse il difensore può lanciarvi. Ad esempio, eseguirai il tuo KDF su un dispositivo mobile con capacità limitata della batteria?

Ma come accade noi di 1Password stiamo cercando di valutare il costo del cracking di offrire premi a persone o gruppi che violano un hash di password di 42,5 bit con 100.000 round di PBKDF2-HMAC-SHA256.

Guadagni decrescenti da hashing lento

Ma ciò che è più importante di capire come pesare tutti questi è capire che l'hashing lento, mentre assolutamente essenziale per un gestore di password KDF, produce guadagni marginali decrescenti una volta che è regolato sufficientemente alto .

Supponiamo che tu stia utilizzando round da 100.000 e che tu abbia una password principale, P . Se hai scelto una cifra casuale, 0-9 e l'hai aggiunta a P , avresti ottenuto un aumento di dieci volte nella resistenza al cracking. Per ottenere lo stesso aumento aumentando i round di PBKDF2, avresti bisogno di andare fino a 1 milione di round. Il punto è che una volta che hai una quantità decente di hashing lento, ottieni molta più forza per lo sforzo del difensore aumentando la forza della password di quanto tu faccia aumentando il numero di round.

Ho scritto su questo alcuni anni fa in Bcrypt è grandioso, ma il cracking della password non è fattibile?

    
risposta data 29.06.2018 - 22:57
fonte
6

Non utilizzare SHA256 crudo: è in grado di essere accelerato utilizzando GPU e quindi soggetto a forza bruta. Non è rotto, da nessuna parte, semplicemente non è più la pratica migliore. PBKDF2 funziona per neutralizzarlo in una certa misura, ma dovresti usare preferibilmente bcrypt o scrypt.

Reinventare la ruota (che è praticamente quello che stai facendo se non stai usando PBKDF2-SHA256, bcrypt o scrypt) non è mai una buona idea in crypto.

    
risposta data 28.06.2018 - 16:43
fonte
3

Guardando il codice nel link che hai postato (riprodotto di seguito, in alcune parti), sono un po 'confuso.

Se lo leggo correttamente, main() chiama store_key() per caricare una chiave da un file sul disco, quindi usa compare() per confrontare una chiave data dall'utente con quella. Entrambe percorrono sha256_rounds() che esegue su di esse PBKDF2.

Quindi usi lo stesso stored_key , quello caricato dal file, per eseguire la crittografia?

Questo è completamente e completamente indietro. Chiunque abbia accesso al file chiave può semplicemente caricare la chiave memorizzata e usarla per decrittografare i dati memorizzati o memorizzare nuovi dati crittografati con la stessa chiave, senza dover passare attraverso lo script a " verificare "la password.

Nel migliore dei casi, penso che tu stia confondendo usando la password / frase di accesso data dall'utente per produrre una chiave per la crittografia e autenticare l'utente con un hash della password memorizzato . Non è possibile eseguire entrambi con lo stesso hash, la crittografia è completamente irrilevante se si è archiviata la chiave di crittografia insieme ai dati crittografati.

Certo, le password vengono elaborate con una sorta di hash / KDF sia quando vengono utilizzate per l'autenticazione sia quando vengono utilizzate per produrre una chiave di crittografia. Ma è più come una fase di pre-elaborazione necessaria solo perché gli esseri umani non sono in grado di memorizzare le chiavi a 256-bit, e invece dobbiamo affidarci alle password a bassa entropia. Quello che fai con l'hash / chiave che hai è diverso tra i due casi d'uso.

Poi c'è la cosa in cui hai un sale codificato in sha256_rounds() , che praticamente sconfigge lo scopo di un sale in generale. Potrei commentare il codice anche più in generale, ma questo non è codereview.SE.

Per favore, dimmi che ho letto il codice sbagliato.

Le parti del codice che ho visto:

def main():
    stored_key = store_key(MAIN_DIR)    
    if not compare(stored_key):
        ...
    else:
            ... # later
            password = prompt("enter the password to store: ", hide=True)
            encrypted_password = AESCipher(stored_key).encrypt(password)



def store_key(path):
    key_file = "{}/.key".format(path)
    if not os.path.exists(key_file):
        ...
    else:
        retval = open(key_file).read()
        amount = retval.split(":")[-1]
        edited = retval[16:]
        edited = edited[:int(amount)]
        return edited
    
risposta data 29.06.2018 - 16:38
fonte
2

Utilizza uno strumento mantenuto che deciderà per te e aumenterà il conteggio predefinito o anche l'algoritmo nel tempo. Suggerirei libsodium, ma ce ne sono altri. Il default di Libsodium è "interattivo" hashing della password e per la crittografia simmetrica. Assicurati che il tuo software possa un giorno "aggiornare" le impostazioni secondo necessità. Uno strumento come libsodium cambierà le loro impostazioni di default di volta in volta in modo da poterli seguire.

Realizzo per un open source gestore password e utilizzo libsodium. Ho anche letto il documento di sicurezza di Mitro che vale la pena leggere.

    
risposta data 29.06.2018 - 16:05
fonte

Leggi altre domande sui tag