导读:本期聚焦于小伙伴创作的《Python 手写 SHA-1 算法时填充步骤总出错?常见错误与正确方案解析》,敬请观看详情。不少人在用 Python 从零实现 SHA-1 时会卡在消息填充这一步,导致最终哈希值与标准结果对不上。SHA-1 填充要求先补一个比特 1,再补若干个 0 比特,使长度模 512 余 448,最后附加 64 位原始比特长度。常见误区是直接用字节层面补零却忘了长度单位的区别,或把大端小端弄反。正确做法是以比特为基准计算填充量,用 struct 模块按大端写入长度。下面通过对比错误代码与修正代码,说明如何稳妥地完成填充,并给出可复用的 Python 实现片段,帮助避开调试坑。

SHA-1 作为经典的密码学哈希算法,其规范中定义的消息填充规则看似简单,但在 Python 中手动实现时却容易因为比特与字节的混淆、长度编码方式错误而得到错误摘要。理解填充的本质,是写出正确 SHA-1 函数的前提。

Python 手写 SHA-1 算法时填充步骤总出错?常见错误与正确方案解析

SHA-1 填充的底层规则

SHA-1 处理的数据单位是 512 比特的分组。在哈希计算前,原始消息必须先填充,使得总长度满足特定条件。填充过程分为三步:首先在消息末尾添加一个比特 1;接着补入若干个比特 0,直到消息总长度(含填充)对 512 取模等于 448;最后在末尾附加一个 64 比特的无符号整数,表示原始消息的比特长度。

这里最容易被忽略的是,所有长度计算都以比特为单位,而不是字节。例如一个 3 字节的消息,原始比特长度是 24,填充时要先加 1 比特变成 25 比特,再补 0 到 448 比特模 512,也就是补 423 个 0 比特,最后加 64 比特长度字段,总共 512 比特刚好一个分组。如果只在字节层面思考,往往会少算那个单独的 1 比特,或者把 0 字节数量算错。

常见错误写法与问题定位

下面这段代码是初学者常写的填充逻辑,它直接在字节串后补零字节,并错误地把字节长度写入末尾:

def bad_padding(msg: bytes):
    # 错误示例:以字节为单位乱补
    ml = len(msg)
    msg += b'x80'          # 只加了 0x80 一个字节,相当于比特1加后面七个0
    while len(msg) % 64 != 56:   # 64字节=512比特,56字节=448比特
        msg += b'x00'
    # 错误:把字节长度当比特长度,且用小端打包
    msg += ml.to_bytes(8, 'little')
    return msg

print(bad_padding(b'abc'))

这段代码的第一个问题是,b'x80' 虽然表示一个字节,但在比特视角下它是 10000000,已经包含了要求的那个 1 比特和七个 0 比特,这本身可行;但循环条件以 64 字节为模、目标 56 字节,看似对应 448 比特,却没有考虑如果原始消息长度模 64 已经大于 56,应当再补一个分组。第二个严重错误是末尾用 to_bytes(8, 'little') 写入了字节长度且为小端,而 SHA-1 要求写入的是原始比特长度,并且必须是大端序。

当用上面错误函数处理 b'abc' 后送入后续分组处理,得到的哈希会和标准 SHA-1 对不上。很多调试者会怀疑主循环里的逻辑运算写错,其实根因就在填充。这种错误在长消息和短消息下表现不一致,短消息可能偶然正确,长消息必然错误,非常隐蔽。

正确的填充方案实现

正确的做法应当显式以比特长度计算,并使用 struct 模块以大端方式写入 64 位比特长度。以下函数可稳妥处理任意长度输入:

import struct

def correct_padding(msg: bytes):
    # 原始比特长度
    bit_len = len(msg) * 8
    # 先加一个字节 0x80,即比特1后跟7个0
    padded = msg + b'x80'
    # 当前字节长度,目标为模64余56
    while len(padded) % 64 != 56:
        padded += b'x00'
    # 附加64位大端比特长度
    padded += struct.pack('>Q', bit_len)
    return padded

# 测试
data = b'abc'
p = correct_padding(data)
print(len(p))   # 应为64字节
print(p.hex())

在这个实现里,bit_len 明确使用比特数,避免了字节与比特的错位。struct.pack('>Q', bit_len) 中的 >Q 表示大端无符号长整型,正好对应 SHA-1 规范里的长度字段。循环补零保证分组前长度模 512 比特为 448,也就是模 64 字节为 56,逻辑清晰且对所有长度安全。

需要注意,如果原始消息字节长度模 64 已经大于或等于 56,例如是 60 字节,那么加完 0x80 后变成 61,循环会继续补到 56+64=120 字节,也就是跨了一个额外分组,这符合标准。手动实现时千万不要为了简化而截断填充,否则长消息必然算错。

填充后如何接入主循环

完成填充的函数输出必然是 64 字节的整数倍,可以直接按每 64 字节切片,转成 16 个 32 位大端整数作为分组送入 SHA-1 压缩函数。下面给出衔接示例:

def to_blocks(padded: bytes):
    blocks = []
    for i in range(0, len(padded), 64):
        chunk = padded[i:i+64]
        words = list(struct.unpack('>16I', chunk))
        blocks.append(words)
    return blocks

blocks = to_blocks(correct_padding(b'abc'))
print(len(blocks), len(blocks[0]))

通过 struct.unpack('>16I', chunk) 把每个分组变成 16 个 unsigned int,就满足了 SHA-1 对消息调度的输入要求。此后只需实现标准的 80 步逻辑函数和五常量初始值,就能完整跑通手写 SHA-1。

从工程角度看,手写哈希算法主要价值在于理解协议细节而非生产使用。但填充作为第一道关口,一旦写错,后面无论运算多严谨都得不到正确结果。把上述填充函数作为独立模块测试,用已知向量如 b'abc' 验证输出长度与十六进制,是排查问题的最快路径。

小结与避坑清单

手写 SHA-1 填充时请牢记以下要点:长度字段必须是原始消息的比特数而非字节数;长度字段用大端 64 位表示;填充字节序列以 0x80 起头,之后全零;当现有长度模 64 余 56 不满足时要跨分组补零。只要填充正确,后续主循环调试会轻松许多。

建议把填充函数单独抽出来,配合标准库的 hashlib.sha1 做对照测试。若两者在多种长度输入下填充后分组数一致,再继续实现压缩逻辑,可显著降低整体调试成本。

SHA-1Python消息填充修改时间:2026-08-11 23:18:47

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。