From 7a6aa28277af7a5fe1b602a11b04fe8970ef35b2 Mon Sep 17 00:00:00 2001 From: Fox2Code Date: Tue, 22 Feb 2022 01:45:56 +0100 Subject: [PATCH] Androidacy WebView hardening, and some bug fixes. --- .../com/fox2code/mmm/ActionButtonType.java | 3 +- .../mmm/androidacy/AndroidacyActivity.java | 21 +++--- .../mmm/androidacy/AndroidacyUtil.java | 24 +++++++ .../mmm/androidacy/AndroidacyWebAPI.java | 67 ++++++++++++++----- .../fox2code/mmm/compat/CompatActivity.java | 17 +++++ .../mmm/installer/InstallerActivity.java | 6 +- .../mmm/installer/InstallerInitializer.java | 10 +-- .../java/com/fox2code/mmm/utils/Hashes.java | 21 ++++++ 8 files changed, 137 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java diff --git a/app/src/main/java/com/fox2code/mmm/ActionButtonType.java b/app/src/main/java/com/fox2code/mmm/ActionButtonType.java index 91e31de..3a3ae4a 100644 --- a/app/src/main/java/com/fox2code/mmm/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/ActionButtonType.java @@ -9,6 +9,7 @@ import android.widget.Toast; import androidx.annotation.DrawableRes; import androidx.appcompat.app.AlertDialog; +import com.fox2code.mmm.androidacy.AndroidacyUtil; import com.fox2code.mmm.compat.CompatActivity; import com.fox2code.mmm.compat.CompatDisplay; import com.fox2code.mmm.installer.InstallerInitializer; @@ -160,7 +161,7 @@ public enum ActionButtonType { public void doAction(ImageButton button, ModuleHolder moduleHolder) { String config = moduleHolder.getMainModuleConfig(); if (config == null) return; - if (config.startsWith("https://www.androidacy.com/")) { + if (AndroidacyUtil.isAndroidacyLink(config)) { IntentHelper.openUrlAndroidacy(button.getContext(), config, true); } else { IntentHelper.openConfig(button.getContext(), config); diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java index 8e39a7e..26830f8 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.util.Log; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; @@ -33,6 +34,7 @@ import com.fox2code.mmm.utils.IntentHelper; * Per Androidacy repo implementation agreement, no request of this WebView shall be modified. */ public class AndroidacyActivity extends CompatActivity { + private static final String TAG = "AndroidacyActivity"; private static final String REFERRER = "utm_source=FoxMMM&utm_medium=app"; static { @@ -42,6 +44,7 @@ public class AndroidacyActivity extends CompatActivity { } WebView webView; + AndroidacyWebAPI androidacyWebAPI; boolean backOnResume; @Override @@ -51,16 +54,14 @@ public class AndroidacyActivity extends CompatActivity { Intent intent = this.getIntent(); Uri uri; if (!MainApplication.checkSecret(intent) || - (uri = intent.getData()) == null || - !uri.getHost().endsWith(".androidacy.com")) { + (uri = intent.getData()) == null) { + Log.w(TAG, "Impersonation detected"); this.forceBackPressed(); return; } String url = uri.toString(); - int i; - if (!url.startsWith("https://") || // Checking twice - (i = url.indexOf("/", 8)) == -1 || - !url.substring(8, i).endsWith(".androidacy.com")) { + if (!AndroidacyUtil.isAndroidacyLink(url, uri)) { + Log.w(TAG, "Calling non androidacy link in secure WebView: " + url); this.forceBackPressed(); return; } @@ -112,9 +113,9 @@ public class AndroidacyActivity extends CompatActivity { @Override public boolean shouldOverrideUrlLoading( @NonNull WebView view, @NonNull WebResourceRequest request) { - // Don't open non andoridacy urls inside WebView + // Don't open non Androidacy urls inside WebView if (request.isForMainFrame() && !(request.getUrl().getScheme().equals("intent") || - request.getUrl().getHost().endsWith(".androidacy.com"))) { + AndroidacyUtil.isAndroidacyLink(request.getUrl()))) { IntentHelper.openUrl(view.getContext(), request.getUrl().toString()); return true; } @@ -159,7 +160,7 @@ public class AndroidacyActivity extends CompatActivity { return true; } }); - this.webView.addJavascriptInterface( + this.webView.addJavascriptInterface(androidacyWebAPI = new AndroidacyWebAPI(this, allowInstall), "mmm"); this.webView.loadUrl(url); } @@ -180,6 +181,8 @@ public class AndroidacyActivity extends CompatActivity { if (this.backOnResume) { this.backOnResume = false; this.forceBackPressed(); + } else if (this.androidacyWebAPI != null) { + this.androidacyWebAPI.consumedAction = false; } } } diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java new file mode 100644 index 0000000..7dc72ce --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java @@ -0,0 +1,24 @@ +package com.fox2code.mmm.androidacy; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class AndroidacyUtil { + public static boolean isAndroidacyLink(@Nullable Uri uri) { + return uri != null && isAndroidacyLink(uri.toString(), uri); + } + + public static boolean isAndroidacyLink(@Nullable String url) { + return url != null && isAndroidacyLink(url, Uri.parse(url)); + } + + static boolean isAndroidacyLink(@NonNull String url,@NonNull Uri uri) { + int i; // Check both string and Uri to mitigate parse exploit + return url.startsWith("https://") && + (i = url.indexOf("/", 8)) != -1 && + url.substring(8, i).endsWith(".androidacy.com") && + uri.getHost().endsWith(".androidacy.com"); + } +} diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java index ebdfd10..50144d1 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java @@ -13,6 +13,7 @@ import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.utils.Files; +import com.fox2code.mmm.utils.Hashes; import com.fox2code.mmm.utils.IntentHelper; import java.io.File; @@ -23,21 +24,30 @@ public class AndroidacyWebAPI { private static final String TAG = "AndroidacyWebAPI"; private final AndroidacyActivity activity; private final boolean allowInstall; + boolean consumedAction; public AndroidacyWebAPI(AndroidacyActivity activity, boolean allowInstall) { this.activity = activity; this.allowInstall = allowInstall; } + public void forceQuitRaw(String error) { + Toast.makeText(this.activity, error, Toast.LENGTH_LONG).show(); + this.activity.runOnUiThread(this.activity::forceBackPressed); + this.activity.backOnResume = true; // Set backOnResume just in case + } + @JavascriptInterface public void forceQuit(String error) { - Toast.makeText(this.activity, error, Toast.LENGTH_LONG).show(); - this.activity.runOnUiThread( - this.activity::forceBackPressed); + if (this.consumedAction) return; + this.consumedAction = true; + this.forceQuitRaw(error); } @JavascriptInterface public void cancel() { + if (this.consumedAction) return; + this.consumedAction = true; this.activity.runOnUiThread( this.activity::forceBackPressed); } @@ -47,6 +57,8 @@ public class AndroidacyWebAPI { */ @JavascriptInterface public void openUrl(String url) { + if (this.consumedAction) return; + this.consumedAction = true; Log.d(TAG, "Received openUrl request: " + url); if (Uri.parse(url).getScheme().equals("https")) { IntentHelper.openUrl(this.activity, url); @@ -82,19 +94,25 @@ public class AndroidacyWebAPI { */ @JavascriptInterface public void install(String moduleUrl, String installTitle,String checksum) { - if (!this.canInstall()) { + if (this.consumedAction || !this.canInstall()) { return; } + this.consumedAction = true; Log.d(TAG, "Received install request: " + moduleUrl + " " + installTitle + " " + checksum); Uri uri = Uri.parse(moduleUrl); - if (uri.getScheme().equals("https") && uri.getHost().endsWith(".androidacy.com")) { - this.activity.backOnResume = true; - IntentHelper.openInstaller(this.activity, - moduleUrl, installTitle, null, checksum); - } else { - this.activity.forceBackPressed(); + if (!AndroidacyUtil.isAndroidacyLink(moduleUrl, uri)) { + this.forceQuitRaw("Non Androidacy module link used on Androidacy"); + return; } + if (checksum != null) checksum = checksum.trim(); + if (!Hashes.checkSumValid(checksum)) { + this.forceQuitRaw("Androidacy didn't provided a valid checksum"); + return; + } + this.activity.backOnResume = true; + IntentHelper.openInstaller(this.activity, + moduleUrl, installTitle, null, checksum); } /** @@ -137,7 +155,25 @@ public class AndroidacyWebAPI { */ @JavascriptInterface public void hideActionBar() { - this.activity.hideActionBar(); + if (this.consumedAction) return; + this.consumedAction = true; + this.activity.runOnUiThread(() -> { + this.activity.hideActionBar(); + this.consumedAction = false; + }); + } + + /** + * Show action bar if not visible, the action bar is only visible by default on notes. + */ + @JavascriptInterface + public void showActionBar() { + if (this.consumedAction) return; + this.consumedAction = true; + this.activity.runOnUiThread(() -> { + this.activity.showActionBar(); + this.consumedAction = false; + }); } /** @@ -147,8 +183,7 @@ public class AndroidacyWebAPI { public boolean isAndroidacyModule(String moduleId) { LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId); return localModuleInfo != null && ("Androidacy".equals(localModuleInfo.author) || - (localModuleInfo.config != null && - localModuleInfo.config.startsWith("https://www.androidacy.com/"))); + AndroidacyUtil.isAndroidacyLink(localModuleInfo.config)); } /** @@ -157,7 +192,8 @@ public class AndroidacyWebAPI { */ @JavascriptInterface public String getAndroidacyModuleFile(String moduleId, String moduleFile) { - if (moduleFile == null || !this.isAndroidacyModule(moduleId)) return ""; + if (moduleFile == null || this.consumedAction || + !this.isAndroidacyModule(moduleId)) return ""; File moduleFolder = new File("/data/adb/modules/" + moduleId); File absModuleFile = new File(moduleFolder, moduleFile).getAbsoluteFile(); if (!absModuleFile.getPath().startsWith(moduleFolder.getPath())) return ""; @@ -175,7 +211,8 @@ public class AndroidacyWebAPI { */ @JavascriptInterface public boolean setAndroidacyModuleMeta(String moduleId, String content) { - if (content == null || !this.isAndroidacyModule(moduleId)) return false; + if (content == null || this.consumedAction || + !this.isAndroidacyModule(moduleId)) return false; File androidacyMetaFile = new File( "/data/adb/modules/" + moduleId + "/.androidacy"); try { diff --git a/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java b/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java index 221664b..2f13c25 100644 --- a/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java +++ b/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java @@ -164,6 +164,23 @@ public class CompatActivity extends AppCompatActivity { } } + public void showActionBar() { + androidx.appcompat.app.ActionBar compatActionBar; + try { + compatActionBar = this.getSupportActionBar(); + } catch (Exception e) { + Log.e(TAG, "Failed to call getSupportActionBar", e); + compatActionBar = null; // Allow fallback to builtin actionBar. + } + if (compatActionBar != null) { + compatActionBar.show(); + } else { + android.app.ActionBar actionBar = this.getActionBar(); + if (actionBar != null) + actionBar.show(); + } + } + @Dimension @Px public int getActionBarHeight() { androidx.appcompat.app.ActionBar compatActionBar; diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java index f5487fb..149b301 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -132,6 +132,10 @@ public class InstallerActivity extends CompatActivity { this.progressIndicator.setProgressCompat(progress, true); }); }); + this.runOnUiThread(() -> { + this.progressIndicator.setVisibility(View.GONE); + this.progressIndicator.setIndeterminate(true); + }); if (this.canceled) return; if (checksum != null && !checksum.isEmpty()) { Log.d(TAG, "Checking for checksum: " + checksum); @@ -178,8 +182,6 @@ public class InstallerActivity extends CompatActivity { errMessage = "Failed to patch module zip"; this.runOnUiThread(() -> { this.installerTerminal.addLine("- Patching " + name); - this.progressIndicator.setVisibility(View.GONE); - this.progressIndicator.setIndeterminate(true); }); Log.i(TAG, "Patching: " + moduleCache.getName()); try (OutputStream outputStream = new FileOutputStream(moduleCache)) { diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java index 2cb58f1..c983549 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerInitializer.java @@ -42,14 +42,14 @@ public class InstallerInitializer extends Shell.Initializer { } public static void tryGetMagiskPathAsync(Callback callback,boolean forceCheck) { - String MAGISK_PATH = InstallerInitializer.MAGISK_PATH; - if (MAGISK_PATH != null && !forceCheck) { - callback.onPathReceived(MAGISK_PATH); - return; - } + final String MAGISK_PATH = InstallerInitializer.MAGISK_PATH; Thread thread = new Thread("Magisk GetPath Thread") { @Override public void run() { + if (MAGISK_PATH != null && !forceCheck) { + callback.onPathReceived(MAGISK_PATH); + return; + } int error; String MAGISK_PATH = null; try { diff --git a/app/src/main/java/com/fox2code/mmm/utils/Hashes.java b/app/src/main/java/com/fox2code/mmm/utils/Hashes.java index 129141c..366027a 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Hashes.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Hashes.java @@ -84,4 +84,25 @@ public class Hashes { Log.d(TAG, "Checksum result (data: " + hash+ ",expected: " + checksum + ")"); return hash.equals(checksum.toLowerCase(Locale.ROOT)); } + + public static boolean checkSumValid(String checksum) { + if (checksum == null) return false; + switch (checksum.length()) { + case 0: + default: + return false; + case 32: + case 40: + case 64: + case 128: + final int len = checksum.length(); + for (int i = 0; i < len; i++) { + char c = checksum.charAt(i); + if (c < '0' || c > 'f') return false; + if (c > '9' && // Easier working with bits + (c & 0b01011111) < 'A') return false; + } + return true; + } + } }