├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------