diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index ae48e7309..2fff01c8c 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
Toast.makeText(weakSelf.get(), msg, duration).show());
}
diff --git a/src/main/java/com/topjohnwu/magisk/SettingsActivity.java b/src/main/java/com/topjohnwu/magisk/SettingsActivity.java
index 2c74eaba3..f714a9522 100644
--- a/src/main/java/com/topjohnwu/magisk/SettingsActivity.java
+++ b/src/main/java/com/topjohnwu/magisk/SettingsActivity.java
@@ -22,6 +22,7 @@ import com.topjohnwu.magisk.asyncs.CheckUpdates;
import com.topjohnwu.magisk.asyncs.HideManager;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.utils.Const;
+import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
@@ -112,13 +113,14 @@ public class SettingsActivity extends Activity implements Topic.Subscriber {
multiuserMode = (ListPreference) findPreference(Const.Key.SU_MULTIUSER_MODE);
namespaceMode = (ListPreference) findPreference(Const.Key.SU_MNT_NS);
SwitchPreference reauth = (SwitchPreference) findPreference(Const.Key.SU_REAUTH);
+ SwitchPreference fingerprint = (SwitchPreference) findPreference(Const.Key.SU_FINGERPRINT);
updateChannel.setOnPreferenceChangeListener((pref, o) -> {
mm.updateChannel = Integer.parseInt((String) o);
if (mm.updateChannel == Const.Value.CUSTOM_CHANNEL) {
View v = LayoutInflater.from(getActivity()).inflate(R.layout.custom_channel_dialog, null);
EditText url = v.findViewById(R.id.custom_url);
- url.setText(mm.customChannelUrl);
+ url.setText(mm.prefs.getString(Const.Key.CUSTOM_CHANNEL, ""));
new AlertDialog.Builder(getActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
@@ -144,6 +146,11 @@ public class SettingsActivity extends Activity implements Topic.Subscriber {
reauth.setSummary(R.string.android_o_not_support);
}
+ // Remove fingerprint option if not possible
+ if (!FingerprintHelper.canUseFingerprint()) {
+ suCategory.removePreference(fingerprint);
+ }
+
if (mm.getPackageName().equals(Const.ORIG_PKG_NAME) && mm.magiskVersionCode >= 1440) {
hideManager.setOnPreferenceClickListener((pref) -> {
Utils.runWithPermission(getActivity(),
diff --git a/src/main/java/com/topjohnwu/magisk/asyncs/CheckUpdates.java b/src/main/java/com/topjohnwu/magisk/asyncs/CheckUpdates.java
index 15419d90c..575f26aa4 100644
--- a/src/main/java/com/topjohnwu/magisk/asyncs/CheckUpdates.java
+++ b/src/main/java/com/topjohnwu/magisk/asyncs/CheckUpdates.java
@@ -33,7 +33,7 @@ public class CheckUpdates extends ParallelTask {
jsonStr = WebService.getString(Const.Url.BETA_URL);
break;
case Const.Value.CUSTOM_CHANNEL:
- jsonStr = WebService.getString(mm.customChannelUrl);
+ jsonStr = WebService.getString(mm.prefs.getString(Const.Key.CUSTOM_CHANNEL, ""));
break;
}
try {
@@ -54,7 +54,7 @@ public class CheckUpdates extends ParallelTask {
@Override
protected void onPostExecute(Void v) {
MagiskManager mm = MagiskManager.get();
- if (showNotification && mm.updateNotification) {
+ if (showNotification && mm.prefs.getBoolean(Const.Key.UPDATE_NOTIFICATION, true)) {
if (BuildConfig.VERSION_CODE < mm.remoteManagerVersionCode) {
ShowUI.managerUpdateNotification();
} else if (mm.magiskVersionCode < mm.remoteMagiskVersionCode) {
diff --git a/src/main/java/com/topjohnwu/magisk/receivers/PackageReceiver.java b/src/main/java/com/topjohnwu/magisk/receivers/PackageReceiver.java
index 40e6cb5d4..52bc9f932 100644
--- a/src/main/java/com/topjohnwu/magisk/receivers/PackageReceiver.java
+++ b/src/main/java/com/topjohnwu/magisk/receivers/PackageReceiver.java
@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent;
import com.topjohnwu.magisk.MagiskManager;
+import com.topjohnwu.magisk.utils.Const;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Utils;
@@ -18,7 +19,7 @@ public class PackageReceiver extends BroadcastReceiver {
switch (intent.getAction()) {
case Intent.ACTION_PACKAGE_REPLACED:
// This will only work pre-O
- if (mm.suReauth) {
+ if (mm.prefs.getBoolean(Const.Key.SU_REAUTH, false)) {
mm.suDB.deletePolicy(pkg);
}
break;
diff --git a/src/main/java/com/topjohnwu/magisk/superuser/RequestActivity.java b/src/main/java/com/topjohnwu/magisk/superuser/RequestActivity.java
index f96b76460..31e2990c6 100644
--- a/src/main/java/com/topjohnwu/magisk/superuser/RequestActivity.java
+++ b/src/main/java/com/topjohnwu/magisk/superuser/RequestActivity.java
@@ -3,12 +3,14 @@ package com.topjohnwu.magisk.superuser;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.hardware.fingerprint.FingerprintManager;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.FileObserver;
import android.text.TextUtils;
+import android.view.View;
import android.view.Window;
import android.widget.ArrayAdapter;
import android.widget.Button;
@@ -23,6 +25,7 @@ import com.topjohnwu.magisk.asyncs.ParallelTask;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.utils.Const;
+import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.utils.Utils;
import java.io.DataInputStream;
@@ -40,6 +43,8 @@ public class RequestActivity extends Activity {
@BindView(R.id.package_name) TextView packageNameView;
@BindView(R.id.grant_btn) Button grant_btn;
@BindView(R.id.deny_btn) Button deny_btn;
+ @BindView(R.id.fingerprint) ImageView fingerprintImg;
+ @BindView(R.id.warning) TextView warning;
private String socketPath;
private LocalSocket socket;
@@ -49,6 +54,7 @@ public class RequestActivity extends Activity {
private boolean hasTimeout;
private Policy policy;
private CountDownTimer timer;
+ private FingerprintHelper fingerprintHelper;
@Override
public int getDarkTheme() {
@@ -80,6 +86,15 @@ public class RequestActivity extends Activity {
new SocketManager(this).exec();
}
+ @Override
+ public void finish() {
+ if (timer != null)
+ timer.cancel();
+ if (fingerprintHelper != null)
+ fingerprintHelper.cancel();
+ super.finish();
+ }
+
private boolean cancelTimeout() {
timer.cancel();
deny_btn.setText(getString(R.string.deny));
@@ -128,11 +143,49 @@ public class RequestActivity extends Activity {
}
};
- grant_btn.setOnClickListener(v -> {
- handleAction(Policy.ALLOW);
- timer.cancel();
- });
- grant_btn.requestFocus();
+ boolean useFingerprint = mm.prefs.getBoolean(Const.Key.SU_FINGERPRINT, false) && FingerprintHelper.canUseFingerprint();
+
+ if (useFingerprint) {
+ try {
+ fingerprintHelper = new FingerprintHelper() {
+ @Override
+ public void onAuthenticationError(int errorCode, CharSequence errString) {
+ warning.setText(errString);
+ }
+
+ @Override
+ public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
+ warning.setText(helpString);
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
+ handleAction(Policy.ALLOW);
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ warning.setText(R.string.auth_fail);
+ }
+ };
+ fingerprintHelper.startAuth();
+ } catch (Exception e) {
+ e.printStackTrace();
+ useFingerprint = false;
+ }
+ }
+
+ if (!useFingerprint) {
+ grant_btn.setOnClickListener(v -> {
+ handleAction(Policy.ALLOW);
+ timer.cancel();
+ });
+ grant_btn.requestFocus();
+ }
+
+ grant_btn.setVisibility(useFingerprint ? View.GONE : View.VISIBLE);
+ fingerprintImg.setVisibility(useFingerprint ? View.VISIBLE : View.GONE);
+
deny_btn.setOnClickListener(v -> {
handleAction(Policy.DENY);
timer.cancel();
@@ -151,8 +204,9 @@ public class RequestActivity extends Activity {
public void onBackPressed() {
if (policy != null) {
handleAction(Policy.DENY);
+ } else {
+ finish();
}
- finish();
}
void handleAction() {
diff --git a/src/main/java/com/topjohnwu/magisk/utils/Const.java b/src/main/java/com/topjohnwu/magisk/utils/Const.java
index cfc52a8cd..0290fde98 100644
--- a/src/main/java/com/topjohnwu/magisk/utils/Const.java
+++ b/src/main/java/com/topjohnwu/magisk/utils/Const.java
@@ -23,6 +23,8 @@ public class Const {
public static final String UTIL_FUNCTIONS= "util_functions.sh";
public static final String ANDROID_MANIFEST = "AndroidManifest.xml";
+ public static final String SU_KEYSTORE_KEY = "su_key";
+
// Paths
public static final String MAGISK_DISABLE_FILE = "/cache/.disable_magisk";
public static final String TMP_FOLDER_PATH = "/dev/tmp";
@@ -109,6 +111,7 @@ public class Const {
public static final String SU_AUTO_RESPONSE = "su_auto_response";
public static final String SU_NOTIFICATION = "su_notification";
public static final String SU_REAUTH = "su_reauth";
+ public static final String SU_FINGERPRINT = "su_fingerprint";
// intents
public static final String OPEN_SECTION = "section";
diff --git a/src/main/java/com/topjohnwu/magisk/utils/FingerprintHelper.java b/src/main/java/com/topjohnwu/magisk/utils/FingerprintHelper.java
new file mode 100644
index 000000000..e556489af
--- /dev/null
+++ b/src/main/java/com/topjohnwu/magisk/utils/FingerprintHelper.java
@@ -0,0 +1,84 @@
+package com.topjohnwu.magisk.utils;
+
+import android.annotation.TargetApi;
+import android.app.KeyguardManager;
+import android.hardware.fingerprint.FingerprintManager;
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+
+import com.topjohnwu.magisk.MagiskManager;
+
+import java.security.KeyStore;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+@TargetApi(Build.VERSION_CODES.M)
+public abstract class FingerprintHelper extends FingerprintManager.AuthenticationCallback {
+
+ private FingerprintManager manager;
+ private Cipher cipher;
+ private CancellationSignal cancel;
+
+ public static boolean canUseFingerprint() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
+ return false;
+ MagiskManager mm = MagiskManager.get();
+ KeyguardManager km = mm.getSystemService(KeyguardManager.class);
+ FingerprintManager fm = mm.getSystemService(FingerprintManager.class);
+ return km.isKeyguardSecure() && fm.isHardwareDetected() && fm.hasEnrolledFingerprints();
+ }
+
+ protected FingerprintHelper() throws Exception {
+ MagiskManager mm = MagiskManager.get();
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ manager = mm.getSystemService(FingerprintManager.class);
+ cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_CBC + "/"
+ + KeyProperties.ENCRYPTION_PADDING_PKCS7);
+ keyStore.load(null);
+ SecretKey key = (SecretKey) keyStore.getKey(Const.SU_KEYSTORE_KEY, null);
+ if (key == null) {
+ key = generateKey();
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ } catch (KeyPermanentlyInvalidatedException e) {
+ // Only happens on Marshmallow
+ key = generateKey();
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ }
+ }
+
+ public void startAuth() {
+ cancel = new CancellationSignal();
+ FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
+ manager.authenticate(cryptoObject, cancel, 0, this, null);
+ }
+
+ public void cancel() {
+ if (cancel != null)
+ cancel.cancel();
+ }
+
+ private SecretKey generateKey() throws Exception {
+ KeyGenerator keygen = KeyGenerator
+ .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
+ KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
+ Const.SU_KEYSTORE_KEY,
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+ .setUserAuthenticationRequired(true)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ builder.setInvalidatedByBiometricEnrollment(false);
+ }
+ keygen.init(builder.build());
+ return keygen.generateKey();
+ }
+}
diff --git a/src/main/res/drawable/ic_fingerprint.xml b/src/main/res/drawable/ic_fingerprint.xml
new file mode 100644
index 000000000..f650f7445
--- /dev/null
+++ b/src/main/res/drawable/ic_fingerprint.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/main/res/layout/activity_request.xml b/src/main/res/layout/activity_request.xml
index e891f2721..9a37d32e6 100644
--- a/src/main/res/layout/activity_request.xml
+++ b/src/main/res/layout/activity_request.xml
@@ -101,18 +101,27 @@
+ style="?android:buttonBarButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/grant" />
+
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index a1250552d..fc369bf99 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -166,6 +166,8 @@
%1$s seconds
Re-authenticate after upgrade
Re-authenticate superuser permissions after an application upgrades
+ Enable Fingerprint Authentication
+ Use fingerprint scanner to allow superuser requests
Multiuser Mode
Device Owner Only
@@ -212,6 +214,7 @@
Confirm to revoke %1$s rights?
Toast
None
+ Authentication Failed
PID:\u0020
diff --git a/src/main/res/xml/app_settings.xml b/src/main/res/xml/app_settings.xml
index 25652ec7b..9995f235f 100644
--- a/src/main/res/xml/app_settings.xml
+++ b/src/main/res/xml/app_settings.xml
@@ -107,6 +107,11 @@
android:entries="@array/su_notification"
android:entryValues="@array/value_array" />
+
+