Skip to content

Commit

Permalink
1.1.2
Browse files Browse the repository at this point in the history
  • Loading branch information
lucky committed Sep 19, 2022
1 parent a92e9d5 commit d0fa3c3
Show file tree
Hide file tree
Showing 25 changed files with 191 additions and 143 deletions.
4 changes: 2 additions & 2 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Privacy Policy

The app may store package names of apps without internet permission in internal database if
Monitor > Internet is checked.
The app Sentry (me.lucky.sentry) may store package names of apps without internet permission in internal database for monitoring permission changes.
All the data is stored on your device and automatically deleted.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ Tiny app to enforce security policies of your device.
It can:
* limit the maximum number of failed password attempts
* disable USB data connections (Android 12, USB HAL 1.3, Device Owner)
* disable safe boot mode (Android 7, Device Owner)
* notify on failed password attempt
* notify when an app without Internet permission got it after an update

Also you can grant it device & app notifications permission to turn off USB data connections
automatically on screen off.
Be aware that the app may not work in _safe_ mode.

## Permissions

* DEVICE_ADMIN - limit the maximum number of failed password attempts
* DEVICE_OWNER - disable USB data connections
* NOTIFICATION_LISTENER - receive lock/package events
* QUERY_ALL_PACKAGES - receiver all package events
* QUERY_ALL_PACKAGES - receive all package events

## Example

