Apply one fixed baseline to every scoped app; drop all settings

Per-app settings never worked: a hooked app reads Veil's config through
XSharedPreferences, which for a legacy module points at Veil's private
data dir (/data/data/space.bdeshi.veil/shared_prefs). Hooked apps can't
read it, so every app silently fell back to "hide everything" and every
toggle, hidden-package edit and picker entry was ignored. Confirmed on
device with Veil Probe: unticking a feature changed nothing.

The framework's fix for this (xposedsharedprefs, redirecting the module's
prefs dir to one hooked apps can read) needs the framework to hook Veil's
own process. On Vector v2.2 here it never does, even with Veil in its own
scope and after a reboot, so there's no reliable channel from Veil's app
to a hooked app.

So stop pretending there is one:
- VeilHook applies all six features with the fixed Features.HIDDEN_PACKAGES
  (KernelSU, Magisk, Veil) to every app in scope, with no prefs read.
- Remove discovery: the Application#attach hook, the exported provider
  (one less way for an app to spot Veil) and the launcher <queries>.
- The settings screen becomes a read-only summary plus the help panel;
  help tips cover leaving an app out of scope, changing the list by
  rebuilding, and turning verbose logging off afterwards, since it
  names Veil in each hooked app's own logcat.

Verified on device: all 24 testable hooked paths in Veil Probe stay
hidden, Veil's provider no longer resolves, a scoped app still loads Veil.

Co-Authored-By: agent <agent@bdeshi.space>
This commit is contained in:
bdeshiandagent committed 2026-11-03 19:51:52 +06:00
1 parent af97a85c54
commit eebba2f9f1
7 files changed
+102 -576

No files matched your search

