4. 密码算法抽象接口设计

密码敏捷架构能否真正落地,关键不在于“支持多少种算法”,而在于上层业务代码是否与具体算法解耦。如果每个业务模块都直接调用 SM4_encrypt()、RSA_sign() 这类具体函数,那么算法替换就意味着全量代码改造。抽象接口设计的核心目标,是把“做什么密码操作”与“用哪个算法实现”彻底分离,让业务只依赖稳定的接口契约,算法实现则作为可插拔的组件在运行时装配。

接口分域:按密码功能而非按算法划分

统一接口应当按密码学功能域划分,而不是按算法名称划分。一个合理的接口集合通常包含六类:对称加密/解密、非对称加密/解密、数字签名/验签、哈希摘要、消息认证码、密钥派生。每一类接口只描述输入输出的语义,不暴露算法内部细节。例如对称加密接口的语义是“给定明文、密钥标识、工作模式和初始向量,返回密文与认证标签”,至于底层是 SM4-CBC 还是 AES-GCM,由实现层决定。

接口设计中有三个容易踩坑的地方。第一,密钥不能以裸字节数组在接口间传递,而应使用不透明的密钥句柄(key handle)或密钥引用,这样密钥的生命周期管理、权限校验和销毁都能收敛到密钥管理模块。第二,算法标识应作为参数而非接口名的一部分,例如 encrypt(algId, keyRef, plaintext, params),而不是为每种算法定义独立方法。第三,参数对象要能容纳不同算法的差异化配置,如分组模式、填充方式、IV、AAD、曲线参数等,通常用键值映射或带默认值的参数结构承载。

密码算法抽象接口分层与动态切换 业务应用层(仅依赖接口契约,不感知算法) 数据加密存储 报文签名验签 口令派生密钥 完整性校验 统一密码服务接口层(CryptoService) encrypt / decrypt 对称加解密 sign / verify 签名验签 hash / mac 摘要与认证 deriveKey 密钥派生 wrap / unwrap 密钥封装 keyRef 密钥句柄 算法路由与策略层(按 algId + 策略选择实现) 算法注册表 Registry 策略配置(默认/灰度) 版本与兼容映射 算法实现插件层(可并行共存、热插拔) SM4 实现 CBC / GCM AES 实现 GCM / CTR SM2 / ECDSA 签名验签 SM3 / SHA-256 哈希摘要 PBKDF2 / HKDF 密钥派生 切换 algId 即可替换实现,业务代码零改动

接口契约的关键约束

抽象接口要真正支撑动态切换,必须在契约层面约定几条硬约束。其一,所有接口方法必须是无状态的或显式携带上下文的,不能把算法状态藏在实现对象的成员变量里,否则并发调用会互相污染。其二,错误语义要统一:密钥不存在、算法不支持、参数非法、认证失败应映射为稳定的错误码,而不是让上层去解析各实现的异常文本。其三,接口要支持能力查询,例如 supports(algId, operation) 和 getCapabilities(),让路由层在切换前就能判断目标算法是否可用,避免运行到一半才失败。

下面给出一个精简的接口定义示例,用伪代码表达契约语义:

interface CryptoService {
    // 对称加解密:algId 如 "SM4-CBC"、"AES-GCM"
    CipherResult encrypt(String algId, KeyRef key, byte[] plaintext, CryptoParams params);
    byte[]        decrypt(String algId, KeyRef key, byte[] ciphertext, CryptoParams params);

    // 签名与验签:algId 如 "SM2-SM3"、"ECDSA-P256-SHA256"
    byte[] sign(String algId, KeyRef key, byte[] message, CryptoParams params);
    boolean verify(String algId, KeyRef key, byte[] message, byte[] signature, CryptoParams params);

    // 哈希与密钥派生
    byte[] hash(String algId, byte[] data);
    byte[] deriveKey(String algId, KeyRef baseKey, byte[] salt, int outLen, CryptoParams params);

    // 能力查询与密钥句柄
    boolean supports(String algId, Operation op);
    KeyRef  importKey(String algId, byte[] rawKey, KeyUsage usage);
}

注意 KeyRef 是句柄而非密钥本身,CryptoParams 承载 IV、AAD、填充模式等算法相关参数。这样同一套接口既能表达国密算法,也能表达国际算法,新增算法只需实现接口并注册,不需要修改接口定义。

多算法并行与动态切换的实现要点

多算法并行意味着同一时刻 SM4 与 AES 实现同时存在于进程中,由算法标识区分。实现上通常维护一张注册表,键为 algId,值为实现工厂或实例。动态切换有两种粒度:一是调用级切换,业务在每次调用时传入不同的 algId,适合灰度发布和双写比对;二是配置级切换,通过策略配置指定某类操作的默认算法,业务不传 algId 时走默认值。灰度场景下可以按流量比例或按数据标识哈希取模,把一部分请求路由到新算法,同时保留旧算法解密历史数据的能力。

切换过程中最需要警惕的是数据兼容性。新算法加密的数据,旧版本可能无法解密,因此切换通常遵循“先双读后双写、再停旧写、最后停旧读”的节奏。接口层应支持在解密时按密文头部携带的算法标识自动选择实现,而不是依赖调用方记忆。一个常见做法是在密文前附加算法标识和版本号的头部,解密接口读取头部后路由到对应实现,这样历史数据无需迁移即可平滑过渡。

设计评审时可以用三个问题检验接口是否合格:新增一种算法是否需要改动接口定义?业务代码中是否出现了具体算法名称?密钥是否以明文形式穿过接口边界?三个答案都应为“否”。

落地检查清单

检查项合格标准
接口按功能域划分加解密、签名验签、哈希、KDF、MAC 各自独立,无算法专属方法
密钥传递方式仅传递 KeyRef 句柄,密钥材料不出密钥管理边界
算法标识参数化algId 作为参数传入,路由层据此选择实现
能力查询提供 supports / getCapabilities,切换前可预判可用性
错误码统一密钥缺失、算法不支持、认证失败等有稳定错误码
密文自描述密文头部含算法标识与版本,解密可自动路由
并发安全实现无共享可变状态,或状态显式随上下文传递

抽象接口设计看似只是定义几个方法签名,实际上它决定了整个密码敏捷架构的替换成本上限。接口划分得越贴近密码功能语义、越少暴露算法细节,后续引入新算法、淘汰旧算法、应对合规要求变化时,改动就越集中在实现层和配置层,业务侧几乎无感。这也是把“算法可替换”从口号变成工程能力的起点。