2、对称密钥生成:AES、DES/3DES、SM4

对称密钥生成,说白了就是给加密算法“造一把锁”。锁的质量好不好,直接决定了数据安全的下限。我这些年做安全审计,见过太多因为密钥生成不规范导致整个系统形同虚设的案例。今天咱们就把 AES、DES/3DES、SM4 这三种主流对称算法的密钥生成讲透。

2.1 AES 密钥生成

AES 是目前应用最广的对称加密算法。它的密钥长度有三种:128 位、192 位、256 位。我个人习惯,只要性能允许,一律用 256 位。为什么?多出来的 128 位,换来的安全边际非常可观。

核心原则:AES 密钥必须是均匀分布的随机字节序列。任何可预测的模式都会降低有效密钥空间。

2.1.1 随机数源的选择

生成 AES 密钥,第一步是选对随机数生成器。我见过有人直接用 rand() 生成密钥,那基本等于没锁门。正确的做法是:

  • Linux/Unix 环境:从 /dev/urandom 读取,或者使用 getrandom() 系统调用。
  • Windows 环境:调用 CryptGenRandom() 或 BCryptGenRandom()。
  • 跨平台库:OpenSSL 的 RAND_bytes(),或者 libsodium 的 randombytes_buf()。

注意:不要使用 /dev/random 生成大量密钥。它会在熵池不足时阻塞,导致你的服务 hang 住。我曾经排查过一个线上故障,最后发现是密钥生成线程被 /dev/random 卡死了。

2.1.2 代码示例:生成 AES-256 密钥

#include <openssl/rand.h>
#include <stdint.h>

int generate_aes256_key(uint8_t *key_out, size_t len) {
    // len 必须等于 32(256 位)
    if (len != 32) return -1;

    // 使用 OpenSSL 的 CSPRNG
    if (RAND_bytes(key_out, len) != 1) {
        // 处理错误:随机数生成失败
        return -1;
    }

    return 0;
}

嗯,这里要注意:RAND_bytes() 返回 1 表示成功,0 表示失败。别搞反了,我刚开始写的时候也犯过这个错。

2.1.3 密钥强度验证

生成完密钥,我建议做一次简单的“冒烟测试”:

  1. 检查密钥是否全零。全零密钥说明随机数生成器可能没初始化。
  2. 检查密钥的字节分布是否均匀。可以用卡方检验,但生产环境一般不做,太耗时。
  3. 确认密钥长度正确。AES-128 是 16 字节,AES-192 是 24 字节,AES-256 是 32 字节。

2.2 DES/3DES 密钥生成

说实话,DES 现在基本可以进博物馆了。它的 56 位有效密钥长度,用现代硬件暴力破解只需要几个小时。但很多遗留系统还在用,咱们还是得会处理。

安全警告:DES 已被 NIST 正式废弃。新系统绝对不要用。3DES 虽然还没完全退役,但 NIST 也计划在 2023 年后逐步淘汰。能用 AES 就别碰 3DES。

2.2.1 DES 密钥的奇偶校验位

DES 密钥有个坑:每个字节的最低有效位是奇偶校验位,不参与加密。也就是说,你生成的 8 字节密钥,实际有效位只有 56 位。我曾经在项目里看到有人直接拿用户输入的密码当 DES 密钥,结果因为奇偶校验不对,加解密死活对不上。

正确的做法是:生成 8 个随机字节,然后调整每个字节的奇偶位。

void adjust_des_parity(uint8_t *key, size_t len) {
    for (size_t i = 0; i < len; i++) {
        // 计算当前字节中 1 的个数
        int bits = 0;
        uint8_t b = key[i];
        while (b) {
            bits += b & 1;
            b >>= 1;
        }
        // 如果 1 的个数是偶数,把最低位设为 1
        if ((bits & 1) == 0) {
            key[i] ^= 1;
        }
    }
}

