First poc

This commit is contained in:
Mika Westphal 2025-05-03 02:22:35 +02:00
parent bfd5a134da
commit 37a8f3c2b7
13 changed files with 453 additions and 83 deletions

View File

@ -76,6 +76,18 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a16x" />
<option name="id" value="a16x" />
<option name="labId" value="google" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A16 5G" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />

View File

@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-02T20:56:34.144725876Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.0.192:37353;connection=b4c14d05" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings" defaultProject="true" />
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -4,12 +4,47 @@
<shared>
<config />
</shared>
<layouts>
<layout url="file://$PROJECT_DIR$/app/src/main/res/layout/activity_transparent.xml">
<config>
<theme>@style/Theme.Transparent</theme>
</config>
</layout>
</layouts>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="NONE" />
</component>
<component name="ChangeListManager">
<list default="true" id="85e0dc1a-d7e2-4ab4-a8ad-e708732e10e5" name="Changes" comment="" />
<list default="true" id="85e0dc1a-d7e2-4ab4-a8ad-e708732e10e5" name="Changes" comment="Improve .gitignore">
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/checksums/checksums.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/checksums/checksums.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/checksums/md5-checksums.bin" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/checksums/md5-checksums.bin" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/checksums/sha1-checksums.bin" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/checksums/sha1-checksums.bin" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/executionHistory/executionHistory.bin" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/executionHistory/executionHistory.bin" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/executionHistory/executionHistory.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/executionHistory/executionHistory.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/fileHashes/fileHashes.bin" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/fileHashes/fileHashes.bin" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/fileHashes/fileHashes.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/fileHashes/fileHashes.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.11.1/fileHashes/resourceHashesCache.bin" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.11.1/fileHashes/resourceHashesCache.bin" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/buildOutputCleanup/buildOutputCleanup.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/buildOutputCleanup/buildOutputCleanup.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/buildOutputCleanup/outputFiles.bin" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/buildOutputCleanup/outputFiles.bin" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/config.properties" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/config.properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/file-system.probe" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/file-system.probe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/caches/deviceStreaming.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/caches/deviceStreaming.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/deploymentTargetSelector.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/deploymentTargetSelector.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/app/build.gradle.kts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/AndroidManifest.xml" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/AndroidManifest.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/java/de/polyfish0/pamauth/MainActivity.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/java/de/polyfish0/pamauth/MainActivity.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/res/values/colors.xml" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/res/values/colors.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/src/main/res/values/themes.xml" beforeDir="false" afterPath="$PROJECT_DIR$/app/src/main/res/values/themes.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build/reports/problems/problems-report.html" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/gradle/libs.versions.toml" beforeDir="false" afterPath="$PROJECT_DIR$/gradle/libs.versions.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/local.properties" beforeDir="false" afterPath="$PROJECT_DIR$/local.properties" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -18,7 +53,7 @@
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="device_and_snapshot_combo_box_target[]" />
<component name="ExecutionTargetManager" SELECTED_TARGET="device_and_snapshot_combo_box_target[DeviceId(pluginId=Default, isTemplate=false, identifier=serial=192.168.0.192:37353;connection=b4c14d05)]" />
<component name="ExternalProjectsData">
<projectState path="$PROJECT_DIR$">
<ProjectState />
@ -28,13 +63,18 @@
<option name="RECENT_TEMPLATES">
<list>
<option value="Kotlin Class" />
<option value="Kotlin Object" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 2
}</component>
<component name="ProjectId" id="2wKOHidxhnHfFSWY1BX7dDJxqEV" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
@ -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"
}
}]]></component>
<component name="RecentsManager">
<key name="android.template.-2066939809">
<recent name="de.polyfish0.pamauth.activities" />
</key>
<key name="android.template.2108362692">
<recent name="de.polyfish0.pamauth.activities" />
</key>
</component>
<component name="RunManager">
<configuration name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="PAM_Auth.app" />
@ -137,19 +188,67 @@
<option name="presentableId" value="Default" />
<updated>1745786712844</updated>
</task>
<task id="LOCAL-00001" summary="Improve .gitignore">
<option name="closed" value="true" />
<created>1746231445912</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1746231445912</updated>
</task>
<task id="LOCAL-00002" summary="Improve .gitignore">
<option name="closed" value="true" />
<created>1746231473505</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1746231473505</updated>
</task>
<task id="LOCAL-00003" summary="Improve .gitignore">
<option name="closed" value="true" />
<created>1746231483427</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1746231483427</updated>
</task>
<task id="LOCAL-00004" summary="Improve .gitignore">
<option name="closed" value="true" />
<created>1746231495331</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1746231495331</updated>
</task>
<option name="localTasksCounter" value="5" />
<servers />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Improve .gitignore" />
<option name="LAST_COMMIT_MESSAGE" value="Improve .gitignore" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/app/src/main/java/de/polyfish0/pamauth/services/PAMServerService.kt</url>
<line>173</line>
<option name="timeStamp" value="9" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
</component>
<component name="play_dynamic_filters_status">
<option name="appIdToCheckInfo">
<map>
<entry key="de.polyfish0.pamauth">
<value>
<CheckInfo lastCheckTimestamp="1745789513434" />
<CheckInfo lastCheckTimestamp="1746209320095" />
</value>
</entry>
<entry key="de.polyfish0.pamauth.test">
<value>
<CheckInfo lastCheckTimestamp="1745789513434" />
<CheckInfo lastCheckTimestamp="1746209320092" />
</value>
</entry>
</map>

