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