Generando una suma de comprobación MD5 de un archivo

348

¿Hay alguna forma simple de generar (y verificar) sumas de verificación MD5 de una lista de archivos en Python? (Tengo un pequeño programa en el que estoy trabajando y me gustaría confirmar las sumas de verificación de los archivos).

Alejandro
fuente
3
¿Por qué no solo usar md5sum?
kennytm
99
Mantenerlo en Python facilita la administración de la compatibilidad multiplataforma.
Alexander
Si desea una solución con "barra de progreso * o similar (para archivos muy grandes), considere esta solución: stackoverflow.com/questions/1131220/…
Laurent LAPORTE
1
@kennytm El enlace que proporcionó dice esto en el segundo párrafo: "El algoritmo MD5 subyacente ya no se considera seguro" durante la descripción md5sum. Es por eso que los programadores conscientes de la seguridad no deberían usarlo en mi opinión.
Debug255
1
@ Debug255 Punto bueno y válido. md5sumDeben evitarse ambas y la técnica descrita en esta pregunta SO: es mejor usar SHA-2 o SHA-3, si es posible: en.wikipedia.org/wiki/Secure_Hash_Algorithms
Según Lundberg el

Respuestas:

462

Puede usar hashlib.md5 ()

Tenga en cuenta que a veces no podrá ajustar todo el archivo en la memoria. En ese caso, tendrá que leer fragmentos de 4096 bytes secuencialmente y alimentarlos al md5método:

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

Nota: hash_md5.hexdigest() devolverá la representación de cadena hexadecimal para el resumen, si solo necesita el uso de bytes empaquetados return hash_md5.digest(), para que no tenga que volver a convertir.

quantumSoup
fuente
297

Hay una manera que es bastante ineficiente de memoria .

archivo único:

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

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

lista de archivos:

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

Recordemos, sin embargo, que MD5 se sabe roto y no debe ser utilizado para cualquier propósito ya que el análisis de la vulnerabilidad puede ser muy difícil, y el análisis de cualquier posible uso futuro de su código podría ser condenado a las cuestiones de seguridad es imposible. En mi humilde opinión, debe eliminarse completamente de la biblioteca para que todos los que lo usen se vean obligados a actualizar. Entonces, esto es lo que debes hacer en su lugar:

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

Si solo quieres 128 bits de digestión, puedes hacerlo .digest()[:16] .

Esto le dará una lista de tuplas, cada tupla contiene el nombre de su archivo y su hash.

Nuevamente, cuestiono mucho su uso de MD5. Al menos debería usar SHA1, y dados los defectos recientes descubiertos en SHA1 , probablemente ni siquiera eso. Algunas personas piensan que mientras no esté usando MD5 para propósitos 'criptográficos', está bien. Pero las cosas tienden a tener un alcance más amplio de lo que inicialmente esperaba, y su análisis de vulnerabilidad casual puede resultar completamente defectuoso. Es mejor acostumbrarse a usar el algoritmo correcto desde el principio. Simplemente escribir un grupo diferente de letras es todo. No es tan dificil.

Aquí hay una manera que es más compleja, pero eficiente en memoria :

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]

Y, de nuevo, dado que MD5 está roto y ya no debería usarse nunca más:

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

Una vez más, puede poner [:16]después de la llamada a hash_bytestr_iter(...)si solo desea un resumen de 128 bits.

De todo género
fuente
66
Solo estoy usando MD5 para confirmar que el archivo no está dañado. No me preocupa que se rompa.
Alexander
87
@TheLifelessOne: Y a pesar de @Omnifarious advertencias de miedo, ese es un uso perfectamente bueno de MD5.
Presidente James K. Polk
22
@GregS, @TheLifelessOne - Sí, y lo siguiente que sabes es que alguien encuentra una manera de usar este hecho sobre tu aplicación para hacer que un archivo sea aceptado como no dañado cuando no es el archivo que estás esperando. No, mantengo mis advertencias de miedo. Creo que MD5 debería eliminarse o venir con advertencias de desaprobación.
Omnifarious
10
Probablemente usaría .hexdigest () en lugar de .digest (), es más fácil de leer para los humanos, que es el propósito de OP.
zbstof
21
Usé esta solución, pero incorrectamente dio el mismo hash para dos archivos pdf diferentes. La solución fue abrir los archivos especificando el modo binario, es decir: [(fname, hashlib.md5 (open (fname, 'rb' ) .read ()). Hexdigest ()) para fname en fnamelst] Esto está más relacionado a la función abierta que md5, pero pensé que podría ser útil informarlo dado el requisito de compatibilidad multiplataforma mencionado anteriormente (ver también: docs.python.org/2/tutorial/… ).
BlueCoder
34

