Hashing securizat cu Python Hashlib

Acest ghid detaliat te va învăța cum să generezi hash-uri robuste folosind capacitățile încorporate ale modulului `hashlib` din Python.

Înțelegerea importanței hashing-ului și a modului în care poți calcula programatic hash-uri securizate este valoroasă, chiar dacă nu lucrezi direct în domeniul securității aplicațiilor. Dar de ce este important acest lucru?

În timpul proiectelor tale Python, te vei confrunta adesea cu situații în care stocarea parolelor și a altor date confidențiale în baze de date sau fișiere de cod sursă reprezintă o problemă. În astfel de cazuri, este mai sigur să aplici un algoritm de hashing pe datele sensibile și să stochezi hash-ul rezultat în locul informațiilor originale.

În acest tutorial, vom examina conceptul de hashing și diferența sa față de criptare. Vom explora, de asemenea, proprietățile funcțiilor hash sigure. Apoi, vom folosi algoritmi de hashing consacrați pentru a calcula hash-ul textului simplu în Python, folosind modulul `hashlib` încorporat.

Așadar, haideți să începem această călătorie captivantă!

Ce înseamnă hashing?

Hashing-ul este procesul prin care se transformă un șir de caractere (numit mesaj) într-o valoare de lungime fixă, denumită hash. Lungimea hash-ului rezultat este constantă pentru un anumit algoritm de hashing, indiferent de lungimea intrării. Dar cum diferă această operațiune de criptare?

În criptare, mesajul original (textul clar) este transformat într-o formă neinteligibilă (text criptat) folosind un algoritm specific. Apoi, textul criptat poate fi readus la forma sa originală prin aplicarea algoritmului de decriptare corespunzător.

Hashing-ul, în schimb, funcționează diferit. Am menționat deja că criptarea este un proces reversibil, permițând transformarea textului criptat în text clar și invers.

Spre deosebire de criptare, hashing-ul este un proces unidirecțional. Asta înseamnă că nu putem recupera mesajul original pornind de la hash-ul obținut.

Proprietățile esențiale ale funcțiilor hash

Să trecem în revistă câteva caracteristici importante pe care funcțiile hash trebuie să le îndeplinească:

  • Deterministe: Funcțiile hash sunt deterministe. Pentru același mesaj (m), hash-ul rezultat va fi întotdeauna identic.
  • Rezistent la preimagine: Am stabilit deja că hashing-ul nu este reversibil. Proprietatea de rezistență la preimagine stipulează că este imposibil să se determine mesajul original (m) pornind de la hash-ul rezultat.
  • Rezistent la coliziuni: Ar trebui să fie extrem de dificil (sau imposibil din punct de vedere computațional) să se găsească două mesaje diferite (m1 și m2) care să producă același hash. Această proprietate se numește rezistență la coliziuni.
  • Rezistent la a doua preimagine: Aceasta înseamnă că, având un mesaj m1 și hash-ul său corespunzător, este practic imposibil să se găsească un alt mesaj m2 care să producă același hash ca m1.

Modulul `hashlib` din Python

Modulul `hashlib` încorporat în Python oferă implementări ale multor algoritmi de hashing și rezumare a mesajelor, inclusiv algoritmi precum SHA și MD5.

Pentru a utiliza funcțiile și constructorii oferiti de modulul `hashlib`, îl poți importa în mediul tău de lucru astfel:

import hashlib

Modulul `hashlib` pune la dispoziție constantele `algorithms_available` și `algorithms_guaranteed`, care indică setul de algoritmi ale căror implementări sunt disponibile și, respectiv, garantate pe o anumită platformă.

Prin urmare, `algorithms_guaranteed` este un subset al lui `algorithms_available`.

Începe o sesiune interactivă Python (REPL), importă `hashlib` și accesează constantele `algorithms_available` și `algorithms_guaranteed`:

>>> hashlib.algorithms_available
# Output
{'md5', 'md5-sha1', 'sha3_256', 'shake_128', 'sha384', 'sha512_256', 'sha512', 'md4', 
'shake_256', 'whirlpool', 'sha1', 'sha3_512', 'sha3_384', 'sha256', 'ripemd160', 'mdc2', 
'sha512_224', 'blake2s', 'blake2b', 'sha3_224', 'sm3', 'sha224'}
>>> hashlib.algorithms_guaranteed
# Output
{'md5', 'shake_256', 'sha3_256', 'shake_128', 'blake2b', 'sha3_224', 'sha3_384', 
'sha384', 'sha256', 'sha1', 'sha3_512', 'sha512', 'blake2s', 'sha224'}

