Add opt-in hash based module.zip verification system.

pull/55/head
Fox2Code 2 years ago
parent 6414bd8c9a
commit 8b086b7925

@ -47,8 +47,9 @@ public enum ActionButtonType {
if (moduleInfo == null) return;
String updateZipUrl = moduleHolder.getUpdateZipUrl();
if (updateZipUrl == null) return;
String updateZipChecksum = moduleHolder.getUpdateZipChecksum();
IntentHelper.openInstaller(button.getContext(), updateZipUrl,
moduleInfo.name, moduleInfo.config);
moduleInfo.name, moduleInfo.config, updateZipChecksum);
}
},
UNINSTALL() {

@ -13,6 +13,7 @@ public class Constants {
public static final String EXTRA_INSTALL_PATH = "extra_install_path";
public static final String EXTRA_INSTALL_NAME = "extra_install_name";
public static final String EXTRA_INSTALL_CONFIG = "extra_install_config";
public static final String EXTRA_INSTALL_CHECKSUM = "extra_install_checksum";
public static final String EXTRA_INSTALL_NO_PATCH = "extra_install_no_patch";
public static final String EXTRA_INSTALL_NO_EXTENSIONS = "extra_install_no_extensions";
public static final String EXTRA_INSTALL_TEST_ROOTLESS = "extra_install_test_rootless";

@ -69,10 +69,18 @@ public final class ModuleHolder implements Comparable<ModuleHolder> {
public String getUpdateZipUrl() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode < this.repoModule.lastUpdated) ?
this.moduleInfo.updateVersionCode <
this.repoModule.moduleInfo.versionCode) ?
this.repoModule.zipUrl : this.moduleInfo.updateZipUrl;
}
public String getUpdateZipChecksum() {
return this.moduleInfo == null || (this.repoModule != null &&
this.moduleInfo.updateVersionCode <
this.repoModule.moduleInfo.versionCode) ?
this.repoModule.checksum : this.moduleInfo.updateChecksum;
}
public String getMainModuleName() {
ModuleInfo moduleInfo = this.getMainModuleInfo();
if (moduleInfo == null || moduleInfo.name == null)

@ -89,7 +89,7 @@ public enum NotificationType implements NotificationTypeCst {
} else {
IntentHelper.openInstaller(compatActivity, d.getAbsolutePath(),
compatActivity.getString(
R.string.local_install_title), null, false,
R.string.local_install_title), null, null, false,
BuildConfig.DEBUG && // Use debug mode if no root
InstallerInitializer.peekMagiskPath() == null);
}
@ -102,7 +102,7 @@ public enum NotificationType implements NotificationTypeCst {
} else if (s == IntentHelper.RESPONSE_URL) {
IntentHelper.openInstaller(compatActivity, u.toString(),
compatActivity.getString(
R.string.remote_install_title), null, false,
R.string.remote_install_title), null, null, false,
BuildConfig.DEBUG && // Use debug mode if no root
InstallerInitializer.peekMagiskPath() == null);
}

@ -79,7 +79,8 @@ public class AndroidacyWebAPI {
moduleUrl + " " + installTitle + " " + checksum);
Uri uri = Uri.parse(moduleUrl);
if (uri.getScheme().equals("https") && uri.getHost().endsWith(".androidacy.com")) {
IntentHelper.openInstaller(this.activity, moduleUrl, installTitle, null);
IntentHelper.openInstaller(this.activity,
moduleUrl, installTitle, null, checksum);
} else {
this.activity.forceBackPressed();
}

