4. 常见加密SDK接口设计模式:init()、encrypt()、decrypt()、sign()、verify()——入参与出参的数据类型约束

聊到加密SDK的接口设计,我脑子里总会蹦出几个“老朋友”——init()、encrypt()、decrypt()、sign()、verify()。你想想看,不管你是用OpenSSL、Bouncy Castle,还是自研的SDK,核心跑不出这五个接口。它们就像乐高积木的基本模块,但玩好这些积木,关键在于弄清楚每一块的“接口”——说白了,就是入参和出参的数据类型约束。

做项目这么多年,我见过太多因为数据类型没对齐而翻车的案例。有的是把uint8_t*当字符串传,结果遇⻅\0提前截断;有的是签名时缓冲区给小了,跑起来直接段错误。嗯,这些坑,今天咱们一个一个填上。

加密SDK接口调用链与核心数据类型 init() encrypt() decrypt() sign() verify() 入参: 密钥对象 · 算法配置 入参: 明文 · 上下文 入参: 密文 · 上下文 入参: 消息 · 私钥 出参: 密钥句柄 出参: 密文 · 标签 出参: 明文 · 完整性状态 出参: 签名值 出参: 布尔结果

4.1 init() 接口:密钥与上下文的“户口本”

init() 是所有操作的前提。它负责把密钥材料、算法参数、工作模式这些东西灌进一个上下文对象里。入参的数据类型,往往是整个SDK数据适配的“第一关”。

入参类别常见数据类型长度约束备注
对称密钥const uint8_t* / ByteArray16 / 24 / 32 字节AES-128 / 192 / 256
非对称密钥对KeyPair 对象由算法决定RSA-2048 / ECC-P256
算法标识枚举 / 整数4 字节SM4, AES-GCM, RSA-OAEP
工作模式枚举 / 整数4 字节ECB, CBC, GCM
随机数源函数指针 / 句柄平台相关可选参数
一个建议:我个人习惯在 init() 里就把密钥的“使用范围”打好标签——只用于加密、只用于签名、还是可导出的。这能帮你免掉很多运行时的麻烦。

出参呢,通常是一个不透明的句柄或者上下文对象。你别指望看到裸密钥,那不安全。好的SDK会把内部状态藏起来,对外只暴露一个Handle或Context*。你想想看,这样后续操作就不用重复传密钥了,多清爽。

4.2 encrypt() 与 decrypt():明⇄密的数据类型适配

这两个接口是对称的,但数据类型约束里藏着不少细节。我们拿 encrypt() 来细说。

  • 入参:明文 — 必须是 const uint8_t* 或 const ByteArray,不是字符串。传字符串容易出问题,比如内部遇到 \0 就停了。
  • 入参:附加认证数据 (AAD) — 仅认证加密模式(如 GCM、CCM)需要。数据类型同上,但可以为空。
  • 出参:密文 — 长度通常是 明文长度 + 可能的分组填充。缓冲区长度千万别猜,用 encrypt() 前置调用 去获取所需长度。
  • 出参:认证标签 — 固定长度(比如 GCM 的 16 字节),独立于密文返回,或者拼接在密文末尾。

decrypt() 的入参同理,是 const uint8_t* 的密文和标签。出参是明文缓冲区。这里有件我之前踩过的坑的事:

我曾经有一次:把密文缓冲区类型写成了 char*,结果解密出来的二进制数据里含有 \0,后续处理直接截断。查了半天才发现是类型不对。从那以后,我所有缓冲区都只用 uint8_t*,绝不偷懒。

4.3 sign() 与 verify():签名场景的数据类型“对偶”

签名和验签是一对逆操作,数据类型也是对应的。但有一个容易混淆的点:待签名数据的处理方式。

接口入参数据类型出参数据类型
sign()消息 / 消息摘要const uint8_t*签名值uint8_t* (长度由算法定)
verify()消息 / 消息摘要 + 签名值const uint8_t* (两份)验证结果bool 或 int (0失败/1成功)

签名长度这块要注意:ECDSA 的签名是两个大整数,比 RSA 短很多。你分配缓冲区时,别写死大小,得用 sign() 提供的 签名长度查询接口 来获取。我见过有人写死 64 字节存 ECDSA P-256 签名——这个倒是够,但换到 P-384 就炸了。

最佳做法:先调一次 sign(NULL, &len) 获取签名长度,再分配缓冲区,再调第二次做实际签名。这个模式在所有主流SDK里几乎都通用。

4.4 数据类型适配的“潜规则”

嗯,聊到这,我归纳几条实战中总结出来的潜规则,你写SDK或调SDK时心里有个数:

  1. 永远不用 char* 传二进制数据。理由我说过了,空字符是恶魔。
  2. 长度用 size_t 传,别用 int。 int 在某些平台上可能只有 32 位,加密数据动不动超过 2GB 就不行了。
  3. 输出参数优先用“先查询再写入”的两步模式。别假设长度,别溢出。
  4. 注意端序。大整数(RSA、ECC)在不同SDK间传递时,字节顺序是 Big-Endian 还是 Little-Endian?统一,一定要统一。
小窍门:如果你在封装SDK,设计 encrypt() 接口时,把 密文缓冲区 和 实际写入长度 都作为出参。调用方就能直接拿到“长度+内容”,省去一次查询调用。

说白了,接口设计就是数据类型的契约。你契约定清楚了,调用方就不容易出错。你契约留了模糊地带,对方就敢往 char* 里塞二进制数据。我曾经在一个项目里因为数据类型没有对齐,导致加密后的数据跨平台传输时全部解析失败——那滋味,谁踩谁知道。