+14 -7
View File
@@ -1,7 +1,7 @@
# Veil
An LSPosed module that hides accessibility, debugging, and root/hook state from specific
apps, per app, without changing the actual state of the device.
An LSPosed module that hides accessibility, debugging, and root/hook state from the apps
in its scope, without changing the actual state of the device.
> [!IMPORTANT]
> Built entirely by AI in one evening to fix a personal problem. YMMV — it works for me
@@ -16,9 +16,8 @@ apps, per app, without changing the actual state of the device.
## Setup
1. Build (below) or grab a release APK from the repo's Releases page.
2. Enable the module in LSPosed Manager and scope it to the target app.
3. Open the target app once — it appears in Veil's own settings screen automatically.
4. Uncheck anything you don't want hidden for that app.
2. Enable the module in LSPosed Manager and scope it to the target apps.
3. Force-stop each target app; Veil applies from its next cold start.
If the target app still won't launch, check whether it's on your root manager's DenyList
(e.g. KernelSU: app profile → "Umount modules"). An app on that list never gets Zygisk
@@ -27,7 +26,7 @@ a few other gotchas.
## Features
Per app, per feature — anything not listed gets everything hidden by default.
Every app in scope gets all of these. Veil's settings screen is a read-only summary.
| Feature | Hides |
| ------------------------ | ------------------------------------------------------ |
@@ -35,12 +34,20 @@ Per app, per feature — anything not listed gets everything hidden by default.
| USB / wireless debugging | `adb_enabled` / `adb_wifi_enabled` |
| Developer options | Developer Options enabled state |
| Mock location | Legacy pre-Marshmallow flag only |
| Root / manager apps | Package list (default: KernelSU, Magisk, Veil itself) |
| Root / manager apps | Fixed list: KernelSU, Magisk, Veil itself |
| Build tags/type | Real build swapped for `release-keys` / `user` |
Doesn't cover native-level root/hook detection or remote attestation (Play Integrity,
SafetyNet) — that's your Zygisk implementation's job, not Veil's.
### Why there's no per-app configuration
A hooked app can't read anything Veil's own app saves: Veil's data dir is private to it,
and the framework's shared-prefs redirect for legacy modules only works if the framework
hooks Veil's own process, which Vector doesn't reliably do. Settings would be silently
ignored, so there are none. To change what's hidden, edit `VeilHook.kt` /
`Features.HIDDEN_PACKAGES` and rebuild. For per-app package lists, use Hide My Applist.
## Building
Requires JDK 17 and Android SDK (platform 35, build-tools 35.0.0).
-15
View File
@@ -1,15 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Lets the "Add app" picker list launchable apps, including ones that can't see Veil
and so never reach its discovery provider. This only widens what Veil can see; it
doesn't make Veil any more visible to the apps it hides things from. -->
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
@@ -39,10 +29,5 @@
</intent-filter>
</activity>
<provider
android:name=".VeilProvider"
android:authorities="space.bdeshi.veil.provider"
android:exported="true" />
</application>
</manifest>
+22 -46
View File
@@ -4,64 +4,40 @@ data class FeatureInfo(val label: String, val description: String)
object Features {
const val SELF_PACKAGE = "space.bdeshi.veil"
const val PREFS_NAME = "veil_prefs"
const val KEY_PREFIX = "features_"
const val HIDDEN_PACKAGES_KEY = "hidden_packages"
const val DISCOVERED_PACKAGES_KEY = "discovered_packages"
const val SELF_HIDDEN_MIGRATED_KEY = "self_hidden_migrated"
const val PROVIDER_AUTHORITY = "space.bdeshi.veil.provider"
const val ACCESSIBILITY = "accessibility"
const val ADB = "adb"
const val DEV_OPTIONS = "dev_options"
const val MOCK_LOCATION = "mock_location"
const val ROOT_APPS = "root_apps"
const val BUILD_TAGS = "build_tags"
val ALL: Set<String> = setOf(
ACCESSIBILITY,
ADB,
DEV_OPTIONS,
MOCK_LOCATION,
ROOT_APPS,
BUILD_TAGS,
)
val INFO: LinkedHashMap<String, FeatureInfo> = linkedMapOf(
ACCESSIBILITY to FeatureInfo(
"Hide accessibility services",
"This app will see zero accessibility services running, even if you have some enabled.",
// Shown in the settings screen. Every one of these is always applied — see VeilHook.
val INFO: List<FeatureInfo> = listOf(
FeatureInfo(
"Accessibility services",
"Apps see zero accessibility services running, even if you have some enabled.",
),
ADB to FeatureInfo(
"Hide USB / wireless debugging",
"This app will see debugging as off, regardless of your actual setting.",
FeatureInfo(
"USB / wireless debugging",
"Apps see debugging as off, regardless of your actual setting.",
),
DEV_OPTIONS to FeatureInfo(
"Hide developer options",
"This app will see Developer Options as disabled.",
FeatureInfo(
"Developer options",
"Apps see Developer Options as disabled.",
),
MOCK_LOCATION to FeatureInfo(
"Hide mock location (legacy flag)",
FeatureInfo(
"Mock location (legacy flag)",
"Only covers the old pre-Android-6 system flag — modern per-app mock location isn't covered.",
),
ROOT_APPS to FeatureInfo(
"Hide root / manager apps",
"Apps in the \"hidden packages\" list below become invisible to this app's PackageManager lookups.",
FeatureInfo(
"Root / manager apps",
"The packages below are invisible to apps' PackageManager lookups.",
),
BUILD_TAGS to FeatureInfo(
"Hide test-keys / userdebug build",
"This app will see build tags as release-keys/user instead of your real build.",
FeatureInfo(
"Test-keys / userdebug build",
"Apps see build tags as release-keys/user instead of your real build.",
),
)
// Veil itself belongs here too: an installed hooking module is as much a tell as a root
// manager. Hiding it from PackageManager doesn't affect discovery, which resolves Veil's
// provider through ActivityManager rather than the hooked ApplicationPackageManager.
val DEFAULT_HIDDEN_PACKAGES: Set<String> = setOf(
// Fixed at build time; edit and rebuild to change. Veil itself is here too: an installed
// hooking module is as much a tell as a root manager.
val HIDDEN_PACKAGES: Set<String> = setOf(
"me.weishu.kernelsu",
"com.topjohnwu.magisk",
SELF_PACKAGE,
)
fun keyFor(packageName: String): String = KEY_PREFIX + packageName
}
@@ -1,75 +1,49 @@
package space.bdeshi.veil
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Typeface
import android.os.Bundle
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.ListView
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.checkbox.MaterialCheckBox
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import java.io.File
private data class LaunchableApp(val label: String, val pkg: String)
private val HELP_TIPS: List<Pair<String, String>> = listOf(
"Target app still won't launch?" to
"Check KernelSU Manager → the target app's profile → make sure \"Umount modules\" " +
"is OFF for it. When that's on, Zygisk (and therefore LSPosed) skips loading into that " +
"app's process entirely — no Veil setting can work around that, since the module " +
"never gets injected in the first place.",
"app's process entirely — Veil never gets injected in the first place.",
"Using a separate Zygisk implementation?" to
"Standalone Zygisk modules (e.g. Zygisk Next / ReZygisk) can carry their own independent " +
"DenyList on top of KernelSU's. If an app is still being skipped after fixing the above, " +
"check there too.",
"Scoped app doesn't show up here?" to
"Android 11+ only lets an app reach packages it can see, and most apps can't see " +
"Veil, so their first-launch check-in never arrives. Veil still hides everything from " +
"them by default; use \"Add app\" to give one its own settings.",
"Changed a setting but nothing happened?" to
"Force-stop and reopen the target app. Veil reads its config once when that app's " +
"process starts, so changes here (or to LSPosed scope) only take effect on the next " +
"cold start — a full device reboot usually isn't necessary.",
"Want something left visible to one app?" to
"Take that app out of Veil's scope in LSPosed Manager. There's no per-app configuration: " +
"hooked apps can't read anything this screen could save, so it would be silently ignored.",
"Need a different package list?" to
"It's fixed in this build: edit Features.HIDDEN_PACKAGES and rebuild. For per-app package " +
"lists, Hide My Applist is built for exactly that.",
"Scope changed but nothing happened?" to
"Force-stop and reopen the target app. Veil is applied when that app's process starts, " +
"so scope changes only take effect on its next cold start.",
"How to confirm it's actually working" to
"Turn on verbose logging in LSPosed Manager, then check its Logs tab after relaunching " +
"the target app. No log lines from Veil there usually means it never got injected " +
"(see the DenyList tip above), not a bug in this module.",
"Forgot what an unconfigured app does?" to
"Any app not in the list below gets every feature applied by default — check here " +
"first if something's being hidden that you didn't expect.",
"(see the DenyList tip above). Turn verbose logging off again afterwards: it writes a " +
"line naming Veil into each hooked app's own logcat, which that app can read.",
)
class SettingsActivity : AppCompatActivity() {
private lateinit var prefs: SharedPreferences
private lateinit var appListContainer: LinearLayout
private lateinit var hiddenPackagesChips: ChipGroup
private lateinit var helpContainer: View
private lateinit var helpToggle: MaterialButton
private lateinit var hiddenPackageInput: TextInputEditText
private var helpVisible = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = getSharedPreferences(Features.PREFS_NAME, Context.MODE_PRIVATE)
migrateSelfIntoHiddenList()
val padding = dp(16)
val root = LinearLayout(this).apply {
@@ -83,21 +57,20 @@ class SettingsActivity : AppCompatActivity() {
setTypeface(typeface, Typeface.BOLD)
})
root.addView(TextView(this).apply {
text = "Hide device state from specific apps, per app."
text = "Hides the same things from every app in Veil's LSPosed scope."
textSize = 14f
alpha = 0.7f
setPadding(0, dp(2), 0, dp(16))
})
helpToggle = MaterialButton(this).apply {
root.addView(MaterialButton(this).apply {
text = "Help & troubleshooting ▾"
setOnClickListener {
helpVisible = !helpVisible
helpContainer.visibility = if (helpVisible) View.VISIBLE else View.GONE
text = if (helpVisible) "Help & troubleshooting ▴" else "Help & troubleshooting ▾"
}
}
root.addView(helpToggle)
})
val (helpCard, helpContent) = card()
helpCard.visibility = View.GONE
@@ -116,57 +89,40 @@ class SettingsActivity : AppCompatActivity() {
}
root.addView(helpCard, marginTop(dp(8)))
root.addView(sectionTitle("Apps"))
root.addView(TextView(this).apply {
text = "Scope this module to a target app in LSPosed Manager, then open that app " +
"once — it shows up here automatically if it can see Veil. If it doesn't, add " +
"it below. Apps not listed here get everything hidden by default."
textSize = 13f
alpha = 0.8f
setPadding(0, dp(4), 0, dp(8))
})
root.addView(MaterialButton(this).apply {
text = "Add app"
setOnClickListener { showAppPicker() }
})
appListContainer = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(0, dp(8), 0, 0)
root.addView(sectionTitle("What's hidden"), marginTop(dp(16)))
val (featureCard, featureContent) = card()
for (info in Features.INFO) {
featureContent.addView(TextView(this).apply {
text = info.label
setTypeface(typeface, Typeface.BOLD)
setPadding(0, dp(6), 0, dp(2))
})
featureContent.addView(TextView(this).apply {
text = info.description
textSize = 13f
alpha = 0.7f
})
}
root.addView(appListContainer)
root.addView(featureCard, marginTop(dp(8)))
root.addView(sectionTitle("Packages hidden from PackageManager"), marginTop(dp(24)))
root.addView(sectionTitle("Hidden packages"), marginTop(dp(16)))
root.addView(TextView(this).apply {
text = "Used by the \"Hide root / manager apps\" checkbox above — apps here become " +
"invisible to PackageManager lookups for any target app with that box checked."
text = "Fixed in this build — edit Features.HIDDEN_PACKAGES and rebuild to change."
textSize = 13f
alpha = 0.8f
setPadding(0, dp(4), 0, dp(8))
})
hiddenPackageInput = textInput("e.g. me.weishu.kernelsu")
root.addView(addRow(hiddenPackageInput) {
val pkg = hiddenPackageInput.text.toString().trim()
if (pkg.isNotEmpty() && pkg.contains(".")) {
val current = hiddenPackages().toMutableSet()
current.add(pkg)
prefs.edit().putStringSet(Features.HIDDEN_PACKAGES_KEY, current).apply()
fixPermissions()
hiddenPackageInput.setText("")
refreshHiddenPackages()
root.addView(ChipGroup(this).apply {
for (pkg in Features.HIDDEN_PACKAGES.sorted()) {
addView(Chip(this@SettingsActivity).apply {
text = pkg
isClickable = false
isCheckable = false
})
}
})
hiddenPackagesChips = ChipGroup(this).apply {
setPadding(0, dp(8), 0, 0)
}
root.addView(hiddenPackagesChips)
setContentView(ScrollView(this).apply { addView(root); fitsSystemWindows = true })
refreshAppList()
refreshHiddenPackages()
}
private fun sectionTitle(text: String): TextView = TextView(this).apply {
@@ -180,31 +136,7 @@ class SettingsActivity : AppCompatActivity() {
topMargin = px
}
private fun textInput(hintText: String): TextInputEditText {
val editText = TextInputEditText(this).apply {
hint = hintText
inputType = InputType.TYPE_CLASS_TEXT
}
return editText
}
private fun addRow(input: TextInputEditText, onAdd: () -> Unit): View {
val layout = TextInputLayout(this).apply {
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
addView(input)
}
return LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
addView(layout)
addView(MaterialButton(this@SettingsActivity).apply {
text = "Add"
setOnClickListener { onAdd() }
})
}
}
/** A MaterialCardView with an inner padded vertical container, added to root with bottom margin. */
/** A MaterialCardView with an inner padded vertical container. */
private fun card(): Pair<MaterialCardView, LinearLayout> {
val content = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
@@ -216,234 +148,11 @@ class SettingsActivity : AppCompatActivity() {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
).apply { bottomMargin = dp(12) }
)
addView(content)
}
return cardView to content
}
// A list saved before Veil joined DEFAULT_HIDDEN_PACKAGES never picks up the new default,
// so add it once. The flag keeps a later manual removal from being undone.
private fun migrateSelfIntoHiddenList() {
if (prefs.getBoolean(Features.SELF_HIDDEN_MIGRATED_KEY, false)) return
val editor = prefs.edit().putBoolean(Features.SELF_HIDDEN_MIGRATED_KEY, true)
if (prefs.contains(Features.HIDDEN_PACKAGES_KEY)) {
editor.putStringSet(Features.HIDDEN_PACKAGES_KEY, hiddenPackages() + Features.SELF_PACKAGE)
}
editor.apply()
fixPermissions()
}
private fun hiddenPackages(): Set<String> =
prefs.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES) ?: Features.DEFAULT_HIDDEN_PACKAGES
private fun configuredPackages(): Set<String> =
prefs.all.keys.filter { it.startsWith(Features.KEY_PREFIX) }.map { it.removePrefix(Features.KEY_PREFIX) }.toSet()
private fun discoveredPackages(): Set<String> =
prefs.getStringSet(Features.DISCOVERED_PACKAGES_KEY, emptySet()) ?: emptySet()
private fun refreshAppList() {
appListContainer.removeAllViews()
val configured = configuredPackages()
val packages = (configured + discoveredPackages()).sorted()
if (packages.isEmpty()) {
appListContainer.addView(TextView(this).apply {
text = "No apps yet — scope Veil to a target app in LSPosed Manager and open " +
"it once, or add one with \"Add app\"."
textSize = 13f
alpha = 0.8f
})
return
}
for (pkg in packages) {
appListContainer.addView(buildPackageCard(pkg, isDiscoveredOnly = pkg !in configured))
}
}
private fun buildPackageCard(pkg: String, isDiscoveredOnly: Boolean): View {
val enabled = prefs.getStringSet(Features.keyFor(pkg), Features.ALL) ?: Features.ALL
val (cardView, content) = card()
val titleRow = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}
val label = appLabel(pkg)
val titles = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
titles.addView(TextView(this).apply {
text = label ?: pkg
textSize = 16f
setTypeface(typeface, Typeface.BOLD)
})
if (label != null) {
titles.addView(TextView(this).apply {
text = pkg
textSize = 12f
alpha = 0.7f
})
}
titleRow.addView(titles)
if (isDiscoveredOnly) {
titleRow.addView(Chip(this).apply {
text = "Auto-detected"
isClickable = false
isCheckable = false
})
}
content.addView(titleRow)
content.addView(TextView(this).apply {
text = "Checking a box below hides that thing from this app specifically — it " +
"stays on for every other app."
textSize = 12f
alpha = 0.7f
setPadding(0, dp(2), 0, dp(8))
})
for ((id, info) in Features.INFO) {
content.addView(MaterialCheckBox(this).apply {
text = info.label
isChecked = id in enabled
setOnCheckedChangeListener { _, isChecked ->
val current = (prefs.getStringSet(Features.keyFor(pkg), Features.ALL) ?: Features.ALL).toMutableSet()
if (isChecked) current.add(id) else current.remove(id)
prefs.edit().putStringSet(Features.keyFor(pkg), current).apply()
fixPermissions()
}
})
content.addView(TextView(this).apply {
text = info.description
textSize = 13f
alpha = 0.7f
setPadding(dp(32), 0, 0, dp(8))
})
}
content.addView(MaterialButton(this).apply {
text = "Apply defaults (hide everything)"
setOnClickListener {
prefs.edit().remove(Features.keyFor(pkg)).apply()
fixPermissions()
refreshAppList()
}
})
return cardView
}
// Null when Veil can't see the package: only launchable apps are in its <queries>.
private fun appLabel(pkg: String): String? = try {
packageManager.getApplicationInfo(pkg, 0).loadLabel(packageManager).toString()
} catch (e: Exception) {
null
}
private fun launchableApps(): List<LaunchableApp> {
val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
return packageManager.queryIntentActivities(intent, 0)
.distinctBy { it.activityInfo.packageName }
.map { LaunchableApp(it.loadLabel(packageManager).toString(), it.activityInfo.packageName) }
.sortedBy { it.label.lowercase() }
}
// Loading labels for every launchable app takes a moment, so it runs off the UI thread.
private fun showAppPicker() {
Thread {
val listed = configuredPackages() + discoveredPackages()
val apps = launchableApps().filter { it.pkg !in listed }
runOnUiThread {
if (!isFinishing) showAppPickerDialog(apps)
}
}.start()
}
private fun showAppPickerDialog(apps: List<LaunchableApp>) {
val adapter = object : ArrayAdapter<LaunchableApp>(
this, android.R.layout.simple_list_item_2, android.R.id.text1, apps.toMutableList(),
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val app = getItem(position) ?: return view
view.findViewById<TextView>(android.R.id.text1).text = app.label
view.findViewById<TextView>(android.R.id.text2).text = app.pkg
return view
}
}
val search = textInput("Search apps")
search.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val query = s.toString().trim()
adapter.clear()
adapter.addAll(apps.filter {
it.label.contains(query, ignoreCase = true) || it.pkg.contains(query, ignoreCase = true)
})
}
})
val list = ListView(this).apply { this.adapter = adapter }
val body = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(24), dp(8), dp(24), 0)
addView(TextInputLayout(this@SettingsActivity).apply { addView(search) })
addView(list, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, dp(360)))
}
val dialog = MaterialAlertDialogBuilder(this)
.setTitle("Add app")
.setView(body)
.setNegativeButton("Cancel", null)
.create()
// Saving the default feature set is what makes an app "configured", so it gets a card
// whether or not it ever checks in.
list.setOnItemClickListener { _, _, position, _ ->
val app = adapter.getItem(position) ?: return@setOnItemClickListener
prefs.edit().putStringSet(Features.keyFor(app.pkg), Features.ALL).apply()
fixPermissions()
refreshAppList()
dialog.dismiss()
}
dialog.show()
}
private fun refreshHiddenPackages() {
hiddenPackagesChips.removeAllViews()
for (pkg in hiddenPackages().sorted()) {
hiddenPackagesChips.addView(Chip(this).apply {
text = pkg
isCloseIconVisible = true
setOnCloseIconClickListener {
val current = hiddenPackages().toMutableSet()
current.remove(pkg)
prefs.edit().putStringSet(Features.HIDDEN_PACKAGES_KEY, current).apply()
fixPermissions()
refreshHiddenPackages()
}
})
}
}
// LSPosed's own SELinux policy is what actually lets hooked processes read this file;
// this just ensures the DAC bits don't block it too.
private fun fixPermissions() {
try {
val prefsDir = File(applicationInfo.dataDir, "shared_prefs")
prefsDir.setReadable(true, false)
prefsDir.setExecutable(true, false)
prefsDir.listFiles()?.forEach { it.setReadable(true, false) }
} catch (t: Throwable) {
// Best-effort.
}
}
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
}
+25 -114
View File
@@ -1,139 +1,50 @@
package space.bdeshi.veil
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XSharedPreferences
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
// Keyed by the Settings class each key actually lives on, so we only ever install hooks on
// the one class that could plausibly be asked about a given feature — never both.
private val SECURE_NULL_STRING_KEYS_BY_FEATURE = mapOf(
Features.ACCESSIBILITY to setOf("enabled_accessibility_services"),
// the one class that could plausibly be asked about a given key — never both.
private val SECURE_NULL_STRING_KEYS = setOf("enabled_accessibility_services")
private val SECURE_ZERO_VALUE_KEYS = setOf(
"accessibility_enabled",
"touch_exploration_enabled",
"mock_location",
"allow_mock_location",
)
private val SECURE_ZERO_VALUE_KEYS_BY_FEATURE = mapOf(
Features.ACCESSIBILITY to setOf("accessibility_enabled", "touch_exploration_enabled"),
Features.MOCK_LOCATION to setOf("mock_location", "allow_mock_location"),
private val GLOBAL_ZERO_VALUE_KEYS = setOf(
"adb_enabled",
"adb_wifi_enabled",
"development_settings_enabled",
)
private val GLOBAL_ZERO_VALUE_KEYS_BY_FEATURE = mapOf(
Features.ADB to setOf("adb_enabled", "adb_wifi_enabled"),
Features.DEV_OPTIONS to setOf("development_settings_enabled"),
)
private fun keysForEnabledFeatures(
enabled: Set<String>,
byFeature: Map<String, Set<String>>,
): Set<String> {
if (enabled.isEmpty() || byFeature.isEmpty()) return emptySet()
val result = mutableSetOf<String>()
for (feature in enabled) {
byFeature[feature]?.let { result.addAll(it) }
}
return result
}
/**
* Applies the same fixed baseline to every app in Veil's LSPosed scope. There is deliberately
* no per-app configuration: a hooked app can't read anything Veil's own app saves (its data
* dir is private to it, and the framework's shared-prefs redirect for legacy modules depends
* on the framework hooking Veil's own process, which Vector doesn't reliably do), so settings
* would silently be ignored. What's hidden lives in this file and Features.HIDDEN_PACKAGES;
* change it there and rebuild.
*/
class VeilHook : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: LoadPackageParam) {
if (lpparam.packageName == Features.SELF_PACKAGE) return
reportScoped(lpparam)
val prefs = openPrefs()
val enabled = readEnabledFeatures(prefs, lpparam.packageName)
if (enabled.isEmpty()) return
if (Features.ACCESSIBILITY in enabled) {
hookAccessibilityManager(lpparam)
}
val secureNullKeys = keysForEnabledFeatures(enabled, SECURE_NULL_STRING_KEYS_BY_FEATURE)
val secureZeroKeys = keysForEnabledFeatures(enabled, SECURE_ZERO_VALUE_KEYS_BY_FEATURE)
if (secureNullKeys.isNotEmpty() || secureZeroKeys.isNotEmpty()) {
hookSettingsClass("android.provider.Settings\$Secure", lpparam, secureNullKeys, secureZeroKeys)
}
val globalZeroKeys = keysForEnabledFeatures(enabled, GLOBAL_ZERO_VALUE_KEYS_BY_FEATURE)
if (globalZeroKeys.isNotEmpty()) {
hookSettingsClass("android.provider.Settings\$Global", lpparam, emptySet(), globalZeroKeys)
}
if (Features.ROOT_APPS in enabled) {
val hiddenPackages = readHiddenPackages(prefs)
if (hiddenPackages.isNotEmpty()) {
hookPackageManager(lpparam, hiddenPackages)
}
}
if (Features.BUILD_TAGS in enabled) {
spoofBuildTags()
}
}
// Lets Veil's own settings screen list this app automatically, without the user re-typing
// a package name it can already see was actually injected. Application#attach is the
// earliest reliable point at which a real Context exists in the hooked process.
private fun reportScoped(lpparam: LoadPackageParam) {
try {
XposedHelpers.findAndHookMethod(
"android.app.Application",
lpparam.classLoader,
"attach",
Context::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
try {
val context = param.args[0] as Context
// Most apps can't see Veil (Android 11+ package visibility), and
// then can't reach its provider either. Checking first skips the
// doomed call, and with it the "Failed to find provider info"
// line naming Veil that the framework would write to the app's
// own, app-readable logcat. Such apps are added via the picker.
if (context.packageManager.resolveContentProvider(Features.PROVIDER_AUTHORITY, 0) == null) {
return
}
context.contentResolver.call(
Uri.parse("content://${Features.PROVIDER_AUTHORITY}"),
"register",
lpparam.packageName,
null,
)
} catch (t: Throwable) {
// Veil app unreachable — doesn't affect the hiding hooks below.
}
}
},
)
} catch (t: Throwable) {
// Application#attach signature not present — safe to skip discovery only.
}
}
private fun openPrefs(): XSharedPreferences? {
return try {
XSharedPreferences(Features.SELF_PACKAGE, Features.PREFS_NAME).apply { reload() }
} catch (t: Throwable) {
null
}
}
private fun readEnabledFeatures(prefs: XSharedPreferences?, packageName: String): Set<String> {
// Config unreadable (never configured, or permissions not yet fixed) — fail open to "hide everything".
return prefs?.getStringSet(Features.keyFor(packageName), Features.ALL) ?: Features.ALL
}
private fun readHiddenPackages(prefs: XSharedPreferences?): Set<String> {
return prefs?.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES)
?: Features.DEFAULT_HIDDEN_PACKAGES
hookAccessibilityManager(lpparam)
hookSettingsClass("android.provider.Settings\$Secure", lpparam, SECURE_NULL_STRING_KEYS, SECURE_ZERO_VALUE_KEYS)
hookSettingsClass("android.provider.Settings\$Global", lpparam, emptySet(), GLOBAL_ZERO_VALUE_KEYS)
hookPackageManager(lpparam, Features.HIDDEN_PACKAGES)
spoofBuildTags()
}
private fun hookAccessibilityManager(lpparam: LoadPackageParam) {
@@ -1,62 +0,0 @@
package space.bdeshi.veil
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import java.io.File
/**
* Exists only so hooked app processes can report "LSPosed loaded me into package X" back to
* Veil's own settings screen — cross-process writes to another app's SharedPreferences aren't
* possible any other way. No persistent service; the OS starts this app briefly on demand for
* each call() and it's free to be killed again right after.
*/
class VeilProvider : ContentProvider() {
override fun onCreate(): Boolean = true
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
if (method == "register" && !arg.isNullOrBlank()) {
registerDiscovered(arg)
}
return null
}
private fun registerDiscovered(packageName: String) {
val ctx = context ?: return
val prefs = ctx.getSharedPreferences(Features.PREFS_NAME, Context.MODE_PRIVATE)
val current = prefs.getStringSet(Features.DISCOVERED_PACKAGES_KEY, emptySet()) ?: emptySet()
if (packageName !in current) {
prefs.edit().putStringSet(Features.DISCOVERED_PACKAGES_KEY, current + packageName).apply()
fixPermissions(ctx)
}
}
private fun fixPermissions(ctx: Context) {
try {
val prefsDir = File(ctx.applicationInfo.dataDir, "shared_prefs")
prefsDir.setReadable(true, false)
prefsDir.setExecutable(true, false)
prefsDir.listFiles()?.forEach { it.setReadable(true, false) }
} catch (t: Throwable) {
// Best-effort.
}
}
// No actual table behind this provider — it exists purely for call().
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?,
): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0
}
+1 -1
View File
@@ -1,4 +1,4 @@
<resources>
<string name="app_name">Veil</string>
<string name="module_description">Hides accessibility, debugging, and root/hook state from apps selected per-package in the settings screen</string>
<string name="module_description">Hides accessibility, debugging, and root/hook state from every app in its scope</string>
</resources>