I programmatori Python utilizzano l’hashing per trasformare i dati in ingresso in un valore di dimensioni fisse. Questo valore rappresenta i dati in modo univoco e la tecnica dell’hashing facilita la trasmissione e l’archiviazione sicura di varie forme di dati.

L’hashing protegge i dati da accessi non autorizzati e da manomissioni. È un ingrediente essenziale nei casi di utilizzo dell’integrità e della sicurezza dei dati.

Questo articolo esplora tutto ciò che c’è da sapere sull’hashing in Python, approfondisce gli usi dell’hashing e mette in evidenza i vari algoritmi di hashing che rendono il codice più efficiente, sicuro e affidabile.

Cos’è l’hashing in Python?

L’hashing converte i dati in ingresso, come una stringa, un file o un oggetto, in una stringa di byte di dimensioni fisse. L’hash o digest rappresenta l’input in modo unico e riproducibile.

L’hash gioca un ruolo importante nel rilevare la manipolazione dei dati e nel migliorare la sicurezza. Può calcolare un valore di hash per un file, un messaggio o altri dati. Un’applicazione memorizza l’hash in modo sicuro per verificare in seguito che i dati non siano stati manomessi.

Uno degli usi più comuni dell’hashing nella sicurezza è l’archiviazione delle password. L’hashing è una valida alternativa alla memorizzazione delle password in chiaro in un database. Quando un utente inserisce la propria password, il sistema esegue un hashing prima di memorizzarla nel database. Se un hacker accede al database, scoprirà che la password è difficile da rubare.

Le funzioni di hashing di Python rendono possibile tutto questo. Queste funzioni matematiche permettono a un’applicazione di manipolare i dati in valori hash.

Come creare una funzione di hashing efficace

Una funzione di hashing deve soddisfare i seguenti criteri per essere efficace e sicura:

  • Deterministica – Dato lo stesso input, la funzione deve restituire sempre lo stesso output.
  • Efficiente – Deve essere efficiente dal punto di vista computazionale nel calcolare il valore di hash di un dato input.
  • Resistente alle collisioni – La funzione deve ridurre al minimo la possibilità che due input producano lo stesso valore hash.
  • Uniforme – Gli output della funzione devono essere distribuiti uniformemente nell’intervallo dei possibili valori hash.
  • Non invertibile – È improbabile che un computer possa calcolare il valore di ingresso della funzione in base al valore hash.
  • Non prevedibile – Prevedere gli output della funzione deve essere difficile, dato un insieme di input.
  • Sensibile alle variazioni dell’input – La funzione deve essere sensibile a piccole differenze nell’input. Piccole variazioni dovrebbero causare una grande differenza nel valore hash risultante.

Casi d’uso dell’hashing

Una volta ottenuta una funzione di hashing adeguata con tutte queste caratteristiche, è possibile applicarla a diversi casi d’uso. Le funzioni di hashing funzionano bene per:

  • Memorizzazione di password – L’hashing è uno dei modi migliori per memorizzare le password degli utenti nei sistemi moderni. Python combina diversi moduli per eseguire l’hash e proteggere le password prima di archiviarle in un database.
  • Caching – L’hashing memorizza l’output di una funzione per risparmiare tempo quando la si richiama in seguito.
  • Recupero dei dati – Python utilizza una tabella hash con una struttura dati a dizionario integrata per recuperare rapidamente i valori in base alle chiavi.
  • Firme digitali – L’hashing può verificare l’autenticità dei messaggi con firma digitale.
  • Verifica dell’integrità dei file – L’hashing può verificare l’integrità di un file durante il trasferimento e il download.

La funzione di hashing integrata in Python

La funzione di hashing integrata in Python, hash(), restituisce un valore intero che rappresenta l’oggetto in ingresso. Il codice utilizza quindi il valore hash risultante per determinare la posizione dell’oggetto nella tabella hash. La tabella hash è una struttura dati che implementa dizionari e insiemi.

Il codice seguente mostra il funzionamento della funzione hash():

my_string = "hello world"

# Calculate the hash value of the string
hash_value = hash(my_string)

