Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Python: cifrare file con password in modo sicuro

Utilizzare il modulo cryptography, ed alcuni opportuni accorgimenti, per cifrare (e decifrare) con password e in modo sicuro un file di testo.
Utilizzare il modulo cryptography, ed alcuni opportuni accorgimenti, per cifrare (e decifrare) con password e in modo sicuro un file di testo.
Link copiato negli appunti

Cifrare un file utilizzando una password è spesso molto utile, ed in effetti esistono moltissimi software in grado di effettuare questa operazione per noi. Ma cosa succede se vogliamo incorporare le funzionalità di cifratura all'interno di uno script Python su cui stiamo lavorando? Come spesso accade con questo linguaggio, non c'è nulla di più semplice.

In questo articolo di approfondimento, vedremo infatti come utilizzare il modulo cryptography, che si contraddistingue dalle altre opzioni disponibili per le seguenti caratteristiche:

  • è uno dei moduli più completi per la crittografia, fornendo sia primitive di basso livello che algoritmi crittografici completi (e, soprattutto, abbondantemente testati)
  • è supportato da una comunità molto attiva
  • il suo utilizzo è molto semplice, anche per i non esperti di crittografia e sicurezza informatica
  • è open source (il codice è interamente scaricabile da GitHub), caratteristica da non sottovalutare, soprattutto dal punto di vista della sicurezza, poiché garantisce la trasparenza degli algoritmi

In particolare, nel seguito partiremo dall'installazione di questo modulo, per poi vedere come utilizzarlo al fine di cifrare un file di testo.

Installazione

Il modulo più semplice per installare il modulo cryptography su Python è sfruttare il package manager pip, che in genere troviamo preinstallato, in quanto generalmente incluso nell'installer ufficiale. Per usare pip, apriamo un terminale, quindi digitiamo ed eseguiamo il comando seguente:

pip install cryptography

In alternativa, possiamo sempre fare riferimento alla documentazione ufficiale relativa all'installazione.

Possiamo verificare se l'installazione è andata a buon fine, avviando il terminale di Python ed importando il modulo, come segue:

import cryptography

Se tutto è andato bene, non resta che capire come eseguire la cifratura.

Cifratura e decifratura di un file

L'intero processo di cifratura che vedremo si basa sull'algoritmo Fernet, che di fatto è un'implementazione di AES128 in modalità CBC, con SHA256 utilizzato come algoritmo di hash per l'HMAC. Se tutte questi acronimi suonano sconosciuti, ci basti sapere che l'algoritmo è sufficientemente sicuro. Se invece vogliamo saperne di più, l'intera specifica dettagliata è disponibile a questo link.

L'implementazione di Fernet includa nel modulo cryptography può essere usata sfruttando la sintassi seguente:

from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher_suite = Fernet(key)
cipher_text = cipher_suite.encrypt(b"Messaggio da cifrare")
plain_text = cipher_suite.decrypt(cipher_text)

Il codice appena visto crea una chiave di cifratura casuale, sfruttando il metodo generate_key(). La chiave così generata è una sequenza di byte (e non una stringa, come qualcuno potrebbe erroneamente ipotizzare), che è quindi passata al costruttore della classe Fernet per generare l'oggetto cipher_suite, che utilizzeremo per la cifratura (e la decifratura) vera e propria: per farlo, ricorriamo rispettivamente ai metodi encrypt e decrypt. Si noti che il metodo encrypt accetta ancora una volta una sequenza di byte, e non una stringa. Vedremo più avanti come risolvere questa limitazione.

Fin qui sembra tutto semplice, ma ci sono alcuni dettagli che, sebbene possano sembra banali, possono complicare ulteriormente il codice. Vediamoli di seguito.

Scegliere una password

Nell'esempio precedente, abbiamo lasciato che il metodo generate_key() generasse una chiave per conto nostro. Ciò può essere comodo, ma in alcuni casi potrebbe non essere sufficiente. Supponiamo quindi di volere specificare noi una password. Per esempio, assumiamo che tale password sia la stringa "HTML.it".
Se proviamo ad utilizzarla direttamente come password, otterremo però un errore:

