第2章:加密SDK架构设计——分层、跨平台与接口抽象

这一章我们聊聊加密SDK的骨架该怎么搭。说实话,很多人一上来就写加密逻辑,结果后面维护时叫苦连天。我最初做嵌入式加密库时也踩过这个坑——代码全揉在一个大文件里,平台一换,改得要命。

加密SDK的架构,说白了就三板斧:分层设计、跨平台策略、抽象接口定义。这三件事做好了,后续适配十几种语言和平台都不是事儿。

2.1 分层设计:把蛋糕切明白

分层不是为了好看,是为了隔离变化。我习惯把加密SDK切成四层,每层只干自己该干的事:

层级 职责 变化频率
应用层 对外API、参数校验、错误码映射 高(跟随业务需求)
服务层 加解密流程编排、密钥生命周期管理 中(流程调整)
核心层 算法实现、模式封装(CBC/GCM等) 低(算法稳定)
平台适配层 OS系统调用、硬件安全模块、随机数生成 最高(每换一个平台都要改)

为什么要把适配层单独拎出来?我在项目中遇到过这种情况:同样一个AES-256-GCM,在Windows上用CNG,在Linux上用OpenSSL,在Android上用Conscrypt。如果这些代码散落在核心层里,维护起来就是噩梦。

我的习惯:在核心层只写纯算法逻辑,不碰任何系统API。这样单元测试也好写,随便找个桌面环境就能跑。

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
}
我曾经踩过的坑:JNI里直接操作Java对象,导致GC崩溃。后来所有跨语言调用都规规矩矩地做数据拷贝,再也不敢偷懒传引用。

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 还是硬件加密芯片。

为什么接口要这么少? 因为每个接口背后都可能涉及复杂的系统调用。接口越少,适配工作量越小。我曾在项目里定义了20多个接口,结果适配一个RTOS时崩溃了——底层根本实现不了所有功能。后来砍到6个核心接口,适配才顺利起来。

2.3.2 错误码与异常处理

加密操作出错的场景很多:密钥格式不对、硬件不支持、内存不足……我建议统一错误码,不要每个语言各自发明一套:

错误码 宏定义 说明
0 CRYPTO_OK 正常
-1 CRYPTO_ERR_PARAM 参数错误
-2 CRYPTO_ERR_KEY 密钥无效
-3 CRYPTO_ERR_PLATFORM 平台不支持
-4 CRYPTO_ERR_MEMORY 内存不足
加密SDK分层架构与跨平台映射 应用层 Java / Python / Go / Rust 等语言的 SDK 封装 服务层 加密流程编排 · 密钥生命周期 · 参数校验 核心层 AES / RSA / ECC · CBC / GCM / CCM 模式 平台适配层 系统随机数 · 硬件安全模块 · OS 加密 API 封装 Windows (CNG) bcrypt.dll Linux (OpenSSL) libcrypto.so Android (Conscrypt) JNI + Provider iOS (CommonCrypto) Security.framework 每层只依赖下一层,平台差异隔离在适配层,核心代码无需修改即可支持多平台

有了这套分层和抽象,后面写各种语言的binding就是纯粹的体力活。你想想看,C 核心编译成 .so / .dll / .dylib,然后 Java 用 JNI 调用,Python 用 ctypes 调用,Go 用 cgo 调用——核心不变,只改薄薄一层胶水代码。

我的经验总结:

  • 分层的核心是控制依赖方向,别让底层知道上层的存在
  • 跨平台不是复制粘贴,是用同一个核心 + 不同的平台壳
  • 接口抽象要足够少、足够稳定,不然适配工作会翻倍

嗯,今天就聊这么多。这一套架构设计的思路,会在后面各语言适配章节里反复用到。记住它,后面你会少走很多弯路。