# Print the string and its hash value
print("String: ", my_string)
print("Hash value: ", hash_value)

Se salviamo questo codice in un file chiamato hash.py, possiamo eseguirlo (e vedere l’output) in questo modo:

% python3 hash.py
String:  hello world
Hash value:  2213812294562653681

Eseguiamolo di nuovo:

% python3 hash.py
String:  hello world
Hash value:  -631897764808734609

Il valore dell’hash è diverso quando viene invocato una seconda volta perché le versioni più recenti di Python (dalla 3.3 in poi), per impostazione predefinita, applicano un hash seed casuale per questa funzione. Il seed cambia ad ogni invocazione di Python. All’interno di una singola istanza, i risultati saranno identici.

Ad esempio, inseriamo questo codice nel nostro file hash.py:

my_string = "hello world"

# Calculate 2 hash values of the string
hash_value1 = hash(my_string)
hash_value2 = hash(my_string)

# Print the string and its hash values
print("String: ", my_string)
print("Hash value 1: ", hash_value1)
print("Hash value 2: ", hash_value2)

Quando viene eseguito, vediamo qualcosa di simile a questo:

String: hello world
Hash value 1:  -7779434013116951864
Hash value 2:  -7779434013116951864

Limiti dell’hash

Sebbene la funzione hash di Python sia promettente per diversi casi d’uso, le sue limitazioni la rendono inadatta per scopi di sicurezza. Ecco come:

  • Attacchi collisione – Una collisione si verifica quando due input diversi producono lo stesso valore hash. Un aggressore potrebbe utilizzare lo stesso metodo di creazione degli input per aggirare le misure di sicurezza che si basano sui valori hash per l’autenticazione o i controlli di integrità dei dati.
  • Dimensione limitata dell’input – Poiché le funzioni hash producono un output di dimensioni fisse indipendentemente dalle dimensioni dell’input, un input di dimensioni maggiori rispetto all’output della funzione hash può causare una collisione.
  • Prevedibilità – Una funzione hash dovrebbe essere deterministica e fornire lo stesso risultato ogni volta che si fornisce lo stesso input. Gli aggressori potrebbero sfruttare questa debolezza precompilando i valori hash per molti input e confrontandoli poi con gli hash dei valori target per trovare una corrispondenza. Questo processo è chiamato attacco rainbow table.

Per prevenire gli attacchi e mantenere i vostri dati al sicuro, utilizzate algoritmi di hashing sicuri progettati per resistere a queste vulnerabilità.

Usare hashlib per l’hashing sicuro in Python

Invece di usare l’algoritmo integrato in Python hash(), usate hashlib per un hashing più sicuro. Questo modulo Python offre una serie di algoritmi di hash per eseguire l’hashing dei dati in modo sicuro. Questi algoritmi includono MD5, SHA-1 e la famiglia SHA-2, più sicura, che comprende SHA-256, SHA-384, SHA-512 e altri.

MD5

L’algoritmo crittografico MD5, molto utilizzato, rivela un valore hash a 128 bit. Utilizza il codice seguente per generare un hash MD5 utilizzando il costruttore md5 di hashlib:

import hashlib

text = "Hello World"
hash_object = hashlib.md5(text.encode())
print(hash_object.hexdigest())

L’output di questo codice (nel nostro file hash.py ) sarà coerente tra le varie invocazioni:

b10a8db164e0754105b7a99be72e3fe5

Nota: il metodo hexdigest() nel codice sopra riportato restituisce l’hash in un formato esadecimale sicuro per qualsiasi presentazione non binaria (come le e-mail).

SHA-1

La funzione hash SHA-1 protegge i dati creando un valore hash di 160 bit. Usate il codice qui sotto con il costruttore sha1 per l’hash SHA-1 del modulo hashlib:

import hashlib

text = "Hello World"
hash_object = hashlib.sha1(text.encode())
print(hash_object.hexdigest())

L’output di questo codice:

0a4d55a8d778e5022fab701977c5d840bbc486d0

SHA-256

