basic update checking

No updates available yet...Lets see.
This commit is contained in:
Moritz Zwerger 2023-12-20 18:32:26 +01:00
parent ec602af0ef
commit 482d337fcb
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
27 changed files with 639 additions and 64 deletions

58
doc/Updater.md Normal file
View File

@ -0,0 +1,58 @@
# Updater
Minosoft can (and will by default; it asks first) check for updates at startup.
## Update check
Minosoft fetches a configured url with the following parameters:
- current commit (if available)
- current version
- configured update channel (`stable` by default)
- os: current operating system (binaries are platform specific)
- arch: current cpu architecture (e.g. `x64` or `x86`)
### Response (v1)
The server internally checks if the user can receive updates for the version or platform. If no update is available, it responds with `204 No content`.
Otherwise it responds with `200 OK` and returns a json object:
```json
{
"id": "Version id",
"name": "Display text of the version",
"stable": "Build from a tag or not",
"page": "<Optional link to a release page>",
"date": "Unix timestamp (in seconds) of the release date",
"download": {
// optional
"url": "https:// where to download it",
"size": <Update size in Bytes>,
"sha512": "SHA512 hash of the binary",
"signature": "Release signature"
},
"release_notes": "<Optional text for release notes to show in the client>"
}
```
All urls in the response **must** start with `https://` or be a localhost link.
## Update process
When an update is available, it prompts the user the install it. It tries to store the file in the current directory (if possible) or asks the user where to store it.
Once it is downloaded, minosoft asks the user to restart. It will then quit and starts the new jar.
The client will refuse to update, if the release date of the next version is lower than the version currently running (i.e. no downgrades)
## Signature
The signature is created by base64 appending the following strings (in order) together and then using RSA (`SHA512withRSA`) to sign it:
1. Version id
2. release date (number to string)
3. Release page (optional)
4. sha512 hash of the binary
## Future
- Maybe split the fat jar and download all dependencies individual (reduces size of the binary; lowers traffic)

View File

@ -0,0 +1,33 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.updater
import org.testng.annotations.Test
@Test
class MinosoftUpdateTest {
fun `invalid signature`() {
TODO()
}
fun `older signature`() {
TODO()
}
fun `correct signature`() {
}
}

View File

@ -51,6 +51,6 @@ class YggdrasilUtilTest {
val texture = "ewogICJ0aW1lc3RhbXAiIDogMTY5MDQwMTIzODM2NCwKICAicHJvZmlsZUlkIiA6ICJhODUxNzQ0MDNlNjg0MDgwYWNkODU3MzhlMjI5NGNhZiIsCiAgInByb2ZpbGVOYW1lIiA6ICJEYVJpdmVyc09uZSIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS80YjJjMjAwYjA1ZWY4NDhiY2IyZGM2YTBlOGY2OTQ0MWE3YzE3Mzg4Y2FjY2UzNzAxMzg5YTU0OGM2NTdjMzZkIgogICAgfQogIH0KfQ=="
val signature = ""
assertThrows { YggdrasilUtil.requireSignature(texture, signature) }
assertThrows { YggdrasilUtil.require(texture, signature) }
}
}

View File

