mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
564 字
2 分钟
sha1从原理到cpp实现
2026-09-14

sha-1从原理到cpp实现#

一、原理#

我们直接看sha-1的流程图

flowchart TD A[输入消息 M] --> B[步骤1: 消息填充 Padding] B --> C[步骤2: 分块 512 bit] C --> D[步骤3: 初始化 5 个 32bit 寄存器 H0~H4] D --> E{还有 512bit 块吗?} E -- 是 --> F[步骤4: 对每个块做 80 轮压缩] F --> G[步骤5: 更新 H0~H4] G --> E E -- 否 --> H[步骤6: 拼接 H0~H4] H --> I[输出 160bit 摘要]

基本和md5大差不差,从图上能直观看到的差别有:有5个寄存器,每个块做80轮压缩,最后拼接的是5个寄存器的值。

SHA-1 把任意长度的消息压缩成 160 位(20 字节) 的摘要,所以也是压缩算法。

咱们的md5则最后压缩成16字节,这里因为sha1多了个寄存器的原因。

步骤一:消息填充#

规则:在原消息的末尾补一个1(0x80),然后补0,知道长度满足56字节(448位),最后会追加8字节(64位)的大端序内容,用于记录长度,最终填充完的长度是512位的倍数。

flowchart LR %% 定义样式类 classDef default fill:#f9fafb,stroke:#6b7280,stroke-width:2px,color:#1f2937,rx:8,ry:8; classDef startEnd fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a,rx:10,ry:10; classDef process fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d; classDef highlight fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f; classDef result fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#4c1d95,rx:10,ry:10; A["原始消息 M<br/>(len = L bits)"]:::startEnd B["追加 1 bit"]:::process C["追加 k 个 0 bit<br/>使 (L+1+k) ≡ 448 mod 512"]:::highlight D["追加 64 bit<br/>大端序的 L"]:::highlight E["总长度是 512 的整数倍"]:::result A --> B B --> C C --> D D --> E %% 添加连线样式 linkStyle default stroke:#6b7280,stroke-width:2px;

步骤二:分块#

规则:拆分成L个512位的块。

步骤三:初始化寄存器#

H0 = 0x67452301
H1 = 0xEFCDAB89
H2 = 0x98BADCFE
H3 = 0x10325476
H4 = 0xC3D2E1F0
就比我们的md5多了一个H4,其他都是一样的哈。

步骤四:将每512位(16个32位字)扩展成80个32位字W[0..79]#

flowchart TD %% 样式定义 classDef default fill:#f9fafb,stroke:#6b7280,stroke-width:2px,color:#1f2937,rx:8,ry:8; classDef input fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a,rx:10,ry:10; classDef loop fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f; classDef condition fill:#fce7f3,stroke:#db2777,stroke-width:2px,color:#831843; classDef formula fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d; classDef output fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#4c1d95,rx:10,ry:10; A["512bit 块<br/>= 16 个 32bit 字<br/>W0..W15"]:::input B["初始化 t = 16"]:::loop C{"t ≤ 79 ?"}:::condition D["读取已计算的<br/>W[t-3], W[t-8],<br/>W[t-14], W[t-16]"]:::formula E["计算异或:<br/>tmp = W[t-3] ^ W[t-8]<br/>^ W[t-14] ^ W[t-16]"]:::formula F["循环左移 1 位:<br/>W[t] = rotl(tmp, 1)"]:::formula G["t = t + 1"]:::loop H["得到 W0..W79"]:::output A --> B B --> C C -- 是 --> D D --> E E --> F F --> G G --> C C -- 否 --> H %% 连线样式 linkStyle default stroke:#6b7280,stroke-width:2px;

步骤五:80轮主循环#

和md5不同,它的k表只有四个常数,同时压缩函数也不相同,也就是f和k与md5不同。

另外他是每20轮换一次k和f。

由于是五个寄存器,所以IV值也是不同的变化。

轮次f(b,c,d)K
0–19`(b & c)(~b & d)` (选择函数)
20–39b ^ c ^ d (异或)0x6ED9EBA1
40–59`(b&c)(b&d)
60–79b ^ c ^ d (异或,和 20–39 相同)0xCA62C1D6
flowchart TD %% 样式定义 classDef default fill:#f9fafb,stroke:#6b7280,stroke-width:2px,color:#1f2937,rx:8,ry:8; classDef init fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a,rx:10,ry:10; classDef loop fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f; classDef condition fill:#fce7f3,stroke:#db2777,stroke-width:2px,color:#831843; classDef round1 fill:#e0f2fe,stroke:#0284c7,stroke-width:2px,color:#0c4a6e; classDef round2 fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d; classDef round3 fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12; classDef round4 fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#7f1d1d; classDef calc fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#4c1d95; classDef update fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87; classDef final fill:#d1fae5,stroke:#059669,stroke-width:2px,color:#064e3b,rx:10,ry:10; A["初始化 a,b,c,d,e = H0..H4"]:::init B["for t = 0..79"]:::loop C{"选择 f 和 K"}:::condition D["0..19<br/>f = (b&c) | (~b&d)<br/>K = 0x5A827999"]:::round1 E["20..39<br/>f = b ^ c ^ d<br/>K = 0x6ED9EBA1"]:::round2 F["40..59<br/>f = (b&c)|(b&d)|(c&d)<br/>K = 0x8F1BBCDC"]:::round3 G["60..79<br/>f = b ^ c ^ d<br/>K = 0xCA62C1D6"]:::round4 H["temp = rotl(a,5) + f + e + K + W[t]"]:::calc I["e = d<br/>d = c<br/>c = rotl(b,30)<br/>b = a<br/>a = temp"]:::update J["H0 += a<br/>H1 += b<br/>H2 += c<br/>H3 += d<br/>H4 += e"]:::final A --> B B --> C C -->|0..19| D C -->|20..39| E C -->|40..59| F C -->|60..79| G D --> H E --> H F --> H G --> H H --> I I --> B B -->|结束| J %% 连线样式 linkStyle default stroke:#6b7280,stroke-width:2px;

二、实现#

废话不多说,理论有了,直接上来干实战了,依旧是从应用层出发。

有了md5的铺垫,sha基本也是手拿把掐。

// ============================================================
// 一次性计算 SHA-1(封装接口)
// ============================================================
std::string sha1(const std::string& input) {
SHA1Context ctx; //初始化上下文
sha1Update(ctx, reinterpret_cast<const uint8_t*>(input.data()), input.size());
uint8_t digest[20]; //声明结果摘要
sha1Final(ctx, digest);
std::ostringstream oss;
for (int i = 0; i < 20; i++) {
oss << std::hex << std::setw(2) << std::setfill('0')
<< (int)digest[i];
}
return oss.str();
}

1.SHA1Context#

来看一下这个结构体,基本跟md5差不多,就多了一个寄存器。

// ============================================================
// SHA-1 上下文
// ============================================================
struct SHA1Context {
uint32_t state[5]; // A, B, C, D, E 五个寄存器
uint64_t bitCount; // 已处理的比特数
uint8_t buffer[64]; // 当前未处理完的 64 字节块
size_t bufferLen; // buffer 中已有多少字节
SHA1Context() { init(); }
void init() {
// SHA-1 的 5 个初始魔数(RFC 3174 定义)
state[0] = 0x67452301; // A
state[1] = 0xEFCDAB89; // B
state[2] = 0x98BADCFE; // C
state[3] = 0x10325476; // D
state[4] = 0xC3D2E1F0; // E
bitCount = 0;
bufferLen = 0;
}
};

2.Update#

基本上没区别,每满64字节进行一次sha1Transform

// ============================================================
// 更新数据(可以分多次调用)
// ============================================================
static void sha1Update(SHA1Context& ctx, const uint8_t* data, size_t len) {
ctx.bitCount += (uint64_t)len * 8; // 累加比特数,计算原始消息长度
while (len > 0) {
size_t space = 64 - ctx.bufferLen;
size_t copy = (len < space) ? len : space;
std::memcpy(ctx.buffer + ctx.bufferLen, data, copy);
ctx.bufferLen += copy;
data += copy;
len -= copy;
if (ctx.bufferLen == 64) {
sha1Transform(ctx.state, ctx.buffer);
ctx.bufferLen = 0;
}
}
}

3.sha1Transform#

这里还是一样贴一下工具函数以及变量

// ============================================================
// SHA-1 每轮常数 K(只有 4 个,每 20 轮换一个)
// ============================================================
static const uint32_t K[4] = {
0x5A827999, // 0..19
0x6ED9EBA1, // 20..39
0x8F1BBCDC, // 40..59
0xCA62C1D6 // 60..79
};
// ============================================================
// 大端序读写工具(SHA-1 规定 32 位字按大端序读/写)
// ============================================================
static inline uint32_t read_be32(const uint8_t* p) {
return ((uint32_t)p[0] << 24)
| ((uint32_t)p[1] << 16)
| ((uint32_t)p[2] << 8)
| (uint32_t)p[3];
}
static inline void write_be32(uint8_t* p, uint32_t v) {
p[0] = (uint8_t)((v >> 24) & 0xff);
p[1] = (uint8_t)((v >> 16) & 0xff);
p[2] = (uint8_t)((v >> 8) & 0xff);
p[3] = (uint8_t)( v & 0xff);
}
// 循环左移
static inline uint32_t rotl(uint32_t x, uint32_t n) {
return (x << n) | (x >> (32 - n));
}
// ============================================================
// 处理一个 512 位(64 字节)块
// ============================================================
static void sha1Transform(uint32_t state[5], const uint8_t block[64]) {
uint32_t W[80]; //声明我们需要将原始消息扩展的空间
// 1. 按大端序把 64 字节拆成 16 个 32 位字
for (int i = 0; i < 16; i++) {
W[i] = read_be32(block + i * 4);
}
// 2. 扩展到 80 个字:W[t] = rotl(W[t-3]^W[t-8]^W[t-14]^W[t-16], 1)
for (int i = 16; i < 80; i++) {
W[i] = rotl(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
}
//这里是循环左移一位,解决了sha0的危险点。
uint32_t A = state[0];
uint32_t B = state[1];
uint32_t C = state[2];
uint32_t D = state[3];
uint32_t E = state[4];
// 3. 80 步主循环
for (int i = 0; i < 80; i++) {
uint32_t F;
uint32_t k;
if (i < 20) {
// 第 1 段:选择函数
F = (B & C) | (~B & D);
k = K[0];
} else if (i < 40) {
// 第 2 段:异或
F = B ^ C ^ D;
k = K[1];
} else if (i < 60) {
// 第 3 段:多数函数
F = (B & C) | (B & D) | (C & D);
k = K[2];
} else {
// 第 4 段:异或(和 20..39 相同)
F = B ^ C ^ D;
k = K[3];
}
uint32_t tmp = rotl(A, 5) + F + E + k + W[i]; //这里是循环左移5位,而md5是动态的s位
E = D;
D = C;
C = rotl(B, 30);
B = A;
A = tmp;
}
// 4. 累加回 state
state[0] += A;
state[1] += B;
state[2] += C;
state[3] += D;
state[4] += E;
}

4.doFinal#

// ============================================================
// 收尾:填充 + 写入原始长度 + 输出 20 字节摘要
// ============================================================
static void sha1Final(SHA1Context& ctx, uint8_t digest[20]) {
uint8_t padding[64];
std::memset(padding, 0, sizeof(padding));
padding[0] = 0x80;
uint64_t bits = ctx.bitCount;
// 让 (bufferLen + 8) % 64 == 0
size_t padLen = (ctx.bufferLen < 56)
? (56 - ctx.bufferLen)
: (120 - ctx.bufferLen);
//注意哈,虽然定义上是第一步就填充,但是咱们写代码时为了优化可以放在最后一步做。
sha1Update(ctx, padding, padLen);
// 64 位原始长度按大端序追加
uint8_t lenBytes[8];
for (int i = 0; i < 8; i++) {
lenBytes[i] = (uint8_t)((bits >> (8 * (7 - i))) & 0xff);
}
sha1Update(ctx, lenBytes, 8);
// 把 A..E 按大端序输出成 20 字节
for (int i = 0; i < 5; i++) {
write_be32(digest + i * 4, ctx.state[i]);
}
}

最后是输出,这个就不说啦。

总体来讲,和md5有些差别,但不多,最后再贴一张图,我们来对比看一下。

三、对比#

OIP-C

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

sha1从原理到cpp实现
https://fatdog.20060113.xyz/posts/sha1/
作者
神秘大胖狗
发布于
2026-09-14
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00