# -*- coding: utf-8 -*- """生成 TlsCompat.java —— 修手表端 "SSLHandshakeException: Chain validation failed"。 问题: 手表连中转报 Chain validation failed。 证书链是:xxmxxs.top <- GTS WE1 <- GTS Root R4 <- GlobalSign Root CA 廉价手表的根证书库常常缺 GTS Root R4(2021 年才加进 AOSP), 于是找不到信任锚 -> 握手直接失败。 修法: 把这两个根证书**内嵌进 APK**,校验时"系统库 或 内置库"任一通过即可。 这两个都是公开的合法根证书,不是"跳过校验" —— 安全性仍然完整:证书必须由可信 CA 签发、且域名匹配。 只是把可信 CA 集合从"手表自带的那一份"扩大到"手表自带的 + 这两个"。 另外把错误信息翻译成人话: * 如果报的是时间相关(not yet valid / expired)-> 提示手表时间不对 * 如果还是校验失败 -> 提示可能被中间人劫持 顺带还把 "HTTP 错误码" 和 "网络不通" 区分开,便于用户自查。 """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") CERTS = ROOT / "android" / "certs" OUT = (ROOT / "android" / "src" / "com" / "lanremote" / "watch" / "TlsCompat.java") def pem_to_java(text: str) -> str: """把 PEM 变成 Java 字符串拼接(每行一个常量,避免超长行)。""" lines = [line.strip() for line in text.strip().splitlines() if line.strip()] parts = " +\n ".join(f'"{line}\\n"' for line in lines) return parts def main() -> int: gts = (CERTS / "gts_root_r4.pem").read_text(encoding="ascii") globalsign = (CERTS / "globalsign_root_ca.pem").read_text(encoding="ascii") source = '''package com.lanremote.watch; import java.io.ByteArrayInputStream; import java.net.HttpURLConnection; import java.net.URL; import java.security.KeyStore; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; /** * TLS 兼容层:让老手表也能连上中转。 * *
为什么需要: * 手表端报 {@code SSLHandshakeException: Chain validation failed}。 * 中转的证书链是 * {@code relay.xxmxxs.top <- GTS WE1 <- GTS Root R4 <- GlobalSign Root CA}, * 而很多廉价手表的系统根证书库缺 GTS Root R4(AOSP 2021 年才加), * 于是找不到信任锚,握手直接失败。 * *
做法: * 把这两个根证书内嵌进 APK,校验时系统库或内置库任一通过即可。 * 这两个都是公开的合法根证书,不是跳过校验: * 证书仍必须由可信 CA 签发、且域名匹配,只是把可信 CA 的集合 * 从"手表自带的那一份"扩大到"手表自带的 + 这两个"。 * *
同时把难懂的异常翻译成用户能看懂的话。 */ public final class TlsCompat { private TlsCompat() { } // ---------------- 内置的根证书 ---------------- /** Google Trust Services Root R4(自签名根,EC P-384)。 */ private static final String GTS_ROOT_R4 = %GTS%; /** GlobalSign Root CA(自签名根,RSA 2048,1998 年起就在各系统里)。 */ private static final String GLOBALSIGN_ROOT_CA = %GS%; private static SSLSocketFactory factory; private static X509TrustManager bundled; private static boolean ready; private static boolean initTried; // ---------------- 对外 ---------------- /** * 打开一个连接,HTTPS 时挂上兼容的 SSLSocketFactory。 * *
HTTP(局域网中转)不走这里,原样返回。 */ public static HttpURLConnection open(String urlText, int timeoutMs) throws Exception { HttpURLConnection connection = (HttpURLConnection) new URL(urlText).openConnection(); connection.setConnectTimeout(timeoutMs); connection.setReadTimeout(timeoutMs); connection.setRequestProperty("User-Agent", "LanRemote/1.4.0"); connection.setRequestProperty("Accept-Encoding", "identity"); if (connection instanceof HttpsURLConnection) { SSLSocketFactory ssl = factory(); if (ssl != null) { ((HttpsURLConnection) connection).setSSLSocketFactory(ssl); } } return connection; } /** 拿到兼容的 SSLSocketFactory;准备失败就返回 null(退回系统默认)。 */ public static synchronized SSLSocketFactory factory() { if (!initTried) { initTried = true; try { KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType()); store.load(null, null); CertificateFactory cf = CertificateFactory.getInstance("X.509"); int index = 0; for (String pem : new String[]{GTS_ROOT_R4, GLOBALSIGN_ROOT_CA}) { X509Certificate cert = (X509Certificate) cf.generateCertificate( new ByteArrayInputStream(pem.getBytes("UTF-8"))); store.setCertificateEntry("lanremote-ca-" + (index++), cert); } TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init(store); for (TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509TrustManager) { bundled = (X509TrustManager) tm; break; } } SSLContext context = SSLContext.getInstance("TLS"); context.init(null, new TrustManager[]{new CompatTrustManager()}, null); factory = context.getSocketFactory(); ready = true; } catch (Throwable error) { // 准备失败就退回系统默认,不影响正常设备 factory = null; ready = false; } } return ready ? factory : null; } /** * 把 TLS / 网络异常翻译成用户能看懂的一句话。 * *
手表上原本只会显示一长串英文类名,用户完全不知道该怎么办。 */ public static String explain(Throwable error) { if (error == null) { return "未知错误"; } String text = String.valueOf(error); String lower = text.toLowerCase(); if (lower.contains("chain validation failed") || lower.contains("trust anchor") || lower.contains("certpathvalidator")) { return "证书校验失败(中转的根证书手表不认识)。" + "可能是手表系统太旧,或网络被劫持。"; } if (lower.contains("not yet valid") || lower.contains("expired") || lower.contains("certificate expired")) { return "证书时间不对 —— 把手表的【日期和时间】调成自动获取," + "再试一次。"; } if (lower.contains("timeout") || lower.contains("timed out")) { return "连接超时(网络太慢或被挡住)"; } if (lower.contains("unable to resolve host") || lower.contains("unknownhost")) { return "域名解析不了(检查手表能不能上网)"; } if (lower.contains("connection refused")) { return "对方拒绝了连接(电脑上 ykxmz 没开?)"; } if (lower.contains("network is unreachable") || lower.contains("no route")) { return "手表没联网(检查 WiFi)"; } if (lower.contains("eof") || lower.contains("reset")) { return "连接被中断(网络不稳)"; } if (text.length() > 80) { return text.substring(0, 80); } return text; } /** * 组合信任:系统默认 或 内置根证书,任一通过即认可。 * *
这是修这个问题的关键 —— * 老手表缺 GTS Root R4,但内置的那份有,于是校验通过; * 新设备走系统默认照样通过。两边都不削弱安全性。 */ private static final class CompatTrustManager implements X509TrustManager { private final X509TrustManager system = findSystem(); private static X509TrustManager findSystem() { try { TrustManagerFactory tmf = TrustManagerFactory.getInstance( TrustManagerFactory.getDefaultAlgorithm()); tmf.init((KeyStore) null); // null = 系统默认 for (TrustManager tm : tmf.getTrustManagers()) { if (tm instanceof X509TrustManager) { return (X509TrustManager) tm; } } } catch (Throwable ignored) { // 拿不到就算了,下面用内置的 } return null; } @Override public void checkClientTrusted(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException { if (system != null) { system.checkClientTrusted(chain, authType); return; } if (bundled != null) { bundled.checkClientTrusted(chain, authType); } } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws java.security.cert.CertificateException { java.security.cert.CertificateException first = null; if (system != null) { try { system.checkServerTrusted(chain, authType); return; } catch (java.security.cert.CertificateException error) { first = error; } } if (bundled != null) { try { bundled.checkServerTrusted(chain, authType); return; } catch (java.security.cert.CertificateException error) { if (first == null) { first = error; } } } throw (first != null ? first : new java.security.cert.CertificateException( "证书校验失败")); } @Override public X509Certificate[] getAcceptedIssuers() { X509Certificate[] a = system == null ? new X509Certificate[0] : system.getAcceptedIssuers(); X509Certificate[] b = bundled == null ? new X509Certificate[0] : bundled.getAcceptedIssuers(); X509Certificate[] all = new X509Certificate[a.length + b.length]; System.arraycopy(a, 0, all, 0, a.length); System.arraycopy(b, 0, all, a.length, b.length); return all; } } } ''' source = source.replace("%GTS%", pem_to_java(gts)) source = source.replace("%GS%", pem_to_java(globalsign)) OUT.parent.mkdir(parents=True, exist_ok=True) OUT.write_text(source, encoding="utf-8", newline="\n") print(f" 已生成 {OUT.name}({OUT.stat().st_size} 字节)") print(f" 内置 GTS Root R4: {len(gts)} 字节 PEM") print(f" 内置 GlobalSign Root CA: {len(globalsign)} 字节 PEM") # 语法自检:括号配平 + 关键结构在 for needle in ("class TlsCompat", "CompatTrustManager", "checkServerTrusted", "setSSLSocketFactory", "explain("): print(f" 含 {needle}: {'✓' if needle in source else '✗'}") return 0 if __name__ == "__main__": sys.exit(main())