Skip to content

Commit

Permalink
add wipe mode
Browse files Browse the repository at this point in the history
  • Loading branch information
x13a authored and lucky committed Jun 27, 2022
1 parent cd358b8 commit 326e3c7
Show file tree
Hide file tree
Showing 21 changed files with 288 additions and 85 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/gradle-wrapper-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: "Validate Gradle Wrapper"
on: [push, pull_request]

jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ Duress password trigger.
height="30%">

Tiny app to listen for a duress password on the lockscreen.
When found, the app will send a broadcast message to the selected receiver.
When found, it can send a broadcast message to the selected receiver or wipe the device.

## Wasted

You have to set:

* Action: `me.lucky.wasted.action.TRIGGER`
* Receiver: `me.lucky.wasted/.TriggerReceiver`
* Authentication code: a code from Wasted
* Password length: your actual password len plus at least two!

Do not forget to activate `Broadcast` trigger in Wasted.

## Permissions

* ACCESSIBILITY - listen for a duress password
* ACCESSIBILITY - listen for a duress password on the lockscreen
* DEVICE_ADMIN - wipe the device (optional)

## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html)
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@

## Reporting a Vulnerability

Contact: mailto:[email protected]
Contact: [email protected]
12 changes: 6 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId "me.lucky.duress"
minSdk 23
targetSdk 32
versionCode 2
versionName "1.0.1"
versionCode 3
versionName "1.0.2"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -39,10 +39,10 @@ android {
}

dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
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'
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="me.lucky.duress">

<uses-feature android:name="android.software.device_admin" android:required="false" />

<application
android:allowBackup="false"
android:fullBackupContent="false"
Expand Down Expand Up @@ -36,5 +38,16 @@
android:resource="@xml/accessibility_service_config" />
</service>

<receiver
android:name=".DeviceAdminReceiver"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
<meta-data android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>

</application>
</manifest>
32 changes: 25 additions & 7 deletions app/src/main/java/me/lucky/duress/AccessibilityService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class AccessibilityService : AccessibilityService() {
}

private lateinit var prefs: Preferences
private val admin by lazy { DeviceAdminManager(this) }
private var keyguardManager: KeyguardManager? = null

override fun onCreate() {
Expand All @@ -25,27 +26,44 @@ class AccessibilityService : AccessibilityService() {
}

override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (event?.isPassword != true ||
if (event?.isEnabled != true ||
!event.isPassword ||
keyguardManager?.isDeviceLocked != true ||
!prefs.isServiceEnabled) return
!prefs.isEnabled) return
val passwordLen = prefs.passwordLen
if (passwordLen < MIN_PASSWORD_LEN ||
event.text.size != 1 ||
event.text[0].length < passwordLen) return
if (prefs.mode == Mode.WIPE.value) {
wipeData()
return
}
val action = prefs.action
if (action.isBlank()) return
if (action.isEmpty()) return
sendBroadcast(action)
}

override fun onInterrupt() {}

private fun sendBroadcast(action: String) {
sendBroadcast(Intent(action).apply {
val cls = prefs.receiver.split('/')
val packageName = cls.firstOrNull() ?: ""
if (packageName.isNotBlank()) {
if (packageName.isNotEmpty()) {
setPackage(packageName)
if (cls.size == 2) setClassName(packageName, "$packageName.${cls[1]}")
if (cls.size == 2)
setClassName(
packageName,
"$packageName.${cls[1].trimStart('.')}",
)
}
addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES)
val code = prefs.authenticationCode
if (code.isNotBlank()) putExtra(KEY, code)
if (code.isNotEmpty()) putExtra(KEY, code)
})
}

override fun onInterrupt() {}
private fun wipeData() {
try { admin.wipeData() } catch (exc: SecurityException) {}
}
}
26 changes: 26 additions & 0 deletions app/src/main/java/me/lucky/duress/DeviceAdminManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package me.lucky.duress

import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build

class DeviceAdminManager(private val ctx: Context) {
private val dpm = ctx.getSystemService(DevicePolicyManager::class.java)
private val deviceAdmin by lazy { ComponentName(ctx, DeviceAdminReceiver::class.java) }

fun remove() = dpm?.removeActiveAdmin(deviceAdmin)
fun isActive() = dpm?.isAdminActive(deviceAdmin) ?: false

fun wipeData() {
var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
flags = flags.or(DevicePolicyManager.WIPE_SILENTLY)
dpm?.wipeData(flags)
}

fun makeRequestIntent() =
Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, deviceAdmin)
}
15 changes: 15 additions & 0 deletions app/src/main/java/me/lucky/duress/DeviceAdminReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package me.lucky.duress

import android.app.admin.DeviceAdminReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class DeviceAdminReceiver : DeviceAdminReceiver() {
override fun onDisabled(context: Context, intent: Intent) {
super.onDisabled(context, intent)
val prefs = Preferences(context)
if (prefs.isEnabled && prefs.mode == Mode.WIPE.value)
Toast.makeText(context, R.string.service_unavailable_popup, Toast.LENGTH_SHORT).show()
}
}
88 changes: 74 additions & 14 deletions app/src/main/java/me/lucky/duress/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.view.accessibility.AccessibilityManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout

