Skip to content

Modernize root and jailbreak detection and align the response on both apps #1293

Description

@0xh3rman

Both apps check for a rooted or jailbroken device at launch, but the checks date from the Cydia and SuperSU era and miss current root and jailbreak tools, and the two apps respond in opposite ways. Neither app uses platform attestation.

Current state

  • Android: android/app/src/main/kotlin/com/gemwallet/android/ui/components/RootWarningDialog.kt (RootChecker), shown by RootWarningHost from MainActivity. Checks nine fixed su paths, runs /system/xbin/which su, and looks for test-keys in Build.TAGS. Skipped in debug builds. Shows a dismissible warning (Exit / Ignore, plus a Learn more link to DocsUrl.RootedDevice). Ignore isn't saved, so the warning comes back on every launch.
  • iOS: ios/Packages/FeatureServices/AppService/UIDevice+Jailbroken.swift, called from OnstartService.validateDeviceSecurity(). Checks the cydia:// URL scheme, a list of Cydia-era app and system paths, a write outside the sandbox, loaded library names (frida, FridaGadget, cynject, libcycript), and a connection to port 27042 on the device itself. Skipped on the simulator. On a match the app calls fatalError(): it crashes with no message, and the shared rootcheck_* strings in localization/app/en.ftl are never shown.

Gaps

  • iOS URL scheme check never fires. cydia:// isn't listed in LSApplicationQueriesSchemes in ios/Gem/Resources/Info.plist (only tg, twitter, youtube), so canOpenURL always returns false.
  • iOS misses rootless jailbreaks. Dopamine and palera1n install under /var/jb and inject code with ElleKit. Neither the path nor the library is checked, and Sileo and Zebra, the package managers these jailbreaks use, aren't checked either.
  • iOS check has two small bugs. The Frida port check opens a socket and never closes it. The sandbox write test uses a relative path and doesn't delete the file if the write succeeds.
  • Android which check is mostly dead. /system/xbin/which doesn't exist on most current Android versions, so the call throws and the check reports "not rooted".
  • Android misses current root tools. Magisk, KernelSU and APatch don't use the listed su paths. Nothing checks for their mounts, Zygisk, Frida (/proc/self/maps, port 27042) or system properties such as ro.debuggable and ro.secure.
  • No attestation. Neither app uses Play Integrity, Android key attestation or App Attest, so every signal is a local check that common hiding tools (Magisk DenyList, Shamiko, RootHide) defeat.
  • The two apps disagree. Android warns and lets the user continue; iOS crashes without saying why. On iOS, a false positive looks like a crash bug to the user.

Current practice

  • OWASP MASVS-RESILIENCE-1 treats local root and jailbreak checks as friction, not as a security boundary. They work best as several independent signals, backed by server-verified attestation for anything that matters.
  • Android:
    • Play Integrity, standard requests. The token is decrypted and checked on the server. The verdicts are MEETS_DEVICE_INTEGRITY and MEETS_STRONG_INTEGRITY; on Android 13+, strong integrity also requires a security patch from the last year. Library 1.5.0+ adds remediation dialogs (GET_INTEGRITY). Play Integrity needs Google Play services, so it can't ship in the F-Droid channel.
    • Hardware key attestation works without Google Play services. The server validates the certificate chain, checks the revocation list at android.googleapis.com/attestation/status, and reads RootOfTrust.verifiedBootState and deviceLocked. A new P-384 root started signing chains on 2026-02-01, and verifiers must trust it. GrapheneOS and other locked custom OSes report a self-signed boot key, and we shouldn't treat them as compromised.
  • iOS: DCAppAttestService (App Attest) proves the request comes from our unmodified app on genuine Apple hardware, using a server challenge and later assertions. It doesn't detect jailbreaks, so it complements the local checks. For local checks, the current signals are:
    • lstat on /var/jb
    • ElleKit, Substrate and libhooker in the loaded libraries
    • DYLD_INSERT_LIBRARIES
    • writable system mounts
    • the Sileo and Zebra URL schemes (these must be listed in LSApplicationQueriesSchemes)
    • sysctl P_TRACED to detect a debugger in release builds
  • Response. The usual pattern is to warn at launch and add friction to sensitive actions such as signing, export and recovery phrase reveal, rather than blocking the app outright.

Plan

  1. Decide one policy for both apps (open questions below), then replace the iOS fatalError() with the same warning Android shows, using the existing rootcheck_* strings and DocsUrl.RootedDevice.
  2. Update the iOS local checks. Add the rootless paths and libraries, DYLD_INSERT_LIBRARIES, writable-mount and debugger checks, and fix the URL scheme allowlist. Close the Frida socket and drop or fix the relative-path write test. Decide whether to adopt IOSSecuritySuite (MIT) or keep our own code.
  3. Update the Android local checks. Replace the which call with a scan of PATH, and add checks for Magisk, KernelSU and APatch mounts, Frida, and system properties. Put the file and process lookups behind an interface we can fake so the checks are unit-testable. Any library must be free and open source to stay F-Droid compatible, which rules out closed SDKs such as freeRASP.
  4. Add attestation as a separate step (needs backend work): App Attest on iOS, Play Integrity in the google channel, and key attestation in every Android channel including fdroid. The attestation would be sent with the /v2/devices registration from docs/DEVICE_AUTHENTICATION.md. The first release only records results without acting on them, so we can measure false positives before enforcing anything.
  5. Update the rooted-device docs page to describe the chosen behavior on both platforms.

Open questions

  • Response: warn only, block at launch, or warn plus extra confirmation for signing, export and recovery phrase reveal?
  • Dismissal: should Ignore be remembered per install, or should the warning show on every launch as it does on Android today?
  • Server side: what should the backend do with attestation results? The wallet is non-custodial, so attestation mainly protects server features (rewards, referrals, subscriptions) rather than keys already on a compromised device.
  • Custom Android OSes: how should GrapheneOS and other locked custom OSes be treated when Play Integrity fails but key attestation passes?

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions