Environment-integrity evidence for Android apps.
Root, runtime instrumentation, repackaging, debuggers and emulators — reported as evidence your app can weigh, not a verdict it has to accept.
The Android library has no third-party dependencies — the Kotlin standard library and nothing
else. No permissions. No Google Play requirement. minSdk 24.
implementation("io.github.rootect:rootect-core:1.0.0")val report = Rootect.analyze(context)
report.risk // SAFE | LOW | MEDIUM | HIGH | CRITICAL
report.score // 0..100
report.signals // the evidence| Root | Magisk, KernelSU, APatch — artefacts, mounts, and kernel-side probes |
| Instrumentation | Frida, objection, Xposed / LSPosed, and inline hooks in our own code |
| App integrity | Repackaging, resigning, debuggable builds, unexpected installers |
| Environment | Unlocked bootloader, permissive SELinux, emulators, debuggers |
| Hardware attestation | A signed statement from the device's secure element, for your server to verify |
Checks live in native code behind raw syscalls with no readable strings, because a check written in Kotlin can be switched off in a single line.
Rootect runs inside your app, on a device the attacker may own. No on-device check survives an attacker with enough privilege, and any library claiming otherwise is overselling. What Rootect does is raise the cost of a bypass from trivial to genuinely difficult, and give you one signal that does not depend on the device being honest:
val challenge = api.requestChallenge() // from your server, used once
val chain = RootectAttestation.chain(challenge) // signed by secure hardware
api.submitAttestation(challenge, chain, Rootect.analyze(context)) // server gates on the chainThe signing key normally stays in hardware. Your server validates the chain, revocation, freshness and app identity before using it as evidence. How it works, and how to verify it →
- Actively hidden root (DenyList + Shamiko) defeats the filesystem signals
isRootedcan readfalseon a rooted device — useriskorscore, and see scoring.md- Attestation can be forged with leaked hardware keys; revocation checking catches the known ones
- Rooted is not the same as malicious, and blocking those users is a product decision with a real cost
Every signal is documented with what defeats it in signals.md, including three ideas that were built, measured, and did not work.
| integration.md | Full API, configuration, common mistakes |
| attestation.md | Hardware-backed evidence and server verification |
| signals.md | Every signal, what it catches, and its limits |
| scoring.md | How signals become a 0–100 score |
| threat-model.md | What Rootect can and cannot promise |
./gradlew :sample:installDebugA live dashboard of every signal plus a separate server-gated attestation card. The card fails closed when the verifier is unavailable; the local dashboard remains diagnostic only.
Requires the Android NDK (28.2.13676358) and CMake. The Gradle daemon JVM is pinned by the
project, so a clone provisions it automatically.
./gradlew :rootect-core:test # unit tests
./gradlew :rootect-core:connectedDebugAndroidTest # instrumented, needs a deviceNative code builds for arm64-v8a, armeabi-v7a and x86_64. The emulator ABI stays in
that list deliberately — it is the false-positive control.
The published AAR is necessarily identical for every Maven consumer. A company or fork that builds Rootect from source can give its native string obfuscation a distinct build seed without changing code:
./gradlew :rootect-core:generateRootectObfuscationSeed -q
./gradlew :rootect-core:assembleRelease --project-prop=rootect.obfuscationSeed=0xYOURSEEDThe seed is a diversity input, not a secret. Keep it stable for reproducible builds or rotate it deliberately per release. Different seeds produce different native result tags and encrypted strings, making byte-for-byte patches less reusable across apps; they do not make an on-device detector impossible to reverse.