>>> f = Fernet(b"HTML.it")
Traceback (most recent call last):
  [...]
binascii.Error: Incorrect padding

Non tutte le password, infatti, sono pronte per essere utilizzate. Piuttosto, dobbiamo utilizzare una funzione di derivazione della chiave che trasformi la nostra password in un formato opportuno. Una delle possibilità suggerite dalla documentazione ufficiale consiste nell'uso della funzione PBKDF2HMAC. Definiamo quindi una funzione make_password come la seguente:

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def make_password(password, salt):
	kdf = PBKDF2HMAC(
		algorithm=hashes.SHA256(),
		length=32,
		salt=salt,
		iterations=100000,
		backend=default_backend()
	)
	return base64.urlsafe_b64encode(kdf.derive(password))

La funzione appena vista restituisce una chiave compatibile con le restrizioni richieste dall'implementazione di Fernet che stiamo utilizzando. Si noti che la funzione appena utilizzata ha 2 parametri: il primo rappresenta la nostra password (codificata come sequenza di byte), mentre il secondo parametro è il cosiddetto salt. L'utilizzo del salt non rende più sicuro l'algoritmo di cifratura, ma complica eventuali attacchi a dizionario, rendendo di fatto più sicura ed imprevedibile la nostra password. In pratica, oltre alla nostra password, avremo bisogno di conoscere sempre il salt, ad esempio inserendolo all'inizio del messaggio cifrato (cosa che sarà più chiara col prosieguo di questo articolo).

L'esempio seguente mostra la generazione di una password, sfruttando il modulo os per la generazione di un salt casuale:

import os
key = make_password(b"HTML.it", os.urandom(16))

Il codice visto all'inizio si modifica quindi come segue:

import os
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Omettiamo per brevità la definizione di make_password
key = make_password(b"HTML.it", os.urandom(16))
cipher_suite = Fernet(key)
cipher_text = cipher_suite.encrypt(b"Messaggio da cifrare")
plain_text = cipher_suite.decrypt(cipher_text)

Stringhe e byte

Abbiamo visto finora come generare una password, ma l'obiettivo è arrivare a cifrare un file di testo. Prima, però, vediamo come cifrare una stringa.

Innanzitutto, dobbiamo convertire la nostra stringa in una sequenza di byte. Per farlo sfruttiamo il metodo encode:

messaggio = ("Messaggio da cifrare").encode("utf-8")

L'operazione inversa (utile nel momento in cui dovremo memorizzare la sequenza di byte cifrata su un file) è invece la seguente:
messaggio = byte_seq.decode("utf-8")

Facciamo un breve passo indietro. Abbiamo detto che, oltre alla password (che rappresenta la nostra chiave personale e segreta), avremo bisogno anche di memorizzare il salt. Non abbiamo la necessità di offuscarlo, e possiamo in realtà memorizzarlo insieme al messaggio cifrato, ad esempio ponendolo prima dell'output della cifratura. Poiché, però, anche il salt è una sequenza di bit, dovremo prestare particolare attenzione alla fase di codifica mediante UTF-8. Inoltre, il fatto che il salt sia memorizzato insieme al messaggio cifrato significa che ad ogni decifratura dovremo anche tenere questa informazione in opportuna considerazione. Guardiamo quindi in che modo modifichiamo l'intero codice, tenendo conto di tutte queste note:

import os
import base64
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Omettiamo per brevità la definizione di make_password
messaggio = "Messaggio da cifrare"
password = b"HTML.it"
# Cifratura
salt = os.urandom(16)
key = make_password(password, salt)
cipher_suite = Fernet(key)
cipher_text = cipher_suite.encrypt(messaggio.encode("utf-8"))
cipher_text_utf8 = base64.b64encode(salt).decode('utf-8') + cipher_text.decode('utf-8')
# Decifratura
salt = base64.b64decode(cipher_text_utf8[:24].encode("utf-8"))
cipher_suite = Fernet(make_password(password, salt))
plain_text = cipher_suite.decrypt(cipher_text_utf8[24:].encode("utf-8"))
plain_text_utf8 = plain_text.decode("utf-8")

