Génération d'une somme de contrôle MD5 d'un fichier

348

Existe-t-il un moyen simple de générer (et de vérifier) ​​les sommes de contrôle MD5 d'une liste de fichiers en Python? (J'ai un petit programme sur lequel je travaille, et je voudrais confirmer les sommes de contrôle des fichiers).

Alexandre
la source
3
Pourquoi ne pas simplement utiliser md5sum?
kennytm
99
Le conserver en Python facilite la gestion de la compatibilité multiplateforme.
Alexander
Si vous voulez une solution avec "barre de progression * ou similaire (pour les très gros fichiers), pensez à cette solution: stackoverflow.com/questions/1131220/…
Laurent LAPORTE
1
@kennytm Le lien que vous avez fourni dit ceci dans le deuxième paragraphe: "L'algorithme MD5 sous-jacent n'est plus considéré comme sécurisé" lors de la description md5sum. C'est pourquoi les programmeurs soucieux de la sécurité ne devraient pas l'utiliser à mon avis.
Debug255
1
@ Debug255 Bon et valide point. Les deux md5sumet la technique décrite dans cette question SO doivent être évitées - il est préférable d'utiliser SHA-2 ou SHA-3, si possible: en.wikipedia.org/wiki/Secure_Hash_Algorithms
Per Lundberg

Réponses:

463

Vous pouvez utiliser hashlib.md5 ()

Notez que parfois, vous ne pourrez pas mettre le fichier entier en mémoire. Dans ce cas, vous devrez lire séquentiellement des morceaux de 4096 octets et les alimenter à la md5méthode:

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()

Remarque: hash_md5.hexdigest() renverra la représentation de la chaîne hexadécimale pour le résumé, si vous avez juste besoin de l'utilisation des octets compressés return hash_md5.digest(), vous n'avez donc pas à reconvertir.

quantumSoup
la source
297

Il y a un moyen qui est assez inefficace en mémoire .

un seul fichier:

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

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

liste des fichiers:

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

Rappelez-vous cependant que MD5 est connu comme cassé et ne doit pas être utilisé à quelque fin que ce soit, car l'analyse de vulnérabilité peut être très délicate, et l'analyse de toute utilisation future possible de votre code pour des problèmes de sécurité est impossible. À mon humble avis, il devrait être retiré de la bibliothèque pour que tous ceux qui l'utilisent soient obligés de mettre à jour. Alors, voici ce que vous devriez faire à la place:

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

Si vous ne voulez que 128 bits de résumé, vous pouvez le faire .digest()[:16].

Cela vous donnera une liste de tuples, chaque tuple contenant le nom de son fichier et son hachage.

Encore une fois, je remets fortement en question votre utilisation de MD5. Vous devriez au moins utiliser SHA1, et compte tenu des failles récentes découvertes dans SHA1 , probablement même pas. Certaines personnes pensent que tant que vous n'utilisez pas MD5 à des fins «cryptographiques», tout va bien. Mais les choses ont tendance à avoir une portée plus large que ce à quoi vous vous attendiez initialement, et votre analyse de vulnérabilité occasionnelle peut s'avérer complètement erronée. Il est préférable de prendre l'habitude d'utiliser le bon algorithme dès le départ. Il suffit de taper un groupe de lettres différent, c'est tout. Ce n'est pas si dur.

Voici un moyen plus complexe, mais efficace en mémoire :

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]

Et, encore une fois, puisque MD5 est cassé et ne devrait plus jamais être utilisé:

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

Encore une fois, vous pouvez mettre [:16]après l'appel à hash_bytestr_iter(...)si vous ne voulez que 128 bits de résumé.

Très varié
la source
66
J'utilise uniquement MD5 pour confirmer que le fichier n'est pas corrompu. Je ne suis pas tellement préoccupé par le fait qu'il soit cassé.
Alexander
87
@TheLifelessOne: Et malgré les avertissements effrayants @Omnifarious, c'est une très bonne utilisation de MD5.
président James K. Polk,
22
@GregS, @TheLifelessOne - Oui, et la prochaine chose que vous savez que quelqu'un trouve un moyen d'utiliser ce fait sur votre application pour faire accepter un fichier comme non corrompu quand ce n'est pas le fichier que vous attendez du tout. Non, je m'en tiens à mes avertissements effrayants. Je pense que MD5 devrait être supprimé ou accompagné d'avertissements de dépréciation.
Omnifarious
10
J'utiliserais probablement .hexdigest () au lieu de .digest () - c'est plus facile à lire pour les humains - ce qui est le but de l'OP.
zbstof
21
J'ai utilisé cette solution mais elle a incorrectement donné le même hachage pour deux fichiers pdf différents. La solution consistait à ouvrir les fichiers en spécifiant le mode binaire, c'est-à-dire: [(fname, hashlib.md5 (open (fname, 'rb' ) .read ()) hexdigest ()) for fname in fnamelst] à la fonction ouverte que md5 mais j'ai pensé qu'il pourrait être utile de le signaler étant donné l'exigence de compatibilité multiplateforme énoncée ci-dessus (voir aussi: docs.python.org/2/tutorial/… ).
BlueCoder
34

