var VERBOSE = false;

var HOOKS = {
    FILE_ACCESS:     true,
    UNIX_FS:         true,
    RUNTIME_EXEC:    true,
    PACKAGE_MANAGER: true,
    SYSTEM_PROPS:    true,
    BUILD_FIELDS:    true,
    SETTINGS:        true,
    PROC_FILTER:     true,
    SSL_PINNING:     true,
    ROOTBEER:        true,
    SENTRY:          true,
    ONFIDO_EMU:      true,
    DEBUG_DETECT:    true,
    FRIDA_PORT:      true,
    TELEPHONY:       true,
    NET_INTERFACE:   true,
};

function log(msg)  { console.log(msg); }
function pass(msg) { if (VERBOSE) console.log(msg); }


var exactPaths = [
    "/su", "/system/bin/su", "/system/xbin/su", "/sbin/su",
    "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
    "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su",
    "/vendor/bin/su", "/cache/su", "/data/su",
    "/system/app/Superuser.apk", "/system/etc/init.d/99SuperSUDaemon",
    "/dev/com.koushikdutta.superuser.daemon/", "/system/xbin/daemonsu",
    "/sbin/magisk", "/sbin/.magisk", "/data/adb/magisk",
    "/data/adb/modules", "/system/xbin/magisk",
    "/cache/.disable_magisk", "/data/magisk",
    "/data/adb/ksu", "/data/adb/ksud",
    "/system/framework/XposedBridge.jar", "/system/lib/libxposed_art.so",
    "/data/adb/lspd", "/data/adb/modules/zygisk_lsposed",
    "/system/xbin/xposed",
    "/system/xbin/busybox", "/system/bin/busybox", "/sbin/busybox",
];

var dangerousPackages = [
    "com.koushikdutta.superuser", "com.thirdparty.superuser",
    "eu.chainfire.supersu", "com.noshufou.android.su",
    "com.topjohnwu.magisk", "me.weishu.kernelsu",
    "io.github.vvb2060.magisk", "io.github.huskydg.magisk",
];

var exactPathSet = {};
exactPaths.forEach(function(p) { exactPathSet[p] = true; });

function isDangerousPath(absPath) {
    if (exactPathSet[absPath]) return true;
    for (var i = 0; i < dangerousPackages.length; i++) {
        var pkg = dangerousPackages[i];
        var idx = absPath.indexOf(pkg);
        if (idx === -1) continue;
        var before = idx > 0 ? absPath.charAt(idx - 1) : "/";
        var after = idx + pkg.length < absPath.length ? absPath.charAt(idx + pkg.length) : "/";
        if (before === "/" && (after === "/" || after === "-" || idx + pkg.length === absPath.length)) return true;
    }
    return false;
}


var alwaysBlocked = ["busybox", "magisk", "resetprop", "daemonsu", "xposed"];

var conditionalCmds = {
    "su":      { blockAlone: true,  rootArgs: null },
    "which":   { blockAlone: false, rootArgs: ["su", "busybox", "magisk", "ksu", "daemonsu", "xposed"] },
    "getprop": { blockAlone: false, rootArgs: ["ro.debuggable", "ro.secure", "ro.build.tags",
                                               "ro.build.type", "init.svc.su", "init.svc.magisk"] },
};

function isRootCommand(cmd) {
    var trimmed = cmd.trim();
    if (!trimmed) return false;

    var tokens = trimmed.split(/\s+/);
    var baseName = tokens[0].substring(tokens[0].lastIndexOf("/") + 1);

    for (var i = 0; i < alwaysBlocked.length; i++) {
        if (cmd.indexOf(alwaysBlocked[i]) !== -1) return true;
    }

    var rule = conditionalCmds[baseName];
    if (rule) {
        if (tokens.length === 1 && rule.blockAlone) return true;
        if (rule.rootArgs) {
            for (var t = 1; t < tokens.length; t++) {
                for (var r = 0; r < rule.rootArgs.length; r++) {
                    if (tokens[t].indexOf(rule.rootArgs[r]) !== -1) return true;
                }
            }
        }
        if (baseName === "su" && tokens.length > 1) return true;
    }

    if (cmd.indexOf("frida") !== -1 || cmd.indexOf("ksu") !== -1) return true;
    return false;
}