Se observă că `algorithms_guaranteed` este într-adevăr un subset al lui `algorithms_available`.

Cum se creează obiecte Hash în Python

Acum, să învățăm cum să generăm obiecte hash în Python. Vom calcula hash-ul SHA256 al unui șir de caractere folosind următoarele metode:

  • Constructorul generic `new()`.
  • Constructori specifici algoritmului.

Folosind constructorul `new()`.

Să inițializăm șirul de caractere:

>>> message = "tipstrick.ro is awesome!"

Pentru a instanția un obiect hash, putem utiliza constructorul `new()` și să specificăm numele algoritmului, după cum se arată:

>>> sha256_hash = hashlib.new("SHA256")

Acum, putem apela metoda `update()` pe obiectul hash, transmițând șirul de caractere ca argument:

>>> sha256_hash.update(message)

Dacă încerci acest lucru, vei întâmpina o eroare, deoarece algoritmii de hashing funcționează doar cu șiruri de octeți.

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Unicode-objects must be encoded before hashing

Pentru a obține șirul codificat, poți apela metoda `encode()` pe șirul de caractere și apoi să-l utilizezi în apelul metodei `update()`. După ce faci acest lucru, poți invoca metoda `hexdigest()` pentru a obține hash-ul sha256 corespunzător șirului de caractere.

sha256_hash.update(message.encode())
sha256_hash.hexdigest()
# Output:'b360c77de704ad8f02af963d7da9b3bb4e0da6b81fceb4c1b36723e9d6d9de3d'

În loc să codifici șirul de caractere folosind metoda `encode()`, poți să-l definești ca un șir de octeți, prefixând șirul cu `b`, astfel:

message = b"tipstrick.ro is awesome!"
sha256_hash.update(message)
sha256_hash.hexdigest()
# Output: 'b360c77de704ad8f02af963d7da9b3bb4e0da6b81fceb4c1b36723e9d6d9de3d'

Hash-ul obținut este identic cu hash-ul anterior, confirmând caracterul determinist al funcțiilor hash.

În plus, o mică modificare a șirului de caractere ar trebui să determine o schimbare semnificativă a hash-ului rezultat (cunoscut și sub numele de „efect de avalanșă”).

Pentru a verifica acest lucru, să înlocuim „a” din „awesome” cu „A” și să recalculăm hash-ul:

message = "tipstrick.ro is Awesome!"
h1 = hashlib.new("SHA256")
h1.update(message.encode())
h1.hexdigest()
# Output: '3c67f334cc598912dc66464f77acb71d88cfd6c8cba8e64a7b749d093c1a53ab'

Se poate observa că hash-ul s-a schimbat complet.

Folosind constructorul specific algoritmului

În exemplul precedent, am folosit constructorul generic `new()` și am specificat „SHA256” ca nume al algoritmului pentru a crea obiectul hash.

În loc de aceasta, putem utiliza direct constructorul `sha256()`, după cum se arată:

sha256_hash = hashlib.sha256()
message= "tipstrick.ro is awesome!"
sha256_hash.update(message.encode())
sha256_hash.hexdigest()
# Output: 'b360c77de704ad8f02af963d7da9b3bb4e0da6b81fceb4c1b36723e9d6d9de3d'

Hash-ul rezultat este identic cu cel obținut anterior pentru șirul de caractere „tipstrick.ro is awesome!”.

Explorarea atributelor obiectelor Hash

Obiectele hash au câteva atribute utile:

  • Atributul `digest_size` specifică dimensiunea digest-ului (hash-ului) în octeți. De exemplu, algoritmul SHA256 returnează un hash de 256 de biți, echivalent cu 32 de octeți.
  • Atributul `block_size` se referă la dimensiunea blocului utilizată în algoritmul de hashing.
  • Atributul `name` este numele algoritmului pe care îl putem folosi în constructorul `new()`. Cunoașterea valorii acestui atribut poate fi utilă atunci când obiectele hash nu au nume descriptive.

Putem verifica aceste atribute pentru obiectul `sha256_hash` creat anterior:

