Python实现SM3国密哈希算法:从原理到代码实战

Python实现SM3国密哈希算法:从原理到代码实战

1. 项目概述:为什么要在Python里实现SM3?

如果你接触过密码学或者国内的软件开发,大概率听说过MD5、SHA-256这些哈希算法。它们就像是数据的“指纹生成器”,能把任意长度的信息(比如一个文件、一段密码)压缩成一段固定长度的、看起来像乱码的字符串。这个“指纹”有两个核心特性:一是几乎不可能从“指纹”反推出原始数据(单向性),二是哪怕原始数据只改动一个标点,生成的“指纹”也会天差地别(雪崩效应)。

SM3算法,就是在这个领域里,我们国家密码管理局发布的国家标准密码杂凑算法。你可以把它理解为“中国版的SHA-256”。它同样输出256位(32字节)的哈希值,在安全性设计上对标国际主流算法,并且广泛应用于国内的电子认证、区块链、物联网安全等需要商用密码资质的场景。

那么,为什么我们要用Python来实现它呢?原因很直接:掌控力、学习性和实用性。虽然Python标准库没有内置SM3,很多第三方库(比如gmssl)也提供了现成的实现,但亲手实现一遍,是理解其精妙设计最扎实的方式。你能彻底搞懂每一轮压缩、每一次位运算背后的逻辑,这对于从事安全开发、密码学研究的工程师来说,是内功的修炼。其次,一个纯Python的实现,意味着极致的可移植性,无需编译依赖,在任何有Python环境的地方都能运行,对于轻量级集成、教学演示或特定受限环境非常友好。最后,这个过程本身也是对Python位运算、字节处理能力的一次深度实战。

接下来,我会带你从零开始,拆解SM3的每一个步骤,并用纯Python代码将其实现。我们不仅会写出能跑的代码,更会深入每一个“为什么”,让你知其然,更知其所以然。

2. SM3算法核心原理深度拆解

在动手写代码之前,我们必须先吃透SM3的“图纸”。SM3算法处理数据的核心过程可以概括为:消息填充 -> 消息扩展 -> 迭代压缩。整个流程就像一个精密的加工流水线。

2.1 消息填充:让数据变得“规整”

哈希算法通常以固定大小的“块”为单位进行处理,SM3的块大小是512位(64字节)。但我们的输入数据长度是任意的,第一步就是要通过填充,让数据的长度满足长度 % 512 = 448。换句话说,填充后,数据的最后64位(8字节)要留出来存放原始数据的长度。

填充规则具体如下:

  1. 补1和补0:在原始消息的比特流末尾,先补上一个比特1,然后补上k个比特0k是满足(原始消息长度 + 1 + k) ≡ 448 (mod 512)的最小非负整数。简单理解,就是先加个1,然后加足够多的0,直到长度满足“除以512余448”。
  2. 附加长度:在补位完成后,再附加上一个64位的比特串,该比特串是原始消息长度(以比特为单位)的二进制表示。如果原始消息长度超过2^64比特,则仅使用该长度的低64位。

注意:这里的“长度”单位是比特(bit),而不是字节(byte)。这是很多初学者容易混淆的地方。一个ASCII字符是1字节=8比特。

举个例子:假设我们对字符串"abc"(长度3字节=24比特)进行SM3哈希。

  1. 原始消息:01100001 01100010 01100011(24 bits)
  2. 1...01100011 1
  3. 计算需要补0的个数:我们需要24 + 1 + k ≡ 448 (mod 512)24+1=25448-25=423,但423不满足k最小非负的要求。实际上,我们要找的是下一个满足条件的数。512 * n + 448。当n=1时,总长为512+448=960。所以需要补0的个数k = 960 - 24 -1 = 9350
  4. 附加长度:原始长度24比特的二进制表示(64位)是0...00011000(前面59个0)。将其附加在末尾。

最终,填充后的消息长度就是960比特,可以被512整除(960/512=1余448,实际上这里是一整块后的剩余,严格说填充后总长是960+64=1024比特,即两个512比特块)。理解这个过程对后续的字节级操作至关重要。

2.2 消息扩展:将一块数据“发酵”出更多原料

