SHA1 实现差异的根源:消息填充长度计算错误

本文详解 Python 手写 SHA-1 算法时因填充逻辑偏差导致哈希结果与标准库 hashlib.sha1 不一致的根本原因,并给出修正方案、完整可运行代码及关键注意事项。

本文详解 python 手写 sha-1 算法时因填充逻辑偏差导致哈希结果与标准库 `hashlib.sha1` 不一致的根本原因,并给出修正方案、完整可运行代码及关键注意事项。

SHA-1 是一种确定性哈希算法,其输出严格依赖于输入消息的精确字节级结构,尤其是消息填充(padding)阶段。许多手写实现失败并非源于核心循环逻辑(如逻辑函数、常量、轮函数或旋转操作),而恰恰卡在看似微小的填充细节上——本例即为典型。

? 核心问题:填充长度计算偏移 1 字节

在 SHA-1 规范中,填充流程严格分为三步:

  1. 追加单字节 0x80;
  2. 追加若干 0x00 字节,使当前消息长度(含 0x80)模 64 等于 56;
  3. 追加 8 字节(64 位)的大端序 消息原始比特长度

关键陷阱在于:步骤 2 中的“当前长度”必须包含已添加的 0x80 字节。原代码中:

msg_len = len(data)           # 此时未加 0x80
data += b"\x80"
data += b"\x00" * ((56 - msg_len % 64) % 64)  # ❌ 错误:用旧长度计算补零数

这导致补零数量少算(或多数)1 字节,进而使后续 64 字节分块错位,最终哈希值全盘错误。

零沫AI工具导航

零沫AI工具导航-AI导航新标杆,探索全球实用AI工具

下载

✅ 正确做法是:在追加 0x80 后,以 len(data)(即 msg_len + 1)为基准计算补零数:

msg_len = len(data)
data += b"\x80"
# 补零至 (msg_len + 1 + zero_count) % 64 == 56
zero_count = (56 - (msg_len + 1) % 64) % 64
data += b"\x00" * zero_count
# 再追加 64-bit 比特长度
data += (msg_len * 8).to_bytes(8, "big")  # 注意:此处 msg_len 是原始字节数,正确!

⚠️ 注意:msg_len * 8 计算的是原始消息的比特长度,该部分无误;但补零基数必须反映 0x80 已加入后的实际长度。

✅ 完整修正版代码(经测试通过)

from hashlib import sha1 as builtin_sha1

def rotl32(value: int, count: int) -> int:
    return ((value <> (32 - count))) & 0xffffffff

def sha1(data: bytes) -> bytes:
    # 初始化哈希状态
    h0 = 0x67452301
    h1 = 0xefcdab89
    h2 = 0x98badcfe
    h3 = 0x10325476
    h4 = 0xc3d2e1f0

    msg_len = len(data)

    # 步骤 1:追加 0x80
    data = data + b"\x80"

    # 步骤 2:补零至长度 ≡ 56 (mod 64),注意此时 data 长度已是 msg_len + 1
    pad_len = (56 - (msg_len + 1) % 64) % 64
    data = data + b"\x00" * pad_len

    # 步骤 3:追加原始消息的比特长度(64 位大端)
    data = data + (msg_len * 8).to_bytes(8, "big")

    # 处理每块 64 字节
    for i in range(0, len(data), 64):
        # 拆分为 16 个 32-bit 大端字
        words = [
            int.from_bytes(data[i + j : i + j + 4], "big")
            for j in range(0, 64, 4)
        ]

        # 扩展至 80 个字(SHA-1 消息调度)
        for j in range(16, 80):
            w = rotl32(
                words[j - 3] ^ words[j - 8] ^ words[j - 14] ^ words[j - 16], 1
            )
            words.append(w)

        # 初始化工作变量
        a, b, c, d, e = h0, h1, h2, h3, h4

        # 80 轮主循环
        for j in range(80):
            if 0 <= j <= 19:
                f = (b & c) | (~b & d)
                k = 0x5a827999
            elif 20 <= j <= 39:
                f = b ^ c ^ d
                k = 0x6ed9eba1
            elif 40 <= j <= 59:
                f = (b & c) | (b & d) | (c & d)
                k = 0x8f1bbcdc
            else:  # 60–79
                f = b ^ c ^ d
                k = 0xca62c1d6

            temp = (rotl32(a, 5) + f + e + k + words[j]) & 0xffffffff
            e = d
            d = c
            c = rotl32(b, 30)
            b = a
            a = temp

        # 累加到哈希状态
        h0 = (h0 + a) & 0xffffffff
        h1 = (h1 + b) & 0xffffffff
        h2 = (h2 + c) & 0xffffffff
        h3 = (h3 + d) & 0xffffffff
        h4 = (h4 + e) & 0xffffffff

    # 输出 20 字节大端结果
    return (
        (h0 << 128) | (h1 << 96) | (h2 << 64) | (h3 << 32) | h4
    ).to_bytes(20, "big")