Esistono diverse opzioni di hash nella famiglia SHA-2. Il costruttore hashlib SHA-256 genera una versione più sicura di questa famiglia con un valore di hash a 256 bit.

I programmatori utilizzano spesso SHA-256 per la crittografia, come le firme digitali o i codici di autenticazione dei messaggi. Il codice seguente mostra come generare un hash SHA-256:

import hashlib

text = "Hello World"
hash_object = hashlib.sha256(text.encode())
print(hash_object.hexdigest())

L’output di questo codice:

a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e

SHA-384

SHA-384 è un valore hash a 384 bit. I programmatori utilizzano spesso la funzione SHA-384 nelle applicazioni che richiedono una maggiore sicurezza dei dati.

In base agli esempi precedenti, potete probabilmente intuire che questa è un’istruzione che genererà un hash SHA-384:

hash_object = hashlib.sha384(text.encode())

SHA-512

SHA-512 è il membro più sicuro della famiglia SHA-2. Crea un valore hash di 512 bit. I programmatori lo utilizzano per applicazioni ad alto rendimento, come la verifica dell’integrità dei dati. Il codice seguente mostra come generare un hash SHA-512 con il modulo hashlib di Python:

hash_object = hashlib.sha512(text.encode())

Come scegliere un algoritmo di hashing

Poiché questi algoritmi sono diversi, è buona norma scegliere l’algoritmo di hashing in base al caso d’uso e ai suoi requisiti di sicurezza. Ecco alcuni passi da seguire:

  • Capire il caso d’uso – Il caso d’uso determina il tipo di algoritmo da utilizzare. Ad esempio, quando si archiviano dati sensibili come le password, l’algoritmo di hashing deve proteggere dagli attacchi brute-force.
  • Considerare i propri requisiti di sicurezza – I requisiti di sicurezza del caso d’uso dipendono dal tipo di dati che si intende archiviare e determinano il tipo di algoritmo da scegliere. Ad esempio, un algoritmo di hashing robusto è il migliore per archiviare informazioni altamente sensibili.
  • Ricerca sugli algoritmi di hashing disponibili – Esplorare ogni tipo di hashing per comprenderne i punti di forza e di debolezza. Queste informazioni aiutano a selezionare l’opzione migliore per il proprio caso d’uso.
  • Valutare l’algoritmo di hashing selezionato – Una volta scelto un algoritmo di hashing, valutare se soddisfa i propri requisiti di sicurezza. Questo processo può comportare la verifica di attacchi o vulnerabilità note.
  • Implementare e testare l’algoritmo di hashing – Infine, implementare e testare a fondo l’algoritmo per assicurarsi che funzioni correttamente e in modo sicuro.

Come usare l’hashing per memorizzare le password

L’hashing ha un ottimo potenziale per l’archiviazione delle password, un componente critico della sicurezza informatica.

Idealmente, l’applicazione esegue l’hashing e archivia le password in un database sicuro per evitare accessi non autorizzati e violazioni dei dati. Tuttavia, l’hashing da solo potrebbe non essere sufficiente a proteggere le informazioni. Le password con hashing sono ancora suscettibili di attacchi di brute-force e dictionary. Gli hacker utilizzano comunemente queste pratiche per indovinare le password e ottenere un accesso non autorizzato agli account.

Un modo più sicuro di utilizzare l’hashing per la memorizzazione delle password è la tecnica del salting. Il salting aggiunge stringhe o caratteri unici e casuali a ogni password prima di eseguire l’hashing. Il salt è unico per ogni password e l’applicazione lo memorizza insieme alla password con hashing nel database.

Ogni volta che un utente accede, l’applicazione recupera il salt dal database, lo aggiunge alla password inserita e poi esegue l’hashing della combinazione di salt e password.

Se un malintenzionato riesce ad accedere al database, deve calcolare l’hash per ogni password e per ogni possibile valore del salt. Il salting rende questi attacchi più complessi, quindi è una tecnica utile per scoraggiare gli attacchi dictionary.

Il modulo secrets di Python semplifica il salting. Questo modulo genera salt casuali, memorizza in modo sicuro le password e gestisce token e chiavi crittografiche.

