react-native-device-integrity 0.2.1 → 0.3.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.
- package/README.md +120 -5
- package/android/src/main/java/com/deviceintegrity/BootloaderUnlockedMode.kt +58 -0
- package/android/src/main/java/com/deviceintegrity/DeviceIntegrityModule.kt +6 -0
- package/android/src/main/java/com/deviceintegrity/IntegrityChecks.kt +94 -13
- package/jest/index.js +1 -0
- package/lib/module/NativeDeviceIntegrity.js.map +1 -1
- package/lib/module/resolveStatus.js +4 -1
- package/lib/module/resolveStatus.js.map +1 -1
- package/lib/module/types.js +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/NativeDeviceIntegrity.d.ts +2 -0
- package/lib/typescript/src/NativeDeviceIntegrity.d.ts.map +1 -1
- package/lib/typescript/src/resolveStatus.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +1 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/plugin/build/index.d.ts +17 -1
- package/plugin/build/index.js +47 -6
- package/src/NativeDeviceIntegrity.ts +2 -0
- package/src/resolveStatus.ts +11 -2
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -79,6 +79,21 @@ To skip Info.plist URL-scheme entries (e.g. you manage them yourself):
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
To change how an unlocked bootloader is treated (see [Unlocked bootloaders](#unlocked-bootloaders)):
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"expo": {
|
|
87
|
+
"plugins": [
|
|
88
|
+
[
|
|
89
|
+
"react-native-device-integrity",
|
|
90
|
+
{ "android": { "bootloaderUnlocked": "report" } }
|
|
91
|
+
]
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
82
97
|
Then run `npx expo prebuild` or use a development build. Expo Go is not supported.
|
|
83
98
|
|
|
84
99
|
### Bare React Native
|
|
@@ -96,7 +111,50 @@ On iOS, add these schemes under `LSApplicationQueriesSchemes` in `Info.plist`:
|
|
|
96
111
|
|
|
97
112
|
Without them, `canOpenURL` cannot query those schemes and the URL-scheme check **silently finds nothing**.
|
|
98
113
|
|
|
99
|
-
Android needs no extra setup — the library manifest `<queries>` list merges automatically via the Gradle manifest merger.
|
|
114
|
+
Android needs no extra setup — the library manifest `<queries>` list merges automatically via the Gradle manifest merger. To change how an unlocked bootloader is treated, add one `<meta-data>` inside `<application>` in `android/app/src/main/AndroidManifest.xml` (see [Unlocked bootloaders](#unlocked-bootloaders)):
|
|
115
|
+
|
|
116
|
+
```xml
|
|
117
|
+
<meta-data
|
|
118
|
+
android:name="com.deviceintegrity.bootloader_unlocked"
|
|
119
|
+
android:value="report" />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Unlocked bootloaders
|
|
123
|
+
|
|
124
|
+
`bootloader_unlocked` (Android) fires on any phone whose bootloader was unlocked — rooted phones, custom-ROM phones, and developers' own test phones. It's set once per app at build time:
|
|
125
|
+
|
|
126
|
+
| Value | Effect |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `compromised` (default) | The signal makes status `'compromised'`, like root. Matches Play Integrity, which also fails the device verdict on unlocked phones. |
|
|
129
|
+
| `report` | The signal is still returned in `signals`, but never makes status `'compromised'` on its own (like `emulator`). |
|
|
130
|
+
| `off` | The check doesn't run. |
|
|
131
|
+
|
|
132
|
+
A missing or unrecognised value counts as `compromised`. The runtime `ignore` option still works on top (`ignore: ['bootloader_unlocked']` moves it to `ignored`).
|
|
133
|
+
|
|
134
|
+
**Relaxing it doesn't hide root.** `bootloader_unlocked` and the root signals are separate checks: one asks whether the phone *can* boot unverified software, the others whether root is installed *now*. Status is `compromised` if any active signal is compromising, so a rooted phone stays `compromised` through `root_su_binary`, `root_management_apps` or `root_magisk_files` whatever this setting is. They can also fire alone — an unlocked phone running a custom ROM without root reports only `bootloader_unlocked`; a rooted phone whose boot properties are faked reports only the root signals.
|
|
135
|
+
|
|
136
|
+
What you give up is the backstop when root is *hidden*: with Magisk's DenyList on, the `su` and mount checks go quiet and `bootloader_unlocked` is one of the few signals left (see [Validating on a real device](#validating-on-a-real-device)). Keep the default in production unless you have a reason not to.
|
|
137
|
+
|
|
138
|
+
To relax it only for development builds of a bare app, use a Gradle placeholder:
|
|
139
|
+
|
|
140
|
+
```xml
|
|
141
|
+
<!-- AndroidManifest.xml -->
|
|
142
|
+
<meta-data
|
|
143
|
+
android:name="com.deviceintegrity.bootloader_unlocked"
|
|
144
|
+
android:value="${deviceIntegrityBootloaderUnlocked}" />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```groovy
|
|
148
|
+
// android/app/build.gradle
|
|
149
|
+
android {
|
|
150
|
+
buildTypes {
|
|
151
|
+
debug { manifestPlaceholders += [deviceIntegrityBootloaderUnlocked: "report"] }
|
|
152
|
+
release { manifestPlaceholders += [deviceIntegrityBootloaderUnlocked: "compromised"] }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
In Expo, do the same from `app.config.js` (e.g. `bootloaderUnlocked: process.env.APP_VARIANT === 'production' ? 'compromised' : 'report'`), or at runtime with `ignore: __DEV__ ? ['bootloader_unlocked'] : []`.
|
|
100
158
|
|
|
101
159
|
## Usage
|
|
102
160
|
|
|
@@ -196,7 +254,7 @@ Tamper checks run **only** when their option is provided and non-empty (or `true
|
|
|
196
254
|
|
|
197
255
|
`'jailbreak'` | `'root'` | `'hooking'` | `'debugger'` | `'emulator'` | `'tamper'` | `'environment'`
|
|
198
256
|
|
|
199
|
-
A native category the JS layer doesn't recognise is reported as `environment`, never dropped. `tamper` is used by the opt-in tamper checks below.
|
|
257
|
+
A native category the JS layer doesn't recognise is reported as `environment`, never dropped. `environment` is also the category of `bootloader_unlocked` (Android). `tamper` is used by the opt-in tamper checks below.
|
|
200
258
|
|
|
201
259
|
#### `UnknownReason`
|
|
202
260
|
|
|
@@ -233,7 +291,7 @@ Behaviour:
|
|
|
233
291
|
|
|
234
292
|
Status is resolved from the native report + options:
|
|
235
293
|
|
|
236
|
-
1. Apply `ignore` — matching signals move to `ignored` and never affect status.
|
|
294
|
+
1. Apply `ignore` — matching signals move to `ignored` and never affect status. Signals the app configured as report-only at build time (`bootloader_unlocked: "report"`) stay in `signals` but never affect status either.
|
|
237
295
|
2. Build the compromising category set: `policy.compromisingCategories` if provided, otherwise every category except `'emulator'`. Union `'emulator'` when `treatEmulatorAsCompromised` is `true`.
|
|
238
296
|
3. **Compromise evidence wins** — any non-ignored signal whose category is in that set → `'compromised'`, even if the run was incomplete.
|
|
239
297
|
4. Non-compromising, non-ignored signals (e.g. emulator by default) stay in `signals` but do not change status.
|
|
@@ -308,6 +366,60 @@ it('blocks compromised devices', async () => {
|
|
|
308
366
|
|
|
309
367
|
`setMockIntegrityResult(partial)` merges onto a default clean result (`status: 'clean'`, empty `signals` / `ignored`, `platform: 'ios'`, `durationMs: 0`). `useDeviceIntegrity` returns `{ status, signals, result, loading: false, refresh }` from the current mock result.
|
|
310
368
|
|
|
369
|
+
## Validating on a real device
|
|
370
|
+
|
|
371
|
+
Unit tests and the Jest mock prove your app's logic; to see the native checks fire, run them against a real, prepared device. The example app ships an **opt-in Frida Gadget build** for testing hooking detection on a stock iPhone — no jailbreak needed:
|
|
372
|
+
|
|
373
|
+
```sh
|
|
374
|
+
# 1. Download frida-gadget-<version>-ios-universal.dylib.xz from
|
|
375
|
+
# https://github.com/frida/frida/releases, unpack it and save it as
|
|
376
|
+
# example/frida/FridaGadget.dylib (gitignored)
|
|
377
|
+
# 2. Regenerate the iOS project with the gadget wired in
|
|
378
|
+
yarn example prebuild:frida
|
|
379
|
+
# 3. Open example/ios/DeviceIntegrityExample.xcworkspace, pick your signing team
|
|
380
|
+
# (a free Personal Team works) and a physical iPhone, then Run
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The [`with-frida-gadget`](example/plugins/with-frida-gadget.js) config plugin does nothing unless `FRIDA_GADGET=1` is set. When it is, it links the gadget on device builds only, embeds and signs it, writes a gadget config that starts the app normally (`on_load: resume`, `code_signing: required` — required on a stock device without a debugger), and sets the scheme to Release without LLDB. `yarn example prebuild:ios` regenerates a clean project.
|
|
384
|
+
|
|
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
|
+
|
|
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.
|
|
388
|
+
|
|
389
|
+
The same phone, built with each [`bootloaderUnlocked`](#unlocked-bootloaders) value:
|
|
390
|
+
|
|
391
|
+
| Build setting | Root signals | Status | `bootloader_unlocked` |
|
|
392
|
+
| --- | --- | --- | --- |
|
|
393
|
+
| `compromised` (default) | active | `compromised` | in `signals` |
|
|
394
|
+
| `compromised` (default) | ignored at runtime | `compromised` — from `bootloader_unlocked` alone | in `signals` |
|
|
395
|
+
| `report` | active | `compromised` — from the root signals | in `signals` |
|
|
396
|
+
| `report` | ignored at runtime | `clean` | in `signals`, not counted |
|
|
397
|
+
| `off` | active | `compromised` — from the root signals | not checked |
|
|
398
|
+
| default + `ignore: ['bootloader_unlocked']` | active | `compromised` — from the root signals | in `ignored` |
|
|
399
|
+
|
|
400
|
+
<table>
|
|
401
|
+
<tr>
|
|
402
|
+
<th>iPhone XS Max + Frida Gadget</th>
|
|
403
|
+
<th>Same build, run from Xcode</th>
|
|
404
|
+
<th>Simulator + LLDB</th>
|
|
405
|
+
<th>Rooted OnePlus, Magisk DenyList</th>
|
|
406
|
+
</tr>
|
|
407
|
+
<tr>
|
|
408
|
+
<td><img src="docs/images/a05-frida-xs-max.jpg" width="200" alt="iPhone XS Max: COMPROMISED, hooking_libraries only"></td>
|
|
409
|
+
<td><img src="docs/images/a05-xcode-debugger-xs-max.jpg" width="200" alt="iPhone XS Max under Xcode: hooking_libraries, debugger_attached, hooking_dyld_insert"></td>
|
|
410
|
+
<td><img src="docs/images/s01-simulator-debugger.jpg" width="200" alt="iOS Simulator with LLDB attached: simulator and debugger_attached"></td>
|
|
411
|
+
<td><img src="docs/images/a02-denylist-oneplus.jpg" width="200" alt="Rooted OnePlus with DenyList: bootloader_unlocked and root_management_apps"></td>
|
|
412
|
+
</tr>
|
|
413
|
+
<tr>
|
|
414
|
+
<td>Launched from the home screen: <code>hooking_libraries</code> only.</td>
|
|
415
|
+
<td>Xcode's debugger adds <code>debugger_attached</code> and <code>hooking_dyld_insert</code>.</td>
|
|
416
|
+
<td><code>debugger_attached</code> fires; the <code>DYLD_INSERT_LIBRARIES</code> check is skipped on the Simulator.</td>
|
|
417
|
+
<td>DenyList hides <code>su</code> and the mounts; <code>bootloader_unlocked</code> and <code>root_management_apps</code> remain.</td>
|
|
418
|
+
</tr>
|
|
419
|
+
</table>
|
|
420
|
+
|
|
421
|
+
The project site's testing guides cover this step by step (including free vs paid Apple accounts and troubleshooting), plus rooting an Android test device and jailbreaking an iPhone (palera1n on A9–A11, Dopamine on supported A12+ builds).
|
|
422
|
+
|
|
311
423
|
## Signal reference
|
|
312
424
|
|
|
313
425
|
Descriptions are generic on purpose (no matched paths or package names).
|
|
@@ -332,9 +444,10 @@ Descriptions are generic on purpose (no matched paths or package names).
|
|
|
332
444
|
| id | category | Checks |
|
|
333
445
|
| --- | --- | --- |
|
|
334
446
|
| `emulator` | `emulator` | Device appears to be an emulator. |
|
|
447
|
+
| `bootloader_unlocked` | `environment` | The bootloader is unlocked (verified boot state `orange`, or the flash/vbmeta lock properties say unlocked). Skipped on emulators. |
|
|
335
448
|
| `root_su_binary` | `root` | A privileged elevation binary was found. |
|
|
336
449
|
| `root_management_apps` | `root` | A root or privilege-management app is installed. |
|
|
337
|
-
| `root_magisk_files` | `root` | Root framework artifacts were detected. |
|
|
450
|
+
| `root_magisk_files` | `root` | Root framework artifacts were detected — Magisk / KernelSU / APatch mounts in the app's own mount table, or legacy paths. |
|
|
338
451
|
| `root_test_keys` | `root` | Build was signed with test keys. |
|
|
339
452
|
| `root_dangerous_props` | `root` | System security properties indicate an insecure build. |
|
|
340
453
|
| `root_rw_system` | `root` | A system partition is mounted read-write. |
|
|
@@ -373,7 +486,9 @@ These checks run only when you pass the corresponding options. They do not run b
|
|
|
373
486
|
|
|
374
487
|
- On the **iOS Simulator**, filesystem / URL-scheme / symlink / writable-system and `DYLD_INSERT_LIBRARIES` checks are **skipped** (they would inspect the host Mac). Skipped-by-design is not incomplete; hooking-library and debugger checks still run, plus the `simulator` signal.
|
|
375
488
|
- `debugger_attached` fires whenever a debugger is attached, including during normal Xcode / Android Studio debugging.
|
|
489
|
+
- Running from Xcode with the debugger also sets `DYLD_INSERT_LIBRARIES` (Xcode's view-debugging support library), so `hooking_dyld_insert` fires too. Launch from the home screen, or untick *Debug executable* in the scheme, to see the production result.
|
|
376
490
|
- Android emulators still run root checks (they may report root-related signals independently of `emulator`).
|
|
491
|
+
- `bootloader_unlocked` is in the `environment` category, so it is compromising by default — it fires on any developer phone whose bootloader you unlocked. Set it to `report` or `off` per app, or per build type, as described in [Unlocked bootloaders](#unlocked-bootloaders). `yellow` verified boot (locked with user keys, e.g. GrapheneOS) does not fire.
|
|
377
492
|
|
|
378
493
|
## Limitations & threat model
|
|
379
494
|
|
|
@@ -381,7 +496,7 @@ These are **client-side risk indicators**. They can be bypassed by an attacker w
|
|
|
381
496
|
|
|
382
497
|
- Frida / objection hooking this module
|
|
383
498
|
- Rootless jailbreaks with hiding tweaks
|
|
384
|
-
- Magisk DenyList / Zygisk / Shamiko
|
|
499
|
+
- Magisk DenyList / Zygisk / Shamiko (unmount root traces for the app; `resetprop`-based modules can also fake the verified boot properties)
|
|
385
500
|
- KernelSU hiding
|
|
386
501
|
|
|
387
502
|
Never rely on them alone for security decisions. For high-assurance flows, use **Apple App Attest / DeviceCheck** and the **Google Play Integrity API** server-side.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
package com.deviceintegrity
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.os.Build
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* App-level defaults set at build time through `<meta-data>` on the app's
|
|
9
|
+
* `<application>` — written by the Expo config plugin, or by hand in bare apps:
|
|
10
|
+
*
|
|
11
|
+
* <meta-data
|
|
12
|
+
* android:name="com.deviceintegrity.bootloader_unlocked"
|
|
13
|
+
* android:value="report" />
|
|
14
|
+
*/
|
|
15
|
+
internal enum class BootloaderUnlockedMode {
|
|
16
|
+
/** Reported and compromising (default). */
|
|
17
|
+
COMPROMISED,
|
|
18
|
+
|
|
19
|
+
/** Reported, but never changes status on its own. */
|
|
20
|
+
REPORT,
|
|
21
|
+
|
|
22
|
+
/** The check doesn't run. */
|
|
23
|
+
OFF;
|
|
24
|
+
|
|
25
|
+
companion object {
|
|
26
|
+
const val META_DATA_KEY = "com.deviceintegrity.bootloader_unlocked"
|
|
27
|
+
|
|
28
|
+
/** Missing, unreadable or unrecognised values fail closed to COMPROMISED. */
|
|
29
|
+
fun read(context: Context): BootloaderUnlockedMode {
|
|
30
|
+
val value =
|
|
31
|
+
try {
|
|
32
|
+
applicationMetaData(context)?.get(META_DATA_KEY)?.toString()?.trim()?.lowercase()
|
|
33
|
+
} catch (_: Throwable) {
|
|
34
|
+
null
|
|
35
|
+
}
|
|
36
|
+
return when (value) {
|
|
37
|
+
"report" -> REPORT
|
|
38
|
+
"off" -> OFF
|
|
39
|
+
else -> COMPROMISED
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Suppress("DEPRECATION")
|
|
44
|
+
private fun applicationMetaData(context: Context) =
|
|
45
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
46
|
+
context.packageManager
|
|
47
|
+
.getApplicationInfo(
|
|
48
|
+
context.packageName,
|
|
49
|
+
PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()),
|
|
50
|
+
)
|
|
51
|
+
.metaData
|
|
52
|
+
} else {
|
|
53
|
+
context.packageManager
|
|
54
|
+
.getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
|
|
55
|
+
.metaData
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -108,6 +108,12 @@ class DeviceIntegrityModule(reactContext: ReactApplicationContext) :
|
|
|
108
108
|
signals.pushMap(entry)
|
|
109
109
|
}
|
|
110
110
|
map.putArray("signals", signals)
|
|
111
|
+
|
|
112
|
+
if (report.reportOnly.isNotEmpty()) {
|
|
113
|
+
val reportOnly: WritableArray = Arguments.createArray()
|
|
114
|
+
report.reportOnly.forEach { reportOnly.pushString(it) }
|
|
115
|
+
map.putArray("reportOnly", reportOnly)
|
|
116
|
+
}
|
|
111
117
|
return map
|
|
112
118
|
}
|
|
113
119
|
}
|
|
@@ -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.util.concurrent.FutureTask
|
|
10
11
|
import java.util.concurrent.TimeUnit
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -24,6 +25,8 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
24
25
|
val completed: Boolean,
|
|
25
26
|
val reason: String? = null,
|
|
26
27
|
val signals: List<Signal>,
|
|
28
|
+
/** Signal ids the app configured as reported-only (never compromising on their own). */
|
|
29
|
+
val reportOnly: List<String> = emptyList(),
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
fun run(androidOptions: AndroidTamperOptions = AndroidTamperOptions()): Report {
|
|
@@ -46,6 +49,12 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
runCheck { checkEmulator() }
|
|
52
|
+
// Emulators boot with an unlocked (orange) state too; they're already
|
|
53
|
+
// reported as `emulator`, which isn't compromising by default.
|
|
54
|
+
val bootloaderMode = BootloaderUnlockedMode.read(context)
|
|
55
|
+
if (bootloaderMode != BootloaderUnlockedMode.OFF) {
|
|
56
|
+
runCheck { if ("emulator" in seen) null else checkBootloaderUnlocked() }
|
|
57
|
+
}
|
|
49
58
|
runCheck { checkRootSuBinary() }
|
|
50
59
|
runCheck { checkRootManagementApps() }
|
|
51
60
|
runCheck { checkRootMagiskFiles() }
|
|
@@ -64,6 +73,8 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
64
73
|
completed = completed,
|
|
65
74
|
reason = if (completed) null else "incomplete",
|
|
66
75
|
signals = signals,
|
|
76
|
+
reportOnly =
|
|
77
|
+
if (bootloaderMode == BootloaderUnlockedMode.REPORT) listOf("bootloader_unlocked") else emptyList(),
|
|
67
78
|
)
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -132,6 +143,35 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
132
143
|
)
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
// --- bootloader ---
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Verified boot state as set by the bootloader: `green` (locked, stock keys),
|
|
150
|
+
* `yellow` (locked, user-installed keys), `orange` (unlocked). Readable by any
|
|
151
|
+
* app; a root framework can still spoof the properties for hidden apps.
|
|
152
|
+
*/
|
|
153
|
+
private fun checkBootloaderUnlocked(): Signal? {
|
|
154
|
+
val verifiedBootState = readSystemProperty("ro.boot.verifiedbootstate")
|
|
155
|
+
val flashLocked = readSystemProperty("ro.boot.flash.locked")
|
|
156
|
+
val deviceState = readSystemProperty("ro.boot.vbmeta.device_state")
|
|
157
|
+
|
|
158
|
+
if (verifiedBootState == null && flashLocked == null && deviceState == null) {
|
|
159
|
+
throw IllegalStateException("getprop failed")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
val unlocked =
|
|
163
|
+
verifiedBootState == "orange" || flashLocked == "0" || deviceState == "unlocked"
|
|
164
|
+
if (!unlocked) {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return Signal(
|
|
169
|
+
id = "bootloader_unlocked",
|
|
170
|
+
category = "environment",
|
|
171
|
+
description = "The device bootloader is unlocked",
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
135
175
|
// --- root ---
|
|
136
176
|
|
|
137
177
|
private fun checkRootSuBinary(): Signal? {
|
|
@@ -221,6 +261,9 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
221
261
|
}
|
|
222
262
|
|
|
223
263
|
private fun checkRootMagiskFiles(): Signal? {
|
|
264
|
+
// Legacy locations (older Magisk / su-based roots). Current Magisk, KernelSU
|
|
265
|
+
// and APatch keep their files under /data/adb and a tmpfs that a normal app
|
|
266
|
+
// can't list, so on their own these miss a modern install.
|
|
224
267
|
val paths =
|
|
225
268
|
listOf(
|
|
226
269
|
"/sbin/.magisk",
|
|
@@ -233,7 +276,7 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
233
276
|
"/init.magisk.rc",
|
|
234
277
|
)
|
|
235
278
|
|
|
236
|
-
if (!paths.any { File(it).exists() }) {
|
|
279
|
+
if (!paths.any { File(it).exists() } && !hasRootFrameworkMounts()) {
|
|
237
280
|
return null
|
|
238
281
|
}
|
|
239
282
|
|
|
@@ -244,6 +287,28 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
244
287
|
)
|
|
245
288
|
}
|
|
246
289
|
|
|
290
|
+
/**
|
|
291
|
+
* The mounts a root framework creates (e.g. Magisk's `magisk` tmpfs over
|
|
292
|
+
* /debug_ramdisk and the bin directories) are listed in the app's own mount
|
|
293
|
+
* table, which it can always read — unless the framework unmounts them for
|
|
294
|
+
* this app (Magisk DenyList, KernelSU umount).
|
|
295
|
+
*/
|
|
296
|
+
private fun hasRootFrameworkMounts(): Boolean {
|
|
297
|
+
val sources = setOf("magisk", "ksu", "apatch")
|
|
298
|
+
return File("/proc/self/mountinfo").useLines { lines ->
|
|
299
|
+
lines.any { line ->
|
|
300
|
+
// "<id> <parent> <dev> <root> <mount point> <options> [optional…] - <fstype> <source> <super options>"
|
|
301
|
+
val separator = line.indexOf(" - ")
|
|
302
|
+
if (separator < 0) {
|
|
303
|
+
return@any false
|
|
304
|
+
}
|
|
305
|
+
val mountPoint = line.substring(0, separator).split(' ').getOrNull(4).orEmpty()
|
|
306
|
+
val source = line.substring(separator + 3).split(' ').getOrNull(1).orEmpty()
|
|
307
|
+
source.lowercase() in sources || mountPoint.contains("/.magisk")
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
247
312
|
private fun checkRootTestKeys(): Signal? {
|
|
248
313
|
val tags = Build.TAGS ?: return null
|
|
249
314
|
if (!tags.contains("test-keys")) {
|
|
@@ -272,25 +337,41 @@ internal class IntegrityChecks(private val context: Context) {
|
|
|
272
337
|
)
|
|
273
338
|
}
|
|
274
339
|
|
|
275
|
-
|
|
340
|
+
/**
|
|
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 "".
|
|
344
|
+
*/
|
|
345
|
+
private val systemProperties: Map<String, String>? by lazy { readAllSystemProperties() }
|
|
346
|
+
|
|
347
|
+
private fun readSystemProperty(name: String): String? =
|
|
348
|
+
systemProperties?.let { it[name].orEmpty() }
|
|
349
|
+
|
|
350
|
+
private fun readAllSystemProperties(): Map<String, String>? {
|
|
276
351
|
var process: Process? = null
|
|
277
352
|
return try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
353
|
+
val started = ProcessBuilder("getprop").redirectErrorStream(true).start()
|
|
354
|
+
process = started
|
|
355
|
+
// Drain stdout while waiting: the full dump can exceed the pipe buffer,
|
|
356
|
+
// and getprop would block on write until it's read.
|
|
357
|
+
val output = FutureTask { started.inputStream.bufferedReader().use { it.readText() } }
|
|
358
|
+
Thread(output, "DeviceIntegrity-getprop").apply { isDaemon = true }.start()
|
|
359
|
+
val text = output.get(1, TimeUnit.SECONDS)
|
|
360
|
+
|
|
361
|
+
// Lines look like "[ro.secure]: [1]".
|
|
362
|
+
buildMap {
|
|
363
|
+
text.lineSequence().forEach { line ->
|
|
364
|
+
val separator = line.indexOf("]: [")
|
|
365
|
+
if (line.startsWith("[") && line.endsWith("]") && separator > 0) {
|
|
366
|
+
put(line.substring(1, separator), line.substring(separator + 4, line.length - 1))
|
|
367
|
+
}
|
|
285
368
|
}
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
process.inputStream.bufferedReader().use { it.readText().trim() }
|
|
369
|
+
}.takeIf { it.isNotEmpty() } // an empty dump means we couldn't read it
|
|
289
370
|
} catch (_: Throwable) {
|
|
290
371
|
null
|
|
291
372
|
} finally {
|
|
292
373
|
try {
|
|
293
|
-
process?.
|
|
374
|
+
process?.destroyForcibly()
|
|
294
375
|
} catch (_: Throwable) {
|
|
295
376
|
// ignore
|
|
296
377
|
}
|
package/jest/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeDeviceIntegrity.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../src","sources":["NativeDeviceIntegrity.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAsBlD,eAAeA,mBAAmB,CAACC,GAAG,CAAO,iBAAiB,CAAC","ignoreList":[]}
|
|
@@ -89,7 +89,10 @@ export function resolveStatus(report, options = {}, platform) {
|
|
|
89
89
|
} = partitionIgnored(allSignals, options.ignore);
|
|
90
90
|
const completed = nativeReport.completed === true;
|
|
91
91
|
const compromising = resolveCompromisingCategories(options);
|
|
92
|
-
|
|
92
|
+
// Build-time app config (e.g. bootloader_unlocked: "report"): the signal is
|
|
93
|
+
// still returned, but never makes the status compromised on its own.
|
|
94
|
+
const reportOnly = new Set(Array.isArray(nativeReport.reportOnly) ? nativeReport.reportOnly.filter(id => typeof id === 'string') : []);
|
|
95
|
+
const hasCompromisingSignal = signals.some(signal => compromising.has(signal.category) && !reportOnly.has(signal.id));
|
|
93
96
|
if (hasCompromisingSignal) {
|
|
94
97
|
return {
|
|
95
98
|
status: 'compromised',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["SIGNAL_CATEGORIES","Set","DEFAULT_COMPROMISING_CATEGORIES","coerceCategory","category","has","normalizeSignal","raw","id","description","signal","length","resolveCompromisingCategories","options","base","policy","compromisingCategories","set","treatEmulatorAsCompromised","add","partitionIgnored","signals","ignore","active","ignored","ignoreSet","push","malformed","platform","error","status","durationMs","reason","resolveStatus","report","Array","isArray","nativeReport","allSignals","map","completed","compromising","hasCompromisingSignal","some"],"sourceRoot":"../../src","sources":["resolveStatus.ts"],"mappings":"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
|
|
1
|
+
{"version":3,"names":["SIGNAL_CATEGORIES","Set","DEFAULT_COMPROMISING_CATEGORIES","coerceCategory","category","has","normalizeSignal","raw","id","description","signal","length","resolveCompromisingCategories","options","base","policy","compromisingCategories","set","treatEmulatorAsCompromised","add","partitionIgnored","signals","ignore","active","ignored","ignoreSet","push","malformed","platform","error","status","durationMs","reason","resolveStatus","report","Array","isArray","nativeReport","allSignals","map","completed","compromising","reportOnly","filter","hasCompromisingSignal","some"],"sourceRoot":"../../src","sources":["resolveStatus.ts"],"mappings":"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","ignoreList":[]}
|
package/lib/module/types.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Stable public signal ids. Unknown future ids remain accepted on `Signal.id`.
|
|
5
5
|
*/
|
|
6
|
-
export const SIGNAL_IDS = ['simulator', 'jailbreak_files', 'jailbreak_url_schemes', 'jailbreak_symlinks', 'jailbreak_writable_system', 'hooking_libraries', 'hooking_dyld_insert', 'debugger_attached', 'emulator', 'root_su_binary', 'root_management_apps', 'root_magisk_files', 'root_test_keys', 'root_dangerous_props', 'root_rw_system', 'hooking_frida', 'hooking_xposed', 'tamper_signature_mismatch', 'tamper_untrusted_installer', 'tamper_binary_decrypted', 'tamper_team_id_mismatch'];
|
|
6
|
+
export const SIGNAL_IDS = ['simulator', 'jailbreak_files', 'jailbreak_url_schemes', 'jailbreak_symlinks', 'jailbreak_writable_system', 'hooking_libraries', 'hooking_dyld_insert', 'debugger_attached', 'emulator', 'bootloader_unlocked', 'root_su_binary', 'root_management_apps', 'root_magisk_files', 'root_test_keys', 'root_dangerous_props', 'root_rw_system', 'hooking_frida', 'hooking_xposed', 'tamper_signature_mismatch', 'tamper_untrusted_installer', 'tamper_binary_decrypted', 'tamper_team_id_mismatch'];
|
|
7
7
|
//# sourceMappingURL=types.js.map
|
package/lib/module/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["SIGNAL_IDS"],"sourceRoot":"../../src","sources":["types.ts"],"mappings":";;AAaA;AACA;AACA;AACA,OAAO,MAAMA,UAAU,GAAG,CACxB,WAAW,EACX,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,CACjB","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["SIGNAL_IDS"],"sourceRoot":"../../src","sources":["types.ts"],"mappings":";;AAaA;AACA;AACA;AACA,OAAO,MAAMA,UAAU,GAAG,CACxB,WAAW,EACX,iBAAiB,EACjB,uBAAuB,EACvB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACV,qBAAqB,EACrB,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,2BAA2B,EAC3B,4BAA4B,EAC5B,yBAAyB,EACzB,yBAAyB,CACjB","ignoreList":[]}
|
|
@@ -8,6 +8,8 @@ export type NativeIntegrityReport = {
|
|
|
8
8
|
completed: boolean;
|
|
9
9
|
reason?: string;
|
|
10
10
|
signals: NativeSignal[];
|
|
11
|
+
/** Signal ids the app configured at build time as reported-only (Android). */
|
|
12
|
+
reportOnly?: string[];
|
|
11
13
|
};
|
|
12
14
|
export interface Spec extends TurboModule {
|
|
13
15
|
checkIntegrity(options: Object): Promise<NativeIntegrityReport>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NativeDeviceIntegrity.d.ts","sourceRoot":"","sources":["../../../src/NativeDeviceIntegrity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,YAAY,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"NativeDeviceIntegrity.d.ts","sourceRoot":"","sources":["../../../src/NativeDeviceIntegrity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,WAAW,IAAK,SAAQ,WAAW;IAGvC,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACjE;;AAED,wBAAgE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveStatus.d.ts","sourceRoot":"","sources":["../../../src/resolveStatus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EACV,qBAAqB,EACrB,eAAe,EAKhB,MAAM,YAAS,CAAC;AA4GjB;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,YAAK,EACnC,QAAQ,EAAE,OAAO,QAAQ,CAAC,EAAE,GAC3B,eAAe,
|
|
1
|
+
{"version":3,"file":"resolveStatus.d.ts","sourceRoot":"","sources":["../../../src/resolveStatus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EACV,qBAAqB,EACrB,eAAe,EAKhB,MAAM,YAAS,CAAC;AA4GjB;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,qBAAqB,YAAK,EACnC,QAAQ,EAAE,OAAO,QAAQ,CAAC,EAAE,GAC3B,eAAe,CAiEjB"}
|
|
@@ -4,7 +4,7 @@ export type SignalCategory = 'jailbreak' | 'root' | 'hooking' | 'debugger' | 'em
|
|
|
4
4
|
/**
|
|
5
5
|
* Stable public signal ids. Unknown future ids remain accepted on `Signal.id`.
|
|
6
6
|
*/
|
|
7
|
-
export declare const SIGNAL_IDS: readonly ["simulator", "jailbreak_files", "jailbreak_url_schemes", "jailbreak_symlinks", "jailbreak_writable_system", "hooking_libraries", "hooking_dyld_insert", "debugger_attached", "emulator", "root_su_binary", "root_management_apps", "root_magisk_files", "root_test_keys", "root_dangerous_props", "root_rw_system", "hooking_frida", "hooking_xposed", "tamper_signature_mismatch", "tamper_untrusted_installer", "tamper_binary_decrypted", "tamper_team_id_mismatch"];
|
|
7
|
+
export declare const SIGNAL_IDS: readonly ["simulator", "jailbreak_files", "jailbreak_url_schemes", "jailbreak_symlinks", "jailbreak_writable_system", "hooking_libraries", "hooking_dyld_insert", "debugger_attached", "emulator", "bootloader_unlocked", "root_su_binary", "root_management_apps", "root_magisk_files", "root_test_keys", "root_dangerous_props", "root_rw_system", "hooking_frida", "hooking_xposed", "tamper_signature_mismatch", "tamper_untrusted_installer", "tamper_binary_decrypted", "tamper_team_id_mismatch"];
|
|
8
8
|
export type SignalId = (typeof SIGNAL_IDS)[number];
|
|
9
9
|
export interface Signal {
|
|
10
10
|
/** Known ids autocomplete; unknown future ids are still accepted. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,aAAa,GAAG,SAAS,CAAC;AAElE,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,QAAQ,GACR,aAAa,CAAC;AAElB;;GAEG;AACH,eAAO,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,aAAa,GAAG,SAAS,CAAC;AAElE,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,MAAM,GACN,SAAS,GACT,UAAU,GACV,UAAU,GACV,QAAQ,GACR,aAAa,CAAC;AAElB;;GAEG;AACH,eAAO,MAAM,UAAU,0eAuBb,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnD,MAAM,WAAW,MAAM;IACrB,qEAAqE;IACrE,EAAE,EAAE,QAAQ,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,cAAc,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,aAAa,GACrB,sBAAsB,GACtB,2BAA2B,GAC3B,cAAc,GACd,SAAS,GACT,YAAY,GACZ,iBAAiB,CAAC;AAEtB,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,QAAQ,CAAC,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACtC,0FAA0F;IAC1F,2BAA2B,CAAC,EAAE,MAAM,EAAE,CAAC;IACvC,wEAAwE;IACxE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,uFAAuF;IACvF,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,cAAc,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,sGAAsG;IACtG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,qEAAqE;IACrE,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAClC,iEAAiE;IACjE,GAAG,CAAC,EAAE,mBAAmB,CAAC;CAC3B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-device-integrity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
package/plugin/build/index.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { type ConfigPlugin, type InfoPlist } from 'expo/config-plugins';
|
|
1
|
+
import { type AndroidManifest, type ConfigPlugin, type InfoPlist } from 'expo/config-plugins';
|
|
2
2
|
export declare const JAILBREAK_URL_SCHEMES: readonly ["cydia", "sileo", "zbra", "filza", "undecimus", "activator"];
|
|
3
|
+
/** How `bootloader_unlocked` affects status. Default `'compromised'`. */
|
|
4
|
+
export type BootloaderUnlockedMode = 'compromised' | 'report' | 'off';
|
|
5
|
+
export declare const BOOTLOADER_UNLOCKED_META_DATA = "com.deviceintegrity.bootloader_unlocked";
|
|
3
6
|
export type DeviceIntegrityPluginProps = {
|
|
4
7
|
ios?: {
|
|
5
8
|
/** When false, skip Info.plist changes. Default true. */
|
|
6
9
|
jailbreakUrlSchemes?: boolean;
|
|
7
10
|
};
|
|
11
|
+
android?: {
|
|
12
|
+
/**
|
|
13
|
+
* `'compromised'` (default): an unlocked bootloader makes status compromised.
|
|
14
|
+
* `'report'`: the signal is returned but never compromising on its own.
|
|
15
|
+
* `'off'`: the check doesn't run.
|
|
16
|
+
*/
|
|
17
|
+
bootloaderUnlocked?: BootloaderUnlockedMode;
|
|
18
|
+
};
|
|
8
19
|
};
|
|
9
20
|
/**
|
|
10
21
|
* Merge jailbreak URL schemes into LSApplicationQueriesSchemes.
|
|
@@ -14,6 +25,11 @@ export type DeviceIntegrityPluginProps = {
|
|
|
14
25
|
* automatically via the Gradle manifest merger.
|
|
15
26
|
*/
|
|
16
27
|
export declare function addJailbreakQueriesSchemes(infoPlist: InfoPlist): InfoPlist;
|
|
28
|
+
/**
|
|
29
|
+
* Write (or, for the default `'compromised'`, remove) the `<meta-data>` the
|
|
30
|
+
* native module reads on the main `<application>`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function setBootloaderUnlockedMetaData(manifest: AndroidManifest, mode: BootloaderUnlockedMode | undefined): AndroidManifest;
|
|
17
33
|
export declare const withDeviceIntegrity: ConfigPlugin<DeviceIntegrityPluginProps | void>;
|
|
18
34
|
declare const _default: ConfigPlugin<void | DeviceIntegrityPluginProps>;
|
|
19
35
|
export default _default;
|
package/plugin/build/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.withDeviceIntegrity = exports.JAILBREAK_URL_SCHEMES = void 0;
|
|
3
|
+
exports.withDeviceIntegrity = exports.BOOTLOADER_UNLOCKED_META_DATA = exports.JAILBREAK_URL_SCHEMES = void 0;
|
|
4
4
|
exports.addJailbreakQueriesSchemes = addJailbreakQueriesSchemes;
|
|
5
|
+
exports.setBootloaderUnlockedMetaData = setBootloaderUnlockedMetaData;
|
|
5
6
|
const config_plugins_1 = require("expo/config-plugins");
|
|
6
7
|
// keep in sync with ios/DIIntegrityChecks.m hasJailbreakURLSchemes
|
|
7
8
|
exports.JAILBREAK_URL_SCHEMES = [
|
|
@@ -12,6 +13,13 @@ exports.JAILBREAK_URL_SCHEMES = [
|
|
|
12
13
|
'undecimus',
|
|
13
14
|
'activator',
|
|
14
15
|
];
|
|
16
|
+
// keep in sync with android/.../BootloaderUnlockedMode.kt
|
|
17
|
+
exports.BOOTLOADER_UNLOCKED_META_DATA = 'com.deviceintegrity.bootloader_unlocked';
|
|
18
|
+
const BOOTLOADER_UNLOCKED_MODES = [
|
|
19
|
+
'compromised',
|
|
20
|
+
'report',
|
|
21
|
+
'off',
|
|
22
|
+
];
|
|
15
23
|
/**
|
|
16
24
|
* Merge jailbreak URL schemes into LSApplicationQueriesSchemes.
|
|
17
25
|
* Keeps existing entries/order, appends missing ones, never removes.
|
|
@@ -34,15 +42,48 @@ function addJailbreakQueriesSchemes(infoPlist) {
|
|
|
34
42
|
LSApplicationQueriesSchemes: current,
|
|
35
43
|
};
|
|
36
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Write (or, for the default `'compromised'`, remove) the `<meta-data>` the
|
|
47
|
+
* native module reads on the main `<application>`.
|
|
48
|
+
*/
|
|
49
|
+
function setBootloaderUnlockedMetaData(manifest, mode) {
|
|
50
|
+
var _a, _b;
|
|
51
|
+
if (mode != null && !BOOTLOADER_UNLOCKED_MODES.includes(mode)) {
|
|
52
|
+
throw new Error(`react-native-device-integrity: android.bootloaderUnlocked must be one of ${BOOTLOADER_UNLOCKED_MODES.map((value) => `"${value}"`).join(', ')} (got ${JSON.stringify(mode)})`);
|
|
53
|
+
}
|
|
54
|
+
const application = (_a = manifest.manifest.application) === null || _a === void 0 ? void 0 : _a[0];
|
|
55
|
+
if (application == null) {
|
|
56
|
+
return manifest;
|
|
57
|
+
}
|
|
58
|
+
const others = ((_b = application['meta-data']) !== null && _b !== void 0 ? _b : []).filter((item) => item.$['android:name'] !== exports.BOOTLOADER_UNLOCKED_META_DATA);
|
|
59
|
+
application['meta-data'] =
|
|
60
|
+
mode == null || mode === 'compromised'
|
|
61
|
+
? others
|
|
62
|
+
: [
|
|
63
|
+
...others,
|
|
64
|
+
{
|
|
65
|
+
$: {
|
|
66
|
+
'android:name': exports.BOOTLOADER_UNLOCKED_META_DATA,
|
|
67
|
+
'android:value': mode,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
return manifest;
|
|
72
|
+
}
|
|
37
73
|
const withDeviceIntegrity = (config, props) => {
|
|
38
74
|
var _a;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return (0, config_plugins_1.withInfoPlist)(config, (modConfig) => {
|
|
43
|
-
modConfig.modResults = addJailbreakQueriesSchemes(modConfig.modResults);
|
|
75
|
+
let result = (0, config_plugins_1.withAndroidManifest)(config, (modConfig) => {
|
|
76
|
+
var _a;
|
|
77
|
+
modConfig.modResults = setBootloaderUnlockedMetaData(modConfig.modResults, (_a = props === null || props === void 0 ? void 0 : props.android) === null || _a === void 0 ? void 0 : _a.bootloaderUnlocked);
|
|
44
78
|
return modConfig;
|
|
45
79
|
});
|
|
80
|
+
if (((_a = props === null || props === void 0 ? void 0 : props.ios) === null || _a === void 0 ? void 0 : _a.jailbreakUrlSchemes) !== false) {
|
|
81
|
+
result = (0, config_plugins_1.withInfoPlist)(result, (modConfig) => {
|
|
82
|
+
modConfig.modResults = addJailbreakQueriesSchemes(modConfig.modResults);
|
|
83
|
+
return modConfig;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
46
87
|
};
|
|
47
88
|
exports.withDeviceIntegrity = withDeviceIntegrity;
|
|
48
89
|
const pkg = require('../../package.json');
|
|
@@ -11,6 +11,8 @@ export type NativeIntegrityReport = {
|
|
|
11
11
|
completed: boolean;
|
|
12
12
|
reason?: string;
|
|
13
13
|
signals: NativeSignal[];
|
|
14
|
+
/** Signal ids the app configured at build time as reported-only (Android). */
|
|
15
|
+
reportOnly?: string[];
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
export interface Spec extends TurboModule {
|
package/src/resolveStatus.ts
CHANGED
|
@@ -141,9 +141,18 @@ export function resolveStatus(
|
|
|
141
141
|
);
|
|
142
142
|
const completed = nativeReport.completed === true;
|
|
143
143
|
const compromising = resolveCompromisingCategories(options);
|
|
144
|
+
// Build-time app config (e.g. bootloader_unlocked: "report"): the signal is
|
|
145
|
+
// still returned, but never makes the status compromised on its own.
|
|
146
|
+
const reportOnly = new Set(
|
|
147
|
+
Array.isArray(nativeReport.reportOnly)
|
|
148
|
+
? nativeReport.reportOnly.filter(
|
|
149
|
+
(id): id is string => typeof id === 'string'
|
|
150
|
+
)
|
|
151
|
+
: []
|
|
152
|
+
);
|
|
144
153
|
|
|
145
|
-
const hasCompromisingSignal = signals.some(
|
|
146
|
-
compromising.has(signal.category)
|
|
154
|
+
const hasCompromisingSignal = signals.some(
|
|
155
|
+
(signal) => compromising.has(signal.category) && !reportOnly.has(signal.id)
|
|
147
156
|
);
|
|
148
157
|
|
|
149
158
|
if (hasCompromisingSignal) {
|