对于一个512位的消息分组B,SM3算法会将其扩展生成132个32位字(W0W67以及W0'W63')。这些字将在后续的压缩函数中被大量使用。扩展的目的在于消除输入数据的局部特征,增强雪崩效应。

扩展步骤:

  1. 将512位的分组B划分为16个32位字,记为W0, W1, ..., W15
  2. 对于j从16到67,按以下公式生成新的WjWj = P1( W_{j-16} XOR W_{j-9} XOR (W_{j-3} <<< 15) ) XOR (W_{j-13} <<< 7) XOR W_{j-6}其中,<<<表示循环左移,P1是一个置换函数:P1(X) = X XOR (X <<< 15) XOR (X <<< 23)
  3. 对于j从0到63,计算Wj' = Wj XOR W_{j+4}

这个过程就像是用最初的16种原料,通过一套固定的、复杂的化学反应(异或、循环移位、置换),生成132种新的中间产物,为后续的“压缩”环节提供丰富的素材。

2.3 迭代压缩:核心的哈希计算引擎

这是算法最核心的部分。SM3使用一个256位的中间状态,称为链接变量,通常由8个32位寄存器A, B, C, D, E, F, G, H表示。初始值IV是一个固定的常量。

对于每一个512位的消息分组:

  1. 将当前的链接变量ABCDEFGH赋值给临时变量SS1, SS2, TT1, TT2等。
  2. 进行64轮迭代运算(j从0到63)。每一轮都会使用到上一步消息扩展产生的WjWj',以及一个固定的常量Tj(前16轮和后48轮的Tj值不同)。
  3. 每一轮的核心是压缩函数,它包含大量的位运算(与、或、非、异或)、模加运算(2^32模加)和循环移位。压缩函数会更新临时变量的值。
  4. 64轮之后,将本轮计算得到的(A, B, C, D, E, F, G, H)与本轮初始的链接变量进行模加,结果作为处理下一个消息分组时的初始链接变量。

压缩函数中的关键组件:

  • 布尔函数FFjGGj:根据轮数j的不同,选择不同的逻辑函数。它们作用于B, C, DF, G, H,是提供非线性特性的关键。
    • FFj(X,Y,Z) = (X & Y) | ((~X) & Z), 当0 <= j <= 15
    • FFj(X,Y,Z) = (X & Y) | (X & Z) | (Y & Z), 当16 <= j <= 63
    • GGj函数类似,定义不同。
  • 置换函数P0P0(X) = X XOR (X <<< 9) XOR (X <<< 17)。在压缩过程中用于进一步打乱数据。

当所有消息分组都处理完毕后,最终得到的ABCDEFGH这8个32位字拼接起来,就是计算出的256位SM3哈希值,通常表示为64位的十六进制字符串。

3. Python实现的关键技术与细节

理解了原理,我们就可以用Python来搭建这个流水线了。Python实现的核心挑战在于精确的位运算大整数处理。Python的整数没有固定位宽,我们需要通过掩码(& 0xffffffff)来模拟32位无符号整数的溢出(模2^32加法)。

3.1 常量、函数与辅助工具的实现

首先,我们定义算法中所有固定的“零件”。

# sm3.py # 初始链接变量 IV IV = [ 0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600, 0xA96F30BC, 0x163138AA, 0xE38DEE4D, 0xB0FB0E4E ] # 常量 Tj:前16轮和后48轮不同 def T(j): if 0 <= j <= 15: return 0x79CC4519 else: # 16 <= j <= 63 return 0x7A879D8A # 置换函数 P0 和 P1 def P0(x): return x ^ left_rotate(x, 9) ^ left_rotate(x, 17) def P1(x): return x ^ left_rotate(x, 15) ^ left_rotate(x, 23) # 布尔函数 FFj 和 GGj def FF(j, x, y, z): if 0 <= j <= 15: return x ^ y ^ z else: # 16 <= j <= 63 return (x & y) | (x & z) | (y & z) def GG(j, x, y, z): if 0 <= j <= 15: return x ^ y ^ z else: # 16 <= j <= 63 return (x & y) | ((~x) & z) # 32位循环左移函数,这是位操作的基础 def left_rotate(x, n): # 确保x是32位无符号整数 x &= 0xFFFFFFFF return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF # 模2^32加法 def add_mod_2_32(a, b): return (a + b) & 0xFFFFFFFF

