@@ -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<zhangsken@qq.com>
* @CreateTime 2026-01-24 10:00:00
* @LastEditTime 2026-01-24 19:5 0:00
* @Describe 客户端签名工具类: 与服务端APKFileUtils签名/哈希校验逻辑严格对齐, 纯Java7实现, 直接读取APK内签名文件计算, 保证校验一致性
* @LastEditTime 2026-01-24 22:0 0: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 ) ;
JarEn try 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 < JarEntry > entries = jarFile . entries ( ) ;
JarEntry targetRsaEntry = null ;
// 未找到有效签名文件, 关闭流后返回null
if ( certEntry = = null ) {
LogUtils . e ( TAG , " readCertRsaRawBytes: APK内未找到CERT.RSA/cert.rsa签名文件 " ) ;
jarFile . clos e ( ) ;
// 遍历所有条目, 找到META-INF下第一个.RSA/.rsa文件
while ( entries . hasMoreElements ( ) ) {
JarEntry entry = entries . nextElement ( ) ;
String entryName = entry . getNam e ( ) ;
// 过滤: 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 ) {
bo s. write ( buffer , 0 , readLen ) ;
try {
while ( ( readLen = i s. 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 ;
}
}