@ -32,6 +32,7 @@ import de.bixilon.kutil.unit.UnitFormatter.formatNanos
import de.bixilon.minosoft.assets.IntegratedAssets
import de.bixilon.minosoft.config.StaticConfiguration
import de.bixilon.minosoft.config.profile.profiles.eros.ErosProfileManager
import de.bixilon.minosoft.config.profile.profiles.other.OtherProfileManager
import de.bixilon.minosoft.data.language.IntegratedLanguage
import de.bixilon.minosoft.data.text.formatting.FormattingCodes
import de.bixilon.minosoft.data.text.formatting.color.ChatColors
@ -49,6 +50,7 @@ import de.bixilon.minosoft.properties.MinosoftPropertiesLoader
import de.bixilon.minosoft.terminal.AutoConnect
import de.bixilon.minosoft.terminal.CommandLineArguments
import de.bixilon.minosoft.terminal.RunConfiguration
import de.bixilon.minosoft.updater.MinosoftUpdater
import de.bixilon.minosoft.util.KUtil
import de.bixilon.minosoft.util.json.Jackson
import de.bixilon.minosoft.util.logging.Log
@ -110,6 +112,8 @@ object Minosoft {
KUtil.initPlayClasses()
GlobalEventMaster.fire(FinishBootEvent())
DefaultThreadPool += { DefaultModPhases.POST.load(); Log.log(LogMessageType.MOD_LOADING, LogLevels.INFO) { "Mod loading completed!" } }
checkForUpdates()
if (RunConfiguration.DISABLE_EROS) {
Log.log(LogMessageType.GENERAL, LogLevels.WARN) { "Eros is disabled, no gui will show up! Use the cli to connect to servers!" }
}
@ -163,4 +167,12 @@ object Minosoft {
Log.log(LogMessageType.GENERAL, LogLevels.WARN) { "You are using macOS. To use rendering you must not set the jvm argument §9-XstartOnFirstThread§r. Please remove it!" }
ShutdownManager.shutdown(reason = AbstractShutdownReason.CRASH)
}
fun checkForUpdates() {
if (!OtherProfileManager.selected.updater.check) return
DefaultThreadPool += ForcePooledRunnable(priority = ThreadPool.LOW) {
val update = MinosoftUpdater.check() ?: return@ForcePooledRunnable
Log.log(LogMessageType.OTHER, LogLevels.INFO) { "A new update is available: ${update.name} (${update.id}). Type \"update\" or click in the gui to update." }
}
}
}

View File