实操心得left_rotate函数是实现中的基石。这里有一个关键细节:(x << n)可能导致Python整数超过32位,所以必须在最后用& 0xFFFFFFFF进行掩码操作,确保结果始终在32位范围内。add_mod_2_32函数同理,所有中间状态的加法都必须通过这个函数进行,以模拟硬件上的溢出行为。

3.2 消息填充的字节级实现

这是第一个需要细致处理的环节。我们需要在字节层面(而非比特层面)实现填充规则。

def sm3_padding(message): """ 对字节串消息进行SM3填充。 参数: message -- bytes类型,原始消息 返回: bytes类型,填充后的消息 """ # 原始消息长度(单位:比特) bit_length = len(message) * 8 # 转换为64位大端序字节串 length_bytes = bit_length.to_bytes(8, byteorder='big') # 计算需要填充的字节 # 先补一个比特1(即字节0x80),然后补0,直到满足 (len + 9) % 64 == 56 # 公式推导:填充后总字节数 % 64 == 56。因为最后要加8字节长度,所以数据部分需满足 (数据字节数) % 64 == 56 # 数据字节数 = 原始长度 + 1(补位字节) + k(补0字节)。设原始长度 = L 字节。 # 则有 (L + 1 + k) % 64 == 56 => k = (55 - L) % 64 # 但更直观的做法是动态构造: padding = b'\x80' # 补1,即二进制10000000 # 补0,直到满足 (len(message) + len(padding) + 8) % 64 == 0 # 等价于 (len(message) + len(padding)) % 64 == 56 while (len(message) + len(padding)) % 64 != 56: padding += b'\x00' # 附加长度 padded_message = message + padding + length_bytes return padded_message

注意事项:这里最容易出错的是字节序(Endianness)。SM3标准规定附加的长度是大端序(Big-Endian),即高位字节在前。to_bytes(8, byteorder='big')确保了这一点。如果你错误地使用了小端序,最终的哈希值将是错误的。

3.3 消息扩展与压缩函数的实现

这是算法的计算核心,代码逻辑直接对应原理。

def sm3_compress(block, current_hash): """ 压缩一个512位(64字节)的消息分组。 参数: block -- bytes类型,长度必须为64字节 current_hash -- list of 8 ints,当前的链接变量 返回: list of 8 ints,更新后的链接变量 """ # 1. 消息扩展 W = [0] * 68 W_ = [0] * 64 # 将block划分为16个32位字(大端序解析) for i in range(16): start = i * 4 W[i] = int.from_bytes(block[start:start+4], byteorder='big') # 生成W16到W67 for j in range(16, 68): W[j] = P1( W[j-16] ^ W[j-9] ^ left_rotate(W[j-3], 15) ) ^ left_rotate(W[j-13], 7) ^ W[j-6] # 确保32位 W[j] &= 0xFFFFFFFF # 生成W0'到W63' for j in range(64): W_[j] = W[j] ^ W[j+4] W_[j] &= 0xFFFFFFFF # 2. 迭代压缩 A, B, C, D, E, F, G, H = current_hash for j in range(64): # 计算中间变量SS1, SS2, TT1, TT2 # 注意:所有中间运算都要进行模2^32加法 SS1_temp = add_mod_2_32(left_rotate(A, 12), E) SS1_temp = add_mod_2_32(SS1_temp, left_rotate(T(j), j % 32)) SS1 = left_rotate(SS1_temp, 7) SS2 = SS1 ^ left_rotate(A, 12) TT1_temp = add_mod_2_32(FF(j, A, B, C), D) TT1_temp = add_mod_2_32(TT1_temp, SS2) TT1_temp = add_mod_2_32(TT1_temp, W_[j]) TT1 = TT1_temp & 0xFFFFFFFF # 实际上add_mod_2_32已保证 TT2_temp = add_mod_2_32(GG(j, E, F, G), H) TT2_temp = add_mod_2_32(TT2_temp, SS1) TT2_temp = add_mod_2_32(TT2_temp, W[j]) TT2 = TT2_temp & 0xFFFFFFFF # 更新寄存器 D = C C = left_rotate(B, 9) B = A A = TT1 H = G G = left_rotate(F, 19) F = E E = P0(TT2) # 3. 与本轮初始哈希值模加,得到本轮结果 new_hash = [ add_mod_2_32(A, current_hash[0]), add_mod_2_32(B, current_hash[1]), add_mod_2_32(C, current_hash[2]), add_mod_2_32(D, current_hash[3]), add_mod_2_32(E, current_hash[4]), add_mod_2_32(F, current_hash[5]), add_mod_2_32(G, current_hash[6]), add_mod_2_32(H, current_hash[7]), ] return new_hash

