MessageDigest / Cipher / ZipFile / JarFile 安全与压缩源码
概述
java.security 与 java.util.zip 看似无关,却在 JarFile 处交汇:JAR 文件本质是带清单与签名的 ZIP。安全侧,MessageDigest(哈希)、Signature(签名)、Cipher(加解密)都建立在 Provider 架构上——Provider 注册算法名到实现类的映射,getInstance 按算法名查找并反射实例化;压缩侧,Deflater / Inflater 是 zlib 的 JNI 封装,ZipFile 解析 ZIP 二进制结构。
两条链路共同的思路:JDK 框架层负责 API 与流程编排,native/SPI 层负责实际算法。本文基于 OpenJDK 21 源码拆解安全与压缩两条链路。
java.security 安全源码解析
① MessageDigest.getInstance("SHA-256") 的 Provider 查找
java
public static MessageDigest getInstance(String algorithm) throws NoSuchAlgorithmException {
...
List<Service> services = GetInstance.getServices("MessageDigest", algorithm); // ① 查服务
MessageDigest md = GetInstance.getInstance(services, ...); // ② 实例化
...
}
// GetInstance 内部
static List<Service> getServices(String type, String algorithm) {
// 遍历已安装 Provider,按算法名匹配 Service
for (Provider provider : providers) {
Service service = provider.getService(type, algorithm); // ③ provider 内精确匹配
if (service != null) services.add(service);
}
...
}getInstance("SHA-256")语义:在所有已安装 Provider 中查找类型为MessageDigest、算法名为SHA-256的Service,返回匹配列表按 Provider 顺序排列。Service是Provider内的一个注册项:含类型、算法名、实现类名与属性;provider.getService(type, algorithm)在 Provider 的LinkedHashMap缓存中查。- 首个可用服务即胜出;找不到抛
NoSuchAlgorithmException。JDK 内建SUNprovider 注册了MessageDigest.SHA-256→sun.security.provider.SHA2。
② MessageDigest.digest(byte[] input) 的哈希计算
java
public byte[] digest(byte[] input) {
update(input); // ① 吸收输入
return digest(); // ② 结算
}
public byte[] digest() {
...
byte[] result = engineDigest(); // ③ 委托 SPI
...
engineReset(); // ④ 重置供复用
return result;
}
// sun.security.provider.DigestBase(SHA2 的父类)
protected byte[] engineDigest() {
...
implCompress(buffer, 0); // ⑤ 压缩最后一块
...
}MessageDigest是模板方法模式的骨架:update→engineUpdate累积输入到内部缓冲(如 SHA-256 的 64 字节块),digest→engineDigest结算。DigestBase负责缓冲与长度计数:块不满时补填充(SHA-2 的 0x80 填充 + 64 位长度),implCompress是 native/核心压缩函数(每 64 字节一轮)。digest()后自动engineReset,同一个MessageDigest实例可反复使用;digest(input)是"单次吸收 + 结算"的便捷组合。
③ Signature.getInstance("SHA256withRSA") 的数字签名
java
public final void initSign(PrivateKey privateKey) throws InvalidKeyException {
...
engineInitSign(privateKey); // ① 绑定私钥
...
}
public final byte[] sign() throws SignatureException {
...
return engineSign(); // ② 签名
}
// sun.security.rsa.RSASignature
protected byte[] engineSign() {
...
byte[] digest = md.digest(); // ③ 内部先做 SHA-256 哈希
byte[] encoded = RSAUtil.encodeSignature(digest, algId); // ④ 编码为 DigestInfo
return RSACore.rsa(encoded, privKey); // ⑤ RSA 私钥运算
}- 算法名
SHA256withRSA是"哈希 + 签名"的组合:RSASignature内部持有一个MessageDigest(SHA-256),engineUpdate喂给它,engineSign时先出摘要。 - 摘要按 PKCS#1 编码成
DigestInfo(0x30 ...结构 + OID + 摘要值),再交给RSACore.rsa做私钥模幂运算得到签名。 - 验证侧对称:
initVerify(publicKey)→engineUpdate→engineVerify(sigBytes),用公钥解密后比对 DigestInfo。
④ Cipher.getInstance("AES/GCM/NoPadding") 的加密
java
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, key, gcmSpec); // ① engineInit 初始化
byte[] ct = c.update(pt); // ② engineUpdate 增量处理
byte[] fin = c.doFinal(); // ③ engineDoFinal 收尾
// com.sun.crypto.provider.AES/GCM(CipherSpi 子类)
protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) {
...
gcm = new GCM(provider); // ④ 底层 GCM 引擎
}
protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) {
...
return gcm.doFinal(buffer, 0, len); // ⑤ GCM 认证加密
}- 转换名
AES/GCM/NoPadding三段式:算法AES/ 模式GCM/ 填充NoPadding;Cipher.getInstance解析三段并定位CipherSpi实现。 Cipher是有状态引擎:ENCRYPT_MODE/DECRYPT_MODE决定方向,init注入密钥、参数(GCM 的 IV 与 tag 长度)与随机源。update处理分块数据,doFinal完成最后一块并输出认证标签(GCM 模式下ciphertext + tag);AES 核心字节运算在AESCrypt中展开为查表实现,GCM 模式在GCTR计数器模式上加 GMAC 认证。
⑤ Provider 的安全服务注册
java
// sun.security.provider.Sun(SUN provider)
public Sun() {
super("SUN", 1.9, "SUN (RSA, DES, ...)");
...
put("MessageDigest.SHA-256", "sun.security.provider.SHA2");
put("MessageDigest.SHA-1", "sun.security.provider.SHA");
put("KeyFactory.RSA", "sun.security.rsa.RSAKeyFactory");
put("Signature.SHA256withRSA", "sun.security.rsa.RSASignature$SHA256withRSA");
put("SecureRandom.SHA1PRNG", "sun.security.provider.SecureRandom");
...
}Provider.put("Type.Algorithm", "实现类")是注册的核心 API:键是类型.算法名,值是实现类全限定名,getInstance据此反射创建。- 属性可附加别名与限制:
put("MessageDigest.SHA-256 ImplementedIn", "Software")之类的键值对描述实现特性;put("Signature.SHA256withRSA KeySize", "1024")可声明约束。 Service是Provider内部对该条目的封装:getService(type, algorithm)返回缓存的服务对象,service.newInstance()经反射实例化实现类(可能带构造参数选择)。- 第三方安全实现(如 Bouncy Castle)正是通过
Provider注册 +Security.addProvider接入 JDK 算法的。
java.util.zip 压缩源码解析
⑥ Deflater.deflate(byte[] b, int off, int len) 的压缩
c
// native(java/util/zip/Deflater.c)
JNIEXPORT jint JNICALL Java_java_util_zip_Deflater_deflateBytes(JNIEnv *env, jobject this,
jarray b, jint off, jint len, jint flush) {
z_stream *strm = jlong_to_ptr(env->GetLongField(this, strmID)); // ① 取 zlib 流
...
strm->next_in = ...; strm->avail_in = ...;
strm->next_out = ...; strm->avail_out = ...;
ret = deflate(strm, flush); // ② zlib 压缩
...
return output produced;
}Deflater构造时 native 层调用deflateInit2_初始化z_stream,配置压缩级别(默认 6)、窗口位数(默认 15)、内存级别与 ZLIB 封装。deflateBytes每次把 Java 缓冲区的输入交给 zlib 的deflate():输入不足返回Z_BUF_ERROR(可用needsInput判断),数据已出可返回finish判定。deflate()内部实现 DEFLATE 算法:LZ77 匹配(哈希链查最近重复串)+ Huffman 编码(动态/静态码表);setLevel/setStrategy(默认/过滤/哈夫曼)改变压缩权衡。finished()为 true 表示所有输入已压缩完成;压缩产物是带 ZLIB 头(2 字节 + 校验)的流格式。
⑦ Inflater.inflate(byte[] b, int off, int len) 的解压
c
// native(java/util/zip/Inflater.c)
JNIEXPORT jint JNICALL Java_java_util_zip_Inflater_inflateBytes(JNIEnv *env, jobject this,
jarray b, jint off, jint len) {
z_stream *strm = jlong_to_ptr(...); // ① zlib 流
...
ret = inflate(strm, Z_NO_FLUSH); // ② zlib 解压
...
}Inflater构造时inflateInit2_初始化z_stream;默认期望 ZLIB 封装,nowrap = true时用inflateInit2(15 - 16)接受裸 DEFLATE 流。inflate()逐块还原:内部维护位缓冲与 Huffman 树解码状态,输出写入 Java 缓冲区;输入耗尽返回Z_BUF_ERROR(needsInput()为 true)。- 解压完成时
finished()为 true,getRemaining()给出未被消费的输入字节;InflaterInputStream与ZipFile的条目读取都基于它构建。
⑧ ZipFile(String name) 的 ZIP 读取
java
public ZipFile(String name, Charset charset) throws IOException {
this(new File(name), ZIP_DEFAULT_STORAGE_ORDER, charset);
// ① 打开文件并解析中央目录
}
// 构造核心
private ZipFile(File file, int mode, Charset charset) throws IOException {
...
jzfile = open(name, mode, lastModifiedTime, usemap); // ② native 打开
...
zsrc = new Source(jzfile, ch); // ③ 建立条目索引
...
}
// Source 构造:解析 End Of Central Directory
private Source(long zfile, ZipCoder zc) {
...
// ④ 定位 END 记录,读取中央目录偏移与条目数
long end = findEND(); // 扫描 EOCD 签名 0x06054b50
...
cen = new ENDHeader(end); // ⑤ END 记录
// ⑥ 读取 CEN 目录:每个 Central Directory File Header(0x02014b50)
...
}- ZIP 文件结构三段:每个条目前的 LOC(Local File Header,
0x04034b50)+ 数据,文件尾部是 CEN(Central Directory)与 END(End of Central Directory,0x06054b50)。 findEND从文件尾部向前扫描定位 END 记录,ENDHeader解析出中央目录偏移与总条目数,再一次性读取整个中央目录建立偏移索引。- 条目元信息(压缩方式、CRC、压缩/原始大小、名字)来自 CEN 记录;
ZipEntry对象按需从 CEN 索引生成,名字经ZipCoder按指定字符集解码。
⑨ ZipFile.entries().asIterator() 的 ZIP 条目遍历
java
public Enumeration<? extends ZipEntry> entries() {
return new Enumeration<ZipEntry>() {
private int i = 0;
public boolean hasMoreElements() {
return i < zsrc.getTotal(); // ① 中央目录总条目数
}
public ZipEntry nextElement() {
synchronized (ZipFile.this) {
ZipEntry e = zsrc.getEntry(i); // ② 按索引取条目
...
return e;
}
}
};
}
// 读取条目内容
public InputStream getInputStream(ZipEntry e) {
...
// ③ 定位 LOC 记录,取数据偏移
// ④ 按压缩方式包装流
return getInputStream(pos, entry.csize, entry.size, entry.method, ...);
}- 遍历基于中央目录索引而非物理顺序:
getEntry(i)按 CEN 数组下标返回ZipEntry,ZipFile的迭代因此与目录内条目声明顺序一致。 getInputStream(entry)解析对应 LOC 记录定位数据起始偏移;按entry.method分流——DEFLATED包InflaterInputStream、STORED直接读原始字节。- 每次
getInputStream打开独立的输入流,读完必须close;ZipFile本身也需close()释放jzfile指针与映射(zsrc.close→ nativeclose)。
JarFile 清单与签名
⑩ JarFile 的 Manifest 读取
java
public Manifest getManifest() throws IOException {
return getManifestFromReference(); // ① 缓存优先
}
private synchronized Manifest getManifestFromReference() throws IOException {
Manifest man = manRef.get();
if (man == null) {
JarEntry manEntry = getManEntry(MANIFEST_NAME); // ② 找 META-INF/MANIFEST.MF
if (manEntry != null) {
man = new Manifest(super.getInputStream(manEntry)); // ③ 读条目构造
manRef = new SoftReference<>(man); // ④ 软引用缓存
}
}
return man;
}
// Manifest 解析
public void read(InputStream is) throws IOException {
...
LineReader lr = new LineReader(is);
// ⑤ 逐行解析:主属性段 + 各条目段
parse(man, lr); // Attributes 键值对
}JarFile extends ZipFile,MANIFEST_NAME(META-INF/MANIFEST.MF)是约定名称;清单缺失时getManifest()返回null。Manifest.read按 72 字节折行规则解析(续行以空格开头),主属性段之后是每个条目(类/资源)的Name+ 属性列表,存入Attributes。Manifest.Entry(内部类)把属性组织成可迭代结构;getAttributes(name)按条目名取属性——签名验证与 classpath 解析都依赖它。
⑪ JarFile 的签名验证(jar 验证)
java
public synchronized Enumeration<JarEntry> entries() { ... }
// JarEntry 签名
public CodeSigner[] getCodeSigners() {
JarFile jf = ...;
return jf.getCodeSigners(this); // ① 委托 JarFile
}
// JarVerifier 验证核心
void processEntry(JarEntry je) throws IOException {
Attributes attrs = ...;
// ② 读该条目的 .SF 与 .DSA/.RSA 签名块文件
// ③ MessageDigest 校验条目数据哈希
// ④ Signature.verify 校验签名块
signer = new CodeSigner(certPath, timestamp); // ⑤ 组装 CodeSigner
}- JAR 签名体系三文件:
MANIFEST.MF(声明每个条目的 SHA 摘要)、META-INF/xxx.SF(对清单的签名 + 各条目再摘要)、META-INF/xxx.RSA/DSA(对.SF的数字签名块)。 JarVerifier验证三步:用MessageDigest重算条目数据哈希对照.SF摘要 → 用.SF摘要对照清单摘要 → 用Signature(如 SHA256withRSA)验证签名块与签名证书。- 验证通过后
JarEntry.getCodeSigners()返回CodeSigner[](含证书链与可选时间戳);失败或篡改抛SecurityException——这就是"jar 文件防篡改"与jarsigner工具运行的底层机制。