sabato 17 aprile 2021

Crittografia e Hashing

Introduzione

Esamineremo in questa nota i concetti principali  di criptografia e hashing, a cosa servono, come vengono usati e le best practices in ambito di sicurezza dei dati.



Occorre innanzitutto distinguere due tipi di funzioni che possono essere applicate ai dati: la cifratura e l'hashing. Entrambi trasformano un testo "in chiaro" in un testo completamente diverso, ma con differenze sostanziali: la cifratura produce una serie di caratteri che può essere, mediante la decifratura, ritrasformata nel testo iniziale, mentre l'hashing non è una funzione invertibile.
La dimensione di un file criptato è in generale molto simile alla dimensione del file originale, mentre l'hashing occupa molto meno spazio (a meno che il file originale non sia di pochi caratteri).
La cifratura serve a trasmettere un file in modo che sia leggibile solo a chi sappia come decifrarlo
Durante la seconda guerra mondiale la decifratura di messaggi ha consentito di intercettare molte rotte di sommergibili e attacchi sia via terra che via mare. La possibilità di veicolare dati in modo che non siano accessibili da tutti è essenziale oggi in cui dati di ogni genere viaggiano e giacciono su server lontani migliaia di chilometri da chi li utilizza.
Viceversa l'hashing produce una sequenza di byte che è una "sintesi", in qualche modo, di un file in chiaro, in modo che leggendo l'hash del messaggio sia possibile stabilire se il messaggio non sia stato modificato in seguito al calcolo dell'hash.
Un altro utilizzo degli algoritmi di hashing è la memorizzazione delle password. In questo caso è memorizzato direttamente l'hash della password e non la password stessa, e poi, quando l'utente fornisce la password, ne è calcolato l'hash e confrontato con quello memorizzato per vedere se coincidono.

Prima di addentrarci nei dettagli dei vari algoritmi, consideriamo un aspetto importante sulla sicurezza:

Principio di Kerckhoffs

La conoscenza dell'algoritmo non deve causare un indebolimento della sua robustezza crittografica

In sostanza la sicurezza non deve dipendere dal nascondere l'algoritmo con cui è implementata ma solo dalla chiave crittografica utilizzata in esso.
Questo principio è diametralmente opposto a quello della "sicurezza tramite segretezza", che già dal 1851 è stato sconfessato dagli esperti di sicurezza, oggi come oggi infatti è ampiamente accettato dalla comunità scientifica che gli algoritmi open source siano molto più robusti, poiché grazie alla collaborazione delle comunità, le falle  di un algoritmo pubblico possono essere scoperte più facilmente e rapidamente corrette, rispetto a quelle di algoritmi "segreti" e manutenuti da un singolo attore.

Il NIST (National Institute of Standard and Technology) negli Stati Uniti sconsiglia espressamente questa pratica. A tal proposito vedasi "Guide to General Server Security", sezione 2.4 Open Design: System security should not depend on the secrecy of the implementations or its components.

Dunque è inutile e sbagliato, quando si tratta di sicurezza, inventarsi algoritmi propri per crittografare i dati: è meglio utilizzare algoritmi pubblici e ben collaudati, e preoccuparsi piuttosto di nascondere le chiavi in modo corretto.


Crittografia

La crittografia serve dunque a "offuscare" un messaggio, in modo che solo chi è autorizzato possa decodificarlo. Oltre al messaggio, il ricevente dovrà conoscere (per altre vie), la "chiave" necessaria a decodificarlo, senza la quale non è possibile risalire al messaggio originale. 

Possiamo dunque ragionare degli algoritmi crittografici come delle funzioni che siano in qualche modo invertibili.
Distinguiamo in tal senso due grandi famiglie di algoritmi crittografici, quelli simmetrici e asimmetrici.
 

Crittografia simmetrica

Nella crittografia simmetrica c'è una sola chiave "segreta", ed è usata sia per la cifratura che nella decifratura di un messaggio. 
Ossia considerata una coppia di funzioni di cifratura/decifratura cypher/decypher, ed un segreto (secret), il procedimento sarà del tipo
Cifratura:     
obfuscated message =  cypher(message, secret)

Decifratura:
message = decypher(obfuscated message, secret)

