Сохраняющееся состояние hashlib

Я хотел бы создать экземпляр hashlib, update() его, а затем каким-то образом сохранить его состояние. Позже я хотел бы воссоздать объект, используя эти данные состояния, и продолжить его update(). Наконец, я хотел бы получить hexdigest() общего кумулятивного набора данных. Постоянство состояния должно сохраняться в течение нескольких прогонов.

Пример:

import hashlib
m = hashlib.sha1()
m.update('one')
m.update('two')
# somehow, persist the state of m here

#later, possibly in another process
# recreate m from the persisted state
m.update('three')
m.update('four')
print m.hexdigest()
# at this point, m.hexdigest() should be equal to hashlib.sha1().update('onetwothreefour').hextdigest()

ИЗМЕНИТЬ:

Я не нашел хорошего способа сделать это с помощью Python в 2010 году и в итоге написал небольшое вспомогательное приложение на C для достижения этой цели. Тем не менее, ниже есть несколько замечательных ответов, которые не были доступны или неизвестны мне в то время.


person anthony    schedule 25.01.2010    source источник
comment
можешь написать решение куда нибудь?   -  person EsseTi    schedule 17.01.2017
comment
@EsseTi, прошло много лет, но я помню, что мне удалось зафиксировать состояние SHA_CTX, а затем воссоздать контекст в аналогичном состоянии позже в другом процессе.   -  person anthony    schedule 18.01.2017


Ответы (5)


Вы можете сделать это с помощью ctypes, вспомогательное приложение на C не требуется:

rehash.py

#! /usr/bin/env python

''' A resumable implementation of SHA-256 using ctypes with the OpenSSL crypto library

    Written by PM 2Ring 2014.11.13
'''

from ctypes import *

SHA_LBLOCK = 16
SHA256_DIGEST_LENGTH = 32

class SHA256_CTX(Structure):
    _fields_ = [
        ("h", c_long * 8),
        ("Nl", c_long),
        ("Nh", c_long),
        ("data", c_long * SHA_LBLOCK),
        ("num", c_uint),
        ("md_len", c_uint)
    ]

HashBuffType = c_ubyte * SHA256_DIGEST_LENGTH

#crypto = cdll.LoadLibrary("libcrypto.so")
crypto = cdll.LoadLibrary("libeay32.dll" if os.name == "nt" else "libssl.so")

class sha256(object):
    digest_size = SHA256_DIGEST_LENGTH

    def __init__(self, datastr=None):
        self.ctx = SHA256_CTX()
        crypto.SHA256_Init(byref(self.ctx))
        if datastr:
            self.update(datastr)

    def update(self, datastr):
        crypto.SHA256_Update(byref(self.ctx), datastr, c_int(len(datastr)))

    #Clone the current context
    def _copy_ctx(self):
        ctx = SHA256_CTX()
        pointer(ctx)[0] = self.ctx
        return ctx

    def copy(self):
        other = sha256()
        other.ctx = self._copy_ctx()
        return other

    def digest(self):
        #Preserve context in case we get called before hashing is
        # really finished, since SHA256_Final() clears the SHA256_CTX
        ctx = self._copy_ctx()
        hashbuff = HashBuffType()
        crypto.SHA256_Final(hashbuff, byref(self.ctx))
        self.ctx = ctx
        return str(bytearray(hashbuff))

    def hexdigest(self):
        return self.digest().encode('hex')

#Tests
def main():
    import cPickle
    import hashlib

    data = ("Nobody expects ", "the spammish ", "imposition!")

    print "rehash\n"

    shaA = sha256(''.join(data))
    print shaA.hexdigest()
    print repr(shaA.digest())
    print "digest size =", shaA.digest_size
    print

    shaB = sha256()
    shaB.update(data[0])
    print shaB.hexdigest()

    #Test pickling
    sha_pickle = cPickle.dumps(shaB, -1)
    print "Pickle length:", len(sha_pickle)
    shaC = cPickle.loads(sha_pickle)

    shaC.update(data[1])
    print shaC.hexdigest()

    #Test copying. Note that copy can be pickled
    shaD = shaC.copy()

    shaC.update(data[2])
    print shaC.hexdigest()


    #Verify against hashlib.sha256()
    print "\nhashlib\n"

    shaD = hashlib.sha256(''.join(data))
    print shaD.hexdigest()
    print repr(shaD.digest())
    print "digest size =", shaD.digest_size
    print

    shaE = hashlib.sha256(data[0])
    print shaE.hexdigest()

    shaE.update(data[1])
    print shaE.hexdigest()

    #Test copying. Note that hashlib copy can NOT be pickled
    shaF = shaE.copy()
    shaF.update(data[2])
    print shaF.hexdigest()


if __name__ == '__main__':
    main()

resumable_SHA-256.py

#! /usr/bin/env python

''' Resumable SHA-256 hash for large files using the OpenSSL crypto library

    The hashing process may be interrupted by Control-C (SIGINT) or SIGTERM.
    When a signal is received, hashing continues until the end of the
    current chunk, then the current file position, total file size, and
    the sha object is saved to a file. The name of this file is formed by
    appending '.hash' to the name of the file being hashed.

    Just re-run the program to resume hashing. The '.hash' file will be deleted
    once hashing is completed.

    Written by PM 2Ring 2014.11.14
'''

import cPickle as pickle
import os
import signal
import sys

import rehash

quit = False

blocksize = 1<<16   # 64kB
blocksperchunk = 1<<8

chunksize = blocksize * blocksperchunk

def handler(signum, frame):
    global quit
    print "\nGot signal %d, cleaning up." % signum
    quit = True


