Generowanie sumy kontrolnej MD5 pliku

348

Czy istnieje prosty sposób generowania (i sprawdzania) sum kontrolnych MD5 listy plików w Pythonie? (Mam mały program, nad którym pracuję i chciałbym potwierdzić sumy kontrolne plików).

Alexander
źródło
3
Dlaczego nie po prostu użyć md5sum?
kennytm,
99
Trzymanie go w Pythonie ułatwia zarządzanie zgodnością między platformami.
Alexander,
Jeśli potrzebujesz rozwiązania z „paskiem postępu * lub podobnym (dla bardzo dużych plików), rozważ to rozwiązanie: stackoverflow.com/questions/1131220/…
Laurent LAPORTE
1
@kennytm Podany przez ciebie link mówi to w drugim akapicie: „Podstawowy algorytm MD5 nie jest już uważany za bezpieczny” podczas opisu md5sum. Właśnie dlatego, moim zdaniem, programiści dbający o bezpieczeństwo nie powinni go używać.
Debug255,
1
@ Debug255 Dobry i ważny punkt. md5sumNależy unikać zarówno techniki opisanej w tym pytaniu SO - lepiej, jeśli to możliwe, użyć SHA-2 lub SHA-3: en.wikipedia.org/wiki/Secure_Hash_Algorytm
Per Lundberg

Odpowiedzi:

464

Możesz użyć hashlib.md5 ()

Pamiętaj, że czasami nie będziesz w stanie zmieścić całego pliku w pamięci. W takim przypadku musisz sekwencyjnie czytać fragmenty 4096 bajtów i podawać je do md5metody:

