Allow permanently trusting unofficial extensions by version code + signature
Closes #10290
This commit is contained in:
parent
14510f1d26
commit
6510a9617a
@ -22,7 +22,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
|
|
||||||
versionCode = 116
|
versionCode = 117
|
||||||
versionName = "0.15.1"
|
versionName = "0.15.1"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
|
@ -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()) }
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -38,11 +38,14 @@ 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(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -410,6 +410,11 @@ object Migrations {
|
|||||||
newKey = { Preference.privateKey(it) },
|
newKey = { Preference.privateKey(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 117) {
|
||||||
|
prefs.edit {
|
||||||
|
remove(Preference.appStateKey("trusted_signatures"))
|
||||||
|
}
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,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 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.api.ExtensionApi
|
import eu.kanade.tachiyomi.extension.api.ExtensionApi
|
||||||
import eu.kanade.tachiyomi.extension.api.ExtensionUpdateNotifier
|
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.asStateFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
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
|
||||||
@ -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
|
* 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
|
||||||
@ -249,18 +247,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 {
|
||||||
|
@ -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
|
||||||
@ -15,7 +16,6 @@ import eu.kanade.tachiyomi.source.Source
|
|||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
import eu.kanade.tachiyomi.util.lang.Hash
|
import eu.kanade.tachiyomi.util.lang.Hash
|
||||||
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo
|
||||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -41,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()
|
||||||
}
|
}
|
||||||
@ -49,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
|
||||||
|
|
||||||
@ -119,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
|
|
||||||
if (!isDevFlavor) {
|
|
||||||
preferences.trustedSignatures().delete()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
||||||
@ -262,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,
|
||||||
@ -281,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) {
|
||||||
@ -393,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 {
|
||||||
|
@ -195,8 +195,8 @@ class ExtensionsScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trustSignature(signatureHash: String) {
|
fun trustExtension(extension: Extension.Untrusted) {
|
||||||
extensionManager.trustSignature(signatureHash)
|
extensionManager.trust(extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user