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