Compare commits

..

4 Commits

3 changed files with 158 additions and 86 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sat Jan 24 19:51:55 HKT 2026 #Sat Jan 24 20:32:20 HKT 2026
stageCount=11 stageCount=12
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.15 baseVersion=15.15
publishVersion=15.15.10 publishVersion=15.15.11
buildCount=0 buildCount=0
baseBetaVersion=15.15.11 baseBetaVersion=15.15.12

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle #Created by .winboll/winboll_app_build.gradle
#Sat Jan 24 19:51:55 HKT 2026 #Sat Jan 24 20:32:20 HKT 2026
stageCount=11 stageCount=12
libraryProject=libappbase libraryProject=libappbase
baseVersion=15.15 baseVersion=15.15
publishVersion=15.15.10 publishVersion=15.15.11
buildCount=0 buildCount=0
baseBetaVersion=15.15.11 baseBetaVersion=15.15.12

View File

@@ -2,6 +2,9 @@ package cc.winboll.studio.libappbase.utils;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64; import android.util.Base64;
import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.LogUtils;
@@ -13,78 +16,67 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
/** /**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com> * @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @CreateTime 2026-01-24 10:00:00 * @CreateTime 2026-01-24 10:00:00
* @LastEditTime 2026-01-24 19:50:00 * @LastEditTime 2026-01-24 22:00:00
* @Describe 客户端签名工具类与服务端APKFileUtils签名/哈希校验逻辑严格对齐纯Java7实现直接读取APK内签名文件计算保证校验一致性 * @Describe 客户端签名工具类与服务端APKFileUtils签名/哈希校验逻辑严格对齐纯Java7实现兼容MT重签名遍历META-INF所有RSA文件增加PackageManager兜底方案
*/ */
public class ApkSignUtils { public class ApkSignUtils {
// ===================================== 全局常量定义 ===================================== // ===================================== 全局常量定义 =====================================
private static final String TAG = "ApkSignUtils"; private static final String TAG = "ApkSignUtils";
// APK内签名文件路径兼容大小写
private static final String CERT_RSA_UPPER = "META-INF/CERT.RSA";
private static final String CERT_RSA_LOWER = "META-INF/cert.rsa";
// 加密算法常量 // 加密算法常量
private static final String ALGORITHM_SHA1 = "SHA1"; private static final String ALGORITHM_SHA1 = "SHA1";
private static final String ALGORITHM_SHA256 = "SHA-256"; private static final String ALGORITHM_SHA256 = "SHA-256";
// 缓冲区大小常量(按业务场景区分) // 缓冲区大小常量(按业务场景区分)
private static final int BUFFER_4K = 4096; private static final int BUFFER_4K = 4096;
private static final int BUFFER_8K = 8192; private static final int BUFFER_8K = 8192;
// 签名文件目录与后缀
private static final String META_INF_DIR = "META-INF/";
private static final String RSA_SUFFIX_UPPER = ".RSA";
private static final String RSA_SUFFIX_LOWER = ".rsa";
// ===================================== 对外核心方法 ===================================== // ===================================== 对外核心方法 =====================================
/** /**
* 获取与服务端对齐的签名Base64串 * 获取与服务端对齐的签名Base64串兼容MT重签名
* 逻辑:读取APK内CERT.RSA原始字节 → SHA1摘要 → Base64.NO_WRAP编码,与服务端完全一致 * 优先逻辑:遍历APK内META-INF所有.RSA文件 → 读取第一个有效文件原始字节 → SHA1摘要 → Base64.NO_WRAP
* @param context 上下文用于获取当前应用APK的真实安装路径 * 兜底逻辑PackageManager获取系统解析的签名 → SHA1摘要 → Base64.NO_WRAP
* @param context 上下文用于获取当前应用APK路径/包信息
* @return 签名Base64字符串任意步骤失败返回null * @return 签名Base64字符串任意步骤失败返回null
*/ */
public static String getApkSignAlignedWithServer(Context context) { public static String getApkSignAlignedWithServer(Context context) {
LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始执行服务端对齐签名计算"); LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始执行服务端对齐签名计算兼容MT重签名");
// 入参空值快速校验 // 入参空值快速校验
if (context == null) { if (context == null) {
LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为null直接返回null"); LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为null直接返回null");
return null; return null;
} }
try { // 方案1优先读取APK内META-INF目录下所有RSA文件兼容MT重签名任意命名
// 1. 获取当前应用APK真实路径 String signBase64 = getSignFromApkRsaFile(context);
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); if (signBase64 != null) {
String apkPath = appInfo.sourceDir; LogUtils.d(TAG, "getApkSignAlignedWithServer: 方案1成功APK内读取RSA文件返回签名Base64");
LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功获取APK路径path=" + apkPath);
if (apkPath == null || apkPath.trim().isEmpty()) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取到的APK路径为空无法读取签名文件");
return null;
}
// 2. 读取APK内CERT.RSA原始字节流
byte[] certRawBytes = readCertRsaRawBytes(apkPath);
if (certRawBytes == null || certRawBytes.length == 0) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 读取CERT.RSA原始字节失败字节数组为空");
return null;
}
LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功读取CERT.RSA字节长度=" + certRawBytes.length);
// 3. SHA1摘要 + Base64编码服务端对齐核心步骤
MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA1);
byte[] signDigest = md.digest(certRawBytes);
String signBase64 = Base64.encodeToString(signDigest, Base64.NO_WRAP);
LogUtils.d(TAG, "getApkSignAlignedWithServer: 服务端对齐签名计算完成成功返回Base64串");
return signBase64; return signBase64;
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取SHA1算法实例失败", e);
} catch (Exception e) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 计算服务端对齐签名发生未知异常", e);
} }
// 方案2兜底 - PackageManager获取系统解析的应用签名避免APK文件读取失败
signBase64 = getSignFromPackageManager(context);
if (signBase64 != null) {
LogUtils.d(TAG, "getApkSignAlignedWithServer: 方案2成功PackageManager兜底返回签名Base64");
return signBase64;
}
// 所有方案失败
LogUtils.e(TAG, "getApkSignAlignedWithServer: 所有签名获取方案均失败");
return null; return null;
} }
/** /**
* 获取当前运行APK的SHA256哈希值 * 获取当前运行APK的SHA256哈希值兼容重签名APK
* 逻辑读取APK完整文件字节流 → SHA256摘要 → 转小写64位16进制字符串服务端同款校验逻辑 * 逻辑读取APK完整文件字节流 → SHA256摘要 → 转小写64位16进制字符串服务端同款校验逻辑
* @param context 上下文用于获取当前应用APK的真实安装路径 * @param context 上下文用于获取当前应用APK的真实安装路径
* @return SHA256小写16进制字符串任意步骤失败返回null * @return SHA256小写16进制字符串任意步骤失败返回null
@@ -97,6 +89,8 @@ public class ApkSignUtils {
return null; return null;
} }
JarFile jarFile = null;
FileInputStream fis = null;
try { try {
// 1. 获取当前应用APK真实路径 // 1. 获取当前应用APK真实路径
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo();
@@ -107,16 +101,15 @@ public class ApkSignUtils {
return null; return null;
} }
// 2. 读取APK文件并计算SHA256哈希 // 2. 读取APK文件并计算SHA256哈希(完善流关闭)
File apkFile = new File(apkPath); File apkFile = new File(apkPath);
MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA256); MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA256);
FileInputStream fis = new FileInputStream(apkFile); fis = new FileInputStream(apkFile);
byte[] buffer = new byte[BUFFER_8K]; byte[] buffer = new byte[BUFFER_8K];
int readLen; int readLen;
while ((readLen = fis.read(buffer)) != -1) { while ((readLen = fis.read(buffer)) != -1) {
md.update(buffer, 0, readLen); md.update(buffer, 0, readLen);
} }
fis.close();
LogUtils.d(TAG, "getApkSHA256Hash: APK文件读取完成开始转换哈希结果"); LogUtils.d(TAG, "getApkSHA256Hash: APK文件读取完成开始转换哈希结果");
// 3. 哈希字节数组转小写64位16进制字符串 // 3. 哈希字节数组转小写64位16进制字符串
@@ -133,50 +126,127 @@ public class ApkSignUtils {
LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败", e); LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败", e);
} catch (Exception e) { } catch (Exception e) {
LogUtils.e(TAG, "getApkSHA256Hash: 计算APK SHA256哈希发生未知异常", e); LogUtils.e(TAG, "getApkSHA256Hash: 计算APK SHA256哈希发生未知异常", e);
} finally {
// 强制关闭流避免重签名APK解析的流泄漏
try {
if (fis != null) fis.close();
if (jarFile != null) jarFile.close();
} catch (IOException e) {
LogUtils.e(TAG, "getApkSHA256Hash: 关闭流资源异常", e);
}
} }
return null; return null;
} }
// ===================================== 内部工具方法 ===================================== // ===================================== 内部核心工具方法(兼容重签名) =====================================
/** /**
* 读取APK内CERT.RSA文件的原始字节流兼容大小写命 * 方案1遍历APK内META-INF所有.RSA/.rsa文件读取第一个有效文件计算签
* @param apkPath APK文件的完整绝对路径 * @param context 上下文
* @return CERT.RSA原始字节数组未找到文件返回null * @return 签名Base64失败返回null
* @throws Exception 流读取、APK解析相关异常向上抛出
*/ */
private static byte[] readCertRsaRawBytes(String apkPath) throws Exception { private static String getSignFromApkRsaFile(Context context) {
LogUtils.d(TAG, "readCertRsaRawBytes: 方法调用开始读取APK内签名文件apkPath=" + apkPath); JarFile jarFile = null;
JarFile jarFile = new JarFile(apkPath); InputStream is = null;
JarEntry certEntry = null; try {
// 获取APK路径
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo();
String apkPath = appInfo.sourceDir;
if (apkPath == null || apkPath.trim().isEmpty()) {
LogUtils.w(TAG, "getSignFromApkRsaFile: APK路径为空跳过该方案");
return null;
}
// 优先读取大写命名,不存在则尝试小写 // 打开APK的JarFile
certEntry = jarFile.getJarEntry(CERT_RSA_UPPER); jarFile = new JarFile(apkPath);
if (certEntry == null) { Enumeration<JarEntry> entries = jarFile.entries();
LogUtils.d(TAG, "readCertRsaRawBytes: 未找到META-INF/CERT.RSA尝试读取小写META-INF/cert.rsa"); JarEntry targetRsaEntry = null;
certEntry = jarFile.getJarEntry(CERT_RSA_LOWER);
}
// 未找到有效签名文件关闭流后返回null // 遍历所有条目找到META-INF下第一个.RSA/.rsa文件
if (certEntry == null) { while (entries.hasMoreElements()) {
LogUtils.e(TAG, "readCertRsaRawBytes: APK内未找到CERT.RSA/cert.rsa签名文件"); JarEntry entry = entries.nextElement();
jarFile.close(); String entryName = entry.getName();
// 过滤META-INF目录下 + 以.RSA/.rsa结尾 + 非目录
if (entryName.startsWith(META_INF_DIR) && !entry.isDirectory()
&& (entryName.endsWith(RSA_SUFFIX_UPPER) || entryName.endsWith(RSA_SUFFIX_LOWER))) {
targetRsaEntry = entry;
LogUtils.d(TAG, "getSignFromApkRsaFile: 找到有效签名文件name=" + entryName);
break; // 取第一个有效RSA文件即可
}
}
// 未找到任何RSA文件
if (targetRsaEntry == null) {
LogUtils.w(TAG, "getSignFromApkRsaFile: 未在META-INF找到任何.RSA/.rsa签名文件");
return null;
}
// 读取RSA文件原始字节
is = jarFile.getInputStream(targetRsaEntry);
byte[] certRawBytes = readStreamToBytes(is);
if (certRawBytes == null || certRawBytes.length == 0) {
LogUtils.w(TAG, "getSignFromApkRsaFile: 读取RSA文件字节为空");
return null;
}
// 计算SHA1+Base64
MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA1);
byte[] signDigest = md.digest(certRawBytes);
return Base64.encodeToString(signDigest, Base64.NO_WRAP);
} catch (Exception e) {
LogUtils.e(TAG, "getSignFromApkRsaFile: 从APK内读取RSA文件失败", e);
return null; return null;
} finally {
// 强制关闭所有流
try {
if (is != null) is.close();
if (jarFile != null) jarFile.close();
} catch (IOException e) {
LogUtils.e(TAG, "getSignFromApkRsaFile: 关闭流资源异常", e);
}
} }
// 读取文件原始字节并关闭所有流资源
InputStream is = jarFile.getInputStream(certEntry);
byte[] certBytes = readStreamToBytes(is);
is.close();
jarFile.close();
LogUtils.d(TAG, "readCertRsaRawBytes: 签名文件读取完成,字节长度=" + certBytes.length);
return certBytes;
} }
/** /**
* 输入流转字节数组,通用工具方法 * 方案2兜底 - 通过PackageManager获取系统解析的应用签名
* 4K缓冲区适配小文件读取如CERT.RSA保证流资源正常关闭 * 避免APK文件读取失败如权限、解析问题兼容所有重签名场景
* @param context 上下文
* @return 签名Base64失败返回null
*/
private static String getSignFromPackageManager(Context context) {
try {
// 获取当前应用包信息(包含签名)
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
if (packageInfo == null || packageInfo.signatures == null || packageInfo.signatures.length == 0) {
LogUtils.w(TAG, "getSignFromPackageManager: 未获取到应用签名信息");
return null;
}
// 取第一个签名(重签名后一般只有一个签名)
Signature signature = packageInfo.signatures[0];
byte[] signBytes = signature.toByteArray();
// 计算SHA1+Base64与服务端逻辑对齐
MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA1);
byte[] signDigest = md.digest(signBytes);
return Base64.encodeToString(signDigest, Base64.NO_WRAP);
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getSignFromPackageManager: 包名未找到,无法获取签名", e);
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getSignFromPackageManager: 获取SHA1算法实例失败", e);
} catch (Exception e) {
LogUtils.e(TAG, "getSignFromPackageManager: PackageManager获取签名失败", e);
}
return null;
}
/**
* 输入流转字节数组通用工具方法完善try-finally
* 4K缓冲区适配小文件读取如RSA签名文件保证流资源正常关闭
* @param is 待读取的输入流 * @param is 待读取的输入流
* @return 转换后的字节数组流为null返回空字节数组 * @return 转换后的字节数组流为null/读取失败返回空字节数组
* @throws IOException 流读取相关异常向上抛出 * @throws IOException 流读取相关异常向上抛出
*/ */
private static byte[] readStreamToBytes(InputStream is) throws IOException { private static byte[] readStreamToBytes(InputStream is) throws IOException {
@@ -187,14 +257,16 @@ public class ApkSignUtils {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_4K]; byte[] buffer = new byte[BUFFER_4K];
int readLen; int readLen;
while ((readLen = is.read(buffer)) != -1) { try {
bos.write(buffer, 0, readLen); while ((readLen = is.read(buffer)) != -1) {
bos.write(buffer, 0, readLen);
}
return bos.toByteArray();
} finally {
// 强制关闭所有流
is.close();
bos.close();
} }
byte[] result = bos.toByteArray();
// 关闭流资源,避免内存泄漏
is.close();
bos.close();
return result;
} }
} }