Răsfoiți Sursa

Allow permanently trusting unofficial extensions by version code + signature

Closes #10290
arkon 1 an în urmă
părinte
comite
6510a9617a

+ 1 - 1
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()}\"")

+ 2 - 0
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()) }

+ 27 - 0
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"
+            }
+        }
+    }
+}

+ 6 - 3
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(),
+    )
 }

+ 5 - 0
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
         }
 

+ 11 - 12
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 {

+ 5 - 16
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<LoadResult> {
-        // 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<String>): Boolean {
+    private fun isTrusted(pkgInfo: PackageInfo, signatures: List<String>): 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<String>): Boolean {

+ 2 - 2
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

+ 1 - 1
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,