常见加密算法接口:AES加解密接口设计与使用

谈到对称加密,AES 几乎是所有 SDK 里绕不开的基础设施。它不像 RSA 那样慢,也不像 DES 那样脆弱。

我个人习惯把 AES 接口分成两大块:核心加密逻辑,以及上层易用性封装。今天我们就从接口设计者的视角,把这两块拆开揉碎讲清楚。

1. AES 核心概念:你别只会调库

很多同学在项目里只用过一句 openssl_encrypt(),但接口出了问题就一头雾水。

说白了,AES 接口设计落地的关键就三个要素:密钥长度、工作模式、填充方式。

要素 常见取值 我踩过的坑
密钥长度 AES-128 / AES-256 密钥长度错了,解密直接崩
工作模式 ECB / CBC / GCM ECB 不安全,CBC 需 IV,GCM 最推荐
填充方式 PKCS7 / NoPadding 数据长度不对齐,解密出来乱码
⚠️ 避坑指南:我曾经在设计中忽略了 IV 的随机性,结果在多租户场景下,两个用户使用相同密钥加密了相似的数据……嗯,你知道那种后果。

2. 接口设计:我是怎么拆的

一个成熟的 AES 接口,绝不是把 openssl_encrypt 的入参直接暴露给用户。那样太混乱了。

我建议至少拆成三层:

  1. 底层引擎层:只负责加密/解密,暴露 encrypt(key, iv, data) 和 decrypt(key, iv, ciphertext)。
  2. 策略层:管理工作模式和填充方式。你想想看,CBC 需要 IV,GCM 需要 tag,ECB 不需要额外参数——该对外还是对内?
  3. 用户层:只传字符串或字节流,隐藏一切密码学细节。

举个例子。这是我早期写的一个 Go 版本接口核心片段:

type AESCipher struct {
    key []byte
    mode CipherMode
}

func (c *AESCipher) Encrypt(plaintext []byte) ([]byte, error) {
    block, err := aes.NewCipher(c.key)
    if err != nil {
        return nil, err
    }
    
    // 带上随机 IV
    iv := make([]byte, aes.BlockSize)
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        return nil, err
    }
    
    // 内部 PKCS7 padding
    plaintext = pkcs7.Pad(plaintext, aes.BlockSize)
    
    ciphertext := make([]byte, len(plaintext))
    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext, plaintext)
    
    // IV + 密文一块返回
    return append(iv, ciphertext...), nil
}

注意一个细节:IV 必须随机生成,且每次加密不同。有些同学图省事用固定 IV…… 解密没问题,但相同密钥加密相同明文时,密文会一模一样——这是安全漏洞。

3. 填充方式:为什么 PKCS7 是默认选择

AES 是块加密,块大小固定 16 字节。如果数据不是 16 的倍数,就要填充。

我见过几种填充方式:

  • PKCS7:填充多少字节,每个字节就填对应数值。比如缺 5 字节,就填 0x05 重复 5 次。解密后根据最后一个字节去掉尾部。简单可靠。
  • NoPadding:要求数据长度恰好是 16 的倍数,否则报错。适合固定长度消息。
  • ZeroPadding:填 0x00。但问题在于数据本身可能末尾就有 0,解密后无法区分。别用。
💡 小技巧:我在封装 SDK 时,默认固定位 PKCS7 并内置处理。用户不用感知填充的存在。如果你暴露了 NoPadding 选项,记得做严格校验。

4. 实战 SVGs:AES-SDK 完整流程

下面这张图,是我设计 SDK 时画的工作流,你可以直接参考。

AES 加解密 SDK 核心数据流 明文输入 密钥 随机 IV 密钥扩展 + 轮密钥加 10/12/14 轮 SubBytes ShiftRows MixColumns 密文输出 解:反向轮函数 + 密钥扩展取逆

5. 避坑清单:我亲身经历过的 5 个坑

你以为 AES 接口很简单?那是对着文档调库。一旦要做 SDK,全是细节。

下面是我回顾自己开发经历时,必须写进接口测试的案例:

  • 密钥长度校验不严格:用户传了个 17 字节的密钥,库没报错,但加解密不稳定。后来我强制校验 key 长度必须为 16/24/32。
  • IV 不返回:CBC 模式下加密时不把 IV 拼到密文里,解密端根本没法还原。我曾经设计过单独传 IV,后来全改成 IV + 密文整体返回。
  • 多线程并发不安全:如果你把 cipher 对象做成单例但没加锁,加密时数据会被相互覆盖。嗯,线上事故就是这么来的。
  • GCM 的 nonce 长度:GCM 模式 nonce 推荐 12 字节,有的库却默认为 16 字节。跨语言互调时,这是大坑。
  • 跨语言 PKCS7 不一致:Java 的 PKCS5Padding 与 PKCS7 在 AES 场景下等价,但 Python 直接就叫 PKCS7。别在文档里乱改名。
核心原则:好的 AES 接口,让调用方既不需要知道填充,也不需要关心 IV,更不用手动处理 nonce。接口越“无脑”,用户出错概率越低。

6. 接口签名:我推荐这么设计

最后分享我目前在用的接口签名模式,供你参考:

函数 签名 说明
Encrypt Encrypt(plain []byte) (cipher []byte, err error) 自动生成 IV,IV 拼在密文头部
Decrypt Decrypt(cipher []byte) (plain []byte, err error) 自动从密文前 16 字节解析 IV
SetKey SetKey(key []byte) error 校验密钥长度,只允许 16/24/32

说白了,你记住一句话:把最容易出错的环节,全部封装在 SDK 内部。

我现在做任何加密接口设计,都坚持这个原则。用户只需传入明文和密钥,其他由 SDK 承担。