mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
832 字
2 分钟
Hmac原理到cpp实现
2026-09-16

Hmac原理及cpp实现#

一、背景#

hmac为什么会出现?传统的哈希算法,MD结构那种都不能抗住长度扩展攻击:知道 H(M) 就能算出 H(M || padding || 后缀)

如果直接做 MAC = H(secret || message),也就是普通的加盐,攻击者就能在不知道 secret 的情况下伪造 H(secret || message || padding || 伪造后缀)。这就是为什么不能用”裸哈希”当 MAC。

这个可以百度一下哈,挺有意思的攻击手段。

HMAC 的设计目标:

即使底层哈希(如 MD5)已经被攻破碰撞性,HMAC-MD5 仍然是一个安全的 MAC。

这是 HMAC 最漂亮的地方——它把哈希的抗碰撞性抗长度扩展性解耦了。

二、公式#

这个就是它的公式啦,我们先来看看各个符号的含义。

HMAC(K, m) = H( (K' ⊕ opad) || H( (K' ⊕ ipad) || m ) )
符号含义
H底层哈希函数(MD5 / SHA-1 / SHA-256)
K原始密钥
K'处理后的密钥(长度 = 块大小 B)
B哈希函数的块大小(字节)
ipad0x36 重复 B 次
opad0x5C 重复 B 次
异或
`

块大小 B

哈希B(字节)
MD564
SHA-164
SHA-25664
SHA-512128

回忆一下啥是块,就是每一段512位或者64字节的消息(最后那块要填充后)。

K’ 的构造规则#

如果密钥比一个块的大小大,那么就对密钥进行一次哈希把长度压到哈希输出长度(MD5 是 16 字节),然后对其填充0到块大小。如果比密钥长度小,就在后面直接填充0直到达到的长度,而不是哈希长度。

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 condition fill:#fce7f3,stroke:#db2777,stroke-width:2px,color:#831843; classDef hash fill:#e0f2fe,stroke:#0284c7,stroke-width:2px,color:#0c4a6e; classDef keep fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d; classDef pad fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f; classDef result fill:#d1fae5,stroke:#059669,stroke-width:2px,color:#064e3b,rx:10,ry:10; A["原始密钥 K"]:::input B{"len(K) > B ?"}:::condition C["K = H(K)<br/>(长度变成哈希输出长度)"]:::hash D["K 保持原样"]:::keep E{"len(K) < B ?"}:::condition F["右侧补 0x00<br/>直到长度 = B"]:::pad G["K' = K"]:::keep H["得到 K'<br/>(长度恰好 B)"]:::result A --> B B -- 是 --> C B -- 否 --> D C --> E D --> E E -- 是 --> F E -- 否 --> G F --> G G --> H %% 连线样式 linkStyle default stroke:#6b7280,stroke-width:2px;

三、原理#

flowchart TD %% 样式定义 classDef default fill:#f9fafb,stroke:#6b7280,stroke-width:2px,color:#1f2937,rx:8,ry:8; classDef key fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a8a,rx:10,ry:10; classDef pad fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f; classDef xor fill:#fce7f3,stroke:#db2777,stroke-width:2px,color:#831843; classDef hash fill:#e0f2fe,stroke:#0284c7,stroke-width:2px,color:#0c4a6e; classDef inner fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d; classDef outer fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87; classDef result fill:#d1fae5,stroke:#059669,stroke-width:2px,color:#064e3b,rx:10,ry:10; A["密钥 K"]:::key B["构造 K'<br/>(长度 = B)"]:::key C["ipad<br/>= 0x36 × B<br/>(常量)"]:::pad D["opad<br/>= 0x5C × B<br/>(常量)"]:::pad E["K' ⊕ ipad"]:::xor F["K' ⊕ opad"]:::xor M["消息 m"]:::key G["内层哈希计算 H"]:::hash H["inner = H(K'⊕ipad ‖ m)"]:::inner I["外层哈希计算 H"]:::hash J["HMAC = H(K'⊕opad ‖ inner)"]:::result A --> B C --> E D --> F B --> E B --> F M --> G E --> G G --> H H --> I F --> I I --> J %% 连线样式 linkStyle default stroke:#6b7280,stroke-width:2px;

用一句话概括:

内层inner = H((K' ⊕ ipad) || m) 外层H((K' ⊕ opad) || inner)

两层哈希,中间夹一个 16/20/32 字节的 inner 结果。

防住长度扩展攻击关键在于外层哈希

  • 攻击者拿到 HMAC(K, m),想算 HMAC(K, m || 后缀),需要构造 H((K'⊕opad) || inner'),其中 inner' 必须等于 H((K'⊕ipad) || m || 后缀)
  • 但攻击者不知道 K'⊕ipad,所以算不出 inner'
  • 就算用长度扩展从 inner 推出某个 inner',外层哈希的输入是 (K'⊕opad) || inner',而 K'⊕opad 攻击者也不知道,所以外层这一步也过不去。

两层都夹着未知密钥派生的前缀,长度扩展被彻底堵死。

四、cpp实现#

废话不多说,直接上代码。应用层入手。

// ============================================================
// HMAC-MD5
// ============================================================
// HMAC 常量
static const size_t MD5_BLOCK_SIZE = 64; // B:MD5 块大小(字节)
static const size_t MD5_DIGEST_SIZE = 16; // MD5 输出长度(字节)
//这里的md5就不说了,可以去看看我之前的博客。
// 底层接口:H(data) -> out[16],复用已有 MD5
static void md5_raw(const uint8_t* data, size_t len,
uint8_t out[MD5_DIGEST_SIZE]) {
MD5Context ctx;
md5Update(ctx, data, len);
md5Final(ctx, out);
}
// HMAC-MD5 核心
// key/keyLen : 密钥
// data/dataLen : 消息
// out : 输出 16 字节
static void hmacMd5(const uint8_t* key, size_t keyLen,
const uint8_t* data, size_t dataLen,
uint8_t out[MD5_DIGEST_SIZE])
{
// ---------- 1. 构造 K'(长度恰好 = B = 64)----------
uint8_t k[MD5_BLOCK_SIZE];
std::memset(k, 0, MD5_BLOCK_SIZE); // 先全 0,天然实现"右侧补零"
if (keyLen > MD5_BLOCK_SIZE) {
// 密钥太长:K' = H(K),结果只占前 16 字节,后面保持 0
md5_raw(key, keyLen, k); //结果拷贝给k
} else {
// 密钥够短:直接拷贝到前面,剩余字节已经是 0
std::memcpy(k, key, keyLen);
}
// ---------- 2. 构造 ipad / opad ----------
uint8_t ipad[MD5_BLOCK_SIZE];
uint8_t opad[MD5_BLOCK_SIZE];
for (size_t i = 0; i < MD5_BLOCK_SIZE; i++) {
ipad[i] = (uint8_t)(k[i] ^ 0x36); //这里跳步骤了,ipad填充原本是0x363636······ 64个36,但是这里直接把k'异或ipad的值也就是结果值赋值给ipad,不是很严谨但是代码可行且节省内存。
opad[i] = (uint8_t)(k[i] ^ 0x5C);//这里也是同样跳步了。
}
// ---------- 3. 内层:inner = H((K'⊕ipad) || m) ----------
uint8_t inner[MD5_DIGEST_SIZE];
{
MD5Context ctx;
md5Update(ctx, ipad, MD5_BLOCK_SIZE); //这里的ipad其实是 (K' ⊕ ipad)
md5Update(ctx, data, dataLen); //同一个ctx上下文,作用就是拼接(K' ⊕ ipad) || m
md5Final(ctx, inner);
}
// ---------- 4. 外层:out = H((K'⊕opad) || inner) ----------
{
MD5Context ctx;
md5Update(ctx, opad, MD5_BLOCK_SIZE); //这里也是同理,就不说了。
md5Update(ctx, inner, MD5_DIGEST_SIZE);
md5Final(ctx, out);
}
}
// HMAC-MD5 便捷接口:字符串进,16 字节出
static void hmacMd5(const std::string& key, const std::string& msg,
uint8_t out[MD5_DIGEST_SIZE]) {
hmacMd5(reinterpret_cast<const uint8_t*>(key.data()), key.size(),
reinterpret_cast<const uint8_t*>(msg.data()), msg.size(),
out);
}

五、换成 SHA-1 / SHA-256 要改什么#

只有三处:

改动点MD5SHA-1SHA-256
B(块大小)646464
输出长度162032
哈希函数md5Update/md5Finalsha1Update/sha1Finalsha256Update/sha256Final
ipad/opad0x36 / 0x5C0x36 / 0x5C0x36 / 0x5C

ipad 和 opad 的字节值是固定的,不随哈希函数变化(RFC 2104 规定)。块大小对 MD5/SHA-1/SHA-256 都是 64,只有 SHA-512 是 128。

所以把上面代码里的 MD5_BLOCK_SIZEMD5_DIGEST_SIZEmd5Update/md5Final 全部替换即可,逻辑一字不改。

六、总结#

flowchart LR %% 样式定义 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 xor fill:#fce7f3,stroke:#db2777,stroke-width:2px,color:#831843; classDef ctx fill:#e0f2fe,stroke:#0284c7,stroke-width:2px,color:#0c4a6e; classDef innerOut fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d; classDef outerOut fill:#d1fae5,stroke:#059669,stroke-width:2px,color:#064e3b,rx:10,ry:10; subgraph Inner["内层哈希 (MD5)"] direction LR I1["K' ⊕ ipad<br/>(64 字节)"]:::xor M["消息 m"]:::input I2["MD5 上下文<br/>(初始化 → 更新 → 最终化)"]:::ctx I3["inner<br/>(16 字节)"]:::innerOut I1 --> I2 M --> I2 I2 --> I3 end subgraph Outer["外层哈希 (MD5)"] direction LR O1["K' ⊕ opad<br/>(64 字节)"]:::xor O2["MD5 上下文<br/>(初始化 → 更新 → 最终化)"]:::ctx O3["HMAC<br/>(16 字节)"]:::outerOut O1 --> O2 I3 --> O2 O2 --> O3 end %% 子图样式 style Inner fill:#f0f9ff,stroke:#0284c7,stroke-width:2px,color:#0c4a6e style Outer fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#14532d %% 连线样式 linkStyle default stroke:#6b7280,stroke-width:2px;

逆向时识别 HMAC 的经典特征:

  1. 连续出现 0x360x5C 的常量(或 0x363636360x5C5C5C5C 这种 32 位扩展)。
  2. 两次调用同一个哈希函数,中间夹着一次 16/20/32 字节的输出。
  3. 看到 0x36363636 基本就锁定 HMAC

除了这个特征,还真没什么好定位的。

妈的,五点多了,要和军爷抢饭吃了,溜了溜了。

分享

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

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

部分信息可能已经过时

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