From 56b565cc51b2d304eed390c4e8d2129701fd11a7 Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 7 Jan 2024 13:35:44 -0500 Subject: [PATCH] Allow permanently trusting unofficial extensions by version code + signature Closes #10290 (cherry picked from commit 6510a9617a2b5b5389cb5776a2fb91019206f6fc) # Conflicts: # app/build.gradle.kts # app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt # app/src/main/java/eu/kanade/tachiyomi/Migrations.kt # app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt # app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt --- .../java/eu/kanade/domain/DomainModule.kt | 2 ++ .../source/interactor/TrustExtension.kt | 27 +++++++++++++++++++ .../source/service/SourcePreferences.kt | 9 ++++--- .../java/eu/kanade/tachiyomi/Migrations.kt | 23 +++++++++------- .../tachiyomi/extension/ExtensionManager.kt | 23 ++++++++-------- .../extension/util/ExtensionLoader.kt | 20 ++++---------- .../browse/extension/ExtensionsScreenModel.kt | 4 +-- .../ui/browse/extension/ExtensionsTab.kt | 2 +- app/src/main/java/exh/EXHMigrations.kt | 20 ++++++-------- 9 files changed, 76 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/eu/kanade/domain/source/interactor/TrustExtension.kt diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 0636489b4..de4c21a75 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -21,6 +21,7 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSourcePin +import eu.kanade.domain.source.interactor.TrustExtension import eu.kanade.domain.track.interactor.AddTracks import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack @@ -170,6 +171,7 @@ class DomainModule : InjektModule { addFactory { ToggleLanguage(get()) } addFactory { ToggleSource(get()) } addFactory { ToggleSourcePin(get()) } + addFactory { TrustExtension(get()) } addFactory { CreateSourceRepo(get()) } addFactory { DeleteSourceRepo(get()) } diff --git a/app/src/main/java/eu/kanade/domain/source/interactor/TrustExtension.kt b/app/src/main/java/eu/kanade/domain/source/interactor/TrustExtension.kt new file mode 100644 index 000000000..f6da6613d --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/source/interactor/TrustExtension.kt @@ -0,0 +1,27 @@ +package eu.kanade.domain.source.interactor + +import android.content.pm.PackageInfo +import androidx.core.content.pm.PackageInfoCompat +import eu.kanade.domain.source.service.SourcePreferences +import tachiyomi.core.preference.getAndSet + +class TrustExtension( + private val preferences: SourcePreferences, +) { + + fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean { + val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash" + return key in preferences.trustedExtensions().get() + } + + fun trust(pkgName: String, versionCode: Long, signatureHash: String) { + preferences.trustedExtensions().getAndSet { exts -> + // Remove previously trusted versions + val removed = exts.filter { it.startsWith("$pkgName:") }.toMutableSet() + + removed.also { + it += "$pkgName:$versionCode:$signatureHash" + } + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt index defe79400..5fa9cdad9 100644 --- a/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/source/service/SourcePreferences.kt @@ -38,13 +38,16 @@ class SourcePreferences( SetMigrateSorting.Direction.ASCENDING, ) + fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) + fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet()) fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) - fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet()) - - fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) + fun trustedExtensions() = preferenceStore.getStringSet( + Preference.appStateKey("trusted_extensions"), + emptySet(), + ) // SY --> fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index ee78460a6..04c6950a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -329,7 +329,7 @@ object Migrations { } } } - if (oldVersion < 95) { + if (oldVersion < 96) { LibraryUpdateJob.cancelAllWorks(context) LibraryUpdateJob.setupTask(context) } @@ -377,13 +377,6 @@ object Migrations { uiPreferences.relativeTime().set(false) } } - if (oldVersion < 107) { - replacePreferences( - preferenceStore = preferenceStore, - filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }, - newKey = { Preference.privateKey(it) }, - ) - } if (oldVersion < 113) { val prefsToReplace = listOf( "pref_download_only", @@ -410,7 +403,19 @@ object Migrations { } if (oldVersion < 114) { sourcePreferences.extensionRepos().getAndSet { - it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet() + it.map { repo -> "https://raw.githubusercontent.com/$repo/repo" }.toSet() + } + } + if (oldVersion < 116) { + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }, + newKey = { Preference.privateKey(it) }, + ) + } + if (oldVersion < 117) { + prefs.edit { + remove(Preference.appStateKey("trusted_signatures")) } } return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index e0a06cf96..8768134b9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension import android.content.Context import android.graphics.drawable.Drawable import androidx.core.content.ContextCompat +import eu.kanade.domain.source.interactor.TrustExtension import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.extension.api.ExtensionApi @@ -30,7 +31,6 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import logcat.LogPriority -import tachiyomi.core.preference.plusAssign import tachiyomi.core.util.lang.launchNow import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat @@ -46,13 +46,11 @@ import java.util.Locale * To avoid malicious distribution, every extension must be signed and it will only be loaded if its * signature is trusted, otherwise the user will be prompted with a warning to trust it before being * loaded. - * - * @param context The application context. - * @param preferences The application preferences. */ class ExtensionManager( private val context: Context, private val preferences: SourcePreferences = Injekt.get(), + private val trustExtension: TrustExtension = Injekt.get(), ) { var isInitialized = false @@ -306,18 +304,19 @@ class ExtensionManager( } /** - * Adds the given signature to the list of trusted signatures. It also loads in background the - * extensions that match this signature. + * Adds the given extension to the list of trusted extensions. It also loads in background the + * now trusted extensions. * - * @param signature The signature to whitelist. + * @param extension the extension to trust */ - fun trustSignature(signature: String) { - val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet() - if (signature !in untrustedSignatures) return + fun trust(extension: Extension.Untrusted) { + val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet() + if (extension.pkgName !in untrustedPkgNames) return - preferences.trustedSignatures() += signature + trustExtension.trust(extension.pkgName, extension.versionCode, extension.signatureHash) - val nowTrustedExtensions = _untrustedExtensionsFlow.value.filter { it.signatureHash == signature } + val nowTrustedExtensions = _untrustedExtensionsFlow.value + .filter { it.pkgName == extension.pkgName && it.versionCode == extension.versionCode } _untrustedExtensionsFlow.value -= nowTrustedExtensions launchNow { diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 63500d480..728773ebe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -7,6 +7,7 @@ import android.content.pm.PackageManager import android.os.Build import androidx.core.content.pm.PackageInfoCompat import dalvik.system.PathClassLoader +import eu.kanade.domain.source.interactor.TrustExtension import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult @@ -40,6 +41,7 @@ import java.io.File internal object ExtensionLoader { private val preferences: SourcePreferences by injectLazy() + private val trustExtension: TrustExtension by injectLazy() private val loadNsfwSource by lazy { preferences.showNsfwSource().get() } @@ -48,8 +50,6 @@ internal object ExtensionLoader { private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" private const val METADATA_NSFW = "tachiyomi.extension.nsfw" - private const val METADATA_HAS_README = "tachiyomi.extension.hasReadme" - private const val METADATA_HAS_CHANGELOG = "tachiyomi.extension.hasChangelog" const val LIB_VERSION_MIN = 1.4 const val LIB_VERSION_MAX = 1.5 @@ -118,12 +118,6 @@ internal object ExtensionLoader { * @param context The application context. */ fun loadExtensions(context: Context): List { - // Always make users trust unknown extensions on cold starts in non-dev builds - // due to inherent security risks - // SY --> if (!isDevFlavor) { - // preferences.trustedSignatures().delete() - // } SY <-- - val pkgManager = context.packageManager val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -261,7 +255,7 @@ internal object ExtensionLoader { if (signatures.isNullOrEmpty()) { logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } return LoadResult.Error - } else if (!hasTrustedSignature(signatures)) { + } else if (!isTrusted(pkgInfo, signatures)) { val extension = Extension.Untrusted( extName, pkgName, @@ -280,9 +274,6 @@ internal object ExtensionLoader { return LoadResult.Error } - val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1 - val hasChangelog = appInfo.metaData.getInt(METADATA_HAS_CHANGELOG, 0) == 1 - val classLoader = try { PathClassLoader(appInfo.sourceDir, null, context.classLoader) } catch (e: Exception) { @@ -392,13 +383,12 @@ internal object ExtensionLoader { ?.toList() } - private fun hasTrustedSignature(signatures: List): Boolean { + private fun isTrusted(pkgInfo: PackageInfo, signatures: List): Boolean { if (officialSignature in signatures) { return true } - val trustedSignatures = preferences.trustedSignatures().get() - return trustedSignatures.any { signatures.contains(it) } + return trustExtension.isTrusted(pkgInfo, signatures.last()) } private fun isOfficiallySigned(signatures: List): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt index 443f1c347..9fead0b44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsScreenModel.kt @@ -194,8 +194,8 @@ class ExtensionsScreenModel( } } - fun trustSignature(signatureHash: String) { - extensionManager.trustSignature(signatureHash) + fun trustExtension(extension: Extension.Untrusted) { + extensionManager.trust(extension) } @Immutable diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt index a5b390a89..635c6cb83 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/ExtensionsTab.kt @@ -61,7 +61,7 @@ fun extensionsTab( }, onInstallExtension = extensionsScreenModel::installExtension, onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) }, - onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) }, + onTrustExtension = { extensionsScreenModel.trustExtension(it) }, onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) }, onUpdateExtension = extensionsScreenModel::updateExtension, onRefresh = extensionsScreenModel::findAvailableExtensions, diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index e56afbf4d..53d6c3829 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -591,18 +591,6 @@ object EXHMigrations { logcat(LogPriority.WARN, e) { "Failed to remove file from cache" } } } - - preferenceStore.getAll() - .filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") } - .forEach { (key, value) -> - if (value is String) { - preferenceStore - .getString(Preference.privateKey(key)) - .set(value) - - preferenceStore.getString(key).delete() - } - } } if (oldVersion under 59) { val prefsToReplace = listOf( @@ -657,6 +645,14 @@ object EXHMigrations { sourcePreferences.extensionRepos().getAndSet { it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet() } + replacePreferences( + preferenceStore = preferenceStore, + filterPredicate = { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }, + newKey = { Preference.privateKey(it) }, + ) + prefs.edit { + remove(Preference.appStateKey("trusted_signatures")) + } } // if (oldVersion under 1) { } (1 is current release version)