commit 7b8d1895660d7974dd37d499d22b4a50819ff007 Author: huanglinhuan Date: Fri Oct 10 00:16:47 2025 +0800 first commit diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..4e2ca72 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +tls \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..c8f004d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..9a9f2ba --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "com.example.tls" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.tls" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + compose = true + } +} + +dependencies { + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + + // 网络和TLS相关依赖 + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") + implementation("org.bouncycastle:bcprov-jdk15on:1.70") + implementation("org.bouncycastle:bcpkix-jdk15on:1.70") + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/tls/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/example/tls/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9eb98e6 --- /dev/null +++ b/app/src/androidTest/java/com/example/tls/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.tls + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.tls", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8ca1cbc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/README.md b/app/src/main/assets/README.md new file mode 100644 index 0000000..007a4b1 --- /dev/null +++ b/app/src/main/assets/README.md @@ -0,0 +1,58 @@ +# 证书文件说明 + +请将您的证书文件放在此目录下: + +- 客户端证书: `client.crt` (必需,用于客户端证书认证) +- 客户端私钥: `client.key` (必需,用于客户端证书认证) +- CA证书: `ca.crt` (可选,用于服务器证书验证) + +## 证书格式要求 + +### 客户端证书 (client.crt) +- 格式: PEM格式的X.509证书 +- 内容示例: +``` +-----BEGIN CERTIFICATE----- +MIIC...证书内容... +-----END CERTIFICATE----- +``` + +### 客户端私钥 (client.key) +- 格式: PEM格式的PKCS#8私钥(未加密) +- 内容示例: +``` +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC... +-----END PRIVATE KEY----- +``` + +**重要**: 私钥必须是PKCS#8格式,不能是PKCS#1格式(RSA PRIVATE KEY)。 + +## 服务器错误解决 + +如果服务器报错 "peer did not return a certificate",说明服务器要求客户端证书认证。 + +### 解决方案: +1. 将客户端证书文件 `client.crt` 和 `client.key` 放在此目录下 +2. 确保证书文件格式正确 +3. 重新运行应用进行测试 + +### 证书生成示例(使用OpenSSL): +```bash +# 生成私钥(PKCS#8格式) +openssl genpkey -algorithm RSA -out client.key -pkcs8 -outform PEM + +# 生成证书请求 +openssl req -new -key client.key -out client.csr + +# 生成证书(自签名) +openssl x509 -req -in client.csr -signkey client.key -out client.crt -days 365 +``` + +### 私钥格式转换(如果已有PKCS#1格式私钥): +```bash +# 将PKCS#1格式转换为PKCS#8格式 +openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in old_key.key -out client.key +``` + +如果没有证书文件,应用将尝试不使用客户端证书进行连接,但可能被服务器拒绝。 diff --git a/app/src/main/assets/ca.crt b/app/src/main/assets/ca.crt new file mode 100644 index 0000000..f88620e --- /dev/null +++ b/app/src/main/assets/ca.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDozCCAougAwIBAgIUPJ2ri4qd4BpnUcb/6vCZ//2uYd8wDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl +aWppbmcxDjAMBgNVBAoMBU15T3JnMQswCQYDVQQLDAJJVDERMA8GA1UEAwwITXlS +b290Q0EwHhcNMjUxMDA3MDYwMDI3WhcNMzUxMDA1MDYwMDI3WjBhMQswCQYDVQQG +EwJDTjEQMA4GA1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEOMAwGA1UE +CgwFTXlPcmcxCzAJBgNVBAsMAklUMREwDwYDVQQDDAhNeVJvb3RDQTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMf0LkX1LF71yhsnqM4laww8B6RRo3nF +UD8pzGr4J8iFhtPyUr8T4Q3B1Et98sV+7zx4qxgYaMZ1IMX6QDti/T1qurAONhy9 +/HDGDrvMYE0AOQhvRvcqicpayqNqjLBJgdzh92Uei5s8F30tqLVm8tN715UaVDuc +enjh6s9e1GpR6CzvbKLEmsUESC9YFeV1kdlbn+b5cFgDElBan2nEpS2sAUamc5pt ++h8cjwojd58o3s+Cfc69DXnbSzRVo/7DSLO0itBLNvoo1YBSmJkuGDzsu0dcUD3m +84scYvxzvOZiUqESEaqXRMPw7OcOAQSbjloUgWfiX35ntmdeK5xyk90CAwEAAaNT +MFEwHQYDVR0OBBYEFH2YOXkKFAfeuB+IXTTB5NjXFsBqMB8GA1UdIwQYMBaAFH2Y +OXkKFAfeuB+IXTTB5NjXFsBqMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAA7+FwMG6yJi9vDHdaFRWA/RFSTBCo5bcFJ7DO7DwvsEOfHnP5ygulDY +astft8RsGelVSlXJtWHaCdByST9V282q+P2Q2jjwfmVB5ATq2MAgobYsU3nwLg3r +c6zd34GeTJ6wjyGF5g3rDG8NqcHlARRwL8GbWX5lbcq9ooYLl9KHgFY+gz6u5njx +WyJdnCMLRUgTt5tsT/3k5PU2V9C5kwZh/eAocEsfhuXDplFHeqdvZTZaXD/oX+So +jPnLL6fsAoCskIhVVM0xGAXtWmhk+1LVtrqCfTQrqBhtXi5qCl3NLmBf+d0OClwh +HgkcqJYr+WaiEHLUyIdfZji/ZEml7aA= +-----END CERTIFICATE----- diff --git a/app/src/main/assets/ca.key b/app/src/main/assets/ca.key new file mode 100644 index 0000000..57b528a --- /dev/null +++ b/app/src/main/assets/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDH9C5F9Sxe9cob +J6jOJWsMPAekUaN5xVA/Kcxq+CfIhYbT8lK/E+ENwdRLffLFfu88eKsYGGjGdSDF ++kA7Yv09arqwDjYcvfxwxg67zGBNADkIb0b3KonKWsqjaoywSYHc4fdlHoubPBd9 +Lai1ZvLTe9eVGlQ7nHp44erPXtRqUegs72yixJrFBEgvWBXldZHZW5/m+XBYAxJQ +Wp9pxKUtrAFGpnOabfofHI8KI3efKN7Pgn3OvQ1520s0VaP+w0iztIrQSzb6KNWA +UpiZLhg87LtHXFA95vOLHGL8c7zmYlKhEhGql0TD8OznDgEEm45aFIFn4l9+Z7Zn +XiuccpPdAgMBAAECggEAIXon7y2LxsBXHLHIqO8J26wHSYMjoCUheNnKMFSo8IEu +oDivku9EnFWJ8jO9nERS0KiRWMDpdeSxXoQ2EdtSc+B1LjnK5IgIhmcam2Wt7+Zs +JhXfZ013cWo/CBo0QOWluPIaRhNVo2Ftu1cUKn74g+D1qLCWTr61oJyOgDar0Lrn +Cf6DvZl5pGJVV3lQUiYNjZR5qEywpaMvVcVjRUaUElNplld9VLm9xE8lhwjq0GER +LQKt63p6b34kUKM8dS4cWr7+EutQPEwMLPtM/0HBUpid0qRIj5iZtROo86g8ksS1 +vzGQJ5asfgiRdZX7tkrj31RDg4latXQ8saHfZeOLzwKBgQDu0jI25NKHpxbWOveS +OBtZK12aiO4n/nria3YsOTGCWYqtFUsZqcfj7/C30KH0vKODNoy9933+5roEdOvc +FIo6PDUADiZNGN3Oy69EhBVuGr4OkK2LjjCSSI2jvWONTDN4lQ17nAiiJ0LMT/TX +CBIef0i/QpAOYeuUh8FT1Ph8DwKBgQDWVkIfGLXFGeRORjUBuVv0ceQMR1K8Nz0/ +ldR0nuwoxMReSgoP2OVlSXqiLWAC6zPuXxE8KBo7GkvxEc4+AcwcgPZoAkK0KGBO +BCyS6V45abp56nanAxgGZJ/BNd3MEma9rbraFjC/yF29heGeE0s+gR6uPMW32qJe +U8nBJFj1UwKBgCvxm3HEWwTA9w/GW+WY01duBlQ4G/JZ/gyJj34FrBl7FmxQvbfk +KLbFYLrB9fsNdtze/bi6wIFVvSayyO9/DAw5JdtzvxJyn+W8TuzBjRvsacpOTtCe +Akv4c6+MWrQWMGZgrtFu3ZvQs5bao4epoYPhEea3fcBXvjxfWnBtgKd7AoGAeTT8 +XWN230heEFmpfhkZRCnnwX3P7rn6O+v54h1BBWkIdx29hOquBtI/tFiek+f4TROb +xn4TH1smmOPt0qjniTLwpS6qFAFFPLklj8rCywrcNjd988JPIsZihTt1+wJo8Vi+ +crfbx4iCYjvEs8TLZ0RTWkrpsKfF7DvLuxpX6BsCgYBygdo3EGCuKlymw1V8xrWU +rumFLCqtTxUfTu4hzWpPM6wDBh6BNMOeXw8Z7bE69Ha8rWxGpu0qEgRM2RegNJza +f2j+hZUZbdJsGoClpq3ROBwBJHZ3ZUs+4po3JWQWc0DblaMOZftVIAh7EbMhmVPe +OwMin2eLv4aEzSEtSYK8aQ== +-----END PRIVATE KEY----- diff --git a/app/src/main/assets/client.crt b/app/src/main/assets/client.crt new file mode 100644 index 0000000..f0341d6 --- /dev/null +++ b/app/src/main/assets/client.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDTDCCAjQCFDcqUklN76USvOskALvfIUttDamKMA0GCSqGSIb3DQEBCwUAMGEx +CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n +MQ4wDAYDVQQKDAVNeU9yZzELMAkGA1UECwwCSVQxETAPBgNVBAMMCE15Um9vdENB +MB4XDTI1MTAwNzA2MDE1NFoXDTI2MTAwNzA2MDE1NFowZDELMAkGA1UEBhMCQ04x +EDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU15 +T3JnMQ8wDQYDVQQLDAZDbGllbnQxEDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaJLDgelGrpFYgFKqGUAvopUg709OHZjwT +FyEiBic7NgWWLSne5MFBMVY18nZF9khaJZn6wN//miubkb6hbP7695GERB/wVDTj +nMd9FjDaZvYoZQkxemjCBDpyYd5vfP7f83EkNEnD2VljBbRATfl7tXxcbELDHC9N +ukkFD/3sD8IiSpZyyO1U3Ljv/vuV4UCTadJlKB+R5hNqI7QWOr59bRQD2mykXGtx ++gf44rBXqfglhY2xevuvBHYvRncHARIjOvRUmf0ue3CHPtS6wafWiPoYz6Q92xa4 +xBDOtY9lrOZuwGzAsA0dRN80Uu2YmRUoe5bAnZiMk3iDBlt6dNafAgMBAAEwDQYJ +KoZIhvcNAQELBQADggEBAHV2eD8V79IF/u74mXjQxEalAJ0nA/ooMASbDIx08/wy +S4mwZl08yMFRHwV4nMp6kbRh81cWYtTe8+LW8w1mHUKd/7MzStKT93IADPULFjCi +4ee0wJmQePnWDxOcUUz6zEfh72W2Fy1CrZ99R0ZMF5ifUGUPNfmj0NsfqGgEReOO +ai3yNTEg0zg35+2SnHzodFerKWDeis5gP5EOURfHcKYKE3oEatxwn9//gb/sICoR +99rxHTO4BZB06DIArdxomZ84eanBkHGsJGuPjncR9fyLQcbP1SP0WUcR2p73xAd3 +EMBByvrT1m68g7L0SVqN05EshJoYWONb4gTHd2BCO0A= +-----END CERTIFICATE----- diff --git a/app/src/main/assets/client.csr b/app/src/main/assets/client.csr new file mode 100644 index 0000000..35cf19c --- /dev/null +++ b/app/src/main/assets/client.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICqTCCAZECAQAwZDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAO +BgNVBAcMB0JlaWppbmcxDjAMBgNVBAoMBU15T3JnMQ8wDQYDVQQLDAZDbGllbnQx +EDAOBgNVBAMMB2NsaWVudDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCaJLDgelGrpFYgFKqGUAvopUg709OHZjwTFyEiBic7NgWWLSne5MFBMVY18nZF +9khaJZn6wN//miubkb6hbP7695GERB/wVDTjnMd9FjDaZvYoZQkxemjCBDpyYd5v +fP7f83EkNEnD2VljBbRATfl7tXxcbELDHC9NukkFD/3sD8IiSpZyyO1U3Ljv/vuV +4UCTadJlKB+R5hNqI7QWOr59bRQD2mykXGtx+gf44rBXqfglhY2xevuvBHYvRncH +ARIjOvRUmf0ue3CHPtS6wafWiPoYz6Q92xa4xBDOtY9lrOZuwGzAsA0dRN80Uu2Y +mRUoe5bAnZiMk3iDBlt6dNafAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAjnuL +FPH9DnQFg8yxNrD9XDDXGxrbRQEJqQeMJotLpafDk6Y8JROUrG9+v8p0KfdXn++6 +uZHAqsYMhTej6lK7ECI4RYt1vnmZ6V8Cwj/RaY+wqUVhx3UhDrcrbhjvf4YzZIQZ +ObcFsGc6qHfjKCENQvwPP7aKxyK9/mMGbXBDhT/8wACsYRh4HL/6Rr+Uad9n7q+j +y19s6h+SkkGST9feE4X4dWCx87w5us9QTRIywD4/L8IlzJXGEhZ285QxOokw/R7t +pQCkKksycajW7cu7kIZOsTus9wW2vyBhsfJnzTfZq8Qrqyt53C1d431Z8uD9yLMa +SfxqqgmsSUSLjfw3nA== +-----END CERTIFICATE REQUEST----- diff --git a/app/src/main/assets/client.key b/app/src/main/assets/client.key new file mode 100644 index 0000000..a72b547 --- /dev/null +++ b/app/src/main/assets/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCaJLDgelGrpFYg +FKqGUAvopUg709OHZjwTFyEiBic7NgWWLSne5MFBMVY18nZF9khaJZn6wN//miub +kb6hbP7695GERB/wVDTjnMd9FjDaZvYoZQkxemjCBDpyYd5vfP7f83EkNEnD2Vlj +BbRATfl7tXxcbELDHC9NukkFD/3sD8IiSpZyyO1U3Ljv/vuV4UCTadJlKB+R5hNq +I7QWOr59bRQD2mykXGtx+gf44rBXqfglhY2xevuvBHYvRncHARIjOvRUmf0ue3CH +PtS6wafWiPoYz6Q92xa4xBDOtY9lrOZuwGzAsA0dRN80Uu2YmRUoe5bAnZiMk3iD +Blt6dNafAgMBAAECggEAHzak1qAOX7qEcjSdH1ugPbkSeoL3h6iNK7R4UiJ62UOk +N/fnTap289OgyIXTq3Emz5JjruJVubWndPY7awbeT0XIoscEzK7QkvLRdqQCuoc0 ++5MSHIHUKs2eZEErQNpH5mOumo04Dr+5mRKzoH3pskJa74BAuK/BaHT7ilnlqmJp +xoVDilQC5jiGys4MpCtx2Kt8l2CAlsE84vZff+PGV4O0kDsqqky6uhuJtrTaZs7j +bFJoWcG9L9s/WZtwb32X11epHOmQv6mrzgFyminzkBoCmO7cBNB44eyQJnnNYece +MnFdklKCPuSYc0lgWSepuUkrz8hLKC8+QzQO668UsQKBgQDYfmM9ES087q+H9ooc +l/GZsEdV/iN+Kcm1JkyyypzRQrmVUTyAp9SFRaLiQP3ea6Jy/CUeCC1SWwPLJM9E +EA0jlCYIPFOhsT7I1EM5Qa7DWf1z8bAVUMs1cqZrzOegIbEiKSu+Ym4MMVtvKvON +OqaTtwwNUvyxVHmsuZBUZzzXOQKBgQC2RZLvI4p/Qzbpznsmq4beCSFEACRJTa/F +e58/Xbmfdwo42WU17548k7sN/Jgyg7PVu4+bY5ZAYMibXFtEHW+sBz/4Gf90BZxE +egiKD7gtEw4kxu8BfygJgWyszK4guzTYKx+DJRl2xtvGCNX10SrV1rEfHvYNHXj8 +BekIht0ElwKBgQCb/FSch2fE42Vt3WEdwQy+45hCiV4hZRKEhxf0KrBaxmzY/TNO +r54ceFQoGRPR0lO17Z8AyHt/Pzy4fckpDTeqTvAoNu87LW5DXU0iUAUPlCNeCuII +ObJwzC7EtVqesifiqS9veZQ5DMcIjjX1qDClddolL4oKawdQQFORvODFYQKBgB/l +FM0b3wRd8qH/K7WclkEMP/HyRGc/XN6lvzwLXov0/KjuAbPqdjoLb9QGu2s7eKCR +7ZM3Xfdt+CyXgLDupbfonN0BT54xzSJ+aDgggA4DI5pz5SbR5WOkbivetSmtGJYr +FZyRRV9vdM22hho5u9EnfF8Bv/STj7QqJJkFYG+JAoGBAKx9evvrTS1tgcduIZXm +fFQJ/Y4r2AtUqPs5TE69wdQg+WL35Njd0r3gguK4isur4NV3Yb97JUN95L9E2cY2 +asRkLNGiWwgFtSp6xTA6ltfLbdW+smZgeFkMTu8O6WBUiep6TEkRiR60UnKxDfwO +eNr+BzEVtLNmY43pf3uRjnfm +-----END PRIVATE KEY----- diff --git a/app/src/main/java/com/example/tls/CertificateManager.kt b/app/src/main/java/com/example/tls/CertificateManager.kt new file mode 100644 index 0000000..f7dc4c1 --- /dev/null +++ b/app/src/main/java/com/example/tls/CertificateManager.kt @@ -0,0 +1,203 @@ +package com.example.tls + +import android.content.Context +import android.util.Log +import java.io.InputStream +import java.security.KeyStore +import java.security.PrivateKey +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import javax.net.ssl.KeyManagerFactory +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager +import javax.net.ssl.X509KeyManager +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLSession +import java.security.KeyFactory +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.RSAPrivateCrtKeySpec +import java.util.Base64 +import java.io.ByteArrayInputStream +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import java.io.StringReader + +class CertificateManager(private val context: Context) { + + fun loadCertificateFromAssets(fileName: String): X509Certificate? { + return try { + val inputStream: InputStream = context.assets.open(fileName) + val certificateFactory = CertificateFactory.getInstance("X.509") + certificateFactory.generateCertificate(inputStream) as X509Certificate + } catch (e: Exception) { + Log.e("CertificateManager", "加载证书失败: ${e.message}") + null + } + } + + fun createTrustManagerWithCustomCert(certificate: X509Certificate): X509TrustManager { + return object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) { + // 客户端证书验证 + } + + override fun checkServerTrusted(chain: Array, authType: String) { + // 服务器证书验证 - 这里可以添加自定义验证逻辑 + for (cert in chain) { + if (cert == certificate) { + return // 信任我们的自定义证书 + } + } + // 如果不匹配,可以选择抛出异常或使用默认验证 + } + + override fun getAcceptedIssuers(): Array { + return arrayOf(certificate) + } + } + } + + fun createDefaultTrustManager(): X509TrustManager { + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(null as KeyStore?) + return trustManagerFactory.trustManagers[0] as X509TrustManager + } + + fun createCustomTrustManager(): X509TrustManager { + return object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) { + // 接受所有客户端证书 + } + + override fun checkServerTrusted(chain: Array, authType: String) { + // 接受所有服务器证书 - 仅用于测试 + Log.d("CertificateManager", "接受服务器证书: ${chain[0].subjectDN}") + Log.d("CertificateManager", "证书颁发者: ${chain[0].issuerDN}") + Log.d("CertificateManager", "证书有效期: ${chain[0].notBefore} 到 ${chain[0].notAfter}") + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + } + } + + fun loadPrivateKeyFromAssets(fileName: String): PrivateKey? { + return try { + val inputStream: InputStream = context.assets.open(fileName) + val keyContent = inputStream.readBytes().toString(Charsets.UTF_8) + + // 首先尝试简单的Base64解码(适用于PKCS#8格式) + if (keyContent.contains("-----BEGIN PRIVATE KEY-----")) { + Log.d("CertificateManager", "检测到PKCS#8格式私钥,使用标准解析") + val keyBytes = extractKeyBytes(keyContent, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----") + val keySpec = PKCS8EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("RSA") + keyFactory.generatePrivate(keySpec) + } else if (keyContent.contains("-----BEGIN RSA PRIVATE KEY-----")) { + Log.d("CertificateManager", "检测到PKCS#1格式私钥,建议转换为PKCS#8格式") + Log.e("CertificateManager", "PKCS#1格式私钥需要转换,请运行转换脚本") + Log.e("CertificateManager", "Linux/Mac: ./convert_private_key.sh") + Log.e("CertificateManager", "Windows: convert_private_key.bat") + null + } else { + Log.e("CertificateManager", "无法识别私钥格式") + Log.e("CertificateManager", "请确保私钥是PEM格式(PKCS#8或PKCS#1)") + null + } + } catch (e: Exception) { + Log.e("CertificateManager", "加载私钥失败: ${e.message}") + Log.e("CertificateManager", "错误类型: ${e.javaClass.simpleName}") + + // 提供更详细的错误信息 + when { + e.message?.contains("parsing") == true -> { + Log.e("CertificateManager", "私钥格式错误,请检查PEM格式是否正确") + } + e.message?.contains("encrypted") == true -> { + Log.e("CertificateManager", "私钥已加密,请使用未加密的私钥") + } + e.message?.contains("PKCS8") == true -> { + Log.e("CertificateManager", "请使用PKCS#8格式的私钥") + } + else -> { + Log.e("CertificateManager", "请确保私钥是PEM格式且未加密") + } + } + null + } + } + + private fun extractKeyBytes(keyContent: String, beginMarker: String, endMarker: String): ByteArray { + val lines = keyContent.split("\n") + val keyLines = lines.filter { + !it.startsWith("-----") && it.trim().isNotEmpty() + } + val keyString = keyLines.joinToString("") + return Base64.getDecoder().decode(keyString) + } + + fun createKeyManager(certificate: X509Certificate, privateKey: PrivateKey): X509KeyManager { + return object : X509KeyManager { + override fun chooseClientAlias(keyType: Array, issuers: Array, socket: java.net.Socket): String { + return "client" + } + + override fun chooseServerAlias(keyType: String, issuers: Array, socket: java.net.Socket): String? { + return null + } + + override fun getCertificateChain(alias: String): Array { + return arrayOf(certificate) + } + + override fun getClientAliases(keyType: String, issuers: Array): Array { + return arrayOf("client") + } + + override fun getServerAliases(keyType: String, issuers: Array): Array { + return arrayOf() + } + + override fun getPrivateKey(alias: String): PrivateKey { + return privateKey + } + } + } + + fun createKeyManagerFactory(): KeyManagerFactory? { + return try { + val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) + keyManagerFactory.init(null, null) + keyManagerFactory + } catch (e: Exception) { + Log.e("CertificateManager", "创建KeyManagerFactory失败: ${e.message}") + null + } + } + + fun createCustomHostnameVerifier(): HostnameVerifier { + return HostnameVerifier { hostname, session -> + Log.d("CertificateManager", "验证主机名: $hostname") + Log.d("CertificateManager", "SSL会话: ${session.protocol}") + + // 对于IP地址,跳过主机名验证 + if (hostname.matches(Regex("^\\d+\\.\\d+\\.\\d+\\.\\d+$"))) { + Log.d("CertificateManager", "IP地址主机名验证通过: $hostname") + return@HostnameVerifier true + } + + // 对于域名,进行标准验证 + try { + val result = javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session) + Log.d("CertificateManager", "域名验证结果: $result") + result + } catch (e: Exception) { + Log.w("CertificateManager", "主机名验证异常: ${e.message}") + // 对于测试环境,允许所有主机名 + true + } + } + } +} diff --git a/app/src/main/java/com/example/tls/MainActivity.kt b/app/src/main/java/com/example/tls/MainActivity.kt new file mode 100644 index 0000000..19e1e79 --- /dev/null +++ b/app/src/main/java/com/example/tls/MainActivity.kt @@ -0,0 +1,338 @@ +package com.example.tls + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.tls.ui.theme.TlsTheme +import kotlinx.coroutines.launch + +class MainActivity : ComponentActivity() { + private lateinit var tlsTestService: TlsTestService + // 可配置的测试服务器列表 + private val testServers = listOf( + // "https://httpbin.org/post", + // "https://www.google.com", + // "https://www.cloudflare.com", + // "https://www.github.com", + "https://43.162.113.116:7271", // 示例IP+端口 + // "https://10.0.0.1:443", // 示例内网IP + // "https://localhost:8080" // 示例本地服务器 + ) + private var currentServerIndex = 0 + private var customServerUrl by mutableStateOf("") + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + tlsTestService = TlsTestService(this) + setContent { + TlsTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + TlsTestScreen( + modifier = Modifier.padding(innerPadding), + tlsTestService = tlsTestService, + serverUrl = if (customServerUrl.isNotEmpty()) customServerUrl else testServers[currentServerIndex], + testServers = testServers, + customServerUrl = customServerUrl, + onServerChange = { index -> + currentServerIndex = index + customServerUrl = "" + }, + onCustomServerChange = { url -> customServerUrl = url } + ) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TlsTestScreen( + modifier: Modifier = Modifier, + tlsTestService: TlsTestService, + serverUrl: String, + testServers: List = emptyList(), + customServerUrl: String = "", + onServerChange: (Int) -> Unit = {}, + onCustomServerChange: (String) -> Unit = {} +) { + var logs by remember { mutableStateOf(listOf()) } + var isTestRunning by remember { mutableStateOf(false) } + var tls12Result by remember { mutableStateOf(null) } + var tls13Result by remember { mutableStateOf(null) } + + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + + // 自动滚动到最新日志 + LaunchedEffect(logs.size) { + if (logs.isNotEmpty()) { + listState.animateScrollToItem(logs.size - 1) + } + } + + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + ) { + // 标题 + Text( + text = "TLS 1.2/1.3 循环测试", + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + + // 服务器选择 + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF0F0F0)) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "测试服务器", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + // 自定义服务器输入 + OutlinedTextField( + value = customServerUrl, + onValueChange = onCustomServerChange, + label = { Text("自定义服务器 (IP:端口)") }, + placeholder = { Text("例如: https://192.168.1.100:8443") }, + keyboardOptions = androidx.compose.foundation.text.KeyboardOptions( + keyboardType = KeyboardType.Uri + ), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + singleLine = true + ) + + // 预设服务器列表 + if (testServers.isNotEmpty()) { + Text( + text = "预设服务器:", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + modifier = Modifier.padding(bottom = 4.dp) + ) + + testServers.forEachIndexed { index, server -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = customServerUrl.isEmpty() && server == serverUrl, + onClick = { onServerChange(index) } + ) + Text( + text = server, + modifier = Modifier.padding(start = 8.dp), + fontSize = 12.sp + ) + } + } + } + + // 当前选择的服务器显示 + if (serverUrl.isNotEmpty()) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFE3F2FD)) + ) { + Text( + text = "当前服务器: $serverUrl", + modifier = Modifier.padding(12.dp), + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFF1976D2) + ) + } + } + } + } + + // 控制按钮 + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { + if (!isTestRunning) { + isTestRunning = true + logs = listOf("开始TLS测试...") + tls12Result = null + tls13Result = null + + scope.launch { + tlsTestService.startContinuousTest( + serverUrl = serverUrl, + onLog = { log -> + logs = logs + log + }, + onTestComplete = { tls12, tls13 -> + tls12Result = tls12 + tls13Result = tls13 + } + ) + } + } + }, + enabled = !isTestRunning, + modifier = Modifier.weight(1f) + ) { + Text("开始测试") + } + + Button( + onClick = { + isTestRunning = false + logs = logs + "测试已停止" + }, + enabled = isTestRunning, + modifier = Modifier.weight(1f) + ) { + Text("停止测试") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 测试结果显示 + if (tls12Result != null || tls13Result != null) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF5F5F5)) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "最新测试结果", + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + modifier = Modifier.padding(bottom = 8.dp) + ) + + tls12Result?.let { result -> + TestResultItem("TLS 1.2", result) + } + + tls13Result?.let { result -> + TestResultItem("TLS 1.3", result) + } + } + } + } + + // 日志显示区域 + Card( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + colors = CardDefaults.cardColors(containerColor = Color.Black) + ) { + Column { + Text( + text = "测试日志", + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(16.dp) + ) + + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(logs) { log -> + Text( + text = log, + color = Color.Green, + fontSize = 12.sp, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace + ) + } + } + } + } + } +} + +@Composable +fun TestResultItem(protocol: String, result: TestResult) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = protocol, + fontWeight = FontWeight.Medium, + modifier = Modifier.weight(1f) + ) + + when (result) { + is TestResult.Success -> { + Text( + text = "成功 - ${result.duration}ms", + color = Color.Green, + fontWeight = FontWeight.Medium + ) + } + is TestResult.Failure -> { + Text( + text = "失败 - ${result.error}", + color = Color.Red, + fontWeight = FontWeight.Medium + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun TlsTestScreenPreview() { + TlsTheme { + // Preview中不显示实际功能 + Text("TLS测试应用预览") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tls/TlsTestService.kt b/app/src/main/java/com/example/tls/TlsTestService.kt new file mode 100644 index 0000000..b47c3fe --- /dev/null +++ b/app/src/main/java/com/example/tls/TlsTestService.kt @@ -0,0 +1,279 @@ +package com.example.tls + +import android.content.Context +import android.util.Log +import kotlinx.coroutines.* +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException +import java.security.cert.X509Certificate +import javax.net.ssl.* +import java.util.concurrent.TimeUnit + +class TlsTestService(private val context: Context) { + private val certificateManager = CertificateManager(context) + + // 尝试加载客户端证书 + private val clientCertificate = certificateManager.loadCertificateFromAssets("client.crt") + private val clientPrivateKey = certificateManager.loadPrivateKeyFromAssets("client.key") + private val hasClientCert = clientCertificate != null && clientPrivateKey != null + + init { + // 在初始化时记录证书加载状态 + if (hasClientCert) { + Log.d("TlsTestService", "客户端证书加载成功") + } else { + Log.w("TlsTestService", "客户端证书加载失败或未找到") + if (clientCertificate == null) Log.w("TlsTestService", "证书文件 client.crt 未找到或格式错误") + if (clientPrivateKey == null) Log.w("TlsTestService", "私钥文件 client.key 未找到或格式错误") + } + } + + // 为每次测试创建新的客户端,避免SSL会话重用问题 + private fun createTls12Client(): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .sslSocketFactory( + createTls12SocketFactory(), + certificateManager.createCustomTrustManager() + ) + .hostnameVerifier(certificateManager.createCustomHostnameVerifier()) + .connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) // 禁用连接池 + .build() + } + + private fun createTls13Client(): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .sslSocketFactory( + createTls13SocketFactory(), + certificateManager.createCustomTrustManager() + ) + .hostnameVerifier(certificateManager.createCustomHostnameVerifier()) + .connectionPool(okhttp3.ConnectionPool(0, 1, TimeUnit.MILLISECONDS)) // 禁用连接池 + .build() + } + + private fun createTls12SocketFactory(): SSLSocketFactory { + val sslContext = SSLContext.getInstance("TLSv1.2") + val keyManagers = if (hasClientCert) { + arrayOf(certificateManager.createKeyManager(clientCertificate!!, clientPrivateKey!!)) + } else { + null + } + sslContext.init(keyManagers, arrayOf(certificateManager.createCustomTrustManager()), null) + return sslContext.socketFactory + } + + private fun createTls13SocketFactory(): SSLSocketFactory { + val sslContext = SSLContext.getInstance("TLSv1.3") + val keyManagers = if (hasClientCert) { + arrayOf(certificateManager.createKeyManager(clientCertificate!!, clientPrivateKey!!)) + } else { + null + } + sslContext.init(keyManagers, arrayOf(certificateManager.createCustomTrustManager()), null) + return sslContext.socketFactory + } + + suspend fun testTls12(serverUrl: String, onLog: (String) -> Unit): TestResult { + return withContext(Dispatchers.IO) { + var response: okhttp3.Response? = null + try { + onLog("开始TLS 1.2测试...") + onLog("目标服务器: $serverUrl") + onLog("客户端证书状态: ${if (hasClientCert) "已加载" else "未找到证书文件"}") + + val testData = createTestData() + onLog("准备发送数据: ${testData.length} 字节") + logTestDataInfo(testData, onLog) + + val startTime = System.currentTimeMillis() + + val request = Request.Builder() + .url(serverUrl) + .post(testData.toRequestBody("text/plain".toMediaType())) + .addHeader("Content-Type", "text/plain") + .addHeader("User-Agent", "TLS-Test-Client/1.0") + .build() + + onLog("正在建立TLS 1.2连接...") + onLog("目标主机: ${java.net.URL(serverUrl).host}") + val tls12Client = createTls12Client() // 创建新的客户端实例 + response = tls12Client.newCall(request).execute() + val endTime = System.currentTimeMillis() + val duration = endTime - startTime + + if (response.isSuccessful) { + val responseBody = response.body?.string() ?: "" + onLog("TLS 1.2连接成功 - 耗时: ${duration}ms") + onLog("已发送数据: ${testData.length} 字节") + onLog("服务器响应长度: ${responseBody.length} 字节") + onLog("TLS版本: ${response.protocol}") + onLog("HTTP状态: ${response.code}") + logResponseInfo(responseBody, onLog) + TestResult.Success(duration, responseBody.length) + } else { + onLog("TLS 1.2测试失败 - HTTP ${response.code}") + onLog("响应头: ${response.headers}") + TestResult.Failure("HTTP ${response.code}") + } + } catch (e: Exception) { + onLog("TLS 1.2测试异常: ${e.message}") + onLog("异常类型: ${e.javaClass.simpleName}") + TestResult.Failure(e.message ?: "未知错误") + } finally { + // 确保连接被正确关闭 + try { + response?.close() + } catch (e: Exception) { + Log.d("TlsTestService", "关闭连接时发生异常: ${e.message}") + } + } + } + } + + suspend fun testTls13(serverUrl: String, onLog: (String) -> Unit): TestResult { + return withContext(Dispatchers.IO) { + var response: okhttp3.Response? = null + try { + onLog("开始TLS 1.3测试...") + onLog("目标服务器: $serverUrl") + onLog("客户端证书状态: ${if (hasClientCert) "已加载" else "未找到证书文件"}") + + val testData = createTestData() + onLog("准备发送数据: ${testData.length} 字节") + logTestDataInfo(testData, onLog) + + val startTime = System.currentTimeMillis() + + val request = Request.Builder() + .url(serverUrl) + .post(testData.toRequestBody("text/plain".toMediaType())) + .addHeader("Content-Type", "text/plain") + .addHeader("User-Agent", "TLS-Test-Client/1.0") + .build() + + onLog("正在建立TLS 1.3连接...") + onLog("目标主机: ${java.net.URL(serverUrl).host}") + val tls13Client = createTls13Client() // 创建新的客户端实例 + response = tls13Client.newCall(request).execute() + val endTime = System.currentTimeMillis() + val duration = endTime - startTime + + if (response.isSuccessful) { + val responseBody = response.body?.string() ?: "" + onLog("TLS 1.3连接成功 - 耗时: ${duration}ms") + onLog("已发送数据: ${testData.length} 字节") + onLog("服务器响应长度: ${responseBody.length} 字节") + onLog("TLS版本: ${response.protocol}") + onLog("HTTP状态: ${response.code}") + logResponseInfo(responseBody, onLog) + TestResult.Success(duration, responseBody.length) + } else { + onLog("TLS 1.3测试失败 - HTTP ${response.code}") + onLog("响应头: ${response.headers}") + TestResult.Failure("HTTP ${response.code}") + } + } catch (e: Exception) { + onLog("TLS 1.3测试异常: ${e.message}") + onLog("异常类型: ${e.javaClass.simpleName}") + TestResult.Failure(e.message ?: "未知错误") + } finally { + // 确保连接被正确关闭 + try { + response?.close() + } catch (e: Exception) { + Log.d("TlsTestService", "关闭连接时发生异常: ${e.message}") + } + } + } + } + + private fun create1KDataRequestBody(): RequestBody { + val data = "A".repeat(1024) // 创建1KB的数据 + return data.toRequestBody("text/plain".toMediaType()) + } + + private fun createTestData(): String { + // 创建包含时间戳的测试数据,确保每次发送的数据都不同 + val timestamp = System.currentTimeMillis() + val testData = "TLS_TEST_DATA_${timestamp}_" + val remainingSize = 1024 - testData.length + val padding = "A".repeat(remainingSize.coerceAtLeast(0)) + return testData + padding + } + + private fun logTestDataInfo(testData: String, onLog: (String) -> Unit) { + onLog("测试数据详情:") + onLog(" - 数据长度: ${testData.length} 字节") + onLog(" - 数据前缀: ${testData.take(50)}...") + onLog(" - 数据后缀: ...${testData.takeLast(20)}") + } + + private fun logResponseInfo(responseBody: String, onLog: (String) -> Unit) { + onLog("服务器响应详情:") + onLog(" - 响应长度: ${responseBody.length} 字节") + if (responseBody.isNotEmpty()) { + onLog(" - 响应前缀: ${responseBody.take(100)}...") + if (responseBody.length > 100) { + onLog(" - 响应后缀: ...${responseBody.takeLast(50)}") + } + } + } + + private fun clearSSLSession() { + try { + // 清理SSL会话缓存 + val sslContext = SSLContext.getDefault() + val sessionContext = sslContext.clientSessionContext + sessionContext.setSessionCacheSize(0) + sessionContext.setSessionTimeout(0) + Log.d("TlsTestService", "SSL会话缓存已清理") + } catch (e: Exception) { + Log.d("TlsTestService", "清理SSL会话时发生异常: ${e.message}") + } + } + + fun startContinuousTest( + serverUrl: String, + onLog: (String) -> Unit, + onTestComplete: (TestResult, TestResult) -> Unit + ) { + CoroutineScope(Dispatchers.Main).launch { + while (true) { + try { + onLog("=== 开始新一轮测试 ===") + + // 测试TLS 1.2 + val tls12Result = testTls12(serverUrl, onLog) + + // 测试TLS 1.3 + val tls13Result = testTls13(serverUrl, onLog) + + onTestComplete(tls12Result, tls13Result) + + // 清理SSL会话缓存,避免会话重用问题 + clearSSLSession() + + onLog("等待3秒后进行下一轮测试...") + delay(3000) + + } catch (e: Exception) { + onLog("测试过程中发生异常: ${e.message}") + delay(3000) + } + } + } + } +} + +sealed class TestResult { + data class Success(val duration: Long, val responseSize: Int) : TestResult() + data class Failure(val error: String) : TestResult() +} diff --git a/app/src/main/java/com/example/tls/ui/theme/Color.kt b/app/src/main/java/com/example/tls/ui/theme/Color.kt new file mode 100644 index 0000000..0d16006 --- /dev/null +++ b/app/src/main/java/com/example/tls/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.tls.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/example/tls/ui/theme/Theme.kt b/app/src/main/java/com/example/tls/ui/theme/Theme.kt new file mode 100644 index 0000000..50e33b7 --- /dev/null +++ b/app/src/main/java/com/example/tls/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.example.tls.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun TlsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tls/ui/theme/Type.kt b/app/src/main/java/com/example/tls/ui/theme/Type.kt new file mode 100644 index 0000000..00171b8 --- /dev/null +++ b/app/src/main/java/com/example/tls/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.tls.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..747d6a0 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + tls + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..547bf79 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +