SHA-1摘要算法
SHA-1 是一种曾经被广泛使用的密码散列函数,它能把任意长度的输入数据(比如一段文字、一个文件)转换成一个固定长度(160位,即40个十六进制字符) 的唯一“指纹”,这个指纹就叫做“摘要”或“哈希值”
与加密算法相比,哈希算法通常更简单,但它同样遵循严格的数学规则 SHA-1 具有以下几个核心特性:
- 固定输出:无论输入多大,输出永远是160位(40个十六进制数)
- 确定性:相同的输入永远产生相同的输出
- 雪崩效应:输入的微小改变,都会导致输出的哈希值发生巨大的,无法预测的变化
- 单向性:从哈希值几乎不可能反向推导出原始输入数据。这是一个非常重要的安全特性
那么这个算法在我刚刚学的时候感觉非常脆弱,没有任何用处,只是一个数值而已,现在依然有使用sha1进行校验的网站,但是sha1不再完全安全 并在 2017 年通过“SHA-1 碰撞攻击”在现实中证实可以通过巨量数据来进行强算, 虽然这种攻击成本极高,但现实安全系统不能依赖“攻击代价高”来保证安全。于是,SHA-1 已逐渐被 SHA-2、SHA-3 等更强的算法所取代。
算法原理
在hash算法系列里面涉及巨量的大数运算和循环迭代,已经不是人力可以一步一步进行推敲的,所以我们会讲流程,然后写代码,不在手动推敲 sha1的主要思想是:
将输入分块 → 填充 → 扩展消息 → 多轮迭代压缩 → 合并结果。
基本概念
SHA-1 将输入消息处理成 512 位的分组,然后对每个分组进行一系列复杂的数学运算,最终生成 160 位的哈希值
第一步:消息填充
无论原始消息长度是多少,我们需要把它填充成512的倍数,最低可以填充1倍就是512本身 填充规则:
- 先在消息末尾添加一个 1 bit
- 然后添加若干个 0 bit
- 最后 64 位用来表示原始消息的长度(以 bit 为单位)
比如我们输入
duck - 原始消息长度为
4 * 8 = 24 - 填充数据为
512 - 1 - 24
第二步:初始化hash值
sha-1使用5个初始hash值
h0 = 0x67452301
h1 = 0xEFCDAB89
h2 = 0x98BADCFE
h3 = 0x10325476
h4 = 0xC3D2E1F0这些是固定的魔术数字,提供了算法的初始状态。
第三步:处理分组消息
- 创建消息调度表
将512位拆分成16个32位的字 -> w[0] => [15] 然后拓展为80个字,算上前面的16个字,总体数据是80个字 W[t] = LEFT_ROTATE(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1) 选择4个之前的字:
W[t-3]:3步前的字 W[t-8]:8步前的字 W[t-14]:14步前的字 W[t-16]:16步前的字 然后进行异或运算
temp = W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16]也就是取前面第 3、8、14、16 个字异或后左循环移 1 位。 最后再循环左移1位
- 主循环:80轮运算 首先我们把上面的初始hash值先赋值到这里 然后就是轮函数了,这个是有明确定义的
- 第 0-19 轮
f(t, b, c, d) = (b & c) | ((~b) & d) K(t) = 0x5A827999
- 第 20-39 轮
f(t, b, c, d) = b ^ c ^ d K(t) = 0x6ED9EBA1
- 第 40-59 轮
f(t, b, c, d) = (b & c) | (b & d) | (c & d) K(t) = 0x8F1BBCDC
- 第 60-79 轮
f(t, b, c, d) = b ^ c ^ d K(t) = 0xCA62C1D6
for t in range(0, 80):
temp = LEFT_ROTATE(a, 5) + f(t, b, c, d) + e + K(t) + W[t]
e = d
d = c
c = LEFT_ROTATE(b, 30)
b = a
a = temppy的伪代码差不多就是这样的,后面我们实际写代码时候再去详细说明
处理完一个512分组后,更新最终的哈希值:
h0 = (h0 + a) & 0xFFFFFFFF
h1 = (h1 + b) & 0xFFFFFFFF
h2 = (h2 + c) & 0xFFFFFFFF
h3 = (h3 + d) & 0xFFFFFFFF
h4 = (h4 + e) & 0xFFFFFFFF第四步:生成最终结果
最终哈希 = h0 || h1 || h2 || h3 || h4 这样就得到了 160 位(5 × 32 位)的 SHA-1 哈希值,通常表示为 40 个十六进制字符
总结
SHA-1 的核心思想是 “迭代压缩”:通过多轮逻辑运算和位移操作,把任意长度的输入“压缩”成一个固定长度的摘要 尽管它在现代安全场景中已被淘汰,但作为学习密码学的第一步,SHA-1 能让你深入理解哈希算法的底层机制,也为后续学习 SHA-256、SHA-3 等算法奠定坚实基础。
代码
package main
import (
"encoding/binary"
"fmt"
)
// SHA1 结构体
type SHA1 struct {
h [5]uint32
}
// 初始化SHA1常量
func NewSHA1() *SHA1 {
return &SHA1{
h: [5]uint32{
0x67452301,
0xEFCDAB89,
0x98BADCFE,
0x10325476,
0xC3D2E1F0,
},
}
}
// 填充消息
func padMessage(message []byte) []byte {
originalLength := len(message)
originalBitLength := uint64(originalLength * 8)
// 添加1比特(0x80字节)
padded := append(message, 0x80)
// 填充0直到长度满足 (长度 % 64 == 56)
for len(padded)%64 != 56 {
padded = append(padded, 0x00)
}
// 添加原始消息长度的64位表示(大端序)
lengthBytes := make([]byte, 8)
binary.BigEndian.PutUint64(lengthBytes, originalBitLength)
padded = append(padded, lengthBytes...)
return padded
}
// 循环左移
func leftRotate(n uint32, shift uint) uint32 {
return (n << shift) | (n >> (32 - shift))
}
// 主要的SHA1处理函数
func (s *SHA1) Process(chunk []byte) {
if len(chunk) != 64 {
panic("chunk size must be 64 bytes")
}
// 将512位块分成16个32位字
var w [80]uint32
for i := 0; i < 16; i++ {
w[i] = binary.BigEndian.Uint32(chunk[i*4 : (i+1)*4])
}
// 扩展16个字到80个字
for i := 16; i < 80; i++ {
w[i] = leftRotate(w[i-3]^w[i-8]^w[i-14]^w[i-16], 1)
}
// 初始化工作变量
a, b, c, d, e := s.h[0], s.h[1], s.h[2], s.h[3], s.h[4]
// 主循环
for i := 0; i < 80; i++ {
var f, k uint32
switch {
case i < 20:
f = (b & c) | (^b & d)
k = 0x5A827999
case i < 40:
f = b ^ c ^ d
k = 0x6ED9EBA1
case i < 60:
f = (b & c) | (b & d) | (c & d)
k = 0x8F1BBCDC
default:
f = b ^ c ^ d
k = 0xCA62C1D6
}
temp := leftRotate(a, 5) + f + e + k + w[i]
e = d
d = c
c = leftRotate(b, 30)
b = a
a = temp
}
// 添加到当前哈希值
s.h[0] += a
s.h[1] += b
s.h[2] += c
s.h[3] += d
s.h[4] += e
}
// 计算SHA1哈希
func (s *SHA1) Sum(message []byte) [20]byte {
// 重置初始哈希值
s.h = [5]uint32{
0x67452301,
0xEFCDAB89,
0x98BADCFE,
0x10325476,
0xC3D2E1F0,
}
// 填充消息
padded := padMessage(message)
// 处理每个512位块
for i := 0; i < len(padded); i += 64 {
s.Process(padded[i : i+64])
}
// 将哈希值转换为字节数组
var result [20]byte
for i, h := range s.h {
binary.BigEndian.PutUint32(result[i*4:(i+1)*4], h)
}
return result
}
// 将字节数组转换为十六进制字符串
func bytesToHex(b [20]byte) string {
const hexChars = "0123456789abcdef"
var result [40]byte
for i, v := range b {
result[i*2] = hexChars[v>>4]
result[i*2+1] = hexChars[v&0x0F]
}
return string(result[:])
}这个过于仓促了,我直接抄了份代码,hash记住轮数,初始hash值,填充区块大小,初始区块大小也就差不多了