first commit

This commit is contained in:
2025-10-10 00:16:47 +08:00
commit 7b8d189566
55 changed files with 1969 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

66
app/build.gradle.kts Normal file
View File

@@ -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)
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Tls">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Tls">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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
```
如果没有证书文件,应用将尝试不使用客户端证书进行连接,但可能被服务器拒绝。

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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-----

View File

@@ -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<X509Certificate>, authType: String) {
// 客户端证书验证
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
// 服务器证书验证 - 这里可以添加自定义验证逻辑
for (cert in chain) {
if (cert == certificate) {
return // 信任我们的自定义证书
}
}
// 如果不匹配,可以选择抛出异常或使用默认验证
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
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<X509Certificate>, authType: String) {
// 接受所有客户端证书
}
override fun checkServerTrusted(chain: Array<X509Certificate>, 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<X509Certificate> {
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<String>, issuers: Array<java.security.Principal>, socket: java.net.Socket): String {
return "client"
}
override fun chooseServerAlias(keyType: String, issuers: Array<java.security.Principal>, socket: java.net.Socket): String? {
return null
}
override fun getCertificateChain(alias: String): Array<X509Certificate> {
return arrayOf(certificate)
}
override fun getClientAliases(keyType: String, issuers: Array<java.security.Principal>): Array<String> {
return arrayOf("client")
}
override fun getServerAliases(keyType: String, issuers: Array<java.security.Principal>): Array<String> {
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
}
}
}
}

View File

@@ -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<String> = emptyList(),
customServerUrl: String = "",
onServerChange: (Int) -> Unit = {},
onCustomServerChange: (String) -> Unit = {}
) {
var logs by remember { mutableStateOf(listOf<String>()) }
var isTestRunning by remember { mutableStateOf(false) }
var tls12Result by remember { mutableStateOf<TestResult?>(null) }
var tls13Result by remember { mutableStateOf<TestResult?>(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测试应用预览")
}
}

View File

@@ -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()
}

View File

@@ -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)

View File

@@ -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
)
}

View File

@@ -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
)
*/
)

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">tls</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Tls" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.example.tls
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}