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
This commit is contained in:
arkon 2024-01-07 13:35:44 -05:00 committed by Jobobby04
parent 28ca7efdca
commit 56b565cc51
9 changed files with 76 additions and 54 deletions

View File

@ -21,6 +21,7 @@ import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.interactor.ToggleSourcePin 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.AddTracks
import eu.kanade.domain.track.interactor.RefreshTracks import eu.kanade.domain.track.interactor.RefreshTracks
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
@ -170,6 +171,7 @@ class DomainModule : InjektModule {
addFactory { ToggleLanguage(get()) } addFactory { ToggleLanguage(get()) }
addFactory { ToggleSource(get()) } addFactory { ToggleSource(get()) }
addFactory { ToggleSourcePin(get()) } addFactory { ToggleSourcePin(get()) }
addFactory { TrustExtension(get()) }
addFactory { CreateSourceRepo(get()) } addFactory { CreateSourceRepo(get()) }
addFactory { DeleteSourceRepo(get()) } addFactory { DeleteSourceRepo(get()) }

View File

@ -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"
}
}
}
}

View File

@ -38,13 +38,16 @@ class SourcePreferences(
SetMigrateSorting.Direction.ASCENDING, SetMigrateSorting.Direction.ASCENDING,
) )
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet()) fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0) fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet()) fun trustedExtensions() = preferenceStore.getStringSet(
Preference.appStateKey("trusted_extensions"),
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false) emptySet(),
)
// SY --> // SY -->
fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true) fun enableSourceBlacklist() = preferenceStore.getBoolean("eh_enable_source_blacklist", true)

View File

