mirror of
https://github.com/revanced/revanced-api.git
synced 2025-04-30 06:34:36 +02:00
Send signatures and verify signature of patches file before loading it
This commit is contained in:
parent
f9cae1ea56
commit
4a685a2b53
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -81,3 +81,6 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
build-args: |
|
||||
GITHUB_ACTOR=${{ github.actor }}
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -65,9 +65,10 @@ dependencies {
|
||||
implementation(libs.ktor.server.rate.limit)
|
||||
implementation(libs.ktor.server.host.common)
|
||||
implementation(libs.ktor.server.jetty)
|
||||
implementation(libs.ktor.server.call.logging)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.koin.ktor)
|
||||
implementation("io.bkbn:kompendium-core:latest.release")
|
||||
implementation(libs.kompendium.core)
|
||||
implementation(libs.h2)
|
||||
implementation(libs.logback.classic)
|
||||
implementation(libs.exposed.core)
|
||||
@ -82,6 +83,8 @@ dependencies {
|
||||
implementation(libs.revanced.patcher)
|
||||
implementation(libs.revanced.library)
|
||||
implementation(libs.caffeine)
|
||||
implementation(libs.bouncy.castle.provider)
|
||||
implementation(libs.bouncy.castle.pgp)
|
||||
}
|
||||
|
||||
// The maven-publish plugin is necessary to make signing work.
|
||||
|
@ -1,8 +1,6 @@
|
||||
organization = "revanced"
|
||||
patches-repository = "revanced-patches"
|
||||
integrations-repositories = [
|
||||
"revanced-integrations"
|
||||
]
|
||||
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "patches-public-key.asc" }
|
||||
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "integrations-public-key.asc" }
|
||||
contributors-repositories = [
|
||||
"revanced-patcher",
|
||||
"revanced-patches",
|
||||
|
@ -1,8 +1,6 @@
|
||||
organization = "revanced"
|
||||
patches-repository = "revanced-patches"
|
||||
integrations-repositories = [
|
||||
"revanced-integrations"
|
||||
]
|
||||
patches = { repository = "revanced-patches", asset-regex = "jar$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
|
||||
integrations = { repository = "revanced-integrations", asset-regex = "apk$", signature-asset-regex = "asc$", public-key-file = "key.asc" }
|
||||
contributors-repositories = [
|
||||
"revanced-patcher",
|
||||
"revanced-patches",
|
||||
|
@ -6,6 +6,8 @@ services:
|
||||
- /data/revanced-api/persistence:/app/persistence
|
||||
- /data/revanced-api/.env:/app/.env
|
||||
- /data/revanced-api/configuration.toml:/app/configuration.toml
|
||||
- /data/revanced-api/patches-public-key.asc:/app/patches-public-key.asc
|
||||
- /data/revanced-api/integrations-public-key.asc:/app/integrations-public-key.asc
|
||||
environment:
|
||||
- COMMAND=start
|
||||
ports:
|
||||
|
@ -1,4 +1,5 @@
|
||||
[versions]
|
||||
kompendium-core = "latest.release"
|
||||
kotlin = "2.0.0"
|
||||
logback = "1.4.14"
|
||||
exposed = "0.41.1"
|
||||
@ -7,13 +8,15 @@ koin = "3.5.3"
|
||||
dotenv = "6.4.1"
|
||||
ktor = "2.3.7"
|
||||
ktoml = "0.5.1"
|
||||
picocli = "4.7.5"
|
||||
picocli = "4.7.6"
|
||||
datetime = "0.5.0"
|
||||
revanced-patcher = "19.3.1"
|
||||
revanced-library = "2.3.0"
|
||||
caffeine = "3.1.8"
|
||||
bouncy-castle = "1.78.1"
|
||||
|
||||
[libraries]
|
||||
kompendium-core = { module = "io.bkbn:kompendium-core", version.ref = "kompendium-core" }
|
||||
ktor-client-core = { module = "io.ktor:ktor-client-core" }
|
||||
ktor-client-cio = { module = "io.ktor:ktor-client-cio" }
|
||||
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp" }
|
||||
@ -29,6 +32,7 @@ ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers" }
|
||||
ktor-server-rate-limit = { module = "io.ktor:ktor-server-rate-limit" }
|
||||
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common" }
|
||||
ktor-server-jetty = { module = "io.ktor:ktor-server-jetty" }
|
||||
ktor-server-call-logging = { module = "io.ktor:ktor-server-call-logging" }
|
||||
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json" }
|
||||
koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
|
||||
h2 = { module = "com.h2database:h2", version.ref = "h2" }
|
||||
@ -45,6 +49,8 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
|
||||
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
|
||||
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
|
||||
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version.ref = "caffeine" }
|
||||
bouncy-castle-provider = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncy-castle" }
|
||||
bouncy-castle-pgp = { module = "org.bouncycastle:bcpg-jdk18on", version.ref = "bouncy-castle" }
|
||||
|
||||
[plugins]
|
||||
serilization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
@ -39,6 +39,7 @@ internal object StartAPICommand : Runnable {
|
||||
configureSerialization()
|
||||
configureSecurity()
|
||||
configureOpenAPI()
|
||||
configureLogging()
|
||||
configureRouting()
|
||||
}.start(wait = true)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import app.revanced.api.configuration.repository.AnnouncementRepository
|
||||
import app.revanced.api.configuration.repository.BackendRepository
|
||||
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||
import app.revanced.api.configuration.repository.GitHubBackendRepository
|
||||
import app.revanced.api.configuration.services.*
|
||||
import app.revanced.api.configuration.services.AnnouncementService
|
||||
import app.revanced.api.configuration.services.ApiService
|
||||
import app.revanced.api.configuration.services.AuthService
|
||||
@ -130,6 +131,7 @@ fun Application.configureDependencies(
|
||||
)
|
||||
}
|
||||
singleOf(::AnnouncementService)
|
||||
singleOf(::SignatureService)
|
||||
singleOf(::PatchesService)
|
||||
singleOf(::ApiService)
|
||||
}
|
||||
|
16
src/main/kotlin/app/revanced/api/configuration/Logging.kt
Normal file
16
src/main/kotlin/app/revanced/api/configuration/Logging.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package app.revanced.api.configuration
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.callloging.*
|
||||
import io.ktor.server.request.*
|
||||
|
||||
internal fun Application.configureLogging() {
|
||||
install(CallLogging) {
|
||||
format { call ->
|
||||
val status = call.response.status()
|
||||
val httpMethod = call.request.httpMethod.value
|
||||
val uri = call.request.uri
|
||||
"$status $httpMethod $uri"
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ import io.ktor.client.*
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
|
||||
/**
|
||||
* The backend of the application used to get data for the API.
|
||||
* The backend of the API used to get data.
|
||||
*
|
||||
* @param client The HTTP client to use for requests.
|
||||
*/
|
||||
@ -97,12 +97,18 @@ abstract class BackendRepository internal constructor(
|
||||
val createdAt: LocalDateTime,
|
||||
val assets: Set<BackendAsset>,
|
||||
) {
|
||||
companion object {
|
||||
fun Set<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An asset of a release.
|
||||
*
|
||||
* @property name The name of the asset.
|
||||
* @property downloadUrl The URL to download the asset.
|
||||
*/
|
||||
class BackendAsset(
|
||||
val name: String,
|
||||
val downloadUrl: String,
|
||||
)
|
||||
}
|
||||
|
@ -1,18 +1,78 @@
|
||||
package app.revanced.api.configuration.repository
|
||||
|
||||
import app.revanced.api.configuration.services.PatchesService
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The repository storing the configuration for the API.
|
||||
*
|
||||
* @property organization The API backends organization name where the repositories for the patches and integrations are.
|
||||
* @property patches The source of the patches.
|
||||
* @property integrations The source of the integrations.
|
||||
* @property contributorsRepositoryNames The names of the repositories to get contributors from.
|
||||
* @property apiVersion The version to use for the API.
|
||||
* @property host The host of the API to configure CORS.
|
||||
*/
|
||||
@Serializable
|
||||
internal class ConfigurationRepository(
|
||||
val organization: String,
|
||||
@SerialName("patches-repository")
|
||||
val patchesRepository: String,
|
||||
@SerialName("integrations-repositories")
|
||||
val integrationsRepositoryNames: Set<String>,
|
||||
val patches: AssetConfiguration,
|
||||
val integrations: AssetConfiguration,
|
||||
@SerialName("contributors-repositories")
|
||||
val contributorsRepositoryNames: Set<String>,
|
||||
@SerialName("api-version")
|
||||
val apiVersion: Int = 1,
|
||||
val host: String,
|
||||
) {
|
||||
/**
|
||||
* An asset configuration.
|
||||
*
|
||||
* [PatchesService] uses [BackendRepository] to get assets from its releases.
|
||||
* A release contains multiple assets.
|
||||
*
|
||||
* This configuration is used in [ConfigurationRepository]
|
||||
* to determine which release assets from repositories to get and to verify them.
|
||||
*
|
||||
* @property repository The repository in which releases are made to get an asset.
|
||||
* @property assetRegex The regex matching the asset name.
|
||||
* @property signatureAssetRegex The regex matching the signature asset name to verify the asset.
|
||||
* @property publicKeyFile The public key file to verify the signature of the asset.
|
||||
*/
|
||||
@Serializable
|
||||
internal class AssetConfiguration(
|
||||
val repository: String,
|
||||
@Serializable(with = RegexSerializer::class)
|
||||
@SerialName("asset-regex")
|
||||
val assetRegex: Regex,
|
||||
@Serializable(with = RegexSerializer::class)
|
||||
@SerialName("signature-asset-regex")
|
||||
val signatureAssetRegex: Regex,
|
||||
@Serializable(with = FileSerializer::class)
|
||||
@SerialName("public-key-file")
|
||||
val publicKeyFile: File,
|
||||
)
|
||||
}
|
||||
|
||||
private object RegexSerializer : KSerializer<Regex> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Regex", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Regex) = encoder.encodeString(value.pattern)
|
||||
|
||||
override fun deserialize(decoder: Decoder) = Regex(decoder.decodeString())
|
||||
}
|
||||
|
||||
private object FileSerializer : KSerializer<File> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("File", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: File) = encoder.encodeString(value.path)
|
||||
|
||||
override fun deserialize(decoder: Decoder) = File(decoder.decodeString())
|
||||
}
|
||||
|
@ -35,7 +35,10 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
|
||||
releaseNote = release.body,
|
||||
createdAt = release.createdAt.toLocalDateTime(TimeZone.UTC),
|
||||
assets = release.assets.map {
|
||||
BackendAsset(downloadUrl = it.browserDownloadUrl)
|
||||
BackendAsset(
|
||||
name = it.name,
|
||||
downloadUrl = it.browserDownloadUrl,
|
||||
)
|
||||
}.toSet(),
|
||||
)
|
||||
}
|
||||
@ -156,6 +159,7 @@ class GitHubOrganization {
|
||||
) {
|
||||
@Serializable
|
||||
class GitHubAsset(
|
||||
val name: String,
|
||||
val browserDownloadUrl: String,
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package app.revanced.api.configuration.routes
|
||||
|
||||
import app.revanced.api.configuration.installCache
|
||||
import app.revanced.api.configuration.installNotarizedRoute
|
||||
import app.revanced.api.configuration.schema.APIAssetPublicKeys
|
||||
import app.revanced.api.configuration.schema.APIRelease
|
||||
import app.revanced.api.configuration.schema.APIReleaseVersion
|
||||
import app.revanced.api.configuration.services.PatchesService
|
||||
@ -10,6 +12,7 @@ import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.ratelimit.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import org.koin.ktor.ext.get as koinGet
|
||||
|
||||
internal fun Route.patchesRoute() = route("patches") {
|
||||
@ -42,6 +45,18 @@ internal fun Route.patchesRoute() = route("patches") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rateLimit(RateLimitName("strong")) {
|
||||
route("keys") {
|
||||
installCache(356.days)
|
||||
|
||||
installPatchesPublicKeyRouteDocumentation()
|
||||
|
||||
get {
|
||||
call.respond(patchesService.publicKeys())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.installLatestPatchesRouteDocumentation() = installNotarizedRoute {
|
||||
@ -88,3 +103,18 @@ fun Route.installLatestPatchesListRouteDocumentation() = installNotarizedRoute {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Route.installPatchesPublicKeyRouteDocumentation() = installNotarizedRoute {
|
||||
tags = setOf("Patches")
|
||||
|
||||
get = GetInfo.builder {
|
||||
description("Get the public keys for verifying patches and integrations assets")
|
||||
summary("Get patches and integrations public keys")
|
||||
response {
|
||||
description("The public keys")
|
||||
mediaTypes("application/json")
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<APIAssetPublicKeys>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
package app.revanced.api.configuration.schema
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class APIRelease(
|
||||
val version: String,
|
||||
val createdAt: LocalDateTime,
|
||||
val changelog: String,
|
||||
val description: String,
|
||||
val assets: Set<APIAsset>,
|
||||
)
|
||||
|
||||
@ -49,23 +48,15 @@ class APIContributable(
|
||||
@Serializable
|
||||
class APIAsset(
|
||||
val downloadUrl: String,
|
||||
) {
|
||||
val type = when {
|
||||
downloadUrl.endsWith(".jar") -> Type.PATCHES
|
||||
downloadUrl.endsWith(".apk") -> Type.INTEGRATIONS
|
||||
else -> Type.UNKNOWN
|
||||
}
|
||||
val signatureDownloadUrl: String,
|
||||
// TODO: Remove this eventually when integrations are merged into patches.
|
||||
val type: APIAssetType,
|
||||
)
|
||||
|
||||
enum class Type {
|
||||
@SerialName("patches")
|
||||
@Serializable
|
||||
enum class APIAssetType {
|
||||
PATCHES,
|
||||
|
||||
@SerialName("integrations")
|
||||
INTEGRATIONS,
|
||||
|
||||
@SerialName("unknown")
|
||||
UNKNOWN,
|
||||
}
|
||||
INTEGRATION,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@ -113,3 +104,9 @@ class APIRateLimit(
|
||||
val remaining: Int,
|
||||
val reset: LocalDateTime,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class APIAssetPublicKeys(
|
||||
val patchesPublicKey: String,
|
||||
val integrationsPublicKey: String,
|
||||
)
|
||||
|
@ -1,87 +1,119 @@
|
||||
package app.revanced.api.configuration.services
|
||||
|
||||
import app.revanced.api.configuration.repository.BackendRepository
|
||||
import app.revanced.api.configuration.repository.BackendRepository.BackendOrganization.BackendRepository.BackendRelease.Companion.first
|
||||
import app.revanced.api.configuration.repository.ConfigurationRepository
|
||||
import app.revanced.api.configuration.schema.APIAsset
|
||||
import app.revanced.api.configuration.schema.APIRelease
|
||||
import app.revanced.api.configuration.schema.APIReleaseVersion
|
||||
import app.revanced.api.configuration.schema.*
|
||||
import app.revanced.library.PatchUtils
|
||||
import app.revanced.patcher.PatchBundleLoader
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import io.ktor.util.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URL
|
||||
|
||||
internal class PatchesService(
|
||||
private val signatureService: SignatureService,
|
||||
private val backendRepository: BackendRepository,
|
||||
private val configurationRepository: ConfigurationRepository,
|
||||
) {
|
||||
private val patchesListCache = Caffeine
|
||||
.newBuilder()
|
||||
.maximumSize(1)
|
||||
.build<String, ByteArray>()
|
||||
|
||||
suspend fun latestRelease(): APIRelease {
|
||||
val patchesRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.patchesRepository,
|
||||
configurationRepository.patches.repository,
|
||||
)
|
||||
val integrationsReleases = withContext(Dispatchers.Default) {
|
||||
configurationRepository.integrationsRepositoryNames.map {
|
||||
async { backendRepository.release(configurationRepository.organization, it) }
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
val assets = (patchesRelease.assets + integrationsReleases.flatMap { it.assets })
|
||||
.map { APIAsset(it.downloadUrl) }
|
||||
.filter { it.type != APIAsset.Type.UNKNOWN }
|
||||
.toSet()
|
||||
val integrationsRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.integrations.repository,
|
||||
)
|
||||
|
||||
fun ConfigurationRepository.AssetConfiguration.asset(
|
||||
release: BackendRepository.BackendOrganization.BackendRepository.BackendRelease,
|
||||
assetType: APIAssetType,
|
||||
) = APIAsset(
|
||||
release.assets.first(assetRegex).downloadUrl,
|
||||
release.assets.first(signatureAssetRegex).downloadUrl,
|
||||
assetType,
|
||||
)
|
||||
|
||||
val patchesAsset = configurationRepository.patches.asset(
|
||||
patchesRelease,
|
||||
APIAssetType.PATCHES,
|
||||
)
|
||||
val integrationsAsset = configurationRepository.integrations.asset(
|
||||
integrationsRelease,
|
||||
APIAssetType.INTEGRATION,
|
||||
)
|
||||
|
||||
return APIRelease(
|
||||
patchesRelease.tag,
|
||||
patchesRelease.createdAt,
|
||||
patchesRelease.releaseNote,
|
||||
assets,
|
||||
setOf(patchesAsset, integrationsAsset),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun latestVersion(): APIReleaseVersion {
|
||||
val patchesRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.patchesRepository,
|
||||
configurationRepository.patches.repository,
|
||||
)
|
||||
|
||||
return APIReleaseVersion(patchesRelease.tag)
|
||||
}
|
||||
|
||||
private val patchesListCache = Caffeine
|
||||
.newBuilder()
|
||||
.maximumSize(1)
|
||||
.build<String, ByteArray>()
|
||||
|
||||
suspend fun list(): ByteArray {
|
||||
val patchesRelease = backendRepository.release(
|
||||
configurationRepository.organization,
|
||||
configurationRepository.patchesRepository,
|
||||
configurationRepository.patches.repository,
|
||||
)
|
||||
|
||||
return patchesListCache.getIfPresent(patchesRelease.tag) ?: run {
|
||||
val downloadUrl = patchesRelease.assets
|
||||
.map { APIAsset(it.downloadUrl) }
|
||||
.find { it.type == APIAsset.Type.PATCHES }
|
||||
?.downloadUrl
|
||||
return patchesListCache.get(patchesRelease.tag) {
|
||||
val patchesDownloadUrl = patchesRelease.assets
|
||||
.first(configurationRepository.patches.assetRegex).downloadUrl
|
||||
|
||||
val patches = kotlin.io.path.createTempFile().toFile().apply {
|
||||
outputStream().use { URL(downloadUrl).openStream().copyTo(it) }
|
||||
}.let { file ->
|
||||
PatchBundleLoader.Jar(file).also { file.delete() }
|
||||
val signatureDownloadUrl = patchesRelease.assets
|
||||
.first(configurationRepository.patches.signatureAssetRegex).downloadUrl
|
||||
|
||||
val patchesFile = kotlin.io.path.createTempFile().toFile().apply {
|
||||
outputStream().use { URL(patchesDownloadUrl).openStream().copyTo(it) }
|
||||
}
|
||||
|
||||
val patches = if (
|
||||
signatureService.verify(
|
||||
patchesFile,
|
||||
signatureDownloadUrl,
|
||||
configurationRepository.patches.publicKeyFile,
|
||||
)
|
||||
) {
|
||||
PatchBundleLoader.Jar(patchesFile)
|
||||
} else {
|
||||
// Use an empty set of patches if the signature is invalid.
|
||||
emptySet()
|
||||
}
|
||||
|
||||
patchesFile.delete()
|
||||
|
||||
ByteArrayOutputStream().use { stream ->
|
||||
PatchUtils.Json.serialize(patches, outputStream = stream)
|
||||
|
||||
stream.toByteArray()
|
||||
}.also {
|
||||
patchesListCache.put(patchesRelease.tag, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun publicKeys(): APIAssetPublicKeys {
|
||||
fun publicKeyBase64(getAssetConfiguration: ConfigurationRepository.() -> ConfigurationRepository.AssetConfiguration) =
|
||||
configurationRepository.getAssetConfiguration().publicKeyFile.readBytes().encodeBase64()
|
||||
|
||||
return APIAssetPublicKeys(
|
||||
publicKeyBase64 { patches },
|
||||
publicKeyBase64 { integrations },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
package app.revanced.api.configuration.services
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.openpgp.*
|
||||
import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator
|
||||
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.security.Security
|
||||
|
||||
internal class SignatureService {
|
||||
private val signatureCache = Caffeine
|
||||
.newBuilder()
|
||||
.maximumSize(2) // Assuming this is enough for patches and integrations.
|
||||
.build<ByteArray, Boolean>() // Hash -> Verified.
|
||||
|
||||
fun verify(
|
||||
file: File,
|
||||
signatureDownloadUrl: String,
|
||||
publicKeyFile: File,
|
||||
): Boolean {
|
||||
val fileBytes = file.readBytes()
|
||||
|
||||
return signatureCache.get(MessageDigest.getInstance("SHA-256").digest(fileBytes)) {
|
||||
verify(
|
||||
fileBytes = fileBytes,
|
||||
signatureInputStream = URL(signatureDownloadUrl).openStream(),
|
||||
publicKeyInputStream = publicKeyFile.inputStream(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verify(
|
||||
fileBytes: ByteArray,
|
||||
signatureInputStream: InputStream,
|
||||
publicKeyInputStream: InputStream,
|
||||
) = getSignature(signatureInputStream).apply {
|
||||
init(BcPGPContentVerifierBuilderProvider(), getPublicKey(publicKeyInputStream))
|
||||
update(fileBytes)
|
||||
}.verify()
|
||||
|
||||
private fun getPublicKey(publicKeyInputStream: InputStream): PGPPublicKey {
|
||||
val decoderStream = PGPUtil.getDecoderStream(publicKeyInputStream)
|
||||
|
||||
PGPPublicKeyRingCollection(decoderStream, BcKeyFingerprintCalculator()).forEach { keyRing ->
|
||||
keyRing.publicKeys.forEach { publicKey ->
|
||||
if (publicKey.isEncryptionKey) {
|
||||
return publicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("Can't find encryption key in key ring.")
|
||||
}
|
||||
|
||||
private fun getSignature(inputStream: InputStream): PGPSignature {
|
||||
val decoderStream = PGPUtil.getDecoderStream(inputStream)
|
||||
val pgpObjectFactory = PGPObjectFactory(decoderStream, BcKeyFingerprintCalculator())
|
||||
val signatureList = pgpObjectFactory.nextObject() as PGPSignatureList
|
||||
|
||||
return signatureList.first()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
init {
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
}
|
||||
}
|
@ -4,8 +4,7 @@
|
||||
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="trace">
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="org.eclipse.jetty" level="INFO"/>
|
||||
</configuration>
|
||||
|
Loading…
x
Reference in New Issue
Block a user