[feat] Abyss 'HELLY' encryption protocol 😈
This commit is contained in:
@@ -38,6 +38,7 @@ android {
|
|||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "21"
|
jvmTarget = "21"
|
||||||
|
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
|||||||
@@ -21,13 +21,22 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
|
||||||
android:theme="@style/Theme.Aether">
|
android:theme="@style/Theme.Aether">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainScreenActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.Aether">
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".AbyssService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
40
app/src/main/java/com/acitelight/aether/AbyssService.kt
Normal file
40
app/src/main/java/com/acitelight/aether/AbyssService.kt
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package com.acitelight.aether
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.IBinder
|
||||||
|
import com.acitelight.aether.service.AbyssTunnelProxy
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class AbyssService : Service() {
|
||||||
|
private val binder = AbyssServiceBinder()
|
||||||
|
private val _isInitialized = MutableStateFlow(false)
|
||||||
|
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.IO + Job())
|
||||||
|
var proxy = AbyssTunnelProxy()
|
||||||
|
|
||||||
|
inner class AbyssServiceBinder : Binder() {
|
||||||
|
fun getService(): AbyssService = this@AbyssService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
|
return binder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
serviceScope.launch {
|
||||||
|
_isInitialized.update { true }
|
||||||
|
proxy.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,51 @@
|
|||||||
package com.acitelight.aether
|
package com.acitelight.aether
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "configure")
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "configure")
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class AetherApp : Application() {
|
class AetherApp : Application() {
|
||||||
|
var abyssService: AbyssService? = null
|
||||||
|
var isServiceBound = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
val isServiceInitialized: StateFlow<Boolean>?
|
||||||
|
get() = abyssService?.isInitialized
|
||||||
|
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
val binder = service as AbyssService.AbyssServiceBinder
|
||||||
|
abyssService = binder.getService()
|
||||||
|
isServiceBound = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
isServiceBound = false
|
||||||
|
abyssService = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
val intent = Intent(this, AbyssService::class.java)
|
||||||
|
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.acitelight.aether
|
package com.acitelight.aether
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.net.http.SslCertificate.saveState
|
import android.net.http.SslCertificate.saveState
|
||||||
@@ -39,6 +40,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
@@ -57,6 +59,9 @@ import com.acitelight.aether.view.VideoPlayer
|
|||||||
import com.acitelight.aether.view.VideoScreen
|
import com.acitelight.aether.view.VideoScreen
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
@@ -66,6 +71,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val app = application as AetherApp
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
app.isServiceInitialized?.filter { it }?.first()
|
||||||
|
val intent = Intent(this@MainActivity, MainScreenActivity::class.java)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MainScreenActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
window.attributes = window.attributes.apply {
|
window.attributes = window.attributes.apply {
|
||||||
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Comic(
|
|||||||
{
|
{
|
||||||
fun getPage(pageNumber: Int): String
|
fun getPage(pageNumber: Int): String
|
||||||
{
|
{
|
||||||
return "${ApiClient.base}api/image/$id/${comic.list[pageNumber]}?token=$token"
|
return "${ApiClient.getBase()}api/image/$id/${comic.list[pageNumber]}?token=$token"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPage(pageName: String): String?
|
fun getPage(pageName: String): String?
|
||||||
|
|||||||
@@ -11,18 +11,18 @@ class Video constructor(
|
|||||||
){
|
){
|
||||||
fun getCover(): String
|
fun getCover(): String
|
||||||
{
|
{
|
||||||
return "${ApiClient.base}api/video/$klass/$id/cover?token=$token"
|
return "${ApiClient.getBase()}api/video/$klass/$id/cover?token=$token"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVideo(): String
|
fun getVideo(): String
|
||||||
{
|
{
|
||||||
return "${ApiClient.base}api/video/$klass/$id/av?token=$token"
|
return "${ApiClient.getBase()}api/video/$klass/$id/av?token=$token"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGallery(): List<KeyImage>
|
fun getGallery(): List<KeyImage>
|
||||||
{
|
{
|
||||||
return video.gallery.map{
|
return video.gallery.map{
|
||||||
KeyImage(url = "${ApiClient.base}api/video/$klass/$id/gallery/$it?token=$token", key = "$klass/$id/gallery/$it")
|
KeyImage(url = "${ApiClient.getBase()}api/video/$klass/$id/gallery/$it?token=$token", key = "$klass/$id/gallery/$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
357
app/src/main/java/com/acitelight/aether/service/AbyssStream.kt
Normal file
357
app/src/main/java/com/acitelight/aether/service/AbyssStream.kt
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
import java.net.Socket
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.ArrayDeque
|
||||||
|
|
||||||
|
import org.bouncycastle.math.ec.rfc7748.X25519
|
||||||
|
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
|
||||||
|
import org.bouncycastle.crypto.digests.SHA256Digest
|
||||||
|
import org.bouncycastle.crypto.params.HKDFParameters
|
||||||
|
import org.bouncycastle.crypto.params.KeyParameter
|
||||||
|
import org.bouncycastle.crypto.params.AEADParameters
|
||||||
|
import org.bouncycastle.crypto.modes.ChaCha20Poly1305
|
||||||
|
import java.io.EOFException
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
|
class AbyssStream private constructor(
|
||||||
|
private val socket: Socket,
|
||||||
|
private val input: InputStream,
|
||||||
|
private val output: OutputStream,
|
||||||
|
private val aeadKey: ByteArray,
|
||||||
|
private val sendSalt: ByteArray,
|
||||||
|
private val recvSalt: ByteArray
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val PUBLIC_KEY_LEN = 32
|
||||||
|
private const val AEAD_KEY_LEN = 32
|
||||||
|
private const val NONCE_SALT_LEN = 4
|
||||||
|
private const val AEAD_TAG_LEN = 16
|
||||||
|
private const val NONCE_LEN = 12
|
||||||
|
private const val MAX_PLAINTEXT_FRAME = 64 * 1024
|
||||||
|
|
||||||
|
private val secureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and perform handshake on an already-connected socket.
|
||||||
|
* If privateKeyRaw is provided, it must be 32 bytes.
|
||||||
|
*/
|
||||||
|
suspend fun create(socket: Socket, privateKeyRaw: ByteArray? = null): AbyssStream = withContext(Dispatchers.IO) {
|
||||||
|
if (!socket.isConnected) throw IllegalArgumentException("socket is not connected")
|
||||||
|
val inStream = socket.getInputStream()
|
||||||
|
val outStream = socket.getOutputStream()
|
||||||
|
|
||||||
|
// 1) keypair (raw)
|
||||||
|
val localPriv = ByteArray(PUBLIC_KEY_LEN)
|
||||||
|
if (privateKeyRaw != null) {
|
||||||
|
if (privateKeyRaw.size != PUBLIC_KEY_LEN) {
|
||||||
|
throw IllegalArgumentException("privateKeyRaw must be $PUBLIC_KEY_LEN bytes")
|
||||||
|
}
|
||||||
|
System.arraycopy(privateKeyRaw, 0, localPriv, 0, PUBLIC_KEY_LEN)
|
||||||
|
} else {
|
||||||
|
X25519.generatePrivateKey(secureRandom, localPriv)
|
||||||
|
}
|
||||||
|
val localPub = ByteArray(PUBLIC_KEY_LEN)
|
||||||
|
X25519.scalarMultBase(localPriv, 0, localPub, 0)
|
||||||
|
|
||||||
|
// 2) exchange raw public keys (exact 32 bytes each) using blocking IO
|
||||||
|
writeExact(outStream, localPub, 0, PUBLIC_KEY_LEN)
|
||||||
|
val remotePub = ByteArray(PUBLIC_KEY_LEN)
|
||||||
|
readExact(inStream, remotePub, 0, PUBLIC_KEY_LEN)
|
||||||
|
|
||||||
|
// 3) compute shared secret: X25519.scalarMult(private, remotePublic)
|
||||||
|
val shared = ByteArray(PUBLIC_KEY_LEN)
|
||||||
|
X25519.scalarMult(localPriv, 0, remotePub, 0, shared, 0)
|
||||||
|
|
||||||
|
// 4) HKDF-SHA256 -> AEAD key + saltA + saltB
|
||||||
|
val hkdf = HKDFBytesGenerator(SHA256Digest())
|
||||||
|
// AEAD key
|
||||||
|
hkdf.init(HKDFParameters(shared, null, "Abyss-AEAD-Key".toByteArray(Charsets.US_ASCII)))
|
||||||
|
val aeadKey = ByteArray(AEAD_KEY_LEN)
|
||||||
|
hkdf.generateBytes(aeadKey, 0, AEAD_KEY_LEN)
|
||||||
|
|
||||||
|
// salt A
|
||||||
|
hkdf.init(HKDFParameters(shared, null, "Abyss-Nonce-Salt-A".toByteArray(Charsets.US_ASCII)))
|
||||||
|
val saltA = ByteArray(NONCE_SALT_LEN)
|
||||||
|
hkdf.generateBytes(saltA, 0, NONCE_SALT_LEN)
|
||||||
|
|
||||||
|
// salt B
|
||||||
|
hkdf.init(HKDFParameters(shared, null, "Abyss-Nonce-Salt-B".toByteArray(Charsets.US_ASCII)))
|
||||||
|
val saltB = ByteArray(NONCE_SALT_LEN)
|
||||||
|
hkdf.generateBytes(saltB, 0, NONCE_SALT_LEN)
|
||||||
|
|
||||||
|
// Deterministic assignment by lexicographic comparison
|
||||||
|
val cmp = lexicographicCompare(localPub, remotePub)
|
||||||
|
val sendSalt: ByteArray
|
||||||
|
val recvSalt: ByteArray
|
||||||
|
if (cmp < 0) {
|
||||||
|
sendSalt = saltA
|
||||||
|
recvSalt = saltB
|
||||||
|
} else if (cmp > 0) {
|
||||||
|
sendSalt = saltB
|
||||||
|
recvSalt = saltA
|
||||||
|
} else {
|
||||||
|
// extremely unlikely
|
||||||
|
sendSalt = saltA
|
||||||
|
recvSalt = saltB
|
||||||
|
}
|
||||||
|
|
||||||
|
// zero sensitive buffers
|
||||||
|
localPriv.fill(0)
|
||||||
|
localPub.fill(0)
|
||||||
|
remotePub.fill(0)
|
||||||
|
shared.fill(0)
|
||||||
|
// keep aeadKey, sendSalt, recvSalt
|
||||||
|
|
||||||
|
return@withContext AbyssStream(socket, inStream, outStream, aeadKey, sendSalt, recvSalt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun lexicographicCompare(a: ByteArray, b: ByteArray): Int {
|
||||||
|
val min = kotlin.math.min(a.size, b.size)
|
||||||
|
for (i in 0 until min) {
|
||||||
|
val av = a[i].toInt() and 0xff
|
||||||
|
val bv = b[i].toInt() and 0xff
|
||||||
|
if (av < bv) return -1
|
||||||
|
if (av > bv) return 1
|
||||||
|
}
|
||||||
|
if (a.size < b.size) return -1
|
||||||
|
if (a.size > b.size) return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readExact(input: InputStream, buffer: ByteArray, offset: Int, count: Int) {
|
||||||
|
var read = 0
|
||||||
|
while (read < count) {
|
||||||
|
val n = input.read(buffer, offset + read, count - read)
|
||||||
|
if (n == -1) {
|
||||||
|
if (read == 0) throw EOFException("Remote closed connection while reading")
|
||||||
|
else throw EOFException("Remote closed connection unexpectedly during read")
|
||||||
|
}
|
||||||
|
read += n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeExact(output: OutputStream, buffer: ByteArray, offset: Int, count: Int) {
|
||||||
|
output.write(buffer, offset, count)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal state
|
||||||
|
private val sendCounter = AtomicLong(0L)
|
||||||
|
private val recvCounter = AtomicLong(0L)
|
||||||
|
private val sendLock = Any()
|
||||||
|
private val aeadLock = Any()
|
||||||
|
|
||||||
|
// leftover read queue
|
||||||
|
private val leftoverQueue = ArrayDeque<ByteArray>()
|
||||||
|
private var currentLeftover: ByteArray? = null
|
||||||
|
private var currentLeftoverOffset = 0
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var closed = false
|
||||||
|
|
||||||
|
// ---- high-level read/write APIs (suspendable) ----
|
||||||
|
|
||||||
|
suspend fun read(buffer: ByteArray, offset: Int, count: Int): Int = withContext(Dispatchers.IO) {
|
||||||
|
if (closed) throw IllegalStateException("AbyssStream closed")
|
||||||
|
if (buffer.size < offset + count) throw IndexOutOfBoundsException()
|
||||||
|
// serve leftover first
|
||||||
|
if (ensureCurrentLeftover()) {
|
||||||
|
val seg = currentLeftover!!
|
||||||
|
val avail = seg.size - currentLeftoverOffset
|
||||||
|
val toCopy = kotlin.math.min(avail, count)
|
||||||
|
System.arraycopy(seg, currentLeftoverOffset, buffer, offset, toCopy)
|
||||||
|
currentLeftoverOffset += toCopy
|
||||||
|
if (currentLeftoverOffset >= seg.size) {
|
||||||
|
currentLeftover = null
|
||||||
|
currentLeftoverOffset = 0
|
||||||
|
}
|
||||||
|
return@withContext toCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
// read one frame and decrypt
|
||||||
|
val plaintext = readOneFrameAndDecrypt()
|
||||||
|
if (plaintext == null || plaintext.isEmpty()) {
|
||||||
|
// EOF
|
||||||
|
return@withContext 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return@withContext if (plaintext.size <= count) {
|
||||||
|
System.arraycopy(plaintext, 0, buffer, offset, plaintext.size)
|
||||||
|
plaintext.size
|
||||||
|
} else {
|
||||||
|
System.arraycopy(plaintext, 0, buffer, offset, count)
|
||||||
|
val leftoverLen = plaintext.size - count
|
||||||
|
val leftover = ByteArray(leftoverLen)
|
||||||
|
System.arraycopy(plaintext, count, leftover, 0, leftoverLen)
|
||||||
|
synchronized(leftoverQueue) { leftoverQueue.addLast(leftover) }
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureCurrentLeftover(): Boolean {
|
||||||
|
if (currentLeftover != null && currentLeftoverOffset < currentLeftover!!.size) return true
|
||||||
|
synchronized(leftoverQueue) {
|
||||||
|
val next = leftoverQueue.pollFirst()
|
||||||
|
if (next != null) {
|
||||||
|
currentLeftover = next
|
||||||
|
currentLeftoverOffset = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readOneFrameAndDecrypt(): ByteArray? {
|
||||||
|
// read 4-byte header
|
||||||
|
val header = ByteArray(4)
|
||||||
|
try {
|
||||||
|
readExact(input, header, 0, 4)
|
||||||
|
} catch (e: EOFException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val payloadLen = ByteBuffer.wrap(header).int and 0xffffffff.toInt()
|
||||||
|
if (payloadLen > MAX_PLAINTEXT_FRAME + AEAD_TAG_LEN) throw IllegalStateException("payload too big")
|
||||||
|
if (payloadLen < AEAD_TAG_LEN) throw IllegalStateException("payload too small")
|
||||||
|
|
||||||
|
val payload = ByteArray(payloadLen)
|
||||||
|
readExact(input, payload, 0, payloadLen)
|
||||||
|
|
||||||
|
val ciphertextLen = payloadLen - AEAD_TAG_LEN
|
||||||
|
val ciphertext = ByteArray(ciphertextLen)
|
||||||
|
val tag = ByteArray(AEAD_TAG_LEN)
|
||||||
|
if (ciphertextLen > 0) System.arraycopy(payload, 0, ciphertext, 0, ciphertextLen)
|
||||||
|
System.arraycopy(payload, ciphertextLen, tag, 0, AEAD_TAG_LEN)
|
||||||
|
|
||||||
|
val remoteCounterValue = recvCounter.getAndIncrement()
|
||||||
|
|
||||||
|
val nonce = ByteArray(NONCE_LEN)
|
||||||
|
System.arraycopy(recvSalt, 0, nonce, 0, NONCE_SALT_LEN)
|
||||||
|
// write 8-byte big-endian counter at nonce[4..11]
|
||||||
|
val bb = ByteBuffer.wrap(nonce, NONCE_SALT_LEN, 8)
|
||||||
|
bb.putLong(remoteCounterValue)
|
||||||
|
|
||||||
|
val plaintext = try {
|
||||||
|
aeadDecrypt(nonce, ciphertext, tag)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
close()
|
||||||
|
throw SecurityException("AEAD authentication failed; connection closed.", ex)
|
||||||
|
} finally {
|
||||||
|
nonce.fill(0)
|
||||||
|
payload.fill(0)
|
||||||
|
ciphertext.fill(0)
|
||||||
|
tag.fill(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun write(buffer: ByteArray, offset: Int, count: Int) = withContext(Dispatchers.IO) {
|
||||||
|
if (closed) throw IllegalStateException("AbyssStream closed")
|
||||||
|
if (buffer.size < offset + count) throw IndexOutOfBoundsException()
|
||||||
|
var remaining = count
|
||||||
|
var idx = offset
|
||||||
|
while (remaining > 0) {
|
||||||
|
val chunk = kotlin.math.min(remaining, MAX_PLAINTEXT_FRAME)
|
||||||
|
val plaintext = buffer.copyOfRange(idx, idx + chunk)
|
||||||
|
sendPlaintextChunk(plaintext)
|
||||||
|
idx += chunk
|
||||||
|
remaining -= chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendPlaintextChunk(plaintext: ByteArray) {
|
||||||
|
if (closed) throw IllegalStateException("AbyssStream closed")
|
||||||
|
|
||||||
|
val ciphertextAndTag: ByteArray
|
||||||
|
val nonce = ByteArray(NONCE_LEN)
|
||||||
|
val counterValue: Long
|
||||||
|
synchronized(sendLock) {
|
||||||
|
counterValue = sendCounter.getAndIncrement()
|
||||||
|
}
|
||||||
|
System.arraycopy(sendSalt, 0, nonce, 0, NONCE_SALT_LEN)
|
||||||
|
val bb = ByteBuffer.wrap(nonce, NONCE_SALT_LEN, 8)
|
||||||
|
bb.putLong(counterValue)
|
||||||
|
|
||||||
|
try {
|
||||||
|
ciphertextAndTag = aeadEncrypt(nonce, plaintext)
|
||||||
|
} finally {
|
||||||
|
nonce.fill(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val payloadLen = ciphertextAndTag.size
|
||||||
|
val header = ByteBuffer.allocate(4).putInt(payloadLen).array()
|
||||||
|
try {
|
||||||
|
synchronized(output) {
|
||||||
|
output.write(header)
|
||||||
|
if (payloadLen > 0) output.write(ciphertextAndTag)
|
||||||
|
output.flush()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// clear sensitive
|
||||||
|
ciphertextAndTag.fill(0)
|
||||||
|
plaintext.fill(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- AEAD helpers using BouncyCastle lightweight API ----
|
||||||
|
// ChaCha20-Poly1305 with 12-byte nonce. BouncyCastle ChaCha20Poly1305 produces ciphertext+tag.
|
||||||
|
|
||||||
|
private fun aeadEncrypt(nonce: ByteArray, plaintext: ByteArray): ByteArray {
|
||||||
|
synchronized(aeadLock) {
|
||||||
|
val cipher = ChaCha20Poly1305()
|
||||||
|
val params = AEADParameters(KeyParameter(aeadKey), AEAD_TAG_LEN * 8, nonce, null)
|
||||||
|
cipher.init(true, params)
|
||||||
|
val outBuf = ByteArray(cipher.getOutputSize(plaintext.size))
|
||||||
|
var len = cipher.processBytes(plaintext, 0, plaintext.size, outBuf, 0)
|
||||||
|
len += cipher.doFinal(outBuf, len)
|
||||||
|
if (len != outBuf.size) return outBuf.copyOf(len)
|
||||||
|
return outBuf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aeadDecrypt(nonce: ByteArray, ciphertext: ByteArray, tag: ByteArray): ByteArray {
|
||||||
|
synchronized(aeadLock) {
|
||||||
|
val cipher = ChaCha20Poly1305()
|
||||||
|
val params = AEADParameters(KeyParameter(aeadKey), AEAD_TAG_LEN * 8, nonce, null)
|
||||||
|
cipher.init(false, params)
|
||||||
|
// input is ciphertext||tag
|
||||||
|
val input = ByteArray(ciphertext.size + tag.size)
|
||||||
|
if (ciphertext.isNotEmpty()) System.arraycopy(ciphertext, 0, input, 0, ciphertext.size)
|
||||||
|
System.arraycopy(tag, 0, input, ciphertext.size, tag.size)
|
||||||
|
val outBuf = ByteArray(cipher.getOutputSize(input.size))
|
||||||
|
var len = cipher.processBytes(input, 0, input.size, outBuf, 0)
|
||||||
|
try {
|
||||||
|
len += cipher.doFinal(outBuf, len)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
// authentication failure or other
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
return if (len != outBuf.size) outBuf.copyOf(len) else outBuf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- utility / lifecycle ----
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
if (!closed) {
|
||||||
|
closed = true
|
||||||
|
try { socket.close() } catch (_: Exception) {}
|
||||||
|
// clear secrets
|
||||||
|
aeadKey.fill(0)
|
||||||
|
sendSalt.fill(0)
|
||||||
|
recvSalt.fill(0)
|
||||||
|
synchronized(leftoverQueue) {
|
||||||
|
leftoverQueue.forEach { it.fill(0) }
|
||||||
|
leftoverQueue.clear()
|
||||||
|
}
|
||||||
|
currentLeftover = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.selects.select
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.ServerSocket
|
||||||
|
import java.net.Socket
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class AbyssTunnelProxy(
|
||||||
|
private val coroutineContext: CoroutineContext = Dispatchers.IO
|
||||||
|
) {
|
||||||
|
private var serverHost: String = ""
|
||||||
|
private var serverPort: Int = 0
|
||||||
|
|
||||||
|
fun config(host: String, port: Int)
|
||||||
|
{
|
||||||
|
serverHost = host
|
||||||
|
serverPort = port
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listenAddress = InetAddress.getLoopbackAddress()
|
||||||
|
private val listenPort = 4095
|
||||||
|
private var serverSocket: ServerSocket? = null
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + coroutineContext)
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
serverSocket = ServerSocket(listenPort, 50, listenAddress)
|
||||||
|
// accept loop
|
||||||
|
scope.launch {
|
||||||
|
val srv = serverSocket ?: return@launch
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
val client = srv.accept()
|
||||||
|
|
||||||
|
if(serverHost.isEmpty())
|
||||||
|
continue
|
||||||
|
|
||||||
|
launch {
|
||||||
|
try { handleLocalConnection(client) }
|
||||||
|
catch (ex: Exception) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
println(ex.message)
|
||||||
|
// server stopped or fatal error
|
||||||
|
} finally {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
try { serverSocket?.close() } catch (_: Exception) {}
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleLocalConnection(localSocket: Socket) = withContext(coroutineContext) {
|
||||||
|
val localIn = localSocket.getInputStream()
|
||||||
|
val localOut = localSocket.getOutputStream()
|
||||||
|
var abyssSocket: Socket? = null
|
||||||
|
var abyssStream: AbyssStream? = null
|
||||||
|
try {
|
||||||
|
abyssSocket = Socket(serverHost, serverPort)
|
||||||
|
abyssStream = AbyssStream.create(abyssSocket)
|
||||||
|
|
||||||
|
// concurrently copy in both directions
|
||||||
|
val job1 = launch { copyExactSuspend(localIn, abyssStream) } // local -> abyss
|
||||||
|
val job2 = launch { copyFromAbyssToLocal(abyssStream, localOut) } // abyss -> local
|
||||||
|
|
||||||
|
// wait for either direction to finish
|
||||||
|
select<Unit> {
|
||||||
|
job1.onJoin { /* completed */ }
|
||||||
|
job2.onJoin { /* completed */ }
|
||||||
|
}
|
||||||
|
// cancel other
|
||||||
|
job1.cancel()
|
||||||
|
job2.cancel()
|
||||||
|
} catch (ex: Exception)
|
||||||
|
{
|
||||||
|
println(ex.message)
|
||||||
|
// log or ignore; we close sockets below
|
||||||
|
} finally {
|
||||||
|
try { localSocket.close() } catch (_: Exception) {}
|
||||||
|
try { abyssStream?.close() } catch (_: Exception) {}
|
||||||
|
try { abyssSocket?.close() } catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy from local InputStream into AbyssStream.write in frames.
|
||||||
|
private suspend fun copyExactSuspend(localIn: InputStream, abyss: AbyssStream) = withContext(coroutineContext) {
|
||||||
|
val buffer = ByteArray(16 * 1024)
|
||||||
|
while (true) {
|
||||||
|
val read = localIn.read(buffer)
|
||||||
|
if (read <= 0)
|
||||||
|
break
|
||||||
|
abyss.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy from AbyssStream (read frames/decrypt) to local OutputStream
|
||||||
|
private suspend fun copyFromAbyssToLocal(abyss: AbyssStream, localOut: OutputStream) = withContext(coroutineContext) {
|
||||||
|
val buffer = ByteArray(16 * 1024)
|
||||||
|
while (true) {
|
||||||
|
val n = abyss.read(buffer, 0, buffer.size)
|
||||||
|
if (n <= 0)
|
||||||
|
break
|
||||||
|
localOut.write(buffer, 0, n)
|
||||||
|
localOut.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
|
|
||||||
package com.acitelight.aether.service
|
package com.acitelight.aether.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.acitelight.aether.AetherApp
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.ConnectionSpec
|
||||||
|
import okhttp3.EventListener
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.cert.CertificateException
|
import java.security.cert.CertificateException
|
||||||
import java.security.cert.CertificateFactory
|
import java.security.cert.CertificateFactory
|
||||||
@@ -18,19 +27,23 @@ import java.security.cert.X509Certificate
|
|||||||
import javax.net.ssl.SSLContext
|
import javax.net.ssl.SSLContext
|
||||||
import javax.net.ssl.TrustManagerFactory
|
import javax.net.ssl.TrustManagerFactory
|
||||||
import javax.net.ssl.X509TrustManager
|
import javax.net.ssl.X509TrustManager
|
||||||
import okhttp3.EventListener
|
|
||||||
import java.net.InetAddress
|
|
||||||
import android.util.Log
|
|
||||||
import okhttp3.ConnectionSpec
|
|
||||||
|
|
||||||
object ApiClient {
|
object ApiClient {
|
||||||
var base: String = ""
|
fun getBase(): String{
|
||||||
|
return replaceAbyssProtocol(base)
|
||||||
|
}
|
||||||
|
private var base: String = ""
|
||||||
var domain: String = ""
|
var domain: String = ""
|
||||||
var cert: String = ""
|
var cert: String = ""
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun replaceAbyssProtocol(uri: String): String {
|
||||||
|
return uri.replaceFirst("^abyss://".toRegex(), "https://")
|
||||||
|
}
|
||||||
|
|
||||||
private val dnsEventListener = object : EventListener() {
|
private val dnsEventListener = object : EventListener() {
|
||||||
override fun dnsEnd(call: okhttp3.Call, domainName: String, inetAddressList: List<InetAddress>) {
|
override fun dnsEnd(call: okhttp3.Call, domainName: String, inetAddressList: List<InetAddress>) {
|
||||||
super.dnsEnd(call, domainName, inetAddressList)
|
super.dnsEnd(call, domainName, inetAddressList)
|
||||||
@@ -112,10 +125,32 @@ object ApiClient {
|
|||||||
init(null, arrayOf(combinedTm), null)
|
init(null, arrayOf(combinedTm), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return OkHttpClient.Builder()
|
return if (base.startsWith("abyss://"))
|
||||||
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
|
OkHttpClient.Builder()
|
||||||
.sslSocketFactory(sslContext.socketFactory, combinedTm)
|
.connectionSpecs(
|
||||||
.build()
|
listOf(
|
||||||
|
ConnectionSpec.MODERN_TLS,
|
||||||
|
ConnectionSpec.COMPATIBLE_TLS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.proxy(
|
||||||
|
Proxy(
|
||||||
|
Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress("127.0.0.1", 4095)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.sslSocketFactory(sslContext.socketFactory, combinedTm)
|
||||||
|
.build()
|
||||||
|
else
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectionSpecs(
|
||||||
|
listOf(
|
||||||
|
ConnectionSpec.MODERN_TLS,
|
||||||
|
ConnectionSpec.COMPATIBLE_TLS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.sslSocketFactory(sslContext.socketFactory, combinedTm)
|
||||||
|
.build()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw RuntimeException("Failed to create OkHttpClient with dynamic certificate", e)
|
throw RuntimeException("Failed to create OkHttpClient with dynamic certificate", e)
|
||||||
@@ -124,11 +159,24 @@ object ApiClient {
|
|||||||
|
|
||||||
fun createOkHttp(): OkHttpClient {
|
fun createOkHttp(): OkHttpClient {
|
||||||
return if (cert == "")
|
return if (cert == "")
|
||||||
OkHttpClient
|
if (base.startsWith("abyss://"))
|
||||||
.Builder()
|
OkHttpClient
|
||||||
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
|
.Builder()
|
||||||
.eventListener(dnsEventListener)
|
.proxy(
|
||||||
.build()
|
Proxy(
|
||||||
|
Proxy.Type.HTTP,
|
||||||
|
InetSocketAddress("::1", 4095)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
|
||||||
|
.eventListener(dnsEventListener)
|
||||||
|
.build()
|
||||||
|
else
|
||||||
|
OkHttpClient
|
||||||
|
.Builder()
|
||||||
|
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS))
|
||||||
|
.eventListener(dnsEventListener)
|
||||||
|
.build()
|
||||||
else
|
else
|
||||||
createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
|
createOkHttpClientWithDynamicCert(loadCertificateFromString(cert))
|
||||||
|
|
||||||
@@ -136,9 +184,10 @@ object ApiClient {
|
|||||||
|
|
||||||
private fun createRetrofit(): Retrofit {
|
private fun createRetrofit(): Retrofit {
|
||||||
val okHttpClient = createOkHttp()
|
val okHttpClient = createOkHttp()
|
||||||
|
val b = replaceAbyssProtocol(base)
|
||||||
|
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl(base)
|
.baseUrl(b)
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||||
@@ -148,13 +197,13 @@ object ApiClient {
|
|||||||
|
|
||||||
var api: ApiInterface? = null
|
var api: ApiInterface? = null
|
||||||
|
|
||||||
suspend fun apply(urls: String, crt: String): String? {
|
suspend fun apply(context: Context, urls: String, crt: String): String? {
|
||||||
try {
|
try {
|
||||||
val urlList = urls.split(";").map { it.trim() }
|
val urlList = urls.split(";").map { it.trim() }
|
||||||
|
|
||||||
var selectedUrl: String? = null
|
var selectedUrl: String? = null
|
||||||
for (url in urlList) {
|
for (url in urlList) {
|
||||||
val host = url.toHttpUrlOrNull()?.host
|
val host = url.toUri().host
|
||||||
if (host != null && pingHost(host)) {
|
if (host != null && pingHost(host)) {
|
||||||
selectedUrl = url
|
selectedUrl = url
|
||||||
break
|
break
|
||||||
@@ -168,6 +217,12 @@ object ApiClient {
|
|||||||
domain = selectedUrl.toHttpUrlOrNull()?.host ?: ""
|
domain = selectedUrl.toHttpUrlOrNull()?.host ?: ""
|
||||||
cert = crt
|
cert = crt
|
||||||
base = selectedUrl
|
base = selectedUrl
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO)
|
||||||
|
{
|
||||||
|
(context as AetherApp).abyssService?.proxy?.config(base.toUri().host!!, 4096)
|
||||||
|
}
|
||||||
|
|
||||||
api = createRetrofit().create(ApiInterface::class.java)
|
api = createRetrofit().create(ApiInterface::class.java)
|
||||||
return base
|
return base
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.acitelight.aether.viewModel
|
package com.acitelight.aether.viewModel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -34,11 +35,13 @@ import kotlinx.coroutines.launch
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.acitelight.aether.service.*
|
import com.acitelight.aether.service.*
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeScreenViewModel @Inject constructor(
|
class HomeScreenViewModel @Inject constructor(
|
||||||
private val settingsDataStoreManager: SettingsDataStoreManager
|
private val settingsDataStoreManager: SettingsDataStoreManager,
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
) : ViewModel()
|
) : ViewModel()
|
||||||
{
|
{
|
||||||
var _init = false
|
var _init = false
|
||||||
@@ -73,7 +76,7 @@ class HomeScreenViewModel @Inject constructor(
|
|||||||
if(u=="" || p=="" || ur=="") return@launch
|
if(u=="" || p=="" || ur=="") return@launch
|
||||||
|
|
||||||
try{
|
try{
|
||||||
val usedUrl = ApiClient.apply(ur, if(uss.first()) c else "")
|
val usedUrl = ApiClient.apply(context, ur, if(uss.first()) c else "")
|
||||||
|
|
||||||
if (MediaManager.token == "null")
|
if (MediaManager.token == "null")
|
||||||
MediaManager.token = AuthManager.fetchToken(
|
MediaManager.token = AuthManager.fetchToken(
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import kotlinx.coroutines.launch
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.acitelight.aether.service.*
|
import com.acitelight.aether.service.*
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MeScreenViewModel @Inject constructor(
|
class MeScreenViewModel @Inject constructor(
|
||||||
private val settingsDataStoreManager: SettingsDataStoreManager
|
private val settingsDataStoreManager: SettingsDataStoreManager,
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val username = mutableStateOf("");
|
val username = mutableStateOf("");
|
||||||
@@ -66,7 +68,7 @@ class MeScreenViewModel @Inject constructor(
|
|||||||
if (u == "" || p == "" || us == "") return@launch
|
if (u == "" || p == "" || us == "") return@launch
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val usedUrl = ApiClient.apply(u, if(uss.first()) c else "")
|
val usedUrl = ApiClient.apply(context, u, if(uss.first()) c else "")
|
||||||
MediaManager.token = AuthManager.fetchToken(
|
MediaManager.token = AuthManager.fetchToken(
|
||||||
us,
|
us,
|
||||||
p
|
p
|
||||||
|
|||||||
Reference in New Issue
Block a user