踩坑记录:在迭代压缩的循环中,寄存器的更新顺序必须严格按照D=C, C=rot(B), B=A, A=TT1, H=G, G=rot(F), F=E, E=P0(TT2)进行。这是一个链式更新,顺序错了,整个状态就全乱了。我最初实现时曾错误地先更新了A再将其赋值给B,导致结果完全对不上。最好的调试方法是找一个官方测试向量,单步跟踪第一轮计算中每一个中间变量的值。

3.4 主流程整合与最终调用

最后,我们将所有部分串联起来,形成一个完整的、用户友好的哈希函数。

def sm3_hash(message): """ 计算字节串消息的SM3哈希值。 参数: message -- bytes类型,原始消息 返回: str类型,64位的十六进制哈希字符串 """ # 1. 消息填充 padded_msg = sm3_padding(message) # 2. 初始化链接变量 current_hash = IV.copy() # 注意使用副本,避免修改全局IV # 3. 迭代处理每个512位分组 total_blocks = len(padded_msg) // 64 for i in range(total_blocks): block = padded_msg[i*64:(i+1)*64] current_hash = sm3_compress(block, current_hash) # 4. 将最终的8个32位整数转换为十六进制字符串并拼接 result = ''.join(f'{x:08x}' for x in current_hash) return result # 提供一个对字符串更友好的接口 def sm3_hash_string(s, encoding='utf-8'): """计算字符串的SM3哈希值""" message_bytes = s.encode(encoding) return sm3_hash(message_bytes)

4. 完整代码、测试与验证

将上述所有代码片段整合到一个sm3.py文件中,我们就得到了一个完整的纯Python SM3实现。

完整的sm3.py文件:

# sm3.py - 纯Python实现的SM3哈希算法 # 常量定义 IV = [0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600, 0xA96F30BC, 0x163138AA, 0xE38DEE4D, 0xB0FB0E4E] def T(j): return 0x79CC4519 if 0 <= j <= 15 else 0x7A879D8A def left_rotate(x, n): x &= 0xFFFFFFFF return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF def P0(x): return x ^ left_rotate(x, 9) ^ left_rotate(x, 17) def P1(x): return x ^ left_rotate(x, 15) ^ left_rotate(x, 23) def FF(j, x, y, z): if 0 <= j <= 15: return x ^ y ^ z else: return (x & y) | (x & z) | (y & z) def GG(j, x, y, z): if 0 <= j <= 15: return x ^ y ^ z else: return (x & y) | ((~x) & z) def add_mod_2_32(a, b): return (a + b) & 0xFFFFFFFF def sm3_padding(message): bit_length = len(message) * 8 length_bytes = bit_length.to_bytes(8, byteorder='big') padding = b'\x80' while (len(message) + len(padding)) % 64 != 56: padding += b'\x00' return message + padding + length_bytes def sm3_compress(block, current_hash): W = [0] * 68 W_ = [0] * 64 for i in range(16): start = i * 4 W[i] = int.from_bytes(block[start:start+4], byteorder='big') for j in range(16, 68): W[j] = P1(W[j-16] ^ W[j-9] ^ left_rotate(W[j-3], 15)) ^ left_rotate(W[j-13], 7) ^ W[j-6] W[j] &= 0xFFFFFFFF for j in range(64): W_[j] = W[j] ^ W[j+4] W_[j] &= 0xFFFFFFFF A, B, C, D, E, F, G, H = current_hash for j in range(64): SS1 = left_rotate(add_mod_2_32(add_mod_2_32(left_rotate(A, 12), E), left_rotate(T(j), j % 32)), 7) SS2 = SS1 ^ left_rotate(A, 12) TT1 = add_mod_2_32(add_mod_2_32(add_mod_2_32(FF(j, A, B, C), D), SS2), W_[j]) TT2 = add_mod_2_32(add_mod_2_32(add_mod_2_32(GG(j, E, F, G), H), SS1), W[j]) D = C C = left_rotate(B, 9) B = A A = TT1 H = G G = left_rotate(F, 19) F = E E = P0(TT2) return [add_mod_2_32(current_hash[i], [A, B, C, D, E, F, G, H][i]) for i in range(8)] def sm3_hash(message): padded_msg = sm3_padding(message) current_hash = IV.copy() total_blocks = len(padded_msg) // 64 for i in range(total_blocks): block = padded_msg[i*64:(i+1)*64] current_hash = sm3_compress(block, current_hash) return ''.join(f'{x:08x}' for x in current_hash) def sm3_hash_string(s, encoding='utf-8'): return sm3_hash(s.encode(encoding)) if __name__ == '__main__': # 测试代码 test_vectors = [ ("", "1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b"), ("abc", "66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0"), ("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", "debe9ff92275b8a138604889c18e5a4d6fdb70e5387e5765293dcba39c0c5732"), ] print("SM3 算法实现自测:") all_passed = True for msg, expected in test_vectors: if msg == "": digest = sm3_hash(b"") else: digest = sm3_hash_string(msg) passed = digest == expected all_passed &= passed status = "✓" if passed else "✗" print(f" {status} '{msg[:20]}{'...' if len(msg)>20 else ''}'") if not passed: print(f" 期望: {expected}") print(f" 得到: {digest}") if all_passed: print("\n所有标准测试向量通过!") else: print("\n部分测试未通过,请检查实现。")

