basic checks for supplied gesture library

This commit is contained in:
Helium314 2024-01-01 20:35:12 +01:00
parent 7c5b96a034
commit d6769f6d65
5 changed files with 80 additions and 21 deletions

View file

@ -18,6 +18,7 @@ import androidx.preference.Preference
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.dslul.openboard.inputmethod.dictionarypack.DictionaryPackConstants 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.KeyboardLayoutSet
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager 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 org.dslul.openboard.inputmethod.latin.utils.infoDialog
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.Writer import java.io.Writer
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -49,7 +51,7 @@ import java.util.zip.ZipOutputStream
* - Debug settings * - Debug settings
*/ */
class AdvancedSettingsFragment : SubScreenFragment() { 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( private val backupFilePatterns by lazy { listOf(
"blacklists/.*\\.txt".toRegex(), "blacklists/.*\\.txt".toRegex(),
"layouts/.*.(txt|json)".toRegex(), "layouts/.*.(txt|json)".toRegex(),
@ -119,10 +121,9 @@ class AdvancedSettingsFragment : SubScreenFragment() {
libraryFilePicker.launch(intent) libraryFilePicker.launch(intent)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
libfile = File(requireContext().filesDir.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) if (libfile.exists()) {
if (libfile?.exists() == true) {
builder.setNeutralButton(R.string.load_gesture_library_button_delete) { _, _ -> builder.setNeutralButton(R.string.load_gesture_library_button_delete) { _, _ ->
libfile?.delete() libfile.delete()
Runtime.getRuntime().exit(0) Runtime.getRuntime().exit(0)
} }
} }
@ -131,16 +132,44 @@ class AdvancedSettingsFragment : SubScreenFragment() {
} }
private fun copyLibrary(uri: Uri) { private fun copyLibrary(uri: Uri) {
if (libfile == null) return val tmpfile = File(requireContext().filesDir.absolutePath + File.separator + "tmplib")
try { try {
val inputStream = requireContext().contentResolver.openInputStream(uri) val inputStream = requireContext().contentResolver.openInputStream(uri)
FileUtils.copyStreamToNewFile(inputStream, libfile) val outputStream = FileOutputStream(tmpfile)
Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded 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) { } catch (e: IOException) {
tmpfile.delete()
// should inform user, but probably the issues will only come when reading the library // 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 { private fun showBackupRestoreDialog(): Boolean {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.backup_restore_title) .setTitle(R.string.backup_restore_title)

View file

@ -4,20 +4,20 @@
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only * 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.IOException
import java.io.InputStream import java.io.InputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
object MD5Calculator { object ChecksumCalculator {
@Throws(IOException::class) @Throws(IOException::class)
fun checksum(`in`: InputStream): String? { // This code from the Android documentation for MessageDigest. Nearly verbatim. fun checksum(`in`: InputStream): String? { // This code from the Android documentation for MessageDigest. Nearly verbatim.
val digester: MessageDigest = try { val digester: MessageDigest = try {
MessageDigest.getInstance("MD5") MessageDigest.getInstance("SHA-256")
} catch (e: NoSuchAlgorithmException) { } 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) val bytes = ByteArray(8192)
var byteCount: Int var byteCount: Int

View file

@ -10,7 +10,6 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import org.dslul.openboard.inputmethod.latin.utils.Log;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;

View file

@ -6,42 +6,71 @@
package org.dslul.openboard.inputmethod.latin.utils; package org.dslul.openboard.inputmethod.latin.utils;
import android.annotation.SuppressLint;
import android.app.Application; import android.app.Application;
import android.os.Build;
import android.text.TextUtils;
import org.dslul.openboard.inputmethod.latin.BuildConfig; import org.dslul.openboard.inputmethod.latin.BuildConfig;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
public final class JniUtils { public final class JniUtils {
private static final String TAG = JniUtils.class.getSimpleName(); private static final String TAG = JniUtils.class.getSimpleName();
public static final String JNI_LIB_NAME = "jni_latinime"; 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_NAME_GOOGLE = "jni_latinimegoogle";
public static final String JNI_LIB_IMPORT_FILE_NAME = "libjni_latinime.so"; 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; public static boolean sHaveGestureLib = false;
static { 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 {
// try using reflection to get (app)context: https://stackoverflow.com/a/38967293 // 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); .getMethod("currentApplication").invoke(null, (Object[]) null);
// and use the actual path if possible
filesDir = app.getFilesDir().getAbsolutePath(); filesDir = app.getFilesDir().getAbsolutePath();
} catch (Exception e) { } catch (Exception ignored) { }
// fall back to hardcoded default path, may not work on all phones
filesDir = "/data/data/" + BuildConfig.APPLICATION_ID + "/files";
}
final File userSuppliedLibrary = new File(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME); final File userSuppliedLibrary = new File(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME);
if (userSuppliedLibrary.exists()) { if (userSuppliedLibrary.exists()) {
final String wantedChecksum = app == null ? expectedDefaultChecksum() : DeviceProtectedUtils.getSharedPreferences(app).getString("lib_checksum", "");
try { try {
System.load(filesDir + File.separator + JNI_LIB_IMPORT_FILE_NAME); final String checksum = ChecksumCalculator.INSTANCE.checksum(new FileInputStream(userSuppliedLibrary));
sHaveGestureLib = true; // this is an assumption, any way to actually check? 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 } catch (Throwable t) { // catch everything, maybe provided library simply doesn't work
Log.w(TAG, "Could not load user-supplied library", t); Log.w(TAG, "Could not load user-supplied library", t);
} }
} }
if (!sHaveGestureLib) { 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 { try {
System.loadLibrary(JNI_LIB_NAME_GOOGLE); System.loadLibrary(JNI_LIB_NAME_GOOGLE);
sHaveGestureLib = true; sHaveGestureLib = true;

View file

@ -177,6 +177,8 @@
<!-- Message in load gesture library dialog --> <!-- Message in load gesture library dialog -->
<string name="load_gesture_library_message">You will need the library for \'%s\'. Incompatible libraries may crash when using gesture typing. <string name="load_gesture_library_message">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.</string> \n\nWarning: loading external code can be a security risk. Only use a library from a source you trust.</string>
<!-- Message when the library checksum doesn't match -->
<string name="checksum_mismatch_message">Unknown library file. Are you sure you got it from a trusted source and it is for \'%s\'?</string>
<!-- Button text for loading gesture library --> <!-- Button text for loading gesture library -->
<string name="load_gesture_library_button_load">Load library</string> <string name="load_gesture_library_button_load">Load library</string>
<!-- Button text for deleting gesture library --> <!-- Button text for deleting gesture library -->