Expand Down
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ plugins {

android {
compileSdk 32
namespace 'me.lucky.sentry'

defaultConfig {
applicationId "me.lucky.sentry"
minSdk 23
targetSdk 32
versionCode 8
versionName "1.1.1"
versionCode 9
versionName "1.1.2"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -47,14 +48,13 @@ android {

dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

implementation 'androidx.security:security-crypto:1.0.0'
// https://issuetracker.google.com/issues/238425626
implementation('androidx.preference:preference-ktx:1.2.0') {
exclude group: 'androidx.lifecycle', module:'lifecycle-viewmodel'
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="me.lucky.sentry">
xmlns:tools="http://schemas.android.com/tools">

<uses-feature android:name="android.software.device_admin" android:required="false" />
<uses-permission
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/me/lucky/sentry/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment
import me.lucky.sentry.databinding.ActivityMainBinding
import me.lucky.sentry.fragment.MainFragment
import me.lucky.sentry.fragment.MonitorFragment
import me.lucky.sentry.fragment.UserRestrictionsFragment

open class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
Expand Down Expand Up @@ -48,6 +49,7 @@ open class MainActivity : AppCompatActivity() {
private fun getFragment(id: Int) = when (id) {
R.id.nav_main -> MainFragment()
R.id.nav_monitor -> MonitorFragment()
R.id.nav_user_restrictions -> UserRestrictionsFragment()
else -> MainFragment()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class NotificationListenerService : NotificationListenerService() {

private fun deinit() {
val unregister = { it: BroadcastReceiver ->
try { unregisterReceiver(it) } catch (exc: IllegalArgumentException) {}
try { unregisterReceiver(it) } catch (_: IllegalArgumentException) {}
}
unregister(lockReceiver)
unregister(packageReceiver)
Expand All @@ -73,7 +73,7 @@ class NotificationListenerService : NotificationListenerService() {
@RequiresApi(Build.VERSION_CODES.S)
private fun setUsbDataSignalingEnabled(ctx: Context, enabled: Boolean) {
try { DeviceAdminManager(ctx).setUsbDataSignalingEnabled(enabled) }
catch (exc: Exception) {}
catch (_: Exception) {}
}
}

Expand All @@ -100,12 +100,14 @@ class NotificationListenerService : NotificationListenerService() {
val packageName = getPackageName(intent) ?: return
if (Utils.hasInternet(ctx, packageName)) return
try { db.insert(Package(0, packageName)) }
catch (exc: SQLiteConstraintException) {}
catch (_: SQLiteConstraintException) {}
}
Intent.ACTION_PACKAGE_REPLACED -> {
val packageName = getPackageName(intent) ?: return
db.select(packageName) ?: return
if (!Utils.hasInternet(ctx, packageName)) return
db.delete(packageName)
if (!Preferences(ctx).isEnabled) return
NotificationManager(ctx).notifyInternet(packageName)
}
Intent.ACTION_PACKAGE_FULLY_REMOVED ->
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/me/lucky/sentry/NotificationManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class NotificationManager(private val ctx: Context) {
try {
app = ctx.packageManager
.getApplicationLabel(ctx.packageManager.getApplicationInfo(packageName, 0))
} catch (exc: PackageManager.NameNotFoundException) {}
} catch (_: PackageManager.NameNotFoundException) {}
return ctx.getString(R.string.notification_internet_text, app.toString(), packageName)
}
}
56 changes: 14 additions & 42 deletions app/src/main/java/me/lucky/sentry/Preferences.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,28 @@
package me.lucky.sentry

import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys

class Preferences(ctx: Context, encrypted: Boolean = true) {
class Preferences(ctx: Context) {
companion object {
private const val ENABLED = "enabled"
private const val MAX_FAILED_PASSWORD_ATTEMPTS = "max_failed_password_attempts"
private const val MAX_FAILED_PASSWORD_ATTEMPTS_WARNING =
"max_failed_password_attempts_warning"
private const val MAX_FAILED_PASSWORD_ATTEMPTS_DEFAULT_API =
"max_failed_password_attempts_default_api"
private const val USB_DATA_SIGNALING_CTL_ENABLED = "usb_data_signaling_ctl_enabled"
private const val MONITOR = "monitor"

private const val FILE_NAME = "sec_shared_prefs"
// migration
private const val SERVICE_ENABLED = "service_enabled"
private const val MAX_FAILED_PASSWORD_ATTEMPTS_WARNING =
"max_failed_password_attempts_warning"
}

private val prefs: SharedPreferences = if (encrypted) {
val mk = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
EncryptedSharedPreferences.create(
FILE_NAME,
mk,
ctx,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
} else {
val context = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
ctx.createDeviceProtectedStorageContext() else ctx
PreferenceManager.getDefaultSharedPreferences(context)
}
private val context = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
ctx.createDeviceProtectedStorageContext() else ctx
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)

var isEnabled: Boolean
get() = prefs.getBoolean(ENABLED, prefs.getBoolean(SERVICE_ENABLED, false))
Expand All @@ -45,9 +32,12 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
get() = prefs.getInt(MAX_FAILED_PASSWORD_ATTEMPTS, 0)
set(value) = prefs.edit { putInt(MAX_FAILED_PASSWORD_ATTEMPTS, value) }

var isMaxFailedPasswordAttemptsWarningChecked: Boolean
get() = prefs.getBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_WARNING, false)
set(value) = prefs.edit { putBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_WARNING, value) }
var isMaxFailedPasswordAttemptsDefaultApiChecked: Boolean
get() = prefs.getBoolean(
MAX_FAILED_PASSWORD_ATTEMPTS_DEFAULT_API,
prefs.getBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_WARNING, false),
)
set(value) = prefs.edit { putBoolean(MAX_FAILED_PASSWORD_ATTEMPTS_DEFAULT_API, value) }

var isUsbDataSignalingCtlEnabled: Boolean
get() = prefs.getBoolean(USB_DATA_SIGNALING_CTL_ENABLED, false)
Expand All @@ -56,24 +46,6 @@ class Preferences(ctx: Context, encrypted: Boolean = true) {
var monitor: Int
get() = prefs.getInt(MONITOR, 0)
set(value) = prefs.edit { putInt(MONITOR, value) }

fun registerListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
prefs.registerOnSharedPreferenceChangeListener(listener)

fun unregisterListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) =
prefs.unregisterOnSharedPreferenceChangeListener(listener)

fun copyTo(dst: Preferences, key: String? = null) = dst.prefs.edit {
for (entry in prefs.all.entries) {
val k = entry.key
if (key != null && k != key) continue
val v = entry.value ?: continue
when (v) {
is Boolean -> putBoolean(k, v)
is Int -> putInt(k, v)
}
}
}
}

enum class Monitor(val value: Int) {
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/me/lucky/sentry/admin/DeviceAdminManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class DeviceAdminManager(private val ctx: Context) {
fun remove() = dpm?.removeActiveAdmin(deviceAdmin)
fun getCurrentFailedPasswordAttempts() = dpm?.currentFailedPasswordAttempts ?: 0
fun isDeviceOwner() = dpm?.isDeviceOwnerApp(ctx.packageName) ?: false
fun addUserRestriction(key: String) = dpm?.addUserRestriction(deviceAdmin, key)
fun clearUserRestriction(key: String) = dpm?.clearUserRestriction(deviceAdmin, key)

@RequiresApi(Build.VERSION_CODES.N)
fun getUserRestrictions() = dpm?.getUserRestrictions(deviceAdmin)

fun setMaximumFailedPasswordsForWipe(num: Int) =
dpm?.setMaximumFailedPasswordsForWipe(deviceAdmin, num)
Expand Down
12 changes: 5 additions & 7 deletions app/src/main/java/me/lucky/sentry/admin/DeviceAdminReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package me.lucky.sentry.admin
import android.app.admin.DeviceAdminReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.UserHandle
import android.os.UserManager

import me.lucky.sentry.Monitor
import me.lucky.sentry.NotificationManager
Expand All @@ -14,15 +12,15 @@ import me.lucky.sentry.Preferences
class DeviceAdminReceiver : DeviceAdminReceiver() {
override fun onPasswordFailed(context: Context, intent: Intent, user: UserHandle) {
super.onPasswordFailed(context, intent, user)
val prefs = Preferences(context, encrypted = Build.VERSION.SDK_INT < Build.VERSION_CODES.N
|| context.getSystemService(UserManager::class.java)?.isUserUnlocked == true)
val prefs = Preferences(context)
if (!prefs.isEnabled) return
if (prefs.monitor.and(Monitor.PASSWORD.value) != 0)
NotificationManager(context).notifyPassword()
if (prefs.isMaxFailedPasswordAttemptsWarningChecked) return
if (prefs.isMaxFailedPasswordAttemptsDefaultApiChecked) return
val maxFailedPasswordAttempts = prefs.maxFailedPasswordAttempts
if (!prefs.isEnabled || maxFailedPasswordAttempts <= 0) return
if (maxFailedPasswordAttempts <= 0) return
val admin = DeviceAdminManager(context)
if (admin.getCurrentFailedPasswordAttempts() >= maxFailedPasswordAttempts)
try { admin.wipeData() } catch (exc: SecurityException) {}
try { admin.wipeData() } catch (_: SecurityException) {}
}
}
51 changes: 18 additions & 33 deletions app/src/main/java/me/lucky/sentry/fragment/MainFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package me.lucky.sentry.fragment

import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
Expand All @@ -22,7 +21,6 @@ class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding
private lateinit var ctx: Context
private lateinit var prefs: Preferences
private lateinit var prefsdb: Preferences
private val admin by lazy { DeviceAdminManager(ctx) }

override fun onCreateView(
Expand All @@ -38,24 +36,16 @@ class MainFragment : Fragment() {

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

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

private fun init() {
ctx = requireContext()
prefs = Preferences(ctx)
prefsdb = Preferences(ctx, encrypted = false)
prefs.copyTo(prefsdb)
binding.apply {
maxFailedPasswordAttempts.editText?.setText(prefs.maxFailedPasswordAttempts.toString())
maxFailedPasswordAttemptsWarning.isChecked =
prefs.isMaxFailedPasswordAttemptsWarningChecked
maxFailedPasswordAttemptsDefaultApi.isChecked =
prefs.isMaxFailedPasswordAttemptsDefaultApiChecked
val canChangeUsbDataSignaling = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
admin.canUsbDataSignalingBeDisabled() &&
admin.isDeviceOwner()
Expand All @@ -69,21 +59,17 @@ class MainFragment : Fragment() {

private fun setup() = binding.apply {
maxFailedPasswordAttempts.editText?.doAfterTextChanged {
val i = it?.toString()?.toIntOrNull() ?: return@doAfterTextChanged
prefs.maxFailedPasswordAttempts = i
if (prefs.isMaxFailedPasswordAttemptsWarningChecked)
try { admin.setMaximumFailedPasswordsForWipe(i) } catch (exc: SecurityException) {}
prefs.maxFailedPasswordAttempts = it?.toString()?.toIntOrNull() ?:
return@doAfterTextChanged
setMaximumFailedPasswordAttempts()
}
maxFailedPasswordAttemptsWarning.setOnCheckedChangeListener { _, isChecked ->
prefs.isMaxFailedPasswordAttemptsWarningChecked = isChecked
try {
admin.setMaximumFailedPasswordsForWipe(
if (isChecked) prefs.maxFailedPasswordAttempts else 0)
} catch (exc: SecurityException) {}
maxFailedPasswordAttemptsDefaultApi.setOnCheckedChangeListener { _, isChecked ->
prefs.isMaxFailedPasswordAttemptsDefaultApiChecked = isChecked
setMaximumFailedPasswordAttempts()
}
usbDataSignaling.setOnCheckedChangeListener { _, isChecked ->
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return@setOnCheckedChangeListener
try { admin.setUsbDataSignalingEnabled(isChecked) } catch (exc: Exception) {
try { admin.setUsbDataSignalingEnabled(isChecked) } catch (_: Exception) {
Snackbar.make(
usbDataSignaling,
R.string.usb_data_signaling_change_failed_popup,
Expand All @@ -101,19 +87,14 @@ class MainFragment : Fragment() {
}

private fun setOn() {
try {
admin.setMaximumFailedPasswordsForWipe(
if (prefs.isMaxFailedPasswordAttemptsWarningChecked) prefs.maxFailedPasswordAttempts
else 0
)
} catch (exc: SecurityException) {}
setMaximumFailedPasswordAttempts()
prefs.isEnabled = true
binding.toggle.isChecked = true
}

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

Expand All @@ -128,7 +109,11 @@ class MainFragment : Fragment() {
if (it.resultCode != Activity.RESULT_OK) setOff() else setOn()
}

private val prefsListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
prefs.copyTo(prefsdb, key)
}
private fun setMaximumFailedPasswordAttempts() = try {
admin.setMaximumFailedPasswordsForWipe(
if (prefs.isMaxFailedPasswordAttemptsDefaultApiChecked)
prefs.maxFailedPasswordAttempts
else 0
)
} catch (_: SecurityException) {}
}
Loading

0 comments on commit d0fa3c3

Please sign in to comment.