4.1 如何验证实现的正确性?

运行上面的脚本,它会使用SM3官方的标准测试向量进行自检。这三个测试用例分别是空字符串、"abc"和一段较长的重复字符串。如果你的实现输出与预期的哈希值一致,那么恭喜你,核心算法实现基本正确。

更全面的测试方法:

  1. 随机性测试:用脚本生成大量随机字符串,计算哈希,观察输出是否看起来是随机的(可以使用hashlibsha256作为参照,但值不同)。
  2. 雪崩效应测试:修改输入的一个比特,观察输出哈希值中发生变化的比特数是否接近50%(约128比特)。这可以验证算法的扩散特性。
  3. 与权威库对比:安装gmssl库(pip install gmssl),用它的SM3实现与你的实现进行交叉验证。
    from gmssl import sm3 import hashlib # 使用gmssl msg = b"Hello, SM3!" h1 = sm3.sm3_hash(msg) # 使用我们的实现 h2 = sm3_hash(msg) print(f"gmssl: {h1}") print(f"我们的: {h2}") print(f"结果一致: {h1 == h2}")

5. 性能优化、常见问题与实战应用

一个可用的实现完成了,但作为一个追求极致的开发者,我们还得看看怎么让它跑得更快,以及在实际中会遇到哪些坑。

5.1 性能瓶颈分析与优化思路

纯Python实现的SM3,其性能瓶颈是显而易见的:密集的位运算和循环。在sm3_compress函数中,64轮迭代,每轮涉及数十次位运算和模加,对于大文件哈希,这会成为主要耗时点。

优化策略:

  1. 使用内置int操作:Python的int位运算在C层面实现,已经非常快。我们的实现已经利用了这一点。避免使用列表存储单个比特,始终以32位整数为单位操作。
  2. 预计算常量T(j)left_rotate(T(j), j%32)可以在循环外预计算好,存入列表,避免在64轮循环中重复计算。
  3. 局部变量缓存:在压缩函数的循环内,将频繁使用的函数(如add_mod_2_32left_rotate)赋值给局部变量,可以减少全局查找的开销。
  4. 使用arraymemoryview处理字节:对于非常大的消息,使用memoryview可以避免在切片时复制字节数据,减少内存开销。
  5. 终极方案:C扩展或Cython:如果对性能有极致要求,可以将核心的压缩函数用C语言重写,编译为Python扩展模块。或者使用Cython,给Python代码加上静态类型声明,编译后能获得接近原生C的性能。这是hashlib中算法实现的方式。

一个简单的预计算优化示例:

