2. 开发环境与密码库选型
密码 API 库是应用与底层密码算法之间的“适配层”。同一段业务逻辑,换一个库、换一个平台,接口形态、依赖方式、默认参数甚至可用算法集合都可能不同。本节围绕 OpenSSL、Bouncy Castle、Web Crypto、JCE 四类主流密码 API,讲清它们各自“是什么、装什么、怎么配、跨平台要注意什么”,并给出可复现的安装与验证示例。
四类库的定位差异
先建立选型坐标系。OpenSSL 是 C 语言实现的底层密码库,同时提供命令行工具与 libcrypto/libssl 编程接口,是 Linux 服务器、Nginx、curl 等大量基础设施的密码底座;Bouncy Castle 是 Java/.NET 生态的纯实现密码库,最大价值在于补齐 JDK 未内置的算法(如国密 SM2/SM3/SM4、Ed25519 早期版本、部分 PGP 能力);Web Crypto 是浏览器与部分运行时内置的 JavaScript 密码 API,以 crypto.subtle 为核心,特点是算法受限但密钥不可导出、运行在受控环境;JCE(Java Cryptography Extension)是 JDK 自带的密码框架,通过 Provider 机制组织算法,SunJCE、SunRsaSign 等是默认 Provider。
理解这四者的关系,关键在“谁提供算法、谁提供接口”。JCE 提供统一接口,Bouncy Castle 作为 Provider 插入 JCE;OpenSSL 自成体系,Java 侧通过 JNI 或 Netty-tcnative 调用;Web Crypto 则是浏览器把密码能力封装成 Promise 风格 API。选型时先问三个问题:目标运行平台是什么、需要哪些算法、密钥是否需要持久化导出。
OpenSSL:系统级依赖与版本陷阱
OpenSSL 的安装方式因平台而异。Linux 上通常由发行版包管理器提供,例如 Ubuntu/Debian 执行 sudo apt install libssl-dev,CentOS/RHEL 执行 sudo yum install openssl-devel;macOS 可用 Homebrew 安装 brew install openssl@3,但需注意系统自带的 LibreSSL 与 Homebrew 的 OpenSSL 路径不同;Windows 官方不提供二进制包,常用 vcpkg 或第三方构建。
安装后必须验证版本与算法支持,因为 OpenSSL 1.1.1 与 3.x 在 Provider 机制、默认算法、废弃 API 上有明显差异:
openssl version -a
openssl list -digest-algorithms | grep -i sm3
openssl list -cipher-algorithms | grep -i sm4
openssl ecparam -list_curves | grep -i sm2
若输出中找不到 SM3/SM4/SM2,说明当前构建未启用国密支持,需要重新编译并加入 enable-sm2 enable-sm3 enable-sm4。跨平台时最常见的坑是“编译期头文件版本与运行期动态库版本不一致”:编译链接了 OpenSSL 3.0 的头文件,运行时却加载了 1.1.1 的 libcrypto.so,会出现符号缺失或行为异常。排查方法是 ldd 查看实际加载路径,并确认 LD_LIBRARY_PATH 与 rpath 设置。
Bouncy Castle:Provider 注册与依赖管理
Bouncy Castle 在 Java 侧以 jar 包形式引入。Maven 依赖通常包含两个坐标:bcprov-jdk18on 提供基础算法,bcpkix-jdk18on 提供证书与 PKCS 相关能力。版本号需与 JDK 匹配,jdk18on 后缀表示面向 JDK 1.8 及以上。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
引入后有两种注册方式。静态注册是在 java.security 文件中添加 security.provider.N=org.bouncycastle.jce.provider.BouncyCastleProvider;动态注册是在代码中调用 Security.addProvider(new BouncyCastleProvider())。动态注册更灵活,但要注意注册顺序:JCE 按 Provider 顺序查找算法,若 SunJCE 已提供同名算法,Bouncy Castle 可能不会被命中。需要强制使用 Bouncy Castle 时,应在 Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC") 中显式指定 Provider 名称。
依赖管理上要警惕“同名类冲突”。Bouncy Castle 的 bcprov 与某些旧版 bcprov-ext、或与其他库内嵌的 BC 类同时存在时,可能出现 NoSuchMethodError。用 mvn dependency:tree 检查是否存在多个 BC 版本,必要时用 exclusion 排除传递依赖。
Web Crypto:浏览器内置但边界明确
Web Crypto 无需安装,通过 window.crypto.subtle 访问。它的核心约束是:只能在安全上下文(HTTPS 或 localhost)使用;密钥对象默认不可导出,generateKey 生成的密钥若设置 extractable: false,则无法通过 exportKey 取出原始字节;算法集合由浏览器实现决定,AES-GCM、RSA-OAEP、ECDSA、ECDH、HMAC、SHA 系列支持较好,但国密算法通常不在标准支持范围内。
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false, // 不可导出
["encrypt", "decrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode("hello")
);
跨平台注意点集中在三处:一是 Node.js 的 webcrypto 与浏览器实现存在细微差异,部分算法参数校验更严格;二是密钥不可导出意味着无法把浏览器生成的密钥直接传给服务端,需要走 ECDH 协商或服务端下发包装密钥;三是 crypto.subtle 在非安全上下文下为 undefined,本地调试用 localhost 可绕过,但部署到 HTTP 域名会直接失败。
JCE:Provider 机制与算法可用性
JCE 随 JDK 发布,无需额外安装,但“有 JCE”不等于“有你要的算法”。JDK 8 与 JDK 17 在默认算法、密钥长度限制、Provider 列表上都有变化。查看当前环境可用 Provider 与算法的命令如下:
java -version
# 在代码中打印
for (Provider p : Security.getProviders()) {
System.out.println(p.getName() + " " + p.getVersion());
}
Cipher.getInstance("AES/GCM/NoPadding"); // 默认走 SunJCE
JDK 8 早期版本对 AES-256 存在管辖权限制,需要安装 JCE Unlimited Strength 策略文件;JDK 8u161 及以后默认放开。跨平台时若同一份代码在开发机(JDK 17)正常、在服务器(JDK 8)报 InvalidKeyException: Illegal key size,基本可定位到策略文件或 JDK 版本差异。此外,Android 的 JCE 实现与标准 JDK 不完全一致,部分算法名称、Provider 行为有差异,移动端需单独验证。
依赖管理与跨平台检查清单
把上述要点收敛成一份可执行的检查清单,便于在项目初始化或环境迁移时逐项确认:
| 检查项 | OpenSSL | Bouncy Castle | Web Crypto | JCE |
|---|---|---|---|---|
| 安装方式 | 包管理器 / 源码编译 | Maven / Gradle 依赖 | 运行时内置 | 随 JDK 发布 |
| 版本验证 | openssl version -a | mvn dependency:tree | 浏览器版本与特性检测 | java -version |
| 算法确认 | openssl list | 显式指定 Provider 名 | subtle 特性探测 | Security.getProviders() |
| 典型跨平台问题 | 头文件与动态库版本不一致 | 多版本 jar 冲突 | 非安全上下文不可用 | JDK 版本与策略文件差异 |
| 密钥可导出性 | 可导出 | 可导出 | 默认不可导出 | 可导出 |