react-native-rootbeer-checkroot 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.
- package/LICENSE +20 -0
- package/README.md +799 -0
- package/RootBeer.podspec +20 -0
- package/android/build.gradle +52 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/rootbeer/RootBeerModule.kt +62 -0
- package/android/src/main/java/com/rootbeer/RootBeerPackage.kt +31 -0
- package/ios/RootBeer.h +5 -0
- package/ios/RootBeer.mm +21 -0
- package/lib/module/NativeRootBeer.js +5 -0
- package/lib/module/NativeRootBeer.js.map +1 -0
- package/lib/module/RootBeer.js +110 -0
- package/lib/module/RootBeer.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeRootBeer.d.ts +33 -0
- package/lib/typescript/src/NativeRootBeer.d.ts.map +1 -0
- package/lib/typescript/src/RootBeer.d.ts +82 -0
- package/lib/typescript/src/RootBeer.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/package.json +182 -0
- package/src/NativeRootBeer.ts +44 -0
- package/src/RootBeer.ts +118 -0
- package/src/index.tsx +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
# react-native-root-beer
|
|
2
|
+
|
|
3
|
+
A **React Native New Architecture (TurboModule)** wrapper around the battle-tested Android root detection library [`com.scottyab:rootbeer-lib`](https://github.com/scottyab/rootbeer). It exposes 12 granular detection methods over JSI (JavaScript Interface) — no bridge overhead, no threading surprises.
|
|
4
|
+
|
|
5
|
+
> **⚠️ Android Only.** This library is strictly an Android module. Its TurboModule is registered with `TurboModuleRegistry.getEnforcing`, which means calling any method on a non-Android platform (iOS, web) will throw a runtime error. All calls must be guarded with `Platform.OS === 'android'`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [How It Works](#how-it-works)
|
|
12
|
+
- [Requirements](#requirements)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Bare React Native](#bare-react-native)
|
|
15
|
+
- [Expo (Managed & Bare Workflow)](#expo-managed--bare-workflow)
|
|
16
|
+
- [Native Android Configuration](#native-android-configuration)
|
|
17
|
+
- [Usage Guide](#usage-guide)
|
|
18
|
+
- [Quick Start — Single Root Check](#quick-start--single-root-check)
|
|
19
|
+
- [Granular Detection Scan](#granular-detection-scan)
|
|
20
|
+
- [Reusable React Hook](#reusable-react-hook)
|
|
21
|
+
- [Navigation Guard Pattern](#navigation-guard-pattern)
|
|
22
|
+
- [API Reference](#api-reference)
|
|
23
|
+
- [Detection Method Deep Dive](#detection-method-deep-dive)
|
|
24
|
+
- [Platform Guard Reference](#platform-guard-reference)
|
|
25
|
+
- [Architecture](#architecture)
|
|
26
|
+
- [Troubleshooting](#troubleshooting)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## How It Works
|
|
31
|
+
|
|
32
|
+
`react-native-root-beer` is built as a **TurboModule** using React Native's New Architecture (Codegen). Instead of going through the old asynchronous JS bridge, every method call crosses the native boundary via JSI — synchronously and with zero serialization overhead.
|
|
33
|
+
|
|
34
|
+
The library ships its own `android/build.gradle` which declares `com.scottyab:rootbeer-lib:0.1.2` as a Gradle `implementation` dependency. When any consumer app installs this package, Gradle resolves and downloads the RootBeer SDK automatically as a **transitive dependency**. You never need to touch your app's `android/` folder.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
| Requirement | Version |
|
|
41
|
+
|---|---|
|
|
42
|
+
| React Native | ≥ 0.73 (New Architecture required) |
|
|
43
|
+
| Android `minSdkVersion` | **24** (Android 7.0 Nougat) |
|
|
44
|
+
| Android `compileSdkVersion` | 36 |
|
|
45
|
+
| Java source/target compatibility | **17** |
|
|
46
|
+
| Kotlin | 2.0.21 (bundled in the library) |
|
|
47
|
+
| `react-native-root-beer` | 0.1.0 |
|
|
48
|
+
|
|
49
|
+
> **New Architecture is mandatory.** This library is a TurboModule — it does not have an Old Architecture (bridge) fallback. Ensure `newArchEnabled=true` is set in your project's `android/gradle.properties`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
### Bare React Native
|
|
56
|
+
|
|
57
|
+
**Step 1 — Install the package**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# npm
|
|
61
|
+
npm install react-native-root-beer
|
|
62
|
+
|
|
63
|
+
# yarn
|
|
64
|
+
yarn add react-native-root-beer
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Step 2 — Enable New Architecture**
|
|
68
|
+
|
|
69
|
+
Open `android/gradle.properties` and confirm the following line is present and not commented out:
|
|
70
|
+
|
|
71
|
+
```properties
|
|
72
|
+
# android/gradle.properties
|
|
73
|
+
newArchEnabled=true
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Step 3 — Rebuild the native layer**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd android && ./gradlew clean
|
|
80
|
+
cd ..
|
|
81
|
+
npx react-native run-android
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Gradle will automatically resolve `com.scottyab:rootbeer-lib:0.1.2` from Maven Central during this build. No manual `build.gradle` edits are required.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### Expo (Managed & Bare Workflow)
|
|
89
|
+
|
|
90
|
+
Because this library contains native Android code, it **cannot** run in the Expo Go client. You must use a [development build](https://docs.expo.dev/develop/development-builds/introduction/).
|
|
91
|
+
|
|
92
|
+
**Step 1 — Install the package**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx expo install react-native-root-beer
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Step 2 — Enable New Architecture in your Expo config**
|
|
99
|
+
|
|
100
|
+
In `app.json` or `app.config.js`, ensure the new architecture flag is set:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"expo": {
|
|
105
|
+
"android": {
|
|
106
|
+
"newArchEnabled": true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Step 3 — Run prebuild**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npx expo prebuild --platform android
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> **Why this is safe with Expo:** The library declares its own Gradle dependency inside its own `android/build.gradle`. When `expo prebuild` (re)generates the native `android/` folder, it does **not** wipe library-level Gradle files — only the app-level files are regenerated. Your native configuration is therefore **upgrade-safe** and survives every future `expo prebuild` run.
|
|
119
|
+
|
|
120
|
+
**Step 4 — Build a development client or production build**
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
# Development build (EAS)
|
|
124
|
+
eas build --profile development --platform android
|
|
125
|
+
|
|
126
|
+
# Or run locally
|
|
127
|
+
npx expo run:android
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Native Android Configuration
|
|
133
|
+
|
|
134
|
+
**None required.**
|
|
135
|
+
|
|
136
|
+
This is the primary design goal of `react-native-root-beer`. The `com.scottyab:rootbeer-lib:0.1.2` dependency is declared inside the library's own `android/build.gradle`:
|
|
137
|
+
|
|
138
|
+
```groovy
|
|
139
|
+
// This lives inside the library — you do not write this yourself.
|
|
140
|
+
dependencies {
|
|
141
|
+
implementation "com.facebook.react:react-android"
|
|
142
|
+
implementation "com.scottyab:rootbeer-lib:0.1.2"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Gradle's dependency resolution propagates this to your app's build graph automatically.
|
|
147
|
+
|
|
148
|
+
**No `AndroidManifest.xml` permissions are needed.** The library's own manifest is empty — root detection is performed entirely through file-system introspection, system property reads, and process execution, none of which require declared permissions on Android.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Usage Guide
|
|
153
|
+
|
|
154
|
+
### Quick Start — Single Root Check
|
|
155
|
+
|
|
156
|
+
The simplest possible integration. Use `RootBeer.isRooted()` to run all available checks in one call.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import React, { useEffect, useState } from 'react';
|
|
160
|
+
import { Platform, Text, View, StyleSheet } from 'react-native';
|
|
161
|
+
import { RootBeer } from 'react-native-root-beer';
|
|
162
|
+
|
|
163
|
+
export default function App() {
|
|
164
|
+
const [isRooted, setIsRooted] = useState<boolean | null>(null);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
// Always guard with Platform.OS — this library is Android-only.
|
|
168
|
+
if (Platform.OS === 'android') {
|
|
169
|
+
const result = RootBeer.isRooted();
|
|
170
|
+
setIsRooted(result);
|
|
171
|
+
}
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
if (Platform.OS !== 'android') {
|
|
175
|
+
return (
|
|
176
|
+
<View style={styles.container}>
|
|
177
|
+
<Text>Root detection is only available on Android.</Text>
|
|
178
|
+
</View>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<View style={styles.container}>
|
|
184
|
+
{isRooted === null ? (
|
|
185
|
+
<Text>Checking device integrity...</Text>
|
|
186
|
+
) : (
|
|
187
|
+
<Text style={isRooted ? styles.danger : styles.safe}>
|
|
188
|
+
{isRooted ? '⚠️ Device is rooted' : '✅ Device is not rooted'}
|
|
189
|
+
</Text>
|
|
190
|
+
)}
|
|
191
|
+
</View>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const styles = StyleSheet.create({
|
|
196
|
+
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
|
197
|
+
danger: { color: '#e53e3e', fontSize: 18, fontWeight: 'bold' },
|
|
198
|
+
safe: { color: '#38a169', fontSize: 18, fontWeight: 'bold' },
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> **Note:** All `RootBeer` methods are **synchronous**. They return `boolean` directly — no `await`, no `.then()`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Granular Detection Scan
|
|
207
|
+
|
|
208
|
+
For security-sensitive applications, run each check individually to understand *why* a device is flagged and log the results for your backend.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { Platform } from 'react-native';
|
|
212
|
+
import { RootBeer } from 'react-native-root-beer';
|
|
213
|
+
|
|
214
|
+
interface RootScanResult {
|
|
215
|
+
isRooted: boolean;
|
|
216
|
+
isRootedWithoutBusyBoxCheck: boolean;
|
|
217
|
+
hasRootManagementApps: boolean;
|
|
218
|
+
hasDangerousApps: boolean;
|
|
219
|
+
hasTestKeys: boolean;
|
|
220
|
+
hasSuBinary: boolean;
|
|
221
|
+
hasMagiskBinary: boolean;
|
|
222
|
+
suExecutes: boolean;
|
|
223
|
+
hasDangerousProps: boolean;
|
|
224
|
+
hasRWPaths: boolean;
|
|
225
|
+
hasRootCloakingApps: boolean;
|
|
226
|
+
hasBusyBox: boolean;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Runs every available RootBeer check and returns a structured result.
|
|
231
|
+
* Call this on Android only.
|
|
232
|
+
*/
|
|
233
|
+
export function performFullRootScan(): RootScanResult {
|
|
234
|
+
if (Platform.OS !== 'android') {
|
|
235
|
+
throw new Error('performFullRootScan() is only supported on Android.');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
// High-level composite checks
|
|
240
|
+
isRooted: RootBeer.isRooted(),
|
|
241
|
+
isRootedWithoutBusyBoxCheck: RootBeer.isRootedWithoutBusyBoxCheck(),
|
|
242
|
+
|
|
243
|
+
// Installed app checks
|
|
244
|
+
hasRootManagementApps: RootBeer.detectRootManagementApps(),
|
|
245
|
+
hasDangerousApps: RootBeer.detectPotentiallyDangerousApps(),
|
|
246
|
+
hasRootCloakingApps: RootBeer.detectRootCloakingApps(),
|
|
247
|
+
|
|
248
|
+
// Build / ROM checks
|
|
249
|
+
hasTestKeys: RootBeer.detectTestKeys(),
|
|
250
|
+
|
|
251
|
+
// Binary presence checks (static — does not execute anything)
|
|
252
|
+
hasSuBinary: RootBeer.checkForSuBinary(),
|
|
253
|
+
hasMagiskBinary: RootBeer.checkForMagiskBinary(),
|
|
254
|
+
hasBusyBox: RootBeer.checkForBinary('busybox'),
|
|
255
|
+
|
|
256
|
+
// Runtime probe (actually attempts to run `su`)
|
|
257
|
+
suExecutes: RootBeer.checkSuExists(),
|
|
258
|
+
|
|
259
|
+
// System property & filesystem checks
|
|
260
|
+
hasDangerousProps: RootBeer.checkForDangerousProps(),
|
|
261
|
+
hasRWPaths: RootBeer.checkForRWPaths(),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Usage ---
|
|
266
|
+
if (Platform.OS === 'android') {
|
|
267
|
+
const scan = performFullRootScan();
|
|
268
|
+
|
|
269
|
+
if (scan.isRooted) {
|
|
270
|
+
console.warn('Root indicators detected:', scan);
|
|
271
|
+
// Send `scan` to your analytics / security backend for forensics
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
### Reusable React Hook
|
|
279
|
+
|
|
280
|
+
Encapsulate all detection logic in a custom hook for clean, idiomatic React consumption across your app.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// hooks/useRootDetection.ts
|
|
284
|
+
import { useEffect, useState } from 'react';
|
|
285
|
+
import { Platform } from 'react-native';
|
|
286
|
+
import { RootBeer } from 'react-native-root-beer';
|
|
287
|
+
|
|
288
|
+
interface RootDetectionState {
|
|
289
|
+
/** True if any root indicator was found. */
|
|
290
|
+
isRooted: boolean;
|
|
291
|
+
/** True while the detection is running (first render only). */
|
|
292
|
+
isChecking: boolean;
|
|
293
|
+
/** True if the check completed without errors. */
|
|
294
|
+
isReady: boolean;
|
|
295
|
+
/** Populated if an unexpected error occurred during detection. */
|
|
296
|
+
error: Error | null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Runs `RootBeer.isRooted()` once on mount.
|
|
301
|
+
* On non-Android platforms, `isRooted` is always `false` and `isReady`
|
|
302
|
+
* is `false` to prevent false security guarantees.
|
|
303
|
+
*/
|
|
304
|
+
export function useRootDetection(): RootDetectionState {
|
|
305
|
+
const [state, setState] = useState<RootDetectionState>({
|
|
306
|
+
isRooted: false,
|
|
307
|
+
isChecking: Platform.OS === 'android',
|
|
308
|
+
isReady: false,
|
|
309
|
+
error: null,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
if (Platform.OS !== 'android') {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
const isRooted = RootBeer.isRooted();
|
|
319
|
+
setState({ isRooted, isChecking: false, isReady: true, error: null });
|
|
320
|
+
} catch (err) {
|
|
321
|
+
setState({
|
|
322
|
+
isRooted: false,
|
|
323
|
+
isChecking: false,
|
|
324
|
+
isReady: false,
|
|
325
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}, []);
|
|
329
|
+
|
|
330
|
+
return state;
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// components/SecureScreen.tsx
|
|
336
|
+
import React from 'react';
|
|
337
|
+
import { ActivityIndicator, Text, View } from 'react-native';
|
|
338
|
+
import { useRootDetection } from '../hooks/useRootDetection';
|
|
339
|
+
|
|
340
|
+
export function SecureScreen() {
|
|
341
|
+
const { isRooted, isChecking, error } = useRootDetection();
|
|
342
|
+
|
|
343
|
+
if (isChecking) {
|
|
344
|
+
return <ActivityIndicator size="large" />;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (error) {
|
|
348
|
+
// Treat detection failure conservatively — block access
|
|
349
|
+
return <Text>Security check failed. Access denied.</Text>;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (isRooted) {
|
|
353
|
+
return (
|
|
354
|
+
<View>
|
|
355
|
+
<Text>⚠️ This app cannot run on a rooted device.</Text>
|
|
356
|
+
</View>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return <Text>✅ Device integrity verified. Welcome!</Text>;
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### Navigation Guard Pattern
|
|
367
|
+
|
|
368
|
+
Block access to sensitive screens at the navigation level. This example uses React Navigation, but the pattern applies to any router.
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// navigation/RootGuard.tsx
|
|
372
|
+
import React, { useEffect } from 'react';
|
|
373
|
+
import { Platform, Alert } from 'react-native';
|
|
374
|
+
import { useNavigation } from '@react-navigation/native';
|
|
375
|
+
import { RootBeer } from 'react-native-root-beer';
|
|
376
|
+
|
|
377
|
+
interface RootGuardProps {
|
|
378
|
+
children: React.ReactNode;
|
|
379
|
+
/** Screen name to redirect to when root is detected. */
|
|
380
|
+
fallbackScreen?: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Wraps a screen and redirects away if the device is rooted.
|
|
385
|
+
* Must be rendered inside a React Navigation navigator.
|
|
386
|
+
*/
|
|
387
|
+
export function RootGuard({ children, fallbackScreen = 'Home' }: RootGuardProps) {
|
|
388
|
+
const navigation = useNavigation();
|
|
389
|
+
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (Platform.OS !== 'android') {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const isRooted = RootBeer.isRooted();
|
|
396
|
+
|
|
397
|
+
if (isRooted) {
|
|
398
|
+
Alert.alert(
|
|
399
|
+
'Security Warning',
|
|
400
|
+
'This feature is not available on rooted devices.',
|
|
401
|
+
[{ text: 'OK', onPress: () => navigation.navigate(fallbackScreen as never) }],
|
|
402
|
+
{ cancelable: false }
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}, [navigation, fallbackScreen]);
|
|
406
|
+
|
|
407
|
+
return <>{children}</>;
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// Inside your screen component
|
|
413
|
+
function PaymentScreen() {
|
|
414
|
+
return (
|
|
415
|
+
<RootGuard fallbackScreen="Home">
|
|
416
|
+
{/* Sensitive payment UI renders here */}
|
|
417
|
+
</RootGuard>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## API Reference
|
|
425
|
+
|
|
426
|
+
All methods are synchronous and return `boolean`. They are exposed on the `RootBeer` object (`as const`) imported from `react-native-root-beer`.
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
import { RootBeer } from 'react-native-root-beer';
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Method Summary
|
|
433
|
+
|
|
434
|
+
| Method | Parameters | Returns | Description |
|
|
435
|
+
|---|---|---|---|
|
|
436
|
+
| `isRooted()` | — | `boolean` | Runs **all** detection checks, including BusyBox. The primary, recommended check. |
|
|
437
|
+
| `isRootedWithoutBusyBoxCheck()` | — | `boolean` | Runs all checks **except** BusyBox. Use when BusyBox causes false positives. |
|
|
438
|
+
| `detectRootManagementApps()` | — | `boolean` | Scans installed apps for known root managers (SuperSU, Magisk Manager, KingRoot, etc.). |
|
|
439
|
+
| `detectPotentiallyDangerousApps()` | — | `boolean` | Scans installed apps for apps commonly associated with root exploitation. |
|
|
440
|
+
| `detectRootCloakingApps()` | — | `boolean` | Scans installed apps for apps that conceal root from other apps (RootCloak, Hide My Root). |
|
|
441
|
+
| `detectTestKeys()` | — | `boolean` | Checks `android.os.Build.TAGS` for `"test-keys"` — a signature of unofficial/custom ROMs. |
|
|
442
|
+
| `checkForSuBinary()` | — | `boolean` | Scans a list of known filesystem paths for the `su` executable. **Static — does not execute anything.** |
|
|
443
|
+
| `checkForMagiskBinary()` | — | `boolean` | Scans filesystem paths for the Magisk binary. Effective even when Magisk is in hide mode. |
|
|
444
|
+
| `checkForBinary(binaryName)` | `binaryName: string` | `boolean` | Scans filesystem paths for **any named binary**. Pass `"busybox"`, `"su"`, or any other name. |
|
|
445
|
+
| `checkSuExists()` | — | `boolean` | **Attempts to execute `su`**. Returns `true` if the call succeeds. Runtime probe — more invasive than `checkForSuBinary()`. |
|
|
446
|
+
| `checkForDangerousProps()` | — | `boolean` | Reads system properties and returns `true` if dangerous values are set (e.g. `ro.debuggable=1`, `ro.secure=0`). |
|
|
447
|
+
| `checkForRWPaths()` | — | `boolean` | Inspects `/proc/mounts` for filesystem paths that should be read-only but are mounted read-write. |
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
### Method Detail
|
|
452
|
+
|
|
453
|
+
#### `RootBeer.isRooted(): boolean`
|
|
454
|
+
|
|
455
|
+
The top-level composite check. Internally calls every other detection method (including BusyBox) and returns `true` if **any single indicator** is found.
|
|
456
|
+
|
|
457
|
+
**Use this** for the vast majority of use cases. Only fall back to individual methods when you need to understand which specific indicator triggered.
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
const safe = !RootBeer.isRooted();
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
#### `RootBeer.isRootedWithoutBusyBoxCheck(): boolean`
|
|
466
|
+
|
|
467
|
+
Identical to `isRooted()`, but skips the BusyBox binary check. BusyBox is a legitimate developer tool that may be present on non-malicious devices (e.g. some manufacturer ROMs include it). Use this variant if you are seeing false positives from `isRooted()` in your production user base.
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// Prefer this if you're seeing false positives from isRooted()
|
|
471
|
+
const safe = !RootBeer.isRootedWithoutBusyBoxCheck();
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
#### `RootBeer.detectRootManagementApps(): boolean`
|
|
477
|
+
|
|
478
|
+
Queries the Android `PackageManager` for the package names of well-known root management applications. Returns `true` if any are installed.
|
|
479
|
+
|
|
480
|
+
**Examples of detected apps:** SuperSU, Magisk Manager, KingRoot, Towelroot, BaiduEasyRoot.
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
if (RootBeer.detectRootManagementApps()) {
|
|
484
|
+
console.warn('Root management app found on device.');
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
#### `RootBeer.detectPotentiallyDangerousApps(): boolean`
|
|
491
|
+
|
|
492
|
+
Queries the `PackageManager` for apps that are commonly used to exploit, abuse, or probe rooted environments — even if they are not root managers themselves.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
if (RootBeer.detectPotentiallyDangerousApps()) {
|
|
496
|
+
console.warn('Potentially dangerous app detected.');
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
#### `RootBeer.detectRootCloakingApps(): boolean`
|
|
503
|
+
|
|
504
|
+
Scans for root-hiding apps (e.g. RootCloak, MagiskHide-related helpers) whose presence implies a deliberate attempt to conceal root access from the system. A positive result here is a particularly strong signal of malicious intent.
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
if (RootBeer.detectRootCloakingApps()) {
|
|
508
|
+
// High-confidence indicator — user is actively trying to hide root
|
|
509
|
+
console.warn('Root cloaking app detected.');
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
#### `RootBeer.detectTestKeys(): boolean`
|
|
516
|
+
|
|
517
|
+
Reads `android.os.Build.TAGS` and returns `true` if it contains `"test-keys"`. Production devices from manufacturers are always signed with `"release-keys"`. Test-key builds indicate either a custom ROM or developer engineering builds — both of which typically have root access.
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
if (RootBeer.detectTestKeys()) {
|
|
521
|
+
console.warn('Device is running a custom or test-key ROM.');
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
#### `RootBeer.checkForSuBinary(): boolean`
|
|
528
|
+
|
|
529
|
+
Scans a curated list of common binary paths (e.g. `/system/bin/su`, `/system/xbin/su`, `/sbin/su`, `/data/local/xbin/su`, etc.) for the presence of the `su` executable. This is a **static file check** — it does not execute `su`.
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
if (RootBeer.checkForSuBinary()) {
|
|
533
|
+
console.warn('su binary found on filesystem.');
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
#### `RootBeer.checkForMagiskBinary(): boolean`
|
|
540
|
+
|
|
541
|
+
Scans common paths for the Magisk binary. Useful as a secondary check because Magisk's systemless root approach can evade package-manager-based detection (`detectRootManagementApps`) if the Magisk Manager APK is uninstalled.
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
if (RootBeer.checkForMagiskBinary()) {
|
|
545
|
+
console.warn('Magisk binary present — device likely uses Magisk root.');
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
#### `RootBeer.checkForBinary(binaryName: string): boolean`
|
|
552
|
+
|
|
553
|
+
| Parameter | Type | Required | Description |
|
|
554
|
+
|---|---|---|---|
|
|
555
|
+
| `binaryName` | `string` | ✅ | The binary filename to scan for (e.g. `"su"`, `"busybox"`, `"magisk"`). |
|
|
556
|
+
|
|
557
|
+
Scans the same set of filesystem paths as `checkForSuBinary()` and `checkForMagiskBinary()`, but for an **arbitrary binary name**. Use this for custom checks beyond the preset methods.
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// Check for busybox specifically
|
|
561
|
+
const hasBusyBox = RootBeer.checkForBinary('busybox');
|
|
562
|
+
|
|
563
|
+
// Check for any custom binary
|
|
564
|
+
const hasFridaServer = RootBeer.checkForBinary('frida-server');
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
#### `RootBeer.checkSuExists(): boolean`
|
|
570
|
+
|
|
571
|
+
Attempts to **execute** `su` using `Runtime.exec()` and returns `true` if the process starts successfully. This is a runtime probe — the most definitive `su` check, but also the most invasive. On a hardened device, this call might be logged by security software.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// Definitive but invasive — su is actually invoked
|
|
575
|
+
if (RootBeer.checkSuExists()) {
|
|
576
|
+
console.warn('su is executable on this device.');
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
#### `RootBeer.checkForDangerousProps(): boolean`
|
|
583
|
+
|
|
584
|
+
Reads Android system properties via `getprop` and returns `true` if any of the following dangerous values are set:
|
|
585
|
+
|
|
586
|
+
| Property | Dangerous Value |
|
|
587
|
+
|---|---|
|
|
588
|
+
| `ro.debuggable` | `1` |
|
|
589
|
+
| `ro.secure` | `0` |
|
|
590
|
+
| `ro.build.type` | `eng` |
|
|
591
|
+
| `ro.build.tags` | `test-keys` |
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
if (RootBeer.checkForDangerousProps()) {
|
|
595
|
+
console.warn('Dangerous system properties detected.');
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
#### `RootBeer.checkForRWPaths(): boolean`
|
|
602
|
+
|
|
603
|
+
Parses `/proc/mounts` and checks whether any paths that should always be read-only on a stock Android device (e.g. `/system`, `/data`) are mounted with write permissions. Read-write system mounts are a strong indicator of root-level filesystem modifications.
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
if (RootBeer.checkForRWPaths()) {
|
|
607
|
+
console.warn('System paths are mounted read-write.');
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Detection Method Deep Dive
|
|
614
|
+
|
|
615
|
+
Different checks have different trade-offs. Use this table to choose the right combination for your threat model.
|
|
616
|
+
|
|
617
|
+
| Method | Technique | Invasiveness | False Positive Risk | Bypass Difficulty |
|
|
618
|
+
|---|---|---|---|---|
|
|
619
|
+
| `isRooted()` | All checks combined | Medium | Low–Medium | Hard |
|
|
620
|
+
| `isRootedWithoutBusyBoxCheck()` | All except BusyBox | Medium | **Lower** | Hard |
|
|
621
|
+
| `detectRootManagementApps()` | Package query | Low | Very Low | Easy (uninstall app) |
|
|
622
|
+
| `detectPotentiallyDangerousApps()` | Package query | Low | Low | Easy (uninstall app) |
|
|
623
|
+
| `detectRootCloakingApps()` | Package query | Low | Very Low | Very Easy (the app hides it) |
|
|
624
|
+
| `detectTestKeys()` | Build property | Very Low | Low | Hard (requires ROM resign) |
|
|
625
|
+
| `checkForSuBinary()` | File path scan | Very Low | Low | Medium (bind mount) |
|
|
626
|
+
| `checkForMagiskBinary()` | File path scan | Very Low | Very Low | Medium (Magisk hide) |
|
|
627
|
+
| `checkForBinary(name)` | File path scan | Very Low | Depends on binary | Medium |
|
|
628
|
+
| `checkSuExists()` | Process execution | **High** | Low | Medium |
|
|
629
|
+
| `checkForDangerousProps()` | `getprop` read | Low | Low | Medium (prop reset) |
|
|
630
|
+
| `checkForRWPaths()` | `/proc/mounts` parse | Very Low | Very Low | Hard |
|
|
631
|
+
|
|
632
|
+
**Recommended strategy for production apps:**
|
|
633
|
+
|
|
634
|
+
1. Start with `isRooted()` as your primary gate.
|
|
635
|
+
2. If you need lower false positives, switch to `isRootedWithoutBusyBoxCheck()`.
|
|
636
|
+
3. For high-security applications (banking, DRM), run all 12 checks individually and send the full result object to your backend for server-side evaluation.
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Platform Guard Reference
|
|
641
|
+
|
|
642
|
+
Because the module is registered with `TurboModuleRegistry.getEnforcing`, it **throws** on any platform where it is not registered (iOS, Android Old Architecture, web). Always guard calls:
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
import { Platform } from 'react-native';
|
|
646
|
+
import { RootBeer } from 'react-native-root-beer';
|
|
647
|
+
|
|
648
|
+
// ✅ Correct — guarded
|
|
649
|
+
if (Platform.OS === 'android') {
|
|
650
|
+
const rooted = RootBeer.isRooted();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ✅ Correct — early return pattern
|
|
654
|
+
function checkDevice(): boolean {
|
|
655
|
+
if (Platform.OS !== 'android') {
|
|
656
|
+
return false; // or handle as you see fit
|
|
657
|
+
}
|
|
658
|
+
return RootBeer.isRooted();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ❌ Incorrect — will throw on iOS / web
|
|
662
|
+
const rooted = RootBeer.isRooted();
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
For TypeScript projects you can create a typed utility:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
// utils/security.ts
|
|
669
|
+
import { Platform } from 'react-native';
|
|
670
|
+
import { RootBeer } from 'react-native-root-beer';
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Returns `null` on non-Android platforms so callers can
|
|
674
|
+
* distinguish "not applicable" from "false".
|
|
675
|
+
*/
|
|
676
|
+
export function isDeviceRooted(): boolean | null {
|
|
677
|
+
if (Platform.OS !== 'android') return null;
|
|
678
|
+
return RootBeer.isRooted();
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## Architecture
|
|
685
|
+
|
|
686
|
+
```
|
|
687
|
+
Consumer App (TypeScript)
|
|
688
|
+
│
|
|
689
|
+
│ import { RootBeer } from 'react-native-root-beer'
|
|
690
|
+
▼
|
|
691
|
+
┌─────────────────────┐
|
|
692
|
+
│ src/RootBeer.ts │ Public API object (as const, fully typed)
|
|
693
|
+
└──────────┬──────────┘
|
|
694
|
+
│
|
|
695
|
+
▼
|
|
696
|
+
┌──────────────────────────┐
|
|
697
|
+
│ src/NativeRootBeer.ts │ TurboModule Spec (Codegen source of truth)
|
|
698
|
+
└──────────┬───────────────┘
|
|
699
|
+
│ React Native Codegen auto-generates ↓
|
|
700
|
+
▼
|
|
701
|
+
┌────────────────────────────────┐
|
|
702
|
+
│ NativeRootBeerSpec.kt │ Abstract Kotlin class (generated)
|
|
703
|
+
│ (com.rootbeer package) │
|
|
704
|
+
└──────────┬─────────────────────┘
|
|
705
|
+
│ extends
|
|
706
|
+
▼
|
|
707
|
+
┌────────────────────────────────┐
|
|
708
|
+
│ RootBeerModule.kt │ Concrete implementation
|
|
709
|
+
│ • Registered as TurboModule │ • lazy-initialises RootBeerLib
|
|
710
|
+
│ • isTurboModule = true │ • delegates each method to SDK
|
|
711
|
+
└──────────┬─────────────────────┘
|
|
712
|
+
│
|
|
713
|
+
▼
|
|
714
|
+
┌──────────────────────────────────────────┐
|
|
715
|
+
│ com.scottyab:rootbeer-lib:0.1.2 │ Android SDK (Maven Central)
|
|
716
|
+
│ Declared in library's build.gradle │ Resolved as transitive dep
|
|
717
|
+
└──────────────────────────────────────────┘
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**Key architectural decisions:**
|
|
721
|
+
|
|
722
|
+
- **`BaseReactPackage`** (not the older `ReactPackage`): The `RootBeerPackage` class extends `BaseReactPackage` — the correct base for New Architecture modules that expose TurboModules.
|
|
723
|
+
- **`isTurboModule = true`** in `ReactModuleInfo`: This opts the module into the TurboModule JSI pathway, bypassing the legacy bridge entirely.
|
|
724
|
+
- **`needsEagerInit = false`**: The native module is only instantiated on first use, not at app startup.
|
|
725
|
+
- **Lazy SDK initialization** (`by lazy`): The `com.scottyab.rootbeer.RootBeer` instance is created once, on first method call, using the `ReactApplicationContext`. This avoids paying the initialization cost if the module is never called.
|
|
726
|
+
- **Codegen spec name** (`codegenConfig.name = "RootBeerSpec"`): The generated spec class is `NativeRootBeerSpec`, and the Java package is `com.rootbeer`, controlled by `codegenConfig.android.javaPackageName`.
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Troubleshooting
|
|
731
|
+
|
|
732
|
+
### Build error: `Cannot find NativeRootBeerSpec`
|
|
733
|
+
|
|
734
|
+
Codegen has not run yet. This happens when you install the package and try to build without cleaning first.
|
|
735
|
+
|
|
736
|
+
```bash
|
|
737
|
+
cd android && ./gradlew clean
|
|
738
|
+
cd .. && npx react-native run-android
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
For Expo:
|
|
742
|
+
|
|
743
|
+
```bash
|
|
744
|
+
npx expo prebuild --clean --platform android
|
|
745
|
+
npx expo run:android
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
### Build error: `Could not resolve com.scottyab:rootbeer-lib:0.1.2`
|
|
751
|
+
|
|
752
|
+
Your Gradle build cannot reach Maven Central. Check that `mavenCentral()` is in your root `android/build.gradle` repositories block:
|
|
753
|
+
|
|
754
|
+
```groovy
|
|
755
|
+
// android/build.gradle
|
|
756
|
+
allprojects {
|
|
757
|
+
repositories {
|
|
758
|
+
google()
|
|
759
|
+
mavenCentral() // ← must be present
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
### `TurboModuleRegistry.getEnforcing` error at runtime
|
|
767
|
+
|
|
768
|
+
You called a `RootBeer` method on a non-Android platform, or your app is running in Old Architecture mode.
|
|
769
|
+
|
|
770
|
+
- **Platform fix:** Wrap all calls in `if (Platform.OS === 'android') { ... }`.
|
|
771
|
+
- **Architecture fix:** Ensure `newArchEnabled=true` in `android/gradle.properties`.
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
### `isRooted()` returns `true` on a production device (false positive)
|
|
776
|
+
|
|
777
|
+
Some manufacturer ROMs (particularly older Xiaomi, Samsung Knox variants) ship with BusyBox or test-keys. Switch to:
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
// Exclude BusyBox from the composite check
|
|
781
|
+
const isRooted = RootBeer.isRootedWithoutBusyBoxCheck();
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
Then run the granular scan to identify the exact triggering check and evaluate whether it represents a real risk for your threat model.
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
### Methods return `false` on a known-rooted emulator
|
|
789
|
+
|
|
790
|
+
Some AVD (Android Virtual Device) emulators and certain Genymotion configurations don't expose root indicators in the paths RootBeer scans. For reliable testing:
|
|
791
|
+
|
|
792
|
+
- Use a **physical device** with Magisk or SuperSU installed.
|
|
793
|
+
- Or try `RootBeer.detectTestKeys()` — emulators are almost always signed with test keys.
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
## License
|
|
798
|
+
|
|
799
|
+
MIT © mr:X
|