Skip to content

Commit

Permalink
biometric
Browse files Browse the repository at this point in the history
  • Loading branch information
lucky committed Jul 9, 2022
1 parent 3182199 commit e68d9ca
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 154 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ It can:
* limit the maximum number of failed password attempts
* disable USB data connections (Android 12, USB HAL 1.3, Device Owner)

Also you can grant it device and app notifications permission to turn off USB data connections
Also you can grant it device & app notifications permission to turn off USB data connections
automatically on screen off.

## Permissions
Expand Down
5 changes: 3 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId "me.lucky.sentry"
minSdk 23
targetSdk 32
versionCode 4
versionName "1.0.3"
versionCode 5
versionName "1.0.4"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -49,4 +49,5 @@ dependencies {

implementation 'androidx.security:security-crypto:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.biometric:biometric:1.1.0'
}
3 changes: 1 addition & 2 deletions app/src/main/java/me/lucky/sentry/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ package me.lucky.sentry
import android.app.Application
import com.google.android.material.color.DynamicColors

@Suppress("unused")
class Application : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
}
}
7 changes: 2 additions & 5 deletions app/src/main/java/me/lucky/sentry/DeviceAdminManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class DeviceAdminManager(private val ctx: Context) {
private val deviceAdmin by lazy { ComponentName(ctx, DeviceAdminReceiver::class.java) }

fun remove() = dpm?.removeActiveAdmin(deviceAdmin)
fun isActive() = dpm?.isAdminActive(deviceAdmin) ?: false
fun getCurrentFailedPasswordAttempts() = dpm?.currentFailedPasswordAttempts ?: 0
fun isDeviceOwner() = dpm?.isDeviceOwnerApp(ctx.packageName) ?: false

@RequiresApi(Build.VERSION_CODES.S)
fun canUsbDataSignalingBeDisabled() = dpm?.canUsbDataSignalingBeDisabled() ?: false
Expand All @@ -33,10 +33,7 @@ class DeviceAdminManager(private val ctx: Context) {
dpm?.wipeData(flags)
}

fun setMaximumFailedPasswordsForWipe(num: Int) =
dpm?.setMaximumFailedPasswordsForWipe(deviceAdmin, num)

fun makeRequestIntent() =
Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, deviceAdmin)
}
}
8 changes: 3 additions & 5 deletions app/src/main/java/me/lucky/sentry/DeviceAdminReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import android.os.UserManager
class DeviceAdminReceiver : DeviceAdminReceiver() {
override fun onPasswordFailed(context: Context, intent: Intent, user: UserHandle) {
super.onPasswordFailed(context, intent, user)
val prefs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
context.getSystemService(UserManager::class.java)?.isUserUnlocked != true)

PreferencesDirectBoot(context) else Preferences(context)
val prefs = Preferences(context, encrypted = Build.VERSION.SDK_INT < Build.VERSION_CODES.N
|| context.getSystemService(UserManager::class.java)?.isUserUnlocked == true)
val maxFailedPasswordAttempts = prefs.maxFailedPasswordAttempts
if (!prefs.isEnabled || maxFailedPasswordAttempts <= 0) return
val admin = DeviceAdminManager(context)
if (admin.getCurrentFailedPasswordAttempts() >= maxFailedPasswordAttempts)
try { admin.wipeData() } catch (exc: SecurityException) {}
}
}
}
135 changes: 79 additions & 56 deletions app/src/main/java/me/lucky/sentry/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,116 +1,139 @@
package me.lucky.sentry

import android.content.pm.PackageManager
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar

import me.lucky.sentry.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var prefs: PreferencesProxy
private lateinit var prefs: Preferences
private lateinit var prefsdb: Preferences
private lateinit var admin: DeviceAdminManager

private val registerForDeviceAdmin =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode != RESULT_OK) binding.toggle.isChecked = false else setOn()
if (it.resultCode != RESULT_OK) setOff() else setOn()
}

private val prefsListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
prefs.copyTo(prefsdb, key)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
init()
if (initBiometric()) return
setup()
}

override fun onStart() {
super.onStart()
prefs.registerListener(prefsListener)
update()
}

override fun onStop() {
super.onStop()
prefs.unregisterListener(prefsListener)
}

private fun initBiometric(): Boolean {
val authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
when (BiometricManager
.from(this)
.canAuthenticate(authenticators))
{
BiometricManager.BIOMETRIC_SUCCESS -> {}
else -> return false
}
val executor = ContextCompat.getMainExecutor(this)
val prompt = BiometricPrompt(
this,
executor,
object : BiometricPrompt.AuthenticationCallback()
{
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
finishAndRemoveTask()
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
setup()
}
})
prompt.authenticate(BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.biometric_title))
.setConfirmationRequired(false)
.setAllowedAuthenticators(authenticators)
.build())
return true
}

