diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml
index 9e9ba09..22baa42 100644
--- a/.idea/caches/deviceStreaming.xml
+++ b/.idea/caches/deviceStreaming.xml
@@ -76,6 +76,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..ebf26f7 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 74dd639..b2c751a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index d843f34..35eb1dd 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -1,4 +1,6 @@
-
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index b0b86af..5e6fedc 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,12 +4,47 @@
+
+
+
+ @style/Theme.Transparent
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -18,7 +53,7 @@
-
+
@@ -28,13 +63,18 @@
+
+
+
{
"associatedIndex": 2
}
+
@@ -44,16 +84,27 @@
"Android App.app.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
+ "RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.readMode.enableVisualFormatting": "true",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
+ "com.google.services.firebase.aqiPopupShown": "true",
+ "git-widget-placeholder": "master",
"kotlin-language-version-configured": "true",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.17",
"project.structure.side.proportion": "0.2",
- "settings.editor.selected.configurable": "preferences.language.Kotlin"
+ "settings.editor.selected.configurable": "editor.preferences.fonts.default"
}
}]]>
+
+
+
+
+
+
+
+
@@ -137,19 +188,67 @@
1745786712844
+
+
+ 1746231445912
+
+
+
+ 1746231445912
+
+
+
+ 1746231473505
+
+
+
+ 1746231473505
+
+
+
+ 1746231483427
+
+
+
+ 1746231483427
+
+
+
+ 1746231495331
+
+
+
+ 1746231495331
+
+
+
+
+
+
+
+
+
+
+ file://$PROJECT_DIR$/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt
+ 173
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4b05c75..5050eb1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
+ kotlin("plugin.serialization") version "2.1.20"
}
android {
@@ -50,6 +51,13 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.biometric.ktx)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.lifecycle.service)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.constraintlayout)
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 368d7d7..026620c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,9 +1,12 @@
+ xmlns:tools="http://schemas.android.com/tools" >
+
+
+
-
+ tools:targetApi="31" >
+
+
+ android:theme="@style/Theme.PAMAuth" >
+
+
\ No newline at end of file
diff --git a/app/src/main/java/de/polyfish0/pamauth/MainActivity.kt b/app/src/main/java/de/polyfish0/pamauth/MainActivity.kt
index 88742c0..9cc758b 100644
--- a/app/src/main/java/de/polyfish0/pamauth/MainActivity.kt
+++ b/app/src/main/java/de/polyfish0/pamauth/MainActivity.kt
@@ -1,83 +1,193 @@
package de.polyfish0.pamauth
+import android.app.ActivityManager
+import android.content.Context
import android.content.Intent
+import android.content.SharedPreferences
import android.os.Bundle
-import android.widget.Toast
-import androidx.activity.ComponentActivity
+import android.provider.Settings
+import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.biometric.BiometricManager
-import androidx.biometric.BiometricPrompt
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
-import androidx.core.content.ContextCompat
+import androidx.core.content.edit
+import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import de.polyfish0.pamauth.services.PAMServerService
import de.polyfish0.pamauth.ui.theme.PAMAuthTheme
+import de.polyfish0.pamauth.viewmodels.PermissionState
class MainActivity : FragmentActivity() {
+ private lateinit var showNotificationDialog: MutableState
+ private lateinit var showOverlayDialog: MutableState
+ private lateinit var context: Context
+ private lateinit var serviceIntent: Intent
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ serviceIntent = Intent(this, PAMServerService::class.java)
+
enableEdgeToEdge()
setContent {
- PAMAuthTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding)
- )
- ElevatedButton(
- modifier = Modifier.padding(innerPadding),
- onClick = { startForegroundService(Intent(this, PAMServerService::class.java)) }
- ) {
- Text(text = "Show Biometric")
+ context = LocalContext.current
+ showNotificationDialog = remember { mutableStateOf(false) }
+ showOverlayDialog = remember { mutableStateOf(!Settings.canDrawOverlays(context)) }
+ val isServiceRunning by rememberServiceRunningState()
+
+ RequestPermission("android.permission.POST_NOTIFICATIONS") { result ->
+ showNotificationDialog.value = !result
+ }
+
+ val isRunning = getSharedPreferences("pam", Context.MODE_PRIVATE).getBoolean("serviceRunning", false)
+ if (isRunning) {
+ if (!isServiceActuallyRunning(context)) {
+ getSharedPreferences("pam", Context.MODE_PRIVATE).edit {
+ putBoolean("serviceRunning", false)
}
}
}
+
+ PAMAuthTheme {
+ Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding)) {
+ Greeting(
+ name = "PAM Server for Linux authentication",
+ modifier = Modifier.padding(innerPadding)
+ )
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ Text("Service enabled")
+ Switch(
+ checked = isServiceRunning,
+ onCheckedChange = {
+ if(it) {
+ startForegroundService(serviceIntent)
+ }else {
+ stopService(serviceIntent)
+ }
+ }
+ )
+ }
+ }
+ }
+
+ CheckOverlayPermission()
+ CheckNotificationsPermission()
+ }
}
}
- private fun showBiometric() {
- var biometricPrompt = BiometricPrompt(
- this,
- ContextCompat.getMainExecutor(this),
- object : BiometricPrompt.AuthenticationCallback() {
- override fun onAuthenticationError(errorCode: Int,
- errString: CharSequence) {
- super.onAuthenticationError(errorCode, errString)
- Toast.makeText(applicationContext,
- "Authentication error: $errString", Toast.LENGTH_SHORT)
- .show()
- }
+ override fun onResume() {
+ super.onResume()
+ PermissionState.updatePermission(applicationContext, "android.permission.POST_NOTIFICATIONS")
+ if(this::showOverlayDialog.isInitialized)
+ showOverlayDialog.value = !Settings.canDrawOverlays(applicationContext)
+ }
- override fun onAuthenticationSucceeded(
- result: BiometricPrompt.AuthenticationResult) {
- super.onAuthenticationSucceeded(result)
- Toast.makeText(applicationContext,
- "Authentication succeeded!", Toast.LENGTH_SHORT)
- .show()
- }
+ private fun isServiceActuallyRunning(context: Context): Boolean {
+ val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ return manager.getRunningServices(Int.MAX_VALUE).any {
+ it.service.className == PAMServerService::class.java.name
+ }
+ }
- override fun onAuthenticationFailed() {
- super.onAuthenticationFailed()
- Toast.makeText(applicationContext, "Authentication failed",
- Toast.LENGTH_SHORT)
- .show()
+ @Composable
+ fun CheckNotificationsPermission() {
+ if(showNotificationDialog.value and !(PermissionState.rememberPermissionState(context, "android.permission.POST_NOTIFICATIONS").value)) {
+ AlertDialog(
+ title = {
+ Text("Notification permission denied")
+ },
+ text = {
+ Text("This app needs the permission for notifications to function. If the app is not able to open the biometrics menu it sends you a notification which opens it when you tap on it.")
+ },
+ onDismissRequest = {},
+ confirmButton = {
+ Button(onClick = {
+ val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = "package:$packageName".toUri()
+ startActivity(intent)
+ }) {
+ Text("Open Settings")
+ }
}
- })
- biometricPrompt.authenticate(BiometricPrompt.PromptInfo.Builder()
- .setTitle("PAM Test")
- .setSubtitle("PAM Subtitle")
- .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
- .setNegativeButtonText("Abort")
- .build()
- )
+ )
+ }
+ }
+
+ @Composable
+ fun CheckOverlayPermission() {
+ if(showOverlayDialog.value) {
+ AlertDialog(
+ onDismissRequest = {},
+ title = { Text("Overlay permission required") },
+ text = {
+ Text("This app needs the permission to show overlays to work properly.")
+ },
+ confirmButton = {
+ Button(onClick = {
+ val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
+ data = "package:${context.packageName}".toUri()
+ }
+ startActivityForResult(intent, 100)
+ }) {
+ Text("Settings")
+ }
+ }
+ )
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if(resultCode == 100) {
+ showOverlayDialog.value = !Settings.canDrawOverlays(applicationContext)
+ }
+ }
+
+ @Composable
+ fun rememberServiceRunningState(): State {
+ val state = remember { mutableStateOf(false) }
+
+ DisposableEffect(Unit) {
+ val prefs = getSharedPreferences("pam", Context.MODE_PRIVATE)
+ val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
+ if (key == "serviceRunning") {
+ state.value = prefs.getBoolean("serviceRunning", false)
+ }
+ }
+ prefs.registerOnSharedPreferenceChangeListener(listener)
+ state.value = prefs.getBoolean("serviceRunning", false)
+
+ onDispose {
+ prefs.unregisterOnSharedPreferenceChangeListener(listener)
+ }
+ }
+
+ return state
}
}
@@ -95,4 +205,23 @@ fun GreetingPreview() {
PAMAuthTheme {
Greeting("Android")
}
-}
\ No newline at end of file
+}
+
+@Composable
+fun RequestPermission(permission: String, onResult: (Boolean) -> Unit) {
+ val context = LocalContext.current
+ val launcher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission()
+ ) { isGranted ->
+ PermissionState.updatePermission(context, permission)
+ onResult(isGranted)
+ }
+
+ LaunchedEffect(Unit) {
+ if (!PermissionState.hasPermission(context, permission)) {
+ launcher.launch(permission)
+ } else {
+ onResult(true)
+ }
+ }
+}
diff --git a/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt b/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt
index bd5017a..5472523 100644
--- a/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt
+++ b/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt
@@ -1,35 +1,64 @@
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
-class PAMServerService : Service() {
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_LOW
+ NotificationManager.IMPORTANCE_MAX
).apply {
description = "Handles PAM authentication server foreground service"
}
- val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val notification = NotificationCompat.Builder(this, "PAMServer")
@@ -37,6 +66,7 @@ class PAMServerService : Service() {
.setContentText("PAM Server is running")
.setSmallIcon(android.R.drawable.ic_lock_lock)
.setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
.build()
ServiceCompat.startForeground(
@@ -59,11 +89,13 @@ class PAMServerService : Service() {
}
}
- override fun onBind(p0: Intent?): IBinder? {
+ 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
@@ -97,11 +129,19 @@ class PAMServerService : Service() {
}
}
- var nsdManager = (getSystemService(NSD_SERVICE) as NsdManager).apply {
+ (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 {
@@ -111,16 +151,64 @@ class PAMServerService : Service() {
}
while (true) {
val client = serverSocket.accept()
- val input = client.getInputStream().bufferedReader()
- val request = input.readLine()
- Log.d("PAM", request)
- client.outputStream.write("Hi $request\n".toByteArray())
- client.outputStream.flush()
- client.close()
+
+ 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 }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f8c6127..d29adb9 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -7,4 +7,5 @@
#FF018786
#FF000000
#FFFFFFFF
+ #00000000
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 3663ae1..be61640 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,12 @@
-
+
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a1f90d3..8eb51a2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,5 @@
[versions]
-agp = "8.9.1"
+agp = "8.9.2"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"
@@ -9,6 +9,9 @@ lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
biometricKtx = "1.4.0-alpha02"
+appcompat = "1.6.1"
+material = "1.10.0"
+constraintlayout = "2.1.4"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -26,9 +29,14 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric", version.ref = "biometricKtx" }
+androidx-activity = { group = "androidx.activity", name = "activity" }
+androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-lifecycle-service = { group = "androidx.lifecycle", name = "lifecycle-service" }
[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" }
-
diff --git a/local.properties b/local.properties
index a279686..a5b258f 100644
--- a/local.properties
+++ b/local.properties
@@ -1,10 +1,8 @@
-## This file is automatically generated by Android Studio.
-# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
-#
-# This file should *NOT* be checked into Version Control Systems,
+## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
-sdk.dir=/Users/mika/Library/Android/sdk
\ No newline at end of file
+#Tue Apr 29 15:43:07 UTC 2025
+sdk.dir=/home/mika/Android/Sdk