@ -329,7 +329,7 @@ object Migrations {
} }
} }
} }
if (oldVersion < 95) { if (oldVersion < 96) {
LibraryUpdateJob.cancelAllWorks(context) LibraryUpdateJob.cancelAllWorks(context)
LibraryUpdateJob.setupTask(context) LibraryUpdateJob.setupTask(context)
} }
@ -377,13 +377,6 @@ object Migrations {
uiPreferences.relativeTime().set(false) 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) { if (oldVersion < 113) {
val prefsToReplace = listOf( val prefsToReplace = listOf(
"pref_download_only", "pref_download_only",
@ -410,7 +403,19 @@ object Migrations {
} }
if (oldVersion < 114) { if (oldVersion < 114) {
sourcePreferences.extensionRepos().getAndSet { 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 return true

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.domain.source.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.api.ExtensionApi 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.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.preference.plusAssign
import tachiyomi.core.util.lang.launchNow import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.logcat 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 * 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 * signature is trusted, otherwise the user will be prompted with a warning to trust it before being
* loaded. * loaded.
*
* @param context The application context.
* @param preferences The application preferences.
*/ */
class ExtensionManager( class ExtensionManager(
private val context: Context, private val context: Context,
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
private val trustExtension: TrustExtension = Injekt.get(),
) { ) {
var isInitialized = false 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 * Adds the given extension to the list of trusted extensions. It also loads in background the
* extensions that match this signature. * now trusted extensions.
* *
* @param signature The signature to whitelist. * @param extension the extension to trust
*/ */
fun trustSignature(signature: String) { fun trust(extension: Extension.Untrusted) {
val untrustedSignatures = _untrustedExtensionsFlow.value.map { it.signatureHash }.toSet() val untrustedPkgNames = _untrustedExtensionsFlow.value.map { it.pkgName }.toSet()
if (signature !in untrustedSignatures) return 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 _untrustedExtensionsFlow.value -= nowTrustedExtensions
launchNow { launchNow {

View File

@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import eu.kanade.domain.source.interactor.TrustExtension
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
@ -40,6 +41,7 @@ import java.io.File
internal object ExtensionLoader { internal object ExtensionLoader {
private val preferences: SourcePreferences by injectLazy() private val preferences: SourcePreferences by injectLazy()
private val trustExtension: TrustExtension by injectLazy()
private val loadNsfwSource by lazy { private val loadNsfwSource by lazy {
preferences.showNsfwSource().get() preferences.showNsfwSource().get()
} }
@ -48,8 +50,6 @@ internal object ExtensionLoader {
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class" private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory" private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw" 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_MIN = 1.4
const val LIB_VERSION_MAX = 1.5 const val LIB_VERSION_MAX = 1.5
@ -118,12 +118,6 @@ internal object ExtensionLoader {
* @param context The application context. * @param context The application context.
*/ */
fun loadExtensions(context: Context): List<LoadResult> { fun loadExtensions(context: Context): List<LoadResult> {
// 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 pkgManager = context.packageManager
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -261,7 +255,7 @@ internal object ExtensionLoader {
if (signatures.isNullOrEmpty()) { if (signatures.isNullOrEmpty()) {
logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error return LoadResult.Error
} else if (!hasTrustedSignature(signatures)) { } else if (!isTrusted(pkgInfo, signatures)) {
val extension = Extension.Untrusted( val extension = Extension.Untrusted(
extName, extName,
pkgName, pkgName,
@ -280,9 +274,6 @@ internal object ExtensionLoader {
return LoadResult.Error 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 { val classLoader = try {
PathClassLoader(appInfo.sourceDir, null, context.classLoader) PathClassLoader(appInfo.sourceDir, null, context.classLoader)
} catch (e: Exception) { } catch (e: Exception) {
@ -392,13 +383,12 @@ internal object ExtensionLoader {
?.toList() ?.toList()
} }
private fun hasTrustedSignature(signatures: List<String>): Boolean { private fun isTrusted(pkgInfo: PackageInfo, signatures: List<String>): Boolean {
if (officialSignature in signatures) { if (officialSignature in signatures) {
return true return true
} }
val trustedSignatures = preferences.trustedSignatures().get() return trustExtension.isTrusted(pkgInfo, signatures.last())
return trustedSignatures.any { signatures.contains(it) }
} }
private fun isOfficiallySigned(signatures: List<String>): Boolean { private fun isOfficiallySigned(signatures: List<String>): Boolean {

View File

@ -194,8 +194,8 @@ class ExtensionsScreenModel(
} }
} }
fun trustSignature(signatureHash: String) { fun trustExtension(extension: Extension.Untrusted) {
extensionManager.trustSignature(signatureHash) extensionManager.trust(extension)
} }
@Immutable @Immutable

View File

@ -61,7 +61,7 @@ fun extensionsTab(
}, },
onInstallExtension = extensionsScreenModel::installExtension, onInstallExtension = extensionsScreenModel::installExtension,
onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) }, onOpenExtension = { navigator.push(ExtensionDetailsScreen(it.pkgName)) },
onTrustExtension = { extensionsScreenModel.trustSignature(it.signatureHash) }, onTrustExtension = { extensionsScreenModel.trustExtension(it) },
onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) }, onUninstallExtension = { extensionsScreenModel.uninstallExtension(it) },
onUpdateExtension = extensionsScreenModel::updateExtension, onUpdateExtension = extensionsScreenModel::updateExtension,
onRefresh = extensionsScreenModel::findAvailableExtensions, onRefresh = extensionsScreenModel::findAvailableExtensions,

View File

@ -591,18 +591,6 @@ object EXHMigrations {
logcat(LogPriority.WARN, e) { "Failed to remove file from cache" } 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) { if (oldVersion under 59) {
val prefsToReplace = listOf( val prefsToReplace = listOf(
@ -657,6 +645,14 @@ object EXHMigrations {
sourcePreferences.extensionRepos().getAndSet { sourcePreferences.extensionRepos().getAndSet {
it.map { "https://raw.githubusercontent.com/$it/repo" }.toSet() 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) // if (oldVersion under 1) { } (1 is current release version)