@ -18,6 +18,7 @@ import de.bixilon.minosoft.config.profile.ProfileType
import de.bixilon.minosoft.config.profile.delegate.primitive.BooleanDelegate
import de.bixilon.minosoft.config.profile.profiles.Profile
import de.bixilon.minosoft.config.profile.profiles.other.log.LogC
import de.bixilon.minosoft.config.profile.profiles.other.updater.UpdaterC
import de.bixilon.minosoft.config.profile.storage.ProfileStorage
import de.bixilon.minosoft.data.registries.identified.Namespaces.minosoft
import org.kordamp.ikonli.Ikon
@ -42,6 +43,7 @@ class OtherProfile(
var listenLAN by BooleanDelegate(this, true)
val log = LogC(this)
val updater = UpdaterC(this)
override fun toString(): String {

View File

@ -0,0 +1,55 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.config.profile.profiles.other.updater
import de.bixilon.minosoft.config.profile.delegate.primitive.BooleanDelegate
import de.bixilon.minosoft.config.profile.delegate.types.NullableStringDelegate
import de.bixilon.minosoft.config.profile.delegate.types.StringDelegate
import de.bixilon.minosoft.config.profile.profiles.other.OtherProfile
class UpdaterC(profile: OtherProfile) {
/**
* The user did not decide whether to check for updates
*/
var ask by BooleanDelegate(profile, true)
/**
* Check for updates
*/
var check by BooleanDelegate(profile, true)
/**
* Update channel
* If automatic, it chooses either stable or beta matching the current executed version
*/
var channel by StringDelegate(profile, "auto")
/**
* URL to check for updates
* Possible variables:
* - VERSION: current version string
* - CHANNEL: target channel
* - COMMIT: optional: the current commit
* - OS: Operating system (windows, linux, mac, ...)
* - ARCH: architecture of the current build (x86, x64, arm64)
*/
var url by StringDelegate(profile, "https://minosoft.bixilon.de/v1/updates?version=\${VERSION}&channel=\${CHANNEL}&commit=\${COMMIT}&os=\${OS}&arch=\${ARCH}")
/**
* If the newest version matches this field, it won't be shown
*/
var dismiss by NullableStringDelegate(profile, null)
}

View File

@ -19,7 +19,7 @@ import de.bixilon.kutil.json.JsonUtil.toJsonObject
import de.bixilon.kutil.primitive.LongUtil.toLong
import de.bixilon.kutil.uuid.UUIDUtil.toUUID
import de.bixilon.minosoft.util.json.Jackson
import de.bixilon.minosoft.util.yggdrasil.YggdrasilException
import de.bixilon.minosoft.util.signature.SignatureException
import de.bixilon.minosoft.util.yggdrasil.YggdrasilUtil
import java.util.*
@ -36,7 +36,7 @@ data class PlayerTextures(
fun of(encoded: String, signature: String): PlayerTextures {
if (!YggdrasilUtil.verify(encoded, signature)) {
throw YggdrasilException("Texture signature is invalid!")
throw SignatureException("Texture signature is invalid!")
}
val json: Map<String, Any> = Jackson.MAPPER.readValue(Base64.getDecoder().decode(encoded), Jackson.JSON_MAP_TYPE)

View File

@ -0,0 +1,51 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.text.events.click
import de.bixilon.kotlinglm.vec2.Vec2
import de.bixilon.kutil.json.JsonObject
import de.bixilon.minosoft.gui.rendering.gui.GUIRenderer
import de.bixilon.minosoft.gui.rendering.gui.input.mouse.MouseActions
import de.bixilon.minosoft.gui.rendering.gui.input.mouse.MouseButtons
import de.bixilon.minosoft.terminal.cli.CLI
class InternalCommandClickEvent(
val command: String,
) : ClickEvent {
override fun onClick(guiRenderer: GUIRenderer, position: Vec2, button: MouseButtons, action: MouseActions) {
if (button != MouseButtons.LEFT || action != MouseActions.PRESS) {
return
}
CLI.execute(command)
}
override fun hashCode(): Int {
return command.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other !is InternalCommandClickEvent) return false
return other.command == command
}
companion object : ClickEventFactory<InternalCommandClickEvent> {
override val name: String = "internal_command"
override fun build(json: JsonObject, restricted: Boolean): InternalCommandClickEvent {
if (restricted) throw IllegalStateException("Its restricted")
return InternalCommandClickEvent(json.data.toString())
}
}
}

View File

@ -20,7 +20,7 @@ enum class BootTasks {
ASSETS_PROPERTIES,
DEFAULT_REGISTRIES,
LAN_SERVERS,
YGGDRASIL,
KEYS,
STARTUP_PROGRESS,
ASSETS_OVERRIDE,
CLI,

View File

@ -29,6 +29,7 @@ import de.bixilon.minosoft.modding.loader.phase.DefaultModPhases
import de.bixilon.minosoft.protocol.protocol.LANServerListener
import de.bixilon.minosoft.protocol.versions.VersionLoader
import de.bixilon.minosoft.terminal.cli.CLI
import de.bixilon.minosoft.updater.UpdateKey
import de.bixilon.minosoft.util.yggdrasil.YggdrasilUtil
object MinosoftBoot {
@ -44,7 +45,7 @@ object MinosoftBoot {
worker += WorkerTask(identifier = BootTasks.LAN_SERVERS, dependencies = arrayOf(BootTasks.PROFILES), executor = LANServerListener::listen)
worker += WorkerTask(identifier = BootTasks.YGGDRASIL, executor = { YggdrasilUtil.load() })
worker += WorkerTask(identifier = BootTasks.KEYS, executor = { YggdrasilUtil.load(); UpdateKey.load() })
worker += WorkerTask(identifier = BootTasks.ASSETS_OVERRIDE, executor = { IntegratedAssets.OVERRIDE.load(it) })
worker += WorkerTask(identifier = BootTasks.MODS, executor = { DefaultModPhases.BOOT.load(it) })

View File

@ -31,8 +31,9 @@ object MinosoftPropertiesLoader {
fun load() {
val properties = IntegratedAssets.DEFAULT.getOrNull(minosoft("version.json"))?.readJson<MinosoftP>(reader = reader)
MinosoftProperties = if (properties == null) {
Log.log(LogMessageType.OTHER, LogLevels.FATAL) { "Can not load version.json! Did you compile with gradle?" }
MinosoftP(GeneralP("unknown", false), null)
// it should be fatal, but then users report it as error if they use an ide or ...
Log.log(LogMessageType.OTHER, LogLevels.WARN) { "Can not load build info! Do you have git installed and compiled with gradle? Updater will not work." }
MinosoftP(GeneralP("unknown", 0L, false), null)
} else {
properties
}

View File

@ -1,6 +1,6 @@
/*
* Minosoft
* Copyright (C) 2020-2022 Moritz Zwerger
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
@ -15,5 +15,6 @@ package de.bixilon.minosoft.properties.general
class GeneralP(
val name: String,
val date: Long,
val stable: Boolean,
)

View File

@ -21,7 +21,7 @@ import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager.encodeNetwork
import de.bixilon.minosoft.util.account.minecraft.key.MinecraftKeyPair
import de.bixilon.minosoft.util.yggdrasil.YggdrasilException
import de.bixilon.minosoft.util.signature.SignatureException
import de.bixilon.minosoft.util.yggdrasil.YggdrasilUtil
import java.nio.charset.StandardCharsets
import java.security.PublicKey
@ -60,9 +60,9 @@ class PlayerPublicKey(
fun requireSignature(versionId: Int, uuid: UUID) {
if (versionId < ProtocolVersions.V_1_19_1_PRE4) {
if (!isLegacySignatureCorrect()) throw YggdrasilException()
if (!isLegacySignatureCorrect()) throw SignatureException()
} else {
if (!isSignatureCorrect(uuid)) throw YggdrasilException()
if (!isSignatureCorrect(uuid)) throw SignatureException()
}
}

View File

@ -11,8 +11,9 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.terminal.commands
package de.bixilon.minosoft.terminal
import de.bixilon.minosoft.terminal.commands.*
import de.bixilon.minosoft.terminal.commands.connection.*
import de.bixilon.minosoft.terminal.commands.rendering.ReloadCommand
@ -27,7 +28,7 @@ object Commands {
CrashCommand, DumpCommand,
AboutCommand,
AboutCommand, UpdateCommand,
SayCommand,

View File

@ -24,7 +24,7 @@ import de.bixilon.minosoft.commands.errors.ReaderError
import de.bixilon.minosoft.commands.nodes.RootNode
import de.bixilon.minosoft.main.MinosoftBoot
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.terminal.commands.Commands
import de.bixilon.minosoft.terminal.Commands
import de.bixilon.minosoft.terminal.commands.connection.ConnectionCommand
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
@ -95,7 +95,7 @@ object CLI {
}
if (line.isBlank()) continue
processLine(line)
execute(line)
}
}
@ -104,7 +104,7 @@ object CLI {
Log.log(LogMessageType.GENERAL, LogLevels.WARN) { "End of file error in cli thread. Disabling cli." }
}
private fun processLine(line: String) {
fun execute(line: String) {
try {
commands.execute(line, connection)
} catch (error: ReaderError) {

View File

@ -0,0 +1,101 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.terminal.commands
import de.bixilon.kutil.concurrent.pool.DefaultThreadPool
import de.bixilon.minosoft.commands.nodes.LiteralNode
import de.bixilon.minosoft.commands.stack.print.PrintTarget
import de.bixilon.minosoft.config.profile.profiles.other.OtherProfileManager
import de.bixilon.minosoft.data.text.BaseComponent
import de.bixilon.minosoft.data.text.TextComponent
import de.bixilon.minosoft.data.text.events.click.InternalCommandClickEvent
import de.bixilon.minosoft.updater.MinosoftUpdate
import de.bixilon.minosoft.updater.MinosoftUpdater
import de.bixilon.minosoft.updater.UpdateProgress
object UpdateCommand : Command {
override var node: LiteralNode = LiteralNode("update", executor = { it.print.check() }).addChild(
LiteralNode("notes", executor = {
val update = MinosoftUpdater.update
if (update != null) {
update.printNotes(it.print)
return@LiteralNode
}
it.print.print("Fetching update release notes...")
DefaultThreadPool += {
val update = MinosoftUpdater.check()
if (update == null) {
print("No update available!")
} else {
update.printNotes(it.print)
}
}
}),
LiteralNode("update", executor = {
val update = MinosoftUpdater.update
if (update != null) {
DefaultThreadPool += {
val progress = UpdateProgress(log = it.print)
MinosoftUpdater.update(update, progress)
}
return@LiteralNode
}
it.print.print("Fetching update details...")
DefaultThreadPool += {
val update = MinosoftUpdater.check()
if (update == null) {
print("No update available!")
} else {
DefaultThreadPool += {
val progress = UpdateProgress(log = it.print)
MinosoftUpdater.update(update, progress)
}
}
}
}),
LiteralNode("dismiss", executor = {
val update = MinosoftUpdater.update
if (update == null) {
it.print.print("§cNot checked for updates!")
return@LiteralNode
}
it.print.print("Dismissed update ${update.name} (${update.id})")
OtherProfileManager.selected.updater.dismiss = update.id
}),
)
private fun PrintTarget.check() {
print("Checking for updates...")
DefaultThreadPool += {
val update = MinosoftUpdater.check()
if (update == null) {
print("No update available!")
} else {
print("There is a new update available:")
print("Version: ${update.name} (${update.id})")
print(BaseComponent("Run ", TextComponent("\"update notes\"").clickEvent(InternalCommandClickEvent("update notes")), " to see the release notes."))
print(BaseComponent("Run ", TextComponent("\"update update\"").clickEvent(InternalCommandClickEvent("update update")), " to download and update."))
}
}
}
private fun MinosoftUpdate.printNotes(target: PrintTarget) {
if (releaseNotes == null) {
target.print("No release notes for $name ($id) available.")
return
}
target.print("Release notes for update $name ($id):")
target.print(releaseNotes)
}
}

View File

@ -0,0 +1,23 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.updater
import java.net.URL
data class DownloadLink(
val url: URL,
val size: Int,
val sha512: String,
val signature: String,
)

View File

@ -0,0 +1,45 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.updater
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.properties.MinosoftProperties
import java.net.URL
data class MinosoftUpdate(
val id: String,
val name: String,
val date: Long,
val stable: Boolean,
val page: URL? = null,
val download: DownloadLink? = null,
val releaseNotes: ChatComponent? = null,
) {
init {
verify()
}
private fun verify() {
if (MinosoftProperties.general.date >= date) throw Exception("Update is older than the current version!")
if (this.download == null) return
val builder = StringBuilder()
builder.append(id)
builder.append(date)
page?.let { builder.append(it) }
builder.append(download.sha512)
UpdateKey.require(builder.toString(), download.signature)
}
}

View File

@ -0,0 +1,90 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.updater
import de.bixilon.kutil.observer.DataObserver.Companion.observed
import de.bixilon.kutil.os.PlatformInfo
import de.bixilon.kutil.string.StringUtil.formatPlaceholder
import de.bixilon.kutil.url.URLUtil.toURL
import de.bixilon.minosoft.config.profile.profiles.other.OtherProfileManager
import de.bixilon.minosoft.properties.MinosoftProperties
import de.bixilon.minosoft.util.http.HTTP2.getJson
import de.bixilon.minosoft.util.http.exceptions.HTTPException
import de.bixilon.minosoft.util.json.Jackson
import java.net.URL
object MinosoftUpdater {
var update: MinosoftUpdate? by observed(null)
private set
private fun validateURL(url: URL) {
if (url.protocol == "https") return
if (url.protocol == "http") {
if (url.host == "localhost" || url.host == "127.0.0.1") return
throw IllegalArgumentException("Using non secure hosts on http is not allowed: $url!")
}
throw IllegalStateException("Illegal protocol: $url")
}
fun check(): MinosoftUpdate? {
val profile = OtherProfileManager.selected.updater
return check(profile.url.toURL(), profile.channel)
}
fun check(url: URL, channel: String): MinosoftUpdate? {
validateURL(url)
val commit = MinosoftProperties.git?.commit ?: ""
val version = MinosoftProperties.general.name
val stable = MinosoftProperties.general.stable
val os = PlatformInfo.OS
val arch = PlatformInfo.ARCHITECTURE
val request = url.toString().formatPlaceholder(
"\${COMMIT}" to commit,
"\${VERSION}" to version,
"\${STABLE}" to stable,
"\${OS}" to os,
"\${ARCH}" to arch,
"\${CHANNEL}" to channel,
)
val update = request(request)
this.update = update
return update
}
private fun request(url: String): MinosoftUpdate? {
val response = url.getJson()
return when (response.statusCode) {
204 -> null
200 -> Jackson.MAPPER.convertValue(response.body, MinosoftUpdate::class.java)
else -> throw HTTPException(response.statusCode, response.body.toString())
}
}
fun update(update: MinosoftUpdate, progress: UpdateProgress) {
val download = update.download
if (download == null) {
progress.log?.print("Update is unavailable for download. Please download it manually!")
return
}
progress.log?.print("Downloading update...")
progress.log?.print("TODO :)")
progress.stage = UpdateProgress.UpdateStage.FAILED
}
}

View File

@ -0,0 +1,20 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.updater
import de.bixilon.minosoft.data.registries.identified.Namespaces.minosoft
import de.bixilon.minosoft.util.signature.SignatureSigner
object UpdateKey : SignatureSigner(minosoft("updater/release.pub"), "SHA512withRSA")

View File

@ -0,0 +1,37 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.updater
import de.bixilon.kutil.latch.AbstractLatch
import de.bixilon.kutil.latch.SimpleLatch
import de.bixilon.kutil.observer.DataObserver.Companion.observed
import de.bixilon.minosoft.commands.stack.print.PrintTarget
class UpdateProgress(
val progress: AbstractLatch = SimpleLatch(0),
var log: PrintTarget? = null,
) {
var stage by observed(UpdateStage.WAITING)
var error: Throwable? = null
enum class UpdateStage {
WAITING,
DOWNLOADING,
VERIFYING,
STORING,
DONE,
FAILED,
}
}

View File

@ -57,7 +57,7 @@ data class MinecraftPrivateKey(
@JsonIgnore
fun requireSignature(uuid: UUID) {
MinecraftKeyPair.requireSignature(uuid, expiresAt, pair.public, signatureBytesV2)
signatureBytes?.let { YggdrasilUtil.requireSignature((expiresAt.toEpochMilli().toString() + pair.publicString).toByteArray(StandardCharsets.US_ASCII), it) }
signatureBytes?.let { YggdrasilUtil.require((expiresAt.toEpochMilli().toString() + pair.publicString).toByteArray(StandardCharsets.US_ASCII), it) }
}
fun getSignature(versionId: Int): ByteArray {

View File

@ -17,7 +17,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import de.bixilon.minosoft.protocol.protocol.buffers.OutByteBuffer
import de.bixilon.minosoft.protocol.protocol.encryption.CryptManager
import de.bixilon.minosoft.util.yggdrasil.YggdrasilException
import de.bixilon.minosoft.util.signature.SignatureException
import de.bixilon.minosoft.util.yggdrasil.YggdrasilUtil
import java.security.PublicKey
import java.time.Instant
@ -42,7 +42,7 @@ data class MinecraftKeyPair(
fun requireSignature(uuid: UUID, expiresAt: Instant, publicKey: PublicKey, signature: ByteArray) {
if (!isSignatureCorrect(uuid, expiresAt, publicKey, signature)) {
throw YggdrasilException()
throw SignatureException()
}
}
}

View File

@ -1,6 +1,6 @@
/*
* Minosoft
* Copyright (C) 2020-2022 Moritz Zwerger
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
@ -11,6 +11,6 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.util.yggdrasil
package de.bixilon.minosoft.util.signature
class YggdrasilException(message: String? = null) : Exception(message)
class SignatureException(message: String? = null) : Exception(message)

View File

@ -0,0 +1,64 @@
/*
* Minosoft
* Copyright (C) 2020-2023 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.util.signature
import de.bixilon.minosoft.assets.IntegratedAssets
import de.bixilon.minosoft.data.registries.identified.ResourceLocation
import java.security.KeyFactory
import java.security.PublicKey
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.*
abstract class SignatureSigner(
val keyPath: ResourceLocation,
val algorithm: String,
) {
private var key: PublicKey? = null
open fun load() {
if (key != null) throw IllegalStateException("Already loaded!")
val spec = X509EncodedKeySpec(IntegratedAssets.DEFAULT[keyPath].readAllBytes())
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
key = keyFactory.generatePublic(spec)
}
protected fun createInstance(): Signature {
val instance = Signature.getInstance(algorithm)
instance.initVerify(key)
return instance
}
open fun verify(data: ByteArray, signature: ByteArray): Boolean {
val instance = createInstance()
instance.update(data)
return instance.verify(signature)
}
fun require(data: ByteArray, signature: ByteArray) {
if (verify(data, signature)) return
throw SignatureException()
}
fun verify(data: String, signature: String): Boolean {
return verify(data.toByteArray(), Base64.getDecoder().decode(signature))
}
fun require(data: String, signature: String) {
if (verify(data, signature)) return
throw SignatureException()
}
}

View File

@ -13,60 +13,26 @@
package de.bixilon.minosoft.util.yggdrasil
import de.bixilon.minosoft.assets.IntegratedAssets
import de.bixilon.minosoft.data.registries.identified.Namespaces.mojang
import de.bixilon.minosoft.terminal.RunConfiguration
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import java.security.KeyFactory
import java.security.PublicKey
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.*
import de.bixilon.minosoft.util.signature.SignatureSigner
object YggdrasilUtil {
lateinit var PUBLIC_KEY: PublicKey
private set
object YggdrasilUtil : SignatureSigner(mojang("yggdrasil/pubkey.der"), "SHA1withRSA") {
fun load() {
override fun load() {
if (RunConfiguration.IGNORE_YGGDRASIL) {
Log.log(LogMessageType.OTHER, LogLevels.WARN) { "Yggdrasil signature checking is disabled. Servers can pretend that they have valid data from mojang!" }
Log.log(LogMessageType.LOADING, LogLevels.WARN) { "Yggdrasil signature checking is disabled. Servers can pretend that they have valid data from mojang!" }
return
}
check(!this::PUBLIC_KEY.isInitialized) { "Already loaded!" }
val spec = X509EncodedKeySpec(IntegratedAssets.DEFAULT[mojang("yggdrasil/pubkey.der")].readAllBytes())
val keyFactory: KeyFactory = KeyFactory.getInstance("RSA")
PUBLIC_KEY = keyFactory.generatePublic(spec)
super.load()
}
fun verify(data: ByteArray, signature: ByteArray): Boolean {
if (RunConfiguration.IGNORE_YGGDRASIL) {
return true
}
val signatureInstance = Signature.getInstance("SHA1withRSA")
signatureInstance.initVerify(PUBLIC_KEY)
signatureInstance.update(data)
return signatureInstance.verify(signature)
}
fun requireSignature(data: ByteArray, signature: ByteArray) {
if (!verify(data, signature)) {
throw YggdrasilException()
}
}
fun verify(data: String, signature: String): Boolean {
if (RunConfiguration.IGNORE_YGGDRASIL) {
return true
}
return verify(data.toByteArray(), Base64.getDecoder().decode(signature))
}
fun requireSignature(data: String, signature: String) {
if (!verify(data, signature)) {
throw YggdrasilException()
}
override fun verify(data: ByteArray, signature: ByteArray): Boolean {
if (RunConfiguration.IGNORE_YGGDRASIL) return true
return super.verify(data, signature)
}
}

View File

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApoG/cXO1wyfitUwf55fs
8ch8JxOmb7RW7YQWGVJ13StZZdsaBPmYNsHkQWwL9G45U4QIEwnlDLpDWPe90pB2
7sf41LHq81PygtxdnByR04fgnIwXYQA2G/A1e7+4CrlyenUCr14F9zjoyVqbUovz
Tf3ibHkJ3DLDSk612tEadJ6LJvc77W4di1GO6V0Kes1WvB1XIM2QhKV1Vyy5QsIf
wpY9RQ4eN3c93YdIhHXElkkdeSAT8csXYiD8ncnt0sDIdrlA5rqDn0wJbkM/tGNP
f7Zbr91RoWZjNCRDCgCDY1Nbs2ZgTL/z2syHoA5AXMzisRGzqCJt8iHSZ/pRlqP6
CWN6wQcSbTNUWpF43I0CkWakM9a1zUgcBkuLEG8yYL/zKFmYo2x3rdjaTyWCQyOO
SWkc4NBVgfwtlgNBrsB7zO+IdIEJwW+Jp5a7yR72AsFjcU5xcz/p9QmNhvWqAFCD
fXrTNHFtr11T91sKdgG2B6E2TbYlyEsKR4BmQfJn7qemlJrIFkX/H3k8wygbnHWg
cHPGcTVAMVxb9rk6M+BAkglG4nS8ldz/ZC/ScaWczwvNPsn0tbkgzql6BOi7jPc0
WO33JN/QxqO00xEg/O5NRXpzSIh3ehHnAQBV1KmxnP1U0bY277F+pwaYXRz5s/iR
KGX3gNbcTlQEfFJe3zKeEuUCAwEAAQ==
-----END PUBLIC KEY-----