APK校验接口调试完成
This commit is contained in:
@@ -5,6 +5,7 @@ import cc.winboll.util.IniConfigUtils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
@@ -14,19 +15,33 @@ import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
/**
|
||||
* APK文件工具类(单例)- 终极稳定版
|
||||
* 适配APK的PKCS7格式CERT.RSA,直接读取原始字节,与客户端getAppSignFingerprint1:1对齐
|
||||
* 解决Too short解析异常,保证签名稳定解析
|
||||
* 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) {
|
||||
@@ -38,23 +53,33 @@ public class APKFileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static APKFileUtils getInstance() {
|
||||
if (sInstance == null) {
|
||||
LogUtils.e("APKFileUtils", "请先调用init()初始化");
|
||||
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配置为空");
|
||||
LogUtils.e("APKFileUtils", "配置项apks_folder_path为空,初始化失败");
|
||||
return;
|
||||
}
|
||||
File rootDir = new File(apksRootPath);
|
||||
if (!rootDir.exists()) rootDir.mkdirs();
|
||||
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);
|
||||
@@ -62,105 +87,202 @@ public class APKFileUtils {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
/**
|
||||
* 对外暴露核心校验方法:签名 + 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(boolean isDebug, String projectName, String versionName, String apkFileName,
|
||||
String clientSignBase64, String clientFileHash) {
|
||||
return getInstance().doCheckAPK(isDebug, projectName, versionName, apkFileName, clientSignBase64, clientFileHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* 终极稳定解析:与客户端getAppSignFingerprint逻辑1:1完全对齐
|
||||
* 直接读取CERT.RSA原始字节(适配PKCS7格式),跳过证书解析,彻底解决Too short异常
|
||||
* 核心校验实现:严格按「哈希先验,签名后验」顺序,哈希不匹配直接返回
|
||||
*/
|
||||
public String getAPKSignFingerprint(File apkFile) {
|
||||
apkFile = new File("/sdcard/WinBoLLStudio/APKs/WinBoLL/tag/WinBoLL_15.11.11.apk");
|
||||
private boolean doCheckAPK(boolean isDebug, 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;
|
||||
if (isDebug) {
|
||||
apkFullPath = String.format("%s/%s/debug/%s_%s.apk",
|
||||
apksRootPath,
|
||||
projectName,
|
||||
projectName,
|
||||
versionName);
|
||||
} else {
|
||||
//正式环境路径(注释保留,切换时解开即可)
|
||||
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);
|
||||
// 适配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(含大小写)");
|
||||
// 先找大写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原始字节(和客户端Signature.toByteArray()底层一致,适配PKCS7)
|
||||
InputStream is = jarFile.getInputStream(sigEntry);
|
||||
byte[] sigRawBytes = readStreamToBytes(is);
|
||||
// 核心:直接读取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编码(去换行=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)
|
||||
// 与客户端完全一致的处理流程: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);
|
||||
LogUtils.d("APKFileUtils", "APK签名解析成功(Base64):" + signBase64);
|
||||
return signBase64;
|
||||
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
LogUtils.e("APKFileUtils", "解析签名失败:SHA1算法不存在", e);
|
||||
LogUtils.e("APKFileUtils", "解析签名失败:" + SIGN_ALGORITHM + "算法不存在", e);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
LogUtils.e("APKFileUtils", "解析APK签名异常", e);
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
} finally {
|
||||
if (jarFile != null) {
|
||||
// 强制关闭流资源,避免内存泄漏
|
||||
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 {
|
||||
jarFile.close();
|
||||
fis.close();
|
||||
} catch (IOException e) {
|
||||
LogUtils.e("APKFileUtils", "关闭JarFile流失败", e);
|
||||
LogUtils.e("APKFileUtils", "关闭APK文件流失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 稳定的流转字节数组:适配各种输入流,无空指针/截断问题
|
||||
* 流转字节数组工具方法:稳定读取任意输入流,无截断/空指针问题
|
||||
*/
|
||||
private byte[] readStreamToBytes(InputStream is) throws IOException {
|
||||
if (is == null) {
|
||||
@@ -168,16 +290,23 @@ public class APKFileUtils {
|
||||
return new byte[0];
|
||||
}
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096]; // 加大缓冲区,适配大签名文件
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -164,57 +164,72 @@ public class AuthHttpHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用签名校验接口(处理/api/app-signatures-check请求)
|
||||
* 适配URI:https://localhost:8080/api/app-signatures-check?projectName=WinBoLL&apkFileName=WinBoLL_15.11.11.apk&signature=xxx
|
||||
* 无validTime参数,纯签名匹配校验 + 全异常捕获(解决500问题)
|
||||
* 应用签名+文件哈希双重校验接口(处理/api/app-signatures-check请求)
|
||||
* 适配URI:http://localhost:8080/api/app-signatures-check?projectName=WinBoLL&versionName=15.11.11&clientSign=xxx&clientHash=xxx
|
||||
* 核心:官方签名校验 + SHA256文件哈希校验,全异常捕获,避免500错误
|
||||
*/
|
||||
public NanoHTTPD.Response handleAppSignatureCheck(Map<String, String> params) {
|
||||
LogUtils.d(TAG, "处理应用签名校验请求,参数:" + params);
|
||||
LogUtils.d(TAG, "处理应用签名+哈希双重校验请求,参数:" + params);
|
||||
// 全局异常捕获:解决500内部错误
|
||||
try {
|
||||
// 前置判空
|
||||
if (params == null) {
|
||||
LogUtils.w(TAG, "签名校验失败:未获取到任何参数");
|
||||
LogUtils.w(TAG, "双重校验失败:未获取到任何参数");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
|
||||
}
|
||||
|
||||
// 1. 解析核心必选参数(projectName/apkFileName/signature)
|
||||
// 1. 解析新核心必选参数(projectName/versionName/clientSign/clientHash)
|
||||
String szIsDebug = params.get("isDebug");
|
||||
String projectName = params.get("projectName");
|
||||
String apkFileName = params.get("apkFileName");
|
||||
String signature = params.get("signature");
|
||||
String versionName = params.get("versionName");
|
||||
String clientSign = params.get("clientSign");
|
||||
String clientHash = params.get("clientHash");
|
||||
|
||||
// 2. 必选参数非空校验
|
||||
if (projectName == null || projectName.isEmpty()
|
||||
|| apkFileName == null || apkFileName.isEmpty()
|
||||
|| signature == null || signature.isEmpty()) {
|
||||
LogUtils.w(TAG, "签名校验失败:必选参数不全(projectName/apkFileName/signature不能为空)");
|
||||
// 2. 必选参数非空+格式基础校验
|
||||
if (szIsDebug == null || szIsDebug.trim().isEmpty()
|
||||
|| projectName == null || projectName.trim().isEmpty()
|
||||
|| versionName == null || versionName.trim().isEmpty()
|
||||
|| clientSign == null || clientSign.trim().isEmpty()
|
||||
|| clientHash == null || clientHash.trim().isEmpty()) {
|
||||
LogUtils.w(TAG, "双重校验失败:必选参数不全(projectName/versionName/clientSign/clientHash不能为空)");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST,
|
||||
"参数错误:projectName、apkFileName、signature为必填参数");
|
||||
"参数错误:projectName、versionName、clientSign、clientHash为必填参数");
|
||||
}
|
||||
// 校验哈希长度(SHA256固定64位16进制,过滤明显非法值)
|
||||
if (clientHash.trim().length() != 64) {
|
||||
LogUtils.w(TAG, "双重校验失败:clientHash格式错误,需为64位SHA256哈希");
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST,
|
||||
"参数错误:clientHash需为64位SHA256十六进制哈希字符串");
|
||||
}
|
||||
|
||||
// 3. 核心校验:调用APKFileUtils完成APK签名精准匹配
|
||||
boolean isValid = APKFileUtils.checkAPKSignature(projectName, apkFileName, signature);
|
||||
// 3. 核心双校验:调用改造后的APKFileUtils.checkAPK(官方签名+文件哈希)
|
||||
// 注:APK文件名拼接规则:项目名_版本名.apk(与客户端包名/版本名对齐,规范命名)
|
||||
String apkFileName = String.format("%s_%s.apk", projectName.trim(), versionName.trim());
|
||||
boolean isValid = APKFileUtils.checkAPK(szIsDebug.trim().equals("true"), projectName.trim(), versionName.trim(),
|
||||
apkFileName, clientSign.trim(), clientHash.trim());
|
||||
|
||||
// 4. 日志输出详情(便于问题排查)
|
||||
LogUtils.d(TAG, String.format("签名校验结果:%s | 项目名:%s | APK文件名:%s | 客户端签名:%s",
|
||||
isValid ? "✅ 成功" : "❌ 失败", projectName, apkFileName, signature));
|
||||
// 4. 日志输出详情(便于问题排查,含完整校验维度)
|
||||
LogUtils.d(TAG, String.format("签名+哈希双重校验结果:%s | 项目名:%s | 版本名:%s | APK文件名:%s | 客户端签名:%s | 客户端哈希:%s",
|
||||
isValid ? "✅ 成功" : "❌ 失败",
|
||||
projectName.trim(), versionName.trim(), apkFileName,
|
||||
clientSign.trim(), clientHash.trim()));
|
||||
|
||||
// 5. 构建响应结果(与APP端预期格式一致,字段简洁无冗余)
|
||||
String msg = isValid ? "应用签名校验通过,为合法应用" : "应用签名校验失败:APK签名与客户端传入签名不匹配";
|
||||
// 5. 构建响应结果(与APP端预期格式一致,字段简洁无冗余,含核心返参)
|
||||
String msg = isValid ? "应用签名+文件哈希双重校验通过,为合法正版应用" : "应用校验失败:签名或文件哈希不匹配,非正版/篡改应用";
|
||||
int code = isValid ? 200 : 403;
|
||||
|
||||
String responseJson = String.format(
|
||||
"{\"code\":%d,\"msg\":\"%s\",\"data\":{\"valid\":%b,\"projectName\":\"%s\",\"apkFileName\":\"%s\",\"signature\":\"%s\"}}",
|
||||
code, msg, isValid, projectName, apkFileName, signature
|
||||
"{\"code\":%d,\"msg\":\"%s\",\"data\":{\"valid\":%b,\"projectName\":\"%s\",\"versionName\":\"%s\",\"apkFileName\":\"%s\"}}",
|
||||
code, msg, isValid, projectName.trim(), versionName.trim(), apkFileName
|
||||
);
|
||||
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, responseJson);
|
||||
} catch (IllegalStateException e) {
|
||||
// 捕获APKFileUtils未初始化异常(最常见原因)
|
||||
LogUtils.e(TAG, "签名校验失败:APKFileUtils未初始化,请在服务启动时调用APKFileUtils.init()", e);
|
||||
LogUtils.e(TAG, "双重校验失败:APKFileUtils未初始化,请在服务启动时调用APKFileUtils.init()", e);
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, "服务内部异常:APKFileUtils未初始化");
|
||||
} catch (Exception e) {
|
||||
// 捕获所有其他异常,打印完整堆栈(定位根因)
|
||||
LogUtils.e(TAG, "签名校验失败:服务内部异常", e);
|
||||
LogUtils.e(TAG, "签名+哈希双重校验失败:服务内部异常", e);
|
||||
return buildErrorResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, "服务内部异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user