固定APK调试文件测试成功
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Fri Jan 23 03:11:18 HKT 2026
|
#Fri Jan 23 13:02:14 GMT 2026
|
||||||
stageCount=8
|
stageCount=8
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.7
|
publishVersion=15.15.7
|
||||||
buildCount=0
|
buildCount=14
|
||||||
baseBetaVersion=15.15.8
|
baseBetaVersion=15.15.8
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Fri Jan 23 03:11:07 HKT 2026
|
#Fri Jan 23 13:02:14 GMT 2026
|
||||||
stageCount=8
|
stageCount=8
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.7
|
publishVersion=15.15.7
|
||||||
buildCount=0
|
buildCount=14
|
||||||
baseBetaVersion=15.15.8
|
baseBetaVersion=15.15.8
|
||||||
|
|||||||
@@ -6,7 +6,17 @@
|
|||||||
<!-- 拥有完全的网络访问权限 -->
|
<!-- 拥有完全的网络访问权限 -->
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<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
|
<application
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -48,18 +48,23 @@ public class SignGetDialog extends Dialog {
|
|||||||
// 核心:获取签名+调用APPUtils校验
|
// 核心:获取签名+调用APPUtils校验
|
||||||
private void initSignAndCheck() {
|
private void initSignAndCheck() {
|
||||||
// 1. 获取当前应用签名
|
// 1. 获取当前应用签名
|
||||||
String sign = getCurrentSign();
|
// String sign = getCurrentSign();
|
||||||
if (sign == null) {
|
// if (sign == null) {
|
||||||
etSignFingerprint.setText("签名获取失败");
|
// etSignFingerprint.setText("签名获取失败");
|
||||||
} else {
|
// } else {
|
||||||
// 签名字符串转0/1 bit数组(每2个bit加空格,每16位换行,下一行无前置空格)
|
// // 签名字符串转0/1 bit数组(每2个bit加空格,每16位换行,下一行无前置空格)
|
||||||
String bitArrayStr = convertSignToBitArrayWithWrap(sign);
|
// String bitArrayStr = convertSignToBitArrayWithWrap(sign);
|
||||||
etSignFingerprint.setText(bitArrayStr);
|
// etSignFingerprint.setText(bitArrayStr);
|
||||||
}
|
// }
|
||||||
LogUtils.d(TAG, "当前应用签名:" + sign);
|
// LogUtils.d(TAG, "当前应用签名:" + sign);
|
||||||
|
|
||||||
// 2. 正版校验+显示结果
|
// 2. 正版校验+显示结果
|
||||||
APPUtils.checkAppValid(mContext, new APPUtils.CheckResultCallback() {
|
// 调用处直接删除base64SignFingerprint参数即可
|
||||||
|
new APPUtils().checkAPKSignature(
|
||||||
|
mContext,
|
||||||
|
"WinBoLL", // projectName
|
||||||
|
"WinBoLL_15.11.11.apk", // apkFileName
|
||||||
|
new APPUtils.CheckResultCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onResult(boolean isValid, String message) {
|
public void onResult(boolean isValid, String message) {
|
||||||
String szOfficialMessage;
|
String szOfficialMessage;
|
||||||
@@ -81,7 +86,8 @@ public class SignGetDialog extends Dialog {
|
|||||||
ToastUtils.show(szOfficialMessage);
|
ToastUtils.show(szOfficialMessage);
|
||||||
tvAuthResult.setText(szOfficialMessage);
|
tvAuthResult.setText(szOfficialMessage);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -123,14 +129,14 @@ public class SignGetDialog extends Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取签名(复用SignGetUtils逻辑,避免重复代码)
|
// 获取签名(复用SignGetUtils逻辑,避免重复代码)
|
||||||
private String getCurrentSign() {
|
// private String getCurrentSign() {
|
||||||
try {
|
// try {
|
||||||
return SignGetUtils.getSignStr(mContext); // 复用工具类逻辑
|
// return SignGetUtils.getSignStr(mContext); // 复用工具类逻辑
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
LogUtils.e(TAG, "获取签名失败", e);
|
// LogUtils.e(TAG, "获取签名失败", e);
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 校验签名是否合法(匹配APPUtils目标签名)
|
// 校验签名是否合法(匹配APPUtils目标签名)
|
||||||
// private boolean isSignValid() {
|
// private boolean isSignValid() {
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package cc.winboll.studio.libappbase.utils;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
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文件工具类(单例)- 终极稳定版
|
||||||
|
* 适配APK的PKCS7格式CERT.RSA,直接读取原始字节,与客户端getAppSignFingerprint1:1对齐
|
||||||
|
* 解决Too short解析异常,保证签名稳定解析
|
||||||
|
* @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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.i("APKFileUtils", "APK根目录加载成功:" + apksRootPath);
|
||||||
|
// } catch (Exception e) {
|
||||||
|
// LogUtils.e("APKFileUtils", "加载APK根目录配置失败", e);
|
||||||
|
// apksRootPath = "";
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
public static boolean checkAPKSignature(String projectName, String apkFileName, String clientSignBase64) {
|
||||||
|
return getInstance().doCheckAPK(projectName, apkFileName, clientSignBase64);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean doCheckAPK(String projectName, String apkFileName, String clientSignBase64) {
|
||||||
|
// 1. 入参校验
|
||||||
|
if (projectName == null || projectName.trim().isEmpty()
|
||||||
|
|| apkFileName == null || apkFileName.trim().isEmpty()
|
||||||
|
|| clientSignBase64 == null || clientSignBase64.trim().isEmpty()) {
|
||||||
|
LogUtils.w("APKFileUtils", "参数不能为空:projectName/apkFileName/clientSignBase64");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 2. 根目录校验
|
||||||
|
if (apksRootPath == null || apksRootPath.trim().isEmpty()) {
|
||||||
|
LogUtils.w("APKFileUtils", "APK根目录未配置:apks_folder_path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 3. APK文件校验
|
||||||
|
String apkFullPath = apksRootPath + File.separator + projectName
|
||||||
|
+ File.separator + "tag" + File.separator + apkFileName;
|
||||||
|
File apkFile = new File(apkFullPath);
|
||||||
|
if (!apkFile.exists() || !apkFile.isFile() || !apkFileName.endsWith(".apk")) {
|
||||||
|
LogUtils.w("APKFileUtils", "APK文件不存在或格式错误:" + apkFullPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 解析APK签名(终极稳定版:直接读取CERT.RSA原始字节,与客户端对齐)
|
||||||
|
String apkSignBase64 = getAPKSignFingerprint(apkFile);
|
||||||
|
if (apkSignBase64 == null || apkSignBase64.trim().isEmpty()) {
|
||||||
|
LogUtils.w("APKFileUtils", "解析APK签名失败,返回null/空值");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 签名对比
|
||||||
|
LogUtils.d("APKFileUtils", "【签名对比】APK解析签名:" + apkSignBase64);
|
||||||
|
LogUtils.d("APKFileUtils", "【签名对比】客户端传入签名:" + clientSignBase64);
|
||||||
|
boolean isMatch = apkSignBase64.equals(clientSignBase64);
|
||||||
|
LogUtils.i("APKFileUtils", "【签名对比结果】" + (isMatch ? "✅ 匹配" : "❌ 不匹配"));
|
||||||
|
return isMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终极稳定解析:与客户端getAppSignFingerprint逻辑1:1完全对齐
|
||||||
|
* 直接读取CERT.RSA原始字节(适配PKCS7格式),跳过证书解析,彻底解决Too short异常
|
||||||
|
*/
|
||||||
|
public String getAPKSignFingerprint(File apkFile) {
|
||||||
|
JarFile jarFile = null;
|
||||||
|
try {
|
||||||
|
jarFile = new JarFile(apkFile);
|
||||||
|
// 适配APK标准签名文件:CERT.RSA(兼容大小写)
|
||||||
|
JarEntry sigEntry = jarFile.getJarEntry("META-INF/CERT.RSA");
|
||||||
|
if (sigEntry == null) {
|
||||||
|
sigEntry = jarFile.getJarEntry("META-INF/cert.rsa");
|
||||||
|
if (sigEntry == null) {
|
||||||
|
LogUtils.w("APKFileUtils", "APK中未找到META-INF/CERT.RSA(含大小写)");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 核心:直接读取CERT.RSA原始字节(和客户端Signature.toByteArray()底层一致,适配PKCS7)
|
||||||
|
InputStream is = jarFile.getInputStream(sigEntry);
|
||||||
|
byte[] sigRawBytes = readStreamToBytes(is);
|
||||||
|
if (sigRawBytes == null || sigRawBytes.length == 0) {
|
||||||
|
LogUtils.w("APKFileUtils", "读取CERT.RSA原始字节为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 与客户端完全一致的流程:原始字节 → SHA1摘要 → Base64编码(去换行=NO_WRAP)
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||||
|
md.update(sigRawBytes);
|
||||||
|
byte[] sha1Digest = md.digest();
|
||||||
|
// 标准Base64编码,去换行符(等效Android的Base64.NO_WRAP)
|
||||||
|
String signBase64 = Base64.getEncoder().encodeToString(sha1Digest)
|
||||||
|
.replaceAll("\\r", "").replaceAll("\\n", "");
|
||||||
|
|
||||||
|
LogUtils.d("APKFileUtils", "APK解析出的签名(Base64):" + signBase64);
|
||||||
|
return signBase64;
|
||||||
|
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
LogUtils.e("APKFileUtils", "解析签名失败:SHA1算法不存在", e);
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e("APKFileUtils", "解析APK签名异常", e);
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (jarFile != null) {
|
||||||
|
try {
|
||||||
|
jarFile.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LogUtils.e("APKFileUtils", "关闭JarFile流失败", 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,18 +1,26 @@
|
|||||||
package cc.winboll.studio.libappbase.utils;
|
package cc.winboll.studio.libappbase.utils;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.Signature;
|
import android.content.pm.Signature;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import cc.winboll.studio.libappbase.GlobalApplication;
|
import cc.winboll.studio.libappbase.GlobalApplication;
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.libappbase.models.SignCheckResponse;
|
import cc.winboll.studio.libappbase.models.SignCheckResponse;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Date;
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.Callback;
|
import okhttp3.Callback;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
@@ -22,7 +30,7 @@ import okhttp3.Response;
|
|||||||
/**
|
/**
|
||||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
* @Date 2026/01/20 19:17
|
* @Date 2026/01/20 19:17
|
||||||
* @Describe APPUtils 应用包名、签名校验工具类(OKHTTP网络校验版)
|
* @Describe APPUtils 应用包名、签名校验工具类(OKHTTP网络校验版,兼容Java7,含URL编码+APK包签名校验)
|
||||||
*/
|
*/
|
||||||
public class APPUtils {
|
public class APPUtils {
|
||||||
public static final String TAG = "APPUtils";
|
public static final String TAG = "APPUtils";
|
||||||
@@ -36,46 +44,75 @@ public class APPUtils {
|
|||||||
/**
|
/**
|
||||||
* 检查应用合法性(包名校验+OKHTTP网络校验签名)
|
* 检查应用合法性(包名校验+OKHTTP网络校验签名)
|
||||||
* @param context 上下文
|
* @param context 上下文
|
||||||
|
* @param projectName 项目名称(入参)
|
||||||
|
* @param apkFileName APK文件名(入参)
|
||||||
* @param callback 校验结果回调(主线程回调)
|
* @param callback 校验结果回调(主线程回调)
|
||||||
*/
|
*/
|
||||||
public static void checkAppValid(Context context, final CheckResultCallback callback) {
|
public void checkAPKSignature(Context context, String projectName, String apkFileName, final CheckResultCallback callback) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
LogUtils.w(TAG, "checkAppValid: context为空,跳过校验");
|
LogUtils.w(TAG, "checkAPKSignature: context为空,跳过校验");
|
||||||
if (callback != null) callback.onResult(false, "context为空");
|
if (callback != null) {
|
||||||
|
callback.onResult(false, "context为空");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 校验剩余入参非空
|
||||||
|
if (projectName == null || projectName.trim().isEmpty()
|
||||||
|
|| apkFileName == null || apkFileName.trim().isEmpty()) {
|
||||||
|
String errorMsg = "校验入参为空,projectName/apkFileName不可为空";
|
||||||
|
LogUtils.e(TAG, "checkAPKSignature: " + errorMsg);
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onResult(false, errorMsg);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取当前应用签名(SHA1+Base64)和证书生效时间
|
// 方式1:从PackageManager获取签名(原逻辑,快速)
|
||||||
String currentSign = getAppSignFingerprint(context);
|
APKFileUtils.init();
|
||||||
long certValidTime = getCertValidTime(context); // 证书生效时间(毫秒时间戳)
|
File apkFile = new File("/sdcard/WinBoLLStudio/APKs/WinBoLL/tag/WinBoLL_15.11.11.apk");
|
||||||
|
String currentSign = APKFileUtils.getInstance().getAPKSignFingerprint(apkFile);
|
||||||
|
//String currentSign = APKFileUtils.getInstance().getAPKSignFingerprint(getCurrentAppApkFile(context));
|
||||||
|
LogUtils.d(TAG, String.format("currentSign : %s", currentSign));
|
||||||
|
// 方式2:从当前应用APK包文件解析签名(兜底,和服务端校验逻辑1:1对齐)
|
||||||
|
// if (currentSign == null) {
|
||||||
|
// LogUtils.w(TAG, "checkAPKSignature: 从PackageManager获取签名失败,尝试从APK包文件解析");
|
||||||
|
// currentSign = getAPKSignFingerprint(getCurrentAppApkFile(context));
|
||||||
|
// }
|
||||||
|
|
||||||
if (currentSign == null) {
|
if (currentSign == null) {
|
||||||
String errorMsg = "获取应用签名失败";
|
String errorMsg = "获取应用签名失败(PackageManager+APK包解析均失败)";
|
||||||
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
|
LogUtils.e(TAG, "checkAPKSignature: " + errorMsg);
|
||||||
if (callback != null) callback.onResult(false, errorMsg);
|
if (callback != null) {
|
||||||
|
callback.onResult(false, errorMsg);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
LogUtils.d(TAG, "checkAPKSignature: 应用最终签名(SHA1+Base64)=" + currentSign);
|
||||||
|
|
||||||
// 新增:对currentSign进行Base64二次加密(URL安全编码,避免特殊字符)
|
// 对动态参数做URL编码,避免特殊字符(/、=、&、空格等)导致解析异常
|
||||||
String encryptedSign = base64Encode(currentSign);
|
String encodeProjectName = urlEncode(projectName);
|
||||||
LogUtils.d(TAG, "checkAppValid: 原始签名=" + currentSign + ",Base64二次加密后=" + encryptedSign);
|
String encodeApkFileName = urlEncode(apkFileName);
|
||||||
|
String encodeSignature = urlEncode(currentSign);
|
||||||
|
LogUtils.d(TAG, "checkAPKSignature: URL编码后-项目名=" + encodeProjectName + ",APK名=" + encodeApkFileName + ",签名=" + encodeSignature);
|
||||||
|
|
||||||
// 3. 构建请求URL(拼接加密后的签名参数)
|
// 构建请求URL - 拼接**编码后**的参数
|
||||||
String requestUrl = String.format("%s?signature=%s&validTime=%d",
|
String requestUrl = String.format("%s?projectName=%s&apkFileName=%s&signature=%s",
|
||||||
GlobalApplication.getWinbollHost() + CHECK_API_URI,
|
GlobalApplication.getWinbollHost() + CHECK_API_URI,
|
||||||
encryptedSign, // 替换为加密后的签名
|
encodeProjectName,
|
||||||
certValidTime);
|
encodeApkFileName,
|
||||||
LogUtils.d(TAG, "checkAppValid: 发起网络校验请求,URL=" + requestUrl);
|
encodeSignature);
|
||||||
|
LogUtils.d(TAG, "checkAPKSignature: 发起网络校验请求,URL=" + requestUrl);
|
||||||
|
|
||||||
// 4. OKHTTP发起异步GET请求
|
// OKHTTP发起异步GET请求
|
||||||
Request request = new Request.Builder().url(requestUrl).build();
|
Request request = new Request.Builder().url(requestUrl).build();
|
||||||
sOkHttpClient.newCall(request).enqueue(new Callback() {
|
sOkHttpClient.newCall(request).enqueue(new Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e) {
|
||||||
final String errorMsg = "网络校验请求失败:" + e.getMessage();
|
final String errorMsg = "网络校验请求失败:" + e.getMessage();
|
||||||
LogUtils.e(TAG, "checkAppValid: " + errorMsg, e);
|
LogUtils.e(TAG, "checkAPKSignature: " + errorMsg, e);
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
// 切换到主线程回调
|
// 切换到主线程回调(Java7 匿名Runnable)
|
||||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
callback.onResult(false, errorMsg);
|
callback.onResult(false, errorMsg);
|
||||||
@@ -88,13 +125,14 @@ public class APPUtils {
|
|||||||
public void onResponse(Call call, Response response) throws IOException {
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
String responseJson = response.body().string();
|
String responseJson = response.body().string();
|
||||||
LogUtils.d(TAG, "checkAppValid: 网络校验响应JSON=" + responseJson);
|
LogUtils.d(TAG, "checkAPKSignature: 网络校验响应JSON=" + responseJson);
|
||||||
// 解析JSON响应
|
// 解析JSON响应
|
||||||
SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
|
final SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
|
||||||
final boolean isValid = checkResponse != null && checkResponse.isValid();
|
final boolean isValid = checkResponse != null && checkResponse.isValid();
|
||||||
final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败";
|
final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败";
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
// 切换到主线程回调(Java7 匿名Runnable)
|
||||||
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
callback.onResult(isValid, msg);
|
callback.onResult(isValid, msg);
|
||||||
@@ -103,9 +141,10 @@ public class APPUtils {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final String errorMsg = "网络校验响应失败,code=" + response.code();
|
final String errorMsg = "网络校验响应失败,code=" + response.code();
|
||||||
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
|
LogUtils.e(TAG, "checkAPKSignature: " + errorMsg);
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
// 切换到主线程回调(Java7 匿名Runnable)
|
||||||
|
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
callback.onResult(false, errorMsg);
|
callback.onResult(false, errorMsg);
|
||||||
@@ -118,23 +157,164 @@ public class APPUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增:Base64加密工具(URL安全编码,避免特殊字符影响URL拼接)
|
* 工具方法:获取当前应用的APK包文件对象
|
||||||
* @param content 待加密内容
|
* @param context 上下文
|
||||||
* @return 加密后的Base64字符串
|
* @return 当前应用APK文件File,失败返回null
|
||||||
*/
|
*/
|
||||||
private static String base64Encode(String content) {
|
private File getCurrentAppApkFile(Context context) {
|
||||||
try {
|
try {
|
||||||
// 使用URL安全的Base64编码(替换+为-,/为_,去除=)
|
// 从PackageManager获取当前应用的APK安装路径
|
||||||
byte[] contentBytes = content.getBytes("UTF-8");
|
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
|
||||||
return Base64.encodeToString(contentBytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP);
|
context.getPackageName(), 0
|
||||||
|
);
|
||||||
|
String apkPath = appInfo.sourceDir;
|
||||||
|
LogUtils.d(TAG, "getCurrentAppApkFile: 当前应用APK路径=" + apkPath);
|
||||||
|
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) {
|
} catch (Exception e) {
|
||||||
LogUtils.e(TAG, "base64Encode: 加密失败", e);
|
LogUtils.e(TAG, "getCurrentAppApkFile: 未知异常", e);
|
||||||
return content; // 加密失败则返回原始内容,避免请求异常
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前应用签名SHA1指纹(BASE64编码)
|
* 核心方法:复刻Android系统Signature解析逻辑
|
||||||
|
* 从CERT.RSA提取与signatures[0].toByteArray()一致的字节,再走SHA1+Base64
|
||||||
|
*/
|
||||||
|
private String getAPKSignFingerprint(File apkFile) {
|
||||||
|
// 先判空APK文件,避免空指针
|
||||||
|
if (apkFile == null || !apkFile.exists() || !apkFile.isFile()) {
|
||||||
|
LogUtils.w(TAG, "getAPKSignFingerprint: APK文件为空或不存在");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JarFile jarFile = null;
|
||||||
|
try {
|
||||||
|
jarFile = new JarFile(apkFile);
|
||||||
|
JarEntry sigEntry = jarFile.getJarEntry("META-INF/CERT.RSA");
|
||||||
|
if (sigEntry == null) {
|
||||||
|
LogUtils.w(TAG, "getAPKSignFingerprint: APK中未找到META-INF/CERT.RSA");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 读取CERT.RSA原始DER编码字节
|
||||||
|
byte[] rsaDerBytes = readStreamToBytes(jarFile.getInputStream(sigEntry));
|
||||||
|
if (rsaDerBytes == null || rsaDerBytes.length == 0) {
|
||||||
|
LogUtils.w(TAG, "getAPKSignFingerprint: 读取CERT.RSA字节为空");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析DER编码,提取Android系统标准Signatrue字节(与客户端完全一致)
|
||||||
|
byte[] androidSigBytes = parseDerForAndroidSignature(rsaDerBytes);
|
||||||
|
if (androidSigBytes == null || androidSigBytes.length == 0) {
|
||||||
|
LogUtils.w(TAG, "getAPKSignFingerprint: 解析Android标准Signature字节失败");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 与客户端完全相同的流程:SHA1摘要 → Android原生Base64.NO_WRAP(核心修复)
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||||
|
md.update(androidSigBytes);
|
||||||
|
byte[] sha1Digest = md.digest();
|
||||||
|
String signBase64 = Base64.encodeToString(sha1Digest, Base64.NO_WRAP);
|
||||||
|
|
||||||
|
LogUtils.d(TAG, "getAPKSignFingerprint: APK解析出的签名(Base64):" + signBase64);
|
||||||
|
return signBase64;
|
||||||
|
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
LogUtils.e(TAG, "getAPKSignFingerprint: 解析签名失败:SHA1算法不存在", e);
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "getAPKSignFingerprint: 解析APK签名异常", e);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (jarFile != null) {
|
||||||
|
try {
|
||||||
|
jarFile.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LogUtils.e(TAG, "getAPKSignFingerprint: 关闭JarFile流失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关键解析:复刻Android系统android.content.pm.Signature的DER编码解析逻辑
|
||||||
|
* 从CERT.RSA的DER字节中,提取与signatures[0].toByteArray()完全一致的签名字节
|
||||||
|
*/
|
||||||
|
private byte[] parseDerForAndroidSignature(byte[] derBytes) {
|
||||||
|
try {
|
||||||
|
int offset = 0;
|
||||||
|
// 跳过顶层SEQUENCE标签(0x30)
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
// 跳过证书主体字段,直到找到签名块的SEQUENCE标签,提取后续所有字节
|
||||||
|
while (offset < derBytes.length) {
|
||||||
|
if (derBytes[offset] == 0x30) {
|
||||||
|
// 提取签名块完整字节(与signatures[0].toByteArray()完全匹配)
|
||||||
|
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编码为Android Signature字节失败", 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Java7适配URL编码工具(UTF-8,处理所有特殊字符:/、=、&、+、空格等)
|
||||||
|
* @param content 待编码内容
|
||||||
|
* @return 编码后的字符串,失败返回原内容
|
||||||
|
*/
|
||||||
|
private static String urlEncode(String content) {
|
||||||
|
try {
|
||||||
|
// 用URLEncoder.encode,指定UTF-8(Java7必须显式指定,避免平台默认编码问题)
|
||||||
|
return URLEncoder.encode(content, "UTF-8");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.e(TAG, "urlEncode: 编码失败,content=" + content, e);
|
||||||
|
return content; // 编码失败返回原内容,避免请求中断
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从PackageManager获取当前应用签名SHA1指纹(BASE64编码,快速获取)
|
||||||
*/
|
*/
|
||||||
private static String getAppSignFingerprint(Context context) {
|
private static String getAppSignFingerprint(Context context) {
|
||||||
try {
|
try {
|
||||||
@@ -148,38 +328,16 @@ public class APPUtils {
|
|||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||||
md.update(signatures[0].toByteArray());
|
md.update(signatures[0].toByteArray());
|
||||||
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
|
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
|
||||||
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
LogUtils.e(TAG, "getAppSignFingerprint: 获取签名异常", e);
|
LogUtils.e(TAG, "getAppSignFingerprint: 获取包信息异常", e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
LogUtils.e(TAG, "getAppSignFingerprint: 获取SHA1算法异常", e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e);
|
LogUtils.e(TAG, "getAppSignFingerprint: 未知异常", e);
|
||||||
}
|
}
|
||||||
return null;
|
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 {
|
public interface CheckResultCallback {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -51,21 +51,21 @@ public class SignGetUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 新增:直接返回签名字符串,供对话框调用
|
// 新增:直接返回签名字符串,供对话框调用
|
||||||
public static String getSignStr(Context context) {
|
// public static String getSignStr(Context context) {
|
||||||
if (context == null) return null;
|
// if (context == null) return null;
|
||||||
try {
|
// try {
|
||||||
PackageManager pm = context.getPackageManager();
|
// PackageManager pm = context.getPackageManager();
|
||||||
PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
|
// PackageInfo pkgInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||||
Signature[] signatures = pkgInfo.signatures;
|
// Signature[] signatures = pkgInfo.signatures;
|
||||||
if (signatures == null || signatures.length == 0) return null;
|
// if (signatures == null || signatures.length == 0) return null;
|
||||||
|
//
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
// MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||||
md.update(signatures[0].toByteArray());
|
// md.update(signatures[0].toByteArray());
|
||||||
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
|
// return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
|
||||||
} catch (Exception e) {
|
// } catch (Exception e) {
|
||||||
LogUtils.e(TAG, "获取签名字符串失败", e);
|
// LogUtils.e(TAG, "获取签名字符串失败", e);
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user