# 在模块初始化时预计算 _TJ_ROT = [left_rotate(T(j), j % 32) for j in range(64)] # 在sm3_compress函数中,替换原来的计算 # SS1_temp = add_mod_2_32(left_rotate(A, 12), E) # SS1_temp = add_mod_2_32(SS1_temp, left_rotate(T(j), j % 32)) # 原来这行 # 改为: SS1_temp = add_mod_2_32(add_mod_2_32(left_rotate(A, 12), E), _TJ_ROT[j])

5.2 常见问题与调试技巧

在实现和使用的过程中,你可能会遇到以下问题:

问题1:哈希结果与标准值或其它库不一致。这是最常见的问题。请按以下顺序排查:

  • 步骤1:检查消息填充。这是错误的重灾区。确认:
    • 补位字节是b'\x80'(二进制10000000)吗?
    • 附加的长度是原始消息的比特长度吗?
    • 长度编码是64位大端序吗?
    • 你可以打印出填充后消息的十六进制,与已知正确的实现进行逐字节对比。
  • 步骤2:检查消息扩展。计算第一个分组扩展后的W[16]W'[0],与标准中间值对比。
  • 步骤3:单步调试压缩函数。找到SM3标准文档(如GMT 0004-2012)中的中间值示例。在j=0轮迭代结束后,对比你的A, B, C, D, E, F, G, H寄存器值是否与文档一致。这是最有效的定位方法。
  • 步骤4:检查所有运算的32位限制。确保每一次加法、每一次循环左移的结果,都通过& 0xFFFFFFFF进行了掩码。Python不会自动溢出,这是与C实现最大的不同。

问题2:处理大文件时内存占用高。我们的实现需要将整个文件读入内存进行填充。对于超大文件,可以优化为流式处理:

def sm3_hash_file(filepath, chunk_size=8192): """流式计算文件的SM3哈希""" # 初始化 current_hash = IV.copy() total_bit_length = 0 # 模拟填充:我们需要知道总长度,所以需要先读取文件大小,或者分两遍处理。 # 更实用的方法是先获取文件大小,然后分块更新。 import os file_size = os.path.getsize(filepath) total_bit_length = file_size * 8 with open(filepath, 'rb') as f: # 处理除最后一块外的所有完整块 remaining = file_size while remaining > 64: # 留出至少一个块的空间用于处理填充 block = f.read(64) current_hash = sm3_compress(block, current_hash) remaining -= 64 # 处理最后一块(包含填充和长度) last_block_data = f.read(remaining) # 手动构造最后一个分组的填充 # ... (此处需要根据剩余字节数手动构造填充块,逻辑稍复杂) # 更简单但低效的方式:还是将整个文件读入内存。对于学习目的,已足够。 # 简化起见,大文件哈希建议使用优化后的库。

对于生产环境,建议直接使用gmssl等成熟库,它们已经做好了流式处理和性能优化。

问题3:Unicode字符串哈希结果与某些在线工具不一致。这通常是编码问题。SM3的输入是字节串。字符串"中文"用UTF-8编码和GBK编码得到的字节串完全不同,哈希值自然不同。确保你与对比工具使用相同的字符编码(通常是UTF-8)。我们的sm3_hash_string函数默认使用UTF-8。

5.3 实战应用场景

自己实现的SM3有什么用?除了学习,它可以在以下场景发挥作用:

  1. 轻量级集成:在没有gmssl等外部依赖的环境(如某些受限的嵌入式Python环境、在线代码沙箱)中,需要SM3功能。
  2. 教学与演示:用于密码学课程、内部技术分享,代码透明,每一步都可追溯。
  3. 自定义协议或格式验证:在自定义的文件格式、通信协议中,需要嵌入SM3校验码,且希望校验逻辑完全自主可控。
  4. 算法变体研究:基于SM3的核心结构,尝试修改轮常数、置换函数等,研究其密码学性质(仅供学习研究)。

最后再分享一个小技巧:如果你需要频繁计算大量小数据的SM3,可以考虑实现一个“增量哈希”接口,即维护内部状态,支持update()digest()方法。这模仿了hashlib的用法,可以避免重复初始化。实现的关键在于要缓存未满一个块的数据,在update时凑满块就压缩,最后在digest时执行填充和最终压缩。这能提升流式处理和小数据重复哈希的效率。