# ✅ 验证
if __name__ == "__main__":
    test_data = b"hello"
    assert sha1(test_data) == builtin_sha1(test_data).digest()
    print("✓ SHA-1 implementation matches hashlib.sha1")
    print(f"SHA1('hello') = {sha1(test_data).hex()}")

? 关键注意事项总结

  • 字节 vs 比特长度:填充末尾的 8 字节必须是原始消息的比特长度(len(data) * 8),而非字节长度,这是 SHA-1 的强制要求。
  • 大端一致性:所有字节转换(int.from_bytes(..., 'big')、.to_bytes(..., 'big')、常量加载)必须统一使用大端序,不可混用。
  • 32 位截断:所有中间运算(尤其加法、旋转)后需显式 & 0xffffffff 以模拟 32 位无符号整数溢出行为。
  • 调试建议:对短输入(如 b"", b"a")手动验证填充后总长度是否为 64 的倍数,并检查前 16 个 words 值是否与标准工具(如 sha1sum 或在线 SHA-1 计算器)一致。

遵循以上修正与规范,即可实现与 hashlib.sha1 逐字节兼容的纯 Python SHA-1 实现——这不仅是算法复现的成功,更是对密码学协议严谨性的深刻实践。

相关推荐:

《锈湖根源》树干部分关卡1图文攻略

锈湖根源树干部分关卡1解法,锈湖,1860年春,詹姆斯范德布姆到达锈湖,他的叔叔刚刚离奇死亡,詹姆斯是它(指房子及其它的遗产唯一的继承人。他完全不知道这些事情将会怎样改变他的人生。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡1图文攻略: ◆进入场景后点击詹姆斯,点击胸前的纸条...

《锈湖根源》树干部分关卡2房子攻略

锈湖根源树干部分关卡2解谜流程,点击房子旁边的黑影开始游戏,进入场景后,把木凳移到詹姆的脚下,点击让他站在木凳上面,点击花盆处,拾取左边的[虫子],点击左边的花盆,拾取[钻石]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡2攻略: 点击房子旁边的黑影开始游戏 ◆进入场景后,把...

《锈湖根源》树干部分关卡3叔叔图文步骤

锈湖根源树干部分关卡3图文攻略,进入场景后点击墙上的相框,点击右上角撕下照片,拾取上面的[钥匙],回退场景后右滑,点击最右边的木柜,打开第二个抽屉,拾取[火柴],用[钥匙]打开第四个抽屉,拾取[斧头]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡3叔叔图文步骤: 点击房子的窗...

《锈湖根源》树干部分关卡4寻找爱图文攻略

锈湖根源树干部分关卡4怎么过,点击下方中间的两个黑影处开始游戏,点击詹姆斯手上的信纸,把[羽毛]给他,用[带血的手帕]把血蹭到信纸上,点击右手蘸血,点击信纸写字“Will you marry me?”,写完后点击信纸,拾取[情书]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡...

《锈湖根源》关卡16星星解谜攻略

锈湖根源关卡16图文攻略,本篇主要带来的是艾玛以及她的儿子弗兰克的故事,其中还包括艾尔伯特,点击房子的二楼窗户开始游戏,进入场景后点击左边的桌子,拾取[鸟食]和[卡]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡16星星解谜攻略: 点击房子的二楼窗户开始游戏 ◆进入场景后点击左边的桌...

《锈湖根源》关卡12搜索解谜攻略

锈湖根源关卡12怎么过,打开柜子后,拾取[图章],回退场景后右滑,点击右边的桌子,将[墨水]倒进左上角的碗中,将[图章]浸到墨水里,得到[有油墨的图章],然后点击信封盖章,盖章后不断点击信封卷起,放入左边的管子中,拾取[管子]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡12搜索解谜...

《锈湖根源》关卡13画图文攻略

锈湖根源关卡13解谜解说,本篇主要带来的是艾玛以及她的儿子弗兰克的故事,其中还包括艾尔伯特,点击树旁边的黑影开始游戏,这一关卡后玩家可以获得第3个牺牲:艾玛(女儿)的眼泪。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》关卡13画图文攻略: 点击树旁边的黑影开始游戏 ◆进入场景后点击左下角的...

《锈湖根源》树干部分关卡9孩子的游戏

锈湖根源树干部分关卡9图文攻略,点击下方中间的三个黑影处开始游戏,进入场景后点击右边的树,拾取[蜂蜜][树枝]。把[蜂蜜]放进艾玛抱着的瓶子里,并多次点击艾玛的头带,拾取[松紧带]。 关卡1 关卡2 关卡3 关卡4 关卡5 关卡6 关卡7 关卡8 关卡9 关卡10 关卡11 关卡12 关卡13 关卡14 关卡15 关卡16 《锈湖根源》树干部分关卡9孩子的游戏: 点击下方中间的三个黑影处开始游戏 ...