Il codice che segue utilizza la libreria hashlib e il modulo secrets per proteggere ulteriormente le password degli utenti:

import hashlib
import secrets

# Generate a random salt using the secrets module
salt = secrets.token_hex(16)

# Get the user's password from input
password = input("Enter your password: ")

# Hash the password using the salt and the SHA-256 algorithm
hash_object = hashlib.sha256((password + salt).encode())

# Get the hexadecimal representation of the hash
hash_hex = hash_object.hexdigest()

# Store the salt and hash_hex in your database

Come usare l’hashing per verificare l’integrità dei dati

L’hashing aiuta anche a verificare l’integrità dei dati e a proteggere i dati trasmessi da modifiche e manomissioni. Questa tecnica in quattro fasi utilizza una funzione hash crittografica per dare al file un valore hash univoco.

Per prima cosa, selezionate la funzione di hash appropriata e usatela per generare un valore di hash per i dati in ingresso. Memorizzate il valore di hash e poi usatelo per il confronto quando necessario. Ogni volta che sarà necessario verificare l’integrità dei dati, l’applicazione genererà il valore hash dei dati correnti utilizzando la stessa funzione hash. Quindi, l’applicazione confronterà il nuovo valore hash con quello memorizzato per verificare che siano identici. In caso affermativo, i dati non sono corrotti.

Il valore hash è unico e anche una minima modifica dei dati in ingresso genera un valore hash significativamente diverso. In questo modo è facile individuare eventuali cambiamenti o modifiche non autorizzate ai dati trasmessi.

I passi seguenti dimostrano l’utilizzo di una funzione hash per il controllo dell’integrità dei dati.

Passo 1: Importare il modulo hashlib

import hashlib

Passo 2: Utilizzare l’algoritmo hashlib

def generate_hash(file_path):

    # Open the file in binary mode
    with open(file_path, "rb") as f:

        # Read the contents of the file
        contents = f.read()

        # Generate the SHA-256 hash of the contents
        hash_object = hashlib.sha256(contents)

        # Return the hexadecimal representation of the hash
        return hash_object.hexdigest()

Passo 3: Chiamare la funzione e inserire il percorso del file

file_path = "path/to/my/file.txt"
hash_value = generate_hash(file_path)
print(hash_value)

Passo 4: Generare gli hash del file originale e del file trasmesso o modificato

# Generate the hash of the original file
original_file_path = "path/to/my/file.txt"
original_file_hash = generate_hash(original_file_path)

# Transmit or modify the file (for example, by copying it to a different location)
transmitted_file_path = "path/to/transmitted/file.txt"

# Generate the hash of the transmitted file
transmitted_file_hash = generate_hash(transmitted_file_path)

Passo 5: Confronto dei due hash

if original_file_hash == transmitted_file_hash:
    print("The file has not been tampered with")
else:
    print("The file has been tampered with")

Riepilogo

L’hashing è prezioso per l’integrità dei dati e la sicurezza delle password. Si può ottenere il massimo da una funzione di hashing quando si implementano tecniche di hashing sicure, come l’uso del modulo hashlib e il salting.

Queste tecniche aiutano a prevenire gli attacchi rainbow, gli attacchi di collisione e altre vulnerabilità di sicurezza che riguardano l’hashing. I programmatori utilizzano spesso queste tecniche con le funzioni di hashing in Python per garantire l’integrità dei dati dei file e memorizzare le password in modo sicuro.

Ora che avete imparato di più sulle tecniche di hashing in Python, usatele per migliorare la sicurezza delle vostre applicazioni. Troverete altri articoli su Python sul blog di Kinsta per saperne di più e poi poter prendere in considerazione la possibilità di distribuire la vostra prossima applicazione Python sulla piattaforma di Hosting di Applicazioni di Kinsta.

Steve Bonisteel Kinsta

Steve Bonisteel è un Technical Editor di Kinsta che ha iniziato la sua carriera di scrittore come giornalista della carta stampata, inseguendo ambulanze e camion dei pompieri. Dalla fine degli anni '90 si occupa di tecnologia legata a Internet.