第2章:加密SDK架构设计——分层、跨平台与接口抽象
这一章我们聊聊加密SDK的骨架该怎么搭。说实话,很多人一上来就写加密逻辑,结果后面维护时叫苦连天。我最初做嵌入式加密库时也踩过这个坑——代码全揉在一个大文件里,平台一换,改得要命。
加密SDK的架构,说白了就三板斧:分层设计、跨平台策略、抽象接口定义。这三件事做好了,后续适配十几种语言和平台都不是事儿。
2.1 分层设计:把蛋糕切明白
分层不是为了好看,是为了隔离变化。我习惯把加密SDK切成四层,每层只干自己该干的事:
| 层级 | 职责 | 变化频率 |
|---|---|---|
| 应用层 | 对外API、参数校验、错误码映射 | 高(跟随业务需求) |
| 服务层 | 加解密流程编排、密钥生命周期管理 | 中(流程调整) |
| 核心层 | 算法实现、模式封装(CBC/GCM等) | 低(算法稳定) |
| 平台适配层 | OS系统调用、硬件安全模块、随机数生成 | 最高(每换一个平台都要改) |
为什么要把适配层单独拎出来?我在项目中遇到过这种情况:同样一个AES-256-GCM,在Windows上用CNG,在Linux上用OpenSSL,在Android上用Conscrypt。如果这些代码散落在核心层里,维护起来就是噩梦。
2.1.1 依赖方向
记住一个原则:上层依赖下层,下层绝不反向依赖上层。服务层调用核心层,核心层调用适配层。适配层通过回调或虚函数把结果往上抛,而不是直接引用上层的头文件。
为什么会这样设计?你想想看,如果核心层直接调了应用层的日志函数,那以后换日志库,整个核心层都得重新编译。嗯,这种耦合我吃过亏,后来重构时花了整整两周解耦。
2.2 跨平台策略:一次编写,到处适配
跨平台不是让所有代码都一样,而是让差异被控制在最小范围内。我这些年总结下来,最有效的做法是:C语言写底层 + 条件编译 + 工厂模式。
2.2.1 为什么选C语言
说白了,C编译器几乎覆盖所有平台。Java、Python、Go、Rust 都能通过 FFI 调用 C 库。你写个 C 的动态库,上面套一层各语言的 binding,一个加密核心就能通吃。
举个实际例子,我在做Android适配时,用的就是 JNI + C 核心:
// 核心层(纯C)
int aes_gcm_encrypt(
const uint8_t *plaintext, int plaintext_len,
const uint8_t *key, const uint8_t *iv,
uint8_t *ciphertext, uint8_t *tag
);
// JNI适配层(C++)
JNIEXPORT jbyteArray JNICALL
Java_com_example_crypto_NativeCrypto_aesGcmEncrypt(
JNIEnv *env, jobject thiz,
jbyteArray plaintext, jbyteArray key, jbyteArray iv
) {
// 参数转换
jbyte *plain = env->GetByteArrayElements(plaintext, nullptr);
// ... 调用核心层
aes_gcm_encrypt(...);
// 结果转回Java
}
2.2.2 条件编译与平台宏
适配层需要处理平台差异。我一般用一套统一的内部API,然后每个平台单独实现:
| 平台 | 随机数 | 密钥存储 | 硬件加速 |
|---|---|---|---|
| Windows | CryptGenRandom | DPAPI | CNG (bcrypt.dll) |
| Linux | /dev/urandom | Keyutils | OpenSSL EVP |
| Android | java.security.SecureRandom | Android Keystore | Conscrypt |
| iOS/macOS | SecRandomCopyBytes | Keychain Services | CommonCrypto |
每个平台写一个 .c 文件,编译时通过 -DPLATFORM_XXX 来决定链接哪个。这样核心层根本不知道底层是谁,它只管调用统一的接口。
2.3 抽象接口定义:约定大于实现
接口抽象是架构的魂。我见过太多团队一上来就定义几十个函数,结果一半是冗余的。一个好的加密SDK,接口应该精简到无法再精简。
2.3.1 核心接口设计
我一般只暴露5~6个抽象接口,其他都是内部调用:
// 加密操作
typedef struct {
int (*encrypt)(const uint8_t *in, int in_len,
const uint8_t *key, const uint8_t *iv,
uint8_t *out, uint8_t *tag);
int (*decrypt)(const uint8_t *in, int in_len,
const uint8_t *key, const uint8_t *iv,
const uint8_t *tag, uint8_t *out);
int (*key_gen)(uint8_t *key, int key_len);
int (*random_bytes)(uint8_t *buf, int len);
void (*set_log_callback)(void (*log)(const char *));
} CryptoOps;
每个平台提供一个 CryptoOps 实例。应用层拿到这个实例后,完全不用关心底层是 OpenSSL 还是硬件加密芯片。
2.3.2 错误码与异常处理
加密操作出错的场景很多:密钥格式不对、硬件不支持、内存不足……我建议统一错误码,不要每个语言各自发明一套:
| 错误码 | 宏定义 | 说明 |
|---|---|---|
| 0 | CRYPTO_OK | 正常 |
| -1 | CRYPTO_ERR_PARAM | 参数错误 |
| -2 | CRYPTO_ERR_KEY | 密钥无效 |
| -3 | CRYPTO_ERR_PLATFORM | 平台不支持 |
| -4 | CRYPTO_ERR_MEMORY | 内存不足 |
有了这套分层和抽象,后面写各种语言的binding就是纯粹的体力活。你想想看,C 核心编译成 .so / .dll / .dylib,然后 Java 用 JNI 调用,Python 用 ctypes 调用,Go 用 cgo 调用——核心不变,只改薄薄一层胶水代码。
我的经验总结:
- 分层的核心是控制依赖方向,别让底层知道上层的存在
- 跨平台不是复制粘贴,是用同一个核心 + 不同的平台壳
- 接口抽象要足够少、足够稳定,不然适配工作会翻倍
嗯,今天就聊这么多。这一套架构设计的思路,会在后面各语言适配章节里反复用到。记住它,后面你会少走很多弯路。