From 0aaf71f285639c5ddab9c9ea0b44d80c95df9216 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sat, 24 Jan 2026 19:50:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E8=AF=81=E4=B9=A6=E4=BF=AE=E6=94=B9=E5=90=8E=E7=9A=84=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=E8=AF=86=E5=88=AB=E8=83=BD=E5=8A=9B=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appbase/build.properties | 4 +- libappbase/build.properties | 4 +- .../studio/libappbase/utils/ApkSignUtils.java | 228 ++++++++++++------ 3 files changed, 154 insertions(+), 82 deletions(-) diff --git a/appbase/build.properties b/appbase/build.properties index 528b30b..5fde0d2 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Jan 24 12:31:08 HKT 2026 +#Sat Jan 24 11:48:16 GMT 2026 stageCount=10 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.9 -buildCount=0 +buildCount=3 baseBetaVersion=15.15.10 diff --git a/libappbase/build.properties b/libappbase/build.properties index 0d39807..5fde0d2 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Sat Jan 24 12:31:02 HKT 2026 +#Sat Jan 24 11:48:16 GMT 2026 stageCount=10 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.9 -buildCount=0 +buildCount=3 baseBetaVersion=15.15.10 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java index 9285bdb..b86aa9e 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/ApkSignUtils.java @@ -2,6 +2,9 @@ package cc.winboll.studio.libappbase.utils; import android.content.Context; 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 cc.winboll.studio.libappbase.LogUtils; @@ -13,78 +16,67 @@ import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Enumeration; import java.util.jar.JarEntry; import java.util.jar.JarFile; /** * @Author 豆包&ZhanGSKen * @CreateTime 2026-01-24 10:00:00 - * @LastEditTime 2026-01-24 19:50:00 - * @Describe 客户端签名工具类:与服务端APKFileUtils签名/哈希校验逻辑严格对齐,纯Java7实现,直接读取APK内签名文件计算,保证校验一致性 + * @LastEditTime 2026-01-24 22:00:00 + * @Describe 客户端签名工具类:与服务端APKFileUtils签名/哈希校验逻辑严格对齐,纯Java7实现;兼容MT重签名(遍历META-INF所有RSA文件),增加PackageManager兜底方案 */ public class 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_SHA256 = "SHA-256"; // 缓冲区大小常量(按业务场景区分) private static final int BUFFER_4K = 4096; 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串 - * 逻辑:读取APK内CERT.RSA原始字节 → SHA1摘要 → Base64.NO_WRAP编码,与服务端完全一致 - * @param context 上下文,用于获取当前应用APK的真实安装路径 + * 获取与服务端对齐的签名Base64串(兼容MT重签名) + * 优先逻辑:遍历APK内META-INF所有.RSA文件 → 读取第一个有效文件原始字节 → SHA1摘要 → Base64.NO_WRAP + * 兜底逻辑:PackageManager获取系统解析的签名 → SHA1摘要 → Base64.NO_WRAP + * @param context 上下文,用于获取当前应用APK路径/包信息 * @return 签名Base64字符串,任意步骤失败返回null */ public static String getApkSignAlignedWithServer(Context context) { - LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始执行服务端对齐签名计算"); + LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始执行服务端对齐签名计算(兼容MT重签名)"); // 入参空值快速校验 if (context == null) { LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为null,直接返回null"); return null; } - try { - // 1. 获取当前应用APK真实路径 - ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); - String apkPath = appInfo.sourceDir; - 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串"); + // 方案1:优先读取APK内META-INF目录下所有RSA文件(兼容MT重签名任意命名) + String signBase64 = getSignFromApkRsaFile(context); + if (signBase64 != null) { + LogUtils.d(TAG, "getApkSignAlignedWithServer: 方案1成功(APK内读取RSA文件),返回签名Base64"); 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; } /** - * 获取当前运行APK的SHA256哈希值 + * 获取当前运行APK的SHA256哈希值(兼容重签名APK) * 逻辑:读取APK完整文件字节流 → SHA256摘要 → 转小写64位16进制字符串,服务端同款校验逻辑 * @param context 上下文,用于获取当前应用APK的真实安装路径 * @return SHA256小写16进制字符串,任意步骤失败返回null @@ -97,6 +89,8 @@ public class ApkSignUtils { return null; } + JarFile jarFile = null; + FileInputStream fis = null; try { // 1. 获取当前应用APK真实路径 ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo(); @@ -107,16 +101,15 @@ public class ApkSignUtils { return null; } - // 2. 读取APK文件并计算SHA256哈希 + // 2. 读取APK文件并计算SHA256哈希(完善流关闭) File apkFile = new File(apkPath); MessageDigest md = MessageDigest.getInstance(ALGORITHM_SHA256); - FileInputStream fis = new FileInputStream(apkFile); + fis = new FileInputStream(apkFile); byte[] buffer = new byte[BUFFER_8K]; int readLen; while ((readLen = fis.read(buffer)) != -1) { md.update(buffer, 0, readLen); } - fis.close(); LogUtils.d(TAG, "getApkSHA256Hash: APK文件读取完成,开始转换哈希结果"); // 3. 哈希字节数组转小写64位16进制字符串 @@ -133,50 +126,127 @@ public class ApkSignUtils { LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败", e); } catch (Exception 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; } - // ===================================== 内部工具方法 ===================================== + // ===================================== 内部核心工具方法(兼容重签名) ===================================== /** - * 读取APK内CERT.RSA文件的原始字节流,兼容大小写命名 - * @param apkPath APK文件的完整绝对路径 - * @return CERT.RSA原始字节数组,未找到文件返回null - * @throws Exception 流读取、APK解析相关异常向上抛出 + * 方案1:遍历APK内META-INF所有.RSA/.rsa文件,读取第一个有效文件计算签名 + * @param context 上下文 + * @return 签名Base64,失败返回null */ - private static byte[] readCertRsaRawBytes(String apkPath) throws Exception { - LogUtils.d(TAG, "readCertRsaRawBytes: 方法调用,开始读取APK内签名文件,apkPath=" + apkPath); - JarFile jarFile = new JarFile(apkPath); - JarEntry certEntry = null; + private static String getSignFromApkRsaFile(Context context) { + JarFile jarFile = null; + InputStream is = 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; + } - // 优先读取大写命名,不存在则尝试小写 - certEntry = jarFile.getJarEntry(CERT_RSA_UPPER); - if (certEntry == null) { - LogUtils.d(TAG, "readCertRsaRawBytes: 未找到META-INF/CERT.RSA,尝试读取小写META-INF/cert.rsa"); - certEntry = jarFile.getJarEntry(CERT_RSA_LOWER); - } + // 打开APK的JarFile + jarFile = new JarFile(apkPath); + Enumeration entries = jarFile.entries(); + JarEntry targetRsaEntry = null; - // 未找到有效签名文件,关闭流后返回null - if (certEntry == null) { - LogUtils.e(TAG, "readCertRsaRawBytes: APK内未找到CERT.RSA/cert.rsa签名文件"); - jarFile.close(); + // 遍历所有条目,找到META-INF下第一个.RSA/.rsa文件 + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + 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; + } 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; } /** - * 输入流转字节数组,通用工具方法 - * 4K缓冲区,适配小文件读取(如CERT.RSA),保证流资源正常关闭 + * 方案2:兜底 - 通过PackageManager获取系统解析的应用签名 + * 避免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 待读取的输入流 - * @return 转换后的字节数组,流为null返回空字节数组 + * @return 转换后的字节数组,流为null/读取失败返回空字节数组 * @throws IOException 流读取相关异常向上抛出 */ private static byte[] readStreamToBytes(InputStream is) throws IOException { @@ -187,14 +257,16 @@ public class ApkSignUtils { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buffer = new byte[BUFFER_4K]; int readLen; - while ((readLen = is.read(buffer)) != -1) { - bos.write(buffer, 0, readLen); + try { + 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; } }