3. 对称加密原理与组件内实现

3.1 本章概述

在组件内持久化敏感数据时,对称加密是最核心的保护手段。AES-GCM(Galois/Counter Mode)不仅提供机密性,还内建认证(防篡改),非常适合小数据块持久化场景。本章从算法参数、密钥派生、随机数生成三个维度,讲解如何在组件内正确实现AES-GCM,并给出可直接使用的API调用流程。

学习目标:掌握AES-GCM参数语义(密钥长度、nonce、tag)、PBKDF2/Argon2密钥派生选型、IV与Salt的安全生成策略,完成可工程化部署的加密模块设计。

3.2 AES-GCM算法参数

AES-GCM本质是AES加密引擎在计数器模式(CTR)下运行,同时通过GHASH计算认证标签。关键参数如下:

  • 密钥长度:128/192/256 bit。移动端通常选择256 bit以获得最高安全强度。
  • Nonce(IV):GCM推荐12字节(96 bit),太大或太小均影响性能与安全。
  • 认证标签长度(Tag):可选12、13、14、15、16字节。推荐16字节(128 bit)以防止伪造攻击。
  • 附加认证数据(AAD):可选的未加密但参与认证的数据,用于绑定上下文(如用户ID、文件名)。

GCM模式要求每个密钥下Nonce唯一,否则加密的机密性完全丧失;因此Nonce必须用安全的随机数生成器(CSPRNG)产生。

AES-GCM参数推荐值(移动端持久化)
参数推荐值说明
密钥256 bit使用派生函数生成
Nonce12 byte单次随机,不可重复
Tag16 byte认证完整性保护
AAD可选绑定持久化上下文

3.3 密钥派生:PBKDF2 vs Argon2

用户提供的密码(如PIN、口令)不能直接作为AES密钥。必须通过慢速密码哈希函数(密钥派生函数KDF)生成固定长度密钥。以下是两种主流方案对比:

PBKDF2 与 Argon2 参数对比
项目PBKDF2Argon2(Argon2id)
算法家族HMAC-SHA256/SHA512内存硬 + 计算硬
抗GPU/ASIC低(仅计算成本)高(内存成本+计算成本)
关键参数迭代次数(iterations)、salt内存(m)、迭代(t)、并行(p)、salt
输出长度任意(通常32字节)任意(通常32字节)
移动端推荐值iter=100000~600000m=64MB, t=3, p=1
系统支持Android/ iOS均有需额外库(libsodium、Bouncy Castle)
选型建议:对兼容性要求高的跨平台组件,优先使用PBKDF2(系统原生支持);对安全性要求极高的金融/密码类组件,引入Argon2id。注意Argon2参数中内存消耗需控制在组件可用内存以内(通常56~64MB不会引起OOM)。

3.4 IV与Salt的生成与持久化

  • Salt:PBKDF2/Argon2都需要随机salt,长度推荐16字节。每个加密操作独立生成,与密码共同参与派生。
  • IV(Nonce):GCM的12字节随机数,使用CSPRNG生成。
  • 持久化方案:Salt和IV都不是敏感数据,可以明文存储在加密数据头部或单独字段中。例如:header = salt(16) || iv(12) || tag(16) || ciphertext。读取时先解析header再解密。
常见错误:使用固定salt/iv、使用伪随机数生成器(如rand())、将salt或IV直接硬编码。这些都导致密钥空间坍缩或重放攻击。必须使用SecRandomCopyBytes(iOS)或SecureRandom(Android)或RAND_bytes(OpenSSL)。

3.5 组件内加密流程(SVG流程图)

下图展示了从用户密码到密文持久化的完整流程:

AES-GCM 组件内加密流程(密钥派生 + 加密) 用户密码 随机Salt (16字节) PBKDF2 或 Argon2 随机IV (12字节) AES-GCM 加密 (密钥+IV+明文+AAD) Salt || IV || Tag + 密文 持久化 关键点 • 每一步的随机数必须由CSPRNG生成 • Salt/IV参与持久化,但不加密;泄露不影响密钥安全 • 派生密钥仅存活于内存,加密完成后立即清除

3.6 代码实现(OpenSSL EVP接口示例)

以下C代码展示完整的加密流程(含PBKDF2派生):

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

#define KEY_LEN 32      // AES-256
#define SALT_LEN 16
#define IV_LEN 12
#define TAG_LEN 16

