package com.lanremote.watch; import java.io.ByteArrayInputStream; import java.io.IOException; 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 = "-----BEGIN CERTIFICATE-----\n" + "MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD\n" + "VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG\n" + "A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw\n" + "WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz\n" + "IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n" + "AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi\n" + "QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR\n" + "HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\n" + "BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D\n" + "9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8\n" + "p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD\n" + "-----END CERTIFICATE-----\n"; /** GlobalSign Root CA(自签名根,RSA 2048,1998 年起就在各系统里)。 */ private static final String GLOBALSIGN_ROOT_CA = "-----BEGIN CERTIFICATE-----\n" + "MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG\n" + "A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv\n" + "b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw\n" + "MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i\n" + "YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT\n" + "aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ\n" + "jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp\n" + "xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp\n" + "1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG\n" + "snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ\n" + "U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8\n" + "9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E\n" + "BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B\n" + "AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz\n" + "yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE\n" + "38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP\n" + "AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad\n" + "DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME\n" + "HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A==\n" + "-----END CERTIFICATE-----\n"; 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) throws IOException { HttpURLConnection connection = (HttpURLConnection) new URL(urlText).openConnection(); connection.setUseCaches(false); 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; } } }