import hashlib
def md5(fname):
    hash_md5 = hashlib.md5()
    with open(fname, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()

Uwaga: hash_md5.hexdigest() zwróci reprezentację ciągu szesnastkowego dla skrótu, jeśli potrzebujesz tylko spakowanych bajtów return hash_md5.digest(), więc nie musisz konwertować z powrotem.

quantumSoup
źródło
297

Jest sposób, który jest dość nieefektywny pamięci .

pojedynczy plik:

import hashlib
def file_as_bytes(file):
    with file:
        return file.read()

print hashlib.md5(file_as_bytes(open(full_path, 'rb'))).hexdigest()

lista plików:

[(fname, hashlib.md5(file_as_bytes(open(fname, 'rb'))).digest()) for fname in fnamelst]

Przypomnijmy jednak, że MD5 jest znany jako uszkodzony i nie powinien być wykorzystywany do żadnych celów, ponieważ analiza podatności może być naprawdę trudna, a analiza ewentualnego przyszłego wykorzystania kodu w celu zapewnienia bezpieczeństwa jest niemożliwa. IMHO powinien być całkowicie usunięty z biblioteki, aby każdy, kto go używa, był zmuszony do aktualizacji. Oto, co powinieneś zrobić zamiast tego:

[(fname, hashlib.sha256(file_as_bytes(open(fname, 'rb'))).digest()) for fname in fnamelst]

Jeśli chcesz tylko 128 bitów skrótu, możesz to zrobić .digest()[:16].

To da ci listę krotek, każda krotka zawiera nazwę swojego pliku i jego skrót.

Ponownie mocno kwestionuję twoje użycie MD5. Powinieneś przynajmniej używać SHA1, i biorąc pod uwagę ostatnie błędy odkryte w SHA1 , prawdopodobnie nawet nie to. Niektórzy uważają, że dopóki nie używasz MD5 do celów „kryptograficznych”, nic ci nie jest. Ale rzeczy mają zwykle szerszy zakres, niż początkowo się spodziewasz, a Twoja przypadkowa analiza podatności może okazać się całkowicie wadliwa. Najlepiej jest po prostu przyzwyczaić się do używania właściwego algorytmu z bramki. Wystarczy wpisać inną wiązkę liter. To nie jest takie trudne.

Oto sposób, który jest bardziej złożony, ale wydajny pod względem pamięci :

import hashlib

def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
    for block in bytesiter:
        hasher.update(block)
    return hasher.hexdigest() if ashexstr else hasher.digest()

def file_as_blockiter(afile, blocksize=65536):
    with afile:
        block = afile.read(blocksize)
        while len(block) > 0:
            yield block
            block = afile.read(blocksize)


[(fname, hash_bytestr_iter(file_as_blockiter(open(fname, 'rb')), hashlib.md5()))
    for fname in fnamelst]

I znowu, ponieważ MD5 jest zepsuty i tak naprawdę nie powinien być nigdy używany:

[(fname, hash_bytestr_iter(file_as_blockiter(open(fname, 'rb')), hashlib.sha256()))
    for fname in fnamelst]

Ponownie możesz [:16]po zakończeniu rozmowy ustawić, hash_bytestr_iter(...)jeśli chcesz tylko 128 bitów wartości.

Wszelaki
źródło
66
Używam tylko MD5, aby potwierdzić, że plik nie jest uszkodzony. Nie martwię się tak bardzo, że zostanie zepsuta.
Alexander,
87
@TheLifelessOne: I pomimo @Omnifarious przerażających ostrzeżeń, jest to całkowicie dobre wykorzystanie MD5.
Prezydent James K. Polk,
22
@GregS, @TheLifelessOne - Tak, i następną rzeczą, o której wiesz, że ktoś znajdzie sposób na wykorzystanie tego faktu w Twojej aplikacji, aby spowodować, że plik zostanie zaakceptowany jako nieuszkodzony, jeśli nie jest to plik, którego się w ogóle spodziewasz. Nie, stoję przy moich przerażających ostrzeżeniach. Myślę, że MD5 powinien zostać usunięty lub zawierać ostrzeżenia o wycofaniu.
Wszechobecny
10
Prawdopodobnie użyłbym .hexdigest () zamiast .digest () - ludzie łatwiej czytają - co jest celem OP.
zbstof
21
Użyłem tego rozwiązania, ale niepoprawnie dał ten sam skrót dla dwóch różnych plików pdf. Rozwiązaniem było otwarcie plików przez określenie trybu binarnego, tj .: [(fname, hashlib.md5 (open (fname, 'rb' ) .read ()). Hexdigest ()) dla fname w fnamelst] Jest to bardziej powiązane do funkcji otwartej niż md5, ale pomyślałem, że przydałoby się to zgłosić, biorąc pod uwagę powyższy wymóg zgodności między platformami (patrz także: docs.python.org/2/tutorial/... ).
BlueCoder
34

Najwyraźniej nie dodam niczego zasadniczo nowego, ale dodałem tę odpowiedź, zanim osiągnąłem status komentowania, a regiony kodu wyjaśniają wszystko - w każdym razie, w szczególności, aby odpowiedzieć na pytanie @ Nemo z odpowiedzi Omnifariousa:

Zdarzyło mi się trochę zastanowić nad sumami kontrolnymi (konkretnie przyjechałem tutaj, szukając sugestii dotyczących rozmiarów bloków) i odkryłem, że ta metoda może być szybsza, niż można się było spodziewać. Biorąc najszybszy (ale dość typowy) timeit.timeitlub /usr/bin/timewynik z każdej z kilku metod sumowania pliku o wielkości ok. 11 MB:

$ ./sum_methods.py
crc32_mmap(filename) 0.0241742134094
crc32_read(filename) 0.0219960212708
subprocess.check_output(['cksum', filename]) 0.0553209781647
md5sum_mmap(filename) 0.0286180973053
md5sum_read(filename) 0.0311000347137
subprocess.check_output(['md5sum', filename]) 0.0332629680634
$ time md5sum /tmp/test.data.300k
d3fe3d5d4c2460b5daacc30c6efbc77f  /tmp/test.data.300k

real    0m0.043s
user    0m0.032s
sys     0m0.010s
$ stat -c '%s' /tmp/test.data.300k
11890400

Wygląda więc na to, że zarówno plik Python, jak i / usr / bin / md5sum zajmuje około 30 ms dla pliku 11 MB. Odpowiednia md5sumfunkcja ( md5sum_readw powyższej liście) jest bardzo podobna do funkcji Omnifarious:

import hashlib
def md5sum(filename, blocksize=65536):
    hash = hashlib.md5()
    with open(filename, "rb") as f:
        for block in iter(lambda: f.read(blocksize), b""):
            hash.update(block)
    return hash.hexdigest()

To prawda, że ​​pochodzą z pojedynczych przebiegów ( mmap te są zawsze o kilka razy szybsze, gdy wykonuje się co najmniej kilkadziesiąt przebiegów), a mój zwykle dostaje dodatkowy f.read(blocksize)po wyczerpaniu bufora, ale jest dość powtarzalny i pokazuje, że md5sumw wierszu poleceń jest niekoniecznie szybsze niż implementacja Pythona ...

EDYCJA: Przepraszam za duże opóźnienie, nie patrzyłem na to od jakiegoś czasu, ale aby odpowiedzieć na pytanie @ EdRandall, napiszę implementację Adler32. Nie uruchomiłem jednak testów porównawczych. Jest to w zasadzie to samo, co byłby CRC32: zamiast wywołań init, update i skrót wszystko jest zlib.adler32()połączeniem:

import zlib
def adler32sum(filename, blocksize=65536):
    checksum = zlib.adler32("")
    with open(filename, "rb") as f:
        for block in iter(lambda: f.read(blocksize), b""):
            checksum = zlib.adler32(block, checksum)
    return checksum & 0xffffffff

Zauważ, że musi to zaczynać się od pustego łańcucha, ponieważ sumy Adlera rzeczywiście różnią się, gdy zaczynają się od zera w porównaniu do ich sumy "", czyli 1- CRC może 0zamiast tego zacząć . ANDJest potrzebne -ing, aby to 32-bitowa liczba całkowita bez znaku, który zapewnia, że zwróci tę samą wartość całej wersjach Pythona.

rsandwick3
źródło
Czy mógłbyś dodać kilka linii porównujących SHA1, a także może zlib.adler32?
Ed Randall,
1
Powyższa funkcja md5sum () zakłada, że ​​masz dostęp do zapisu do pliku. Jeśli zamienisz „r + b” w wywołaniu open () na „rb”, to zadziała dobrze.
Kevin Lyda
1
@EdRandall: adler32 naprawdę nie jest wart zawracania sobie głowy np. leviathansecurity.com/blog/analysis-of-adler32
MikeW
6

W Python 3.8+ możesz to zrobić

import hashlib
with open("your_filename.txt", "rb") as f:
    file_hash = hashlib.md5()
    while chunk := f.read(8192):
        file_hash.update(chunk)

print(file_hash.digest())
print(file_hash.hexdigest())  # to get a printable str instead of bytes

Rozważyć użycie hashlib.blake2bzamiast md5(wystarczy wymienić md5ze blake2bw powyższym fragmencie). Jest kryptograficznie bezpieczny i szybszy niż MD5.

Boris
źródło
:=Operator jest ona "przydział" (New Python 3.8+); pozwala przypisać wartości do większego wyrażenia; więcej informacji tutaj: docs.python.org/3/whatsnew/3.8.html#assignment-expressions
Benjamin
0
hashlib.md5(pathlib.Path('path/to/file').read_bytes()).hexdigest()
Johnson
źródło
3
Cześć! Dodaj wyjaśnienie do swojego kodu, dlaczego jest to rozwiązanie problemu. Co więcej, ten post jest dość stary, więc powinieneś również dodać trochę informacji, dlaczego twoje rozwiązanie dodaje coś, czego inni jeszcze nie zajęli.
d_kennetz
1
To kolejny nieefektywny sposób na pamięć
Erik Aronesty
-2

Myślę, że poleganie na pakiecie invoke i pliku binarnym md5sum jest nieco wygodniejsze niż podproces lub pakiet md5

import invoke

def get_file_hash(path):

    return invoke.Context().run("md5sum {}".format(path), hide=True).stdout.split(" ")[0]

To oczywiście zakłada, że ​​masz invoke i zainstalowany md5sum.

Puchatek
źródło
3
Jeśli pathjest to ścieżka podana przez użytkownika, pozwoli to każdemu użytkownikowi wykonać dowolne polecenia bash w twoim systemie.
Boris