Apache Commons Crypto 快速入门与集成示例
手把手完成 Apache Commons Crypto 1.2.0 的 Maven 集成与 AES-GCM 加解密回环:流式加解密与 GCM 限制、CryptoRandom 密钥生成,命令与输出双环境逐条实测。
Apache Commons Crypto 快速入门与集成示例
目标:十分钟内在真实 Linux 服务器上完成 Apache Commons Crypto 1.2.0 的 Maven 集成,跑通第一个 AES-GCM 加解密工程,并掌握流式加解密与随机数/密钥生成两大场景;文中每条命令与回显均已在 Ubuntu 22.04 与 CentOS 8 双环境逐条实测。
一、环境信息
本文所有命令与回显来自以下两台服务器,同一份代码在两端运行,用于展示 Commons Crypto 工厂在不同 OpenSSL 环境下的实现选择差异:
| 项目 | 环境1 | 环境2 |
|---|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS | CentOS Linux 8(8.4.2105) |
| JDK | OpenJDK 17.0.20.1 | OpenJDK 17.0.1 |
| Maven | 3.6.3 | 3.5.4(Red Hat 3.5.4-5) |
| OpenSSL | 3.0.2 | 1.1.1k FIPS |
| 加密实现实测结果 | 加密回退 JceCipher、随机数走 OpenSSL | 加密与随机数均走 OpenSSL |
| 组件 | Apache Commons Crypto 1.2.0 | 同左 |
集成示例只依赖 JDK 17 与 Maven,先确认两端版本:
java -version
回显(环境1,实测):
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)
mvn -version
回显(环境1,实测):
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "6.8.0-138-generic", arch: "amd64", family: "unix"
环境2 的 java -version 与 mvn -version(实测):
openjdk version "17.0.1" 2021-10-19 LTS
OpenJDK Runtime Environment 21.9 (build 17.0.1+12-LTS)
OpenJDK 64-Bit Server VM 21.9 (build 17.0.1+12-LTS, mixed mode, sharing)
Apache Maven 3.5.4 (Red Hat 3.5.4-5)
Maven home: /usr/share/maven
Java version: 1.8.0_312, vendor: Red Hat, Inc., runtime: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-2.el8_5.x86_64/jre
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "6.8.0-138-generic", arch: "amd64", family: "unix"
注意环境2 的 Maven 默认挂到了 java-1.8.0(CentOS 8 的默认 Java),直接编译 JDK 17 工程会报错(详见第六章 Q2),需先切换 JAVA_HOME:
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
设置后再执行 mvn -version,回显(环境2,实测):
Apache Maven 3.5.4 (Red Hat 3.5.4-5)
Maven home: /usr/share/maven
Java version: 17.0.1, vendor: Red Hat, Inc., runtime: /usr/lib/jvm/java-17-openjdk-17.0.1.0.12-2.el8_5.x86_64
Default locale: en_US, platform encoding: ANSI_X3.4-1968
OS name: "linux", version: "6.8.0-138-generic", arch: "amd64", family: "unix"
后文环境2 的所有 Maven 命令均在该 JAVA_HOME 已导出的前提下执行。本文两端的 Maven 工程目录分别为 /opt/quickstart(环境1)与 /opt/crypto-quickstart(环境2),散装示例统一放在 /opt/cc-lab。
二、五分钟上手:Maven 工程集成
本节从零创建一个 Maven 工程,引入 commons-crypto 依赖,跑通第一个 AES-GCM 加解密回环示例。Maven 会在首次构建时自动从中央仓库下载 commons-crypto 1.2.0 及其传递依赖(本文回显来自依赖已就绪的环境,故无下载日志)。
2.1 创建工程目录与 pom.xml
在环境1 上创建标准 Maven 目录结构:
mkdir -p /opt/quickstart/src/main/java/demo && cd /opt/quickstart
创建 pom.xml,内容如下(全文):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>crypto-quickstart</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-crypto</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
</project>
依赖坐标只有 org.apache.commons:commons-crypto:1.2.0 一项。此时工程内应当只有两个文件,用 find 核验:
find . -type f -not -path "./lib/*" -not -path "./target/*" | sort
回显(环境1,实测):
./pom.xml
./src/main/java/demo/QuickStart.java
2.2 首个示例 QuickStart.java 完整源码
创建 src/main/java/demo/QuickStart.java,全文如下。它用 CryptoRandom 生成 AES-128 密钥与 96 bit GCM nonce,再用 CryptoCipher 完成 AES/GCM/NoPadding 加解密回环:
package demo;
import org.apache.commons.crypto.cipher.CryptoCipher;
import org.apache.commons.crypto.cipher.CryptoCipherFactory;
import org.apache.commons.crypto.random.CryptoRandom;
import org.apache.commons.crypto.random.CryptoRandomFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
/**
* 快速入门:AES/GCM/NoPadding 加解密回环 + CryptoRandom 生成密钥与 nonce。
*/
public class QuickStart {
public static void main(String[] args) throws Exception {
String plainText = "Apache Commons Crypto quick start on a real server.";
// 1. 用 CryptoRandom 生成 AES-128 密钥与 96 bit GCM nonce
Properties randomProps = new Properties();
try (CryptoRandom random = CryptoRandomFactory.getCryptoRandom(randomProps)) {
System.out.println("CryptoRandom impl: " + random.getClass().getName());
byte[] keyBytes = new byte[16];
byte[] nonce = new byte[12];
random.nextBytes(keyBytes);
random.nextBytes(nonce);
// 2. 获取 CryptoCipher(默认优先 OpenSSL JNI,不可用时回退 JCE)
Properties cipherProps = new Properties();
CryptoCipher cipher = CryptoCipherFactory.getCryptoCipher("AES/GCM/NoPadding", cipherProps);
System.out.println("CryptoCipher impl: " + cipher.getClass().getName());
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, nonce);
// 3. 加密
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
ByteBuffer inBuf = ByteBuffer.allocateDirect(plainText.getBytes(StandardCharsets.UTF_8).length);
inBuf.put(plainText.getBytes(StandardCharsets.UTF_8));
inBuf.flip();
ByteBuffer encBuf = ByteBuffer.allocateDirect(plainText.length() + 16); // 预留 16B tag
cipher.doFinal(inBuf, encBuf);
encBuf.flip();
byte[] cipherText = new byte[encBuf.remaining()];
encBuf.get(cipherText);
System.out.println("key(hex) : " + toHex(keyBytes));
System.out.println("nonce(hex) : " + toHex(nonce));
System.out.println("ciphertext : " + toHex(cipherText));
// 4. 解密
cipher.init(Cipher.DECRYPT_MODE, key, spec);
ByteBuffer decBuf = ByteBuffer.allocateDirect(cipherText.length);
ByteBuffer ctBuf = ByteBuffer.allocateDirect(cipherText.length);
ctBuf.put(cipherText);
ctBuf.flip();
cipher.doFinal(ctBuf, decBuf);
decBuf.flip();
byte[] recovered = new byte[decBuf.remaining()];
decBuf.get(recovered);
String decrypted = new String(recovered, StandardCharsets.UTF_8);
System.out.println("decrypted : " + decrypted);
System.out.println("roundtrip ok : " + plainText.equals(decrypted));
cipher.close();
}
}
private static String toHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
源码四个要点:
- 工厂入参传空
Properties即使用默认配置,实现选择交给 Commons Crypto 自动完成; - GCM 的参数对象必须是
GCMParameterSpec(128, nonce),tag 长度 128 bit,nonce 12 字节; CryptoCipher沿用 JDK 的Cipher.ENCRYPT_MODE/DECRYPT_MODE常量语义(详见第三章);- 输入输出用
ByteBuffer.allocateDirect(堆外缓冲),密文缓冲按「明文长度 + 16 字节 tag」预分配。
2.3 编译与依赖树
先编译:
mvn -q compile && echo BUILD_OK
回显(环境1,实测):
BUILD_OK
环境2 在导出 JAVA_HOME 后执行同样命令,回显(实测):
BUILD_OK
再看依赖树,确认 commons-crypto 到底带了哪些传递依赖:
mvn dependency:tree
回显(环境1,实测):
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< demo:crypto-quickstart >-----------------------
[INFO] Building crypto-quickstart 1.0.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ crypto-quickstart ---
[INFO] demo:crypto-quickstart:jar:1.0.0
[INFO] \- org.apache.commons:commons-crypto:jar:1.2.0:compile
[INFO] +- net.java.dev.jna:jna:jar:5.13.0:compile
[INFO] \- com.sun.xml.bind:jaxb-impl:jar:2.3.7:compile
[INFO] +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:compile
[INFO] \- com.sun.activation:jakarta.activation:jar:1.2.2:runtime
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.863 s
[INFO] Finished at: 2026-10-13T08:37:59Z
[INFO] ------------------------------------------------------------------------
传递依赖只有三组:jna 5.13.0(JNA 实现路径的运行支撑)、jaxb-impl 2.3.7(commons-crypto 的 pom 针对 JDK 11+ 的 profile 引入,因为 JDK 11 起内置 JAXB 被移除)及其下游 jakarta.xml.bind-api 2.3.3 与 jakarta.activation 1.2.2(runtime 范围)。commons-crypto 本体仅 176035 字节,因为各平台的 OpenSSL JNI 胶水库直接内嵌在 jar 内,无需额外安装 native 包。
2.4 拷贝依赖并运行第一个示例
为了脱离 Maven 直接用 java 命令运行,先把运行期依赖拷贝到一个目录:
mvn -q dependency:copy-dependencies -DoutputDirectory=lib && ls -l lib/
回显(环境1,实测):
total 3284
-rw-r--r-- 1 root root 176035 Oct 13 08:05 commons-crypto-1.2.0.jar
-rw-r--r-- 1 root root 68453 Oct 13 08:05 jakarta.activation-1.2.2.jar
-rw-r--r-- 1 root root 115638 Oct 13 08:05 jakarta.xml.bind-api-2.3.3.jar
-rw-r--r-- 1 root root 1117669 Oct 13 08:05 jaxb-impl-2.3.7.jar
-rw-r--r-- 1 root root 1879325 Oct 13 08:05 jna-5.13.0.jar
运行示例(类路径 = 编译产物 + lib 下全部 jar):
java -Xmx512m -cp "target/classes:lib/*" demo.QuickStart
回显(环境1,实测):
CryptoRandom impl: org.apache.commons.crypto.random.OpenSslCryptoRandom
CryptoCipher impl: org.apache.commons.crypto.cipher.JceCipher
key(hex) : 27a8a6afd7b337a0efa3632203ac6259
nonce(hex) : 735b0fdaa7ddd2b55e1b2ae9
ciphertext : ffb4bc404c9e421df33139aa7f4b01b50ee9e7a7079d69d90084941d13ee92126237d181b60573539499ce5495908f34ff953fb58f8a3bfd3fa0b41ee64815f666891b
decrypted : Apache Commons Crypto quick start on a real server.
roundtrip ok : true
环境2 上以相同流程执行(工程目录 /opt/crypto-quickstart),ls -l lib/ 列出的 5 个 jar 与环境1 完全相同(大小逐一相等),运行回显(实测):
CryptoRandom impl: org.apache.commons.crypto.random.OpenSslCryptoRandom
CryptoCipher impl: org.apache.commons.crypto.cipher.OpenSslCipher
key(hex) : 70d9473762cd84b1f0bdd3c1b01ca244
nonce(hex) : 84083ccc1dbdfdf1bd763672
ciphertext : 7f5e8fd1708e3df288b04919c0c9dd9f3ef384adbb652fb4077fbf3e01a80635040f945f0ad4d0a6412a9fb0e17ca587a81c04eed54684af3757cf721867460eb07cfc
decrypted : Apache Commons Crypto quick start on a real server.
roundtrip ok : true
同一份代码、同一版本 jar,两端的 CryptoCipher impl 却不同:环境2(OpenSSL 1.1.1)选中了 OpenSSL JNI 路径的 OpenSslCipher,环境1(OpenSSL 3.0)因 JNI 胶水库初始化失败静默回退到 JceCipher;随机数两端都选中 OpenSslCryptoRandom。密钥与 nonce 每次运行都随机生成,回显中的十六进制值仅是当次运行样本。两端的密文虽由不同实现产出,但 GCM 是标准算法,互换密钥与 nonce 后可互相解密。
💡 技术拓展:Commons Crypto 的三条实现路径对比——JCE(JVM 内置软件实现,JDK 17 下带 AES-NI 内建优化,无需任何本地库)、OpenSSL JNI(jar 内嵌各平台 JNI 胶水库,调用系统 OpenSSL 的 libcrypto,长数据吞吐通常最高)、JNA(经 jna 库动态映射调用 OpenSSL,免编译但每次调用有映射开销,1.2.0 中 jna 5.13.0 只是传递依赖,工厂默认实现列表仅 OpenSslCipher 与 JceCipher 两项)。工厂按
commons.crypto.cipher.classes的默认顺序「OpenSslCipher,JceCipher」逐个尝试,首个初始化成功的胜出——这就是两端实现不同的根因。
三、CryptoCipher 直连加解密的三个关键约定
QuickStart 已经演示了最小用法,本节把直连 CryptoCipher 时最容易踩的三个 API 约定讲透:transformation 字符串与工厂回退、模式常量的来源、GCM 的参数对象要求与 nonce 一次性原则。
3.1 transformation 字符串与工厂回退行为
CryptoCipherFactory.getCryptoCipher(transformation, properties) 的第一个参数是标准 JCE 格式的 transformation 字符串「算法/模式/填充」,例如 AES/GCM/NoPadding、AES/CTR/NoPadding、AES/CBC/PKCS5Padding;第二个参数为空 Properties 时使用默认实现列表。工厂的回退是静默的:环境1 上 OpenSslCipher 初始化失败不会抛异常、不会打日志,直接换 JceCipher。想知道当前用的是哪条路径,最直接的办法就是像 QuickStart 那样打印 cipher.getClass().getName()。
3.2 ENCRYPT_MODE 常量来自 javax.crypto.Cipher
CryptoCipher 接口本身不定义模式常量,init() 的第一个参数要使用 JDK 的 javax.crypto.Cipher.ENCRYPT_MODE(值为 1)与 Cipher.DECRYPT_MODE(值为 2)。QuickStart 源码中的 import javax.crypto.Cipher; 就是为这个常量服务的,与 JDK 原生 Cipher 的编程习惯保持一致,迁移成本为零。
3.3 GCM 必须传 GCMParameterSpec,传 IvParameterSpec 直接报错
GCM 是认证加密模式,参数对象必须携带 tag 长度,因此 JDK 与 Commons Crypto 的 OpenSSL 封装都要求 GCMParameterSpec。用 CBC/CTR 习惯的 IvParameterSpec 去初始化 GCM 会直接失败。下面是反例程序 GcmIvSpec.java(全文,放在 /opt/cc-lab):
import org.apache.commons.crypto.cipher.CryptoCipher;
import org.apache.commons.crypto.cipher.CryptoCipherFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.util.Properties;
/**
* 反例演示:给 AES/GCM/NoPadding 传 IvParameterSpec(GCM 必须用 GCMParameterSpec)。
*/
public class GcmIvSpec {
public static void main(String[] args) throws Exception {
SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
IvParameterSpec wrongSpec = new IvParameterSpec(new byte[16]);
CryptoCipher cipher = CryptoCipherFactory.getCryptoCipher("AES/GCM/NoPadding", new Properties());
System.out.println("cipher impl: " + cipher.getClass().getName());
cipher.init(Cipher.ENCRYPT_MODE, key, wrongSpec);
cipher.doFinal(ByteBuffer.allocateDirect(5), ByteBuffer.allocateDirect(21));
System.out.println("encrypt ok");
}
}
编译并运行(环境1):
javac -encoding UTF-8 -cp commons-crypto-1.2.0.jar GcmIvSpec.java && java -Xmx512m -cp .:commons-crypto-1.2.0.jar GcmIvSpec
回显(环境1,实测):
cipher impl: org.apache.commons.crypto.cipher.JceCipher
Exception in thread "main" java.security.InvalidAlgorithmParameterException: AlgorithmParameterSpec not of GCMParameterSpec
at java.base/com.sun.crypto.provider.GaloisCounterMode.engineInit(GaloisCounterMode.java:281)
at java.base/javax.crypto.Cipher.implInit(Cipher.java:886)
at java.base/javax.crypto.Cipher.chooseProvider(Cipher.java:944)
at java.base/javax.crypto.Cipher.init(Cipher.java:1459)
at java.base/javax.crypto.Cipher.init(Cipher.java:1390)
at org.apache.commons.crypto.cipher.JceCipher.init(JceCipher.java:167)
at GcmIvSpec.main(GcmIvSpec.java:19)
环境2 同样编译运行,回显(实测):
cipher impl: org.apache.commons.crypto.cipher.OpenSslCipher
Exception in thread "main" java.security.InvalidAlgorithmParameterException: Illegal parameters
at org.apache.commons.crypto.cipher.OpenSslGaloisCounterMode.init(OpenSslGaloisCounterMode.java:241)
at org.apache.commons.crypto.cipher.OpenSsl.init(OpenSsl.java:225)
at org.apache.commons.crypto.cipher.OpenSslCipher.init(OpenSslCipher.java:172)
at GcmIvSpec.main(GcmIvSpec.java:19)
两条路径都拒绝 IvParameterSpec,只是报错文案不同:JCE 路径明说「not of GCMParameterSpec」,OpenSSL 封装路径只给「Illegal parameters」。改成 new GCMParameterSpec(128, nonce12字节) 即可两端口通(QuickStart 已验证)。
3.4 GCM nonce 绝不可复用
GCM 在同一密钥下重复使用 nonce 会严重破坏安全性,JDK 17 的 SunJCE 直接在代码层拒绝。用 GcmReuse.java 验证(全文,放在 /opt/cc-lab):
import org.apache.commons.crypto.cipher.CryptoCipher;
import org.apache.commons.crypto.cipher.CryptoCipherFactory;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.util.Properties;
public class GcmReuse {
public static void main(String[] args) throws Exception {
SecretKeySpec key = new SecretKeySpec(new byte[16], "AES");
GCMParameterSpec spec = new GCMParameterSpec(128, new byte[12]);
CryptoCipher cipher = CryptoCipherFactory.getCryptoCipher("AES/GCM/NoPadding", new Properties());
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
cipher.doFinal(ByteBuffer.allocateDirect(5), ByteBuffer.allocateDirect(21));
System.out.println("first encrypt ok, reusing same nonce...");
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
cipher.doFinal(ByteBuffer.allocateDirect(5), ByteBuffer.allocateDirect(21));
System.out.println("second encrypt ok");
}
}
javac -encoding UTF-8 -cp commons-crypto-1.2.0.jar GcmReuse.java && java -Xmx512m -cp .:commons-crypto-1.2.0.jar GcmReuse
回显(环境1,实测):
first encrypt ok, reusing same nonce...
Exception in thread "main" java.security.InvalidAlgorithmParameterException: Cannot reuse iv for GCM encryption
at java.base/com.sun.crypto.provider.GaloisCounterMode.init(GaloisCounterMode.java:151)
at java.base/com.sun.crypto.provider.GaloisCounterMode.engineInit(GaloisCounterMode.java:293)
at java.base/javax.crypto.Cipher.init(Cipher.java:1457)
at java.base/javax.crypto.Cipher.init(Cipher.java:1390)
at org.apache.commons.crypto.cipher.JceCipher.init(JceCipher.java:167)
at GcmReuse.main(GcmReuse.java:17)
第一次加密成功,同一 GCMParameterSpec 再次 init 加密立即抛 Cannot reuse iv for GCM encryption。工程实践中应像 QuickStart 那样每次加密前用 CryptoRandom 生成新 nonce,并把 nonce 与密文一起存储(GCM 的 nonce 不需要保密,但必须唯一)。
💡 技术拓展:三种常见分组模式对比——CBC 需随机 IV、串行加解密、有填充膨胀,本文第四章实测 86 字节明文密文膨胀到 96 字节;CTR 把分组密码变成流密码、无填充、可并行加密,实测密文与明文等长(86 字节),但同一密钥下 IV 绝不能重复;GCM 在 CTR 基础上附加认证标签,能发现密文被篡改,实测 51 字节明文产出 67 字节密文(51 + 16 字节 tag,见第二章),是当前业务首选;ECB 无 IV、相同明文块得到相同密文块,已不安全,不建议使用。流式 API 对三种模式的支持差异见第四章。
四、CryptoInputStream / CryptoOutputStream 流式加解密
对大文件加解密时,把整份数据读进内存再调 doFinal 并不现实。Commons Crypto 提供的 CryptoInputStream/CryptoOutputStream 以装饰器方式包装普通流,边读边加密、边读边解密。本节用「加密再解密后与原文件逐字节比对」的方式实测 CBC、CTR 两种模式,并演示 GCM 在流式 API 下不可用的原因。
4.1 准备明文与实验目录
在环境1 准备实验目录与 86 字节明文(jar 复用第二章 Maven 工程拷出的依赖):
mkdir -p /opt/cc-lab && cd /opt/cc-lab && cp /opt/quickstart/lib/commons-crypto-1.2.0.jar . && printf "Hello Apache Commons Crypto 1.2.0! This line is encrypted with AES on a Linux server.\n" > plain.txt && sha256sum plain.txt && wc -c plain.txt
回显(环境1,实测):
c416873ec75477a31c53cef4faeef75ce507c835109562596026907f7dd208cf plain.txt
86 plain.txt
环境2 的 jar 来自 /opt/crypto-quickstart/lib/,执行相同命令后 sha256sum 与 wc -c 的回显完全一致(同一份明文)。后续小节的编译运行均在 /opt/cc-lab 内进行。
4.2 StreamRoundtrip.java 完整源码
创建 StreamRoundtrip.java(全文)。程序把 plain.txt 加密为 plain.txt.enc,再解密为 plain.txt.dec,transformation 由命令行参数指定:
import org.apache.commons.crypto.stream.CryptoInputStream;
import org.apache.commons.crypto.stream.CryptoOutputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;
/**
* 流式加解密演示:源文件 --加密--> .enc --解--> .dec,两端 sha256sum 比对一致。
* 用法: StreamRoundtrip <transformation>
*/
public class StreamRoundtrip {
public static void main(String[] args) throws Exception {
// 注意:CryptoInputStream/CryptoOutputStream 的参数校验要求 IvParameterSpec,
// 因此流式 API 仅支持 CBC/CTR 等基于 IV 的模式,不支持 GCM(实测 1.2.0)。
String transformation = args.length > 0 ? args[0] : "AES/CTR/NoPadding";
// 16 字节 AES-128 密钥与 16 字节 IV(示例固定值,生产环境请勿复用)
byte[] key = hex("000102030405060708090a0b0c0d0e0f");
byte[] iv = hex("101112131415161718191a1b1c1d1e1f");
Properties props = new Properties();
props.setProperty(CryptoInputStream.STREAM_BUFFER_SIZE_KEY, "4096");
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec spec = new IvParameterSpec(iv);
// 加密:plain.txt -> plain.txt.enc
try (InputStream in = new FileInputStream("plain.txt");
OutputStream out = new FileOutputStream("plain.txt.enc");
CryptoOutputStream cos = new CryptoOutputStream(transformation, props, out,
keySpec, spec)) {
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) != -1) {
cos.write(buf, 0, n);
}
}
// 解密:plain.txt.enc -> plain.txt.dec
try (InputStream in = new FileInputStream("plain.txt.enc");
OutputStream out = new FileOutputStream("plain.txt.dec");
CryptoInputStream cis = new CryptoInputStream(transformation, props, in,
keySpec, spec)) {
byte[] buf = new byte[4096];
int n;
while ((n = cis.read(buf)) != -1) {
out.write(buf, 0, n);
}
}
System.out.println("transformation=" + transformation);
System.out.println("plain.txt -> plain.txt.enc -> plain.txt.dec done; verify with sha256sum");
}
private static byte[] hex(String hex) {
byte[] out = new byte[hex.length() / 2];
for (int i = 0; i < out.length; i++) {
out[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return out;
}
}
要点:CryptoInputStream.STREAM_BUFFER_SIZE_KEY 即配置参数 commons.crypto.stream.buffer.size,此处设为 4096 字节的内部缓冲;流式构造器要求显式传入 SecretKeySpec 与 AlgorithmParameterSpec,无需手工调用 init。
4.3 CTR 模式流式回环(环境1 实测)
javac -encoding UTF-8 -cp commons-crypto-1.2.0.jar StreamRoundtrip.java
编译无输出即成功。执行 CTR 模式回环并校验:
java -Xmx512m -cp .:commons-crypto-1.2.0.jar StreamRoundtrip AES/CTR/NoPadding && sha256sum plain.txt plain.txt.dec
回显(环境1,实测):
transformation=AES/CTR/NoPadding
plain.txt -> plain.txt.enc -> plain.txt.dec done; verify with sha256sum
c416873ec75477a31c53cef4faeef75ce507c835109562596026907f7dd208cf plain.txt
c416873ec75477a31c53cef4faeef75ce507c835109562596026907f7dd208cf plain.txt.dec
明文与解密结果的 sha256 完全一致,回环成立。CTR 无填充,密文与明文等长;看一下密文文件大小与内容形态:
wc -c plain.txt plain.txt.enc && od -A x -t x1z plain.txt.enc | head -4
回显(环境1,实测):
86 plain.txt
86 plain.txt.enc
172 total
000000 4f 9b 83 18 8e f5 42 1e f1 6d 86 74 ae d7 fd fe >O.....B..m.t....<
000010 ff ad ab 77 f8 4f f0 7b 36 0d 12 42 ba d7 ec 26 >...w.O.{6..B...&<
000020 fa 00 18 33 5d 8c 8b 8b 09 ff e4 49 81 bc 4e 5a >...3]......I..NZ<
000030 35 9c 05 c6 22 32 c9 10 48 b3 42 6b ba 64 f1 49 >5..."2..H.Bk.d.I<
4.4 CBC 模式流式回环(环境2 实测)
环境2 在 /opt/cc-lab 编译同一份源码后运行 CBC 模式:
javac -encoding UTF-8 -cp commons-crypto-1.2.0.jar StreamRoundtrip.java && java -Xmx512m -cp .:commons-crypto-1.2.0.jar StreamRoundtrip AES/CBC/PKCS5Padding && sha256sum plain.txt plain.txt.dec && wc -c plain.txt plain.txt.enc
回显(环境2,实测):
transformation=AES/CBC/PKCS5Padding
plain.txt -> plain.txt.enc -> plain.txt.dec done; verify with sha256sum
c416873ec75477a31c53cef4faeef75ce507c835109562596026907f7dd208cf plain.txt
c416873ec75477a31c53cef4faeef75ce507c835109562596026907f7dd208cf plain.txt.dec
86 plain.txt
96 plain.txt.enc
182 total
CBC 回环同样成立,且能看到填充膨胀:86 字节明文加密后为 96 字节(补齐到 16 字节块的整数倍),与 4.3 的 CTR 等长密文形成对照。环境1 上同样运行 CBC 实测回环成功,sha256 比对一致,密文长度同为 96 字节。
4.5 流式 API 为什么不支持 GCM
给 StreamRoundtrip 传 AES/GCM/NoPadding 会在创建 CryptoOutputStream 时立即失败。1.2.0 源码中流式实现把构造器传入的参数对象原样交给底层加密器(引自 commons-crypto 1.2.0 源码 CryptoOutputStream.initCipher()):
protected void initCipher() throws IOException {
try {
cipher.init(Cipher.ENCRYPT_MODE, key, params);
} catch (final GeneralSecurityException e) {
throw new IOException(e);
}
}
而第三章已实测:GCM 的两条实现路径都要求 GCMParameterSpec,流式 API 的设计契约(CBC/CTR 这类 IV 模式)传入的是 IvParameterSpec,二者必然冲突。实测报错(环境1,JCE 路径):
java -Xmx512m -cp .:commons-crypto-1.2.0.jar StreamRoundtrip AES/GCM/NoPadding
回显(环境1,实测):
Exception in thread "main" java.io.IOException: java.security.InvalidAlgorithmParameterException: AlgorithmParameterSpec not of GCMParameterSpec
at org.apache.commons.crypto.stream.CryptoOutputStream.initCipher(CryptoOutputStream.java:349)
at org.apache.commons.crypto.stream.CryptoOutputStream.<init>(CryptoOutputStream.java:116)
at org.apache.commons.crypto.stream.CryptoOutputStream.<init>(CryptoOutputStream.java:133)
at org.apache.commons.crypto.stream.CryptoOutputStream.<init>(CryptoOutputStream.java:155)
at StreamRoundtrip.main(StreamRoundtrip.java:36)
Caused by: java.security.InvalidAlgorithmParameterException: AlgorithmParameterSpec not of GCMParameterSpec
at java.base/com.sun.crypto.provider.GaloisCounterMode.engineInit(GaloisCounterMode.java:281)
at java.base/javax.crypto.Cipher.init(Cipher.java:1457)
at java.base/javax.crypto.Cipher.init(Cipher.java:1390)
at org.apache.commons.crypto.cipher.JceCipher.init(JceCipher.java:167)
at org.apache.commons.crypto.stream.CryptoOutputStream.initCipher(CryptoOutputStream.java:347)
... 4 more
回显(环境2,实测,OpenSSL 封装路径):
Exception in thread "main" java.io.IOException: java.security.InvalidAlgorithmParameterException: Illegal parameters
at org.apache.commons.crypto.stream.CryptoOutputStream.initCipher(CryptoOutputStream.java:349)
at org.apache.commons.crypto.stream.CryptoOutputStream.<init>(CryptoOutputStream.java:116)
at org.apache.commons.crypto.stream.CryptoOutputStream.<init>(CryptoOutputStream.java:133)
at org.apache.commons.crypto.stream.CryptoOutputStream.<init>(CryptoOutputStream.java:155)
at StreamRoundtrip.main(StreamRoundtrip.java:36)
Caused by: java.security.InvalidAlgorithmParameterException: Illegal parameters
at org.apache.commons.crypto.cipher.OpenSslGaloisCounterMode.init(OpenSslGaloisCounterMode.java:241)
at org.apache.commons.crypto.cipher.OpenSsl.init(OpenSsl.java:225)
at org.apache.commons.crypto.cipher.OpenSslCipher.init(OpenSslCipher.java:172)
at org.apache.commons.crypto.stream.CryptoOutputStream.initCipher(CryptoOutputStream.java:347)
... 4 more
两条路径退出码均为 1。结论:1.2.0 的流式 API 面向 CBC/CTR 等 IV 模式设计;需要 GCM 的流式语义时,用直连 CryptoCipher + GCMParameterSpec 自己分块处理(认证 tag 要求整条消息完整后才能校验,也不适合「边读边解密即消费」的管道模型)。
💡 技术拓展:直连 CryptoCipher 与流式 API 的选型对比——直连方式(二章、三章)以
ByteBuffer为数据入口,调用方自己分块、自己管理缓冲,支持全部 transformation(含 GCM),适合报文级、定长块级加密;流式方式(本章)以InputStream/OutputStream为入口,内部按commons.crypto.stream.buffer.size(默认 8192 字节)缓冲,对调用方透明,适合文件、网络流等大数据场景,但参数校验决定了只支持 IV 类模式。两类构造器都要求显式传 Key 与 AlgorithmParameterSpec,加解密双方必须使用同一套参数。
五、CryptoRandom 生成随机数与密钥
密钥、IV、nonce、盐都离不开密码学安全的随机数。Commons Crypto 的 CryptoRandom 接口封装了两种来源:系统 OpenSSL 的 RAND_bytes(OpenSslCryptoRandom)与 JVM 内置 SecureRandom(JavaCryptoRandom),工厂默认优先前者。本节用同一个程序实测两种实现。
5.1 GenKey.java 完整源码
创建 GenKey.java(全文,放在 /opt/cc-lab)。不带参数时走工厂默认实现,带 java 参数时通过 CLASSES_KEY 强制指定 JavaCryptoRandom:
import org.apache.commons.crypto.random.CryptoRandom;
import org.apache.commons.crypto.random.CryptoRandomFactory;
import java.util.Base64;
import java.util.Properties;
/**
* CryptoRandom 演示:生成 AES-256 密钥与 GCM nonce。
* 用法: GenKey [java]
*/
public class GenKey {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
if (args.length > 0 && "java".equals(args[0])) {
props.setProperty(CryptoRandomFactory.CLASSES_KEY,
CryptoRandomFactory.RandomProvider.JAVA.getClassName());
}
try (CryptoRandom random = CryptoRandomFactory.getCryptoRandom(props)) {
System.out.println("random impl: " + random.getClass().getName());
byte[] aes256Key = new byte[32];
random.nextBytes(aes256Key);
System.out.println("AES-256 key (hex) : " + toHex(aes256Key));
System.out.println("AES-256 key (base64): " + Base64.getEncoder().encodeToString(aes256Key));
byte[] gcmNonce = new byte[12];
random.nextBytes(gcmNonce);
System.out.println("GCM nonce (hex) : " + toHex(gcmNonce));
}
}
private static String toHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
CryptoRandomFactory.CLASSES_KEY 对应配置参数 commons.crypto.secure.random.classes;RandomProvider.JAVA.getClassName() 的取值即 org.apache.commons.crypto.random.JavaCryptoRandom。
5.2 默认实现实测(双环境)
编译并在环境1 运行:
javac -encoding UTF-8 -cp commons-crypto-1.2.0.jar GenKey.java && java -Xmx512m -cp .:commons-crypto-1.2.0.jar GenKey
回显(环境1,实测):
random impl: org.apache.commons.crypto.random.OpenSslCryptoRandom
AES-256 key (hex) : e97524f1cd5dcdde1cd0671db4db72e9d5586eedf9c11656f0767e0365f1fe16
AES-256 key (base64): 6XUk8c1dzd4c0GcdtNty6dVYbu35wRZW8HZ+A2Xx/hY=
GCM nonce (hex) : 92d3960d7cc4bc85ec64a6a1
环境2 同样编译运行,回显(实测):
random impl: org.apache.commons.crypto.random.OpenSslCryptoRandom
AES-256 key (hex) : df144338d99dd2e9103ebdb2371a804443063a368291fa1ccbde0b09852318ed
AES-256 key (base64): 3xRDONmd0ukQPr2yNxqAREMGOjaCkfocy94LCYUjGO0=
GCM nonce (hex) : 3af7f3fbe4fccffdbec4f4d7
两端默认都选中 OpenSslCryptoRandom(随机数路径对 OpenSSL 版本不敏感,与加密路径在环境1 的回退形成对比)。32 字节输出对应 AES-256 密钥,12 字节输出对应 GCM nonce;每次运行输出都不同,回显仅为当次样本。
5.3 强制 JavaCryptoRandom 对比
java -Xmx512m -cp .:commons-crypto-1.2.0.jar GenKey java
回显(环境1,实测):
random impl: org.apache.commons.crypto.random.JavaCryptoRandom
AES-256 key (hex) : e03814db2fb8b83acd77609bb1e482d40bc1304e589b5584918be4175db8c420
AES-256 key (base64): 4DgU2y+4uDrNd2CbseSC1AvBME5Ym1WEkYvkF124xCA=
GCM nonce (hex) : 4c84299ced6fe8f3ed0b0a8b
切换只需一个属性值,调用代码零改动。两种实现都实现了 CryptoRandom 的 nextBytes,业务侧无需感知差异。
💡 技术拓展:两种随机数实现对比——OpenSslCryptoRandom 通过 JNI 调用 OpenSSL 的
RAND_bytes,底层熵源是操作系统的/dev/urandom(由commons.crypto.secure.random.device.file.path控制,默认该路径),无需 JVM 预热;JavaCryptoRandom 包装 JDKSecureRandom,默认算法SHA1PRNG(由commons.crypto.secure.random.java.algorithm控制),首次取数可能触发熵池 seeding 等待。一致性要求高的跨平台环境可显式指定 JavaCryptoRandom,追求低延迟与高吞吐优先 OpenSslCryptoRandom;两者均属密码学安全随机数,可放心用于密钥生成。
六、常见错误速查
以下问答均来自本文实测过程中真实触发过的报错,可直接对照排查。
Q1:运行时报 NoClassDefFoundError: org/apache/commons/crypto/... 怎么办?
类路径缺了依赖 jar。只用编译产物运行必现:
java -Xmx512m -cp target/classes demo.QuickStart
回显(环境1,实测):
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/commons/crypto/random/CryptoRandomFactory
at demo.QuickStart.main(QuickStart.java:25)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.crypto.random.CryptoRandomFactory
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
... 1 more
解法是按 2.4 拷出依赖并把 lib/* 加进 -cp;或直接打包为带依赖的 fat jar。注意 -cp "target/classes:lib/*" 中的通配符要写在引号内,避免被 shell 展开。
Q2:CentOS 8 上 mvn clean compile 报 invalid target release: 17?
Maven 走了 JDK 8(见第一章 mvn -version 显示 Java version: 1.8.0_312)。实测报错:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project crypto-quickstart: Fatal error compiling: invalid target release: 17 -> [Help 1]
解法:export JAVA_HOME=/usr/lib/jvm/java-17-openjdk 后再编译。另一个实测细节:如果 target/ 已有编译产物,直接 mvn compile 会因增量编译跳过编译而假报 BUILD SUCCESS,务必用 clean compile 或先 mvn clean 才能暴露真实问题。需要持久化时可把 export 写入 /etc/profile.d/jdk17.sh。
Q3:流式加解密报 Illegal parameters 或 AlgorithmParameterSpec not of GCMParameterSpec?
给 CryptoInputStream/CryptoOutputStream 传了 AES/GCM/NoPadding。1.2.0 流式 API 只支持 CBC/CTR 等 IV 模式,两条实现路径的完整报错与源码原因见 4.5;GCM 需求改用直连 CryptoCipher + GCMParameterSpec(见第三章)。
Q4:GCM 加密报 Cannot reuse iv for GCM encryption?
同一密钥下复用了 nonce,JDK 17 的 SunJCE 主动拒绝(完整复现见 3.4)。GCM 的 nonce 必须每次加密新生成,用 CryptoRandom 取 12 字节即可(见第五章),nonce 本身可以明文随密文存储。
Q5:同一份代码,为什么 Ubuntu 22.04 上 CryptoCipher impl 是 JceCipher,CentOS 8 上是 OpenSslCipher?
工厂默认按「OpenSslCipher,JceCipher」顺序尝试。环境2 的 OpenSSL 1.1.1k 具备 JNI 胶水库所需的全部符号,OpenSslCipher 初始化成功;环境1 的 OpenSSL 3.0.2 移除了 JNI 层依赖的 EVP_CIPHER_CTX_block_size 等符号,OpenSslCipher 初始化失败后工厂静默回退 JceCipher(不抛异常、不打日志)。随机数不受影响,两端都是 OpenSslCryptoRandom(RAND_bytes 所需符号仍在)。排查方法是打印 getClass().getName()(见 2.4),而不是假设「装了 Commons Crypto 就一定走 OpenSSL」。
七、参考链接
- Apache Commons Crypto 官方主页:https://commons.apache.org/proper/commons-crypto/
- 官方用户指南:https://commons.apache.org/proper/commons-crypto/userguide.html
- Javadoc 在线文档:https://commons.apache.org/proper/commons-crypto/javadocs/api-release/index.html
- Maven 中央仓库构件页:https://central.sonatype.com/artifact/org.apache.commons/commons-crypto
- 项目源码仓库:https://github.com/apache/commons-crypto
- OpenSSL 官方文档:https://www.openssl.org/docs/