react-native-device-integrity 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -384,7 +384,7 @@ The [`with-frida-gadget`](example/plugins/with-frida-gadget.js) config plugin do
384
384
 
385
385
  Observed on an iPhone XS Max (A12, iOS 18.7.10, stock): `status: compromised` with a single signal, **`hooking_libraries`**. Launched from Xcode with the debugger attached, the same build also reports `debugger_attached` and `hooking_dyld_insert` (Xcode injects its view-debugging library) — expected, not false positives.
386
386
 
387
- Observed on a rooted OnePlus Nord 3 (CPH2493, OxygenOS 15, unlocked bootloader, Magisk 31, no hiding): `status: compromised` with **`bootloader_unlocked`**, **`root_su_binary`**, **`root_management_apps`** and **`root_magisk_files`**, in about 150 ms. With Zygisk + DenyList hiding Magisk from the app, `root_su_binary` and `root_magisk_files` disappear (Magisk removes `su` and its mounts from the app's view), but **`bootloader_unlocked`** and **`root_management_apps`** still fire, so the status stays `compromised`. Hiding the Magisk app (randomised package name) or a `resetprop` module would remove those too — pair with server-side attestation for high-value flows.
387
+ Observed on a rooted OnePlus Nord 3 (CPH2493, OxygenOS 15, unlocked bootloader, Magisk 31, no hiding): `status: compromised` with **`bootloader_unlocked`**, **`root_su_binary`**, **`root_management_apps`** and **`root_magisk_files`**, in about 40 ms (release build). With Zygisk + DenyList hiding Magisk from the app, `root_su_binary` and `root_magisk_files` disappear (Magisk removes `su` and its mounts from the app's view), but **`bootloader_unlocked`** and **`root_management_apps`** still fire, so the status stays `compromised`. Hiding the Magisk app (randomised package name) or a `resetprop` module would remove those too — pair with server-side attestation for high-value flows.
388
388
 
389
389
  The same phone, built with each [`bootloaderUnlocked`](#unlocked-bootloaders) value:
390
390
 
@@ -7,6 +7,7 @@ import android.os.Debug
7
7
  import java.io.BufferedReader
8
8
  import java.io.File
9
9
  import java.io.FileReader
10
+ import java.lang.reflect.Method
10
11
  import java.util.concurrent.FutureTask
11
12
  import java.util.concurrent.TimeUnit
12
13
 
@@ -338,14 +339,23 @@ internal class IntegrityChecks(private val context: Context) {
338
339
  }
339
340
 
340
341
  /**
341
- * All system properties from one `getprop` dump, read once per run. Spawning
342
- * a process costs ~100 ms from an app, so per-property calls add up quickly.
343
- * Null when the dump can't be read; a property missing from it is "".
342
+ * A system property, read in-process through `android.os.SystemProperties`
343
+ * (microseconds). If that hidden API is unavailable, falls back to one
344
+ * `getprop` dump per run — spawning a process costs ~40–100 ms from an app.
345
+ * Null when neither can be read; a missing property is "".
344
346
  */
345
- private val systemProperties: Map<String, String>? by lazy { readAllSystemProperties() }
347
+ private fun readSystemProperty(name: String): String? {
348
+ systemPropertiesGet?.let { get ->
349
+ try {
350
+ return get.invoke(null, name) as? String ?: ""
351
+ } catch (_: Throwable) {
352
+ // fall through to the getprop dump
353
+ }
354
+ }
355
+ return systemPropertiesDump?.let { it[name].orEmpty() }
356
+ }
346
357
 
347
- private fun readSystemProperty(name: String): String? =
348
- systemProperties?.let { it[name].orEmpty() }
358
+ private val systemPropertiesDump: Map<String, String>? by lazy { readAllSystemProperties() }
349
359
 
350
360
  private fun readAllSystemProperties(): Map<String, String>? {
351
361
  var process: Process? = null
@@ -385,13 +395,17 @@ internal class IntegrityChecks(private val context: Context) {
385
395
  BufferedReader(FileReader("/proc/mounts")).use { reader ->
386
396
  var line: String?
387
397
  while (reader.readLine().also { line = it } != null) {
388
- val parts = line!!.split(Regex("\\s+"))
398
+ // "<source> <mount point> <fstype> <options> <dump> <pass>"; the kernel
399
+ // separates fields with single spaces and escapes spaces inside them.
400
+ val parts = line!!.split(' ', limit = 5)
389
401
  if (parts.size < 4) {
390
402
  continue
391
403
  }
392
404
  val mountPoint = parts[1]
393
- val options = parts[3].split(',')
394
- if (mountPoint in targets && "rw" in options) {
405
+ if (mountPoint !in targets) {
406
+ continue
407
+ }
408
+ if ("rw" in parts[3].split(',')) {
395
409
  return Signal(
396
410
  id = "root_rw_system",
397
411
  category = "root",
@@ -504,18 +518,15 @@ internal class IntegrityChecks(private val context: Context) {
504
518
 
505
519
  // --- helpers ---
506
520
 
507
- private fun mapsContainAny(markers: List<String>): Boolean {
508
- BufferedReader(FileReader("/proc/self/maps")).use { reader ->
509
- var line: String?
510
- while (reader.readLine().also { line = it } != null) {
511
- val current = line!!
512
- if (markers.any { marker -> current.contains(marker) }) {
513
- return true
514
- }
515
- }
516
- }
517
- return false
518
- }
521
+ /**
522
+ * `/proc/self/maps`, read once per run and shared by the hooking checks. The
523
+ * kernel regenerates it on every read, and a React Native process maps
524
+ * thousands of regions. A failed read throws again on the next access.
525
+ */
526
+ private val selfMaps: String by lazy { File("/proc/self/maps").readText() }
527
+
528
+ private fun mapsContainAny(markers: List<String>): Boolean =
529
+ markers.any { marker -> selfMaps.contains(marker) }
519
530
 
520
531
  private fun taskCommContains(name: String): Boolean {
521
532
  val taskDir = File("/proc/self/task")
@@ -536,4 +547,15 @@ internal class IntegrityChecks(private val context: Context) {
536
547
  }
537
548
  return false
538
549
  }
550
+
551
+ companion object {
552
+ /** `android.os.SystemProperties.get(String)`, or null if hidden-API access is blocked. */
553
+ private val systemPropertiesGet: Method? by lazy {
554
+ try {
555
+ Class.forName("android.os.SystemProperties").getMethod("get", String::class.java)
556
+ } catch (_: Throwable) {
557
+ null
558
+ }
559
+ }
560
+ }
539
561
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-device-integrity",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Jailbreak, root, hooking, debugger and emulator detection for React Native and Expo",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",