Je n'ajoute clairement rien de fondamentalement nouveau, mais j'ai ajouté cette réponse avant que je ne sois en train de commenter le statut, plus les régions de code rendent les choses plus claires - de toute façon, spécifiquement pour répondre à la question de @ Nemo à partir de la réponse d'Omnifarious:

Je pensais un peu aux sommes de contrôle (je suis venu ici à la recherche de suggestions sur la taille des blocs, en particulier) et j'ai constaté que cette méthode pouvait être plus rapide que ce à quoi vous vous attendiez. Prendre le plus rapide (mais assez typique)timeit.timeit ou le /usr/bin/timerésultat de chacune des méthodes de somme de contrôle d'un fichier d'env. 11 Mo:

$ ./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

Ainsi, il semble que Python et / usr / bin / md5sum prennent environ 30 ms pour un fichier de 11 Mo. Le pertinentmd5sum fonction ( md5sum_readdans la liste ci-dessus) est assez similaire à celle d'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()

Certes, ceux-ci proviennent de cycles uniques ( mmapceux-ci sont toujours plus rapides quand au moins quelques dizaines de cycles sont effectués), et le mien a généralement un supplément f.read(blocksize)après que le tampon est épuisé, mais il est raisonnablement répétable et montre que md5sumsur la ligne de commande est pas nécessairement plus rapide qu'une implémentation Python ...

EDIT: Désolé pour le long retard, je n'ai pas regardé cela depuis un certain temps, mais pour répondre à la question de @ EdRandall, je vais écrire une implémentation Adler32. Cependant, je n'ai pas exécuté les repères pour cela. C'est fondamentalement le même que le CRC32 aurait été: au lieu des appels init, update et digest, tout est unzlib.adler32() appel:

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

Notez que cela doit commencer avec la chaîne vide, car les sommes d'Adler diffèrent en effet lors du démarrage de zéro par rapport à leur somme pour "", ce qui est 1- CRC peut commencer à la 0place. Le AND-ing est nécessaire pour en faire un entier non signé 32 bits, ce qui garantit qu'il renvoie la même valeur sur toutes les versions de Python.

rsandwick3
la source
Pourriez-vous éventuellement ajouter quelques lignes comparant SHA1, et aussi zlib.adler32 peut-être?
Ed Randall
1
La fonction md5sum () ci-dessus suppose que vous disposez d'un accès en écriture au fichier. Si vous remplacez "r + b" dans l'appel open () par "rb" cela fonctionnera très bien.
Kevin Lyda
1
@EdRandall: adler32 ne vaut vraiment pas la peine d'être dérangé, par exemple. leviathansecurity.com/blog/analysis-of-adler32
MikeW
6

Dans Python 3.8+, vous pouvez faire

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

Pensez à utiliser hashlib.blake2b place de md5(remplacez simplement md5par blake2bdans l'extrait ci-dessus). Il est cryptographiquement sécurisé et plus rapide que MD5.

Boris
la source
L' :=opérateur est un "opérateur d'affectation" (nouveau dans Python 3.8+); il vous permet d'affecter des valeurs à l'intérieur d'une expression plus grande; plus d'informations ici: docs.python.org/3/whatsnew/3.8.html#assignment-expressions
Benjamin
0
hashlib.md5(pathlib.Path('path/to/file').read_bytes()).hexdigest()
Johnson
la source
3
Salut! Veuillez ajouter des explications à votre code pour expliquer pourquoi il s'agit d'une solution au problème. En outre, ce message est assez ancien, vous devez donc également ajouter des informations sur les raisons pour lesquelles votre solution ajoute quelque chose que les autres n'ont pas déjà traité.
d_kennetz
1
C'est une autre manière inefficace de mémoire
Erik Aronesty
-2

Je pense que compter sur le paquet invoke et le binaire md5sum est un peu plus pratique que le sous-processus ou le paquet md5

import invoke

def get_file_hash(path):

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

Cela suppose bien sûr que vous ayez invoqué et md5sum installé.

Puchatek
la source
3
S'il paths'agit d'un chemin fourni par l'utilisateur, cela permettra à tout utilisateur d'exécuter des commandes bash arbitraires sur votre système.
Boris