Apache Commons Crypto 快速入门与集成示例

手把手完成 Apache Commons Crypto 1.2.0 的 Maven 集成与 AES-GCM 加解密回环:流式加解密与 GCM 限制、CryptoRandom 密钥生成,命令与输出双环境逐条实测。

Apache Commons CryptoApache Commons Crypto2026/10/13

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 LTSCentOS Linux 8(8.4.2105)
JDKOpenJDK 17.0.20.1OpenJDK 17.0.1
Maven3.6.33.5.4(Red Hat 3.5.4-5)
OpenSSL3.0.21.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();
    }
}

源码四个要点:

  1. 工厂入参传空 Properties 即使用默认配置,实现选择交给 Commons Crypto 自动完成;
  2. GCM 的参数对象必须是 GCMParameterSpec(128, nonce),tag 长度 128 bit,nonce 12 字节;
  3. CryptoCipher 沿用 JDK 的 Cipher.ENCRYPT_MODE/DECRYPT_MODE 常量语义(详见第三章);
  4. 输入输出用 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 包装 JDK SecureRandom,默认算法 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」。

七、参考链接

  1. Apache Commons Crypto 官方主页:https://commons.apache.org/proper/commons-crypto/
  2. 官方用户指南:https://commons.apache.org/proper/commons-crypto/userguide.html
  3. Javadoc 在线文档:https://commons.apache.org/proper/commons-crypto/javadocs/api-release/index.html
  4. Maven 中央仓库构件页:https://central.sonatype.com/artifact/org.apache.commons/commons-crypto
  5. 项目源码仓库:https://github.com/apache/commons-crypto
  6. OpenSSL 官方文档:https://www.openssl.org/docs/