Determine if running on a rooted device

Viewed 222972

My app has a certain piece of functionality that will only work on a device where root is available. Rather than having this feature fail when it is used (and then show an appropriate error message to the user), I'd prefer an ability to silently check if root is available first, and if not,hide the respective options in the first place.

Is there a way to do this?

28 Answers

Some modified builds used to set the system property ro.modversion for this purpose. Things seem to have moved on; my build from TheDude a few months ago has this:

cmb@apollo:~$ adb -d shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [htc_dream-eng 1.5 CUPCAKE eng.TheDudeAbides.20090427.235325 test-keys]
[ro.build.version.incremental]: [eng.TheDude.2009027.235325]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Mon Apr 20 01:42:32 CDT 2009]
[ro.build.date.utc]: [1240209752]
[ro.build.type]: [eng]
[ro.build.user]: [TheDude]
[ro.build.host]: [ender]
[ro.build.tags]: [test-keys]
[ro.build.product]: [dream]
[ro.build.description]: [kila-user 1.1 PLAT-RC33 126986 ota-rel-keys,release-keys]
[ro.build.fingerprint]: [tmobile/kila/dream/trout:1.1/PLAT-RC33/126986:user/ota-rel-keys,release-keys]
[ro.build.changelist]: [17615# end build properties]

The emulator from the 1.5 SDK on the other hand, running the 1.5 image, also has root, is probably similar to the Android Dev Phone 1 (which you presumably want to allow) and has this:

cmb@apollo:~$ adb -e shell getprop |grep build
[ro.build.id]: [CUPCAKE]
[ro.build.display.id]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.version.incremental]: [148875]
[ro.build.version.sdk]: [3]
[ro.build.version.release]: [1.5]
[ro.build.date]: [Thu May 14 18:09:10 PDT 2009]
[ro.build.date.utc]: [1242349750]
[ro.build.type]: [eng]
[ro.build.user]: [android-build]
[ro.build.host]: [undroid16.mtv.corp.google.com]
[ro.build.tags]: [test-keys]
[ro.build.product]: [generic]
[ro.build.description]: [sdk-eng 1.5 CUPCAKE 148875 test-keys]
[ro.build.fingerprint]: [generic/sdk/generic/:1.5/CUPCAKE/148875:eng/test-keys]

As for the retail builds, I don't have one to hand, but various searches under site:xda-developers.com are informative. Here is a G1 in the Netherlands, you can see that ro.build.tags does not have test-keys, and I think that's probably the most reliable property to use.

I suggest using native code for root detection. Here is a full working example.

enter image description here

JAVA wrapper:

package com.kozhevin.rootchecks.util;


import android.support.annotation.NonNull;

import com.kozhevin.rootchecks.BuildConfig;

public class MeatGrinder {
    private final static String LIB_NAME = "native-lib";
    private static boolean isLoaded;
    private static boolean isUnderTest = false;

    private MeatGrinder() {

    }

    public boolean isLibraryLoaded() {
        if (isLoaded) {
            return true;
        }
        try {
            if(isUnderTest) {
                throw new UnsatisfiedLinkError("under test");
            }
            System.loadLibrary(LIB_NAME);
            isLoaded = true;
        } catch (UnsatisfiedLinkError e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
        }
        return isLoaded;
    }

    public native boolean isDetectedDevKeys();

    public native boolean isDetectedTestKeys();

    public native boolean isNotFoundReleaseKeys();

    public native boolean isFoundDangerousProps();

    public native boolean isPermissiveSelinux();

    public native boolean isSuExists();

    public native boolean isAccessedSuperuserApk();

    public native boolean isFoundSuBinary();

    public native boolean isFoundBusyboxBinary();

    public native boolean isFoundXposed();

    public native boolean isFoundResetprop();

    public native boolean isFoundWrongPathPermission();

    public native boolean isFoundHooks();

    @NonNull
    public static MeatGrinder getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private static class InstanceHolder {
        private static final MeatGrinder INSTANCE = new MeatGrinder();
    }
}