Java.perform(function() {
    log("[*] loaded, active: " + Object.keys(HOOKS).filter(function(k) { return HOOKS[k]; }).join(", "));

    if (HOOKS.SSL_PINNING) {
        var okhttp3 = null;
        try { okhttp3 = Java.use('okhttp3.CertificatePinner'); } catch(e) {}

        if (okhttp3) {
            [
                'java.lang.String, java.util.List',
                'java.lang.String, java.security.cert.Certificate',
                'java.lang.String, [Ljava.security.cert.Certificate;',
            ].forEach(function(args) {
                try {
                    okhttp3.check.overload.apply(okhttp3.check, args.split(', ')).implementation = function(str) {
                        log("[ssl] okhttp3 check: " + str);
                        return true;
                    };
                } catch(e) {}
            });
            ["check$okhttp", "$check$1"].forEach(function(method) {
                try {
                    okhttp3[method].implementation = function(str) { log("[ssl] okhttp3 " + method + ": " + str); };
                } catch(e) {}
            });
        }

        try {
            var TMI = Java.use('com.android.org.conscrypt.TrustManagerImpl');
            TMI.verifyChain.implementation = function(untrusted, anchor, host) {
                log("[ssl] verifyChain host=" + host);
                return untrusted;
            };
            TMI.checkTrustedRecursive.implementation = function(certs, host) {
                log("[ssl] checkTrustedRecursive host=" + host);
                return Java.use('java.util.ArrayList').$new();
            };
        } catch(e) {}

        try {
            var X509 = Java.use('javax.net.ssl.X509TrustManager');
            X509.checkClientTrusted.implementation = function() {};
            X509.checkServerTrusted.implementation = function() {};
        } catch(e) {}
    }

    if (HOOKS.FILE_ACCESS) {
        var File = Java.use("java.io.File");

        ["exists", "canRead", "canExecute"].forEach(function(method) {
            File[method].implementation = function() {
                var path = this.getAbsolutePath();
                if (isDangerousPath(path)) {
                    log("[root] File." + method + ": " + path + " -> false");
                    return false;
                }
                var result = this[method]();
                pass("[pass] File." + method + ": " + path + " -> " + result);
                return result;
            };
        });

        File.canWrite.implementation = function() {
            var path = this.getAbsolutePath();
            if (isDangerousPath(path) || path === "/system" || path === "/data") {
                log("[root] File.canWrite: " + path + " -> false");
                return false;
            }
            var result = this.canWrite();
            pass("[pass] File.canWrite: " + path + " -> " + result);
            return result;
        };
    }

    if (HOOKS.UNIX_FS) {
        try {
            var UnixFS = Java.use("java.io.UnixFileSystem");
            UnixFS.checkAccess.implementation = function(file, access) {
                var path = file.toString();
                if (isDangerousPath(path)) {
                    log("[root] UnixFS.checkAccess: " + path + " -> false");
                    return false;
                }
                var r = this.checkAccess(file, access);
                pass("[pass] UnixFS.checkAccess: " + path + " -> " + r);
                return r;
            };
        } catch(e) {}
    }

    if (HOOKS.RUNTIME_EXEC) {
        var Runtime = Java.use("java.lang.Runtime");
        var PB = Java.use("java.lang.ProcessBuilder");

        Runtime.exec.overload('java.lang.String').implementation = function(cmd) {
            if (isRootCommand(cmd)) { log("[root] exec blocked: " + cmd); return this.exec("echo"); }
            pass("[pass] exec: " + cmd);
            return this.exec(cmd);
        };
        Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmds) {
            var cmd = cmds.join(" ");
            if (isRootCommand(cmd)) { log("[root] exec[] blocked: " + cmd); return this.exec(Java.array('java.lang.String', ["echo"])); }
            pass("[pass] exec[]: " + cmd);
            return this.exec(cmds);
        };
        Runtime.exec.overload('java.lang.String', '[Ljava.lang.String;').implementation = function(cmd, env) {
            if (isRootCommand(cmd)) { log("[root] exec+env blocked: " + cmd); return this.exec("echo"); }
            pass("[pass] exec+env: " + cmd);
            return this.exec(cmd, env);
        };
        Runtime.exec.overload('[Ljava.lang.String;', '[Ljava.lang.String;').implementation = function(cmds, env) {
            var cmd = cmds.join(" ");
            if (isRootCommand(cmd)) { log("[root] exec[]+env blocked: " + cmd); return this.exec(Java.array('java.lang.String', ["echo"]), env); }
            pass("[pass] exec[]+env: " + cmd);
            return this.exec(cmds, env);
        };

        PB.start.implementation = function() {
            var arr = [];
            var it = this.command().iterator();
            while (it.hasNext()) arr.push(it.next().toString());
            var cmdStr = arr.join(" ");
            if (isRootCommand(cmdStr)) {
                log("[root] ProcessBuilder blocked: " + cmdStr);
                this.command(Java.array('java.lang.String', ["echo"]));
            } else {
                pass("[pass] ProcessBuilder: " + cmdStr);
            }
            return this.start();
        };
    }

    if (HOOKS.BUILD_FIELDS) {
        var Build = Java.use("android.os.Build");
        Build.TAGS.value = "release-keys";
        Build.TYPE.value = "user";
        Build.FINGERPRINT.value = Build.FINGERPRINT.value.toString().replace("test-keys", "release-keys");

        var emuIndicators = ["generic", "sdk", "google_sdk", "vbox", "goldfish",
                             "ranchu", "nox", "bluestacks", "genymotion"];
        function looksEmulated(val) {
            if (!val) return false;
            var lower = val.toString().toLowerCase();
            for (var i = 0; i < emuIndicators.length; i++)
                if (lower.indexOf(emuIndicators[i]) !== -1) return true;
            return false;
        }

        var spoofs = { HARDWARE:"qcom", PRODUCT:"dream2lte", MODEL:"SM-G955F",
                       MANUFACTURER:"samsung", BRAND:"samsung", BOARD:"msm8998",
                       DEVICE:"dream2lte", HOST:"ubuntu-build" };
        Object.keys(spoofs).forEach(function(f) {
            if (looksEmulated(Build[f].value)) Build[f].value = spoofs[f];
        });
    }

    if (HOOKS.SYSTEM_PROPS) {
        try {
            var SP = Java.use("android.os.SystemProperties");
            var spoofedProps = {
                "ro.debuggable":"0", "ro.secure":"1", "ro.build.selinux":"1",
                "ro.build.tags":"release-keys", "ro.build.type":"user",
                "service.bootanim.exit":"1", "init.svc.su_daemon":"", "init.svc.magisk_daemon":""
            };
            SP.get.overload('java.lang.String').implementation = function(key) {
                if (spoofedProps.hasOwnProperty(key)) { log("[root] prop " + key + " -> " + spoofedProps[key]); return spoofedProps[key]; }
                return this.get(key);
            };
            SP.get.overload('java.lang.String', 'java.lang.String').implementation = function(key, def) {
                if (spoofedProps.hasOwnProperty(key)) { log("[root] prop " + key + " -> " + spoofedProps[key]); return spoofedProps[key]; }
                return this.get(key, def);
            };
        } catch(e) {}
    }

    if (HOOKS.PACKAGE_MANAGER) {
        try {
            var PM = Java.use("android.app.ApplicationPackageManager");
            var dangerousPkgs = [
                "com.koushikdutta.superuser","com.thirdparty.superuser",
                "eu.chainfire.supersu","com.noshufou.android.su",
                "com.zachspong.temprootremovejb","com.ramdroid.appquarantine",
                "com.koushikdutta.rommanager","com.koushikdutta.rommanager.license",
                "com.dimonvideo.luckypatcher","com.chelpus.lackypatch",
                "com.ramdroid.appquarantinepro",
                "com.topjohnwu.magisk","io.github.vvb2060.magisk","io.github.huskydg.magisk",
                "me.weishu.kernelsu",
                "de.robv.android.xposed.installer","org.meowcat.edxposed.manager","org.lsposed.manager",
                "re.frida.server",
                "com.amphoras.hidemyroot","com.amphoras.hidemyrootadfree",
                "com.formyhm.hideroot","com.saurik.substrate",
                "com.devadvance.rootcloak","com.devadvance.rootcloak2",
                "com.yellowes.su","com.jrummy.root.browserfree","com.jrummy.busybox.installer"
            ];
            var dangerousPkgSet = {};
            dangerousPkgs.forEach(function(p) { dangerousPkgSet[p] = true; });

            PM.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pkg, flags) {
                if (dangerousPkgSet[pkg]) {
                    log("[root] getPackageInfo hidden: " + pkg);
                    throw Java.use("android.content.pm.PackageManager$NameNotFoundException").$new(pkg);
                }
                return this.getPackageInfo(pkg, flags);
            };
            try {
                PM.getPackageInfo.overload('java.lang.String', 'android.content.pm.PackageManager$PackageInfoFlags').implementation = function(pkg, flags) {
                    if (dangerousPkgSet[pkg]) {
                        log("[root] getPackageInfo hidden: " + pkg);
                        throw Java.use("android.content.pm.PackageManager$NameNotFoundException").$new(pkg);
                    }
                    return this.getPackageInfo(pkg, flags);
                };
            } catch(e) {}

            PM.getInstalledPackages.overload('int').implementation = function(flags) {
                var pkgs = this.getInstalledPackages(flags);
                var it = pkgs.iterator();
                while (it.hasNext()) { if (dangerousPkgSet[it.next().packageName.value]) it.remove(); }
                return pkgs;
            };
            PM.getInstalledApplications.overload('int').implementation = function(flags) {
                var apps = this.getInstalledApplications(flags);
                var it = apps.iterator();
                while (it.hasNext()) { if (dangerousPkgSet[it.next().packageName.value]) it.remove(); }
                return apps;
            };
        } catch(e) {}
    }

    if (HOOKS.SETTINGS) {
        var adbKeys = { "adb_enabled": true, "development_settings_enabled": true };
        try {
            var SS = Java.use("android.provider.Settings$Secure");
            SS.getInt.overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function(cr, name, def) {
                if (adbKeys[name]) { log("[root] Secure.getInt " + name + " -> 0"); return 0; }
                return this.getInt(cr, name, def);
            };
            SS.getInt.overload('android.content.ContentResolver', 'java.lang.String').implementation = function(cr, name) {
                if (adbKeys[name]) { log("[root] Secure.getInt " + name + " -> 0"); return 0; }
                return this.getInt(cr, name);
            };
            SS.getString.overload('android.content.ContentResolver', 'java.lang.String').implementation = function(cr, name) {
                if (adbKeys[name]) { log("[root] Secure.getString " + name + " -> 0"); return "0"; }
                return this.getString(cr, name);
            };
        } catch(e) {}

        try {
            var SG = Java.use("android.provider.Settings$Global");
            SG.getInt.overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function(cr, name, def) {
                if (name === "adb_enabled") { log("[root] Global.getInt adb_enabled -> 0"); return 0; }
                return this.getInt(cr, name, def);
            };
        } catch(e) {}
    }

    if (HOOKS.PROC_FILTER) {
        try {
            var FR = Java.use("java.io.FileReader");
            var BR = Java.use("java.io.BufferedReader");
            var JS = Java.use("java.lang.System");
            var sensitiveFiles = ["/proc/mounts","/proc/self/mounts","/proc/self/maps",
                                  "/proc/self/mountinfo","/proc/self/status"];
            var lineFilters = ["magisk","/su","supersu","ksu","kernelsu","zygisk",
                               "frida","gadget","linjector","gmain"];
            var sensHashes = {};
            function isSensitive(p) {
                for (var i = 0; i < sensitiveFiles.length; i++)
                    if (p.indexOf(sensitiveFiles[i]) !== -1) return true;
                return false;
            }
            function shouldFilter(l) {
                for (var i = 0; i < lineFilters.length; i++)
                    if (l.indexOf(lineFilters[i]) !== -1) return true;
                return false;
            }
            FR.$init.overload('java.lang.String').implementation = function(path) {
                this.$init(path);
                if (isSensitive(path)) sensHashes[JS.identityHashCode(this)] = path;
            };
            FR.$init.overload('java.io.File').implementation = function(file) {
                this.$init(file);
                var p = file.getAbsolutePath();
                if (isSensitive(p)) sensHashes[JS.identityHashCode(this)] = p;
            };
            BR.$init.overload('java.io.Reader').implementation = function(reader) {
                this.$init(reader);
                var rh = JS.identityHashCode(reader);
                if (sensHashes[rh]) { sensHashes[JS.identityHashCode(this)] = sensHashes[rh]; delete sensHashes[rh]; }
            };
            BR.$init.overload('java.io.Reader', 'int').implementation = function(reader, sz) {
                this.$init(reader, sz);
                var rh = JS.identityHashCode(reader);
                if (sensHashes[rh]) { sensHashes[JS.identityHashCode(this)] = sensHashes[rh]; delete sensHashes[rh]; }
            };
            BR.readLine.overload().implementation = function() {
                var line = this.readLine();
                var h = JS.identityHashCode(this);
                if (sensHashes[h] && line !== null && shouldFilter(line.toString())) {
                    log("[root] filtered (" + sensHashes[h] + "): " + line.toString().substring(0, 80));
                    return this.readLine();
                }
                return line;
            };
        } catch(e) {}
    }

    if (HOOKS.ROOTBEER) {
        try {
            var RB = Java.use("com.scottyab.rootbeer.RootBeer");
            ["isRooted","isRootedWithoutBusyBoxCheck","detectRootManagementApps",
             "detectPotentiallyDangerousApps","detectTestKeys","checkForBusyBoxBinary",
             "checkForSuBinary","checkSuExists","checkForRWPaths","checkForDangerousProps",
             "checkForRootNative","detectRootCloakingApps","isSelinuxFlagInEnabled",
             "checkForMagiskBinary"].forEach(function(m) {
                try { RB[m].implementation = function() { return false; }; } catch(e) {}
            });
        } catch(e) {}
        try {
            var RBN = Java.use("com.scottyab.rootbeer.RootBeerNative");
            RBN.checkForRoot.implementation = function() { return 0; };
        } catch(e) {}
    }

    if (HOOKS.SENTRY) {
        try {
            var SR = Java.use('io.sentry.android.core.internal.util.RootChecker');
            SR.isDeviceRooted.implementation = function() { return false; };
        } catch(e) {}
        try {
            var SE = Java.use('io.sentry.android.core.BuildInfoProvider');
            SE.isEmulator.implementation = function() { return Java.use("java.lang.Boolean").$new(false); };
        } catch(e) {}
    }

    if (HOOKS.ONFIDO_EMU) {
        try {
            var OE = Java.use('com.onfido.android.sdk.capture.internal.util.EmulatorDetector');
            OE.isEmulator.implementation = function() { return false; };
        } catch(e) {}
    }

    if (HOOKS.DEBUG_DETECT) {
        try {
            var Dbg = Java.use('android.os.Debug');
            Dbg.isDebuggerConnected.implementation = function() { return false; };
            Dbg.waitingForDebugger.implementation = function() { return false; };
        } catch(e) {}
    }

    if (HOOKS.FRIDA_PORT) {
        try {
            var Sock = Java.use("java.net.Socket");
            function blockFrida(s) { return s.indexOf("27042") !== -1 || s.indexOf("27043") !== -1; }
            Sock.connect.overload('java.net.SocketAddress', 'int').implementation = function(addr, timeout) {
                if (blockFrida(addr.toString())) {
                    log("[frida] blocked connect: " + addr);
                    throw Java.use("java.net.ConnectException").$new("Connection refused");
                }
                return this.connect(addr, timeout);
            };
            Sock.connect.overload('java.net.SocketAddress').implementation = function(addr) {
                if (blockFrida(addr.toString())) {
                    log("[frida] blocked connect: " + addr);
                    throw Java.use("java.net.ConnectException").$new("Connection refused");
                }
                return this.connect(addr);
            };
        } catch(e) {}
    }

    if (HOOKS.TELEPHONY) {
        try {
            var TM = Java.use('android.telephony.TelephonyManager');
            TM.getNetworkOperatorName.overload().implementation = function() { return "T-Mobile"; };
            TM.getSimOperatorName.overload().implementation = function() { return "T-Mobile"; };
            TM.getLine1Number.overload().implementation = function() { return "+1234567890"; };
        } catch(e) {}
    }

    if (HOOKS.NET_INTERFACE) {
        try {
            var NI = Java.use("java.net.NetworkInterface");
            NI.getName.implementation = function() {
                var name = this.getName();
                return name === "eth0" ? "wlan0" : name;
            };
        } catch(e) {}
    }

    log("[*] ready");
});