Ossia secret è lo stesso sia in cifratura che decifratura, e deve essere noto a priori da chi riceve il messaggio da decodificare.
Alcuni algoritmi di crittografia simmetrica sono: 
  • DES (Data Encryption Standard): a 16 stadi, si basa su una chiave a 56 bit e cifra sequenze di 64 bit, da molto tempo, con le potenze di calcolo disponibili, è possibile forzarlo in poche ore o minuti
  • 3DES: a 48 stadi, si basa sulla triplice applicazione del DES, con 3 chiavi distinte, per cui in totale la chiave è di 168 bits ed il blocco cifrato è di 64 bit. La sicurezza garantita è però solo di 112 bits, ossia il doppio del DES. E' ai fini pratici un algoritmo molto sicuro, poiché il migliore attacco conosciuto richiede circa 2^32 parole conosciute, 2^113 passi, 2^90 cifrature DES e memoria per 2^88 bytes. Di fatto se usata solo all'interno di una singola sessione non è praticamente attaccabile 
  • AES (Advanced Encryption Standard), o anche Rijndael: a 10/12/14 stadi si basa su una chiave a 128/192/256 bit, e codifica sequenze di 128 bit, sta sostituendo il 3DES in tutti gli usi, è più veloce da applicare ed è facilmente "cablabile" in circuiti fisici, richiede poca memoria e fornisce un ottimo livello di sicurezza, che varia a seconda della dimensione della chiave usata. Non esistono al momento attacchi possibili a questo codice attuabili "praticamente".
  • IDEA, CAST5, BLOWFISH, TWOFISH: sono altri algoritmi ma meno usati, poiché l'AES è stato quello scelto dal NIST nel 1997 per rimpiazzare il 3DES
Un punto debole del 3DES rispetto all'AES è che codifica blocchi di 64 bit anziché 128, e questo lo rende maggiormente vulnerabile ad un certo tipo di crittoanalisi .

Ai fini pratici, il 3DES o l'AES sono ottimi algoritmi di crittografia simmetrica, ovviamente poi bisogna effettuare delle considerazioni:
  • per quanto tempo si desidera nascondere le informazioni? se supponiamo ad esempio di nascondere un'informazione che sarà resa pubblica tre giorni dopo, e supponiamo che con la massima potenza di calcolo disponibile sia possibile decifrare il messaggio criptato in 30 anni, questo rende l'algoritmo assolutamente sicuro. Viceversa se si vuole nascondere un'informazione per periodi molto più lunghi, bisogna anche considerare che in futuro la potenza di calcolo potrebbe aumentare e rendere la cifratura meno sicura.
  • quanto è importante quell'informazione e quanto chi ha interesse a decifrarla potrebbe essere disposto a spendere per riuscirci

Questi algoritmi sono presenti in tutte le librerie standard, per cui non saranno forniti qui i dettagli di funzionamento, già descritti ampiamente altrove.


Crittografia asimmetrica, o anche "crittografia a chiave pubblica"

In questo caso nella cifratura e nella decifratura del messaggio sono usate chiavi diverse, una pubblica ed una privata. La forza del metodo risiede nel fatto che non è praticamente possibile ottenere la chiave privata dalla conoscenza della chiave  pubblica, e tutto questo si basa sull'impossibilità di decomporre in fattori primi numeri molto grandi. 
I fondamenti matematici affondano le radici nel piccolo teorema di Fermat e sul teorema cinese del resto, in sintesi sul fatto che 

 ove p,q sono numeri primi, e detto n=pq,  e detto T = (p-1)(q-1), ed "e" (esponente pubblico) un numero coprimo con T e più piccolo di T, e "d" (esponente privato) tale che ed=1 (modulo T)

In questo caso il procedimento è dunque:
Cifratura:     
obfuscated message =  cypher(message, chiave pubblica)

Decifratura:
message = decypher(obfuscated message, chiave privata)

ossia 
 decypher( cypher(message,chiave pubblica) , chiave privata) = message

Le due chiavi, pubblica e privata, sono in un certo senso una inversa dell'altra, poiché sono (anche) tali che, usando la chiave privata nell'algoritmo di cypher, questo possa essere decifrato con la chiave pubblica, ossia vale anche:
 decypher( cypher(message,chiave privata) , chiave pubblica) = message

Garanzia sul mittente mediante RSA

