diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/AdvancedSettingsFragment.kt b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/AdvancedSettingsFragment.kt index b4c03705..34eac69c 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/AdvancedSettingsFragment.kt +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/AdvancedSettingsFragment.kt @@ -18,6 +18,7 @@ import androidx.preference.Preference import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants +import org.dslul.openboard.inputmethod.latin.utils.ChecksumCalculator import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager @@ -30,6 +31,7 @@ import org.dslul.openboard.inputmethod.latin.utils.JniUtils import org.dslul.openboard.inputmethod.latin.utils.infoDialog import java.io.File import java.io.FileInputStream +import java.io.FileOutputStream import java.io.IOException import java.io.Writer import java.util.zip.ZipEntry @@ -49,7 +51,7 @@ import java.util.zip.ZipOutputStream * - Debug settings */ class AdvancedSettingsFragment : SubScreenFragment() { - private var libfile: File? = null + private val libfile by lazy { File(requireContext().filesDir.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) } private val backupFilePatterns by lazy { listOf( "blacklists/.*\\.txt".toRegex(), "layouts/.*.(txt|json)".toRegex(), @@ -119,10 +121,9 @@ class AdvancedSettingsFragment : SubScreenFragment() { libraryFilePicker.launch(intent) } .setNegativeButton(android.R.string.cancel, null) - libfile = File(requireContext().filesDir.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) - if (libfile?.exists() == true) { + if (libfile.exists()) { builder.setNeutralButton(R.string.load_gesture_library_button_delete) { _, _ -> - libfile?.delete() + libfile.delete() Runtime.getRuntime().exit(0) } } @@ -131,16 +132,44 @@ class AdvancedSettingsFragment : SubScreenFragment() { } private fun copyLibrary(uri: Uri) { - if (libfile == null) return + val tmpfile = File(requireContext().filesDir.absolutePath + File.separator + "tmplib") try { val inputStream = requireContext().contentResolver.openInputStream(uri) - FileUtils.copyStreamToNewFile(inputStream, libfile) - Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded + val outputStream = FileOutputStream(tmpfile) + outputStream.use { + tmpfile.setReadOnly() // as per recommendations in https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading + FileUtils.copyStreamToOtherStream(inputStream, it) + } + + val checksum = ChecksumCalculator.checksum(tmpfile.inputStream()) ?: "" + Log.i("test", "cs $checksum") + if (checksum == JniUtils.expectedDefaultChecksum()) { + renameToLibfileAndRestart(tmpfile, checksum) + } else { + val abi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Build.SUPPORTED_ABIS[0] + } else { + @Suppress("Deprecation") Build.CPU_ABI + } + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.checksum_mismatch_message, abi)) + .setPositiveButton(android.R.string.ok) { _, _ -> renameToLibfileAndRestart(tmpfile, checksum) } + .setNegativeButton(android.R.string.cancel) { _, _ -> tmpfile.delete() } + .show() + } } catch (e: IOException) { + tmpfile.delete() // should inform user, but probably the issues will only come when reading the library } } + private fun renameToLibfileAndRestart(file: File, checksum: String) { + libfile.delete() + sharedPreferences.edit().putString("lib_checksum", checksum).commit() + file.renameTo(libfile) + Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded + } + private fun showBackupRestoreDialog(): Boolean { AlertDialog.Builder(requireContext()) .setTitle(R.string.backup_restore_title) diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MD5Calculator.kt b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ChecksumCalculator.kt similarity index 80% rename from app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MD5Calculator.kt rename to app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ChecksumCalculator.kt index de4c01f6..7d6b180d 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MD5Calculator.kt +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ChecksumCalculator.kt @@ -4,20 +4,20 @@ * SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only */ -package org.dslul.openboard.inputmethod.dictionarypack +package org.dslul.openboard.inputmethod.latin.utils import java.io.IOException import java.io.InputStream import java.security.MessageDigest import java.security.NoSuchAlgorithmException -object MD5Calculator { +object ChecksumCalculator { @Throws(IOException::class) fun checksum(`in`: InputStream): String? { // This code from the Android documentation for MessageDigest. Nearly verbatim. val digester: MessageDigest = try { - MessageDigest.getInstance("MD5") + MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { - return null // Platform does not support MD5 : can't check, so return null + return null // Platform does not support SHA-256 : can't check, so return null } val bytes = ByteArray(8192) var byteCount: Int diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DeviceProtectedUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DeviceProtectedUtils.java index c1e002b1..0f4c4c78 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DeviceProtectedUtils.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/DeviceProtectedUtils.java @@ -10,7 +10,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; import android.preference.PreferenceManager; -import org.dslul.openboard.inputmethod.latin.utils.Log; import androidx.annotation.RequiresApi; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/JniUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/JniUtils.java index f976f8b1..20ee7b42 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/JniUtils.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/JniUtils.java @@ -6,42 +6,71 @@ package org.dslul.openboard.inputmethod.latin.utils; +import android.annotation.SuppressLint; import android.app.Application; +import android.os.Build; +import android.text.TextUtils; import org.dslul.openboard.inputmethod.latin.BuildConfig; import java.io.File; +import java.io.FileInputStream; public final class JniUtils { private static final String TAG = JniUtils.class.getSimpleName(); public static final String JNI_LIB_NAME = "jni_latinime"; public static final String JNI_LIB_NAME_GOOGLE = "jni_latinimegoogle"; public static final String JNI_LIB_IMPORT_FILE_NAME = "libjni_latinime.so"; + private static final String CHECKSUM_ARM64 = "b1049983e6ac5cfc6d1c66e38959751044fad213dff0637a6cf1d2a2703e754f"; + private static final String CHECKSUM_ARM32 = "442a2a8bfcb25489564bc9433a916fa4dc0dba9000fe6f6f03f5939b985091e6"; + private static final String CHECKSUM_X86_64 = "c882e12e6d48dd946e0b644c66868a720bd11ac3fecf152000e21a3d5abd59c9"; + private static final String CHECKSUM_X86 = "bd946d126c957b5a6dea3bafa07fa36a27950b30e2b684dffc60746d0a1c7ad8"; + public static String expectedDefaultChecksum() { + final String abi = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? Build.SUPPORTED_ABIS[0] : Build.CPU_ABI; + return switch (abi) { + case "arm64-v8a" -> CHECKSUM_ARM64; + case "armeabi-v7a" -> CHECKSUM_ARM32; + case "x86_64" -> CHECKSUM_X86_64; + case "x86" -> CHECKSUM_X86; + default -> "-"; // invalid checksum that definitely will not match + }; + } public static boolean sHaveGestureLib = false; static { - String filesDir; + // hardcoded default path, may not work on all phones + @SuppressLint("SdCardPath") String filesDir = "/data/data/" + BuildConfig.APPLICATION_ID + "/files"; + Application app = null; try { // try using reflection to get (app)context: https://stackoverflow.com/a/38967293 - final Application app = (Application) Class.forName("android.app.ActivityThread") + app = (Application) Class.forName("android.app.ActivityThread") .getMethod("currentApplication").invoke(null, (Object[]) null); + // and use the actual path if possible filesDir = app.getFilesDir().getAbsolutePath(); - } catch (Exception e) { - // fall back to hardcoded default path, may not work on all phones - filesDir = "/data/data/" + BuildConfig.APPLICATION_ID + "/files"; - } + } catch (Exception ignored) { } final File userSuppliedLibrary = new File(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME); if (userSuppliedLibrary.exists()) { + final String wantedChecksum = app == null ? expectedDefaultChecksum() : DeviceProtectedUtils.getSharedPreferences(app).getString("lib_checksum", ""); try { - System.load(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME); - sHaveGestureLib = true; // this is an assumption, any way to actually check? + final String checksum = ChecksumCalculator.INSTANCE.checksum(new FileInputStream(userSuppliedLibrary)); + if (TextUtils.equals(wantedChecksum, checksum)) { + // try loading the library + System.load(userSuppliedLibrary.getAbsolutePath()); + sHaveGestureLib = true; // this is an assumption, any way to actually check? + } else { + // delete if checksum doesn't match + // this actually is bad if we can't get the application and the user has a different library than expected + // todo: this is disabled until app is renamed, otherwise it will delete everyone's library! + // though there could be a default check? +// userSuppliedLibrary.delete(); + } } catch (Throwable t) { // catch everything, maybe provided library simply doesn't work Log.w(TAG, "Could not load user-supplied library", t); } } if (!sHaveGestureLib) { - // try loading google library, will fail unless it's in system and this is a system app + // try loading google library, will fail unless the library is in system and this is a system app try { System.loadLibrary(JNI_LIB_NAME_GOOGLE); sHaveGestureLib = true; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4d41df45..b2eae471 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -177,6 +177,8 @@ You will need the library for \'%s\'. Incompatible libraries may crash when using gesture typing. \n\nWarning: loading external code can be a security risk. Only use a library from a source you trust. + + Unknown library file. Are you sure you got it from a trusted source and it is for \'%s\'? Load library