4. 常见加密SDK接口设计模式:init()、encrypt()、decrypt()、sign()、verify()——入参与出参的数据类型约束
聊到加密SDK的接口设计,我脑子里总会蹦出几个“老朋友”——init()、encrypt()、decrypt()、sign()、verify()。你想想看,不管你是用OpenSSL、Bouncy Castle,还是自研的SDK,核心跑不出这五个接口。它们就像乐高积木的基本模块,但玩好这些积木,关键在于弄清楚每一块的“接口”——说白了,就是入参和出参的数据类型约束。
做项目这么多年,我见过太多因为数据类型没对齐而翻车的案例。有的是把uint8_t*当字符串传,结果遇⻅\0提前截断;有的是签名时缓冲区给小了,跑起来直接段错误。嗯,这些坑,今天咱们一个一个填上。
4.1 init() 接口:密钥与上下文的“户口本”
init() 是所有操作的前提。它负责把密钥材料、算法参数、工作模式这些东西灌进一个上下文对象里。入参的数据类型,往往是整个SDK数据适配的“第一关”。
| 入参类别 | 常见数据类型 | 长度约束 | 备注 |
|---|---|---|---|
| 对称密钥 | const uint8_t* / ByteArray | 16 / 24 / 32 字节 | AES-128 / 192 / 256 |
| 非对称密钥对 | KeyPair 对象 | 由算法决定 | RSA-2048 / ECC-P256 |
| 算法标识 | 枚举 / 整数 | 4 字节 | SM4, AES-GCM, RSA-OAEP |
| 工作模式 | 枚举 / 整数 | 4 字节 | ECB, CBC, GCM |
| 随机数源 | 函数指针 / 句柄 | 平台相关 | 可选参数 |
出参呢,通常是一个不透明的句柄或者上下文对象。你别指望看到裸密钥,那不安全。好的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时心里有个数:
- 永远不用
char*传二进制数据。理由我说过了,空字符是恶魔。 - 长度用
size_t传,别用int。int在某些平台上可能只有 32 位,加密数据动不动超过 2GB 就不行了。 - 输出参数优先用“先查询再写入”的两步模式。别假设长度,别溢出。
- 注意端序。大整数(RSA、ECC)在不同SDK间传递时,字节顺序是 Big-Endian 还是 Little-Endian?统一,一定要统一。
encrypt() 接口时,把 密文缓冲区 和 实际写入长度 都作为出参。调用方就能直接拿到“长度+内容”,省去一次查询调用。
说白了,接口设计就是数据类型的契约。你契约定清楚了,调用方就不容易出错。你契约留了模糊地带,对方就敢往 char* 里塞二进制数据。我曾经在一个项目里因为数据类型没有对齐,导致加密后的数据跨平台传输时全部解析失败——那滋味,谁踩谁知道。