private fun init() {
prefs = PreferencesProxy(this)
prefs.clone()
prefs = Preferences(this)
prefsdb = Preferences(this, encrypted = false)
prefs.copyTo(prefsdb)
admin = DeviceAdminManager(this)
if (prefs.isEnabled && prefs.maxFailedPasswordAttempts > 0)
try { admin.setMaximumFailedPasswordsForWipe(0) } catch (exc: SecurityException) {}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
!packageManager.hasSystemFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN))
hideSecureLockScreenRequired()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !admin.canUsbDataSignalingBeDisabled())
hideUsbDataSignaling()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
!admin.canUsbDataSignalingBeDisabled() ||
!admin.isDeviceOwner())
disableUsbDataSignaling()
binding.apply {
maxFailedPasswordAttempts.value = prefs.maxFailedPasswordAttempts.toFloat()
usbDataSignaling.isChecked = isUsbDataSignalingEnabled()
toggle.isChecked = prefs.isEnabled
}
}

private fun setup() {
binding.apply {
maxFailedPasswordAttempts.addOnChangeListener { _, value, _ ->
prefs.maxFailedPasswordAttempts = value.toInt()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
usbDataSignaling.setOnCheckedChangeListener { _, isChecked ->
try {
admin.setUsbDataSignalingEnabled(isChecked)
} catch (exc: Exception) {
Snackbar.make(
usbDataSignaling,
R.string.usb_data_signaling_change_failed_popup,
Snackbar.LENGTH_SHORT,
).show()
usbDataSignaling.isChecked = !isChecked
}
private fun setup() = binding.apply {
maxFailedPasswordAttempts.addOnChangeListener { _, value, _ ->
prefs.maxFailedPasswordAttempts = value.toInt()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
usbDataSignaling.setOnCheckedChangeListener { _, isChecked ->
try { admin.setUsbDataSignalingEnabled(isChecked) } catch (exc: Exception) {
Snackbar.make(
usbDataSignaling,
R.string.usb_data_signaling_change_failed_popup,
Snackbar.LENGTH_SHORT,
).show()
usbDataSignaling.isChecked = !isChecked
}
toggle.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) requestAdmin() else setOff()
}
toggle.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) requestAdmin() else setOff()
}
}

private fun hideSecureLockScreenRequired() {
binding.apply {
maxFailedPasswordAttempts.visibility = View.GONE
maxFailedPasswordAttemptsDescription.visibility = View.GONE
space.visibility = View.GONE
}
}

private fun hideUsbDataSignaling() {
binding.apply {
usbDataSignaling.visibility = View.GONE
usbDataSignalingDescription.visibility = View.GONE
}
}
private fun disableUsbDataSignaling() { binding.usbDataSignaling.isEnabled = false }

private fun setOn() {
prefs.isEnabled = true
binding.toggle.isChecked = true
}

private fun setOff() {
prefs.isEnabled = false
try { admin.remove() } catch (exc: SecurityException) {}
binding.toggle.isChecked = false
}

private fun update() {
binding.usbDataSignaling.isChecked = isUsbDataSignalingEnabled()
if (prefs.isEnabled && !admin.isActive())
Snackbar.make(
binding.toggle,
R.string.service_unavailable_popup,
Snackbar.LENGTH_SHORT,
).show()
}
private fun update() { binding.usbDataSignaling.isChecked = isUsbDataSignalingEnabled() }

private fun isUsbDataSignalingEnabled() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
admin.isUsbDataSignalingEnabled() else true

private fun requestAdmin() = registerForDeviceAdmin.launch(admin.makeRequestIntent())
}
}
25 changes: 10 additions & 15 deletions app/src/main/java/me/lucky/sentry/NotificationListenerService.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package me.lucky.sentry

import android.app.KeyguardManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
Expand All @@ -23,14 +22,14 @@ class NotificationListenerService : NotificationListenerService() {
}

private fun init() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
DeviceAdminManager(this).canUsbDataSignalingBeDisabled())
{
registerReceiver(lockReceiver, IntentFilter().apply {
addAction(Intent.ACTION_USER_PRESENT)
addAction(Intent.ACTION_SCREEN_OFF)
})
}
val admin = DeviceAdminManager(this)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
!admin.canUsbDataSignalingBeDisabled() ||
!admin.isDeviceOwner()) { return }
registerReceiver(lockReceiver, IntentFilter().apply {
addAction(Intent.ACTION_USER_PRESENT)
addAction(Intent.ACTION_SCREEN_OFF)
})
}

private fun deinit() {
Expand All @@ -48,11 +47,7 @@ class NotificationListenerService : NotificationListenerService() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
!Preferences(context ?: return).isEnabled) return
when (intent?.action) {
Intent.ACTION_USER_PRESENT -> {
if (context.getSystemService(KeyguardManager::class.java)
?.isDeviceSecure != true) return
setUsbDataSignalingEnabled(context, true)
}
Intent.ACTION_USER_PRESENT -> setUsbDataSignalingEnabled(context, true)
Intent.ACTION_SCREEN_OFF -> setUsbDataSignalingEnabled(context, false)
}
}
Expand All @@ -63,4 +58,4 @@ class NotificationListenerService : NotificationListenerService() {
catch (exc: Exception) {}
}
}
}
}
Loading

0 comments on commit e68d9ca

Please sign in to comment.