react-native-device-integrity 0.1.0

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.
Files changed (54) hide show
  1. package/DeviceIntegrity.podspec +21 -0
  2. package/LICENSE +20 -0
  3. package/README.md +286 -0
  4. package/android/build.gradle +55 -0
  5. package/android/src/main/AndroidManifest.xml +20 -0
  6. package/android/src/main/java/com/deviceintegrity/DeviceIntegrityModule.kt +48 -0
  7. package/android/src/main/java/com/deviceintegrity/DeviceIntegrityPackage.kt +31 -0
  8. package/android/src/main/java/com/deviceintegrity/IntegrityChecks.kt +454 -0
  9. package/app.plugin.js +1 -0
  10. package/ios/DIIntegrityChecks.h +15 -0
  11. package/ios/DIIntegrityChecks.m +395 -0
  12. package/ios/DeviceIntegrity.h +5 -0
  13. package/ios/DeviceIntegrity.mm +29 -0
  14. package/ios/PrivacyInfo.xcprivacy +14 -0
  15. package/lib/module/NativeDeviceIntegrity.js +5 -0
  16. package/lib/module/NativeDeviceIntegrity.js.map +1 -0
  17. package/lib/module/checkIntegrity.js +15 -0
  18. package/lib/module/checkIntegrity.js.map +1 -0
  19. package/lib/module/checkIntegrity.native.js +67 -0
  20. package/lib/module/checkIntegrity.native.js.map +1 -0
  21. package/lib/module/index.js +5 -0
  22. package/lib/module/index.js.map +1 -0
  23. package/lib/module/package.json +1 -0
  24. package/lib/module/resolveStatus.js +76 -0
  25. package/lib/module/resolveStatus.js.map +1 -0
  26. package/lib/module/types.js +4 -0
  27. package/lib/module/types.js.map +1 -0
  28. package/lib/module/useDeviceIntegrity.js +62 -0
  29. package/lib/module/useDeviceIntegrity.js.map +1 -0
  30. package/lib/typescript/package.json +1 -0
  31. package/lib/typescript/src/NativeDeviceIntegrity.d.ts +17 -0
  32. package/lib/typescript/src/NativeDeviceIntegrity.d.ts.map +1 -0
  33. package/lib/typescript/src/checkIntegrity.d.ts +6 -0
  34. package/lib/typescript/src/checkIntegrity.d.ts.map +1 -0
  35. package/lib/typescript/src/checkIntegrity.native.d.ts +3 -0
  36. package/lib/typescript/src/checkIntegrity.native.d.ts.map +1 -0
  37. package/lib/typescript/src/index.d.ts +4 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -0
  39. package/lib/typescript/src/resolveStatus.d.ts +8 -0
  40. package/lib/typescript/src/resolveStatus.d.ts.map +1 -0
  41. package/lib/typescript/src/types.d.ts +23 -0
  42. package/lib/typescript/src/types.d.ts.map +1 -0
  43. package/lib/typescript/src/useDeviceIntegrity.d.ts +10 -0
  44. package/lib/typescript/src/useDeviceIntegrity.d.ts.map +1 -0
  45. package/package.json +210 -0
  46. package/plugin/build/index.d.ts +19 -0
  47. package/plugin/build/index.js +49 -0
  48. package/src/NativeDeviceIntegrity.ts +19 -0
  49. package/src/checkIntegrity.native.ts +88 -0
  50. package/src/checkIntegrity.ts +16 -0
  51. package/src/index.tsx +10 -0
  52. package/src/resolveStatus.ts +125 -0
  53. package/src/types.ts +41 -0
  54. package/src/useDeviceIntegrity.ts +92 -0
