Open-source Android device-integrity SDK.
Collects APK integrity, hardware key attestation, root / hook / emulator / cloner signals into one deterministic JSON your backend can act on.
Not a RASP — it observes and reports; your backend owns the policy.
Distributed via JitPack. Apply the Gradle plugin — it auto-wires the runtime AAR and bakes the build-time APK fingerprint.
settings.gradle.kts
pluginManagement {
repositories { maven("https://jitpack.io"); gradlePluginPortal(); google() }
resolutionStrategy {
eachPlugin {
if (requested.id.id == "io.ssemaj.deviceintelligence") {
useModule(
"com.github.iamjosephmj.DeviceIntelligence:" +
"deviceintelligence-gradle:${requested.version}"
)
}
}
}
}
dependencyResolutionManagement {
repositories { google(); mavenCentral(); maven("https://jitpack.io") }
}app/build.gradle.kts
plugins {
id("io.ssemaj.deviceintelligence") version "2.0.1"
}minSdk 28. Ships native binaries for arm64-v8a, x86_64, and armeabi-v7a. kotlinx-coroutines-android is the only runtime dependency.
Four entry points — pick the one that fits your use case.
One-shot collect — one structured snapshot at startup.
lifecycleScope.launch {
val report = DeviceIntelligence.collect(context) // TelemetryReport
val json = DeviceIntelligence.collectJson(context) // canonical JSON
val signals = report.toIntegritySignals()
if (IntegritySignal.HOOKING_FRAMEWORK_DETECTED in signals) {
// Ship to your backend, gate the action, raise a flag — your call.
}
}Periodic observe — a fresh snapshot every N seconds (e.g. catch a Frida agent that attaches mid-flow).
DeviceIntelligence.observe(context, interval = 2.seconds)
.onEach { report -> render(report) }
.launchIn(lifecycleScope)Cumulative session observe — like observe(), but accumulates findings across emissions. A transient hook that fires once and detaches stays visible with stillActive = false.
DeviceIntelligence.observeSession(context, interval = 2.seconds)
.onEach { session: SessionFindings ->
render(session.findings) // List<TrackedFinding>
ship(session.toJson()) // canonical wire format
}
.launchIn(lifecycleScope)Each TrackedFinding adds firstSeenAtEpochMs, lastSeenAtEpochMs, observationCount, and stillActive on top of the underlying Finding.
Java / synchronous — for Java consumers, worker threads, JNI bridges.
TelemetryReport report = DeviceIntelligence.collectBlocking(context);
String json = DeviceIntelligence.collectJsonBlocking(context);Detectors emit granular Findings; the IntegritySignal mapper collapses them into product-shaped verdicts you branch on.
IntegritySignal |
Meaning |
|---|---|
APK_TAMPERED |
APK modified, repackaged, signer mismatch, or installer not allowlisted. |
APK_FINGERPRINT_UNAVAILABLE |
Build-time fingerprint asset missing/corrupt — no verdict either way. |
BOOTLOADER_INTEGRITY_FAILED |
Attestation chain has anomalies, or device claims StrongBox but attests lower. |
TEE_ATTESTATION_DEGRADED |
Attestation verdict below MEETS_STRONG_INTEGRITY (or a CBOR/EAT leaf needing re-verify). |
HOOKING_FRAMEWORK_DETECTED |
Active code hooking — Frida, Xposed/LSPosed, Pine, SandHook, Substrate, DEX injection, .text/GOT tampering. |
INJECTED_NATIVE_CODE |
Unknown post-baseline .so / anon-exec mapping (precondition for hooking). |
ROOT_INDICATORS_PRESENT |
su, Magisk artifacts, test-keys, root-manager app, Shamiko bypass, or TLS-trust-store MITM. |
EMULATOR_DETECTED |
CPU-level signals (arm64 MRS / x86_64 CPUID hypervisor bit). |
APP_CLONED |
Foreign APK mappings, mount-namespace inconsistencies, UID mismatches. |
DEBUGGER_ATTACHED |
JVM debugger or ptrace tracer attached. |
DEBUG_FLAG_MISMATCH |
App's FLAG_DEBUGGABLE disagrees with ro.debuggable. |
HARDWARE_ATTESTED_USERSPACE_TAMPERED |
Strongest signal. Verified boot and a userspace hook in the same report — treat as a hard block. |
val report = DeviceIntelligence.collect(context).toIntegritySignalReport()
when {
IntegritySignal.HARDWARE_ATTESTED_USERSPACE_TAMPERED in report.signals -> hardBlock()
IntegritySignal.HOOKING_FRAMEWORK_DETECTED in report.signals -> denyPayment()
IntegritySignal.ROOT_INDICATORS_PRESENT in report.signals -> warnUser()
IntegritySignal.EMULATOR_DETECTED in report.signals -> requireExtra2FA()
else -> allow()
}
report.evidence[IntegritySignal.HOOKING_FRAMEWORK_DETECTED]?.forEach { finding ->
log.info("hook detected — kind=${finding.kind} subject=${finding.subject}")
}The underlying detectors (integrity.apk, integrity.bootloader, integrity.art, attestation.key, runtime.environment, runtime.root, runtime.emulator, runtime.cloner) and their finding kinds are documented in docs/DETECTORS.md.
Not a RASP. It never blocks sessions, kills processes, or interrupts a flow. It only observes. Build enforcement on the JSON your backend ingests; keep the policy off-device.
collectJson(context) returns one deterministic document with a stable schema_version (currently 2). The envelope:
statusvsfindingsanswer different questions.status(ok/inconclusive/error) = "did the detector run?";findings[]= "what did it see?". A rooted device isstatus: "ok"with a non-emptyfindings[]— drive decisions offsummary.detectors_with_findings, notstatus.- For every
Finding,kind/severity/subject/messageare stable;detailsis opaque diagnostic data whose keys may change without aschema_versionbump — don't key on them server-side. SessionFindings.toJson()(fromobserveSession) addsfirst_seen_at_epoch_ms/last_seen_at_epoch_ms/observation_count/still_activeper finding.
A full clean-device report and tripped-detector examples are in docs/DETECTORS.md.
| Permission | Required by | Default | Opt-out / opt-in |
|---|---|---|---|
QUERY_ALL_PACKAGES |
runtime.root root_manager_app_installed channel |
on | Strip via tools:node="remove" |
ACCESS_NETWORK_STATE |
DeviceContext.vpnActive |
off | enableVpnDetection.set(true) |
USE_BIOMETRIC |
DeviceContext.biometricsEnrolled |
off | enableBiometricsDetection.set(true) |
When opted out, vpnActive / biometricsEnrolled report null (not false).
git clone https://github.com/iamjosephmj/DeviceIntelligence.git
cd DeviceIntelligence
./gradlew :samples:minimal:installDebug
adb shell am start -n io.ssemaj.sample/.MainActivityThe SDK makes zero network calls and reads no GAID, ANDROID_ID, IMEI/IMSI, SIM serial, account, contact, or location data. The output of collectJson(context) stays in your process; what you upload — and where — is entirely your decision. Because the library transmits nothing, it is neither a data controller nor processor under GDPR; your app remains the sole controller for any telemetry it forwards. Every field is documented in the output contract so you can audit exactly what exists before shipping it.
docs/DETECTORS.md— per-detector reference: finding kinds, sample tripped JSON, costs, caveats.NATIVE_INTEGRITY_DESIGN.md— design of the native anti-hooking stack.CHANGELOG.md— version history with per-release wire-format impact.SECURITY.md— vulnerability disclosure process and supported-versions policy.
Apache 2.0 — see LICENSE.
{ "schema_version": 2, "library_version": "2.0.1", "collected_at_epoch_ms": 1777400000000, "device": { /* model, abi, soc, strongbox_available, ... */ }, "app": { "package_name": "...", "signer_cert_sha256": ["..."], "attestation": { /* chain + verdict */ } }, "detectors": [ { "id": "integrity.apk", "status": "ok", "findings": [] }, /* ... */ ], "summary": { "total_findings": 0, "findings_by_severity": {}, "detectors_with_findings": [] } }