E' possibile anche garantire al ricevente che il messaggio sia autentico, ossia proveniente dal giusto mittente e non manipolato, se il mittente, prima di inviare il file, lo cifra con la  propria chiave privata.
Chiamiamo Mpubb e Mpriv chiavi pubbliche e private del mittente, e Rpubb e Rpriv  chiavi del ricevente.
Il mittente cifra prima il messaggio con la chiave pubblica del ricevente e poi con la propria chiave privata:
obfuscated message =  cypher(cypher (message, Rpubb), Mpriv)

Il ricevente decifra prima il messaggio con la chiave pubblica del mittente e poi con la propria chiave privata, e sa che il mittente è l'unico che può aver cifrato il messaggio con la chiave Mpriv
message = decypher( decypher( obfuscated message, Mpubb), Rpriv)

Per ottenere un buon livello di sicurezza è necessario utilizzare chiavi di almeno 2048 bit. Una chiave di 1024 bit può essere decomposta in circa un anno di tempo al costo di circa un milione di dollari.


Protocollo di Diffie-Hellman

Sviluppato da Diffie ed Hellman nel 1976, permette a due utenti di stabilire una chiave condivisa segreta utilizzando un canale non sicuro. Questo è di solito usato nella procedura di handshake che precede una comunicazione con uno schema di crittografia simmetrica. La chiave simmetrica è usata come chiave di sessione, temporanea, prima dell'invio dei dati con la crittografia asimmetrica.

In sostanza i due interlocutori A e B, che si sono preventivamente messi d'accordo su un gruppo ciclico G (pubblico), generano rispettivamente due numeri casuali a e b. A invia g^a a B e B invia g^b ad A.
A calcola  (g^b)^a, e B calcola (g^a)^b, che sono uguali, e che costituisce la chiave condivisa per una crittografia simmetrica.

L'algoritmo può essere reso più sicuro se l'invio di A è effettuato usando con un algoritmo RSA facendo in modo che A invii a B un dato crittografato con la chiave privata di A (e viceversa).


Digital Signature Algorithm (DSA)

E' uno standard per la firma digitale, che si basa sul calcolo dello SHA (che vedremo tra poco) del messaggio e alla firma mediante una chiave privata di esso, in modo tale che per chi riceve il messaggio sia possibile, con un calcolo inverso, verificare che la firma presente contenga effettivamente lo SHA del messaggio cifrato con la chiave privata del firmatario.
I principi alla base del DSA sono sostanzialmente quelli dell'algoritmo RSA.


Hashing

Una funzione di Hashing mappa una stringa di lunghezza qualsiasi in una stringa di dimensione predefinita.
La caratteristica principale di una funzione di hashing è che non deve essere computazionalmente trattabile il problema di cercare una stringa avente l'hash dato. La differenza tra i vari algoritmi di hashing sta in quanto sia probabile avere delle collisioni. Le principali funzioni di hash sono  l'MD5, lo SHA, il RIPEMD.

SHA

Lo SHA è una famiglia di vari algoritmi, SHA1, SHA-224, SHA-256, SHA-384, SHA-512, ognuno dei quali produce un hash di tanti bit quanti indicati dal nome, tranne lo SHA-1 che produce un digest di 160 bit.
Lo SHA-1 è stato "craccato" da algoritmi di critto analisi, quindi sono da preferirsi i fratelli maggiori. Opera con blocchi di 10 bytes e produce un hash di altrettanti bytes.
 Lo SHA-256 o 512 sono quelli attualmente ritenuti sicuri, e operano e restituiscono blocchi rispettivamente di 16/32 bytes.
Lo SHA-1 è tuttavia usato in molti protocolli e applicazioni, tra cui TLS, SSL, PGP. S/MIME e IPSec.


MD5

L'MD5, message-digest algorithm, lavora su blocchi di 32 bytes e produce un hash di 16 bytes. Nel 2012 è stato craccato e la sua debolezza usata dal malware Flame , pertanto non dvrebbe più essere usato. In particolare è stato trovato un algoritmo in grado di trovare in pochi secondi dei messaggi aventi un determinato hash. Questo rende l'algoritmo inutilizzabile, poiché sarebbe potrebbe essere facile per un utente maligno lasciare intatta la firma (vedasi DSA) e cambiare il messaggio con un altro avente lo stesso hash per far credere al ricevente che sia un messaggio genuino.


RIPEMD

Ne esistono varie versioni operanti con  varie lunghezze di blocchi: 160,256,320. Sviluppati per risolvere i problemi dell'MD5 sono però sono meno sicuri degli SHA, per cui si raccomanda l'uso degli SHA.

Crittografia Ellittica o ECC

E' un tipo di crittografia a chiave pubblica basata sulle curve ellittiche definite su campi finiti.

A differenza degli algoritmi RSA, che si basano sulla difficoltà di decomporre in fattori primi grossi numeri. questi algoritmi si basano sulla difficoltà di risolvere l'equazione a^x=b quando questa si presenta in un campo finito. In altre parole sulla difficoltà di calcolare il logaritmo discreto di un numero. Al momento gode di una buona reputazione ed è stata inserita tra gli algoritmi consigliati dalla NSA.

Memorizzazione delle password

Un problema ricorrente per chi scrive software è memorizzare le password degli utenti, per poter in seguito verificare se quella che inseriscono è corretta.
Senz'altro non vanno memorizzate in chiaro, e bisogna fare in modo tale che chi dovesse venire in possesso del file o del database ove sono memorizzate le password "criptate", non possa ottenere le password in chiaro e possibilmente nemmeno collegarle agli utenti.
Per oscurare la password, il metodo principe utilizzato è senz'altro l'hash, nella fattispecie è stato a lungo usato l'MD5 ma oggi è preferito lo SHA256 o 512.

Attacchi dizionario e sicurezza delle password

E' una tecnica di attacco ad una password confrontandone l'hash con gli hash contenuti in uno o più dizionari di password già calcolate. Per le proprietà della funzione di hash, non è possibile in maniera diretta risalire ad un testo (password) avente lo stesso hash, ma se la password è semplice o già usata da altri, è facile che questa sia presente in un "dizionario" di hash.
I dizionari sono spesso calcolati da dizionari veri e propri di lingua, e poi ottenute a partire dalle parole in esse contenute delle varianti inserendo uno o più numeri o altri caratteri. 
Tuttavia tante più caratteri si considerano e tanto più un attacco a dizionario non ha alcun modo di riuscire. 
Ad esempio se consideriamo una password di 16 caratteri, con caratteri scelti a caso tra lettere maiuscole, minuscole, caratteri speciali e cifre, per semplicità supponiamo da un set di 80 caratteri, le combinazioni possibili sono di 80^16 = 2.8 * 10^30
Uno dei più grandi database esistenti di password contiene circa 1.5 * 10^10 hash di password, ossia 15 miliardi.
Da un rapido calcolo si può facilmente evincere che la probabilità che la password così generata sia contenuta in un siffatto database è di circa 1 / (1.8 * 10^20 ) ossia  una possibilità ogni duecento  miliardi di miliardi.
Ora, se tutti usassimo password così sicure e usassimo le stesse una volta sola per ogni sito o programma a cui ci iscriviamo, e magari  periodicamente, mettiamo ogni 6 mesi, le cambiassimo, nessun attacco a dizionario avrebbe mai successo. In sostanza 15 miliardi di password in un dizionario possono sembrare tante, ma rispetto alle possibilità combinatorie con 16 caratteri sono meno di un granello di sabbia nel deserto.

E' inoltre da notare che si volesse tentare di violare una siffatta password, anche con tutta la potenza di calcolo dei più potenti processori, anche potendo testare 1 miliardo di hash al secondo (quelli più potenti attuali non superano qualche centinaio di migliaia al secondo), ci vorrebbero circa 2.8 * 10^20 secondi, siamo intorno ai 10^13 anni. Si consideri che si prevede che il sole si spegnerà tra soli 5-7 miliardi di anni.

Tuttavia molti utenti scelgono password ben più semplici  e/o le utilizzano in più occasioni, pertanto tali password diventano tanto sicure quanto il meno sicuro dei siti in cui le usano.
E ci sono siti che addirittura le memorizzano in chiaro, perché non gestiti da professionisti.



Hashing ripetuto e Salt 

