固定APK调试文件测试成功

This commit is contained in:
2026-01-23 21:05:12 +08:00
parent 0b44bab651
commit 687c5a62d2
4 changed files with 141 additions and 226 deletions

View File

@@ -2,24 +2,24 @@ package cc.winboll.app;
import cc.winboll.LogUtils;
import cc.winboll.util.IniConfigUtils;
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.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
/**
* APK文件工具类单例模式- 读取APK+签名校验匹配客户端1次Base64
* 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";
@@ -62,22 +62,25 @@ public class APKFileUtils {
}
}
public static boolean checkAPKSignature(String projectName, String apkFileName, String base64SignFingerprint) {
return getInstance().doCheckAPK(projectName, apkFileName, base64SignFingerprint);
public static boolean checkAPKSignature(String projectName, String apkFileName, String clientSignBase64) {
return getInstance().doCheckAPK(projectName, apkFileName, clientSignBase64);
}
private boolean doCheckAPK(String projectName, String apkFileName, String base64SignFingerprint) {
if (projectName == null || apkFileName == null || base64SignFingerprint == null
|| projectName.isEmpty() || apkFileName.isEmpty() || base64SignFingerprint.isEmpty()) {
LogUtils.w("APKFileUtils", "参数不能为空");
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;
}
if (apksRootPath.isEmpty()) {
LogUtils.w("APKFileUtils", "APK根目录未配置");
// 2. 根目录校验
if (apksRootPath == null || apksRootPath.trim().isEmpty()) {
LogUtils.w("APKFileUtils", "APK根目录未配置apks_folder_path");
return false;
}
String apkFullPath = apksRootPath + File.separator + projectName
// 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")) {
@@ -85,82 +88,96 @@ public class APKFileUtils {
return false;
}
String apkSignBase64;
try {
apkSignBase64 = getAPKSignFingerprint(apkFile);
if (apkSignBase64 == null) {
LogUtils.w("APKFileUtils", "解析APK签名失败");
return false;
}
LogUtils.d("APKFileUtils", "APK原始签名(Base64)" + apkSignBase64);
} catch (Exception e) {
LogUtils.e("APKFileUtils", "解析APK异常", e);
// 4. 解析APK签名终极稳定版直接读取CERT.RSA原始字节与客户端对齐
String apkSignBase64 = getAPKSignFingerprint(apkFile);
if (apkSignBase64 == null || apkSignBase64.trim().isEmpty()) {
LogUtils.w("APKFileUtils", "解析APK签名失败返回null/空值");
return false;
}
// 关键改1去掉二次编码直接对比兼容末尾=号
boolean match = apkSignBase64.equals(base64SignFingerprint);
LogUtils.d("APKFileUtils", "签名对比APK签名:"+apkSignBase64+"|客户端传入:"+base64SignFingerprint+"|匹配:"+match);
return match;
// 5. 签名对比
LogUtils.d("APKFileUtils", "【签名对比】APK解析签名" + apkSignBase64);
LogUtils.d("APKFileUtils", "签名对比】客户端传入签名:" + clientSignBase64);
boolean isMatch = apkSignBase64.equals(clientSignBase64);
LogUtils.i("APKFileUtils", "【签名对比结果】" + (isMatch ? "✅ 匹配" : "❌ 不匹配"));
return isMatch;
}
private String getAPKSignFingerprint(File apkFile) throws NoSuchAlgorithmException, IOException, CertificateException {
try (JarFile jarFile = new JarFile(apkFile)) {
ZipEntry certEntry = jarFile.getEntry("META-INF/CERT.RSA");
if (certEntry == null) {
certEntry = jarFile.getEntry("META-INF/CERT.DSA");
if (certEntry == null) {
LogUtils.w("APKFileUtils", "未找到META-INF下的签名证书文件");
/**
* 终极稳定解析与客户端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");
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;
}
}
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
InputStream certIs = jarFile.getInputStream(certEntry);
X509Certificate cert = (X509Certificate) certFactory.generateCertificate(certIs);
certIs.close();
// 核心直接读取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");
byte[] digest = md.digest(cert.getEncoded());
// 关键改2加padding补=号和客户端Base64.NO_WRAP完全一致
return Base64.getEncoder().encodeToString(digest);
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 {
byte[] buffer = new byte[1024];
if (is == null) {
LogUtils.w("APKFileUtils", "readStreamToBytes: 输入流为null");
return new byte[0];
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096]; // 加大缓冲区,适配大签名文件
int len;
java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
while ((len = is.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
byte[] result = bos.toByteArray();
bos.close();
// 关闭流(顺序不可换)
is.close();
bos.close();
return result;
}
public static void main(String[] args) {
test();
}
private static void test() {
IniConfigUtils.init();
LogUtils.init();
APKFileUtils.init();
String testProject = "WinBoLL";
String testApkName = "WinBoLL_15.11.11.apk";
String testBase64Sign = "bMArVdXE4ZZo42vS9e/kXE63MkE=";
System.out.println("===== APKFileUtils 单元测试 =====");
System.out.println("APK根目录" + getInstance().apksRootPath);
System.out.println("项目名:" + testProject);
System.out.println("APK名" + testApkName);
boolean result = checkAPKSignature(testProject, testApkName, testBase64Sign);
System.out.println("签名校验结果:" + (result ? "✅ 成功" : "❌ 失败"));
System.out.println("==================================");
}
}

View File

@@ -1,100 +0,0 @@
package cc.winboll.app;
import cc.winboll.LogUtils;
import cc.winboll.util.IniConfigUtils;
import java.util.Base64;
public class AppSignaturesUtils {
// 1. 懒汉单例(解决过早加载问题)
private static AppSignaturesUtils INSTANCE = null;
private static final String CONFIG_SECTION = "APP";
private static final String KEY_SIGN_FINGERPRINT = "app_sign_fingerprint";
private static final String KEY_EFFECTIVE_TIME = "app_sign_effective_time";
private String targetSign;
private long effectiveTime;
// 私有构造
private AppSignaturesUtils() {}
// 2. 懒加载单例+确保配置已加载
public static AppSignaturesUtils getInstance() {
if (INSTANCE == null) {
synchronized (AppSignaturesUtils.class) {
if (INSTANCE == null) {
INSTANCE = new AppSignaturesUtils();
INSTANCE.initConfig(); // 延迟初始化配置
}
}
}
return INSTANCE;
}
// 公共init方法供外部主动初始化可选
public static void init() {
getInstance();
}
private void initConfig() {
try {
this.targetSign = IniConfigUtils.getConfigValue(CONFIG_SECTION, KEY_SIGN_FINGERPRINT, "").trim();
String timeStr = IniConfigUtils.getConfigValue(CONFIG_SECTION, KEY_EFFECTIVE_TIME, "0").trim();
this.effectiveTime = Long.parseLong(timeStr);
LogUtils.i("AppSignaturesUtils", "配置读取完成|目标签名:" + targetSign + "|生效时间戳:" + effectiveTime);
} catch (Exception e) {
LogUtils.e("AppSignaturesUtils", "配置读取失败", e);
this.targetSign = "";
this.effectiveTime = 0L;
}
}
public static boolean checksignatures(String signature, long validTime) {
return getInstance().doCheck(signature, validTime);
}
private boolean doCheck(String signature, long validTime) {
if (signature == null || signature.isEmpty() || targetSign.isEmpty() || effectiveTime == 0) {
LogUtils.w("AppSignaturesUtils", "校验失败:签名为空或配置未正确加载");
return false;
}
String decryptedSign;
try {
byte[] signBytes = Base64.getDecoder().decode(signature);
decryptedSign = new String(signBytes, "UTF-8");
} catch (Exception e) {
LogUtils.w("AppSignaturesUtils", "签名解密失败", e);
return false;
}
boolean signMatch = targetSign.equals(decryptedSign);
boolean timeValid = validTime >= effectiveTime;
LogUtils.d("AppSignaturesUtils", "解密后签名:" + decryptedSign + "|签名匹配:" + signMatch + "|时间有效:" + timeValid);
return signMatch && timeValid;
}
public static void main(String[] args) {
test();
}
private static void test() {
IniConfigUtils.init();
LogUtils.init();
AppSignaturesUtils.init();
// 关键修改:手动生成正确签名,彻底杜绝复制粘贴隐形字符
String rawSign = "WinBoLL_AuthCenter_Valid_Sign";
String testSignature = Base64.getEncoder().encodeToString(rawSign.getBytes());
long testValidTime = 1769000000000L;
System.out.println("===== AppSignaturesUtils 单元测试 =====");
System.out.println("原文字符串:" + rawSign);
System.out.println("自动Base64编码后" + testSignature);
System.out.println("示例生效时间戳:" + testValidTime);
boolean result = AppSignaturesUtils.checksignatures(testSignature, testValidTime);
System.out.println("校验结果:" + (result ? "✅ 成功" : "❌ 失败"));
System.out.println("======================================");
}
}

View File

@@ -1,10 +1,11 @@
package cc.winboll.service;
import cc.winboll.LogUtils;
import cc.winboll.app.APKFileUtils;
import cc.winboll.util.IniConfigUtils;
import fi.iki.elonen.NanoHTTPD;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
/**
* HTTP监听服务类仅负责服务启停、请求接收与分发
@@ -34,6 +35,11 @@ public class AuthCenterHttpService extends NanoHTTPD {
public void start() throws IOException {
LogUtils.d(TAG, "start() 函数调用启动HTTP监听服务");
// ========== 必须按此顺序添加(核心修复)==========
IniConfigUtils.init(); // 1. 先初始化配置工具
LogUtils.init(); // 2. 再初始化日志工具
APKFileUtils.init(); // 3. 最后初始化APK签名校验工具
// ==============================================
super.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
isRunning = true;
LogUtils.i(TAG, "HTTP监听服务启动成功端口" + getListeningPort());

View File

@@ -1,7 +1,7 @@
package cc.winboll.service;
import cc.winboll.LogUtils;
import cc.winboll.app.AppSignaturesUtils;
import cc.winboll.app.APKFileUtils;
import cc.winboll.util.ServerUtils;
import cc.winboll.util.ConsoleVersionUtils;
import fi.iki.elonen.NanoHTTPD;
@@ -162,70 +162,62 @@ public class AuthHttpHandler {
}
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, result);
}
/**
* 应用签名校验接口(处理/api/app-signatures-check请求
* 功能调用AppSignaturesUtils完成校验返回JSON格式校验结果
* 适配URIhttps://localhost:8080/api/app-signatures-check?projectName=WinBoLL&apkFileName=WinBoLL_15.11.11.apk&signature=xxx
* 无validTime参数纯签名匹配校验 + 全异常捕获解决500问题
*/
public NanoHTTPD.Response handleAppSignatureCheck(Map<String, String> params) {
LogUtils.d(TAG, "处理应用签名校验请求,参数:" + params);
// 前置判空
if (params == null) {
LogUtils.w(TAG, "签名校验失败:未获取到任何参数");
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
}
// 1. 参数校验signature和validTime必填
String signature = params.get("signature");
String validTimeStr = params.get("validTime");
if (signature == null || signature.isEmpty() || validTimeStr == null || validTimeStr.isEmpty()) {
LogUtils.w(TAG, "签名校验失败参数不全signature或validTime为空");
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "参数错误signature和validTime不能为空");
}
// 2. validTime格式校验必须是数字时间戳
long validTime;
// 全局异常捕获解决500内部错误
try {
validTime = Long.parseLong(validTimeStr);
} catch (NumberFormatException e) {
LogUtils.w(TAG, "签名校验失败:validTime格式错误非数字" + validTimeStr);
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "参数错误validTime必须是数字时间戳");
}
// 前置判空
if (params == null) {
LogUtils.w(TAG, "签名校验失败:未获取到任何参数");
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST, "未获取到请求参数");
}
// 3. 核心校验逻辑调用AppSignaturesUtils 替代原有硬编码逻辑
boolean isValid = AppSignaturesUtils.checksignatures(signature, validTime);
// 1. 解析核心必选参数projectName/apkFileName/signature
String projectName = params.get("projectName");
String apkFileName = params.get("apkFileName");
String signature = params.get("signature");
// 4. Base64解密仅用于日志输出不影响校验逻辑
String decryptedSign = "";
try {
byte[] signBytes = Base64.getDecoder().decode(signature);
decryptedSign = new String(signBytes, "UTF-8");
// 2. 必选参数非空校验
if (projectName == null || projectName.isEmpty()
|| apkFileName == null || apkFileName.isEmpty()
|| signature == null || signature.isEmpty()) {
LogUtils.w(TAG, "签名校验失败必选参数不全projectName/apkFileName/signature不能为空");
return buildErrorResponse(NanoHTTPD.Response.Status.BAD_REQUEST,
"参数错误projectName、apkFileName、signature为必填参数");
}
// 3. 核心校验调用APKFileUtils完成APK签名精准匹配
boolean isValid = APKFileUtils.checkAPKSignature(projectName, apkFileName, signature);
// 4. 日志输出详情(便于问题排查)
LogUtils.d(TAG, String.format("签名校验结果:%s | 项目名:%s | APK文件名%s | 客户端签名:%s",
isValid ? "✅ 成功" : "❌ 失败", projectName, apkFileName, signature));
// 5. 构建响应结果与APP端预期格式一致字段简洁无冗余
String msg = isValid ? "应用签名校验通过,为合法应用" : "应用签名校验失败APK签名与客户端传入签名不匹配";
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
);
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, responseJson);
} catch (IllegalStateException e) {
// 捕获APKFileUtils未初始化异常最常见原因
LogUtils.e(TAG, "签名校验失败APKFileUtils未初始化请在服务启动时调用APKFileUtils.init()", e);
return buildErrorResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, "服务内部异常APKFileUtils未初始化");
} catch (Exception e) {
decryptedSign = "解密失败";
// 捕获所有其他异常,打印完整堆栈(定位根因)
LogUtils.e(TAG, "签名校验失败:服务内部异常", e);
return buildErrorResponse(NanoHTTPD.Response.Status.INTERNAL_ERROR, "服务内部异常:" + e.getMessage());
}
// 5. 构建响应结果
String msg;
if (isValid) {
msg = "应用签名校验通过,为合法应用";
LogUtils.d(TAG, "签名校验通过:" + msg + ",解密后签名:" + decryptedSign + ",时间戳:" + validTime);
} else {
msg = "应用签名校验失败(签名不匹配或时间不满足生效要求)";
LogUtils.w(TAG, "签名校验失败:" + msg + ",解密后签名:" + decryptedSign + ",时间戳:" + validTime);
}
// 6. 返回JSON响应与APP端预期格式一致字段完整
String responseJson = String.format(
"{\"code\":%d,\"msg\":\"%s\",\"data\":{\"valid\":%b,\"signature\":\"%s\",\"decryptedSign\":\"%s\",\"validTime\":%d}}",
isValid ? 200 : 403,
msg,
isValid,
signature,
decryptedSign,
validTime
);
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, CONTENT_TYPE_JSON, responseJson);
}
}