JNI wrapper(native-lib.c):

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedTestKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isDetectedTestKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isDetectedDevKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isDetectedDevKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isNotFoundReleaseKeys(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isNotFoundReleaseKeys();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundDangerousProps(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundDangerousProps();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isPermissiveSelinux(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isPermissiveSelinux();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isSuExists(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isSuExists();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isAccessedSuperuserApk(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isAccessedSuperuserApk();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundSuBinary(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundSuBinary();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundBusyboxBinary(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundBusyboxBinary();
}


JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundXposed(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundXposed();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundResetprop(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundResetprop();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundWrongPathPermission(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundWrongPathPermission();
}

JNIEXPORT jboolean JNICALL
Java_com_kozhevin_rootchecks_util_MeatGrinder_isFoundHooks(
        JNIEnv *env,
        jobject  this ) {

    return (jboolean) isFoundHooks();
}

constants:

// Comma-separated tags describing the build, like= "unsigned,debug".
const char *const ANDROID_OS_BUILD_TAGS = "ro.build.tags";

// A string that uniquely identifies this build. 'BRAND/PRODUCT/DEVICE:RELEASE/ID/VERSION.INCREMENTAL:TYPE/TAGS'.
const char *const ANDROID_OS_BUILD_FINGERPRINT = "ro.build.fingerprint";

const char *const ANDROID_OS_SECURE = "ro.secure";

const char *const ANDROID_OS_DEBUGGABLE = "ro.debuggable";
const char *const ANDROID_OS_SYS_INITD = "sys.initd";
const char *const ANDROID_OS_BUILD_SELINUX = "ro.build.selinux";
//see https://android.googlesource.com/platform/system/core/+/master/adb/services.cpp#86
const char *const SERVICE_ADB_ROOT = "service.adb.root";

const char * const MG_SU_PATH[] = {
        "/data/local/",
        "/data/local/bin/",
        "/data/local/xbin/",
        "/sbin/",
        "/system/bin/",
        "/system/bin/.ext/",
        "/system/bin/failsafe/",
        "/system/sd/xbin/",
        "/su/xbin/",
        "/su/bin/",
        "/magisk/.core/bin/",
        "/system/usr/we-need-root/",
        "/system/xbin/",
        0
};

const char * const MG_EXPOSED_FILES[] = {
        "/system/lib/libxposed_art.so",
        "/system/lib64/libxposed_art.so",
        "/system/xposed.prop",
        "/cache/recovery/xposed.zip",
        "/system/framework/XposedBridge.jar",
        "/system/bin/app_process64_xposed",
        "/system/bin/app_process32_xposed",
        "/magisk/xposed/system/lib/libsigchain.so",
        "/magisk/xposed/system/lib/libart.so",
        "/magisk/xposed/system/lib/libart-disassembler.so",
        "/magisk/xposed/system/lib/libart-compiler.so",
        "/system/bin/app_process32_orig",
        "/system/bin/app_process64_orig",
        0
};

const char * const MG_READ_ONLY_PATH[] = {
        "/system",
        "/system/bin",
        "/system/sbin",
        "/system/xbin",
        "/vendor/bin",
        "/sbin",
        "/etc",
        0
};

root detections from native code:

struct mntent *getMntent(FILE *fp, struct mntent *e, char *buf, int buf_len) {

    while (fgets(buf, buf_len, fp) != NULL) {
        // Entries look like "/dev/block/vda /system ext4 ro,seclabel,relatime,data=ordered 0 0".
        // That is: mnt_fsname mnt_dir mnt_type mnt_opts mnt_freq mnt_passno.
        int fsname0, fsname1, dir0, dir1, type0, type1, opts0, opts1;
        if (sscanf(buf, " %n%*s%n %n%*s%n %n%*s%n %n%*s%n %d %d",
                   &fsname0, &fsname1, &dir0, &dir1, &type0, &type1, &opts0, &opts1,
                   &e->mnt_freq, &e->mnt_passno) == 2) {
            e->mnt_fsname = &buf[fsname0];
            buf[fsname1] = '\0';
            e->mnt_dir = &buf[dir0];
            buf[dir1] = '\0';
            e->mnt_type = &buf[type0];
            buf[type1] = '\0';
            e->mnt_opts = &buf[opts0];
            buf[opts1] = '\0';
            return e;
        }
    }
    return NULL;
}


bool isPresentMntOpt(const struct mntent *pMnt, const char *pOpt) {
    char *token = pMnt->mnt_opts;
    const char *end = pMnt->mnt_opts + strlen(pMnt->mnt_opts);
    const size_t optLen = strlen(pOpt);
    while (token != NULL) {
        const char *tokenEnd = token + optLen;
        if (tokenEnd > end) break;
        if (memcmp(token, pOpt, optLen) == 0 &&
            (*tokenEnd == '\0' || *tokenEnd == ',' || *tokenEnd == '=')) {
            return true;
        }
        token = strchr(token, ',');
        if (token != NULL) {
            token++;
        }
    }
    return false;
}

static char *concat2str(const char *pString1, const char *pString2) {
    char *result;
    size_t lengthBuffer = 0;

    lengthBuffer = strlen(pString1) +
                   strlen(pString2) + 1;
    result = malloc(lengthBuffer);
    if (result == NULL) {
        GR_LOGW("malloc failed\n");
        return NULL;
    }
    memset(result, 0, lengthBuffer);
    strcpy(result, pString1);
    strcat(result, pString2);
    return result;
}

static bool
isBadPropertyState(const char *key, const char *badValue, bool isObligatoryProperty, bool isExact) {
    if (badValue == NULL) {
        GR_LOGE("badValue may not be NULL");
        return false;
    }
    if (key == NULL) {
        GR_LOGE("key may not be NULL");
        return false;
    }
    char value[PROP_VALUE_MAX + 1];
    int length = __system_property_get(key, value);
    bool result = false;
    /* A length 0 value indicates that the property is not defined */
    if (length > 0) {
        GR_LOGI("property:[%s]==[%s]", key, value);
        if (isExact) {
            if (strcmp(value, badValue) == 0) {
                GR_LOGW("bad value[%s] equals to [%s] in the property [%s]", value, badValue, key);
                result = true;
            }
        } else {
            if (strlen(value) >= strlen(badValue) && strstr(value, badValue) != NULL) {
                GR_LOGW("bad value[%s] found in [%s] in the property [%s]", value, badValue, key);
                result = true;
            }
        }
    } else {
        GR_LOGI("[%s] property not found", key);
        if (isObligatoryProperty) {
            result = true;
        }
    }
    return result;
}

bool isDetectedTestKeys() {
    const char *TEST_KEYS_VALUE = "test-keys";
    return isBadPropertyState(ANDROID_OS_BUILD_TAGS, TEST_KEYS_VALUE, true, false);
}

bool isDetectedDevKeys() {
    const char *DEV_KEYS_VALUE = "dev-keys";
    return isBadPropertyState(ANDROID_OS_BUILD_TAGS, DEV_KEYS_VALUE, true, false);
}

bool isNotFoundReleaseKeys() {
    const char *RELEASE_KEYS_VALUE = "release-keys";
    return !isBadPropertyState(ANDROID_OS_BUILD_TAGS, RELEASE_KEYS_VALUE, false, true);
}

bool isFoundWrongPathPermission() {

    bool result = false;
    FILE *file = fopen("/proc/mounts", "r");
    char mntent_strings[BUFSIZ];
    if (file == NULL) {
        GR_LOGE("setmntent");
        return result;
    }

    struct mntent ent = {0};
    while (NULL != getMntent(file, &ent, mntent_strings, sizeof(mntent_strings))) {
        for (size_t i = 0; MG_READ_ONLY_PATH[i]; i++) {
            if (strcmp((&ent)->mnt_dir, MG_READ_ONLY_PATH[i]) == 0 &&
                isPresentMntOpt(&ent, "rw")) {
                GR_LOGI("%s %s %s %s\n", (&ent)->mnt_fsname, (&ent)->mnt_dir, (&ent)->mnt_opts,
                        (&ent)->mnt_type);
                result = true;
                break;
            }
        }
        memset(&ent, 0, sizeof(ent));
    }
    fclose(file);
    return result;
}


bool isFoundDangerousProps() {
    const char *BAD_DEBUGGABLE_VALUE = "1";
    const char *BAD_SECURE_VALUE = "0";
    const char *BAD_SYS_INITD_VALUE = "1";
    const char *BAD_SERVICE_ADB_ROOT_VALUE = "1";

    bool result = isBadPropertyState(ANDROID_OS_DEBUGGABLE, BAD_DEBUGGABLE_VALUE, true, true) ||
                  isBadPropertyState(SERVICE_ADB_ROOT, BAD_SERVICE_ADB_ROOT_VALUE, false, true) ||
                  isBadPropertyState(ANDROID_OS_SECURE, BAD_SECURE_VALUE, true, true) ||
                  isBadPropertyState(ANDROID_OS_SYS_INITD, BAD_SYS_INITD_VALUE, false, true);

    return result;
}

bool isPermissiveSelinux() {
    const char *BAD_VALUE = "0";
    return isBadPropertyState(ANDROID_OS_BUILD_SELINUX, BAD_VALUE, false, false);
}

bool isSuExists() {
    char buf[BUFSIZ];
    char *str = NULL;
    char *temp = NULL;
    size_t size = 1;  // start with size of 1 to make room for null terminator
    size_t strlength;

    FILE *pipe = popen("which su", "r");
    if (pipe == NULL) {
        GR_LOGI("pipe is null");
        return false;
    }

    while (fgets(buf, sizeof(buf), pipe) != NULL) {
        strlength = strlen(buf);
        temp = realloc(str, size + strlength);  // allocate room for the buf that gets appended
        if (temp == NULL) {
            // allocation error
            GR_LOGE("Error (re)allocating memory");
            pclose(pipe);
            if (str != NULL) {
                free(str);
            }
            return false;
        } else {
            str = temp;
        }
        strcpy(str + size - 1, buf);
        size += strlength;
    }
    pclose(pipe);
    GR_LOGW("A size of the result from pipe is [%zu], result:\n [%s] ", size, str);
    if (str != NULL) {
        free(str);
    }
    return size > 1 ? true : false;
}

static bool isAccessedFile(const char *path) {
    int result = access(path, F_OK);
    GR_LOGV("[%s] has been accessed with result: [%d]", path, result);
    return result == 0 ? true : false;
}

static bool isFoundBinaryFromArray(const char *const *array, const char *binary) {
    for (size_t i = 0; array[i]; ++i) {
        char *checkedPath = concat2str(array[i], binary);
        if (checkedPath == NULL) { // malloc failed
            return false;
        }
        bool result = isAccessedFile(checkedPath);
        free(checkedPath);
        if (result) {
            return result;
        }
    }
    return false;
}

bool isAccessedSuperuserApk() {
    return isAccessedFile("/system/app/Superuser.apk");
}

bool isFoundResetprop() {
    return isAccessedFile("/data/magisk/resetprop");
}

bool isFoundSuBinary() {
    return isFoundBinaryFromArray(MG_SU_PATH, "su");
}

bool isFoundBusyboxBinary() {
    return isFoundBinaryFromArray(MG_SU_PATH, "busybox");
}

bool isFoundXposed() {
    for (size_t i = 0; MG_EXPOSED_FILES[i]; ++i) {
        bool result = isAccessedFile(MG_EXPOSED_FILES[i]);
        if (result) {
            return result;
        }
    }
    return false;
}

bool isFoundHooks() {
    bool result = false;
    pid_t pid = getpid();
    char maps_file_name[512];
    sprintf(maps_file_name, "/proc/%d/maps", pid);
    GR_LOGI("try to open [%s]", maps_file_name);
    const size_t line_size = BUFSIZ;
    char *line = malloc(line_size);
    if (line == NULL) {
        return result;
    }
    FILE *fp = fopen(maps_file_name, "r");
    if (fp == NULL) {
        free(line);
        return result;
    }
    memset(line, 0, line_size);
    const char *substrate = "com.saurik.substrate";
    const char *xposed = "XposedBridge.jar";
    while (fgets(line, line_size, fp) != NULL) {
        const size_t real_line_size = strlen(line);
        if ((real_line_size >= strlen(substrate) && strstr(line, substrate) != NULL) ||
            (real_line_size >= strlen(xposed) && strstr(line, xposed) != NULL)) {
            GR_LOGI("found in [%s]: [%s]", maps_file_name, line);
            result = true;
            break;
        }
    }
    free(line);
    fclose(fp);
    return result;
}

Forget all that detecting root apps and su binaries. Check for the root daemon process. This can be done from the terminal and you can run terminal commands within an app. Try this one-liner.

if [ ! "$(/system/bin/ps -A | grep -v grep | grep -c daemonsu)" = "0" ]; then echo "device is rooted"; else echo "device is not rooted"; fi

You don't need root permission to achieve this either.

Edit: Now using this method for better detection!

if [ $(ps -A | grep -e ^shell -e ^root | grep -v "\[" | tr -s ' ' | cut -d ' ' -f 9 | grep -c su) ] || [ $(which su) ]; then echo 'rooted'; else echo 'not rooted'; fi

Based on some of the answers here, I merged them and also added a check if some known root-manager app is installed:

fun isProbablyRooted(context: Context, alsoIncludeCheckingRootManagerApp: Boolean = false): Boolean {
    return hasRootManagerSystemApp(context) || (alsoIncludeCheckingRootManagerApp && hasRootManagerSystemApp(context))
}

fun hasRootManagerSystemApp(context: Context): Boolean {
    val rootAppsPackageNames = arrayOf("com.topjohnwu.magisk", "eu.chainfire.supersu", "com.koushikdutta.superuser", "com.noshufou.android.su", "me.phh.superuser")
    rootAppsPackageNames.forEach { rootAppPackageName ->
        try {
            context.packageManager.getApplicationInfo(rootAppPackageName, 0)
            return true
        } catch (e: Exception) {
        }
    }
    return false
}

fun hasSuBinary(): Boolean {
    return try {
        findBinary("su")
    } catch (e: Exception) {
        e.printStackTrace()
        false
    }
}

private fun findBinary(binaryName: String): Boolean {
    val paths = System.getenv("PATH")
    if (!paths.isNullOrBlank()) {
        val systemPlaces: List<String> = paths.split(":")
        return systemPlaces.firstOrNull { File(it, binaryName).exists() } != null
    }
    val places = arrayOf("/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/", "/data/local/bin/",
            "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/")
    return places.firstOrNull { File(it, binaryName).exists() } != null
}

manifest:

<queries>
    <package android:name="com.topjohnwu.magisk" />
    <package android:name="eu.chainfire.supersu" />
    <package android:name="com.koushikdutta.superuser" />
    <package android:name="com.noshufou.android.su" />
    <package android:name="me.phh.superuser" />
</queries>

Of course, this is still a guess, like all the other solutions. Users can install Magisk without having the device rooted, for example.

Using google SafetyNet Attestation API you can easily check whether your device is rooted or not :

  1. Add dependency in build.gradle(:app)

    implementation 'com.google.android.gms:play-services-safetynet:17.0.0'

  2. Get Api key and enable Android Device Verification API using link

  3. public static void sendSafetyNetRequest(Activity context) {

     if(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context, 13000000) == ConnectionResult.SUCCESS) {
         Log.e(TAG, "The SafetyNet Attestation API is available");
    
         // TODO(developer): Change the nonce generation to include your own, used once value,
         // ideally from your remote server.
    
         String nonceData = "Safety Net Sample: " + System.currentTimeMillis();
         ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
         Random mRandom = new SecureRandom();
    
         byte[] bytes = new byte[24];
         mRandom.nextBytes(bytes);
         try {
             byteStream.write(bytes);
             byteStream.write(nonceData.getBytes());
         } catch (IOException e) {
             e.printStackTrace();
         }
    
         byte[] nonce = byteStream.toByteArray();
    
         SafetyNetClient client = SafetyNet.getClient(context);
         Task<SafetyNetApi.AttestationResponse> task = client.attest(nonce, API_KEY_FROM_STEP_2_LINK);
    
         task.addOnSuccessListener(context, attestationResponse -> {
    
    
           /*
                  TODO(developer): Forward this result to your server together with
                  the nonce for verification.
                  You can also parse the JwsResult locally to confirm that the API
                  returned a response by checking for an 'error' field first and before
                  retrying the request with an exponential backoff.
                  NOTE: Do NOT rely on a local, client-side only check for security, you
                  must verify the response on a remote server!
                 */
    
             String jwsResult = attestationResponse.getJwsResult();
    
             Log.e(TAG, "Success! SafetyNet result:\n" + jwsResult + "\n");
    
             if (jwsResult == null) {
                 Log.e(TAG, "jwsResult Null");
    
             }
             final String[] jwtParts = jwsResult.split("\\.");
    
             if (jwtParts.length == 3) {
                 String decodedPayload = new String(Base64.decode(jwtParts[1], Base64.DEFAULT));
                 Log.e(TAG, "decodedPayload :     " + decodedPayload);
             }
    
    
         });
    
         task.addOnFailureListener(context, e -> {
             // An error occurred while communicating with the service.
             String mResult = null;
    
             if (e instanceof ApiException) {
                 // An error with the Google Play Services API contains some additional details.
                 ApiException apiException = (ApiException) e;
    
                 Util.showLog(TAG, "Error: " +
                         CommonStatusCodes.getStatusCodeString(apiException.getStatusCode()) + ": " +
                         apiException.getStatusMessage());
             } else {
                 // A different, unknown type of error occurred.
                Log.e(TAG, "ERROR! " + e.getMessage());
             }
    
         });
    
     } else {
      Log.e(TAG, "Prompt user to update Google Play services.";
    
     }
    

    } `

  4. Check your logs for decodedPayload if ctsProfileMatch and basicIntegrity both are false it means your device is rooted . The Attestation API returns a JWS response which looks like:

{ "nonce": "6pLrr9zWyl6TNzj+kpbR4LZcfPY3U2FmZXR5IE5ldCBTYW1wbGU6IDE2MTQ2NzkwMTIzNjc=", "timestampMs": 9860437986543, "apkPackageName": " your package name will be displayed here", "ctsProfileMatch": true, "apkDigestSha256": [ "base64 encoded, SHA-256 hash of the certificate used to sign requesting app" ], "basicIntegrity": true, "evaluationType": "BASIC" }

status of ctsProfileMatch and basicIntegrity For more info check this link.

As last quarter of 2021 today, I tried to use SafetyNet regarding @HimanshiThakur 's answer. But I got an issue and opened a question in here. Still no answer.

So I decided to use RootBeer. It works perfectly but when Magisk hides the root, it doesn't work.

If you don't care this case (and many bank apps can't solve this issue too), you can use these steps:

  1. Add this to Gradle:
implementation 'com.scottyab:rootbeer-lib:0.1.0'
  1. Use these lines:
RootBeer rootBeer = new RootBeer(context);
if (rootBeer.isRooted()) {
    //we found indication of root
} else {
    //we didn't find indication of root
}

There is Safety Net Attestation API of Google play services by which we can assess the device and determine if it is rooted/tampered.

Please go through my answer to deal with rooted devices:
https://stackoverflow.com/a/58304556/3908895

You can do this by following code :

public boolean getRootInfo() {
    if (checkRootFiles() || checkTags()) {
        return true;
    }
    return false;
}

private boolean checkRootFiles() {
    boolean root = false;
    String[] paths = {"/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
            "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su"};
    for (String path : paths) {
        root = new File(path).exists();
        if (root)
            break;
    }
    return root;
}

private boolean checkTags() {
    String tag = Build.TAGS;
    return tag != null && tag.trim().contains("test-keys");
}

You can also check this library RootBeer.

if you don't want to use any 3rd party library or any random solution then just use google lib for detecting it.

Android Device Verification

response :

{
  "timestampMs": 9860437986543,
  "nonce": "R2Rra24fVm5xa2Mg",
  "apkPackageName": "com.package.name.of.requesting.app",
  "apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the
                                  certificate used to sign requesting app"],
  "ctsProfileMatch": true,
  "basicIntegrity": true,
}

ctsProfileMatch it gives false if the device is rooted.

ref link: [1]: https://developer.android.com/training/safetynet/attestation

As of 2021 (today), looks like there's no any reliable way or method for detecting root, especially when powerful hiding tool such as MagiskHide is enabled. Most of the answers here are no longer relevant, so don't use it in production. Rely on proven check like SafetyNet, and instead of going the extra miles to detect root, I suggest to protect your app at both runtime, such as prevent debugger/instrumentation and make sure to use obfuscation.

Related