diff --git a/build.gradle.kts b/build.gradle.kts index b61e6eb87..90680cde0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") diff --git a/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java b/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java index ed8664cb1..272cc4699 100644 --- a/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java +++ b/src/main/java/gg/skytils/skytilsmod/tweaker/DependencyLoader.java @@ -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; @@ -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 { @@ -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"); diff --git a/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt b/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt index a5b5d75ec..5132c3391 100644 --- a/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt +++ b/src/main/kotlin/gg/skytils/skytilsmod/gui/updater/UpdateGui.kt @@ -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 /** @@ -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() @@ -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) { @@ -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) @@ -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) { @@ -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 @@ -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) } } diff --git a/src/main/resources/assets/skytils/my-name-is-jeff.gpg b/src/main/resources/assets/skytils/my-name-is-jeff.gpg new file mode 100644 index 000000000..7c27ea91c --- /dev/null +++ b/src/main/resources/assets/skytils/my-name-is-jeff.gpg @@ -0,0 +1,64 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=Nkmk +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/src/main/resources/assets/skytils/sychic.gpg b/src/main/resources/assets/skytils/sychic.gpg new file mode 100644 index 000000000..ed38060dd --- /dev/null +++ b/src/main/resources/assets/skytils/sychic.gpg @@ -0,0 +1,86 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +LBxoCg== +=WLv5 +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file