Compare commits

...

4 Commits

Author SHA1 Message Date
35527374da <appbase>APK 15.15.8 release Publish. 2026-01-24 11:26:28 +08:00
2751ce4a39 APK校验接口调试完成 2026-01-24 11:16:37 +08:00
730022a9f0 固定APK调试文件测试成功 2026-01-23 21:05:41 +08:00
a3bc90d9b8 <libappbase>Library Release 15.15.7 2026-01-23 03:11:58 +08:00
9 changed files with 751 additions and 120 deletions

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Jan 23 03:11:07 HKT 2026
stageCount=8
#Sat Jan 24 11:26:28 HKT 2026
stageCount=9
libraryProject=libappbase
baseVersion=15.15
publishVersion=15.15.7
publishVersion=15.15.8
buildCount=0
baseBetaVersion=15.15.8
baseBetaVersion=15.15.9

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Fri Jan 23 03:11:07 HKT 2026
stageCount=8
#Sat Jan 24 11:26:28 HKT 2026
stageCount=9
libraryProject=libappbase
baseVersion=15.15
publishVersion=15.15.7
publishVersion=15.15.8
buildCount=0
baseBetaVersion=15.15.8
baseBetaVersion=15.15.9

View File

@@ -6,7 +6,17 @@
<!-- 拥有完全的网络访问权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- 读取您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 修改或删除您共享存储空间中的内容 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- MANAGE_EXTERNAL_STORAGE -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"
android:networkSecurityConfig="@xml/network_security_config">
<activity

View File

