diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a81b7efd0..824102ab8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,7 +22,7 @@ android { defaultConfig { applicationId = "eu.kanade.tachiyomi" - versionCode = 116 + versionCode = 117 versionName = "0.15.1" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 5bcaf48e6..33be9a3b7 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 ea00bfc69..5779020a2 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,11 +38,14 @@ 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(), + ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index eacb67b93..9947f98ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -410,6 +410,11 @@ object Migrations { 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 4c342dd3d..942e8f4a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension import android.content.Context import android.graphics.drawable.Drawable +import eu.kanade.domain.source.interactor.TrustExtension import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.tachiyomi.extension.api.ExtensionApi import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier @@ -18,7 +19,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow 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 @@ -34,13 +34,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 @@ -249,18 +247,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 a01ee5cb4..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 @@ -15,7 +16,6 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.util.lang.Hash import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo -import eu.kanade.tachiyomi.util.system.isDevFlavor import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking @@ -41,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() } @@ -49,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 @@ -119,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 - if (!isDevFlavor) { - preferences.trustedSignatures().delete() - } - val pkgManager = context.packageManager val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -262,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, @@ -281,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) { @@ -393,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 f6ed811a7..5617e506b 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 @@ -195,8 +195,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,