From ea18b4ea1f853c90e84d27eb0a787fd061014449 Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 31 Dec 2023 16:28:21 +0100 Subject: [PATCH 01/10] Move import export manager to separate folder --- .../BackupRestoreSettingsFragment.java | 5 ++-- .../ImportExportManager.kt} | 5 ++-- ...agerTest.kt => ImportExportManagerTest.kt} | 25 ++++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) rename app/src/main/java/org/schabi/newpipe/settings/{ContentSettingsManager.kt => export/ImportExportManager.kt} (96%) rename app/src/test/java/org/schabi/newpipe/settings/{ContentSettingsManagerTest.kt => ImportExportManagerTest.kt} (87%) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index bc24fbe81..842023e50 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -24,6 +24,7 @@ import androidx.preference.PreferenceManager; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.settings.export.ImportExportManager; import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.util.NavigationHelper; @@ -42,7 +43,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { private final SimpleDateFormat exportDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - private ContentSettingsManager manager; + private ImportExportManager manager; private String importExportDataPathKey; private final ActivityResultLauncher requestImportPathLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), @@ -57,7 +58,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { @Nullable final String rootKey) { final File homeDir = ContextCompat.getDataDir(requireContext()); Objects.requireNonNull(homeDir); - manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir)); + manager = new ImportExportManager(new NewPipeFileLocator(homeDir)); manager.deleteSettingsFile(); importExportDataPathKey = getString(R.string.import_export_data_path); diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt similarity index 96% rename from app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt rename to app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index df56de516..9954a2b36 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -1,8 +1,9 @@ -package org.schabi.newpipe.settings +package org.schabi.newpipe.settings.export import android.content.SharedPreferences import android.util.Log import org.schabi.newpipe.MainActivity.DEBUG +import org.schabi.newpipe.settings.NewPipeFileLocator import org.schabi.newpipe.streams.io.SharpOutputStream import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.util.ZipHelper @@ -11,7 +12,7 @@ import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.util.zip.ZipOutputStream -class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) { +class ImportExportManager(private val fileLocator: NewPipeFileLocator) { companion object { const val TAG = "ContentSetManager" } diff --git a/app/src/test/java/org/schabi/newpipe/settings/ContentSettingsManagerTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt similarity index 87% rename from app/src/test/java/org/schabi/newpipe/settings/ContentSettingsManagerTest.kt rename to app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt index ec41a77f8..7b219df18 100644 --- a/app/src/test/java/org/schabi/newpipe/settings/ContentSettingsManagerTest.kt +++ b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt @@ -17,6 +17,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.Mockito.withSettings import org.mockito.junit.MockitoJUnitRunner +import org.schabi.newpipe.settings.export.ImportExportManager import org.schabi.newpipe.streams.io.StoredFileHelper import us.shandian.giga.io.FileStream import java.io.File @@ -25,10 +26,10 @@ import java.nio.file.Files import java.util.zip.ZipFile @RunWith(MockitoJUnitRunner::class) -class ContentSettingsManagerTest { +class ImportExportManagerTest { companion object { - private val classloader = ContentSettingsManager::class.java.classLoader!! + private val classloader = ImportExportManager::class.java.classLoader!! } private lateinit var fileLocator: NewPipeFileLocator @@ -54,7 +55,7 @@ class ContentSettingsManagerTest { val output = File.createTempFile("newpipe_", "") `when`(storedFileHelper.stream).thenReturn(FileStream(output)) - ContentSettingsManager(fileLocator).exportDatabase(sharedPreferences, storedFileHelper) + ImportExportManager(fileLocator).exportDatabase(sharedPreferences, storedFileHelper) val zipFile = ZipFile(output) val entries = zipFile.entries().toList() @@ -77,7 +78,7 @@ class ContentSettingsManagerTest { val settings = File.createTempFile("newpipe_", "") `when`(fileLocator.settings).thenReturn(settings) - ContentSettingsManager(fileLocator).deleteSettingsFile() + ImportExportManager(fileLocator).deleteSettingsFile() assertFalse(settings.exists()) } @@ -87,7 +88,7 @@ class ContentSettingsManagerTest { val settings = File("non_existent") `when`(fileLocator.settings).thenReturn(settings) - ContentSettingsManager(fileLocator).deleteSettingsFile() + ImportExportManager(fileLocator).deleteSettingsFile() assertFalse(settings.exists()) } @@ -98,7 +99,7 @@ class ContentSettingsManagerTest { Assume.assumeTrue(dir.delete()) `when`(fileLocator.dbDir).thenReturn(dir) - ContentSettingsManager(fileLocator).ensureDbDirectoryExists() + ImportExportManager(fileLocator).ensureDbDirectoryExists() assertTrue(dir.exists()) } @@ -107,7 +108,7 @@ class ContentSettingsManagerTest { val dir = Files.createTempDirectory("newpipe_").toFile() `when`(fileLocator.dbDir).thenReturn(dir) - ContentSettingsManager(fileLocator).ensureDbDirectoryExists() + ImportExportManager(fileLocator).ensureDbDirectoryExists() assertTrue(dir.exists()) } @@ -124,7 +125,7 @@ class ContentSettingsManagerTest { val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) - val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper) + val success = ImportExportManager(fileLocator).extractDb(storedFileHelper) assertTrue(success) assertFalse(dbJournal.exists()) @@ -143,7 +144,7 @@ class ContentSettingsManagerTest { val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) - val success = ContentSettingsManager(fileLocator).extractDb(storedFileHelper) + val success = ImportExportManager(fileLocator).extractDb(storedFileHelper) assertFalse(success) assertTrue(dbJournal.exists()) @@ -159,7 +160,7 @@ class ContentSettingsManagerTest { val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) - val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper) + val contains = ImportExportManager(fileLocator).extractSettings(storedFileHelper) assertTrue(contains) } @@ -171,7 +172,7 @@ class ContentSettingsManagerTest { val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) - val contains = ContentSettingsManager(fileLocator).extractSettings(storedFileHelper) + val contains = ImportExportManager(fileLocator).extractSettings(storedFileHelper) assertFalse(contains) } @@ -185,7 +186,7 @@ class ContentSettingsManagerTest { val editor = Mockito.mock(SharedPreferences.Editor::class.java) `when`(preferences.edit()).thenReturn(editor) - ContentSettingsManager(fileLocator).loadSharedPreferences(preferences) + ImportExportManager(fileLocator).loadSharedPreferences(preferences) verify(editor, atLeastOnce()).putBoolean(anyString(), anyBoolean()) verify(editor, atLeastOnce()).putString(anyString(), anyString()) From 235fb926382cfed5a6353106c732e85a9140d6da Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 31 Dec 2023 18:11:35 +0100 Subject: [PATCH 02/10] Make checkstyle accept javadocs with long links --- checkstyle/checkstyle.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml index 3377e3b84..ee091fa9f 100644 --- a/checkstyle/checkstyle.xml +++ b/checkstyle/checkstyle.xml @@ -39,11 +39,13 @@ - + + + From d75a6eaa4164d3fc0ac67bc35048b021b85e9b6f Mon Sep 17 00:00:00 2001 From: Stypox Date: Sun, 31 Dec 2023 18:21:48 +0100 Subject: [PATCH 03/10] Fix vulnerability with whitelist-aware ObjectInputStream Only a few specific classes are now allowed. --- .../settings/export/ImportExportManager.kt | 5 +- .../export/PreferencesObjectInputStream.java | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/settings/export/PreferencesObjectInputStream.java diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index 9954a2b36..298841c5f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -8,7 +8,6 @@ import org.schabi.newpipe.streams.io.SharpOutputStream import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.util.ZipHelper import java.io.IOException -import java.io.ObjectInputStream import java.io.ObjectOutputStream import java.util.zip.ZipOutputStream @@ -78,7 +77,9 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) { try { val preferenceEditor = preferences.edit() - ObjectInputStream(fileLocator.settings.inputStream()).use { input -> + PreferencesObjectInputStream( + fileLocator.settings.inputStream() + ).use { input -> preferenceEditor.clear() @Suppress("UNCHECKED_CAST") val entries = input.readObject() as Map diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/PreferencesObjectInputStream.java b/app/src/main/java/org/schabi/newpipe/settings/export/PreferencesObjectInputStream.java new file mode 100644 index 000000000..0d11b0b61 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/export/PreferencesObjectInputStream.java @@ -0,0 +1,58 @@ +package org.schabi.newpipe.settings.export; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectStreamClass; +import java.util.Set; + +/** + * An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to + * prevent injections. The only allowed types are: all primitive types, all boxed primitive types, + * null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources: + * + * cmu.edu + * , + * + * OWASP cheatsheet + * , + * + * Apache's {@code ValidatingObjectInputStream} + * + */ +public class PreferencesObjectInputStream extends ObjectInputStream { + + /** + * Primitive types, strings and other built-in types do not pass through resolveClass() but + * instead have a custom encoding; see + * + * official docs. + */ + private static final Set CLASS_WHITELIST = Set.of( + "java.lang.Boolean", + "java.lang.Byte", + "java.lang.Character", + "java.lang.Short", + "java.lang.Integer", + "java.lang.Long", + "java.lang.Float", + "java.lang.Double", + "java.lang.Void", + "java.util.HashMap", + "java.util.HashSet" + ); + + public PreferencesObjectInputStream(final InputStream in) throws IOException { + super(in); + } + + @Override + protected Class resolveClass(final ObjectStreamClass desc) + throws ClassNotFoundException, IOException { + if (CLASS_WHITELIST.contains(desc.getName())) { + return super.resolveClass(desc); + } else { + throw new ClassNotFoundException("Class not allowed: " + desc.getName()); + } + } +} From d8668ed226dccf36666fc325204acdb29a078f6d Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 27 Mar 2024 15:02:27 +0100 Subject: [PATCH 04/10] Show snackbar error when settings import fails --- .../org/schabi/newpipe/error/UserAction.java | 1 + .../BackupRestoreSettingsFragment.java | 17 ++++- .../settings/export/ImportExportManager.kt | 69 ++++++++----------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.java index c8701cd77..6ca66e0d2 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.java @@ -6,6 +6,7 @@ package org.schabi.newpipe.error; public enum UserAction { USER_REPORT("user report"), UI_ERROR("ui error"), + DATABASE_IMPORT_EXPORT("database import or export"), SUBSCRIPTION_CHANGE("subscription change"), SUBSCRIPTION_UPDATE("subscription update"), SUBSCRIPTION_GET("get subscription"), diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index 842023e50..1d00ef287 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -23,7 +23,9 @@ import androidx.preference.PreferenceManager; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.settings.export.ImportExportManager; import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; import org.schabi.newpipe.streams.io.StoredFileHelper; @@ -166,7 +168,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { Toast.makeText(requireContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT) .show(); } catch (final Exception e) { - ErrorUtil.showUiErrorSnackbar(this, "Exporting database", e); + showErrorSnackbar(e, "Exporting database and settings"); } } @@ -202,7 +204,12 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { final Context context = requireContext(); final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context); - manager.loadSharedPreferences(prefs); + try { + manager.loadSharedPreferences(prefs); + } catch (IOException | ClassNotFoundException e) { + showErrorSnackbar(e, "Importing preferences"); + return; + } cleanImport(context, prefs); finishImport(importDataUri); }) @@ -211,7 +218,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { finishImport(importDataUri); } } catch (final Exception e) { - ErrorUtil.showUiErrorSnackbar(this, "Importing database", e); + showErrorSnackbar(e, "Importing database and settings"); } } @@ -269,4 +276,8 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { .putString(importExportDataPathKey, importExportDataUri.toString()); editor.apply(); } + + private void showErrorSnackbar(final Throwable e, final String request) { + ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index 298841c5f..b4503bdd6 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -73,50 +73,41 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) { /** * Remove all shared preferences from the app and load the preferences supplied to the manager. */ + @Throws(IOException::class, ClassNotFoundException::class) fun loadSharedPreferences(preferences: SharedPreferences) { - try { - val preferenceEditor = preferences.edit() + val preferenceEditor = preferences.edit() - PreferencesObjectInputStream( - fileLocator.settings.inputStream() - ).use { input -> - preferenceEditor.clear() - @Suppress("UNCHECKED_CAST") - val entries = input.readObject() as Map - for ((key, value) in entries) { - when (value) { - is Boolean -> { - preferenceEditor.putBoolean(key, value) - } - is Float -> { - preferenceEditor.putFloat(key, value) - } - is Int -> { - preferenceEditor.putInt(key, value) - } - is Long -> { - preferenceEditor.putLong(key, value) - } - is String -> { - preferenceEditor.putString(key, value) - } - is Set<*> -> { - // There are currently only Sets with type String possible - @Suppress("UNCHECKED_CAST") - preferenceEditor.putStringSet(key, value as Set?) - } + PreferencesObjectInputStream( + fileLocator.settings.inputStream() + ).use { input -> + preferenceEditor.clear() + @Suppress("UNCHECKED_CAST") + val entries = input.readObject() as Map + for ((key, value) in entries) { + when (value) { + is Boolean -> { + preferenceEditor.putBoolean(key, value) + } + is Float -> { + preferenceEditor.putFloat(key, value) + } + is Int -> { + preferenceEditor.putInt(key, value) + } + is Long -> { + preferenceEditor.putLong(key, value) + } + is String -> { + preferenceEditor.putString(key, value) + } + is Set<*> -> { + // There are currently only Sets with type String possible + @Suppress("UNCHECKED_CAST") + preferenceEditor.putStringSet(key, value as Set?) } } - preferenceEditor.commit() - } - } catch (e: IOException) { - if (DEBUG) { - Log.e(TAG, "Unable to loadSharedPreferences", e) - } - } catch (e: ClassNotFoundException) { - if (DEBUG) { - Log.e(TAG, "Unable to loadSharedPreferences", e) } + preferenceEditor.commit() } } } From 6afdbd6fd39cd1c5020dafde65215cfa66fa781f Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 27 Mar 2024 15:12:57 +0100 Subject: [PATCH 05/10] Add test: vulnerable settings should fail importing --- .../settings/ImportExportManagerTest.kt | 19 ++++++++++++++++++ .../settings/vulnerable_serialization.zip | Bin 0 -> 3536 bytes 2 files changed, 19 insertions(+) create mode 100644 app/src/test/resources/settings/vulnerable_serialization.zip diff --git a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt index 7b219df18..2743ba098 100644 --- a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt +++ b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt @@ -3,6 +3,7 @@ package org.schabi.newpipe.settings import android.content.SharedPreferences import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Assume import org.junit.Before @@ -192,4 +193,22 @@ class ImportExportManagerTest { verify(editor, atLeastOnce()).putString(anyString(), anyString()) verify(editor, atLeastOnce()).putInt(anyString(), anyInt()) } + + @Test + fun `Importing preferences with a serialization injected class should fail`() { + val settings = File.createTempFile("newpipe_", "") + `when`(fileLocator.settings).thenReturn(settings) + + val emptyZip = File(classloader.getResource("settings/vulnerable_serialization.zip")?.file!!) + `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) + Assume.assumeTrue(ImportExportManager(fileLocator).extractSettings(storedFileHelper)) + + val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) + val editor = Mockito.mock(SharedPreferences.Editor::class.java) + `when`(preferences.edit()).thenReturn(editor) + + assertThrows(ClassNotFoundException::class.java) { + ImportExportManager(fileLocator).loadSharedPreferences(preferences) + } + } } diff --git a/app/src/test/resources/settings/vulnerable_serialization.zip b/app/src/test/resources/settings/vulnerable_serialization.zip new file mode 100644 index 0000000000000000000000000000000000000000..d57a5f8d0150cd11dec35d7c29ed2bcb9e65a774 GIT binary patch literal 3536 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 D$x)Jd literal 0 HcmV?d00001 From d8423499dc695f06e30209047c9274ba6b6d21f2 Mon Sep 17 00:00:00 2001 From: Stypox Date: Wed, 27 Mar 2024 12:31:16 +0100 Subject: [PATCH 06/10] Use JSON for settings imports/exports --- .../BackupRestoreSettingsFragment.java | 20 +- .../newpipe/settings/NewPipeFileLocator.kt | 21 --- .../settings/export/BackupFileLocator.kt | 28 +++ .../settings/export/ImportExportManager.kt | 154 +++++++++++----- .../org/schabi/newpipe/util/ZipHelper.java | 172 +++++++++++++----- app/src/main/res/values/strings.xml | 1 + .../settings/ImportExportManagerTest.kt | 60 ++---- .../test/resources/settings/newpipe.settings | Bin 2445 -> 0 bytes 8 files changed, 292 insertions(+), 164 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt create mode 100644 app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt delete mode 100644 app/src/test/resources/settings/newpipe.settings diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index 1d00ef287..f4080acd3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -21,11 +21,14 @@ import androidx.core.content.ContextCompat; import androidx.preference.Preference; import androidx.preference.PreferenceManager; +import com.grack.nanojson.JsonParserException; + import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.settings.export.BackupFileLocator; import org.schabi.newpipe.settings.export.ImportExportManager; import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard; import org.schabi.newpipe.streams.io.StoredFileHelper; @@ -60,8 +63,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { @Nullable final String rootKey) { final File homeDir = ContextCompat.getDataDir(requireContext()); Objects.requireNonNull(homeDir); - manager = new ImportExportManager(new NewPipeFileLocator(homeDir)); - manager.deleteSettingsFile(); + manager = new ImportExportManager(new BackupFileLocator(homeDir)); importExportDataPathKey = getString(R.string.import_export_data_path); @@ -192,9 +194,13 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { } // if settings file exist, ask if it should be imported. - if (manager.extractSettings(file)) { + final boolean hasJsonPrefs = manager.exportHasJsonPrefs(file); + if (hasJsonPrefs || manager.exportHasSerializedPrefs(file)) { new androidx.appcompat.app.AlertDialog.Builder(requireContext()) .setTitle(R.string.import_settings) + .setMessage(hasJsonPrefs ? null : requireContext() + .getString(R.string.import_settings_vulnerable_format)) + .setOnDismissListener(dialog -> finishImport(importDataUri)) .setNegativeButton(R.string.cancel, (dialog, which) -> { dialog.dismiss(); finishImport(importDataUri); @@ -205,8 +211,12 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { final SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(context); try { - manager.loadSharedPreferences(prefs); - } catch (IOException | ClassNotFoundException e) { + if (hasJsonPrefs) { + manager.loadJsonPrefs(file, prefs); + } else { + manager.loadSerializedPrefs(file, prefs); + } + } catch (IOException | ClassNotFoundException | JsonParserException e) { showErrorSnackbar(e, "Importing preferences"); return; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt b/app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt deleted file mode 100644 index c2f93d15f..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeFileLocator.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.schabi.newpipe.settings - -import java.io.File - -/** - * Locates specific files of NewPipe based on the home directory of the app. - */ -class NewPipeFileLocator(private val homeDir: File) { - - val dbDir by lazy { File(homeDir, "/databases") } - - val db by lazy { File(homeDir, "/databases/newpipe.db") } - - val dbJournal by lazy { File(homeDir, "/databases/newpipe.db-journal") } - - val dbShm by lazy { File(homeDir, "/databases/newpipe.db-shm") } - - val dbWal by lazy { File(homeDir, "/databases/newpipe.db-wal") } - - val settings by lazy { File(homeDir, "/databases/newpipe.settings") } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt b/app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt new file mode 100644 index 000000000..c864e4a0d --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/export/BackupFileLocator.kt @@ -0,0 +1,28 @@ +package org.schabi.newpipe.settings.export + +import java.io.File + +/** + * Locates specific files of NewPipe based on the home directory of the app. + */ +class BackupFileLocator(private val homeDir: File) { + companion object { + const val FILE_NAME_DB = "newpipe.db" + @Deprecated( + "Serializing preferences with Java's ObjectOutputStream is vulnerable to injections", + replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS") + ) + const val FILE_NAME_SERIALIZED_PREFS = "newpipe.settings" + const val FILE_NAME_JSON_PREFS = "preferences.json" + } + + val dbDir by lazy { File(homeDir, "/databases") } + + val db by lazy { File(dbDir, FILE_NAME_DB) } + + val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") } + + val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") } + + val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index b4503bdd6..339ebf644 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -2,8 +2,10 @@ package org.schabi.newpipe.settings.export import android.content.SharedPreferences import android.util.Log -import org.schabi.newpipe.MainActivity.DEBUG -import org.schabi.newpipe.settings.NewPipeFileLocator +import com.grack.nanojson.JsonArray +import com.grack.nanojson.JsonParser +import com.grack.nanojson.JsonParserException +import com.grack.nanojson.JsonWriter import org.schabi.newpipe.streams.io.SharpOutputStream import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.util.ZipHelper @@ -11,9 +13,9 @@ import java.io.IOException import java.io.ObjectOutputStream import java.util.zip.ZipOutputStream -class ImportExportManager(private val fileLocator: NewPipeFileLocator) { +class ImportExportManager(private val fileLocator: BackupFileLocator) { companion object { - const val TAG = "ContentSetManager" + const val TAG = "ImportExportManager" } /** @@ -23,27 +25,41 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) { @Throws(Exception::class) fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) { file.create() - ZipOutputStream(SharpOutputStream(file.stream).buffered()) - .use { outZip -> - ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db") + ZipOutputStream(SharpOutputStream(file.stream).buffered()).use { outZip -> + try { + // add the database + ZipHelper.addFileToZip( + outZip, + BackupFileLocator.FILE_NAME_DB, + fileLocator.db.path, + ) - try { - ObjectOutputStream(fileLocator.settings.outputStream()).use { output -> + // add the legacy vulnerable serialized preferences (will be removed in the future) + ZipHelper.addFileToZip( + outZip, + BackupFileLocator.FILE_NAME_SERIALIZED_PREFS + ) { byteOutput -> + ObjectOutputStream(byteOutput).use { output -> output.writeObject(preferences.all) output.flush() } - } catch (e: IOException) { - if (DEBUG) { - Log.e(TAG, "Unable to exportDatabase", e) - } } - ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings") + // add the JSON preferences + ZipHelper.addFileToZip( + outZip, + BackupFileLocator.FILE_NAME_JSON_PREFS + ) { byteOutput -> + JsonWriter + .indent("") + .on(byteOutput) + .`object`(preferences.all) + .done() + } + } catch (e: Exception) { + Log.e(TAG, "Unable to export serialized settings", e) } - } - - fun deleteSettingsFile() { - fileLocator.settings.delete() + } } /** @@ -56,7 +72,12 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) { } fun extractDb(file: StoredFileHelper): Boolean { - val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db") + val success = ZipHelper.extractFileFromZip( + file, + BackupFileLocator.FILE_NAME_DB, + fileLocator.db.path, + ) + if (success) { fileLocator.dbJournal.delete() fileLocator.dbWal.delete() @@ -66,48 +87,81 @@ class ImportExportManager(private val fileLocator: NewPipeFileLocator) { return success } - fun extractSettings(file: StoredFileHelper): Boolean { - return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings") + @Deprecated( + "Serializing preferences with Java's ObjectOutputStream is vulnerable to injections", + replaceWith = ReplaceWith("exportHasJsonPrefs") + ) + fun exportHasSerializedPrefs(zipFile: StoredFileHelper): Boolean { + return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) + } + + fun exportHasJsonPrefs(zipFile: StoredFileHelper): Boolean { + return ZipHelper.zipContainsFile(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) } /** * Remove all shared preferences from the app and load the preferences supplied to the manager. */ + @Deprecated( + "Serializing preferences with Java's ObjectOutputStream is vulnerable to injections", + replaceWith = ReplaceWith("loadJsonPrefs") + ) @Throws(IOException::class, ClassNotFoundException::class) - fun loadSharedPreferences(preferences: SharedPreferences) { - val preferenceEditor = preferences.edit() + fun loadSerializedPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) { + ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) { + PreferencesObjectInputStream(it).use { input -> + val editor = preferences.edit() + editor.clear() + @Suppress("UNCHECKED_CAST") + val entries = input.readObject() as Map + for ((key, value) in entries) { + when (value) { + is Boolean -> editor.putBoolean(key, value) + is Float -> editor.putFloat(key, value) + is Int -> editor.putInt(key, value) + is Long -> editor.putLong(key, value) + is String -> editor.putString(key, value) + is Set<*> -> { + // There are currently only Sets with type String possible + @Suppress("UNCHECKED_CAST") + editor.putStringSet(key, value as Set?) + } + } + } - PreferencesObjectInputStream( - fileLocator.settings.inputStream() - ).use { input -> - preferenceEditor.clear() - @Suppress("UNCHECKED_CAST") - val entries = input.readObject() as Map - for ((key, value) in entries) { + if (!editor.commit()) { + Log.e(TAG, "Unable to loadSerializedPrefs") + } + } + } + } + + /** + * Remove all shared preferences from the app and load the preferences supplied to the manager. + */ + @Throws(JsonParserException::class) + fun loadJsonPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) { + ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) { + val editor = preferences.edit() + editor.clear() + + val jsonObject = JsonParser.`object`().from(it) + for ((key, value) in jsonObject) { when (value) { - is Boolean -> { - preferenceEditor.putBoolean(key, value) - } - is Float -> { - preferenceEditor.putFloat(key, value) - } - is Int -> { - preferenceEditor.putInt(key, value) - } - is Long -> { - preferenceEditor.putLong(key, value) - } - is String -> { - preferenceEditor.putString(key, value) - } - is Set<*> -> { - // There are currently only Sets with type String possible - @Suppress("UNCHECKED_CAST") - preferenceEditor.putStringSet(key, value as Set?) + is Boolean -> editor.putBoolean(key, value) + is Float -> editor.putFloat(key, value) + is Int -> editor.putInt(key, value) + is Long -> editor.putLong(key, value) + is String -> editor.putString(key, value) + is JsonArray -> { + editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet()) } } } - preferenceEditor.commit() + + if (!editor.commit()) { + Log.e(TAG, "Unable to loadJsonPrefs") + } } } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java index bc08e6197..b2aebac42 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ZipHelper.java @@ -1,18 +1,21 @@ package org.schabi.newpipe.util; import org.schabi.newpipe.streams.io.SharpInputStream; +import org.schabi.newpipe.streams.io.StoredFileHelper; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; -import org.schabi.newpipe.streams.io.StoredFileHelper; - /** * Created by Christian Schabesberger on 28.01.18. * Copyright 2018 Christian Schabesberger @@ -34,73 +37,154 @@ import org.schabi.newpipe.streams.io.StoredFileHelper; */ public final class ZipHelper { - private ZipHelper() { } private static final int BUFFER_SIZE = 2048; + @FunctionalInterface + public interface InputStreamConsumer { + void acceptStream(InputStream inputStream) throws IOException; + } + + @FunctionalInterface + public interface OutputStreamConsumer { + void acceptStream(OutputStream outputStream) throws IOException; + } + + + private ZipHelper() { } + + /** - * This function helps to create zip files. - * Caution this will override the original file. + * This function helps to create zip files. Caution this will overwrite the original file. * - * @param outZip The ZipOutputStream where the data should be stored in - * @param file The path of the file that should be added to zip. - * @param name The path of the file inside the zip. - * @throws Exception + * @param outZip the ZipOutputStream where the data should be stored in + * @param nameInZip the path of the file inside the zip + * @param fileOnDisk the path of the file on the disk that should be added to zip */ - public static void addFileToZip(final ZipOutputStream outZip, final String file, - final String name) throws Exception { + public static void addFileToZip(final ZipOutputStream outZip, + final String nameInZip, + final String fileOnDisk) throws IOException { + try (FileInputStream fi = new FileInputStream(fileOnDisk)) { + addFileToZip(outZip, nameInZip, fi); + } + } + + /** + * This function helps to create zip files. Caution this will overwrite the original file. + * + * @param outZip the ZipOutputStream where the data should be stored in + * @param nameInZip the path of the file inside the zip + * @param streamConsumer will be called with an output stream that will go to the output file + */ + public static void addFileToZip(final ZipOutputStream outZip, + final String nameInZip, + final OutputStreamConsumer streamConsumer) throws IOException { + final byte[] bytes; + try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream()) { + streamConsumer.acceptStream(byteOutput); + bytes = byteOutput.toByteArray(); + } + + try (ByteArrayInputStream byteInput = new ByteArrayInputStream(bytes)) { + ZipHelper.addFileToZip(outZip, nameInZip, byteInput); + } + } + + /** + * This function helps to create zip files. Caution this will overwrite the original file. + * + * @param outZip the ZipOutputStream where the data should be stored in + * @param nameInZip the path of the file inside the zip + * @param inputStream the content to put inside the file + */ + public static void addFileToZip(final ZipOutputStream outZip, + final String nameInZip, + final InputStream inputStream) throws IOException { final byte[] data = new byte[BUFFER_SIZE]; - try (FileInputStream fi = new FileInputStream(file); - BufferedInputStream inputStream = new BufferedInputStream(fi, BUFFER_SIZE)) { - final ZipEntry entry = new ZipEntry(name); + try (BufferedInputStream bufferedInputStream = + new BufferedInputStream(inputStream, BUFFER_SIZE)) { + final ZipEntry entry = new ZipEntry(nameInZip); outZip.putNextEntry(entry); int count; - while ((count = inputStream.read(data, 0, BUFFER_SIZE)) != -1) { + while ((count = bufferedInputStream.read(data, 0, BUFFER_SIZE)) != -1) { outZip.write(data, 0, count); } } } /** - * This will extract data from ZipInputStream. - * Caution this will override the original file. + * This will extract data from ZipInputStream. Caution this will overwrite the original file. * - * @param zipFile The zip file - * @param file The path of the file on the disk where the data should be extracted to. - * @param name The path of the file inside the zip. + * @param zipFile the zip file to extract from + * @param nameInZip the path of the file inside the zip + * @param fileOnDisk the path of the file on the disk where the data should be extracted to * @return will return true if the file was found within the zip file - * @throws Exception */ - public static boolean extractFileFromZip(final StoredFileHelper zipFile, final String file, - final String name) throws Exception { + public static boolean extractFileFromZip(final StoredFileHelper zipFile, + final String nameInZip, + final String fileOnDisk) throws IOException { + return extractFileFromZip(zipFile, nameInZip, input -> { + // delete old file first + final File oldFile = new File(fileOnDisk); + if (oldFile.exists()) { + if (!oldFile.delete()) { + throw new IOException("Could not delete " + fileOnDisk); + } + } + + final byte[] data = new byte[BUFFER_SIZE]; + try (FileOutputStream outFile = new FileOutputStream(fileOnDisk)) { + int count; + while ((count = input.read(data)) != -1) { + outFile.write(data, 0, count); + } + } + }); + } + + /** + * This will extract data from ZipInputStream. + * + * @param zipFile the zip file to extract from + * @param nameInZip the path of the file inside the zip + * @param streamConsumer will be called with the input stream from the file inside the zip + * @return will return true if the file was found within the zip file + */ + public static boolean extractFileFromZip(final StoredFileHelper zipFile, + final String nameInZip, + final InputStreamConsumer streamConsumer) + throws IOException { + try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream( + new SharpInputStream(zipFile.getStream())))) { + ZipEntry ze; + while ((ze = inZip.getNextEntry()) != null) { + if (ze.getName().equals(nameInZip)) { + streamConsumer.acceptStream(inZip); + return true; + } + } + + return false; + } + } + + /** + * @param zipFile the zip file + * @param fileInZip the filename to check + * @return whether the provided filename is in the zip; only the first level is checked + */ + public static boolean zipContainsFile(final StoredFileHelper zipFile, final String fileInZip) + throws Exception { try (ZipInputStream inZip = new ZipInputStream(new BufferedInputStream( new SharpInputStream(zipFile.getStream())))) { - final byte[] data = new byte[BUFFER_SIZE]; - boolean found = false; ZipEntry ze; while ((ze = inZip.getNextEntry()) != null) { - if (ze.getName().equals(name)) { - found = true; - // delete old file first - final File oldFile = new File(file); - if (oldFile.exists()) { - if (!oldFile.delete()) { - throw new Exception("Could not delete " + file); - } - } - - try (FileOutputStream outFile = new FileOutputStream(file)) { - int count = 0; - while ((count = inZip.read(data)) != -1) { - outFile.write(data, 0, count); - } - } - - inZip.closeEntry(); + if (ze.getName().equals(fileInZip)) { + return true; } } - return found; + return false; } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4ad8b1d9..56140441c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -856,4 +856,5 @@ Show more Show less + The settings in the export being imported use a vulnerable format that was deprecated since NewPipe 0.27.0. Make sure the export being imported is from a trusted source, and prefer using only exports obtained from NewPipe 0.27.0 or newer in the future. Support for importing settings in this vulnerable format will soon be removed completely, and then old versions of NewPipe will not be able to import settings of exports from new versions anymore. diff --git a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt index 2743ba098..70420801c 100644 --- a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt +++ b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt @@ -1,6 +1,7 @@ package org.schabi.newpipe.settings import android.content.SharedPreferences +import com.grack.nanojson.JsonParser import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertThrows @@ -18,6 +19,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.Mockito.withSettings import org.mockito.junit.MockitoJUnitRunner +import org.schabi.newpipe.settings.export.BackupFileLocator import org.schabi.newpipe.settings.export.ImportExportManager import org.schabi.newpipe.streams.io.StoredFileHelper import us.shandian.giga.io.FileStream @@ -33,21 +35,19 @@ class ImportExportManagerTest { private val classloader = ImportExportManager::class.java.classLoader!! } - private lateinit var fileLocator: NewPipeFileLocator + private lateinit var fileLocator: BackupFileLocator private lateinit var storedFileHelper: StoredFileHelper @Before fun setupFileLocator() { - fileLocator = Mockito.mock(NewPipeFileLocator::class.java, withSettings().stubOnly()) + fileLocator = Mockito.mock(BackupFileLocator::class.java, withSettings().stubOnly()) storedFileHelper = Mockito.mock(StoredFileHelper::class.java, withSettings().stubOnly()) } @Test fun `The settings must be exported successfully in the correct format`() { val db = File(classloader.getResource("settings/newpipe.db")!!.file) - val newpipeSettings = File.createTempFile("newpipe_", "") `when`(fileLocator.db).thenReturn(db) - `when`(fileLocator.settings).thenReturn(newpipeSettings) val expectedPreferences = mapOf("such pref" to "much wow") val sharedPreferences = @@ -60,7 +60,7 @@ class ImportExportManagerTest { val zipFile = ZipFile(output) val entries = zipFile.entries().toList() - assertEquals(2, entries.size) + assertEquals(3, entries.size) zipFile.getInputStream(entries.first { it.name == "newpipe.db" }).use { actual -> db.inputStream().use { expected -> @@ -72,26 +72,11 @@ class ImportExportManagerTest { val actualPreferences = ObjectInputStream(actual).readObject() assertEquals(expectedPreferences, actualPreferences) } - } - @Test - fun `Settings file must be deleted`() { - val settings = File.createTempFile("newpipe_", "") - `when`(fileLocator.settings).thenReturn(settings) - - ImportExportManager(fileLocator).deleteSettingsFile() - - assertFalse(settings.exists()) - } - - @Test - fun `Deleting settings file must do nothing if none exist`() { - val settings = File("non_existent") - `when`(fileLocator.settings).thenReturn(settings) - - ImportExportManager(fileLocator).deleteSettingsFile() - - assertFalse(settings.exists()) + zipFile.getInputStream(entries.first { it.name == "preferences.json" }).use { actual -> + val actualPreferences = JsonParser.`object`().from(actual) + assertEquals(expectedPreferences, actualPreferences) + } } @Test @@ -156,38 +141,29 @@ class ImportExportManagerTest { @Test fun `Contains setting must return true if a settings file exists in the zip`() { - val settings = File.createTempFile("newpipe_", "") - `when`(fileLocator.settings).thenReturn(settings) - val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) - val contains = ImportExportManager(fileLocator).extractSettings(storedFileHelper) - - assertTrue(contains) + assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper)) } @Test fun `Contains setting must return false if a no settings file exists in the zip`() { - val settings = File.createTempFile("newpipe_", "") - `when`(fileLocator.settings).thenReturn(settings) - val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) - val contains = ImportExportManager(fileLocator).extractSettings(storedFileHelper) - - assertFalse(contains) + assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper)) } @Test fun `Preferences must be set from the settings file`() { - val settings = File(classloader.getResource("settings/newpipe.settings")!!.path) - `when`(fileLocator.settings).thenReturn(settings) + val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) + `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) val editor = Mockito.mock(SharedPreferences.Editor::class.java) `when`(preferences.edit()).thenReturn(editor) + `when`(editor.commit()).thenReturn(true) - ImportExportManager(fileLocator).loadSharedPreferences(preferences) + ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences) verify(editor, atLeastOnce()).putBoolean(anyString(), anyBoolean()) verify(editor, atLeastOnce()).putString(anyString(), anyString()) @@ -196,19 +172,15 @@ class ImportExportManagerTest { @Test fun `Importing preferences with a serialization injected class should fail`() { - val settings = File.createTempFile("newpipe_", "") - `when`(fileLocator.settings).thenReturn(settings) - val emptyZip = File(classloader.getResource("settings/vulnerable_serialization.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) - Assume.assumeTrue(ImportExportManager(fileLocator).extractSettings(storedFileHelper)) val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) val editor = Mockito.mock(SharedPreferences.Editor::class.java) `when`(preferences.edit()).thenReturn(editor) assertThrows(ClassNotFoundException::class.java) { - ImportExportManager(fileLocator).loadSharedPreferences(preferences) + ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences) } } } diff --git a/app/src/test/resources/settings/newpipe.settings b/app/src/test/resources/settings/newpipe.settings deleted file mode 100644 index 56e6c5d5dd53ba66b0144215b928f6137f67fc30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2445 zcmaJ@zi%8x7#-VjLL8FV`Qea26rcba>tWC@AwsfT$@bXs8f9v`AFEZ+6$YOH59>lXiFZn>X*h?|t{^FSQuFTKUxM znYEg2zINM$?2f4xkNp1A&#Mo9T~PD4)KczDddnp2{FXY(nWvDsJRMi>zNh5#VDub* zAL4tR)oUR!Icat}?AFGUwuKqux74L$JFtmi>&mddYi`Qn+I8o0GUYEX`-i`M_wV2H zYVH%YxM%X3#?>6F3)rzPNV`&-I<-y@Tv7);G`0-8>T|WASe>bZ>_TJU)PZP6@6bdZ z9%l6-@Q$>=Kq2-vVM=x4N~V(AB`+la;Pq}zHOZra^+iNFU8ZtXU7UAfw;Se;JYs1z#%U6?Ueu%DpT+pJEuT!Of|jCT)*Nnk2iU7A*1hui^( zD=HDGC;QI&U<;BA$)Ec3`yc-K;;T*gV5{?iSIg0*(Jyyv5#Y;5-(EcS%^zRSD>bg* zKDgjyR2nW|y=PN$t-8x{N_n#u6JT3b`v}iIK30IkWq}DhEse*!hq^MHv3f&jeP_L0 zxK2f#gT!>bw|>X%SqkfSX>`w4w9e{^t8Q-p483ZdsB*vt+ynCUU$oh43)KX42h z!4@eLKP2YDYl(LuXn0`c{EqGrcCQ_eRY9_--rYPe-2qm}mnLthiEbp{GPG*)96gee zsdQQwl`V%;ws?k$m!pbO;ErW9c(hCS))im5}2_^Pw<68fK2z8dISMv0Drk&AjoMs#>b9it&?BwVX2r$E{;3Vfb9jr!2~|;gdEOfJ zRg-wLmebHAlWVyrlTL29au110>3z4cwz<}8C4S|ZE8h>UY!@zahID4Nlp3^hdvU(< zG$8MR!&QWtmk5#u=!(flW)dW#+6HDd+uY5B6oU7g`vA$VNV({1Y1^Z4NS7l(@kHy2 zpk0{7RK@(2>B#D6b6HdytS;sTafKF5b%USEgn|Y=0qEttZ%)w|ni~KpkK%laVjRSU^ zjkP0)wMJ5$@#E%1cYR0S!jG8J9etsl{&8xzDqo6d(dI)d@yix%W`<6)%&E>_ Date: Sat, 30 Mar 2024 18:42:11 +0100 Subject: [PATCH 07/10] Add test zips and extensive tests for ImportExportManager Now all possible combinations of files in the zip (present or not) are checked at the same time --- .../settings/export/ImportExportManager.kt | 20 +- .../settings/ImportAllCombinationsTest.kt | 184 ++++++++++++++++++ .../settings/ImportExportManagerTest.kt | 16 +- app/src/test/resources/settings/README.md | 4 + .../test/resources/settings/db_noser_json.zip | Bin 0 -> 5428 bytes .../resources/settings/db_noser_nojson.zip | Bin 0 -> 4040 bytes .../test/resources/settings/db_ser_json.zip | Bin 0 -> 7243 bytes .../test/resources/settings/db_ser_nojson.zip | Bin 0 -> 5807 bytes .../resources/settings/db_vulnser_json.zip | Bin 0 -> 6752 bytes .../resources/settings/db_vulnser_nojson.zip | Bin 0 -> 5364 bytes app/src/test/resources/settings/newpipe.zip | Bin 8317 -> 0 bytes .../resources/settings/nodb_noser_json.zip | Bin 0 -> 1410 bytes .../{empty.zip => nodb_noser_nojson.zip} | Bin .../test/resources/settings/nodb_ser_json.zip | Bin 0 -> 3177 bytes .../resources/settings/nodb_ser_nojson.zip | Bin 0 -> 1789 bytes .../resources/settings/nodb_vulnser_json.zip | Bin 0 -> 2734 bytes .../settings/nodb_vulnser_nojson.zip | Bin 0 -> 1346 bytes .../settings/vulnerable_serialization.zip | Bin 3536 -> 0 bytes 18 files changed, 211 insertions(+), 13 deletions(-) create mode 100644 app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt create mode 100644 app/src/test/resources/settings/README.md create mode 100644 app/src/test/resources/settings/db_noser_json.zip create mode 100644 app/src/test/resources/settings/db_noser_nojson.zip create mode 100644 app/src/test/resources/settings/db_ser_json.zip create mode 100644 app/src/test/resources/settings/db_ser_nojson.zip create mode 100644 app/src/test/resources/settings/db_vulnser_json.zip create mode 100644 app/src/test/resources/settings/db_vulnser_nojson.zip delete mode 100644 app/src/test/resources/settings/newpipe.zip create mode 100644 app/src/test/resources/settings/nodb_noser_json.zip rename app/src/test/resources/settings/{empty.zip => nodb_noser_nojson.zip} (100%) create mode 100644 app/src/test/resources/settings/nodb_ser_json.zip create mode 100644 app/src/test/resources/settings/nodb_ser_nojson.zip create mode 100644 app/src/test/resources/settings/nodb_vulnser_json.zip create mode 100644 app/src/test/resources/settings/nodb_vulnser_nojson.zip delete mode 100644 app/src/test/resources/settings/vulnerable_serialization.zip diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index 339ebf644..5558a1b37 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -9,6 +9,7 @@ import com.grack.nanojson.JsonWriter import org.schabi.newpipe.streams.io.SharpOutputStream import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.util.ZipHelper +import java.io.FileNotFoundException import java.io.IOException import java.io.ObjectOutputStream import java.util.zip.ZipOutputStream @@ -110,10 +111,12 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) { fun loadSerializedPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) { ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) { PreferencesObjectInputStream(it).use { input -> - val editor = preferences.edit() - editor.clear() @Suppress("UNCHECKED_CAST") val entries = input.readObject() as Map + + val editor = preferences.edit() + editor.clear() + for ((key, value) in entries) { when (value) { is Boolean -> editor.putBoolean(key, value) @@ -133,19 +136,24 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) { Log.e(TAG, "Unable to loadSerializedPrefs") } } + }.let { fileExists -> + if (!fileExists) { + throw FileNotFoundException(BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) + } } } /** * Remove all shared preferences from the app and load the preferences supplied to the manager. */ - @Throws(JsonParserException::class) + @Throws(IOException::class, JsonParserException::class) fun loadJsonPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) { ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) { + val jsonObject = JsonParser.`object`().from(it) + val editor = preferences.edit() editor.clear() - val jsonObject = JsonParser.`object`().from(it) for ((key, value) in jsonObject) { when (value) { is Boolean -> editor.putBoolean(key, value) @@ -162,6 +170,10 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) { if (!editor.commit()) { Log.e(TAG, "Unable to loadJsonPrefs") } + }.let { fileExists -> + if (!fileExists) { + throw FileNotFoundException(BackupFileLocator.FILE_NAME_JSON_PREFS) + } } } } diff --git a/app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt new file mode 100644 index 000000000..862ac3b80 --- /dev/null +++ b/app/src/test/java/org/schabi/newpipe/settings/ImportAllCombinationsTest.kt @@ -0,0 +1,184 @@ +package org.schabi.newpipe.settings + +import android.content.SharedPreferences +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import org.schabi.newpipe.settings.export.BackupFileLocator +import org.schabi.newpipe.settings.export.ImportExportManager +import org.schabi.newpipe.streams.io.StoredFileHelper +import us.shandian.giga.io.FileStream +import java.io.File +import java.io.IOException +import java.nio.file.Files + +class ImportAllCombinationsTest { + + companion object { + private val classloader = ImportExportManager::class.java.classLoader!! + } + + private enum class Ser(val id: String) { + YES("ser"), + VULNERABLE("vulnser"), + NO("noser"); + } + + private data class FailData( + val containsDb: Boolean, + val containsSer: Ser, + val containsJson: Boolean, + val filename: String, + val throwable: Throwable, + ) + + private fun testZipCombination( + containsDb: Boolean, + containsSer: Ser, + containsJson: Boolean, + filename: String, + runTest: (test: () -> Unit) -> Unit, + ) { + val zipFile = File(classloader.getResource(filename)?.file!!) + val zip = Mockito.mock(StoredFileHelper::class.java, Mockito.withSettings().stubOnly()) + Mockito.`when`(zip.stream).then { FileStream(zipFile) } + + val fileLocator = Mockito.mock( + BackupFileLocator::class.java, + Mockito.withSettings().stubOnly() + ) + val db = File.createTempFile("newpipe_", "") + val dbJournal = File.createTempFile("newpipe_", "") + val dbWal = File.createTempFile("newpipe_", "") + val dbShm = File.createTempFile("newpipe_", "") + Mockito.`when`(fileLocator.db).thenReturn(db) + Mockito.`when`(fileLocator.dbJournal).thenReturn(dbJournal) + Mockito.`when`(fileLocator.dbShm).thenReturn(dbShm) + Mockito.`when`(fileLocator.dbWal).thenReturn(dbWal) + + if (containsDb) { + runTest { + Assert.assertTrue(ImportExportManager(fileLocator).extractDb(zip)) + Assert.assertFalse(dbJournal.exists()) + Assert.assertFalse(dbWal.exists()) + Assert.assertFalse(dbShm.exists()) + Assert.assertTrue("database file size is zero", Files.size(db.toPath()) > 0) + } + } else { + runTest { + Assert.assertFalse(ImportExportManager(fileLocator).extractDb(zip)) + Assert.assertTrue(dbJournal.exists()) + Assert.assertTrue(dbWal.exists()) + Assert.assertTrue(dbShm.exists()) + Assert.assertEquals(0, Files.size(db.toPath())) + } + } + + val preferences = Mockito.mock(SharedPreferences::class.java, Mockito.withSettings().stubOnly()) + var editor = Mockito.mock(SharedPreferences.Editor::class.java) + Mockito.`when`(preferences.edit()).thenReturn(editor) + Mockito.`when`(editor.commit()).thenReturn(true) + + when (containsSer) { + Ser.YES -> runTest { + Assert.assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip)) + ImportExportManager(fileLocator).loadSerializedPrefs(zip, preferences) + + Mockito.verify(editor, Mockito.times(1)).clear() + Mockito.verify(editor, Mockito.times(1)).commit() + Mockito.verify(editor, Mockito.atLeastOnce()) + .putBoolean(Mockito.anyString(), Mockito.anyBoolean()) + Mockito.verify(editor, Mockito.atLeastOnce()) + .putString(Mockito.anyString(), Mockito.anyString()) + Mockito.verify(editor, Mockito.atLeastOnce()) + .putInt(Mockito.anyString(), Mockito.anyInt()) + } + Ser.VULNERABLE -> runTest { + Assert.assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip)) + Assert.assertThrows(ClassNotFoundException::class.java) { + ImportExportManager(fileLocator).loadSerializedPrefs(zip, preferences) + } + + Mockito.verify(editor, Mockito.never()).clear() + Mockito.verify(editor, Mockito.never()).commit() + } + Ser.NO -> runTest { + Assert.assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip)) + Assert.assertThrows(IOException::class.java) { + ImportExportManager(fileLocator).loadSerializedPrefs(zip, preferences) + } + + Mockito.verify(editor, Mockito.never()).clear() + Mockito.verify(editor, Mockito.never()).commit() + } + } + + // recreate editor mock so verify() behaves correctly + editor = Mockito.mock(SharedPreferences.Editor::class.java) + Mockito.`when`(preferences.edit()).thenReturn(editor) + Mockito.`when`(editor.commit()).thenReturn(true) + + if (containsJson) { + runTest { + Assert.assertTrue(ImportExportManager(fileLocator).exportHasJsonPrefs(zip)) + ImportExportManager(fileLocator).loadJsonPrefs(zip, preferences) + + Mockito.verify(editor, Mockito.times(1)).clear() + Mockito.verify(editor, Mockito.times(1)).commit() + Mockito.verify(editor, Mockito.atLeastOnce()) + .putBoolean(Mockito.anyString(), Mockito.anyBoolean()) + Mockito.verify(editor, Mockito.atLeastOnce()) + .putString(Mockito.anyString(), Mockito.anyString()) + Mockito.verify(editor, Mockito.atLeastOnce()) + .putInt(Mockito.anyString(), Mockito.anyInt()) + } + } else { + runTest { + Assert.assertFalse(ImportExportManager(fileLocator).exportHasJsonPrefs(zip)) + Assert.assertThrows(IOException::class.java) { + ImportExportManager(fileLocator).loadJsonPrefs(zip, preferences) + } + + Mockito.verify(editor, Mockito.never()).clear() + Mockito.verify(editor, Mockito.never()).commit() + } + } + } + + @Test + fun `Importing all possible combinations of zip files`() { + val failedAssertions = mutableListOf() + for (containsDb in listOf(true, false)) { + for (containsSer in Ser.entries) { + for (containsJson in listOf(true, false)) { + val filename = "settings/${if (containsDb) "db" else "nodb"}_${ + containsSer.id}_${if (containsJson) "json" else "nojson"}.zip" + testZipCombination(containsDb, containsSer, containsJson, filename) { test -> + try { + test() + } catch (e: Throwable) { + failedAssertions.add( + FailData( + containsDb, containsSer, containsJson, + filename, e + ) + ) + } + } + } + } + } + + if (failedAssertions.isNotEmpty()) { + for (a in failedAssertions) { + println( + "Assertion failed with containsDb=${a.containsDb}, containsSer=${ + a.containsSer}, containsJson=${a.containsJson}, filename=${a.filename}:" + ) + a.throwable.printStackTrace() + println() + } + Assert.fail("${failedAssertions.size} assertions failed") + } + } +} diff --git a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt index 70420801c..a524f64f3 100644 --- a/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt +++ b/app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt @@ -109,7 +109,7 @@ class ImportExportManagerTest { `when`(fileLocator.dbShm).thenReturn(dbShm) `when`(fileLocator.dbWal).thenReturn(dbWal) - val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) + val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) val success = ImportExportManager(fileLocator).extractDb(storedFileHelper) @@ -128,7 +128,7 @@ class ImportExportManagerTest { val dbShm = File.createTempFile("newpipe_", "") `when`(fileLocator.db).thenReturn(db) - val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) + val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) val success = ImportExportManager(fileLocator).extractDb(storedFileHelper) @@ -141,21 +141,21 @@ class ImportExportManagerTest { @Test fun `Contains setting must return true if a settings file exists in the zip`() { - val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) + val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper)) } @Test - fun `Contains setting must return false if a no settings file exists in the zip`() { - val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!) + fun `Contains setting must return false if no settings file exists in the zip`() { + val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper)) } @Test fun `Preferences must be set from the settings file`() { - val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!) + val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(zip)) val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) @@ -172,12 +172,10 @@ class ImportExportManagerTest { @Test fun `Importing preferences with a serialization injected class should fail`() { - val emptyZip = File(classloader.getResource("settings/vulnerable_serialization.zip")?.file!!) + val emptyZip = File(classloader.getResource("settings/db_vulnser_json.zip")?.file!!) `when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip)) val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly()) - val editor = Mockito.mock(SharedPreferences.Editor::class.java) - `when`(preferences.edit()).thenReturn(editor) assertThrows(ClassNotFoundException::class.java) { ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences) diff --git a/app/src/test/resources/settings/README.md b/app/src/test/resources/settings/README.md new file mode 100644 index 000000000..13a3440e1 --- /dev/null +++ b/app/src/test/resources/settings/README.md @@ -0,0 +1,4 @@ +`*.zip` files in this folder are NewPipe database exports, in all possible configurations: +- `db` / `nodb` indicates if there is a `newpipe.db` database included or not +- `ser` / `vulnser` / `noser` indicates if there is a `newpipe.settings` Java-serialized preferences file included, if it is included and contains an injection attack, of if it is not included +- `json` / `nojson` indicates if there is a `preferences.json` JSON preferences file included or not diff --git a/app/src/test/resources/settings/db_noser_json.zip b/app/src/test/resources/settings/db_noser_json.zip new file mode 100644 index 0000000000000000000000000000000000000000..4bbb7523cff0acc4bbd2746598f6673285897cbd GIT binary patch literal 5428 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 z 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 O_;{e7sjUBuYX1c!>QLnX literal 0 HcmV?d00001 diff --git a/app/src/test/resources/settings/db_vulnser_json.zip b/app/src/test/resources/settings/db_vulnser_json.zip new file mode 100644 index 0000000000000000000000000000000000000000..64444acdda9f674ff0e0828f59316da73dba438c GIT binary patch literal 6752 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 literal 0 HcmV?d00001 diff --git a/app/src/test/resources/settings/db_vulnser_nojson.zip b/app/src/test/resources/settings/db_vulnser_nojson.zip new file mode 100644 index 0000000000000000000000000000000000000000..56d58a22e2d2568311cd041e4d6619b8224a60b1 GIT binary patch literal 5364 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 literal 0 HcmV?d00001 diff --git a/app/src/test/resources/settings/newpipe.zip b/app/src/test/resources/settings/newpipe.zip deleted file mode 100644 index 1ce8431feb3d4f2b14e770ac7f8a3b7efac07436..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8317 zcmZ{Jby!s0_b-Z+gmj0*(A|w99Yc#qN!QTbAl)xRcej9mGz{IPl0yvLjRHf?_50qR ze!cfTcmHvov(AcVpR+&vbM{(GT^acqArd+|I#Rb+k_OUW(834=_kZ~;)UxX)>lo8KJn0OlvfdIP@} z>8q=P&%42I1s?@AR!#c$!?CWjw<7C#&sDb{mP85uyjF*EZiXi6d3dvBAxuzMX)qxv za3ccsw;HmkLXm9sW0iZ2=Jz9{Y!1uKe^BafK<}v#YPwi(wExj`Bl#h+oqM^ja!Ia| zN@>t_8KOv5>pp)437$5J=z%f!s3&Gg_MQ*ampA=$={VYMV{AW5VXz2#zhpgOH#Q97 z>-}7>V2Nu;%ZJ4!xu~M=vdort6J6|3%<&D=$g%00`IyFJ7nN~EScQkpSlyRaoz>VH zZHgZJMwsOfQV8ugot`Gp2|omHj2YV_q!} zKcHYE#+)D#{FM<9|4BMHarwTMTXkToqs>%9`j z*Av)Y3Rsm9YQDlo6&PqQW~PO`x7JUoASWQ(Wy0juVgp-{?G7C>%W9d2;FB^mrf7;( zxWIWn0pe3-wY3F1LaY`0d6cJ#I*J5-#z&FbQlB74k)xvtt18y37tbhIvDUw2t?kPC zvIH|}SbxRHoXNt&AMb;lDB>8aqT9ka;wqboUn0Ns4lf~C@KDNjR>^rminzj5~jC|r4LW6(=*#7I>atuRLt2$ zAcleh{}?#U#o|oGZ=QEuNX<~tJ-*KPL3utgiTOvLjpx`Enq*a$yo(TGx({bL?9T%# z^eZPx$@!TdXeF&?-YmHJT)1kj@}0X**Q#cgAzW@VD!P~!GPowbSh4r-mPv9qh(53p zJSv8#XS#JkX;A)+-BAE4mhy>QMJ#n@$d4k*c5H^Dgl6j9G2Cj0t;mcQrQ-FG$J5Gse zwvdhk4vXR@rmUsR@7HAFu;El=SPJWMaShh2d)e6hqN4o$MraL|xO|#4WXnU4ih4dJ z7-}xzY-d~hPeT0uDDG>;FYC>1X3%0i-EbubmMMqDQJg4>Sx?}u$AMc7XjN}Ke$UP_ z^Ke937!MnSrrO4vygBl&vhMBL;jD@OY&*CN)VRF3h${3Yh)?{A2H7h)cx6YZS^wV1 zP3_?>u&-6<4*%)z^&Q=Tr7;H!$J};I1-2fPN`( zyN$8_Kt_ndJ=@0)UQS$1@w!ABb^6q=xj*HBrO0cpV3|P6DJO4 z{F(MCLuppu;CWUlg>a`yTm1B4shx1AWn1)gRVkZrr)gX4^cW*1kfT&txYIndZ$EpV z4#<^h=`3p37Br1n`a@X4Q8Z%OmGNm=s10fvU#U$tFssx_)FQKgzu4w!89}LYc6`>L zlc*){<_w5@V87hvSs6Ve0nqv5w8=Cs@ci@)-g0q|U^LMhL>SXUr0)UeyZw3V-w;+& zY6M-c@1LK?cpV|M`V;VJ7y{D0uoLbnp2=&YM=uO+KBD6>z+~As$NzjYGCo`0cnU|D zHj|J;dD7wh>`0^C!PbF}yUTX8Xse%I&O7#lt?vMLUZFA|r;#>OL&L#VFN(X%V~41S zCol&$R5T+)gK2vDz}!|TM+I1!6oui=D_H{^9Gr=QegewL&yEa7it=R4jGTQIB_rp} z7^I0qRH1I@_X|ZLWc|Y6Z2wsb+DGJbrhXc{Jb>gIzi7DohHjKGqa@HT8BVsr6cza9 z9{rpv>YAVy?-X!_p0Va^`Fs9I;-=)(LNIMGg_4V*VhziP4xqJ=AuXl<*Cp8;sL07W z@(FC94*sF!0#rl?WN3hG02!KKML>o+*cm{kh2H>%kF){Zg>+$2#>zfUnQH0a4dmCW z6Q5;gG?O}l4zfcp{NilqNj30)(BYF~#a*cIaA7v~4G_%i1^Taq{AaIr^juMBS&l8O zI{3N2E_^P)p!Brxz+0oRxMrU3QTuBa2AHgt3!j0;nndDdUf|DM7Ri@=bt>G1Dv5Po z;jtFFu~)wc&YE}mjPOA!hFz)+sS*Ci!r(-gWJcgJOY`z!*W?GzY9 zuy|ASw0&2N29=%?^O?{HTA7u9KH+1&uZ`#M&I^d$GAJWY2qI^l(6ipQa>{r|9RbNY z_GDnrd>j8pLjvTViWw=NoLnZ7k&?UdDre#1&}Z2wo3yciHx54twFM&I z1A7Q~t_$aBoJ)w!;OMa%eAvzn39P5_687BSK7+m!&N%knQ?s0*_R^5xmybyaVzC?V z5;YOjuQB8c2-I=i8t5DG>N2Z#P@JPSn&6@BgsO;B_T3cryp!|&AYgGVc5-lH`7z7T zn?bI!00xbqOBX(yEZ*vPkJVq|!PhftTg>Xj=_yVIhvx~YapwxO)Pt!FmJXz>OY*(D&6$Qb z*0;+$mZJhRq~|j{T7*y-e&#yLx_v2%kQcF{YWl^2#|ZIa^zS_& z(ords>%yN`w8tcm(vopmckI0MTX=NHqfDo;>f7V5*>$IsR0jeLW{)RbwTaq&X_%U( zr}u?hJK7Jwx!+2tqA}!y03`w4PVCtjzVwOJeHlI7_W=jt(0U5~ldeJ=q*>u)6yQ#K zX5Br`$(Jp)qg|Ur*nww>cV?U?`RKIEPci8qwGcn->(q~f*PUJlFQ}6cUo3=fXX3Ry ziW>Hub#s{J3u_i#zSK#2tIcH>xwlu1pFiT%Kso>ipp)D35^_I-gxi^03n}}3)jku) zyOgRHGxzp(vyzEn49#2ur=c^iol7R2%OY{6!!Pec64EA5yAGqFE_Y4mFWm|+sJq&( zTB2``4bu|1nigBJ9^hBNI~yQpE(vEA2TWx1u`%h};k1i^KWKq^3IC_W=IJ{*4z9_Z z_kfups)1z( z{n1RTQ|`K_0ZUX~o{%cc+T-?X8Zpm=#)OFDm6f|2|BH`MushlHL*|;)?aPFdQnxnz z1YmxEGS`(DewkybtHN!y>&D4Kz(ohNw>1_mNo{eUD5HhAl5fWSu3FSrX(b?9HSdv( z-#dA$Ip{xo3h-d%ftC2 zRkbXRW+xF9-DA82Bix6>k!EPGyMT#*X0`YB9YX6(9^d%=`p;8Uf76RT0vbxk*s$?k z`KgS@)3dx6{*!P=Y^m#=_(7dFQ=L#>B-jDYq}l}O;NJ35gpqJM6-0v9D3QU)Z>U$g z0opOYW3I@V@5`@iZ2iAa*@{M&sl1v?NfY*Ar~J4hg{b%nc(kd-3r*T43%q(}m682i z1HRWM_MylZRl3+%c&lb@$&~!HL*LW7@(`F-+n5v&A4ndrS2{jx##C6d zXj0-_Oifc%qAP-_@=}eP_tvT3HVLwtCEXSlvRH6yBu0-Il(*;`s9BmSV|5PIx)vyj zyscy!JH5|Z6(dV|IHk}nG@Yj+$}Li+HFgop!l~7SGuYDAkJq82(K#UkbQ|G@{!hMw zoVcx2Si10{_$g-_$vNO*_ESxLd3vy-Il(s*Ka3wHa&6a1>79_SCUVe)b19!#CSCSd zS9D5ClK0UF9{~yYx$^h0_w~eE_1gr-N>wYI+6=yh9`m2% z>@__*qw*OR&-7R7K)fOPX}CJN!17SzidQHZ6^iCj1jEm@n8*@;%5^TmV|&+?2i}-R zvus+HbvtXSEz4!!Zj`e4b6aI}WyEYReH*+5LI~}wB=%dhRg=rri*p$QHXfAC6K!L1 zoH{$sPN}I4$aE&_>rbq8T$H^%rr&xfEVI`9+Q)%QXP;tAd?le!)e8}M(pQqbyy((T;3kv$U@g+A1uGvGQF_mrH+HX<#wa*5I1qvM zhF%FI-e%U2wp20QTJLYgLy>y27DJ>ZZyV}lSPkY~bzh;L^LG9Ya(nq2-Rq#PL*800 zHC^47=QBiee8B}08lmbn>S~PW&>{%^etiLNv-fE`ywY@)&_ib`3_i|ZOp8JhIwLJ8 z|I**-Pqs|Ao`jjR_Z=(H$k>`+7Mqbj%Eg1io}Kd;b> z0fy?O)#KWF&&_(-;ZDl$#7(E9Lw-UvIZ0MVGQn!oNdZfB*7o?O#&xIbFea)r z_Q^nh4)cPSq|T?>#Z4XCZ){@~93#ew$=y2((gK0OPe7PwVAYdMK;+ z=-aXCYI`S>dS)l_1Pb>O%g^PCw**jMrh~y@uKk!0%l42~YQ5{{_B@>mCFKUt6q_>_ z+KvOGs!^6MPtaw=u@a@I$v<7Ui+#vh8W?@PVkYmX$$T|Q7wiLE+Xqf%uQMak931JW zqau2jS|0q2yyafGlz7)8ES6S$p}Ds$5N?}MDiwmI_Y*DC6CW5?@j=vr#4r;IztS8- z*f~Q);8n;_nmt8SEn%<8giTi0F!Lq?W10ku9x?JWEMrO~5OjdXlo54GmJr90@VI%# z^`g^s3p=BXCOYo%s9W9>3F%A*ZHH`|Zf$-=DzdE?5a?_#G$oL5intdhR#M(*DuQv{wT{dxD1uC z!-b!4gzqb=Wf*~SeRUsL{eRI$Gwq;&=t}s2Mg5dhpoUeLC@iAqSv9e#*;0lQ3%G!Q z;*!0Km~*`cYO6WQAfNY8T@(=VCJtuOY_fC8`{D)Xq>V#v=kdrl{f1-ykCQ=Foc1ip z3v}|~ifz5lbZrTJ$qMbm;R+7KEyNjePeab^?EPXz?{qRXQ3>*TmGtX{41yI}N4Jgx z=2jLJ81`EnJ^Pe9?z>1DP4w>P@+8F5A?2ZfN-_37HI=foT|0D7E?LF3Md6)GK{5~v zQ@Rg+^onO*ps}$ye-@33I4M(?RwMNMYfGZFS5Z8bBO`}bxdF2Q}UgpvfJ#bkCsb%RtEJIgzI)~{bm+T=SUTFgb5$?<}j@TO$2zPm;IxKp;O zdOM-t4(L4%%{Lu^PC=@K2ScT_$)MPk17CY5=O4`XkiFX&q92cPR|03g{N zZg*TheXfTc__>BF>ln_QEcnKz@V9(HwgSAz*K$Q(GsLpOj8lt-f>|zh>~YrOpLH`o zi1!Y}ab@J6rB6ZyzW6s!792YVhg7^8aq8bLqKE^T2sPL=?CO`4bY5xNdp75Gv|jYU z&Q$`|jV!U+JH?z^BowOR@7%T@9oYT_qHwo=oqPK5urHfc> zxC@{j#I2yVL>mipwQPy=N*A>n4r%IDbpRV4UY+E_xX>B$04C}a2 zsMo<2$GHZIoKQ7zQX3BYN()ImDE6g8LFqReGh8YG)^t+2WUg5g(_N32*Big`=pdo) zo=ys--2;g>FOHTJlj>*lMjck$ZOwAC#@WZ z25@%b!N{#8NNKgV>LsuZrL3^d9hbgS^XP%%8{@#^^3}DOpA=hNHy67!)CR3J-mfZ( zgfbg%U3y+AqWkPo0@j7$A~mIw7xL0k1P6r5^p}YURvk9?lMW256?Z$EJ;zHFMXi|+ zZAIQ{FIn`mCKib|T^2!LE{*cJ)TW1H^&fth%|4Ftv4*~F8mC~6AYx?>8gLR$)>5hD zS0Fb{kNQ63?kqmP)a-5k0!arSrcmu(+=3+_5TfQGEf_R` zr&A-Vy-+(put3b(_fpU_EC~0ky&+LWxY93JnCOCq{=;QMsYR|XwdF0Fc6Imy?*SyQ z^K;b?WxSyQEZZ6UVM5Yyu=uiz--t2j_!CJ*Hq|Wt_PJ+3u@Glpnt(C(H@vjcEgLR@ zR9#b8wHwu}?Ocm{2wsZsqDoo8Nn6rS8-I#I7Jc4;M=N*kN@bhtLk&26xT zVC2&aj+yqD#SQ2$HM)Eb4()Rs&(Dk$r9JmXHb>GWUEYGTJ=K-x58EseoPjGDzkSRO zC}LBe+QUpPUG90ty60X~&7)Y^+99pT=F-WslqzWMIiZ?n6*vpi%6ODgS4Kl0;^@hk zBmY%-`S27;{r}qy!~JFOyBp?e;pS%JVCCws)8$AhOGu4qc_zl-Y&z5v&iv{LKb*5) zh8SD;Z6T=%!2mJJgt^1@%CS>B-(iN(H9g96Y{D;JHPqF25|A{s)W53J`dBBHdo?up z!s6mMZi;iff9zzP`kwkuJ(_aZEu>8KtAm7-_Xm+pk|YwFClQa+Q&kCpIc5#lP}53T zKVzBl2~Pd){vcI1@`NAPQXd{q&-YMn{gn`&iJd0&iBIJxjuQz`TgGMb0*G|UE!5x&45xGFp0xU>dOD61xrT^@Gd1(tZHxUB zv*HNvE_;HU=dVr3E|g@Du%ukp zx*aczN1hj7^0!VJ0!4U-+dj0Us0HmVzNP&!%!wh-^+L}U?R6AqBDl=-JJGiweL{5c z6k>Za7m1V%ncV%F;_i3(Ym@k<=JUOi=lFOP*8Z0_#lgk+!#-2HJg?gE)^&!9WJotq zvFgcy3Wn6axO7u?3blh@2{>i*RLUj^f!UD(MoJ&Xht&SGGo9h^XPbX5= zaDeoHTZeH8`YruK5ef}eIzWUaF+UWEJD+OI&5+{(|}!k@{aFeFNysH`(tyS=xt=@9x}AqZ25 z_ZAH~_9X8g*Eo_9b6W{-xUpUDq1sS_(Grn}?CnRUDmSQql{a0HZ)Fy6PPq`*iOxx`XGY+Az$Y$sf zlfjlo+Sqhzx}x|5ha8olwcRW+m0>y8JUZxcxH+aa?ev_;w>G1%qvIdle}2h}A13a# zN_E5!t9aS&lG_b z0&%lWBS!xb+T`Fa3gPKXEHtr^nSO<&107@$60|=Shra(O83tGM`Ua_J+6@4yByvB* zR2T}lISVv;8~cQ*Yi313cJ_9)WiR0WQrw8)&~(W&B&0YDq+b;G~|F_@YG5G{81rT+!Ia@nE) diff --git a/app/src/test/resources/settings/nodb_noser_json.zip b/app/src/test/resources/settings/nodb_noser_json.zip new file mode 100644 index 0000000000000000000000000000000000000000..7881f939b1cbb33e98318906a029fd6356fccc53 GIT binary patch literal 1410 zcmV-|1%3KZO9KQH0000808NR0SZL&>KmY{*0J{tT01yBG0B~|;W@U0^ZewM0E^2dc zZhcnUZWK8XedjYq;teGOL9pZvv8;BbWm#?V29f1%SI=1Ewi{nEnV|gpp0fKoG&~Td zeW@x}ojRw!Kih;_8WPco@GVJCLbq~lAISH!?-H!D`sMqhQx?5WTJ=DN#Ec z$;RnwZAg0YEm0!gY_xB4d`kyLhA5N~@zjyH*NPnctSsVa=epFvWlL>EUIhI}qI03x zi$1YAjh6+0TVr-;$Ow zKwhFpx;9ri8wK3}a;`mD9PrzhW-=F|^IUfs4paWQAV%6<;+WjE45kBSCIFJHeV=+`2LkCIDAF8<)_dJA6HK==AUn$?w)didYo}KgKQ>} z;2iLxBE>2(W~8OKcj_$1gR11C&W9Byt-TC;X2-rt%Ojnq25TeTX7M?*5Fic+Af(+6 zi7PPbNya$H+}eS@gP{Npgtl_vZhULuytoHU_s*z3n^)URZF98*q6`9kq-7nV^x$#` zsz&fc~C?R8C6xyDpb|mmlbLO*=mlxZK!mWfTF?$Dsif%sq`ToC~ zkGJB}=bKw`^Y6zGf4%>kgZEjb_{qsFF zL}v2ZR7XX4xb7k#eqKtwvkp-pXm#X)qy@gDs8Wn5$1+i6D<*CUZc;_}8WLJTxDJIk z$cM}~@7}$hG44Q-3wq3zDGcs{E;Q9Yte_Y3kO$;)2XUv9UB5Q}nUu%a0ZWg0_! zF%5nG^y%Y2w>OjiGo50{#`r>3tqeP5`pz?N*?so(KTt~n0u%rg0000808NR0SZL&> zKmY{*0J{tT01yBG0000000000000000001Ra%E;^a%FB~WpgfSb8l`?O9ci100001 Q0096#0000k1poj504D>Qy#N3J literal 0 HcmV?d00001 diff --git a/app/src/test/resources/settings/empty.zip b/app/src/test/resources/settings/nodb_noser_nojson.zip similarity index 100% rename from app/src/test/resources/settings/empty.zip rename to app/src/test/resources/settings/nodb_noser_nojson.zip diff --git a/app/src/test/resources/settings/nodb_ser_json.zip b/app/src/test/resources/settings/nodb_ser_json.zip new file mode 100644 index 0000000000000000000000000000000000000000..aa8316c6ca38c2c5198f21f7c2fd5ee18a9eb604 GIT binary patch literal 3177 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 Q066()!~Wcn)BlJ51y#WA{{R30 literal 0 HcmV?d00001 diff --git a/app/src/test/resources/settings/nodb_ser_nojson.zip b/app/src/test/resources/settings/nodb_ser_nojson.zip new file mode 100644 index 0000000000000000000000000000000000000000..437d30d8bb8b73e9257a4b58a4f3556f246568cc GIT binary patch literal 1789 zcmVaA|O5E^}pcbZKs9 zb9GleY#c=xo!GI-uj4p&oH&?-h#(4dHgQCv2q}alRxIHJVOJoS%G+*rC)vb0I}m9Ru8Y8Fv%&VemXWrlqp+J^d7aU} zJDq*_eIGx!9DC6QB9h!`ZtrHoNUmbh#P5VB+Np*Lgi|`P{aquGG;42Y9Z8Y=a>m^M z?W=$PnqYh0VN-1pWwM(x$Cj{T7E7)ZF0xQ*-q1m2c~hw5F8he>b!6`9kHox&A5oKA+=&Zxl2kOw+(GI7Os&jvV?;de8cNt$XX z6Z^wgV$VgwIvz&c^-{^(DwO&ugQrrRW+`@yWn7nrTeT~Bsx1Yx6m4>kV>5KPV#mQI zOa83dsHRE;qAEZkn!0+$L^WHEyB4B5b|4gHr+hh?Mxs|2!49`OD#f;uOoD3k#Yn}9 z(5YjJhr$V-SpehMLOwGXLJAX%pOOh7$_J61aFR#LI)9TH$BzGhNuI%CcBw&I8Hw2d zhfEv~TEZZ8(m1w<1BWaPDWpi#D%hcHh%9mxOh*}|!m;U#=U3BLR~ukZ`eU2@AkgG9pp*C?tir6}KVszeTcKnox>KT2t zG&)thjQ`F7g&j;SjtN=UnH&2NMvSv1r-!Bpf1j7GW5+_>NvM=9k3|9A^nM;5 zp&@>w3%VYAx^*s(eJjrKoLpOl4xX&~Ecg}S+`F=phcl3C1Hp%TmX~d0QDg&DR@K{1 z_aHb~eaIoH0(h?NCBSoijZYP!Pkv_Dk%76dqb%H`CqIAl`)hyy32R?3t=+KM!Kzys z+2W@UA1ojI;*ZZKNTRQxrGRV|W|0Wk9{Gfokiq$DH?O|Ebz_@v-rm~gTW{UCwsG}M zB)~G`UE3KQfqQLKbzB*kFAS<9=*%re94i4jLIBwXXE+_^^1+PQFk;e+iFXa<1qf(-34`o}ypX;#3om_dOv6TFw zhG_$$*_3AP@z>w}^YLewfa*qxDh1wVZ1ORLxYYGhj4MJs=kuSNgtm&vQe6LNUM%R| zBIODU=7-1V&>4a(6et!{9fol}RtXx2UEjCKE;h#Dkk*8^3Jq3?vd=;RI1uC{t zTF!DVjiItQFQ_QHflPf_RN$P&W8V3Ad1}hC$n$&|e5X^e9KA--$WeZyj_6r3u%aFu zn<~1egHg4y8rJl@6&=#_MHQzISgi5?`OYc#IXm9xQlm+Bjji?lqvCbt(~1!tcpW^h zkU~7kIEzU0@VJVfiVwCk?R0GytYq`0`Q`r8(A2@ALCjw}vW^^1gLX(?382TmPwPG@ zmVHl~5%4*b)J57pIce1$XxwyV&$3m|_uQYq#ur3YGHgn6R<^)fdBrbN17An$jh(#j z4!H^aiSg8Va`BR-I7%cNW5i&t?o}d@u)sN4u!J=l2b_pX~kzP)h*< z6aW+e000O8O^JS3j?uFWdj$g-o literal 0 HcmV?d00001 diff --git a/app/src/test/resources/settings/nodb_vulnser_json.zip b/app/src/test/resources/settings/nodb_vulnser_json.zip new file mode 100644 index 0000000000000000000000000000000000000000..84f97aac0cfd73aad44a0bfdb47e0525e5cd55d6 GIT binary patch literal 2734 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 E01*6)NdN!< literal 0 HcmV?d00001 diff --git a/app/src/test/resources/settings/vulnerable_serialization.zip b/app/src/test/resources/settings/vulnerable_serialization.zip deleted file mode 100644 index d57a5f8d0150cd11dec35d7c29ed2bcb9e65a774..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3536 zcmZ`+XHb*d)(#yhDpd?dLKO(Th$2W8L3-~>umhOGllAlo0>`Py*abY>n3H!!d^x1XY9> zKt&K94gp@yUJgR`w!depA9d<5lFz4)>zE6Cwx-L-&Ghs7PO(j7@yJ`~0mnvm#O)a2 z`Glju#p3xzO@3G-B}eksQpU^+c6&LjT4BPvgCTQ3***@=FVrFO#cZLJSCR5FdTD!u z_h4w^?2bQkGeZbcW(ixT{Ydzo|SAS^O!U?L2Jw5Kj;v?jOsY zYuaa52x3+Ig6ss2|6AMsh5#V}fHo1z-7O$n{?qF1-Ct+t3}<^U3G2})o^by8;WNVR zhRU5@II!m|ZCMwTFAvW7v5oi(KkmZ+`y$czqR7jW3;!yf5Vc)&o>=E9I4q=O1J_M! zYCx4;76RJIELx)MSPDHl6U&ma~fu`(A%b&q?J|teYAl11FlAcgR)2?bt0h5J;CLrBt|wE;6Chqy+!Dt0I4n zZMHU8d~SQ1Bx=vd5{3>oa8#i)V zMDS@FrS71*x_Ao5^^*GeQ4&_k_AHLxxq8x8#TgE9-cBtDwC2FVH!5Rky~|Hhr~*>T zyfEII2ExG4^@#C$zXS)zHRbO#0dOxDxXw=vwfOXT8TxV6xRLBGy>68fz7F&8D~K0w zAs-7NYwkZ9*@d!3HC0M1eh!J$t&~XaE=yX(6usMUOVK2O7Hk$|0%7$>Pb5qzjPJte z62FQ&AiYO_UY-6O&rGi2@Sr{>uf z3NzNk`9qZX>J1f=6CIB%&1fH7&6!GPMr~_>mvNO}Ffp16F4iNg^zc>AVrOd4Rd*s& zvp4~uw75v>y$2)kaX3=ZeGSUBv``zsAi?s`l4G5liV<~^bhD&M$s(o8T)9goo#&t~ zfIXnD+}jH}c0>AwVD$W(qsZ<{411==Is4C^36YO>9{l9mn@0BlA&w*SKc)Lp_Li%H zb<4{1`e}MveMBrd{1#~aZGVlqe)pvN6nU$yI~HXMY4v}ofT(D&soi87>Ja;_U*Sn5 zHuGA=jbb3Xz3D0Hi`#KmSR1+cpXldLTc73Rxr$pjfoHDzwKtz-JP}fqg#A!nAzeGs z_gnpF&~TjxV%rOOBdf{XwmfJs+ZTgmG$b=rf71lcoj&dH7PuE8Sc}L(MUa2-7#>QM z6|WaL;DP=SJv3ZXUt3+7{*LWEn;dp3TCv>;UVO?SyRM6!DQ^+q)Mv6kXqfkhlAAZ^ zb@M}PN^^VR0#Bs*)nL|4(3;RRQ{gC^pee4Xs;Cp3`&o3hWwrR!Wc>&{z|+|0l486k z_;Imo+J+JhB}>Ktkt~Nq_jbsQ{Q+;!|5VL3;!d7h?GXesojDB>bx|>iK(A8U zJ*n){y>ehG6PdsGP*oIej+o@F-h@g%JUUtf!rI?6RjMrMD=c_;e#phLR5Cd7Gtj;O=Jxi5K!V9?Rh)Q5icXF)%Q<*tv6kU6NkgmUS@>@E=1#GW z5wgoj2{tQ#ix}@eSxKHVy_sV%DX@Eee>#X2#ew3{DW5Dh8T^X5Ce&@pCR+Pa9txH8 z5Xc|2T=g4YY?(vK4b88&7oAC-`ny%$Kl<=4>r19mvnSgyaDW)7f#I{8(M0TmXNQ6Q zWdTMODgJa;Q8qJZ?n||Qf?372hKgQ&ERxP6QS^KLBu%i&tyAM#V56)CdZB_dt35v8 z+h%I_&D3PTuibVs5S@aMMw4|@TA{*yZP`1HbW#I68n@@v=NCSKUnh`Q0X9jl{Knz* zF}Fiz#E?OtL2Wm9>v};^9*XCkf%8RkG&CCe!z%-;z%Xj8$td#YOnW zcR$|=2MxK~9P~&v_||3fQ){vue7nBxMfcjSm00Kho(FNoc1D9-BwUrG0D$i$0N^U& zNA%b8z}Lag&)LJtH`efBFM|SOi1WdYd6l&=8rfw2#8+H-Lh>3$eb5$?$ zaFCHIG$XdqA^xYd9*>1nA><=Bx^3Cxndv%DlJm=|H#} zTuncf5(E`49@GqKr8i5FQIC2MOCv!y1e)av1NKK^bcM2mRXPe~Y|KiIyS{q)QRXI( zfuxjss#GmEI`0?`+I-;%{j&zv{YcM!H+h?bgF8*G$)~PA?L|ubB(l#ZQZ#*~!v^?; z@qxxZKc$IPbmNv- zeIm9I*6lPYcK;1|CN-*1BiiV`X`hB|PDY?e; zpZ&UWn;nubxx~CSY~lN=_+fHO;Hc|p25%EYCQ)=HD?%n#RPR{CR_6JyR_#1y+B8#? zP^2w6y)uFUDXI#UfiQ`*1%36S-bF9GHGbr1CQ9)jt6!3t$aq#>^CR4O{aamTzUee` z^m~r2?|9Q!B)@dndHc=cV0-&N0Y4g&nG9P@bo`@ch_asHF7>}S!iTA&^^+!6uTA5W zbR(T|nCMv6;kS4vvg@2#QIC7=F%Dpl1bW(G^~CCdaEhd;-px#6Ti49^nx50!cY^N= zV))WGk03mU&nACAz$$7idn*}0<-?|WTL-cO@@o9N!iBR>s4cu{ZRcodIhSscc1=j< zF~+>24K2L)$$j9!%`zo!Hs(MMzxUIHwr--2gFFrj~P5<4RzQdQnEzA14e(#z`nS<=+ zG;~0tj_xdJWF|dwhe3B9`yk=@SdlFfpICYm$dWZZP3a|Np9M1fpuEsaCZ~$S6?#3` zIRDbqma`o|$dZIAr5^HKc98{L0xSv-8RxWCZ|!? zu3N)oLsaeTv24ms+2XmdWE``0>E+SLKCR(w&=NgEbM@*(XUK0kK52TjV%~S<%lconx2lu*~evpwDlPE0yHj>RTF&Z#XY1eFkbD8TSh^4((R-X^$W|_jcd2nKBow=EXu%VMQ>O{nHfd4i9a1k2ZIYKYiznh)CEfoNKVIu+%#tzpW zekIMx^V#HI_$T-OZ=HW}g9$lolz(&o)kyzY;d4F2oLcL@RUjijFG)=J5Cn;(IoJLJ D$x)Jd From 7da1d3001037ec91baf7a99980deeadf7d0f1c9e Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 30 Mar 2024 18:47:20 +0100 Subject: [PATCH 08/10] Expose all import/export errors to the user --- .../settings/export/ImportExportManager.kt | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index 5558a1b37..85370449a 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -27,38 +27,34 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) { fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) { file.create() ZipOutputStream(SharpOutputStream(file.stream).buffered()).use { outZip -> - try { - // add the database - ZipHelper.addFileToZip( - outZip, - BackupFileLocator.FILE_NAME_DB, - fileLocator.db.path, - ) + // add the database + ZipHelper.addFileToZip( + outZip, + BackupFileLocator.FILE_NAME_DB, + fileLocator.db.path, + ) - // add the legacy vulnerable serialized preferences (will be removed in the future) - ZipHelper.addFileToZip( - outZip, - BackupFileLocator.FILE_NAME_SERIALIZED_PREFS - ) { byteOutput -> - ObjectOutputStream(byteOutput).use { output -> - output.writeObject(preferences.all) - output.flush() - } + // add the legacy vulnerable serialized preferences (will be removed in the future) + ZipHelper.addFileToZip( + outZip, + BackupFileLocator.FILE_NAME_SERIALIZED_PREFS + ) { byteOutput -> + ObjectOutputStream(byteOutput).use { output -> + output.writeObject(preferences.all) + output.flush() } + } - // add the JSON preferences - ZipHelper.addFileToZip( - outZip, - BackupFileLocator.FILE_NAME_JSON_PREFS - ) { byteOutput -> - JsonWriter - .indent("") - .on(byteOutput) - .`object`(preferences.all) - .done() - } - } catch (e: Exception) { - Log.e(TAG, "Unable to export serialized settings", e) + // add the JSON preferences + ZipHelper.addFileToZip( + outZip, + BackupFileLocator.FILE_NAME_JSON_PREFS + ) { byteOutput -> + JsonWriter + .indent("") + .on(byteOutput) + .`object`(preferences.all) + .done() } } } @@ -133,7 +129,7 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) { } if (!editor.commit()) { - Log.e(TAG, "Unable to loadSerializedPrefs") + throw IOException("Unable to commit loadSerializedPrefs") } } }.let { fileExists -> @@ -168,7 +164,7 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) { } if (!editor.commit()) { - Log.e(TAG, "Unable to loadJsonPrefs") + throw IOException("Unable to commit loadJsonPrefs") } }.let { fileExists -> if (!fileExists) { From 2756ef6d2fd998cf30b689db3a60fdc32ccf3c3b Mon Sep 17 00:00:00 2001 From: Stypox Date: Sat, 30 Mar 2024 18:53:45 +0100 Subject: [PATCH 09/10] Show notification when failing to import settings --- .../newpipe/settings/BackupRestoreSettingsFragment.java | 8 +++++++- .../schabi/newpipe/settings/export/ImportExportManager.kt | 1 - 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index f4080acd3..20af8c150 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -217,7 +217,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { manager.loadSerializedPrefs(file, prefs); } } catch (IOException | ClassNotFoundException | JsonParserException e) { - showErrorSnackbar(e, "Importing preferences"); + createErrorNotification(e, "Importing preferences"); return; } cleanImport(context, prefs); @@ -290,4 +290,10 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { private void showErrorSnackbar(final Throwable e, final String request) { ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)); } + private void createErrorNotification(final Throwable e, final String request) { + ErrorUtil.createNotification( + requireContext(), + new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request) + ); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index 85370449a..843806b80 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -1,7 +1,6 @@ package org.schabi.newpipe.settings.export import android.content.SharedPreferences -import android.util.Log import com.grack.nanojson.JsonArray import com.grack.nanojson.JsonParser import com.grack.nanojson.JsonParserException From f0db2aa43cbfa4d0523e8571042807427c2be2f6 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Thu, 4 Apr 2024 11:38:57 +0200 Subject: [PATCH 10/10] Improve documentation --- .../settings/BackupRestoreSettingsFragment.java | 4 +++- .../newpipe/settings/export/ImportExportManager.kt | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index 20af8c150..97df1549b 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -187,6 +187,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { throw new IOException("Could not create databases dir"); } + // replace the current database if (!manager.extractDb(file)) { Toast.makeText(requireContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG) @@ -265,7 +266,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { } /** - * Save import path and restart system. + * Save import path and restart app. * * @param importDataUri The import path to save */ @@ -290,6 +291,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { private void showErrorSnackbar(final Throwable e, final String request) { ErrorUtil.showSnackbar(this, new ErrorInfo(e, UserAction.DATABASE_IMPORT_EXPORT, request)); } + private void createErrorNotification(final Throwable e, final String request) { ErrorUtil.createNotification( requireContext(), diff --git a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt index 843806b80..9a0d842a4 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt @@ -67,11 +67,17 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) { return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir() } + /** + * Extracts the database from the given file to the app's database directory. + * The current app's database will be overwritten. + * @param file the .zip file to extract the database from + * @return true if the database was successfully extracted, false otherwise + */ fun extractDb(file: StoredFileHelper): Boolean { val success = ZipHelper.extractFileFromZip( - file, - BackupFileLocator.FILE_NAME_DB, - fileLocator.db.path, + file, + BackupFileLocator.FILE_NAME_DB, + fileLocator.db.path, ) if (success) {