@@ -23,10 +23,14 @@ public class SignGetDialog extends Dialog {
private EditText etSignFingerprint;
private TextView tvAuthResult;
private Context mContext;
String projectName;
String versionName;
public SignGetDialog(Context context) {
public SignGetDialog(Context context, String projectName, String versionName) {
super(context, R.style.DialogStyle); // 适配默认对话框样式
this.mContext = context;
this.projectName = projectName;
this.versionName = versionName;
}
@Override
@@ -47,19 +51,13 @@ public class SignGetDialog extends Dialog {
// 核心:获取签名+调用APPUtils校验
private void initSignAndCheck() {
// 1. 获取当前应用签名
String sign = getCurrentSign();
if (sign == null) {
etSignFingerprint.setText("签名获取失败");
} else {
// 签名字符串转0/1 bit数组每2个bit加空格每16位换行下一行无前置空格
String bitArrayStr = convertSignToBitArrayWithWrap(sign);
etSignFingerprint.setText(bitArrayStr);
}
LogUtils.d(TAG, "当前应用签名:" + sign);
// 2. 正版校验+显示结果
APPUtils.checkAppValid(mContext, new APPUtils.CheckResultCallback() {
// 调用处直接删除base64SignFingerprint参数即可
new APPUtils().checkAPKValidation(
mContext,
this.projectName,
this.versionName,
new APPUtils.CheckResultCallback() {
@Override
public void onResult(boolean isValid, String message) {
String szOfficialMessage;
@@ -81,7 +79,8 @@ public class SignGetDialog extends Dialog {
ToastUtils.show(szOfficialMessage);
tvAuthResult.setText(szOfficialMessage);
}
});
}
);
}
@@ -123,14 +122,14 @@ public class SignGetDialog extends Dialog {
}
// 获取签名复用SignGetUtils逻辑避免重复代码
private String getCurrentSign() {
try {
return SignGetUtils.getSignStr(mContext); // 复用工具类逻辑
} catch (Exception e) {
LogUtils.e(TAG, "获取签名失败", e);
return null;
}
}
// private String getCurrentSign() {
// try {
// return SignGetUtils.getSignStr(mContext); // 复用工具类逻辑
// } catch (Exception e) {
// LogUtils.e(TAG, "获取签名失败", e);
// return null;
// }
// }
// 校验签名是否合法匹配APPUtils目标签名
// private boolean isSignValid() {

View File

@@ -0,0 +1,306 @@
package cc.winboll.studio.libappbase.utils;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* APK文件工具类单例- 生产级签名+哈希双校验版修复Too short异常
* 1. 稳定解析CERT.RSA原始字节与客户端Signature.toByteArray()1:1对齐解决X509解析异常
* 2. 支持SHA256文件哈希字节级唯一校验签名+哈希双重验证
* 3. 入参包含:项目名/版本名/APK名/客户端签名/客户端哈希,适配生产级版本管理
* 4. APK路径规范apks_root/项目名/debug/tag/APK文件支持调试/正式环境)
* @Author ZhanGSKen<zhangsken@qq.com>
*/
public class APKFileUtils {
// 单例实例
private static volatile APKFileUtils sInstance;
// 配置项
private static final String CONFIG_SECTION = "APP";
private static final String KEY_APKS_FOLDER = "apks_folder_path";
// 算法常量(与客户端严格对齐)
private static final String SIGN_ALGORITHM = "SHA1"; // 签名摘要算法
private static final String HASH_ALGORITHM = "SHA-256"; // 文件哈希算法
// 签名文件(兼容大小写,适配所有打包工具)
private static final String CERT_RSA_UPPER = "META-INF/CERT.RSA";
private static final String CERT_RSA_LOWER = "META-INF/cert.rsa";
// APK根目录
private String apksRootPath;
private APKFileUtils() {}
/**
* 初始化工具类(需在应用启动时调用)
*/
public static void init() {
if (sInstance == null) {
synchronized (APKFileUtils.class) {
if (sInstance == null) {
sInstance = new APKFileUtils();
//sInstance.loadConfig();
}
}
}
}
/**
* 获取单例实例
*/
public static APKFileUtils getInstance() {
if (sInstance == null) {
LogUtils.e("APKFileUtils", "请先调用init()初始化工具类");
throw new IllegalStateException("APKFileUtils未初始化请先调用init()");
}
return sInstance;
}
/**
* 加载配置文件中的APK根目录
*/
// private void loadConfig() {
// try {
// apksRootPath = IniConfigUtils.getConfigValue(CONFIG_SECTION, KEY_APKS_FOLDER, "").trim();
// if (apksRootPath.isEmpty()) {
// LogUtils.e("APKFileUtils", "配置项apks_folder_path为空初始化失败");
// return;
// }
// File rootDir = new File(apksRootPath);
// if (!rootDir.exists() && !rootDir.mkdirs()) {
// LogUtils.e("APKFileUtils", "APK根目录创建失败" + apksRootPath);
// apksRootPath = "";
// return;
// }
// LogUtils.i("APKFileUtils", "APK根目录加载成功" + apksRootPath);
// } catch (Exception e) {
// LogUtils.e("APKFileUtils", "加载APK根目录配置失败", e);
// apksRootPath = "";
// }
// }
/**
* 对外暴露核心校验方法:签名 + SHA256文件哈希 双校验
* 入参包含:项目名/版本名/APK文件名/客户端签名Base64/客户端文件哈希
* APK路径规范apksRootPath/项目名/版本名/APK文件
* @param projectName 项目名(非空)
* @param versionName 版本名非空如15.11.11
* @param apkFileName APK文件名非空需以.apk结尾
* @param clientSignBase64 客户端传入的签名Base64非空
* @param clientFileHash 客户端传入的APK文件SHA256哈希小写/大写均可,非空)
* @return 校验通过返回true否则false
*/
public static boolean checkAPK(String projectName, String versionName, String apkFileName,
String clientSignBase64, String clientFileHash) {
return getInstance().doCheckAPK(projectName, versionName, apkFileName, clientSignBase64, clientFileHash);
}
/**
* 核心校验实现:严格按「哈希先验,签名后验」顺序,哈希不匹配直接返回
*/
private boolean doCheckAPK(String projectName, String versionName, String apkFileName,
String clientSignBase64, String clientFileHash) {
// 1. 基础入参非空校验
if (isParamEmpty(projectName) || isParamEmpty(versionName) || isParamEmpty(apkFileName)
|| isParamEmpty(clientSignBase64) || isParamEmpty(clientFileHash)) {
LogUtils.w("APKFileUtils", "基础参数不能为空projectName/versionName/apkFileName/clientSignBase64/clientFileHash");
return false;
}
// 2. APK文件名格式校验
if (!apkFileName.endsWith(".apk")) {
LogUtils.w("APKFileUtils", "APK文件名格式错误需以.apk结尾" + apkFileName);
return false;
}
// 3. APK根目录校验
if (isParamEmpty(apksRootPath)) {
LogUtils.w("APKFileUtils", "APK根目录未配置无法进行校验");
return false;
}
// 4. 拼接标准APK路径根目录/项目名/debug/项目名_版本名.apk调试环境可切换tag
String apkFullPath = String.format("%s/%s/debug/%s_%s.apk",
apksRootPath,
projectName,
projectName,
versionName);
//正式环境路径(注释保留,切换时解开即可)
// String apkFullPath = String.format("%s/%s/tag/%s_%s.apk",
// apksRootPath,
// projectName,
// projectName,
// versionName);
LogUtils.d("APKFileUtils", String.format("apkFullPath : %s", apkFullPath));
File apkFile = new File(apkFullPath);
// 5. APK文件存在性校验
if (!apkFile.exists() || !apkFile.isFile()) {
LogUtils.w("APKFileUtils", "APK文件不存在或非文件类型" + apkFullPath);
return false;
}
try {
// ===== 第一步SHA256文件哈希校验字节级唯一优先级最高=====
String serverFileHash = getAPKFileHash(apkFile);
if (isParamEmpty(serverFileHash)) {
LogUtils.w("APKFileUtils", "解析服务端APK文件哈希失败" + apkFileName);
return false;
}
boolean isHashMatch = serverFileHash.equalsIgnoreCase(clientFileHash.trim());
LogUtils.d("APKFileUtils", "【哈希对比】服务端SHA256" + serverFileHash);
LogUtils.d("APKFileUtils", "【哈希对比】客户端SHA256" + clientFileHash.trim());
if (!isHashMatch) {
LogUtils.i("APKFileUtils", "【哈希对比结果】❌ 不匹配(字节级文件不一致)");
return false;
}
LogUtils.i("APKFileUtils", "【哈希对比结果】✅ 匹配(字节级文件完全一致)");
// ===== 第二步签名校验直接读取CERT.RSA原始字节与客户端严格对齐=====
String serverSignBase64 = getAPKSign(apkFile);
if (isParamEmpty(serverSignBase64)) {
LogUtils.w("APKFileUtils", "解析服务端APK签名失败" + apkFileName);
return false;
}
boolean isSignMatch = serverSignBase64.equals(clientSignBase64.trim());
LogUtils.d("APKFileUtils", "【签名对比】服务端Base64" + serverSignBase64);
LogUtils.d("APKFileUtils", "【签名对比】客户端Base64" + clientSignBase64.trim());
if (!isSignMatch) {
LogUtils.i("APKFileUtils", "【签名对比结果】❌ 不匹配(签名不一致)");
return false;
}
LogUtils.i("APKFileUtils", "【签名对比结果】✅ 匹配(签名完全一致)");
// 所有校验通过
LogUtils.i("APKFileUtils", "APK双校验全部通过项目名=" + projectName + ",版本名=" + versionName + ",文件名=" + apkFileName);
return true;
} catch (Exception e) {
LogUtils.e("APKFileUtils", "APK双校验异常", e);
return false;
}
}
/**
* 稳定解析APK签名直接读取CERT.RSA原始字节SHA1+Base64与客户端1:1对齐
* 解决X509证书解析的Too short异常兼容所有APK普通/加固/自定义打包)
* @param apkFile APK文件
* @return 签名Base64字符串失败返回null
*/
private String getAPKSign(File apkFile) {
JarFile jarFile = null;
InputStream certIs = null;
try {
jarFile = new JarFile(apkFile);
// 先找大写CERT.RSA找不到再找小写兼容所有打包工具
JarEntry certEntry = jarFile.getJarEntry(CERT_RSA_UPPER);
if (certEntry == null) {
certEntry = jarFile.getJarEntry(CERT_RSA_LOWER);
if (certEntry == null) {
LogUtils.w("APKFileUtils", "APK中未找到签名文件META-INF/CERT.RSA/cert.rsa");
return null;
}
}
// 核心直接读取CERT.RSA的原始字节流不做证书解析适配PKCS7签名块
certIs = jarFile.getInputStream(certEntry);
byte[] sigRawBytes = readStreamToBytes(certIs);
if (sigRawBytes == null || sigRawBytes.length == 0) {
LogUtils.w("APKFileUtils", "读取CERT.RSA原始字节为空");
return null;
}
// 与客户端完全一致的处理流程SHA1摘要 → Base64编码去换行
MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM);
byte[] signDigest = md.digest(sigRawBytes);
String signBase64 = Base64.getEncoder().encodeToString(signDigest)
.replaceAll("\\r", "").replaceAll("\\n", "");
LogUtils.d("APKFileUtils", "APK签名解析成功(Base64)" + signBase64);
return signBase64;
} catch (NoSuchAlgorithmException e) {
LogUtils.e("APKFileUtils", "解析签名失败:" + SIGN_ALGORITHM + "算法不存在", e);
return null;
} catch (Exception e) {
LogUtils.e("APKFileUtils", "解析APK签名异常", e);
return null;
} finally {
// 强制关闭流资源,避免内存泄漏
try {
if (certIs != null) certIs.close();
if (jarFile != null) jarFile.close();
} catch (IOException e) {
LogUtils.e("APKFileUtils", "关闭签名文件流失败", e);
}
}
}
/**
* 解析APK文件的SHA256哈希字节级唯一任何字节修改都会改变
* @param apkFile APK文件
* @return 小写64位SHA256哈希字符串失败返回null
*/
private String getAPKFileHash(File apkFile) {
FileInputStream fis = null;
try {
MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM);
fis = new FileInputStream(apkFile);
byte[] buffer = new byte[8192]; // 8K缓冲区提升大APK读取效率
int len;
while ((len = fis.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
// 哈希字节转小写16进制字符串64位官方标准格式
byte[] hashBytes = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
String fileHash = sb.toString();
LogUtils.d("APKFileUtils", "APK文件SHA256哈希解析成功" + fileHash);
return fileHash;
} catch (NoSuchAlgorithmException e) {
LogUtils.e("APKFileUtils", "获取文件哈希失败:" + HASH_ALGORITHM + "算法不存在", e);
return null;
} catch (Exception e) {
LogUtils.e("APKFileUtils", "解析APK文件哈希异常", e);
return null;
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
LogUtils.e("APKFileUtils", "关闭APK文件流失败", e);
}
}
}
}
/**
* 流转字节数组工具方法:稳定读取任意输入流,无截断/空指针问题
*/
private byte[] readStreamToBytes(InputStream is) throws IOException {
if (is == null) {
LogUtils.w("APKFileUtils", "readStreamToBytes: 输入流为null");
return new byte[0];
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
byte[] result = bos.toByteArray();
// 按顺序关闭流
is.close();
bos.close();
return result;
}
/**
* 工具方法判断参数是否为空null/空字符串/全空格)
*/
private boolean isParamEmpty(String param) {
return param == null || param.trim().isEmpty();
}
}

View File

@@ -1,18 +1,31 @@
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.os.Handler;
import android.os.Looper;
import android.util.Base64;
import cc.winboll.studio.libappbase.GlobalApplication;
import cc.winboll.studio.libappbase.LogUtils;
import cc.winboll.studio.libappbase.models.SignCheckResponse;
import com.google.gson.Gson;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
@@ -21,61 +34,108 @@ import okhttp3.Response;
/**
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
* @Date 2026/01/20 19:17
* @Describe APPUtils 应用包名、签名校验工具类OKHTTP网络校验版
* @CreateTime 2026-01-20 19:17:00
* @LastEditTime 2026-01-24 02:18:00
* @Describe APPUtils 应用包名、签名校验工具类OKHTTP网络校验版兼容Java7含URL编码+APK包签名+SHA256哈希校验
*/
public class APPUtils {
// ===================================== 全局常量/属性 =====================================
public static final String TAG = "APPUtils";
// 网络校验接口地址
private static final String CHECK_API_URI = "api/app-signatures-check";
// OKHTTP客户端单例复用
private static OkHttpClient sOkHttpClient = new OkHttpClient();
// Gson解析实例
// Gson解析实例(单例复用)
private static Gson sGson = new Gson();
// ===================================== 对外核心方法 =====================================
/**
* 检查应用合法性(名校验+OKHTTP网络校验签名
* @param context 上下文
* @param callback 校验结果回调(主线程回调)
* 检查应用合法性(名校验+APK哈希校验+网络接口校验
* @param context 上下文
* @param projectName 项目名称
* @param versionName 应用版本名
* @param callback 校验结果回调(主线程回调)
*/
public static void checkAppValid(Context context, final CheckResultCallback callback) {
public void checkAPKValidation(Context context, String projectName, String versionName, final CheckResultCallback callback) {
// 入参调试日志
LogUtils.d(TAG, "checkAPKValidation: 入参 projectName=" + projectName + ", versionName=" + versionName);
// 空参校验
if (context == null) {
LogUtils.w(TAG, "checkAppValid: context为空跳过校验");
if (callback != null) callback.onResult(false, "context为空");
LogUtils.w(TAG, "checkAPKValidation: 入参context为空跳过校验");
if (callback != null) {
callback.onResult(false, "context为空");
}
return;
}
if (projectName == null || projectName.trim().isEmpty()) {
LogUtils.w(TAG, "checkAPKValidation: 入参projectName为空跳过校验");
if (callback != null) {
callback.onResult(false, "projectName为空");
}
return;
}
if (versionName == null || versionName.trim().isEmpty()) {
LogUtils.w(TAG, "checkAPKValidation: 入参versionName为空跳过校验");
if (callback != null) {
callback.onResult(false, "versionName为空");
}
return;
}
// 2. 获取当前应用签名SHA1+Base64和证书生效时间
String currentSign = getAppSignFingerprint(context);
long certValidTime = getCertValidTime(context); // 证书生效时间(毫秒时间戳)
if (currentSign == null) {
String errorMsg = "获取应用签名失败";
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
if (callback != null) callback.onResult(false, errorMsg);
// 调用签名/哈希获取方法
LogUtils.d(TAG, "checkAPKValidation: 开始获取应用官方签名与APK SHA256哈希");
// String clientSign = getOfficialSignBase64(context);
// String clientHash = getApkSHA256Hash(context);
// 获取与服务端对齐的签名
String clientSign = ApkSignUtils.getApkSignAlignedWithServer(context);
// 获取哈希(不变)
String clientHash = ApkSignUtils.getApkSHA256Hash(context);
// 传服务端校验
// 签名/哈希结果校验
if (clientSign == null) {
LogUtils.e(TAG, "checkAPKValidation: 获取应用官方签名失败");
if (callback != null) {
callback.onResult(false, "获取应用签名失败");
}
return;
}
if (clientHash == null) {
LogUtils.e(TAG, "checkAPKValidation: 获取APK SHA256哈希失败");
if (callback != null) {
callback.onResult(false, "获取APK哈希失败");
}
return;
}
LogUtils.d(TAG, "checkAPKValidation: 签名获取成功,哈希获取成功");
// 新增对currentSign进行Base64二次加密URL安全编码避免特殊字符
String encryptedSign = base64Encode(currentSign);
LogUtils.d(TAG, "checkAppValid: 原始签名=" + currentSign + "Base64二次加密后=" + encryptedSign);
// URL编码动态参数
LogUtils.d(TAG, "checkAPKValidation: 开始对动态参数进行URL编码");
String encodeProjectName = urlEncode(projectName);
String encodeVersionName = urlEncode(versionName);
String encodeClientSign = urlEncode(clientSign);
String encodeClientHash = urlEncode(clientHash);
// 3. 构建请求URL(拼接加密后的签名参数)
String requestUrl = String.format("%s?signature=%s&validTime=%d",
GlobalApplication.getWinbollHost() + CHECK_API_URI,
encryptedSign, // 替换为加密后的签名
certValidTime);
LogUtils.d(TAG, "checkAppValid: 发起网络校验请求URL=" + requestUrl);
// 构建请求URL
String requestUrl = String.format("%s?isDebug=%s&projectName=%s&versionName=%s&clientSign=%s&clientHash=%s",
GlobalApplication.getWinbollHost() + CHECK_API_URI,
String.format("%s", GlobalApplication.isDebugging()),
encodeProjectName,
encodeVersionName,
encodeClientSign,
encodeClientHash);
LogUtils.d(TAG, "checkAPKValidation: 构建校验请求URL=" + requestUrl);
// 4. OKHTTP发起异步GET请求
// 发起OKHTTP异步GET请求
LogUtils.d(TAG, "checkAPKValidation: 发起网络校验异步请求");
Request request = new Request.Builder().url(requestUrl).build();
sOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
final String errorMsg = "网络校验请求失败:" + e.getMessage();
LogUtils.e(TAG, "checkAppValid: " + errorMsg, e);
LogUtils.e(TAG, "checkAPKValidation: " + errorMsg, e);
if (callback != null) {
// 切换到主线程回调
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(false, errorMsg);
@@ -88,13 +148,14 @@ public class APPUtils {
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful() && response.body() != null) {
String responseJson = response.body().string();
LogUtils.d(TAG, "checkAppValid: 网络校验响应JSON=" + responseJson);
// 解析JSON响应
SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
LogUtils.d(TAG, "checkAPKValidation: 网络校验响应JSON=" + responseJson);
// 解析响应结果
final SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
final boolean isValid = checkResponse != null && checkResponse.isValid();
final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败";
LogUtils.d(TAG, "checkAPKValidation: 校验结果解析完成isValid=" + isValid + ", msg=" + msg);
if (callback != null) {
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(isValid, msg);
@@ -103,9 +164,9 @@ public class APPUtils {
}
} else {
final String errorMsg = "网络校验响应失败code=" + response.code();
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
LogUtils.e(TAG, "checkAPKValidation: " + errorMsg);
if (callback != null) {
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
callback.onResult(false, errorMsg);
@@ -117,24 +178,105 @@ public class APPUtils {
});
}
// ===================================== 内部工具方法 =====================================
/**
* 新增Base64加密工具URL安全编码避免特殊字符影响URL拼接
* @param content 待加密内容
* @return 加密后的Base64字符串
* 获取当前应用的APK包文件对象
* @param context 上下文
* @return APK文件File失败返回null
*/
private static String base64Encode(String content) {
private File getCurrentAppApkFile(Context context) {
try {
// 使用URL安全的Base64编码替换+为-/为_去除=
byte[] contentBytes = content.getBytes("UTF-8");
return Base64.encodeToString(contentBytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
context.getPackageName(), 0
);
String apkPath = appInfo.sourceDir;
File apkFile = new File(apkPath);
return apkFile.exists() && apkFile.isFile() ? apkFile : null;
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getCurrentAppApkFile: 获取应用APK路径失败", e);
return null;
} catch (Exception e) {
LogUtils.e(TAG, "base64Encode: 加密失败", e);
return content; // 加密失败则返回原始内容,避免请求异常
LogUtils.e(TAG, "getCurrentAppApkFile: 未知异常", e);
return null;
}
}
/**
* 获取当前应用签名SHA1指纹BASE64编码
* 复刻Android系统Signature的DER编码解析逻辑
* 从CERT.RSA的DER字节中提取与signatures[0].toByteArray()一致的签名字节
* @param derBytes CERT.RSA的DER编码字节
* @return 标准签名字节失败返回null
*/
private byte[] parseDerForAndroidSignature(byte[] derBytes) {
try {
int offset = 0;
if (derBytes == null || derBytes.length < 2 || derBytes[offset++] != 0x30) {
LogUtils.w(TAG, "parseDerForAndroidSignature: DER编码非标准SEQUENCE格式");
return null;
}
// 跳过长度字段
if (derBytes[offset] > 0x80) {
int lenLen = derBytes[offset++] & 0x7F;
offset += lenLen;
} else {
offset++;
}
// 提取签名块字节
while (offset < derBytes.length) {
if (derBytes[offset] == 0x30) {
byte[] sigBytes = new byte[derBytes.length - offset];
System.arraycopy(derBytes, offset, sigBytes, 0, sigBytes.length);
return sigBytes;
}
offset++;
}
LogUtils.w(TAG, "parseDerForAndroidSignature: DER编码中未找到签名块SEQUENCE");
return null;
} catch (Exception e) {
LogUtils.e(TAG, "parseDerForAndroidSignature: 解析DER编码失败", e);
return null;
}
}
/**
* 输入流转字节数组Java7适配无第三方依赖
* @param is 输入流
* @return 字节数组,失败返回空数组
* @throws IOException 流读取异常
*/
private byte[] readStreamToBytes(InputStream is) throws IOException {
if (is == null) return new byte[0];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
byte[] result = bos.toByteArray();
bos.close();
is.close();
return result;
}
/**
* URL编码工具Java7适配UTF-8编码处理特殊字符
* @param content 待编码内容
* @return 编码后的字符串,失败返回原内容
*/
private static String urlEncode(String content) {
try {
return URLEncoder.encode(content, "UTF-8");
} catch (Exception e) {
LogUtils.e(TAG, "urlEncode: 编码失败content=" + content, e);
return content;
}
}
/**
* 从PackageManager获取应用签名SHA1指纹BASE64编码快速获取
* @param context 上下文
* @return 签名Base64字符串失败返回null
*/
private static String getAppSignFingerprint(Context context) {
try {
@@ -148,44 +290,25 @@ public class APPUtils {
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(signatures[0].toByteArray());
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getAppSignFingerprint: 获取签名异常", e);
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getAppSignFingerprint: 获取包信息异常", e);
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getAppSignFingerprint: 获取SHA1算法异常", e);
} catch (Exception e) {
LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e);
}
return null;
}
// ===================================== 回调接口 =====================================
/**
* 获取应用证书生效时间(毫秒时间戳
* 校验结果回调接口(主线程调用
*/
private static long getCertValidTime(Context context) {
try {
PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = pkgInfo.signatures;
if (signatures == null || signatures.length == 0) {
LogUtils.w(TAG, "getCertValidTime: 未获取到应用签名");
return new Date().getTime(); // 默认当前时间
}
// 解析签名证书获取生效时间简化实现实际需解析X.509证书)
// 注意若需精准获取证书生效时间需解析Signature的toByteArray()为X509Certificate
// 此处为简化版,若需精准实现可告知,将补充完整证书解析逻辑
return new Date().getTime();
} catch (PackageManager.NameNotFoundException e) {
LogUtils.e(TAG, "getCertValidTime: 获取包信息异常", e);
} catch (Exception e) {
LogUtils.e(TAG, "getCertValidTime: 未知异常", e);
}
return new Date().getTime();
}
// ==================== 校验结果回调接口 ====================
public interface CheckResultCallback {
/**
* 校验结果回调(主线程调用)
* 校验结果回调
* @param isValid 是否合法
* @param message 校验信息
* @param message 校验信息/错误信息
*/
void onResult(boolean isValid, String message);
}

View File

@@ -0,0 +1,193 @@
package cc.winboll.studio.libappbase.utils;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.util.Base64;
import cc.winboll.studio.libappbase.LogUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
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 16:45:00
* @Describe 客户端签名工具类与服务端APKFileUtils签名/哈希校验逻辑严格对齐兼容Java7
*/
public class ApkSignUtils {
// ===================================== 全局常量 =====================================
private static final String TAG = "ApkSignUtils";
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 SIGN_ALGORITHM_SHA1 = "SHA1";
private static final String SIGN_ALGORITHM_SHA256 = "SHA-256";
private static final int BUFFER_SIZE_4K = 4096;
private static final int BUFFER_SIZE_8K = 8192;
// ===================================== 对外核心方法 =====================================
/**
* 获取与服务端对齐的签名Base64核心方法
* 直接读取APK内CERT.RSA原始字节 → SHA1摘要 → Base64.NO_WRAP编码与服务端逻辑完全一致
* @param context 上下文用于获取当前应用APK路径
* @return 签名Base64字符串失败返回null
*/
public static String getApkSignAlignedWithServer(Context context) {
LogUtils.d(TAG, "getApkSignAlignedWithServer: 方法调用,开始获取服务端对齐签名");
if (context == null) {
LogUtils.w(TAG, "getApkSignAlignedWithServer: 入参context为空直接返回null");
return null;
}
try {
// 1. 获取当前应用APK的真实安装路径
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo();
String apkPath = appInfo.sourceDir;
LogUtils.d(TAG, "getApkSignAlignedWithServer: 获取到当前应用APK路径=" + 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.NO_WRAP编码与服务端完全对齐
MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM_SHA1);
byte[] signDigest = md.digest(certRawBytes);
String signBase64 = Base64.encodeToString(signDigest, Base64.NO_WRAP);
LogUtils.d(TAG, "getApkSignAlignedWithServer: 成功计算服务端对齐签名Base64完成方法执行");
return signBase64;
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取SHA1算法实例失败获取签名失败", e);
} catch (Exception e) {
LogUtils.e(TAG, "getApkSignAlignedWithServer: 获取服务端对齐签名发生未知异常", e);
}
return null;
}
/**
* 获取当前运行APK的SHA256哈希值
* 读取APK文件完整字节流计算SHA256转小写64位16进制字符串与服务端校验逻辑一致
* @param context 上下文用于获取当前应用APK路径
* @return SHA256哈希小写字符串失败返回null
*/
public static String getApkSHA256Hash(Context context) {
LogUtils.d(TAG, "getApkSHA256Hash: 方法调用开始获取APK SHA256哈希值");
if (context == null) {
LogUtils.w(TAG, "getApkSHA256Hash: 入参context为空直接返回null");
return null;
}
try {
// 1. 获取当前应用APK的真实安装路径
ApplicationInfo appInfo = context.getApplicationContext().getApplicationInfo();
String apkPath = appInfo.sourceDir;
LogUtils.d(TAG, "getApkSHA256Hash: 获取到当前应用APK路径=" + apkPath);
if (apkPath == null || apkPath.trim().isEmpty()) {
LogUtils.e(TAG, "getApkSHA256Hash: 获取APK路径为空获取哈希失败");
return null;
}
// 2. 读取APK文件并计算SHA256哈希
File apkFile = new File(apkPath);
MessageDigest md = MessageDigest.getInstance(SIGN_ALGORITHM_SHA256);
FileInputStream fis = new FileInputStream(apkFile);
byte[] buffer = new byte[BUFFER_SIZE_8K];
int len;
while ((len = fis.read(buffer)) != -1) {
md.update(buffer, 0, len);
}
fis.close();
// 3. 哈希字节转小写64位16进制字符串
byte[] hashBytes = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
String sha256Hash = sb.toString();
LogUtils.d(TAG, "getApkSHA256Hash: 成功计算APK SHA256哈希值完成方法执行");
return sha256Hash;
} catch (NoSuchAlgorithmException e) {
LogUtils.e(TAG, "getApkSHA256Hash: 获取SHA-256算法实例失败获取哈希失败", e);
} catch (Exception e) {
LogUtils.e(TAG, "getApkSHA256Hash: 获取APK SHA256哈希发生未知异常", e);
}
return null;
}
// ===================================== 内部工具方法 =====================================
/**
* 读取APK内CERT.RSA文件的原始字节流兼容大小写命名CERT.RSA/cert.rsa
* @param apkPath 当前应用APK的完整安装路径
* @return CERT.RSA原始字节数组失败返回null
* @throws Exception 流读取/APK解析异常
*/
private static byte[] readCertRsaRawBytes(String apkPath) throws Exception {
LogUtils.d(TAG, "readCertRsaRawBytes: 方法调用APK路径=" + apkPath);
JarFile jarFile = new JarFile(apkPath);
JarEntry certEntry = 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);
}
// 未找到有效CERT.RSA文件
if (certEntry == null) {
LogUtils.e(TAG, "readCertRsaRawBytes: APK内未找到CERT.RSA/cert.rsa签名文件");
jarFile.close();
return null;
}
// 读取文件原始字节流并关闭流
InputStream is = jarFile.getInputStream(certEntry);
byte[] bytes = readStreamToBytes(is);
is.close();
jarFile.close();
LogUtils.d(TAG, "readCertRsaRawBytes: 成功读取CERT.RSA字节完成方法执行");
return bytes;
}
/**
* 输入流转字节数组与服务端工具方法逻辑完全一致4K缓冲区
* @param is 待读取的输入流
* @return 字节数组流为null返回空字节数组
* @throws IOException 流读取异常
*/
private static byte[] readStreamToBytes(InputStream is) throws IOException {
if (is == null) {
LogUtils.w(TAG, "readStreamToBytes: 入参输入流为null返回空字节数组");
return new byte[0];
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE_4K];
int len;
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
byte[] result = bos.toByteArray();
// 关闭流资源,避免泄漏
is.close();
bos.close();
return result;
}
}

View File

@@ -51,21 +51,21 @@ public class SignGetUtils {
}
// 新增:直接返回签名字符串,供对话框调用
public static String getSignStr(Context context) {
if (context == null) return null;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = pkgInfo.signatures;
if (signatures == null || signatures.length == 0) return null;
MessageDigest md = MessageDigest.getInstance("SHA1");
md.update(signatures[0].toByteArray());
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
} catch (Exception e) {
LogUtils.e(TAG, "获取签名字符串失败", e);
return null;
}
}
// public static String getSignStr(Context context) {
// if (context == null) return null;
// try {
// PackageManager pm = context.getPackageManager();
// PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
// Signature[] signatures = pkgInfo.signatures;
// if (signatures == null || signatures.length == 0) return null;
//
// MessageDigest md = MessageDigest.getInstance("SHA1");
// md.update(signatures[0].toByteArray());
// return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
// } catch (Exception e) {
// LogUtils.e(TAG, "获取签名字符串失败", e);
// return null;
// }
// }
}

View File

@@ -140,7 +140,7 @@ public class AboutView extends LinearLayout {
@Override
public void onClick(View v) {
LogUtils.d(TAG, "签名获取按钮点击弹出SignGetDialog");
new SignGetDialog(mContext).show(); // 弹出对话框
new SignGetDialog(mContext, mszAppGitName, mszAppVersionName).show(); // 弹出对话框
}
});
ibWinBoLLHostDialog.setOnClickListener(new OnClickListener() {