int encrypt_data(const char* password, const unsigned char* plaintext,
                 size_t pt_len, unsigned char** out, size_t* out_len) {
    unsigned char salt[SALT_LEN], iv[IV_LEN], key[KEY_LEN], tag[TAG_LEN];
    EVP_CIPHER_CTX *ctx = NULL;
    int ret = 0;

    // 1. 生成随机 salt 和 iv
    if (RAND_bytes(salt, SALT_LEN) != 1) return -1;
    if (RAND_bytes(iv, IV_LEN) != 1) return -1;

    // 2. PBKDF2 派生密钥
    if (PKCS5_PBKDF2_HMAC(password, strlen(password),
                          salt, SALT_LEN, 200000,
                          EVP_sha256(), KEY_LEN, key) != 1) return -1;

    // 3. 初始化 GCM 加密上下文
    ctx = EVP_CIPHER_CTX_new();
    if (!ctx) return -1;
    EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, IV_LEN, NULL);
    EVP_EncryptInit_ex(ctx, NULL, NULL, key, iv);

    // 4. 处理 AAD(可选,示例略)
    // EVP_EncryptUpdate(ctx, NULL, &len, aad, aad_len);

    // 5. 加密明文
    size_t max_out = pt_len + EVP_MAX_BLOCK_LENGTH;
    *out = malloc(max_out + TAG_LEN);
    int outl = 0, tmp = 0;
    EVP_EncryptUpdate(ctx, *out, &outl, plaintext, pt_len);
    EVP_EncryptFinal_ex(ctx, *out + outl, &tmp);
    outl += tmp;

    // 6. 获取认证标签
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, TAG_LEN, tag);

    // 7. 构建持久化结构:salt || iv || tag || ciphertext
    memmove(*out + SALT_LEN + IV_LEN + TAG_LEN, *out, outl);
    memcpy(*out, salt, SALT_LEN);
    memcpy(*out + SALT_LEN, iv, IV_LEN);
    memcpy(*out + SALT_LEN + IV_LEN, tag, TAG_LEN);
    *out_len = SALT_LEN + IV_LEN + TAG_LEN + outl;

    EVP_CIPHER_CTX_free(ctx);
    OPENSSL_cleanse(key, KEY_LEN);
    return 0;
}
内存安全:派生出的密钥使用OPENSSL_cleanse()清零;salt和IV不需要清零因为它们已持久化且非敏感。

3.7 调试与常见错误

错误场景现象解决方案
IV重用(同一密钥两次加密用相同IV)解密后数据部分被还原,但GHASH认证失败每次加密前重新生成IV,使用线程安全随机数
Tag长度不匹配解密时报错“bad decrypt”确保加密和解密使用相同TAG_LEN(建议16字节)
Salt长度不足8字节彩虹表攻击风险增大固定使用16字节随机salt
PBKDF2迭代次数过低(<1000)暴力破解速度太快移动端最低100000次,推荐600000次以上(需平衡性能)
未清除派生密钥密钥泄露可能导致批量解密使用专用函数清零,并禁止在日志中打印

3.8 性能与稳定性优化

  • 硬件加速:AES-NI指令集可显著加速GCM;OpenSSL自动检测并启用。Android NDK编译时开启-march=armv8-a+crypto。
  • 密钥派生耗时控制:PBKDF2迭代次数在不影响用户体验前提下(单次加密<100ms)尽量高。可设置迭代次数为可配置常量,方便后续升级。
  • 内存警告:Argon2若设置内存过大,可能在低端设备触发OOM。推荐运行时检查可用内存,动态降低内存成本。
  • 缓存派生结果:同一密码在短时间内多次加密同一组件时,可在内存中缓存cached_key(谨慎使用,需考虑安全生命周期)。
稳定性陷阱:GCM在解密时若tag验证失败,必须返回错误并丢弃所有解密数据,不可部分使用。同时,持久化数据的Salt/IV/tag字段需使用固定字节序(大端或小端),否则跨平台兼容性会崩溃。

3.9 总结与检查清单

  • ☐ 密钥派生使用PBKDF2或Argon2,参数满足安全基线
  • ☐ 每次加密生成新的随机IV和salt(CSPRNG)
  • ☐ 持久化结构为 salt(16) || iv(12) || tag(16) || ciphertext
  • ☐ 解密时将派生密钥、IV、salt、tag正确提取,验证tag后再使用明文
  • ☐ 派生密钥用后立即清零,不在堆/栈中残留
  • ☐ 移动端性能测试通过(单次加密<150ms)