[feat] Optional self signed certificate

This commit is contained in:
acite
2025-09-10 23:51:13 +08:00
parent b48f8ce6b0
commit 10f316cb48
12 changed files with 246 additions and 111 deletions

View File

@@ -3,7 +3,9 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "1.9.0"
id("kotlin-kapt")
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
}
android {
@@ -31,11 +33,11 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "21"
}
buildFeatures {
compose = true
@@ -43,9 +45,14 @@ android {
}
dependencies {
implementation(libs.hilt.android)
implementation(libs.hilt.navigation.compose)
ksp(libs.hilt.android.compiler)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
kapt("androidx.room:room-compiler:2.7.2")
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.datastore.preferences)
implementation(libs.bcprov.jdk15on)
implementation(libs.converter.gson)

View File

@@ -5,9 +5,11 @@ import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.HiltAndroidApp
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "configure")
@HiltAndroidApp
class AetherApp : Application() {
override fun onCreate() {
super.onCreate()

View File

@@ -55,10 +55,17 @@ import com.acitelight.aether.view.HomeScreen
import com.acitelight.aether.view.MeScreen
import com.acitelight.aether.view.VideoPlayer
import com.acitelight.aether.view.VideoScreen
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.attributes = window.attributes.apply {
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
}

View File

@@ -1,30 +1,27 @@
package com.acitelight.aether.service
import android.content.Context
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import okhttp3.CertificatePinner
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.ByteArrayInputStream
import java.net.HttpURLConnection
import java.net.InetAddress
import java.net.URL
import java.security.KeyStore
import java.security.cert.Certificate
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import okhttp3.EventListener
import java.net.InetAddress
import android.util.Log
import okhttp3.ConnectionSpec
object ApiClient {
var base: String = ""
@@ -34,6 +31,14 @@ object ApiClient {
ignoreUnknownKeys = true
}
private val dnsEventListener = object : EventListener() {
override fun dnsEnd(call: okhttp3.Call, domainName: String, inetAddressList: List<InetAddress>) {
super.dnsEnd(call, domainName, inetAddressList)
val ipAddresses = inetAddressList.joinToString(", ") { it.hostAddress }
Log.d("OkHttp_DNS", "Domain '$domainName' resolved to IPs: [$ipAddresses]")
}
}
fun loadCertificateFromString(pemString: String): X509Certificate {
val certificateFactory = CertificateFactory.getInstance("X.509")
val decodedPem = pemString
@@ -108,6 +113,7 @@ object ApiClient {
}
return OkHttpClient.Builder()
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.sslSocketFactory(sslContext.socketFactory, combinedTm)
.build()
@@ -116,13 +122,20 @@ object ApiClient {
}
}
fun createOkHttp(cert: String?): OkHttpClient {
val trustedCert = cert?.let { loadCertificateFromString(it) }
return createOkHttpClientWithDynamicCert(trustedCert)
fun createOkHttp(): OkHttpClient {
return if (cert == "")
OkHttpClient
.Builder()
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
.eventListener(dnsEventListener)
.build()
else
createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
}
private fun createRetrofit(): Retrofit {
val okHttpClient = createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
val okHttpClient = createOkHttp()
return Retrofit.Builder()
.baseUrl(base)

View File

@@ -1,10 +1,21 @@
package com.acitelight.aether.service
import android.util.Base64
import android.util.Log
import com.acitelight.aether.model.ChallengeResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.Call
import okhttp3.EventListener
import okhttp3.Handshake
import okhttp3.OkHttpClient
import okhttp3.Request
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
import org.bouncycastle.crypto.signers.Ed25519Signer
import java.io.IOException
import java.lang.reflect.Proxy
import java.net.InetSocketAddress
import java.security.PrivateKey
import java.security.Signature
@@ -17,13 +28,13 @@ object AuthManager {
challengeBase64 = api!!.getChallenge(username).string()
}catch (e: Exception)
{
print(e.message)
return null
}
val signedBase64 = signChallenge(db64(privateKey), db64(challengeBase64))
return try {
api!!.verifyChallenge(username, ChallengeResponse(response = signedBase64)).string()
api.verifyChallenge(username, ChallengeResponse(response = signedBase64)).string()
} catch (e: Exception) {
e.printStackTrace()
null

View File

@@ -0,0 +1,85 @@
package com.acitelight.aether.service
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
@Singleton
class SettingsDataStoreManager @Inject constructor(
@ApplicationContext private val context: Context
) {
companion object {
val USER_NAME_KEY = stringPreferencesKey("user_name")
val PRIVATE_KEY = stringPreferencesKey("private_key")
val URL_KEY = stringPreferencesKey("url")
val CERT_KEY = stringPreferencesKey("cert")
val USE_SELF_SIGNED_KEY = booleanPreferencesKey("use_self_signed")
}
val userNameFlow: Flow<String> = context.dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
val privateKeyFlow: Flow<String> = context.dataStore.data.map { preferences ->
preferences[PRIVATE_KEY] ?: ""
}
val urlFlow: Flow<String> = context.dataStore.data.map { preferences ->
preferences[URL_KEY] ?: ""
}
val certFlow: Flow<String> = context.dataStore.data.map { preferences ->
preferences[CERT_KEY] ?: ""
}
val useSelfSignedFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
preferences[USE_SELF_SIGNED_KEY] ?: false
}
suspend fun saveUserName(name: String) {
context.dataStore.edit { preferences ->
preferences[USER_NAME_KEY] = name
}
}
suspend fun savePrivateKey(key: String) {
context.dataStore.edit { preferences ->
preferences[PRIVATE_KEY] = key
}
}
suspend fun saveUrl(url: String) {
context.dataStore.edit { preferences ->
preferences[URL_KEY] = url
}
}
suspend fun saveCert(cert: String) {
context.dataStore.edit { preferences ->
preferences[CERT_KEY] = cert
}
}
suspend fun saveUseSelfSigned(useSelfSigned: Boolean) {
context.dataStore.edit { preferences ->
preferences[USE_SELF_SIGNED_KEY] = useSelfSigned
}
}
suspend fun clearAll() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}

View File

@@ -22,6 +22,7 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.acitelight.aether.Global
@@ -30,7 +31,7 @@ import com.acitelight.aether.service.RecentManager
import com.acitelight.aether.viewModel.HomeScreenViewModel
@Composable
fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = viewModel(), navController: NavController)
fun HomeScreen(homeScreenViewModel: HomeScreenViewModel = hiltViewModel(), navController: NavController)
{
if(Global.loggedIn)
homeScreenViewModel.Init()

View File

@@ -3,11 +3,13 @@ package com.acitelight.aether.view
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -17,6 +19,7 @@ import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
@@ -32,17 +35,20 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.acitelight.aether.viewModel.MeScreenViewModel
@Composable
fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel()) {
fun MeScreen(meScreenViewModel: MeScreenViewModel = hiltViewModel()) {
val context = LocalContext.current
var username by meScreenViewModel.username;
var privateKey by meScreenViewModel.privateKey;
var url by meScreenViewModel.url
var cert by meScreenViewModel.cert
val uss by meScreenViewModel.uss.collectAsState(initial = false)
LazyColumn(
modifier = Modifier
.fillMaxSize()
@@ -134,6 +140,8 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel()) {
.align(Alignment.Start)
)
Spacer(modifier = Modifier.width(8.dp))
// Username input field
OutlinedTextField(
value = url,
@@ -146,21 +154,37 @@ fun MeScreen(meScreenViewModel: MeScreenViewModel = viewModel()) {
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(4.dp))
// Private key input field
OutlinedTextField(
value = cert,
onValueChange = { cert = it },
label = { Text("Cert") },
singleLine = false,
maxLines = 40,
minLines = 20,
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 8.sp
Row(Modifier.align(Alignment.Start)) {
Checkbox(
checked = uss,
onCheckedChange = { isChecked ->
meScreenViewModel.onUseSelfSignedCheckedChange(isChecked)
},
modifier = Modifier.align(Alignment.CenterVertically)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Use Self-Signed Cert",
modifier = Modifier.align(Alignment.CenterVertically)
)
}
Spacer(modifier = Modifier.height(4.dp))
// Private key input field
if (uss)
OutlinedTextField(
value = cert,
onValueChange = { cert = it },
label = { Text("Cert") },
singleLine = false,
maxLines = 40,
minLines = 20,
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle(
fontSize = 8.sp
)
)
)
Spacer(modifier = Modifier.height(24.dp))

View File

@@ -31,33 +31,19 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.acitelight.aether.service.*
import dagger.hilt.android.lifecycle.HiltViewModel
class HomeScreenViewModel(application: Application) : AndroidViewModel(application)
@HiltViewModel
class HomeScreenViewModel @Inject constructor(
private val settingsDataStoreManager: SettingsDataStoreManager
) : ViewModel()
{
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val URL_KEY = stringPreferencesKey("url")
private val CERT_KEY = stringPreferencesKey("cert")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[PRIVATE_KEY] ?: ""
}
val urlFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[URL_KEY] ?: ""
}
val certFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[CERT_KEY] ?: ""
}
var _init = false
var imageLoader: ImageLoader? = null;
val uss = settingsDataStoreManager.useSelfSignedFlow
@Composable
fun Init(){
@@ -79,15 +65,15 @@ class HomeScreenViewModel(application: Application) : AndroidViewModel(applicati
init {
viewModelScope.launch {
val u = userNameFlow.first()
val p = privateKeyFlow.first()
val ur = urlFlow.first()
val c = certFlow.first()
val u = settingsDataStoreManager.userNameFlow.first()
val p = settingsDataStoreManager.privateKeyFlow.first()
val ur = settingsDataStoreManager.urlFlow.first()
val c = settingsDataStoreManager.certFlow.first()
if(u=="" || p=="" || ur=="" || c=="") return@launch
if(u=="" || p=="" || ur=="") return@launch
try{
val usedUrl = ApiClient.apply(ur, c)
val usedUrl = ApiClient.apply(ur, if(uss.first()) c else "")
if (MediaManager.token == "null")
MediaManager.token = AuthManager.fetchToken(

View File

@@ -8,6 +8,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.acitelight.aether.Global
import com.acitelight.aether.dataStore
@@ -20,63 +21,52 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.acitelight.aether.service.*
import dagger.hilt.android.lifecycle.HiltViewModel
class MeScreenViewModel(application: Application) : AndroidViewModel(application) {
private val dataStore = application.dataStore
private val USER_NAME_KEY = stringPreferencesKey("user_name")
private val PRIVATE_KEY = stringPreferencesKey("private_key")
private val URL_KEY = stringPreferencesKey("url")
private val CERT_KEY = stringPreferencesKey("cert")
val userNameFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[USER_NAME_KEY] ?: ""
}
val privateKeyFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[PRIVATE_KEY] ?: ""
}
val urlFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[URL_KEY] ?: ""
}
val certFlow: Flow<String> = dataStore.data.map { preferences ->
preferences[CERT_KEY] ?: ""
}
@HiltViewModel
class MeScreenViewModel @Inject constructor(
private val settingsDataStoreManager: SettingsDataStoreManager
) : ViewModel() {
val username = mutableStateOf("");
val privateKey = mutableStateOf("")
val url = mutableStateOf("");
val cert = mutableStateOf("")
val uss = settingsDataStoreManager.useSelfSignedFlow
init {
viewModelScope.launch {
username.value = userNameFlow.first()
privateKey.value = if (privateKeyFlow.first() == "") "" else "******"
url.value = urlFlow.first()
cert.value = certFlow.first()
username.value = settingsDataStoreManager.userNameFlow.first()
privateKey.value = if (settingsDataStoreManager.privateKeyFlow.first() == "") "" else "******"
url.value = settingsDataStoreManager.urlFlow.first()
cert.value = settingsDataStoreManager.certFlow.first()
}
}
fun onUseSelfSignedCheckedChange(isChecked: Boolean) {
viewModelScope.launch {
settingsDataStoreManager.saveUseSelfSigned(isChecked)
}
}
fun updateServer(u: String, c: String, context: Context)
{
viewModelScope.launch {
dataStore.edit { preferences ->
preferences[URL_KEY] = u
preferences[CERT_KEY] = c
}
settingsDataStoreManager.saveUrl(u)
settingsDataStoreManager.saveCert(c)
Global.loggedIn = false
val us = userNameFlow.first()
val u = urlFlow.first()
val c = certFlow.first()
val p = privateKeyFlow.first()
val us = settingsDataStoreManager.userNameFlow.first()
val p = settingsDataStoreManager.privateKeyFlow.first()
if (u == "" || c == "" || p == "" || us == "") return@launch
if (u == "" || p == "" || us == "") return@launch
try {
val usedUrl = ApiClient.apply(u, c)
val usedUrl = ApiClient.apply(u, if(uss.first()) c else "")
MediaManager.token = AuthManager.fetchToken(
us,
p
@@ -93,22 +83,18 @@ class MeScreenViewModel(application: Application) : AndroidViewModel(application
fun updateAccount(u: String, p: String, context: Context) {
viewModelScope.launch {
dataStore.edit { preferences ->
preferences[USER_NAME_KEY] = u
preferences[PRIVATE_KEY] = p
}
settingsDataStoreManager.saveUserName(u)
settingsDataStoreManager.savePrivateKey(p)
privateKey.value = "******"
Global.loggedIn = false
val u = userNameFlow.first()
val p = privateKeyFlow.first()
val u = settingsDataStoreManager.userNameFlow.first()
val p = settingsDataStoreManager.privateKeyFlow.first()
val ur = settingsDataStoreManager.urlFlow.first()
val ur = urlFlow.first()
val c = certFlow.first()
if (u == "" || p == "" || ur == "" || c == "") return@launch
if (u == "" || p == "" || ur == "") return@launch
try {
MediaManager.token = AuthManager.fetchToken(

View File

@@ -3,4 +3,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.ksp) apply false
}

View File

@@ -8,15 +8,15 @@ converterGson = "3.0.0"
datastorePreferences = "1.1.7"
exoplayerplus = "0.2.0"
gson = "2.13.1"
kotlin = "2.2.10"
kotlin = "2.2.20"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
kotlinxSerializationJson = "1.9.0"
lifecycleRuntimeKtx = "2.9.2"
lifecycleRuntimeKtx = "2.9.3"
activityCompose = "1.10.1"
composeBom = "2025.08.00"
composeBom = "2025.08.01"
media3Common = "1.8.0"
media3Exoplayer = "1.8.0"
media3Ui = "1.8.0"
@@ -29,6 +29,10 @@ roomCompiler = "2.7.2"
roomKtx = "2.7.2"
roomRuntime = "2.7.2"
ksp = "2.1.21-2.0.2"
hilt = "2.57.1"
hilt-navigation-compose = "1.2.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
@@ -64,8 +68,14 @@ retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit
retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" }
androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3DatasourceOkhttp" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }