564 字
2 分钟
sha1从原理到cpp实现
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 = 0x67452301H1 = 0xEFCDAB89H2 = 0x98BADCFEH3 = 0x10325476H4 = 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–39 | b ^ c ^ d (异或) | 0x6ED9EBA1 |
| 40–59 | `(b&c) | (b&d) |
| 60–79 | b ^ 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有些差别,但不多,最后再贴一张图,我们来对比看一下。
三、对比

部分信息可能已经过时