def do_hash(fname, filesize):
    hashname = fname + '.hash'
    if os.path.exists(hashname):
        with open(hashname, 'rb') as f:
            pos, fsize, sha = pickle.load(f)
        if fsize != filesize:
            print "Error: file size of '%s' doesn't match size recorded in '%s'" % (fname, hashname)
            print "%d != %d. Aborting" % (fsize, filesize)
            exit(1)
    else:
        pos, fsize, sha = 0, filesize, rehash.sha256()

    finished = False
    with open(fname, 'rb') as f:
        f.seek(pos)
        while not (quit or finished):
            for _ in xrange(blocksperchunk):
                block = f.read(blocksize)
                if block == '':
                    finished = True
                    break
                sha.update(block)

            pos += chunksize
            sys.stderr.write(" %6.2f%% of %d\r" % (100.0 * pos / fsize, fsize))
            if finished or quit:
                break

    if quit:
        with open(hashname, 'wb') as f:
            pickle.dump((pos, fsize, sha), f, -1)
    elif os.path.exists(hashname):
        os.remove(hashname)

    return (not quit), pos, sha.hexdigest()


def main():
    if len(sys.argv) != 2:
        print "Resumable SHA-256 hash of a file."
        print "Usage:\npython %s filename\n" % sys.argv[0]
        exit(1)

    fname = sys.argv[1]
    filesize = os.path.getsize(fname)

    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGTERM, handler)

    finished, pos, hexdigest = do_hash(fname, filesize)
    if finished:
        print "%s  %s" % (hexdigest, fname)
    else:
        print "sha-256 hash of '%s' incomplete" % fname
        print "%s" % hexdigest
        print "%d / %d bytes processed." % (pos, filesize)


if __name__ == '__main__':
    main()

демонстрация

import rehash
import pickle
sha=rehash.sha256("Hello ")
s=pickle.dumps(sha.ctx)
sha=rehash.sha256()
sha.ctx=pickle.loads(s)
sha.update("World")
print sha.hexdigest()

вывод

a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e

Примечание. Я хотел бы поблагодарить PM2Ring за его замечательный код.

person Devesh Saini    schedule 10.06.2017
comment
Это феноменальный ответ и отличный пример использования ctypes. Спасибо. - person anthony; 12.06.2017
comment
В итоге я написал библиотеку, делающую что-то очень похожее: github.com/kislyuk/rehash - person weaver; 12.07.2017
comment
@antony Рад, что тебе понравилось. ;) FWIW, мой первоначальный ответ здесь - person PM 2Ring; 30.09.2017

hashlib.sha1 — это оболочка для библиотеки C, поэтому вы не сможете ее распарить.

Для доступа к внутреннему состоянию Python потребуется реализовать методы __getstate__ и __setstate__.

Вы можете использовать чистую реализацию Python для sha1, если она достаточно быстра для вашего требования

person John La Rooy    schedule 25.01.2010

Я тоже столкнулся с этой проблемой и не нашел существующего решения, поэтому в итоге я написал библиотеку, которая делает что-то очень похожее на то, что описал Девеш Сайни: https://github.com/kislyuk/rehash. Пример:

import pickle, rehash
hasher = rehash.sha256(b"foo")
state = pickle.dumps(hasher)

hasher2 = pickle.loads(state)
hasher2.update(b"bar")

assert hasher2.hexdigest() == rehash.sha256(b"foobar").hexdigest()
person weaver    schedule 12.07.2017


Вы можете легко создать объект-оболочку вокруг хеш-объекта, который может прозрачно сохранять данные.

Очевидным недостатком является то, что для восстановления состояния необходимо полностью сохранить хешированные данные, поэтому в зависимости от размера данных, с которыми вы имеете дело, это может не соответствовать вашим потребностям. Но он должен нормально работать до нескольких десятков МБ.

К сожалению, хеш-библиотека не предоставляет хэш-алгоритмы как правильные классы, а скорее предоставляет фабричные функции, которые создают хеш-объекты, поэтому мы не можем правильно подклассифицировать их без загрузки зарезервированных символов - ситуации, которую я бы предпочел избежать. Это означает только то, что вы должны создать свой класс-оболочку с самого начала, что в любом случае не является накладным расходом Python.

вот пример кода, который может даже удовлетворить ваши потребности:

import hashlib
from cStringIO import StringIO

class PersistentSha1(object):
    def __init__(self, salt=""):
        self.__setstate__(salt)

    def update(self, data):
        self.__data.write(data)
        self.hash.update(data)

    def __getattr__(self, attr):
        return getattr(self.hash, attr)

    def __setstate__(self, salt=""):
        self.__data = StringIO()
        self.__data.write(salt)
        self.hash = hashlib.sha1(salt)

    def __getstate__(self):
        return self.data

    def _get_data(self):
        self.__data.seek(0)
        return self.__data.read()

    data = property(_get_data, __setstate__)

Вы можете получить доступ к самому члену «данных», чтобы получить и установить состояние прямо, или вы можете использовать функции травления python:

>>> a = PersistentSha1()
>>> a
<__main__.PersistentSha1 object at 0xb7d10f0c>
>>> a.update("lixo")
>>> a.data
'lixo'
>>> a.hexdigest()
'6d6332a54574aeb35dcde5cf6a8774f938a65bec'
>>> import pickle
>>> b = pickle.dumps(a)
>>>
>>> c = pickle.loads(b)
>>> c.hexdigest()
'6d6332a54574aeb35dcde5cf6a8774f938a65bec'

>>> c.data
'lixo'
person jsbueno    schedule 25.01.2010
comment
Это хороший пример того, как создать класс, который можно выбрать, но хранить хэшированные данные не получится, он может быть огромным. Сам хэш-контекст крошечный, но кажется, что Python не может его раскрыть. - person anthony; 26.01.2010