>>> sha256_hash.digest_size
32
>>> sha256_hash.block_size
64
>>> sha256_hash.name
'sha256'

În continuare, să explorăm câteva aplicații practice ale hashing-ului, folosind modulul `hashlib` din Python.

Exemple practice de hashing

Verificarea integrității software-ului și fișierelor

Ca dezvoltatori, descărcăm și instalăm pachete software în mod constant. Acest lucru este valabil indiferent dacă lucrăm pe o distribuție Linux, pe Windows sau macOS.

Cu toate acestea, anumite surse de descărcare pentru pachetele software ar putea să nu fie de încredere. Este recomandat să verifici hash-ul (sau suma de control) al fișierului descărcat, disponibil adesea alături de linkul de descărcare. Pentru a verifica integritatea software-ului descărcat, calculează hash-ul local al fișierului și compară-l cu hash-ul oficial.

Această tehnică se aplică și fișierelor de pe computer. Chiar și cea mai mică modificare a conținutului unui fișier va schimba hash-ul drastic. Astfel, verificarea hash-ului îți permite să stabilești dacă un fișier a fost modificat.

Iată un exemplu simplu. Creează un fișier text „my_file.txt” în directorul tău de lucru și adaugă conținutul de mai jos:

$ cat my_file.txt
This is a sample text file.
We are  going to compute the SHA256 hash of this text file and also
check if the file has been modified by
recomputing the hash.

Apoi, poți deschide fișierul în modul binar de citire („rb”), poți citi conținutul și poți calcula hash-ul SHA256, după cum se arată mai jos:

>>> import hashlib
>>> with open("my_file.txt","rb") as file:
...     file_contents = file.read()
...     sha256_hash = hashlib.sha256()
...     sha256_hash.update(file_contents)
...     original_hash = sha256_hash.hexdigest()

În acest caz, variabila `original_hash` stochează hash-ul fișierului „my_file.txt” în starea sa actuală.

>>> original_hash
# Output: '53bfd0551dc06c4515069d1f0dc715d002d451c8799add29f3e5b7328fda9f8f'

Acum, modifică fișierul „my_file.txt”. Poți elimina spațiul suplimentar dinaintea cuvântului „going”. 🙂

Calculează din nou hash-ul fișierului și stochează-l în variabila `computed_hash`.

>>> import hashlib
>>> with open("my_file.txt","rb") as file:
...     file_contents = file.read()
...     sha256_hash = hashlib.sha256()
...     sha256_hash.update(file_contents)
...     computed_hash = sha256_hash.hexdigest()

Apoi, poți adăuga o simplă instrucțiune `assert` care verifică dacă `computed_hash` este egal cu `original_hash`.

>>> assert computed_hash == original_hash

Dacă fișierul a fost modificat (ceea ce este valabil în acest caz), vei primi o `AssertionError`:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

Hashing-ul este deosebit de util atunci când stochezi informații sensibile, cum ar fi parolele, în baze de date. De asemenea, îl poți folosi în procesul de autentificare la baze de date. În timpul autentificării, verifici hash-ul parolei introduse față de hash-ul parolei corecte stocat.

Concluzie

Sper că acest tutorial te-a ajutat să înțelegi cum să generezi hash-uri sigure cu Python. Iată câteva concluzii cheie:

  • Modulul `hashlib` din Python oferă implementări gata de utilizare pentru o varietate de algoritmi de hashing. Poți obține lista algoritmilor disponibili pe platforma ta folosind `hashlib.algorithms_guaranteed`.
  • Pentru a crea un obiect hash, poți utiliza constructorul generic `new()`, folosind sintaxa: `hashlib.new(„nume algoritm”)`. Alternativ, poți utiliza direct constructorii corespunzători algoritmilor de hashing specifici, de exemplu: `hashlib.sha256()` pentru hash-ul SHA256.
  • După inițializarea șirului de caractere pe care dorești să-l hashezi și a obiectului hash corespunzător, poți apela metoda `update()` pe obiectul hash, urmată de metoda `hexdigest()` pentru a obține hash-ul final.
  • Hashing-ul este o tehnică utilă pentru verificarea integrității fișierelor și software-ului, pentru stocarea informațiilor sensibile în baze de date și pentru multe alte scopuri.

În continuare, poți învăța cum să creezi un generator de parole aleatorii în Python.