import me.lucky.duress.databinding.ActivityMainBinding
import java.util.regex.Pattern

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var prefs: Preferences
private val admin by lazy { DeviceAdminManager(this) }
private var accessibilityManager: AccessibilityManager? = null
private val actionPatternRegex by lazy { Pattern.compile("^\\w+(\\.\\w+)+\$") }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -37,24 +38,37 @@ class MainActivity : AppCompatActivity() {
prefs = Preferences(this)
accessibilityManager = getSystemService(AccessibilityManager::class.java)
binding.apply {
tabs.selectTab(tabs.getTabAt(prefs.mode))
action.editText?.setText(prefs.action)
receiver.editText?.setText(prefs.receiver)
authenticationCode.editText?.setText(prefs.authenticationCode)
passwordLen.editText?.setText(prefs.passwordLen.toString())
toggle.isChecked = prefs.isServiceEnabled
toggle.isChecked = prefs.isEnabled
}
selectInterface()
}

private fun setup() {
binding.apply {
action.editText?.doAfterTextChanged {
val str = it?.toString() ?: ""
if (actionPatternRegex.matcher(str).matches()) {
prefs.action = str
action.error = null
} else {
action.error = getString(R.string.action_error)
tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
if (tab == null) return
setOff()
for (m in Mode.values()) {
if (m.value == tab.position) {
prefs.mode = m.value
break
}
}
selectInterface()
}

override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {}

})
action.editText?.doAfterTextChanged {
prefs.action = it?.toString() ?: ""
}
receiver.editText?.doAfterTextChanged {
prefs.receiver = it?.toString() ?: ""
Expand All @@ -68,15 +82,39 @@ class MainActivity : AppCompatActivity() {
} catch (exc: NumberFormatException) {}
}
toggle.setOnCheckedChangeListener { _, isChecked ->
prefs.isServiceEnabled = isChecked
if (isChecked && !hasPermissions())
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
if (isChecked && !hasPermissions()) {
toggle.isChecked = false
requestPermissions()
return@setOnCheckedChangeListener
}
prefs.isEnabled = isChecked
}
}
}
private fun selectInterface() {
val v = when (prefs.mode) {
Mode.BROADCAST.value -> View.VISIBLE
Mode.WIPE.value -> View.GONE
else -> View.GONE
}
binding.apply {
action.visibility = v
receiver.visibility = v
authenticationCode.visibility = v
space1.visibility = v
space2.visibility = v
space3.visibility = v
}
}

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

private fun update() {
if (prefs.isServiceEnabled && !hasPermissions())
if (prefs.isEnabled && !hasPermissions())
Snackbar.make(
binding.toggle,
R.string.service_unavailable_popup,
Expand All @@ -97,12 +135,34 @@ class MainActivity : AppCompatActivity() {
.show()
}

private fun requestPermissions() {
if (!hasAccessibilityPermission()) {
requestAccessibilityPermission()
return
}
if (prefs.mode == Mode.WIPE.value && !hasAdminPermission()) requestAdminPermission()
}

private fun requestAccessibilityPermission() =
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))

private fun requestAdminPermission() = startActivity(admin.makeRequestIntent())

private fun hasPermissions(): Boolean {
var ok = hasAccessibilityPermission()
if (prefs.mode == Mode.WIPE.value)
ok = ok && hasAdminPermission()
return ok
}

private fun hasAccessibilityPermission(): Boolean {
for (info in accessibilityManager?.getEnabledAccessibilityServiceList(
AccessibilityServiceInfo.FEEDBACK_GENERIC,
) ?: return true) {
if (info.resolveInfo.serviceInfo.packageName == packageName) return true
}
return false
}

private fun hasAdminPermission() = admin.isActive()
}
20 changes: 16 additions & 4 deletions app/src/main/java/me/lucky/duress/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import androidx.security.crypto.MasterKeys

class Preferences(ctx: Context) {
companion object {
private const val SERVICE_ENABLED = "service_enabled"
private const val ENABLED = "enabled"
private const val MODE = "mode"
private const val ACTION = "action"
private const val RECEIVER = "receiver"
private const val AUTHENTICATION_CODE = "authentication_code"
private const val PASSWORD_LEN = "password_len"
private const val SHOW_PROMINENT_DISCLOSURE = "show_prominent_disclosure"

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

private val mk = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
Expand All @@ -26,9 +29,13 @@ class Preferences(ctx: Context) {
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)

var isServiceEnabled: Boolean
get() = prefs.getBoolean(SERVICE_ENABLED, false)
set(value) = prefs.edit { putBoolean(SERVICE_ENABLED, value) }
var isEnabled: Boolean
get() = prefs.getBoolean(ENABLED, prefs.getBoolean(SERVICE_ENABLED, false))
set(value) = prefs.edit { putBoolean(ENABLED, value) }

var mode: Int
get() = prefs.getInt(MODE, Mode.BROADCAST.value)
set(value) = prefs.edit { putInt(MODE, value) }

var action: String
get() = prefs.getString(ACTION, "") ?: ""
Expand All @@ -50,3 +57,8 @@ class Preferences(ctx: Context) {
get() = prefs.getBoolean(SHOW_PROMINENT_DISCLOSURE, true)
set(value) = prefs.edit { putBoolean(SHOW_PROMINENT_DISCLOSURE, value) }
}

enum class Mode(val value: Int) {
BROADCAST(0),
WIPE(1),
}
Loading

0 comments on commit 326e3c7

Please sign in to comment.