2.2.2 3DES 密钥的三种模式

3DES 有三种密钥选项,很多人搞混:

模式 密钥长度 有效安全强度 说明
TDEA Keying Option 1 24 字节(3 个 8 字节密钥) 112 位 三个独立密钥,安全性最高
TDEA Keying Option 2 16 字节(2 个 8 字节密钥) 80 位 K1 和 K2 独立,K3 = K1
TDEA Keying Option 3 8 字节(3 个密钥相同) 56 位 等同于 DES,毫无意义

你想想看,Option 3 其实就是 DES,用 3DES 的壳跑 DES 的逻辑。我见过一些老旧金融系统还在用 Option 2,说实话,80 位的安全强度在今天已经岌岌可危了。

2.3 SM4 密钥生成

SM4 是中国国家密码管理局发布的对称加密算法,也是国密标准的一部分。它的密钥长度固定为 128 位(16 字节)。

2.3.1 SM4 密钥的特殊要求

SM4 的密钥生成流程和 AES 类似,但有一个关键区别:SM4 的密钥扩展算法对某些“弱密钥”比较敏感。虽然 SM4 标准没有像 DES 那样明确列出弱密钥列表,但我在实践中发现,全零密钥和全一密钥会导致加密结果出现可预测的模式。

我的建议:生成 SM4 密钥后,额外检查一下密钥是否全零、全一,或者所有字节都相同。虽然概率极低,但多一道检查总没坏处。

2.3.2 使用 GMSSL 生成 SM4 密钥

#include <gmssl/sm4.h>
#include <gmssl/rand.h>

int generate_sm4_key(uint8_t *key_out) {
    // SM4 密钥固定 16 字节
    if (gmssl_rand_bytes(key_out, 16) != 1) {
        return -1;
    }

    // 可选:检查弱密钥
    uint8_t weak_patterns[][16] = {
        {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
        {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
         0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}
    };

    for (int i = 0; i < 2; i++) {
        if (memcmp(key_out, weak_patterns[i], 16) == 0) {
            // 重新生成
            return generate_sm4_key(key_out);
        }
    }

    return 0;
}

2.4 三种算法密钥生成对比

为了方便你快速查阅,我把三种算法的密钥生成要点整理成了一张表:

算法 密钥长度 随机数要求 特殊处理 推荐状态
AES 16/24/32 字节 CSPRNG 无 强烈推荐
DES 8 字节(有效 56 位) CSPRNG 奇偶校验位调整 已废弃
3DES 16/24 字节 CSPRNG 奇偶校验位调整 逐步淘汰
SM4 16 字节 CSPRNG 建议检查弱密钥 国密推荐

2.5 密钥生成的核心流程

下面这张图展示了对称密钥生成的通用流程,不管用哪种算法,骨架都是一样的:

对称密钥生成核心流程 熵源采集 密码学安全随机数生成器 算法特定处理(奇偶校验/弱密钥检查) 密钥字节序列输出 密钥完整性验证 常见错误 • 使用伪随机数生成器 • 忽略奇偶校验位 最佳实践 • 使用操作系统 CSPRNG • 生成后立即使用

2.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要用时间戳做种子。 我曾经接手过一个项目,密钥生成用的是 srand(time(NULL)) + rand()。同一秒内生成的所有密钥一模一样。攻击者只要知道大致时间范围,就能穷举出所有密钥。
  • 生成后不要打印密钥。 日志里打印密钥是安全大忌。我见过有人为了方便调试,把密钥 hex 打到了日志里,结果日志文件泄露,整个系统沦陷。
  • 密钥生成和密钥使用要分离。 生成密钥的模块和使用密钥的模块,最好由不同的团队维护。这样可以避免单点故障。
  • 定期轮换密钥。 密钥不是生成一次用一辈子。AES 密钥建议每 1-2 年轮换一次,SM4 同样。3DES 如果还在用,建议每半年轮换。