Android-Biometrics/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt
2025-05-03 02:22:35 +02:00

214 lines
8.2 KiB
Kotlin

package de.polyfish0.pamauth.services
import android.app.ActivityManager
import android.app.ForegroundServiceStartNotAllowedException
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ServiceInfo
import android.graphics.PixelFormat
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.Window
import android.view.WindowManager
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
import de.polyfish0.pamauth.R
import de.polyfish0.pamauth.activities.TransparentBiometricActivity
import de.polyfish0.pamauth.data.PAMRequestData
import de.polyfish0.pamauth.utils.CallbackManager
import kotlinx.serialization.json.Json
import java.net.ServerSocket
import java.net.Socket
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import androidx.core.content.edit
class PAMServerService : LifecycleService() {
private val socketMap = mutableMapOf<String, Socket>()
private lateinit var notificationManager: NotificationManager
override fun onCreate() {
super.onCreate()
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
getSharedPreferences("pam", Context.MODE_PRIVATE).edit {
putBoolean("serviceRunning", true)
}
try {
val channel = NotificationChannel(
"PAMServer",
"PAM Authentication Server",
NotificationManager.IMPORTANCE_MAX
).apply {
description = "Handles PAM authentication server foreground service"
}
notificationManager.createNotificationChannel(channel)
val notification = NotificationCompat.Builder(this, "PAMServer")
.setContentTitle("PAM Authentication")
.setContentText("PAM Server is running")
.setSmallIcon(android.R.drawable.ic_lock_lock)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
ServiceCompat.startForeground(
this,
100,
notification,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
}
)
} catch (e: Exception) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
&& e is ForegroundServiceStartNotAllowedException
) {
// App not in a valid state to start foreground service
// (e.g. started from bg)
}
}
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
startServer()
return START_STICKY
}
private fun registerService(localPort: Int) {
val serviceInfo = NsdServiceInfo().apply {
serviceName = "PAMAuthServer"
serviceType = "_pamAuthServer._tcp"
port = localPort
}
val registrationListener = object : NsdManager.RegistrationListener {
override fun onServiceRegistered(NsdServiceInfo: NsdServiceInfo) {
// Save the service name. Android may have changed it in order to
// resolve a conflict, so update the name you initially requested
// with the name Android actually used.
}
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Registration failed! Put debugging code here to determine why.
}
override fun onServiceUnregistered(arg0: NsdServiceInfo) {
// Service has been unregistered. This only happens when you call
// NsdManager.unregisterService() and pass in this listener.
}
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
// Unregistration failed. Put debugging code here to determine why.
}
}
(getSystemService(NSD_SERVICE) as NsdManager).apply {
registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, registrationListener)
}
}
override fun onDestroy() {
super.onDestroy()
getSharedPreferences("pam", Context.MODE_PRIVATE).edit {
putBoolean("serviceRunning", false)
}
}
@OptIn(ExperimentalUuidApi::class)
private fun startServer() {
Thread {
try {
val serverSocket = ServerSocket(0).also { socket ->
Log.d("PAM", "$socket")
registerService(socket.localPort)
}
while (true) {
val client = serverSocket.accept()
Thread {
val input = client.getInputStream().bufferedReader()
val data = Json.decodeFromString<PAMRequestData>(input.readLine())
val callbackID = Uuid.random().toString()
CallbackManager.put(callbackID, ::test)
socketMap[callbackID] = client
val intent = Intent(applicationContext, TransparentBiometricActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
putExtra("computerName", data.computerName)
putExtra("process", data.process)
putExtra("callbackID", callbackID)
}
if(hasAppTask(applicationContext)) {
startActivity(intent)
}else {
val notification = NotificationCompat.Builder(applicationContext, "PAMServer")
.setContentTitle("PAM Authentication request")
.setContentText("The computer \"${data.computerName}\" has requested an authentication for the process \"${data.process}\"")
.setSmallIcon(android.R.drawable.ic_lock_lock)
.setContentIntent(
PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_MAX)
.build()
notificationManager.notify(callbackID.hashCode(), notification)
}
}.start()
}
} catch (e: Exception) {
Log.e("PAM", "Server error", e)
}
}.start()
}
fun test(result: Boolean, callbackID: String) {
Thread {
val client = socketMap.remove(callbackID)?: throw RuntimeException("No client with callbackID \"$callbackID\" registered")
client.getOutputStream().write("Result: $result".toByteArray())
client.close()
}.start()
}
fun hasAppTask(context: Context): Boolean {
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val tasks = am.appTasks
return tasks.any { it.taskInfo.baseIntent.component?.packageName == context.packageName }
}
}