From 96234592ac795d156f938924f26c04699320fd73 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:18:44 +0200 Subject: [PATCH] Minor refactoring Added UNTRUSTED trust level for plugins with invalid signature --- .../general/cards/PluginManagementCard.tsx | 4 ++ .../management/GameyfinPluginManager.kt | 64 +++++++++++-------- .../plugins/management/PluginTrustLevel.kt | 1 + gameyfin/vite.generated.ts | 2 +- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx b/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx index 1de63b5..8d50bca 100644 --- a/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx +++ b/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx @@ -81,6 +81,10 @@ export function PluginManagementCard({plugin, updatePlugin}: { return ; + case PluginTrustLevel.UNTRUSTED: + return + + ; default: return diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt index bae0198..cf2f5fa 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt @@ -31,6 +31,7 @@ class GameyfinPluginManager( } private val log = KotlinLogging.logger {} + private val publicKey: PublicKey = loadPluginSignaturePublicKey() // This took me way too long to figure out... // But I learned a lot about Kotlin and Java interoperability in the process @@ -66,37 +67,46 @@ class GameyfinPluginManager( override fun loadPluginFromPath(pluginPath: Path?): PluginWrapper? { val pluginWrapper = super.loadPluginFromPath(pluginPath) - if (pluginWrapper != null) { - // Inject config after loading, before starting - configurePlugin(pluginWrapper) + if (pluginWrapper == null || pluginPath == null) return null - // Update or create the PluginManagementEntry - if (pluginPath != null) { + // Inject config after loading, before starting + configurePlugin(pluginWrapper) - // Set priority to the max value of the current plugins + 1 (which is the lowest priority) or 1 if there are no entries - val currentMaxPriority = pluginManagementRepository.findMaxPriority() ?: 0 + var pluginManagementEntry = pluginManagementRepository.findByIdOrNull(pluginWrapper.pluginId) - val pluginManagementEntry = pluginManagementRepository.findByIdOrNull(pluginWrapper.pluginId) - ?: PluginManagementEntry(pluginId = pluginWrapper.pluginId, priority = currentMaxPriority + 1) + if (pluginManagementEntry == null) { + // Create a new entry - if (pluginPath.extension == "jar") { - log.debug { "Verifying plugin signature for ${pluginWrapper.pluginId}" } - pluginManagementEntry.trustLevel = verifyPluginSignature(pluginPath) - log.debug { "Plugin ${pluginWrapper.pluginId} verification status: ${pluginManagementEntry.trustLevel}" } - } else { - pluginManagementEntry.trustLevel = PluginTrustLevel.BUNDLED - } + // Set priority to the max value of the current plugins + 1 (which is the lowest priority) or 1 if there are no entries + val currentMaxPriority = pluginManagementRepository.findMaxPriority() ?: 0 - if (pluginManagementEntry.trustLevel in listOf(PluginTrustLevel.OFFICIAL, PluginTrustLevel.BUNDLED)) { - pluginManagementEntry.enabled = true - log.info { "Plugin ${pluginWrapper.pluginId} verified, starting" } - startPlugin(pluginWrapper.pluginId) - } + pluginManagementEntry = + PluginManagementEntry(pluginId = pluginWrapper.pluginId, priority = currentMaxPriority + 1) - pluginManagementRepository.save(pluginManagementEntry) + pluginManagementEntry.trustLevel = when (pluginPath.extension) { + "jar" -> verifyPluginSignature(pluginPath) + else -> PluginTrustLevel.BUNDLED + } + + // If the plugin is official or bundled, we can enable it and start it by default + if (pluginManagementEntry.trustLevel == PluginTrustLevel.OFFICIAL + || pluginManagementEntry.trustLevel == PluginTrustLevel.BUNDLED + ) { + pluginManagementEntry.enabled = true + log.info { "Plugin ${pluginWrapper.pluginId} verified, starting" } + startPlugin(pluginWrapper.pluginId) + } + } else { + // Just re-verify the plugin if it was already in the database + pluginManagementEntry.trustLevel = when (pluginPath.extension) { + "jar" -> verifyPluginSignature(pluginPath) + else -> PluginTrustLevel.BUNDLED } } + log.debug { "Plugin ${pluginWrapper.pluginId} verification status: ${pluginManagementEntry.trustLevel}" } + pluginManagementRepository.save(pluginManagementEntry) + return pluginWrapper } @@ -174,13 +184,15 @@ class GameyfinPluginManager( return pluginConfigRepository.findAllById_PluginId(pluginId).associate { it.id.key to it.value } } - fun verifyPluginSignature(pluginPath: Path): PluginTrustLevel { + private fun loadPluginSignaturePublicKey(): PublicKey { val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509") val certFileInputStream = javaClass.classLoader.getResourceAsStream(PUBLIC_KEY_FILE) val cert: X509Certificate = certFactory.generateCertificate(certFileInputStream) as X509Certificate - val publicKey: PublicKey = cert.publicKey certFileInputStream?.close() + return cert.publicKey + } + private fun verifyPluginSignature(pluginPath: Path): PluginTrustLevel { val jarFile = JarFile(pluginPath.toFile(), true) val entries = jarFile.entries() @@ -197,7 +209,7 @@ class GameyfinPluginManager( } } catch (_: SecurityException) { // Signature verification failed - return PluginTrustLevel.THIRD_PARTY + return PluginTrustLevel.UNTRUSTED } val codeSigners = entry.codeSigners @@ -216,7 +228,7 @@ class GameyfinPluginManager( cert.verify(publicKey) } catch (_: Exception) { // Signature verification failed - return PluginTrustLevel.THIRD_PARTY + return PluginTrustLevel.UNTRUSTED } } } diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel.kt index 6c100e7..7383719 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel.kt @@ -4,5 +4,6 @@ enum class PluginTrustLevel { BUNDLED, OFFICIAL, THIRD_PARTY, + UNTRUSTED, UNKNOWN, } \ No newline at end of file diff --git a/gameyfin/vite.generated.ts b/gameyfin/vite.generated.ts index 6bebb28..22fde9f 100644 --- a/gameyfin/vite.generated.ts +++ b/gameyfin/vite.generated.ts @@ -274,7 +274,7 @@ function statsExtracterPlugin(): PluginOption { const frontendFiles: Record = {}; frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); - const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map']; + const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map', '.']; const isThemeComponentsResource = (id: string) => id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))