Skip to content

Commit

Permalink
feat: add GPG verification to UpdateChecker
Browse files Browse the repository at this point in the history
  • Loading branch information
My-Name-Is-Jeff committed May 17, 2024
1 parent 1e0a37c commit 856ded9
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 25 deletions.
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ dependencies {
shadowMe(project(":events"))
shadowMe(project(":hypixel-api:types"))

shadowMe("org.bouncycastle:bcpg-jdk18on:1.78.1") {
exclude(module = "bcprov-jdk18on")
}
compileOnly("org.bouncycastle:bcprov-jdk18on:1.78.1")

shadowMe(annotationProcessor("io.github.llamalad7:mixinextras-common:0.3.5")!!)
annotationProcessor("org.spongepowered:mixin:0.8.5:processor")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.Security;

import static gg.skytils.skytilsmod.tweaker.TweakerUtil.addToClasspath;

Expand All @@ -36,6 +37,7 @@ public class DependencyLoader {

public static void loadDependencies() {
loadBrotli();
if (Security.getProvider("BC") == null) loadBCProv();
}

public static File loadDependency(String path) throws Throwable {
Expand All @@ -49,13 +51,23 @@ public static File loadDependency(String path) throws Throwable {
}
}

System.out.println("Brotli size: " + Files.size(downloadPath));
System.out.printf("Dependency size for %s: %s%n", path.substring(path.lastIndexOf('/') + 1), Files.size(downloadPath));

addToClasspath(downloadLocation.toURI().toURL());

return downloadLocation;
}

public static void loadBCProv() {
try {
loadDependency("org/bouncycastle/bcprov-jdk18on/1.78.1/bcprov-jdk18on-1.78.1.jar");
System.out.println("Bouncy Castle provider loaded");
} catch (Throwable t) {
System.out.println("Failed to load Bouncy Castle providers");
t.printStackTrace();
}
}

public static void loadBrotli() {
if (System.getProperty("skytils.noNativeBrotli") != null) {
System.out.println("Native Brotli disabled by system property");
Expand Down
85 changes: 61 additions & 24 deletions src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.launch
import net.minecraft.client.gui.GuiButton
import net.minecraft.client.gui.GuiScreen
import net.minecraft.util.EnumChatFormatting
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
import org.bouncycastle.openpgp.PGPSignatureList
import org.bouncycastle.openpgp.PGPUtil
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider
import java.io.File
import java.security.Security
import kotlin.math.floor

/**
Expand All @@ -46,12 +53,14 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() {
companion object {
private val DOTS = arrayOf(".", "..", "...", "...", "...")
private const val DOT_TIME = 200 // ms between "." -> ".." -> "..."
var failed = false
var complete = false
}

private var backButton: GuiButton? = null
private var progress = 0.0
private var stage = "Downloading"
var failed = false

override fun initGui() {
buttonList.add(GuiButton(0, width / 2 - 100, height / 3 * 2, 200, 20, "").also { backButton = it })
updateText()
Expand All @@ -63,14 +72,48 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() {
val url = UpdateChecker.updateDownloadURL
val jarName = UpdateChecker.getJarNameFromUrl(url)
IO.launch(CoroutineName("Skytils-update-downloader-thread")) {
downloadUpdate(url, directory)
val updateFile = downloadUpdate(url, directory)
val signFile = downloadUpdate("$url.asc", directory)
if (!failed) {
UpdateChecker.scheduleCopyUpdateAtShutdown(jarName)
if (restartNow) {
mc.shutdown()
if (updateFile != null && signFile != null) {
stage = "Verifying signature"
val finger = JcaKeyFingerprintCalculator()

fun getKeyRingCollection(fileName: String): PGPPublicKeyRingCollection =
this::class.java.classLoader.getResourceAsStream("assets/skytils/$fileName.gpg")!!.use {
PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(it), finger)
}

val keys = listOf(
getKeyRingCollection("my-name-is-jeff"),
getKeyRingCollection("sychic")
)

val sig = (JcaPGPObjectFactory(PGPUtil.getDecoderStream(signFile.inputStream())).nextObject() as PGPSignatureList).first()
val key = keys.firstNotNullOfOrNull { it.getPublicKey(sig.keyID) }
if (key != null) {
sig.init(JcaPGPContentVerifierBuilderProvider().setProvider(Security.getProvider("BC") ?: BouncyCastleProvider().also(Security::addProvider)), key)
sig.update(updateFile.readBytes())
if (sig.verify()) {
signFile.deleteOnExit()
UpdateChecker.scheduleCopyUpdateAtShutdown(jarName)
if (restartNow) {
mc.shutdown()
}
complete = true
updateText()
} else {
failed = true
println("Signature verification failed")
}
} else {
println("Key not found")
failed = true
}
} else {
println("Files are missing")
failed = true
}
complete = true
updateText()
}
}
} catch (ex: Exception) {
Expand All @@ -82,7 +125,7 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() {
backButton!!.displayString = if (failed || complete) "Back" else "Cancel"
}

private suspend fun downloadUpdate(urlString: String, directory: File) {
private suspend fun downloadUpdate(urlString: String, directory: File): File? {
try {
val url = Url(urlString)

Expand All @@ -102,25 +145,27 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() {
failed = true
updateText()
println("$url returned status code ${st.status}")
return
return null
}
if (!directory.exists() && !directory.mkdirs()) {
failed = true
updateText()
println("Couldn't create update file directory")
return
return null
}
val fileSaved = File(directory, url.pathSegments.last().decodeURLPart())
if (mc.currentScreen !== this@UpdateGui || st.bodyAsChannel().copyTo(fileSaved.writeChannel()) == 0L) {
failed = true
return
return null
}
println("Downloaded update to $fileSaved")
return fileSaved
} catch (ex: Exception) {
ex.printStackTrace()
failed = true
updateText()
}
return null
}

public override fun actionPerformed(button: GuiButton) {
Expand All @@ -134,14 +179,14 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() {
when {
failed -> drawCenteredString(
mc.fontRendererObj,
EnumChatFormatting.RED.toString() + "Update download failed",
"§cUpdate download failed",
width / 2,
height / 2,
-0x1
)
complete -> drawCenteredString(
mc.fontRendererObj,
EnumChatFormatting.GREEN.toString() + "Update download complete",
"§aUpdate download complete",
width / 2,
height / 2,
0xFFFFFF
Expand All @@ -162,16 +207,8 @@ class UpdateGui(restartNow: Boolean) : GuiScreen() {
top + 3,
-0x1000000
)
val x = (width - mc.fontRendererObj.getStringWidth(
String.format(
"Downloading %s",
DOTS[DOTS.size - 1]
)
)) / 2
val title = String.format(
"Downloading %s",
DOTS[(System.currentTimeMillis() % (DOT_TIME * DOTS.size)).toInt() / DOT_TIME]
)
val x = (width - mc.fontRendererObj.getStringWidth("$stage ${DOTS[DOTS.size - 1]}")) / 2
val title = "$stage ${DOTS[(System.currentTimeMillis() % (DOT_TIME * DOTS.size)).toInt() / DOT_TIME]}"
drawString(mc.fontRendererObj, title, x, top - mc.fontRendererObj.FONT_HEIGHT - 2, -0x1)
}
}
Expand Down
64 changes: 64 additions & 0 deletions src/main/resources/assets/skytils/my-name-is-jeff.gpg
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

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
=Nkmk
-----END PGP PUBLIC KEY BLOCK-----
86 changes: 86 additions & 0 deletions src/main/resources/assets/skytils/sychic.gpg
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

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
LBxoCg==
=WLv5
-----END PGP PUBLIC KEY BLOCK-----

0 comments on commit 856ded9

Please sign in to comment.