@@ -0,0 +1,454 @@
1
+ package com.deviceintegrity
2
+
3
+ import android.content.Context
4
+ import android.content.pm.PackageManager
5
+ import android.os.Build
6
+ import android.os.Debug
7
+ import java.io.BufferedReader
8
+ import java.io.File
9
+ import java.io.FileReader
10
+ import java.util.concurrent.TimeUnit
11
+
12
+ /**
13
+ * Check set informed by RootBeer and the OWASP MASTG; independent implementation.
14
+ */
15
+ internal class IntegrityChecks(private val context: Context) {
16
+
17
+ data class Signal(
18
+ val id: String,
19
+ val category: String,
20
+ val description: String,
21
+ )
22
+
23
+ data class Report(
24
+ val completed: Boolean,
25
+ val reason: String? = null,
26
+ val signals: List<Signal>,
27
+ )
28
+
29
+ fun run(): Report {
30
+ val signals = mutableListOf<Signal>()
31
+ val seen = mutableSetOf<String>()
32
+ var completed = true
33
+
34
+ fun add(signal: Signal) {
35
+ if (seen.add(signal.id)) {
36
+ signals.add(signal)
37
+ }
38
+ }
39
+
40
+ fun runCheck(block: () -> Signal?) {
41
+ try {
42
+ block()?.let { add(it) }
43
+ } catch (_: Throwable) {
44
+ completed = false
45
+ }
46
+ }
47
+
48
+ runCheck { checkEmulator() }
49
+ runCheck { checkRootSuBinary() }
50
+ runCheck { checkRootManagementApps() }
51
+ runCheck { checkRootMagiskFiles() }
52
+ runCheck { checkRootTestKeys() }
53
+ runCheck { checkRootDangerousProps() }
54
+ runCheck { checkRootRwSystem() }
55
+ runCheck { checkHookingFrida() }
56
+ runCheck { checkHookingXposed() }
57
+ runCheck { checkDebuggerAttached() }
58
+
59
+ return Report(
60
+ completed = completed,
61
+ reason = if (completed) null else "incomplete",
62
+ signals = signals,
63
+ )
64
+ }
65
+
66
+ // --- emulator ---
67
+
68
+ private fun checkEmulator(): Signal? {
69
+ val fingerprint = Build.FINGERPRINT.orEmpty().lowercase()
70
+ val model = Build.MODEL.orEmpty()
71
+ val manufacturer = Build.MANUFACTURER.orEmpty()
72
+ val hardware = Build.HARDWARE.orEmpty().lowercase()
73
+ val product = Build.PRODUCT.orEmpty().lowercase()
74
+ val brand = Build.BRAND.orEmpty().lowercase()
75
+ val device = Build.DEVICE.orEmpty().lowercase()
76
+
77
+ val fingerprintGeneric =
78
+ fingerprint.startsWith("generic") ||
79
+ fingerprint.startsWith("unknown") ||
80
+ fingerprint.contains("emulator") ||
81
+ fingerprint.contains("sdk_gphone")
82
+
83
+ val hardwareEmulator = hardware in setOf("goldfish", "ranchu", "vbox86")
84
+
85
+ val qemuFiles =
86
+ File("/dev/qemu_pipe").exists() || File("/dev/socket/qemud").exists()
87
+
88
+ val modelHit =
89
+ model.contains("google_sdk", ignoreCase = true) ||
90
+ model.contains("Emulator", ignoreCase = true) ||
91
+ model.contains("Android SDK built for", ignoreCase = true) ||
92
+ model.contains("sdk_gphone", ignoreCase = true)
93
+
94
+ val manufacturerHit = manufacturer.contains("Genymotion", ignoreCase = true)
95
+
96
+ val productHit =
97
+ product.contains("sdk") ||
98
+ product.contains("sdk_gphone") ||
99
+ product.contains("vbox86p") ||
100
+ product.contains("emulator") ||
101
+ product.contains("simulator")
102
+
103
+ val brandDeviceGeneric = brand.startsWith("generic") && device.startsWith("generic")
104
+
105
+ val anyIndicator =
106
+ fingerprintGeneric ||
107
+ hardwareEmulator ||
108
+ qemuFiles ||
109
+ modelHit ||
110
+ manufacturerHit ||
111
+ productHit ||
112
+ brandDeviceGeneric
113
+
114
+ if (!anyIndicator) {
115
+ return null
116
+ }
117
+
118
+ // Require at least one strong indicator to avoid false positives.
119
+ val strongIndicators = fingerprintGeneric || hardwareEmulator || qemuFiles
120
+ if (!strongIndicators) {
121
+ return null
122
+ }
123
+
124
+ return Signal(
125
+ id = "emulator",
126
+ category = "emulator",
127
+ description = "Device appears to be an emulator",
128
+ )
129
+ }
130
+
131
+ // --- root ---
132
+
133
+ private fun checkRootSuBinary(): Signal? {
134
+ val knownDirs =
135
+ listOf(
136
+ "/system/bin",
137
+ "/system/xbin",
138
+ "/sbin",
139
+ "/system/sd/xbin",
140
+ "/system/bin/failsafe",
141
+ "/data/local/xbin",
142
+ "/data/local/bin",
143
+ "/data/local",
144
+ "/su/bin",
145
+ )
146
+
147
+ val pathDirs =
148
+ System.getenv("PATH")
149
+ ?.split(':')
150
+ ?.map { it.trim() }
151
+ ?.filter { it.isNotEmpty() }
152
+ .orEmpty()
153
+
154
+ val dirs = (knownDirs + pathDirs).distinct()
155
+
156
+ val foundSu =
157
+ dirs.any { dir -> File(dir, "su").exists() } ||
158
+ File("/system/app/Superuser.apk").exists()
159
+
160
+ if (!foundSu) {
161
+ return null
162
+ }
163
+
164
+ return Signal(
165
+ id = "root_su_binary",
166
+ category = "root",
167
+ description = "A privileged elevation binary was found",
168
+ )
169
+ }
170
+
171
+ private fun checkRootManagementApps(): Signal? {
172
+ val packages =
173
+ listOf(
174
+ "com.topjohnwu.magisk",
175
+ "io.github.huskydg.magisk",
176
+ "eu.chainfire.supersu",
177
+ "com.koushikdutta.superuser",
178
+ "com.noshufou.android.su",
179
+ "com.thirdparty.superuser",
180
+ "com.yellowes.su",
181
+ "com.kingroot.kinguser",
182
+ "com.kingo.root",
183
+ "com.zhiqupk.root.global",
184
+ "com.smedialink.oneclickroot",
185
+ "me.weishu.kernelsu",
186
+ "me.bmax.apatch",
187
+ "com.devadvance.rootcloak",
188
+ "de.robv.android.xposed.installer",
189
+ "org.lsposed.manager",
190
+ )
191
+
192
+ val pm = context.packageManager
193
+ val found =
194
+ packages.any { packageName ->
195
+ try {
196
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
197
+ pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
198
+ } else {
199
+ @Suppress("DEPRECATION")
200
+ pm.getPackageInfo(packageName, 0)
201
+ }
202
+ true
203
+ } catch (_: PackageManager.NameNotFoundException) {
204
+ false
205
+ }
206
+ }
207
+
208
+ if (!found) {
209
+ return null
210
+ }
211
+
212
+ return Signal(
213
+ id = "root_management_apps",
214
+ category = "root",
215
+ description = "A root or privilege-management app is installed",
216
+ )
217
+ }
218
+
219
+ private fun checkRootMagiskFiles(): Signal? {
220
+ val paths =
221
+ listOf(
222
+ "/sbin/.magisk",
223
+ "/sbin/.core",
224
+ "/cache/.disable_magisk",
225
+ "/dev/.magisk.unblock",
226
+ "/data/adb/magisk",
227
+ "/data/adb/ksu",
228
+ "/data/adb/ap",
229
+ "/init.magisk.rc",
230
+ )
231
+
232
+ if (!paths.any { File(it).exists() }) {
233
+ return null
234
+ }
235
+
236
+ return Signal(
237
+ id = "root_magisk_files",
238
+ category = "root",
239
+ description = "Root framework artifacts were detected",
240
+ )
241
+ }
242
+
243
+ private fun checkRootTestKeys(): Signal? {
244
+ val tags = Build.TAGS ?: return null
245
+ if (!tags.contains("test-keys")) {
246
+ return null
247
+ }
248
+
249
+ return Signal(
250
+ id = "root_test_keys",
251
+ category = "root",
252
+ description = "Build was signed with test keys",
253
+ )
254
+ }
255
+
256
+ private fun checkRootDangerousProps(): Signal? {
257
+ val debuggable = readSystemProperty("ro.debuggable") ?: throw IllegalStateException("getprop failed")
258
+ val secure = readSystemProperty("ro.secure") ?: throw IllegalStateException("getprop failed")
259
+
260
+ if (debuggable != "1" && secure != "0") {
261
+ return null
262
+ }
263
+
264
+ return Signal(
265
+ id = "root_dangerous_props",
266
+ category = "root",
267
+ description = "System security properties indicate an insecure build",
268
+ )
269
+ }
270
+
271
+ private fun readSystemProperty(name: String): String? {
272
+ var process: Process? = null
273
+ return try {
274
+ process = ProcessBuilder("getprop", name).redirectErrorStream(true).start()
275
+ val finished = process.waitFor(1, TimeUnit.SECONDS)
276
+ if (!finished) {
277
+ process.destroy()
278
+ process.waitFor(200, TimeUnit.MILLISECONDS)
279
+ if (process.isAlive) {
280
+ process.destroyForcibly()
281
+ }
282
+ return null
283
+ }
284
+ process.inputStream.bufferedReader().use { it.readText().trim() }
285
+ } catch (_: Throwable) {
286
+ null
287
+ } finally {
288
+ try {
289
+ process?.destroy()
290
+ } catch (_: Throwable) {
291
+ // ignore
292
+ }
293
+ }
294
+ }
295
+
296
+ private fun checkRootRwSystem(): Signal? {
297
+ val targets =
298
+ setOf("/system", "/system/bin", "/system/xbin", "/vendor", "/sbin", "/etc")
299
+
300
+ BufferedReader(FileReader("/proc/mounts")).use { reader ->
301
+ var line: String?
302
+ while (reader.readLine().also { line = it } != null) {
303
+ val parts = line!!.split(Regex("\\s+"))
304
+ if (parts.size < 4) {
305
+ continue
306
+ }
307
+ val mountPoint = parts[1]
308
+ val options = parts[3].split(',')
309
+ if (mountPoint in targets && "rw" in options) {
310
+ return Signal(
311
+ id = "root_rw_system",
312
+ category = "root",
313
+ description = "A system partition is mounted read-write",
314
+ )
315
+ }
316
+ }
317
+ }
318
+
319
+ return null
320
+ }
321
+
322
+ // --- hooking ---
323
+
324
+ private fun checkHookingFrida(): Signal? {
325
+ val mapMarkers =
326
+ listOf("frida-agent", "frida-gadget", "libfrida", "gum-js-loop", "linjector")
327
+
328
+ if (mapsContainAny(mapMarkers)) {
329
+ return Signal(
330
+ id = "hooking_frida",
331
+ category = "hooking",
332
+ description = "Instrumentation framework artifacts were detected",
333
+ )
334
+ }
335
+
336
+ // Only gum-js-loop is treated as a strong task-name indicator.
337
+ if (taskCommContains("gum-js-loop")) {
338
+ return Signal(
339
+ id = "hooking_frida",
340
+ category = "hooking",
341
+ description = "Instrumentation framework artifacts were detected",
342
+ )
343
+ }
344
+
345
+ return null
346
+ }
347
+
348
+ private fun checkHookingXposed(): Signal? {
349
+ val mapMarkers =
350
+ listOf(
351
+ "XposedBridge.jar",
352
+ "liblspd",
353
+ "libriru",
354
+ "libsubstrate",
355
+ "libzygisk",
356
+ )
357
+
358
+ if (mapsContainAny(mapMarkers)) {
359
+ return xposedSignal()
360
+ }
361
+
362
+ if (classPresent("de.robv.android.xposed.XposedBridge")) {
363
+ return xposedSignal()
364
+ }
365
+
366
+ val stack = Thread.currentThread().stackTrace
367
+ val hooked =
368
+ stack.any { frame ->
369
+ val name = frame.className
370
+ name.startsWith("de.robv.android.xposed") ||
371
+ name.startsWith("com.saurik.substrate")
372
+ }
373
+
374
+ if (hooked) {
375
+ return xposedSignal()
376
+ }
377
+
378
+ return null
379
+ }
380
+
381
+ private fun xposedSignal(): Signal =
382
+ Signal(
383
+ id = "hooking_xposed",
384
+ category = "hooking",
385
+ description = "Code-injection framework artifacts were detected",
386
+ )
387
+
388
+ private fun classPresent(name: String): Boolean {
389
+ val loaders =
390
+ listOfNotNull(
391
+ context.classLoader,
392
+ ClassLoader.getSystemClassLoader(),
393
+ ).distinct()
394
+
395
+ for (loader in loaders) {
396
+ try {
397
+ Class.forName(name, false, loader)
398
+ return true
399
+ } catch (_: ClassNotFoundException) {
400
+ // continue
401
+ }
402
+ }
403
+ return false
404
+ }
405
+
406
+ // --- debugger ---
407
+
408
+ private fun checkDebuggerAttached(): Signal? {
409
+ if (!Debug.isDebuggerConnected() && !Debug.waitingForDebugger()) {
410
+ return null
411
+ }
412
+
413
+ return Signal(
414
+ id = "debugger_attached",
415
+ category = "debugger",
416
+ description = "A debugger is attached to the process",
417
+ )
418
+ }
419
+
420
+ // --- helpers ---
421
+
422
+ private fun mapsContainAny(markers: List<String>): Boolean {
423
+ BufferedReader(FileReader("/proc/self/maps")).use { reader ->
424
+ var line: String?
425
+ while (reader.readLine().also { line = it } != null) {
426
+ val current = line!!
427
+ if (markers.any { marker -> current.contains(marker) }) {
428
+ return true
429
+ }
430
+ }
431
+ }
432
+ return false
433
+ }
434
+
435
+ private fun taskCommContains(name: String): Boolean {
436
+ val taskDir = File("/proc/self/task")
437
+ val tasks = taskDir.listFiles() ?: return false
438
+ for (task in tasks) {
439
+ val comm = File(task, "comm")
440
+ if (!comm.exists()) {
441
+ continue
442
+ }
443
+ try {
444
+ val text = comm.readText().trim()
445
+ if (text == name) {
446
+ return true
447
+ }
448
+ } catch (_: Throwable) {
449
+ // skip unreadable task entries
450
+ }
451
+ }
452
+ return false
453
+ }
454
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/build');
@@ -0,0 +1,15 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /**
6
+ * Internal integrity checks. Runs off the main thread (except URL-scheme
7
+ * queries) and delivers a NativeIntegrityReport-shaped dictionary.
8
+ */
9
+ @interface DIIntegrityChecks : NSObject
10
+
11
+ + (void)runWithCompletion:(void (^)(NSDictionary *report))completion;
12
+
13
+ @end
14
+
15
+ NS_ASSUME_NONNULL_END