├── .classpath ├── .gitignore ├── .project ├── AndroidManifest.xml ├── LICENSE ├── assets └── busybox ├── proguard.cfg ├── project.properties ├── res ├── drawable-hdpi │ └── icon.png ├── drawable-ldpi │ └── icon.png ├── drawable-mdpi │ └── icon.png ├── drawable-xhdpi │ └── icon.png ├── layout │ ├── main.xml │ ├── status_available.xml │ ├── status_unavailable.xml │ └── status_unavailable_with_google_play_link.xml ├── menu │ └── main_menu.xml └── values │ ├── strings.xml │ └── styles.xml └── src └── org └── projectvoodoo └── otarootkeeper ├── MainActivity.java ├── backend ├── Device.java ├── SuOperations.java └── Utils.java └── ui └── StatusRow.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated GUI files 12 | *R.java 13 | 14 | BuildConfig.java 15 | 16 | proguard/ 17 | bin/ 18 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | VoodooOTARootKeeper 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 19 | 20 | 21 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /assets/busybox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-voodoo/ota-rootkeeper-app/0c998e456364eb8c4829c4a3581093a88726b5af/assets/busybox -------------------------------------------------------------------------------- /proguard.cfg: -------------------------------------------------------------------------------- 1 | -optimizationpasses 5 2 | -dontusemixedcaseclassnames 3 | -dontskipnonpubliclibraryclasses 4 | -dontpreverify 5 | -verbose 6 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/* 7 | 8 | -keep public class * extends android.app.Activity 9 | -keep public class * extends android.app.Application 10 | -keep public class * extends android.app.Service 11 | -keep public class * extends android.content.BroadcastReceiver 12 | -keep public class * extends android.content.ContentProvider 13 | -keep public class * extends android.app.backup.BackupAgentHelper 14 | -keep public class * extends android.preference.Preference 15 | -keep public class com.android.vending.licensing.ILicensingService 16 | 17 | -keepclasseswithmembernames class * { 18 | native ; 19 | } 20 | 21 | -keepclasseswithmembers class * { 22 | public (android.content.Context, android.util.AttributeSet); 23 | } 24 | 25 | -keepclasseswithmembers class * { 26 | public (android.content.Context, android.util.AttributeSet, int); 27 | } 28 | 29 | -keepclassmembers class * extends android.app.Activity { 30 | public void *(android.view.View); 31 | } 32 | 33 | -keepclassmembers enum * { 34 | public static **[] values(); 35 | public static ** valueOf(java.lang.String); 36 | } 37 | 38 | -keep class * implements android.os.Parcelable { 39 | public static final android.os.Parcelable$Creator *; 40 | } 41 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | # Project target. 11 | target=android-15 12 | -------------------------------------------------------------------------------- /res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-voodoo/ota-rootkeeper-app/0c998e456364eb8c4829c4a3581093a88726b5af/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-voodoo/ota-rootkeeper-app/0c998e456364eb8c4829c4a3581093a88726b5af/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-voodoo/ota-rootkeeper-app/0c998e456364eb8c4829c4a3581093a88726b5af/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-voodoo/ota-rootkeeper-app/0c998e456364eb8c4829c4a3581093a88726b5af/res/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 12 | 17 | 24 | 25 | 31 | 32 | 39 | 40 | 46 | 47 | 54 | 55 | 60 | 65 | 70 | 71 | 72 | 77 | 82 | 83 | 84 | 89 | 94 | 95 | 96 | 101 | 106 | 107 | 108 | 113 | 118 | 119 | 120 | 121 | 128 | 135 | 142 | 149 | 156 | 157 | 163 | 168 | 174 | 180 | 181 | -------------------------------------------------------------------------------- /res/layout/status_available.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /res/layout/status_unavailable.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 11 | 12 | -------------------------------------------------------------------------------- /res/layout/status_unavailable_with_google_play_link.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | -------------------------------------------------------------------------------- /res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Voodoo OTA RootKeeper 5 | Introduction: 6 | This app saves a protected copy of \"su\" 7 | for devices already rooted.\nIf you loose root permissions after an 8 | OTA update, RootKeeper might be able to restore it. 9 | Works with: 10 | Any device using Ext3 or Ext4 filesystem and Google 11 | style incremental OTA update.zip 12 | Useless with: 13 | Samsung phones updated via KIES or Odin and older 14 | devices using YAFFS filesystems only, any device flashing entiere 15 | system partition. 16 | Status: 17 | Superuser app installed 18 | Device rooted 19 | Root permission granted 20 | /system supports root protection 21 | Protected su copy available 22 | Google Play 23 | Protect root 24 | Restore root 25 | Backup root 26 | root su protected 27 | root su simple backup completed 28 | root su restored 29 | root su backup deleted 30 | Error when upgrading su backup. 31 | You need to send a logcat error report to developer 32 | Device un-rooted! (with backup) 33 | Delete su backup 34 | Temp. un-root\n(keeps backup) 35 | Notes: 36 | If you\'re using both Superuser and SuperSU, 37 | take care keeping application and su backup consistent. 38 | Author: 39 | Application by François Simond aka supercurio 40 | \nFollow on Twitter:\nhttps://twitter.com/supercurio 41 | \n\nMore apps:\nhttps://play.google.com/store/apps/developer?id=supercurio+-+Project+Voodoo 42 | \n\nXDA discussion thread for community help and support\nhttp://forum.xda-developers.com/showthread.php?t=1241517 43 | 44 | -------------------------------------------------------------------------------- /res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 16 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/org/projectvoodoo/otarootkeeper/MainActivity.java: -------------------------------------------------------------------------------- 1 | 2 | package org.projectvoodoo.otarootkeeper; 3 | 4 | import android.app.Activity; 5 | import android.os.AsyncTask; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.Menu; 9 | import android.view.MenuInflater; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.view.View.OnClickListener; 13 | import android.widget.Button; 14 | 15 | import org.projectvoodoo.otarootkeeper.R.id; 16 | import org.projectvoodoo.otarootkeeper.backend.Device; 17 | import org.projectvoodoo.otarootkeeper.backend.Device.FileSystem; 18 | import org.projectvoodoo.otarootkeeper.backend.Utils; 19 | import org.projectvoodoo.otarootkeeper.ui.StatusRow; 20 | 21 | public class MainActivity extends Activity implements OnClickListener { 22 | 23 | private static final String TAG = "Voodoo OTA RootKeeper MainActivity"; 24 | 25 | private Device mDevice; 26 | 27 | private StatusRow mSuperuserRow; 28 | private StatusRow mRootedRow; 29 | private StatusRow mRootGrantedRow; 30 | private StatusRow mFsSupportedRow; 31 | private StatusRow mSuProtectedRow; 32 | private Button mBackupButton; 33 | private Button mRestoreButton; 34 | private Button mDeleteBackupButton; 35 | private Button mUnrootButton; 36 | 37 | private boolean canGainSu; 38 | 39 | @Override 40 | public void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | 43 | Log.i(TAG, "Starting app"); 44 | 45 | setContentView(R.layout.main); 46 | setTitle(getString(R.string.app_name) + " v" + getVersionName()); 47 | 48 | mSuperuserRow = (StatusRow) findViewById(id.superuser_app_installed); 49 | mRootedRow = (StatusRow) findViewById(id.rooted); 50 | mRootGrantedRow = (StatusRow) findViewById(id.root_granted); 51 | mFsSupportedRow = (StatusRow) findViewById(id.fs_supported); 52 | mSuProtectedRow = (StatusRow) findViewById(id.su_protected); 53 | mBackupButton = (Button) findViewById(id.button_backup_root); 54 | mBackupButton.setOnClickListener(this); 55 | mRestoreButton = (Button) findViewById(id.button_restore_root); 56 | mRestoreButton.setOnClickListener(this); 57 | mDeleteBackupButton = (Button) findViewById(id.button_delete_backup); 58 | mDeleteBackupButton.setOnClickListener(this); 59 | mUnrootButton = (Button) findViewById(id.button_unroot); 60 | mUnrootButton.setOnClickListener(this); 61 | 62 | mBackupButton.setVisibility(View.GONE); 63 | mRestoreButton.setVisibility(View.GONE); 64 | mDeleteBackupButton.setVisibility(View.GONE); 65 | mUnrootButton.setVisibility(View.GONE); 66 | 67 | new UiSetup().execute(); 68 | } 69 | 70 | private void showStatus() { 71 | if (mDevice.isSuperuserAppInstalled) 72 | mSuperuserRow.setAvailable(true); 73 | else 74 | mSuperuserRow.setAvailable(false, "market://details?id=com.noshufou.android.su"); 75 | 76 | mRootedRow.setAvailable(mDevice.isRooted); 77 | 78 | mRootGrantedRow.setAvailable(canGainSu); 79 | 80 | if (mDevice.mFileSystem == FileSystem.EXTFS) 81 | mFsSupportedRow.setAvailable(true); 82 | else 83 | mFsSupportedRow.setAvailable(false); 84 | 85 | mSuProtectedRow.setAvailable(mDevice.isSuProtected); 86 | 87 | mBackupButton.setText(mDevice.mFileSystem == FileSystem.EXTFS ? 88 | R.string.protect_root : R.string.backup_root); 89 | 90 | mBackupButton.setVisibility( 91 | mDevice.isRooted 92 | && !mDevice.isSuProtected ? 93 | View.VISIBLE : View.GONE); 94 | 95 | mRestoreButton.setVisibility( 96 | !mDevice.isRooted 97 | && mDevice.isSuProtected ? 98 | View.VISIBLE : View.GONE); 99 | 100 | mDeleteBackupButton.setVisibility( 101 | mDevice.isSuProtected 102 | && mDevice.isRooted ? 103 | View.VISIBLE : View.GONE); 104 | 105 | mUnrootButton.setVisibility( 106 | mDevice.isSuProtected 107 | && mDevice.isRooted ? 108 | View.VISIBLE : View.GONE); 109 | } 110 | 111 | @Override 112 | public boolean onCreateOptionsMenu(Menu menu) { 113 | MenuInflater inflater = getMenuInflater(); 114 | inflater.inflate(R.menu.main_menu, menu); 115 | return true; 116 | } 117 | 118 | @Override 119 | public boolean onOptionsItemSelected(MenuItem item) { 120 | // Handle item selection 121 | switch (item.getItemId()) { 122 | case id.refresh: 123 | mDevice.analyzeSu(); 124 | showStatus(); 125 | return true; 126 | default: 127 | return super.onOptionsItemSelected(item); 128 | } 129 | } 130 | 131 | @Override 132 | public void onClick(View v) { 133 | 134 | switch (v.getId()) { 135 | 136 | case R.id.button_backup_root: 137 | mDevice.mSuOps.backup(); 138 | break; 139 | 140 | case R.id.button_restore_root: 141 | mDevice.mSuOps.restore(); 142 | break; 143 | 144 | case R.id.button_delete_backup: 145 | mDevice.mSuOps.deleteBackup(); 146 | break; 147 | 148 | case R.id.button_unroot: 149 | mDevice.mSuOps.unRoot(); 150 | break; 151 | 152 | default: 153 | break; 154 | } 155 | 156 | mDevice.analyzeSu(); 157 | showStatus(); 158 | } 159 | 160 | private String getVersionName() { 161 | try { 162 | return getPackageManager().getPackageInfo(getPackageName(), 0).versionName; 163 | } catch (Exception e) { 164 | } 165 | return null; 166 | } 167 | 168 | class UiSetup extends AsyncTask { 169 | 170 | @Override 171 | protected Void doInBackground(Void... params) { 172 | mDevice = new Device(getApplicationContext()); 173 | canGainSu = Utils.canGainSu(getApplicationContext()); 174 | return null; 175 | } 176 | 177 | @Override 178 | protected void onPostExecute(Void result) { 179 | showStatus(); 180 | super.onPostExecute(result); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/org/projectvoodoo/otarootkeeper/backend/Device.java: -------------------------------------------------------------------------------- 1 | 2 | package org.projectvoodoo.otarootkeeper.backend; 3 | 4 | import android.content.Context; 5 | import android.content.pm.PackageManager.NameNotFoundException; 6 | import android.util.Log; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.File; 10 | import java.io.FileNotFoundException; 11 | import java.io.FileReader; 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | 15 | public class Device { 16 | 17 | private static final String TAG = "Voodoo OTA RootKeeper Device"; 18 | 19 | private Context mContext; 20 | public SuOperations mSuOps; 21 | 22 | public Boolean isRooted = false; 23 | public Boolean isSuperuserAppInstalled = false; 24 | public Boolean isSuProtected = false; 25 | 26 | public String validSuPath = SuOperations.SU_BACKUP_PATH; 27 | public boolean needSuBackupUpgrade = false; 28 | 29 | public enum FileSystem { 30 | EXTFS, 31 | UNSUPPORTED 32 | } 33 | 34 | public FileSystem mFileSystem = FileSystem.UNSUPPORTED; 35 | 36 | public Device(Context context) { 37 | mContext = context; 38 | 39 | ensureAttributeUtilsAvailability(); 40 | detectSystemFs(); 41 | 42 | analyzeSu(); 43 | 44 | mSuOps = new SuOperations(context, this); 45 | } 46 | 47 | private void detectSystemFs() { 48 | // detect an ExtFS filesystem 49 | 50 | try { 51 | BufferedReader in = new BufferedReader(new FileReader("/proc/mounts"), 8192); 52 | 53 | String line; 54 | String parsedFs; 55 | 56 | while ((line = in.readLine()) != null) { 57 | if (line.matches(".*system.*")) { 58 | Log.i(TAG, "/system mount point: " + line); 59 | parsedFs = line.split(" ")[2].trim(); 60 | 61 | if (parsedFs.equals("ext2") 62 | || parsedFs.equals("ext3") 63 | || parsedFs.equals("ext4")) { 64 | Log.i(TAG, "/system filesystem support extended attributes"); 65 | mFileSystem = FileSystem.EXTFS; 66 | in.close(); 67 | return; 68 | } 69 | } 70 | } 71 | in.close(); 72 | 73 | } catch (Exception e) { 74 | Log.e(TAG, "Impossible to parse /proc/mounts"); 75 | e.printStackTrace(); 76 | } 77 | 78 | Log.i(TAG, "/system filesystem doesn't support extended attributes"); 79 | mFileSystem = FileSystem.UNSUPPORTED; 80 | 81 | } 82 | 83 | private void ensureAttributeUtilsAvailability() { 84 | 85 | String[] symlinks = { 86 | "test", 87 | "lsattr", 88 | "chattr" 89 | }; 90 | 91 | // verify custom busybox presence by test, lsattr and chattr 92 | // files/symlinks 93 | try { 94 | mContext.openFileInput("busybox"); 95 | for (String s : symlinks) 96 | mContext.openFileInput(s); 97 | 98 | } catch (FileNotFoundException notfoundE) { 99 | Log.d(TAG, "Extracting tools from assets is required"); 100 | 101 | try { 102 | Utils.copyFromAssets(mContext, "busybox", "busybox"); 103 | 104 | String filesPath = mContext.getFilesDir().getAbsolutePath(); 105 | String script = "chmod 700 " + filesPath + "/busybox\n"; 106 | for (String s : symlinks) { 107 | script += "rm " + filesPath + "/" + s + "\n"; 108 | script += "ln -s busybox " + filesPath + "/" + s + "\n"; 109 | } 110 | 111 | Utils.run(script); 112 | 113 | } catch (Exception e) { 114 | e.printStackTrace(); 115 | } 116 | } 117 | } 118 | 119 | public void analyzeSu() { 120 | isRooted = detectValidSuBinaryInPath(); 121 | isSuperuserAppInstalled = isSuperUserApkinstalled(); 122 | isSuProtected = isSuProtected(); 123 | } 124 | 125 | private Boolean isSuProtected() { 126 | 127 | HashMap paths = new HashMap(); 128 | paths.put(SuOperations.SU_BACKUP_PATH, false); 129 | paths.put(SuOperations.SU_BACKUP_PATH_OLD, true); 130 | 131 | for (String path : paths.keySet()) 132 | switch (mFileSystem) { 133 | case EXTFS: 134 | String lsattr = mContext.getFilesDir().getAbsolutePath() + "/lsattr"; 135 | 136 | ArrayList output = Utils.run(lsattr + " " + path); 137 | 138 | if (output.size() == 1) { 139 | String attrs = output.get(0); 140 | Log.d(TAG, "attributes: " + attrs); 141 | 142 | if (attrs.matches(".*-i-.*" + SuOperations.SU_BACKUP_FILENAME)) { 143 | if (Utils.isSuid(mContext, path)) { 144 | Log.i(TAG, "su binary is already protected"); 145 | validSuPath = path; 146 | needSuBackupUpgrade = paths.get(path); 147 | return true; 148 | } 149 | } 150 | } 151 | 152 | break; 153 | 154 | case UNSUPPORTED: 155 | if (Utils.isSuid(mContext, SuOperations.SU_BACKUP_PATH)) { 156 | validSuPath = path; 157 | needSuBackupUpgrade = paths.get(path); 158 | return true; 159 | } 160 | 161 | } 162 | return false; 163 | } 164 | 165 | private Boolean detectValidSuBinaryInPath() { 166 | // search for valid su binaries in PATH 167 | 168 | String[] pathToTest = System.getenv("PATH").split(":"); 169 | 170 | for (String path : pathToTest) { 171 | File suBinary = new File(path + "/su"); 172 | 173 | if (suBinary.exists()) { 174 | if (Utils.isSuid(mContext, suBinary.getAbsolutePath())) { 175 | Log.d(TAG, "Found adequate su binary at " + suBinary.getAbsolutePath()); 176 | return true; 177 | } 178 | } 179 | } 180 | return false; 181 | } 182 | 183 | private Boolean isSuperUserApkinstalled() { 184 | try { 185 | mContext.getPackageManager().getPackageInfo("com.noshufou.android.su", 0); 186 | Log.d(TAG, "Superuser.apk installed"); 187 | return true; 188 | } catch (NameNotFoundException e) { 189 | return false; 190 | } 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/org/projectvoodoo/otarootkeeper/backend/SuOperations.java: -------------------------------------------------------------------------------- 1 | 2 | package org.projectvoodoo.otarootkeeper.backend; 3 | 4 | import android.content.Context; 5 | import android.util.Log; 6 | import android.widget.Toast; 7 | 8 | import org.projectvoodoo.otarootkeeper.R; 9 | import org.projectvoodoo.otarootkeeper.backend.Device.FileSystem; 10 | 11 | import java.util.ArrayList; 12 | 13 | public class SuOperations { 14 | 15 | private Context mContext; 16 | private Device mDevice; 17 | 18 | private static final String TAG = "Voodoo OTA RootKeeper ProtectedSuOperation"; 19 | public static final String SU_PATH = "/system/bin/su"; 20 | public static final String SU_BACKUP_BASE_DIR = "/system/usr"; 21 | public static final String SU_BACKUP_DIR = SU_BACKUP_BASE_DIR + "/we-need-root"; 22 | public static final String SU_BACKUP_FILENAME = "su-backup"; 23 | public static final String SU_BACKUP_PATH = SU_BACKUP_DIR + "/" + SU_BACKUP_FILENAME; 24 | public static final String SU_BACKUP_PATH_OLD = "/system/" + SU_BACKUP_FILENAME; 25 | public static final String CMD_REMOUNT_RW = "mount -o remount,rw /system /system"; 26 | public static final String CMD_REMOUNT_RO = "mount -o remount,ro /system /system"; 27 | 28 | public SuOperations(Context context, Device device) { 29 | mContext = context; 30 | mDevice = device; 31 | } 32 | 33 | public final void backup() { 34 | 35 | Log.i(TAG, "Backup to protected su"); 36 | 37 | String suSource = "/system/xbin/su"; 38 | 39 | ArrayList commands = new ArrayList(); 40 | commands.add(CMD_REMOUNT_RW); 41 | 42 | // de-protect 43 | if (mDevice.mFileSystem == FileSystem.EXTFS) 44 | commands.add(mContext.getFilesDir().getAbsolutePath() 45 | + "/chattr -i " + SU_BACKUP_PATH); 46 | 47 | if (Utils.isSuid(mContext, SU_PATH)) 48 | suSource = SU_PATH; 49 | 50 | commands.add("mkdir " + SU_BACKUP_BASE_DIR); 51 | commands.add("mkdir " + SU_BACKUP_DIR); 52 | commands.add("chmod 001 " + SU_BACKUP_DIR); 53 | commands.add("cat " + suSource + " > " + SU_BACKUP_PATH); 54 | commands.add("chmod 06755 " + SU_BACKUP_PATH); 55 | 56 | // protect 57 | if (mDevice.mFileSystem == FileSystem.EXTFS) 58 | commands.add(mContext.getFilesDir().getAbsolutePath() 59 | + "/chattr +i " + SU_BACKUP_PATH); 60 | 61 | commands.add(CMD_REMOUNT_RO); 62 | 63 | Utils.run("su", commands); 64 | 65 | int toastText; 66 | if (mDevice.mFileSystem == FileSystem.EXTFS) 67 | toastText = R.string.toast_su_protected; 68 | else 69 | toastText = R.string.toast_su_backup; 70 | 71 | Toast.makeText(mContext, toastText, Toast.LENGTH_LONG).show(); 72 | } 73 | 74 | public final void restore() { 75 | String[] commands = { 76 | CMD_REMOUNT_RW, 77 | 78 | // restore su binary to /system/bin/su 79 | // choose bin over xbin to avoid confusion 80 | "cat " + mDevice.validSuPath + " > " + SU_PATH, 81 | "chown 0:0 " + SU_PATH, 82 | "chmod 06755 " + SU_PATH, 83 | "rm /system/xbin/su", 84 | 85 | CMD_REMOUNT_RO, 86 | }; 87 | 88 | Utils.run(mDevice.validSuPath, commands); 89 | upgradeSuBackup(); 90 | 91 | Toast.makeText(mContext, R.string.toast_su_restore, Toast.LENGTH_LONG).show(); 92 | } 93 | 94 | public final void deleteBackup() { 95 | 96 | Log.i(TAG, "Delete protected or backup su"); 97 | 98 | ArrayList commands = new ArrayList(); 99 | commands.add(CMD_REMOUNT_RW); 100 | 101 | // de-protect 102 | if (mDevice.mFileSystem == FileSystem.EXTFS) 103 | commands.add(mContext.getFilesDir().getAbsolutePath() 104 | + "/chattr -i " + mDevice.validSuPath); 105 | 106 | commands.add("rm " + mDevice.validSuPath); 107 | commands.add("rm -r " + SU_BACKUP_DIR); 108 | commands.add(CMD_REMOUNT_RO); 109 | 110 | Utils.run("su", commands); 111 | 112 | Toast.makeText(mContext, R.string.toast_su_delete_backup, Toast.LENGTH_LONG).show(); 113 | } 114 | 115 | public final void unRoot() { 116 | 117 | Log.i(TAG, "Unroot device but keep su backup"); 118 | 119 | upgradeSuBackup(); 120 | if (!mDevice.isSuProtected) { 121 | Toast.makeText(mContext, R.string.toast_su_protection_error, Toast.LENGTH_LONG).show(); 122 | return; 123 | } 124 | 125 | String[] commands = new String[] { 126 | CMD_REMOUNT_RW, 127 | "rm /system/*bin/su", 128 | CMD_REMOUNT_RO, 129 | }; 130 | 131 | Utils.run("su", commands); 132 | 133 | Toast.makeText(mContext, R.string.toast_unroot, Toast.LENGTH_LONG).show(); 134 | } 135 | 136 | private void upgradeSuBackup() { 137 | if (!mDevice.needSuBackupUpgrade) 138 | return; 139 | 140 | Log.i(TAG, "Upgrade su backup"); 141 | deleteBackup(); 142 | backup(); 143 | mDevice.analyzeSu(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/org/projectvoodoo/otarootkeeper/backend/Utils.java: -------------------------------------------------------------------------------- 1 | 2 | package org.projectvoodoo.otarootkeeper.backend; 3 | 4 | import android.content.Context; 5 | import android.util.Log; 6 | 7 | import java.io.BufferedOutputStream; 8 | import java.io.BufferedReader; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.InputStreamReader; 13 | import java.util.ArrayList; 14 | 15 | public class Utils { 16 | 17 | private static final String TAG = "Voodoo Utils"; 18 | 19 | public static final void copyFromAssets(Context context, String source, String destination) 20 | throws IOException { 21 | 22 | // read file from the apk 23 | InputStream is = context.getAssets().open(source); 24 | int size = is.available(); 25 | byte[] buffer = new byte[size]; 26 | is.read(buffer); 27 | is.close(); 28 | 29 | // write files in app private storage 30 | FileOutputStream output = context.openFileOutput(destination, Context.MODE_PRIVATE); 31 | output.write(buffer); 32 | output.close(); 33 | 34 | Log.d(TAG, source + " asset copied to " + destination); 35 | } 36 | 37 | public static final Boolean isSuid(Context context, String filename) { 38 | 39 | try { 40 | 41 | Process p = Runtime.getRuntime().exec(context.getFilesDir() + "/test -u " + filename); 42 | p.waitFor(); 43 | if (p.exitValue() == 0) { 44 | Log.d(TAG, filename + " is set-user-ID"); 45 | return true; 46 | } 47 | } catch (Exception e) { 48 | e.printStackTrace(); 49 | } 50 | Log.d(TAG, filename + " is not set-user-ID"); 51 | return false; 52 | 53 | } 54 | 55 | public static ArrayList run(String command) { 56 | return run("/system/bin/sh", command); 57 | } 58 | 59 | public static ArrayList run(String shell, String command) { 60 | return run(shell, new String[] { 61 | command 62 | }); 63 | } 64 | 65 | public static ArrayList run(String shell, ArrayList commands) { 66 | String[] commandsArray = new String[commands.size()]; 67 | commands.toArray(commandsArray); 68 | return run(shell, commandsArray); 69 | } 70 | 71 | public static ArrayList run(String shell, String[] commands) { 72 | ArrayList output = new ArrayList(); 73 | 74 | try { 75 | Process process = Runtime.getRuntime().exec(shell); 76 | 77 | BufferedOutputStream shellInput = 78 | new BufferedOutputStream(process.getOutputStream()); 79 | BufferedReader shellOutput = 80 | new BufferedReader(new InputStreamReader(process.getInputStream())); 81 | 82 | for (String command : commands) { 83 | Log.i(TAG, "command: " + command); 84 | shellInput.write((command + " 2>&1\n").getBytes()); 85 | } 86 | 87 | shellInput.write("exit\n".getBytes()); 88 | shellInput.flush(); 89 | 90 | String line; 91 | while ((line = shellOutput.readLine()) != null) { 92 | Log.d(TAG, "command output: " + line); 93 | output.add(line); 94 | } 95 | 96 | process.waitFor(); 97 | } catch (IOException e) { 98 | e.printStackTrace(); 99 | } catch (InterruptedException e) { 100 | e.printStackTrace(); 101 | } 102 | 103 | return output; 104 | } 105 | 106 | public static final String getCommandOutput(String command) throws IOException { 107 | 108 | StringBuilder output = new StringBuilder(); 109 | 110 | Log.d(TAG, "Getting output for command: " + command); 111 | Process p = Runtime.getRuntime().exec(command); 112 | InputStream is = p.getInputStream(); 113 | InputStreamReader r = new InputStreamReader(is); 114 | BufferedReader in = new BufferedReader(r); 115 | 116 | String line; 117 | while ((line = in.readLine()) != null) { 118 | output.append(line); 119 | output.append("\n"); 120 | } 121 | 122 | return output.toString(); 123 | } 124 | 125 | public static final Boolean canGainSu(Context context) { 126 | 127 | String suTestScript = "#!/system/bin/sh\necho "; 128 | String suTestScriptValid = "SuPermsOkay"; 129 | 130 | ArrayList output = run("su", suTestScript + suTestScriptValid); 131 | 132 | if (output.size() == 1 && output.get(0).trim().equals(suTestScriptValid)) { 133 | Log.d(TAG, "Superuser command auth confirmed"); 134 | return true; 135 | 136 | } else { 137 | Log.d(TAG, "Superuser command auth refused"); 138 | return false; 139 | 140 | } 141 | 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /src/org/projectvoodoo/otarootkeeper/ui/StatusRow.java: -------------------------------------------------------------------------------- 1 | 2 | package org.projectvoodoo.otarootkeeper.ui; 3 | 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | import android.view.View.OnClickListener; 10 | import android.widget.Button; 11 | import android.widget.TableRow; 12 | 13 | import org.projectvoodoo.otarootkeeper.R; 14 | import org.projectvoodoo.otarootkeeper.R.id; 15 | 16 | public class StatusRow extends TableRow implements OnClickListener { 17 | 18 | private Context context; 19 | 20 | private View mView; 21 | 22 | public StatusRow(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | 25 | this.context = context; 26 | } 27 | 28 | public void setAvailable(Boolean availability) { 29 | 30 | if (mView != null) 31 | removeView(mView); 32 | 33 | if (availability) 34 | mView = inflate(context, R.layout.status_available, null); 35 | else 36 | mView = inflate(context, R.layout.status_unavailable, null); 37 | 38 | setCustomPadding(); 39 | addView(mView); 40 | } 41 | 42 | public void setAvailable(Boolean availability, String googlePlayUrl) { 43 | 44 | if (mView != null) 45 | removeView(mView); 46 | 47 | if (!availability) { 48 | mView = inflate(context, R.layout.status_unavailable_with_google_play_link, null); 49 | 50 | Button installButton = (Button) mView.findViewById(id.button_install); 51 | installButton.setOnClickListener(this); 52 | installButton.setTag(googlePlayUrl); 53 | setCustomPadding(); 54 | addView(mView); 55 | } 56 | } 57 | 58 | private void setCustomPadding() { 59 | mView.setPadding(8, 0, 0, 0); 60 | } 61 | 62 | @Override 63 | public void onClick(View v) { 64 | 65 | Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse((String) v.getTag())); 66 | context.startActivity(intent); 67 | 68 | } 69 | 70 | } 71 | --------------------------------------------------------------------------------