View File

@ -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)

View File

@ -1,9 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools" >
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:allowBackup="true"
@ -14,22 +17,30 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.PAMAuth"
tools:targetApi="31">
<service
android:name=".services.PAMServerService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
tools:targetApi="31" >
<activity
android:name=".activities.TransparentBiometricActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
android:excludeFromRecents="true"
android:exported="true"
android:taskAffinity=""
android:finishOnTaskLaunch="true" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.PAMAuth">
android:theme="@style/Theme.PAMAuth" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".services.PAMServerService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>

View File

@ -1,84 +1,194 @@
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<Boolean>
private lateinit var showOverlayDialog: MutableState<Boolean>
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 {
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 = "Android",
name = "PAM Server for Linux authentication",
modifier = Modifier.padding(innerPadding)
)
ElevatedButton(
modifier = Modifier.padding(innerPadding),
onClick = { startForegroundService(Intent(this, PAMServerService::class.java)) }
) {
Text(text = "Show Biometric")
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Service enabled")
Switch(
checked = isServiceRunning,
onCheckedChange = {
if(it) {
startForegroundService(serviceIntent)
}else {
stopService(serviceIntent)
}
}
}
}
}
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 onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Toast.makeText(applicationContext,
"Authentication succeeded!", Toast.LENGTH_SHORT)
.show()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Toast.makeText(applicationContext, "Authentication failed",
Toast.LENGTH_SHORT)
.show()
}
})
biometricPrompt.authenticate(BiometricPrompt.PromptInfo.Builder()
.setTitle("PAM Test")
.setSubtitle("PAM Subtitle")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setNegativeButtonText("Abort")
.build()
)
}
}
}
CheckOverlayPermission()
CheckNotificationsPermission()
}
}
}
override fun onResume() {
super.onResume()
PermissionState.updatePermission(applicationContext, "android.permission.POST_NOTIFICATIONS")
if(this::showOverlayDialog.isInitialized)
showOverlayDialog.value = !Settings.canDrawOverlays(applicationContext)
}
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
}
}
@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")
}
}
)
}
}
@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<Boolean> {
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
}
}
@Composable
@ -96,3 +206,22 @@ fun GreetingPreview() {
Greeting("Android")
}
}
@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)
}
}
}

View File

@ -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<String, Socket>()
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()
Thread {
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()
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 }
}
}

View File

@ -7,4 +7,5 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="transparent">#00000000</color>
</resources>

View File

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.PAMAuth" parent="android:Theme.Material.Light.NoActionBar" />
<style name="Theme.Transparent" parent="android:Theme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>

View File

@ -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" }

View File

@ -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
#Tue Apr 29 15:43:07 UTC 2025
sdk.dir=/home/mika/Android/Sdk