Analizziamo la parte di cifratura. Generiamo innanzitutto un salt casuale, e lo combiniamo con la nostra password per creare una chiave opportuna, passandola poi al costruttore Fernet. Quindi cifriamo il messaggio, dopo averlo trasformato in una sequenza di byte. Infine, codifichiamo anche il salt in UTF-8 (che quindi corrisponderà a 24 caratteri), e concateniamolo alla stringa di testo cifrato (anch'essa codificata come UTF-8).

Per effettuare una corretta decodifica, a questo punto, otteniamo innanzitutto il salt, ritrasformandolo nell'originale sequenza di byte come mostrato nel codice (sfruttando lo stesso modulo base64 utilizzato per l'operazione inversa). Si noti che, sebbene avevamo già generato il salt in precedenza, qui stiamo supponendo di doverlo ottenere nuovamente (e per questo motivo lo stiamo estraendo, anziché utilizzare il valore già memorizzato nella variabile salt). Otteniamo quindi la stessa chiave di cifratura precedentemente ottenuta, poiché sfruttiamo la stessa password e lo stesso salt. Decifriamo la sola porzione di stringa che non contiene il salt (sfruttando lo slicing), e convertiamo la sequenza di byte così ottenuta in una stringa UTF-8.

Cifratura e decifratura di un file

Compreso in che modo cifrare e decifrare una stringa, il passaggio ai file di testo è abbastanza immediato: basta sfruttare le funzionalità di gestione dei file disponibili di default con Python. Il codice seguente introduce esattamente questo tipo di aggiunzioni:

import os
import base64
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Omettiamo per brevità la definizione di make_password
password = b"HTML.it"
# Cifratura
# Il messaggio da cifrare è all'interno di un file:
with open("text.txt", "r") as file_to_encrypt:
    messaggio = file_to_encrypt.read()
salt = os.urandom(16)
key = make_password(password, salt)
cipher_suite = Fernet(key)
cipher_text = cipher_suite.encrypt(messaggio.encode("utf-8"))
cipher_text_utf8 = base64.b64encode(salt).decode('utf-8') + cipher_text.decode('utf-8')
# Scriviamo quindi il valore cifrato su un nuovo file:
encrypted_file = open("encrypted.txt", "w")
encrypted_file.write(cipher_text_utf8)
encrypted_file.close()
# Decifratura
# Decifriamo ora il file encrypted.txt:
with open("encrypted.txt", "r") as file_to_encrypt:
    messaggio = file_to_encrypt.read()
salt = base64.b64decode(cipher_text_utf8[:24].encode("utf-8"))
cipher_suite = Fernet(make_password(password, salt))
plain_text = cipher_suite.decrypt(cipher_text_utf8[24:].encode("utf-8"))
plain_text_utf8 = plain_text.decode("utf-8")
# Scriviamo quindi il testo decifrato sul file decrypted.txt:
encrypted_file = open("decrypted.txt", "w")
encrypted_file.write(plain_text_utf8)
encrypted_file.close()

Note conclusive

Quello visto fin qui è la base per poter cifrare file con Python. Ovviamente, però, cryptography non è il solo modulo che permette di fare questa cosa: altre possibilità sono infatti rappresentate da PyCrypto ed i GPGME Python bindings, ognuno dei quali implementa funzionalità leggermente diverse ma che permettono di arrivare a soluzioni praticamente simili a quella fin qui presentata. Le considerazioni a monte della scelta di cryptography restano però valide: il supporto da una comunità così attiva non è da sottovalutare, così come la facilità d'uso.

Il codice visto in questo articolo di approfondimento è disponibile su BitBucket.

Ti consigliamo anche