Claramente, no agrego nada fundamentalmente nuevo, pero agregué esta respuesta antes de comentar el estado, además de que las regiones de código aclaran las cosas, de todos modos, específicamente para responder a la pregunta de @ Nemo de la respuesta de Omnifarious:

Estaba pensando un poco en las sumas de comprobación (vine aquí buscando sugerencias sobre tamaños de bloque, específicamente), y descubrí que este método puede ser más rápido de lo que cabría esperar. Tomando el más rápido (pero bastante típico) timeit.timeito el /usr/bin/timeresultado de cada uno de varios métodos de suma de verificación de un archivo de aprox. 11MB:

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

Por lo tanto, parece que tanto Python como / usr / bin / md5sum tardan unos 30 ms en un archivo de 11 MB. La md5sumfunción relevante ( md5sum_readen la lista anterior) es bastante similar a la de 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()

De acuerdo, estos son de ejecuciones individuales (las mmapque siempre son un poco más rápidas cuando se realizan al menos unas pocas docenas de ejecuciones), y la mía generalmente tiene un extra f.read(blocksize)después de que se agota el búfer, pero es razonablemente repetible y muestra que md5sumen la línea de comando está no necesariamente más rápido que una implementación de Python ...

EDITAR: Perdón por el largo retraso, no he visto esto en algún tiempo, pero para responder a la pregunta de @ EdRandall, escribiré una implementación de Adler32. Sin embargo, no he ejecutado los puntos de referencia para ello. Básicamente es lo mismo que habría sido el CRC32: en lugar de las llamadas init, update y digest, todo es una zlib.adler32()llamada:

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

Tenga en cuenta que esto debe comenzar con la cadena vacía, ya que las sumas de Adler realmente difieren al comenzar desde cero versus su suma para "", que es 1- CRC puede comenzar con su 0lugar. losAND -ing es necesario para convertirlo en un entero sin signo de 32 bits, lo que garantiza que devuelve el mismo valor en todas las versiones de Python.

rsandwick3
fuente
¿Podría agregar un par de líneas comparando SHA1 y quizás también zlib.adler32?
Ed Randall
1
La función md5sum () anterior supone que tiene acceso de escritura al archivo. Si reemplaza "r + b" en la llamada abierta () con "rb", funcionará bien.
Kevin Lyda
1
@EdRandall: no vale la pena molestarse con adler32, por ejemplo. leviathansecurity.com/blog/analysis-of-adler32
MikeW
6

En Python 3.8+ puedes hacer

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

Considere usar en hashlib.blake2blugar de md5(solo reemplace md5con blake2ben el fragmento anterior). Es criptográficamente seguro y más rápido que MD5.

Boris
fuente
El :=operador es un "operador de asignación" (nuevo en Python 3.8+); le permite asignar valores dentro de una expresión más grande; Más información aquí: docs.python.org/3/whatsnew/3.8.html#assignment-expressions
Benjamin
0
hashlib.md5(pathlib.Path('path/to/file').read_bytes()).hexdigest()
johnson
fuente
3
¡Hola! Agregue alguna explicación a su código de por qué esta es una solución al problema. Además, esta publicación es bastante antigua, por lo que también debe agregar información sobre por qué su solución agrega algo que los demás aún no han abordado.
d_kennetz
1
Es otra forma ineficiente de memoria
Erik Aronesty
-2

Creo que confiar en el paquete invoke y el binario md5sum es un poco más conveniente que el subproceso o el paquete md5

import invoke

def get_file_hash(path):

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

Por supuesto, esto supone que tiene invoke y md5sum instalados.

Puchatek
fuente
3
Si pathes una ruta proporcionada por el usuario, esto permitirá que cualquier usuario ejecute comandos bash arbitrarios en su sistema.
Boris