固定APK调试文件测试成功
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -48,18 +48,23 @@ 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);
|
||||
// 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().checkAPKSignature(
|
||||
mContext,
|
||||
"WinBoLL", // projectName
|
||||
"WinBoLL_15.11.11.apk", // apkFileName
|
||||
new APPUtils.CheckResultCallback() {
|
||||
@Override
|
||||
public void onResult(boolean isValid, String message) {
|
||||
String szOfficialMessage;
|
||||
@@ -81,7 +86,8 @@ public class SignGetDialog extends Dialog {
|
||||
ToastUtils.show(szOfficialMessage);
|
||||
tvAuthResult.setText(szOfficialMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
@@ -123,14 +129,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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
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.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;
|
||||
@@ -22,7 +30,7 @@ import okhttp3.Response;
|
||||
/**
|
||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||
* @Date 2026/01/20 19:17
|
||||
* @Describe APPUtils 应用包名、签名校验工具类(OKHTTP网络校验版)
|
||||
* @Describe APPUtils 应用包名、签名校验工具类(OKHTTP网络校验版,兼容Java7,含URL编码+APK包签名校验)
|
||||
*/
|
||||
public class APPUtils {
|
||||
public static final String TAG = "APPUtils";
|
||||
@@ -36,46 +44,75 @@ public class APPUtils {
|
||||
/**
|
||||
* 检查应用合法性(包名校验+OKHTTP网络校验签名)
|
||||
* @param context 上下文
|
||||
* @param projectName 项目名称(入参)
|
||||
* @param apkFileName APK文件名(入参)
|
||||
* @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) {
|
||||
LogUtils.w(TAG, "checkAppValid: context为空,跳过校验");
|
||||
if (callback != null) callback.onResult(false, "context为空");
|
||||
LogUtils.w(TAG, "checkAPKSignature: 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;
|
||||
}
|
||||
|
||||
// 2. 获取当前应用签名(SHA1+Base64)和证书生效时间
|
||||
String currentSign = getAppSignFingerprint(context);
|
||||
long certValidTime = getCertValidTime(context); // 证书生效时间(毫秒时间戳)
|
||||
// 方式1:从PackageManager获取签名(原逻辑,快速)
|
||||
APKFileUtils.init();
|
||||
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) {
|
||||
String errorMsg = "获取应用签名失败";
|
||||
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
|
||||
if (callback != null) callback.onResult(false, errorMsg);
|
||||
String errorMsg = "获取应用签名失败(PackageManager+APK包解析均失败)";
|
||||
LogUtils.e(TAG, "checkAPKSignature: " + errorMsg);
|
||||
if (callback != null) {
|
||||
callback.onResult(false, errorMsg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG, "checkAPKSignature: 应用最终签名(SHA1+Base64)=" + currentSign);
|
||||
|
||||
// 新增:对currentSign进行Base64二次加密(URL安全编码,避免特殊字符)
|
||||
String encryptedSign = base64Encode(currentSign);
|
||||
LogUtils.d(TAG, "checkAppValid: 原始签名=" + currentSign + ",Base64二次加密后=" + encryptedSign);
|
||||
// 对动态参数做URL编码,避免特殊字符(/、=、&、空格等)导致解析异常
|
||||
String encodeProjectName = urlEncode(projectName);
|
||||
String encodeApkFileName = urlEncode(apkFileName);
|
||||
String encodeSignature = urlEncode(currentSign);
|
||||
LogUtils.d(TAG, "checkAPKSignature: URL编码后-项目名=" + encodeProjectName + ",APK名=" + encodeApkFileName + ",签名=" + encodeSignature);
|
||||
|
||||
// 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?projectName=%s&apkFileName=%s&signature=%s",
|
||||
GlobalApplication.getWinbollHost() + CHECK_API_URI,
|
||||
encodeProjectName,
|
||||
encodeApkFileName,
|
||||
encodeSignature);
|
||||
LogUtils.d(TAG, "checkAPKSignature: 发起网络校验请求,URL=" + requestUrl);
|
||||
|
||||
// 4. OKHTTP发起异步GET请求
|
||||
// OKHTTP发起异步GET请求
|
||||
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, "checkAPKSignature: " + errorMsg, e);
|
||||
if (callback != null) {
|
||||
// 切换到主线程回调
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||
// 切换到主线程回调(Java7 匿名Runnable)
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onResult(false, errorMsg);
|
||||
@@ -88,13 +125,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);
|
||||
LogUtils.d(TAG, "checkAPKSignature: 网络校验响应JSON=" + responseJson);
|
||||
// 解析JSON响应
|
||||
SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
|
||||
final SignCheckResponse checkResponse = sGson.fromJson(responseJson, SignCheckResponse.class);
|
||||
final boolean isValid = checkResponse != null && checkResponse.isValid();
|
||||
final String msg = checkResponse != null ? checkResponse.getMessage() : "响应解析失败";
|
||||
if (callback != null) {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||
// 切换到主线程回调(Java7 匿名Runnable)
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onResult(isValid, msg);
|
||||
@@ -103,9 +141,10 @@ public class APPUtils {
|
||||
}
|
||||
} else {
|
||||
final String errorMsg = "网络校验响应失败,code=" + response.code();
|
||||
LogUtils.e(TAG, "checkAppValid: " + errorMsg);
|
||||
LogUtils.e(TAG, "checkAPKSignature: " + errorMsg);
|
||||
if (callback != null) {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||
// 切换到主线程回调(Java7 匿名Runnable)
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onResult(false, errorMsg);
|
||||
@@ -118,23 +157,164 @@ 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);
|
||||
// 从PackageManager获取当前应用的APK安装路径
|
||||
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
|
||||
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) {
|
||||
LogUtils.e(TAG, "base64Encode: 加密失败", e);
|
||||
return content; // 加密失败则返回原始内容,避免请求异常
|
||||
LogUtils.e(TAG, "getCurrentAppApkFile: 未知异常", e);
|
||||
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) {
|
||||
try {
|
||||
@@ -148,38 +328,16 @@ 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 {
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user