Minor refactoring

Added UNTRUSTED trust level for plugins with invalid signature
This commit is contained in:
grimsi
2025-04-02 11:18:44 +02:00
parent 4ab7dbddec
commit 96234592ac
4 changed files with 44 additions and 27 deletions
@@ -81,6 +81,10 @@ export function PluginManagementCard({plugin, updatePlugin}: {
return <Tooltip color="foreground" placement="bottom" content="3rd party plugin"> return <Tooltip color="foreground" placement="bottom" content="3rd party plugin">
<SealWarning/> <SealWarning/>
</Tooltip>; </Tooltip>;
case PluginTrustLevel.UNTRUSTED:
return <Tooltip color="foreground" placement="bottom" content="Plugin verification failed">
<SealWarning weight="fill" className="fill-danger"/>
</Tooltip>;
default: default:
return <Tooltip color="foreground" placement="bottom" content="Unkown verification status"> return <Tooltip color="foreground" placement="bottom" content="Unkown verification status">
<SealQuestion/> <SealQuestion/>
@@ -31,6 +31,7 @@ class GameyfinPluginManager(
} }
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
private val publicKey: PublicKey = loadPluginSignaturePublicKey()
// This took me way too long to figure out... // This took me way too long to figure out...
// But I learned a lot about Kotlin and Java interoperability in the process // 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? { override fun loadPluginFromPath(pluginPath: Path?): PluginWrapper? {
val pluginWrapper = super.loadPluginFromPath(pluginPath) val pluginWrapper = super.loadPluginFromPath(pluginPath)
if (pluginWrapper != null) { if (pluginWrapper == null || pluginPath == null) return null
// Inject config after loading, before starting
configurePlugin(pluginWrapper)
// Update or create the PluginManagementEntry // Inject config after loading, before starting
if (pluginPath != null) { 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 var pluginManagementEntry = pluginManagementRepository.findByIdOrNull(pluginWrapper.pluginId)
val currentMaxPriority = pluginManagementRepository.findMaxPriority() ?: 0
val pluginManagementEntry = pluginManagementRepository.findByIdOrNull(pluginWrapper.pluginId) if (pluginManagementEntry == null) {
?: PluginManagementEntry(pluginId = pluginWrapper.pluginId, priority = currentMaxPriority + 1) // Create a new entry
if (pluginPath.extension == "jar") { // Set priority to the max value of the current plugins + 1 (which is the lowest priority) or 1 if there are no entries
log.debug { "Verifying plugin signature for ${pluginWrapper.pluginId}" } val currentMaxPriority = pluginManagementRepository.findMaxPriority() ?: 0
pluginManagementEntry.trustLevel = verifyPluginSignature(pluginPath)
log.debug { "Plugin ${pluginWrapper.pluginId} verification status: ${pluginManagementEntry.trustLevel}" }
} else {
pluginManagementEntry.trustLevel = PluginTrustLevel.BUNDLED
}
if (pluginManagementEntry.trustLevel in listOf(PluginTrustLevel.OFFICIAL, PluginTrustLevel.BUNDLED)) { pluginManagementEntry =
pluginManagementEntry.enabled = true PluginManagementEntry(pluginId = pluginWrapper.pluginId, priority = currentMaxPriority + 1)
log.info { "Plugin ${pluginWrapper.pluginId} verified, starting" }
startPlugin(pluginWrapper.pluginId)
}
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 return pluginWrapper
} }
@@ -174,13 +184,15 @@ class GameyfinPluginManager(
return pluginConfigRepository.findAllById_PluginId(pluginId).associate { it.id.key to it.value } 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 certFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
val certFileInputStream = javaClass.classLoader.getResourceAsStream(PUBLIC_KEY_FILE) val certFileInputStream = javaClass.classLoader.getResourceAsStream(PUBLIC_KEY_FILE)
val cert: X509Certificate = certFactory.generateCertificate(certFileInputStream) as X509Certificate val cert: X509Certificate = certFactory.generateCertificate(certFileInputStream) as X509Certificate
val publicKey: PublicKey = cert.publicKey
certFileInputStream?.close() certFileInputStream?.close()
return cert.publicKey
}
private fun verifyPluginSignature(pluginPath: Path): PluginTrustLevel {
val jarFile = JarFile(pluginPath.toFile(), true) val jarFile = JarFile(pluginPath.toFile(), true)
val entries = jarFile.entries() val entries = jarFile.entries()
@@ -197,7 +209,7 @@ class GameyfinPluginManager(
} }
} catch (_: SecurityException) { } catch (_: SecurityException) {
// Signature verification failed // Signature verification failed
return PluginTrustLevel.THIRD_PARTY return PluginTrustLevel.UNTRUSTED
} }
val codeSigners = entry.codeSigners val codeSigners = entry.codeSigners
@@ -216,7 +228,7 @@ class GameyfinPluginManager(
cert.verify(publicKey) cert.verify(publicKey)
} catch (_: Exception) { } catch (_: Exception) {
// Signature verification failed // Signature verification failed
return PluginTrustLevel.THIRD_PARTY return PluginTrustLevel.UNTRUSTED
} }
} }
} }
@@ -4,5 +4,6 @@ enum class PluginTrustLevel {
BUNDLED, BUNDLED,
OFFICIAL, OFFICIAL,
THIRD_PARTY, THIRD_PARTY,
UNTRUSTED,
UNKNOWN, UNKNOWN,
} }
+1 -1
View File
@@ -274,7 +274,7 @@ function statsExtracterPlugin(): PluginOption {
const frontendFiles: Record<string, string> = {}; const frontendFiles: Record<string, string> = {};
frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); 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) => const isThemeComponentsResource = (id: string) =>
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/')) id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))