Per proteggere le password degli utenti, soprattutto quelli che malauguratamente abbiano usato una password "semplice" o l'abbiano usata su più database si usano tecniche di key stretching e di salt.
Il key stretching consiste nel rafforzare la password inserita dall'utente applicando l'algoritmo di hash ripetutamente anziché una volta sola. In questo modo il confronto tra le hash dovrà prevedere che nel dizionario di ricerca dovrà essere presente la stessa password a cui è stato applicato l'algoritmo di hash lo stesso numero di volte. Questo comporta che quanto meno il database dovrà essere una quantità di dati tanto maggiore (linearmente) quante sono le volte in cui è stato applicato l'algoritmo.
Se si pensa che un algoritmo si può applicare facilmente 100 volte, questo può significare che l'attaccante dovrà avere un database 100 volte più grande e tempi 100 volte più lunghi.
Ovviamente sarà poi necessario ricordarsi quante volte bisogna applicare l'algoritmo di hashing, o in alternativa provarlo iterativamente sino ad un numero massimo di volte prefissato, quando si va a verificare la corrispondenza in fase di autenticazione dell'utente.
 
Un'altra tecnica che si utilizza, sempre per prevenire attacchi a dizionario, è di aggiungere, magari sparpagliati o in coda alla password dell'utente, dei caratteri random, il cosiddetto salt
Il salt è diverso da utente ad utente  e viene generato in maniera casuale ogni volta che è salvata una nuova password.
Tali caratteri sono aggiunti prima di applicare l'algoritmo di hashing, con il risultato da rendere non confrontabili le hash memorizzate sui database-dizionario. Se si pensa che l'aggiunta di un solo bit raddoppia le combinazioni, aggiungendo soli due caratteri si ottiene che le combinazioni possibili per ogni password (volendo provare tutti i salt collegati) sono 65 mila volte maggiori, e 4 miliardi di volte se aggiungiamo 4 byte di salt.
Se il database fosse quello a cui facevo riferimento prima, di 15 miliardi di hash, calcolare per ogni hash 4 miliardi di varianti sarebbe fisicamente impossibile.
Dunque è buona prassi, per rendere inefficaci gli attacchi dizionario, aggiungere dei byte di salt e memorizzare questi separatamente dalle password (stessa cosa nel numero di ripetizioni visto nel key stretching).
In questo modo, anche se il file delle password dovesse finire in mano ad un hacker, sarà impossibile per lui risalire alle password in chiaro se non munito anche del file dei salt e del numero di ripetizioni.


Idea: Pin come Salt

Il salt ed il numero di iterazioni andrebbero memorizzati, come detto, in un luogo diverso rispetto al database delle password.
Un'idea è di non memorizzarli affatto, e di utilizzare, in alternativa, un "pin" come quello richiesto nell'accesso da alcune banche. Ad esempio considerato un pin di 6 cifre, queste possono essere considerate il salt. In fase di login saranno chieste solo 3 cifre in posizioni random del pin, e sarà effettuato, per le restanti tre, un mini attacco brute force, di massimo 1000 tentativi, quindi abbastanza veloce da attuare. In questo modo si ottiene il vantaggio che un dizionario per includere quelle 6 cifre aggiuntive dovrebbe essere 1 milione di volte più grande, ad esempio il dizionario visto prima dovrebbe contenere invece di 15 miliardi 15 milioni di miliardi di hash e relative password,  cosa molto più costosa e fisicamente inattuabile (anche la dimensione, da qualche giga bytes, diverrebbe di petabytes ossia migliaia di terabytes).



Conclusioni

Abbiamo esaminato diverse tecniche di crittografia ed hashing, elencato i livelli di sicurezza di ognuno e annotato quelli ormai da non usare perché "craccati" o non più abbastanza sicuri. Abbiamo esaminato alcuni tipi di attacco e  le best practices di come memorizzare le password, oltre a proporre un modo di salvare il salt in modo sicuro (ossia non salvandolo affatto).
Si conclude ricordando che quando si usano delle password, non bisogna mai riusarle in più siti o applicazioni, e devono sempre essere di più di 10 caratteri, presi a caso tra maiuscoli, minuscoli, numeri, caratteri speciali, in modo da ampliare il set di caratteri che dovrà essere usato per un eventuale attacco brute force. Ad esempio la differenza tra 26^10 = 1.41 10^14  e 80^10 = 10^19 è già dell'ordine di 10^5.


Nessun commento:

Posta un commento

Refactoring: improving modularization

  Modularità In generale, per modularità si intende la misura in cui un sistema può essere decomposto e ricombinato. La modularizzazione è a...