react-native-rootbeer-checkroot
v0.1.2
Published
RootBeer
Maintainers
Readme
react-native-rootbeer-checkroot
A React Native New Architecture (TurboModule) wrapper around the battle-tested Android root detection library com.scottyab:rootbeer-lib. It exposes 12 granular detection methods over JSI (JavaScript Interface) — no bridge overhead, no threading surprises.
⚠️ 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 withPlatform.OS === 'android'.
Table of Contents
- How It Works
- Requirements
- Installation
- Native Android Configuration
- Usage Guide
- API Reference
- Detection Method Deep Dive
- Platform Guard Reference
- Architecture
- Troubleshooting
How It Works
react-native-rootbeer-checkroot 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.
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.
Requirements
| Requirement | Version |
|---|---|
| React Native | ≥ 0.73 (New Architecture required) |
| Android minSdkVersion | 24 (Android 7.0 Nougat) |
| Android compileSdkVersion | 36 |
| Java source/target compatibility | 17 |
| Kotlin | 2.0.21 (bundled in the library) |
| react-native-rootbeer-checkroot | 0.1.2 |
New Architecture is mandatory. This library is a TurboModule — it does not have an Old Architecture (bridge) fallback. Ensure
newArchEnabled=trueis set in your project'sandroid/gradle.properties.
Installation
Bare React Native
Step 1 — Install the package
# npm
npm install react-native-rootbeer-checkroot
# yarn
yarn add react-native-rootbeer-checkrootStep 2 — Enable New Architecture
Open android/gradle.properties and confirm the following line is present and not commented out:
# android/gradle.properties
newArchEnabled=trueStep 3 — Rebuild the native layer
cd android && ./gradlew clean
cd ..
npx react-native run-androidGradle will automatically resolve com.scottyab:rootbeer-lib:0.1.2 from Maven Central during this build. No manual build.gradle edits are required.
Expo (Managed & Bare Workflow)
Because this library contains native Android code, it cannot run in the Expo Go client. You must use a development build.
Step 1 — Install the package
npx expo install react-native-rootbeer-checkrootStep 2 — Enable New Architecture in your Expo config
In app.json or app.config.js, ensure the new architecture flag is set:
{
"expo": {
"android": {
"newArchEnabled": true
}
}
}Step 3 — Run prebuild
npx expo prebuild --platform androidWhy this is safe with Expo: The library declares its own Gradle dependency inside its own
android/build.gradle. Whenexpo prebuild(re)generates the nativeandroid/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 futureexpo prebuildrun.
Step 4 — Build a development client or production build
# Development build (EAS)
eas build --profile development --platform android
# Or run locally
npx expo run:androidNative Android Configuration
None required.
This is the primary design goal of react-native-rootbeer-checkroot. The com.scottyab:rootbeer-lib:0.1.2 dependency is declared inside the library's own android/build.gradle:
// This lives inside the library — you do not write this yourself.
dependencies {
implementation "com.facebook.react:react-android"
implementation "com.scottyab:rootbeer-lib:0.1.2"
}Gradle's dependency resolution propagates this to your app's build graph automatically.
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.
Usage Guide
Quick Start — Single Root Check
The simplest possible integration. Use RootBeer.isRooted() to run all available checks in one call.
import React, { useEffect, useState } from 'react';
import { Platform, Text, View, StyleSheet } from 'react-native';
import { RootBeer } from 'react-native-rootbeer-checkroot';
export default function App() {
const [isRooted, setIsRooted] = useState<boolean | null>(null);
useEffect(() => {
// Always guard with Platform.OS — this library is Android-only.
if (Platform.OS === 'android') {
const result = RootBeer.isRooted();
setIsRooted(result);
}
}, []);
if (Platform.OS !== 'android') {
return (
<View style={styles.container}>
<Text>Root detection is only available on Android.</Text>
</View>
);
}
return (
<View style={styles.container}>
{isRooted === null ? (
<Text>Checking device integrity...</Text>
) : (
<Text style={isRooted ? styles.danger : styles.safe}>
{isRooted ? '⚠️ Device is rooted' : '✅ Device is not rooted'}
</Text>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
danger: { color: '#e53e3e', fontSize: 18, fontWeight: 'bold' },
safe: { color: '#38a169', fontSize: 18, fontWeight: 'bold' },
});Note: All
RootBeermethods are synchronous. They returnbooleandirectly — noawait, no.then().
Granular Detection Scan
For security-sensitive applications, run each check individually to understand why a device is flagged and log the results for your backend.
import { Platform } from 'react-native';
import { RootBeer } from 'react-native-rootbeer-checkroot';
interface RootScanResult {
isRooted: boolean;
isRootedWithoutBusyBoxCheck: boolean;
hasRootManagementApps: boolean;
hasDangerousApps: boolean;
hasTestKeys: boolean;
hasSuBinary: boolean;
hasMagiskBinary: boolean;
suExecutes: boolean;
hasDangerousProps: boolean;
hasRWPaths: boolean;
hasRootCloakingApps: boolean;
hasBusyBox: boolean;
}
/**
* Runs every available RootBeer check and returns a structured result.
* Call this on Android only.
*/
export function performFullRootScan(): RootScanResult {
if (Platform.OS !== 'android') {
throw new Error('performFullRootScan() is only supported on Android.');
}
return {
// High-level composite checks
isRooted: RootBeer.isRooted(),
isRootedWithoutBusyBoxCheck: RootBeer.isRootedWithoutBusyBoxCheck(),
// Installed app checks
hasRootManagementApps: RootBeer.detectRootManagementApps(),
hasDangerousApps: RootBeer.detectPotentiallyDangerousApps(),
hasRootCloakingApps: RootBeer.detectRootCloakingApps(),
// Build / ROM checks
hasTestKeys: RootBeer.detectTestKeys(),
// Binary presence checks (static — does not execute anything)
hasSuBinary: RootBeer.checkForSuBinary(),
hasMagiskBinary: RootBeer.checkForMagiskBinary(),
hasBusyBox: RootBeer.checkForBinary('busybox'),
// Runtime probe (actually attempts to run `su`)
suExecutes: RootBeer.checkSuExists(),
// System property & filesystem checks
hasDangerousProps: RootBeer.checkForDangerousProps(),
hasRWPaths: RootBeer.checkForRWPaths(),
};
}
// --- Usage ---
if (Platform.OS === 'android') {
const scan = performFullRootScan();
if (scan.isRooted) {
console.warn('Root indicators detected:', scan);
// Send `scan` to your analytics / security backend for forensics
}
}Reusable React Hook
Encapsulate all detection logic in a custom hook for clean, idiomatic React consumption across your app.
// hooks/useRootDetection.ts
import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
import { RootBeer } from 'react-native-rootbeer-checkroot';
interface RootDetectionState {
/** True if any root indicator was found. */
isRooted: boolean;
/** True while the detection is running (first render only). */
isChecking: boolean;
/** True if the check completed without errors. */
isReady: boolean;
/** Populated if an unexpected error occurred during detection. */
error: Error | null;
}
/**
* Runs `RootBeer.isRooted()` once on mount.
* On non-Android platforms, `isRooted` is always `false` and `isReady`
* is `false` to prevent false security guarantees.
*/
export function useRootDetection(): RootDetectionState {
const [state, setState] = useState<RootDetectionState>({
isRooted: false,
isChecking: Platform.OS === 'android',
isReady: false,
error: null,
});
useEffect(() => {
if (Platform.OS !== 'android') {
return;
}
try {
const isRooted = RootBeer.isRooted();
setState({ isRooted, isChecking: false, isReady: true, error: null });
} catch (err) {
setState({
isRooted: false,
isChecking: false,
isReady: false,
error: err instanceof Error ? err : new Error(String(err)),
});
}
}, []);
return state;
}// components/SecureScreen.tsx
import React from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
import { useRootDetection } from '../hooks/useRootDetection';
export function SecureScreen() {
const { isRooted, isChecking, error } = useRootDetection();
if (isChecking) {
return <ActivityIndicator size="large" />;
}
if (error) {
// Treat detection failure conservatively — block access
return <Text>Security check failed. Access denied.</Text>;
}
if (isRooted) {
return (
<View>
<Text>⚠️ This app cannot run on a rooted device.</Text>
</View>
);
}
return <Text>✅ Device integrity verified. Welcome!</Text>;
}Navigation Guard Pattern
Block access to sensitive screens at the navigation level. This example uses React Navigation, but the pattern applies to any router.
// navigation/RootGuard.tsx
import React, { useEffect } from 'react';
import { Platform, Alert } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { RootBeer } from 'react-native-rootbeer-checkroot';
interface RootGuardProps {
children: React.ReactNode;
/** Screen name to redirect to when root is detected. */
fallbackScreen?: string;
}
/**
* Wraps a screen and redirects away if the device is rooted.
* Must be rendered inside a React Navigation navigator.
*/
export function RootGuard({ children, fallbackScreen = 'Home' }: RootGuardProps) {
const navigation = useNavigation();
useEffect(() => {
if (Platform.OS !== 'android') {
return;
}
const isRooted = RootBeer.isRooted();
if (isRooted) {
Alert.alert(
'Security Warning',
'This feature is not available on rooted devices.',
[{ text: 'OK', onPress: () => navigation.navigate(fallbackScreen as never) }],
{ cancelable: false }
);
}
}, [navigation, fallbackScreen]);
return <>{children}</>;
}// Inside your screen component
function PaymentScreen() {
return (
<RootGuard fallbackScreen="Home">
{/* Sensitive payment UI renders here */}
</RootGuard>
);
}API Reference
All methods are synchronous and return boolean. They are exposed on the RootBeer object (as const) imported from react-native-rootbeer-checkroot.
import { RootBeer } from 'react-native-rootbeer-checkroot';Method Summary
| Method | Parameters | Returns | Description |
|---|---|---|---|
| isRooted() | — | boolean | Runs all detection checks, including BusyBox. The primary, recommended check. |
| isRootedWithoutBusyBoxCheck() | — | boolean | Runs all checks except BusyBox. Use when BusyBox causes false positives. |
| detectRootManagementApps() | — | boolean | Scans installed apps for known root managers (SuperSU, Magisk Manager, KingRoot, etc.). |
| detectPotentiallyDangerousApps() | — | boolean | Scans installed apps for apps commonly associated with root exploitation. |
| detectRootCloakingApps() | — | boolean | Scans installed apps for apps that conceal root from other apps (RootCloak, Hide My Root). |
| detectTestKeys() | — | boolean | Checks android.os.Build.TAGS for "test-keys" — a signature of unofficial/custom ROMs. |
| checkForSuBinary() | — | boolean | Scans a list of known filesystem paths for the su executable. Static — does not execute anything. |
| checkForMagiskBinary() | — | boolean | Scans filesystem paths for the Magisk binary. Effective even when Magisk is in hide mode. |
| checkForBinary(binaryName) | binaryName: string | boolean | Scans filesystem paths for any named binary. Pass "busybox", "su", or any other name. |
| checkSuExists() | — | boolean | Attempts to execute su. Returns true if the call succeeds. Runtime probe — more invasive than checkForSuBinary(). |
| checkForDangerousProps() | — | boolean | Reads system properties and returns true if dangerous values are set (e.g. ro.debuggable=1, ro.secure=0). |
| checkForRWPaths() | — | boolean | Inspects /proc/mounts for filesystem paths that should be read-only but are mounted read-write. |
Method Detail
RootBeer.isRooted(): boolean
The top-level composite check. Internally calls every other detection method (including BusyBox) and returns true if any single indicator is found.
Use this for the vast majority of use cases. Only fall back to individual methods when you need to understand which specific indicator triggered.
const safe = !RootBeer.isRooted();RootBeer.isRootedWithoutBusyBoxCheck(): boolean
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.
// Prefer this if you're seeing false positives from isRooted()
const safe = !RootBeer.isRootedWithoutBusyBoxCheck();RootBeer.detectRootManagementApps(): boolean
Queries the Android PackageManager for the package names of well-known root management applications. Returns true if any are installed.
Examples of detected apps: SuperSU, Magisk Manager, KingRoot, Towelroot, BaiduEasyRoot.
if (RootBeer.detectRootManagementApps()) {
console.warn('Root management app found on device.');
}RootBeer.detectPotentiallyDangerousApps(): boolean
Queries the PackageManager for apps that are commonly used to exploit, abuse, or probe rooted environments — even if they are not root managers themselves.
if (RootBeer.detectPotentiallyDangerousApps()) {
console.warn('Potentially dangerous app detected.');
}RootBeer.detectRootCloakingApps(): boolean
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.
if (RootBeer.detectRootCloakingApps()) {
// High-confidence indicator — user is actively trying to hide root
console.warn('Root cloaking app detected.');
}RootBeer.detectTestKeys(): boolean
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.
if (RootBeer.detectTestKeys()) {
console.warn('Device is running a custom or test-key ROM.');
}RootBeer.checkForSuBinary(): boolean
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.
if (RootBeer.checkForSuBinary()) {
console.warn('su binary found on filesystem.');
}RootBeer.checkForMagiskBinary(): boolean
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.
if (RootBeer.checkForMagiskBinary()) {
console.warn('Magisk binary present — device likely uses Magisk root.');
}RootBeer.checkForBinary(binaryName: string): boolean
| Parameter | Type | Required | Description |
|---|---|---|---|
| binaryName | string | ✅ | The binary filename to scan for (e.g. "su", "busybox", "magisk"). |
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.
// Check for busybox specifically
const hasBusyBox = RootBeer.checkForBinary('busybox');
// Check for any custom binary
const hasFridaServer = RootBeer.checkForBinary('frida-server');RootBeer.checkSuExists(): boolean
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.
// Definitive but invasive — su is actually invoked
if (RootBeer.checkSuExists()) {
console.warn('su is executable on this device.');
}RootBeer.checkForDangerousProps(): boolean
Reads Android system properties via getprop and returns true if any of the following dangerous values are set:
| Property | Dangerous Value |
|---|---|
| ro.debuggable | 1 |
| ro.secure | 0 |
| ro.build.type | eng |
| ro.build.tags | test-keys |
if (RootBeer.checkForDangerousProps()) {
console.warn('Dangerous system properties detected.');
}RootBeer.checkForRWPaths(): boolean
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.
if (RootBeer.checkForRWPaths()) {
console.warn('System paths are mounted read-write.');
}Detection Method Deep Dive
Different checks have different trade-offs. Use this table to choose the right combination for your threat model.
| Method | Technique | Invasiveness | False Positive Risk | Bypass Difficulty |
|---|---|---|---|---|
| isRooted() | All checks combined | Medium | Low–Medium | Hard |
| isRootedWithoutBusyBoxCheck() | All except BusyBox | Medium | Lower | Hard |
| detectRootManagementApps() | Package query | Low | Very Low | Easy (uninstall app) |
| detectPotentiallyDangerousApps() | Package query | Low | Low | Easy (uninstall app) |
| detectRootCloakingApps() | Package query | Low | Very Low | Very Easy (the app hides it) |
| detectTestKeys() | Build property | Very Low | Low | Hard (requires ROM resign) |
| checkForSuBinary() | File path scan | Very Low | Low | Medium (bind mount) |
| checkForMagiskBinary() | File path scan | Very Low | Very Low | Medium (Magisk hide) |
| checkForBinary(name) | File path scan | Very Low | Depends on binary | Medium |
| checkSuExists() | Process execution | High | Low | Medium |
| checkForDangerousProps() | getprop read | Low | Low | Medium (prop reset) |
| checkForRWPaths() | /proc/mounts parse | Very Low | Very Low | Hard |
Recommended strategy for production apps:
- Start with
isRooted()as your primary gate. - If you need lower false positives, switch to
isRootedWithoutBusyBoxCheck(). - For high-security applications (banking, DRM), run all 12 checks individually and send the full result object to your backend for server-side evaluation.
Platform Guard Reference
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:
import { Platform } from 'react-native';
import { RootBeer } from 'react-native-rootbeer-checkroot';
// ✅ Correct — guarded
if (Platform.OS === 'android') {
const rooted = RootBeer.isRooted();
}
// ✅ Correct — early return pattern
function checkDevice(): boolean {
if (Platform.OS !== 'android') {
return false; // or handle as you see fit
}
return RootBeer.isRooted();
}
// ❌ Incorrect — will throw on iOS / web
const rooted = RootBeer.isRooted();For TypeScript projects you can create a typed utility:
// utils/security.ts
import { Platform } from 'react-native';
import { RootBeer } from 'react-native-rootbeer-checkroot';
/**
* Returns `null` on non-Android platforms so callers can
* distinguish "not applicable" from "false".
*/
export function isDeviceRooted(): boolean | null {
if (Platform.OS !== 'android') return null;
return RootBeer.isRooted();
}Architecture
Consumer App (TypeScript)
│
│ import { RootBeer } from 'react-native-rootbeer-checkroot'
▼
┌─────────────────────┐
│ src/RootBeer.ts │ Public API object (as const, fully typed)
└──────────┬──────────┘
│
▼
┌──────────────────────────┐
│ src/NativeRootBeer.ts │ TurboModule Spec (Codegen source of truth)
└──────────┬───────────────┘
│ React Native Codegen auto-generates ↓
▼
┌────────────────────────────────┐
│ NativeRootBeerSpec.kt │ Abstract Kotlin class (generated)
│ (com.rootbeer package) │
└──────────┬─────────────────────┘
│ extends
▼
┌────────────────────────────────┐
│ RootBeerModule.kt │ Concrete implementation
│ • Registered as TurboModule │ • lazy-initialises RootBeerLib
│ • isTurboModule = true │ • delegates each method to SDK
└──────────┬─────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ com.scottyab:rootbeer-lib:0.1.2 │ Android SDK (Maven Central)
│ Declared in library's build.gradle │ Resolved as transitive dep
└──────────────────────────────────────────┘Key architectural decisions:
BaseReactPackage(not the olderReactPackage): TheRootBeerPackageclass extendsBaseReactPackage— the correct base for New Architecture modules that expose TurboModules.isTurboModule = trueinReactModuleInfo: This opts the module into the TurboModule JSI pathway, bypassing the legacy bridge entirely.needsEagerInit = false: The native module is only instantiated on first use, not at app startup.- Lazy SDK initialization (
by lazy): Thecom.scottyab.rootbeer.RootBeerinstance is created once, on first method call, using theReactApplicationContext. This avoids paying the initialization cost if the module is never called. - Codegen spec name (
codegenConfig.name = "RootBeerSpec"): The generated spec class isNativeRootBeerSpec, and the Java package iscom.rootbeer, controlled bycodegenConfig.android.javaPackageName.
Troubleshooting
Build error: Cannot find NativeRootBeerSpec
Codegen has not run yet. This happens when you install the package and try to build without cleaning first.
cd android && ./gradlew clean
cd .. && npx react-native run-androidFor Expo:
npx expo prebuild --clean --platform android
npx expo run:androidBuild error: Could not resolve com.scottyab:rootbeer-lib:0.1.2
Your Gradle build cannot reach Maven Central. Check that mavenCentral() is in your root android/build.gradle repositories block:
// android/build.gradle
allprojects {
repositories {
google()
mavenCentral() // ← must be present
}
}TurboModuleRegistry.getEnforcing error at runtime
You called a RootBeer method on a non-Android platform, or your app is running in Old Architecture mode.
- Platform fix: Wrap all calls in
if (Platform.OS === 'android') { ... }. - Architecture fix: Ensure
newArchEnabled=trueinandroid/gradle.properties.
isRooted() returns true on a production device (false positive)
Some manufacturer ROMs (particularly older Xiaomi, Samsung Knox variants) ship with BusyBox or test-keys. Switch to:
// Exclude BusyBox from the composite check
const isRooted = RootBeer.isRootedWithoutBusyBoxCheck();Then run the granular scan to identify the exact triggering check and evaluate whether it represents a real risk for your threat model.
Methods return false on a known-rooted emulator
Some AVD (Android Virtual Device) emulators and certain Genymotion configurations don't expose root indicators in the paths RootBeer scans. For reliable testing:
- Use a physical device with Magisk or SuperSU installed.
- Or try
RootBeer.detectTestKeys()— emulators are almost always signed with test keys.
License
MIT © mr:X