@ -22,6 +22,7 @@ import com.fox2code.mmm.R;
import com.fox2code.mmm.compat.CompatActivity;
import com.fox2code.mmm.utils.FastException;
import com.fox2code.mmm.utils.Files;
import com.fox2code.mmm.utils.Hashes;
import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.IntentHelper;
import com.google.android.material.progressindicator.LinearProgressIndicator;
@ -54,6 +55,7 @@ public class InstallerActivity extends CompatActivity {
final Intent intent = this.getIntent();
final String target;
final String name;
final String checksum;
final boolean noPatch;
final boolean noExtensions;
final boolean rootless;
@ -66,6 +68,7 @@ public class InstallerActivity extends CompatActivity {
}
target = intent.getStringExtra(Constants.EXTRA_INSTALL_PATH);
name = intent.getStringExtra(Constants.EXTRA_INSTALL_NAME);
checksum = intent.getStringExtra(Constants.EXTRA_INSTALL_CHECKSUM);
noPatch = intent.getBooleanExtra(Constants.EXTRA_INSTALL_NO_PATCH, false);
noExtensions = intent.getBooleanExtra(// Allow intent to disable extensions
Constants.EXTRA_INSTALL_NO_EXTENSIONS, false);
@ -122,6 +125,15 @@ public class InstallerActivity extends CompatActivity {
this.progressIndicator.setProgressCompat(progress, true);
});
});
if (checksum != null && !checksum.isEmpty()) {
Log.d(TAG, "Checking for checksum: " + checksum);
this.installerTerminal.addLine("- Checking file integrity");
if (!Hashes.checkSumMatch(rawModule, checksum)) {
this.setInstallStateFinished(false,
"! File integrity check failed", "");
return;
}
}
if (noPatch) {
try (OutputStream outputStream = new FileOutputStream(moduleCache)) {
outputStream.write(rawModule);
@ -152,13 +164,31 @@ public class InstallerActivity extends CompatActivity {
}
}, "Module download Thread").start();
} else {
final File moduleFile = new File(target);
if (checksum != null && !checksum.isEmpty()) {
Log.d(TAG, "Checking for checksum: " + checksum);
this.installerTerminal.addLine("- Checking file integrity");
try {
if (!Hashes.checkSumMatch(Files.readSU(moduleFile), checksum)) {
this.setInstallStateFinished(false,
"! File integrity check failed", "");
return;
}
} catch (IOException e) {
Log.e(TAG, "Failed to read file for checksum check", e);
this.setInstallStateFinished(false,
"! File integrity check failed", "");
return;
}
}
this.installerTerminal.addLine("- Installing " + name);
new Thread(() -> this.doInstall(
this.toDelete = new File(target), noExtensions, rootless),
this.toDelete = moduleFile, noExtensions, rootless),
"Install Thread").start();
}
}
private void doInstall(File file,boolean noExtensions,boolean rootless) {
Log.i(TAG, "Installing: " + moduleCache.getName());
InstallerController installerController = new InstallerController(

@ -15,6 +15,7 @@ public class LocalModuleInfo extends ModuleInfo {
public long updateVersionCode = Long.MIN_VALUE;
public String updateZipUrl;
public String updateChangeLog;
public String updateChecksum;
public LocalModuleInfo(String id) {
super(id);
@ -29,6 +30,7 @@ public class LocalModuleInfo extends ModuleInfo {
this.updateVersionCode = jsonUpdate.getLong("versionCode");
this.updateZipUrl = jsonUpdate.getString("zipUrl");
this.updateChangeLog = jsonUpdate.optString("changelog");
this.updateChecksum = jsonUpdate.optString("checksum");
if (this.updateZipUrl.isEmpty()) throw FastException.INSTANCE;
this.updateVersion = PropUtils.shortenVersionName(
this.updateVersion.trim(), this.updateVersionCode);
@ -37,6 +39,7 @@ public class LocalModuleInfo extends ModuleInfo {
this.updateVersionCode = Long.MIN_VALUE;
this.updateZipUrl = null;
this.updateChangeLog = null;
this.updateChecksum = null;
Log.w("LocalModuleInfo",
"Failed update checking for module: " + this.id, e);
}

@ -118,6 +118,7 @@ public class RepoData {
String moduleNotesUrl = module.getString("notes_url");
String modulePropsUrl = module.getString("prop_url");
String moduleZipUrl = module.getString("zip_url");
String moduleChecksum = module.optString("checksum");
if (moduleLastUpdateSpecial != null) { // Fix last update time
Log.d("RepoData", "Data: " + moduleLastUpdate + " -> " +
moduleLastUpdateSpecial + " for " + moduleId);
@ -147,6 +148,7 @@ public class RepoData {
repoModule.notesUrl = moduleNotesUrl;
repoModule.propUrl = modulePropsUrl;
repoModule.zipUrl = moduleZipUrl;
repoModule.checksum = moduleChecksum;
}
// Remove no longer existing modules
Iterator<RepoModule> moduleInfoIterator = this.moduleHashMap.values().iterator();

@ -12,6 +12,7 @@ import com.fox2code.mmm.utils.Http;
import com.fox2code.mmm.utils.PropUtils;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@ -230,7 +231,8 @@ public final class RepoManager {
case MAGISK_ALT_REPO_JSDELIVR:
return "magisk_alt_repo";
default:
return "repo_" + Hashes.hashSha1(url);
return "repo_" + Hashes.hashSha1(
url.getBytes(StandardCharsets.UTF_8));
}
}

@ -10,6 +10,7 @@ public class RepoModule {
public String propUrl;
public String zipUrl;
public String notesUrl;
public String checksum;
boolean processed;
public RepoModule(String id) {

@ -1,10 +1,13 @@
package com.fox2code.mmm.utils;
import java.nio.charset.StandardCharsets;
import android.util.Log;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
public class Hashes {
private static final String TAG = "Hashes";
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
@ -16,13 +19,69 @@ public class Hashes {
return new String(hexChars);
}
public static String hashSha1(String input) {
public static String hashMd5(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String hashSha1(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return bytesToHex(md.digest(input.getBytes(StandardCharsets.UTF_8)));
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String hashSha256(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String hashSha512(byte[] input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
return bytesToHex(md.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
/**
* Check if the checksum match a file by picking the correct
* hashing algorithm depending on the length of the checksum
*/
public static boolean checkSumMatch(byte[] data, String checksum) {
String hash;
switch (checksum.length()) {
case 0:
return true; // No checksum
case 32:
hash = Hashes.hashMd5(data); break;
case 40:
hash = Hashes.hashSha1(data); break;
case 64:
hash = Hashes.hashSha256(data); break;
case 128:
hash = Hashes.hashSha512(data); break;
default:
Log.e(TAG, "No hash algorithm for " +
checksum.length() * 8 + "bit checksums");
return false;
}
Log.d(TAG, "Checksum result (data: " + hash+ ",expected: " + checksum);
return hash.equals(checksum.toLowerCase(Locale.ROOT));
}
}

@ -104,12 +104,13 @@ public class IntentHelper {
}
}
public static void openInstaller(Context context, String url, String title, String config) {
openInstaller(context, url, title, config, false, false);
public static void openInstaller(Context context, String url, String title,
String config, String checksum) {
openInstaller(context, url, title, config, checksum, false, false);
}
public static void openInstaller(Context context, String url, String title, String config,
boolean noPatch,boolean testDebug) {
String checksum, boolean noPatch,boolean testDebug) {
try {
Intent intent = new Intent(context, InstallerActivity.class);
intent.setAction(Constants.INTENT_INSTALL_INTERNAL);
@ -118,6 +119,8 @@ public class IntentHelper {
intent.putExtra(Constants.EXTRA_INSTALL_NAME, title);
if (config != null && !config.isEmpty())
intent.putExtra(Constants.EXTRA_INSTALL_CONFIG, config);
if (checksum != null && !checksum.isEmpty())
intent.putExtra(Constants.EXTRA_INSTALL_CHECKSUM, checksum);
if (noPatch)
intent.putExtra(Constants.EXTRA_INSTALL_NO_PATCH, true);
if (testDebug && BuildConfig.DEBUG)

Loading…
Cancel
Save