properly verify assets, GH-6

This commit is contained in:
Bixilon 2022-07-10 12:06:44 +02:00
parent 8bde0bc65e
commit 6f071c81d0
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
6 changed files with 134 additions and 24 deletions

View File

@ -0,0 +1,19 @@
/*
* Minosoft
* Copyright (C) 2020-2022 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.assets.error
import de.bixilon.minosoft.data.registries.ResourceLocation
import java.io.IOException
class AssetCorruptedError(path: ResourceLocation) : IOException("Asset corrupted: $path")

View File

@ -0,0 +1,19 @@
/*
* Minosoft
* Copyright (C) 2020-2022 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.assets.error
import de.bixilon.minosoft.data.registries.ResourceLocation
import java.io.FileNotFoundException
class AssetNotFoundError(path: ResourceLocation) : FileNotFoundException("Asset not found: $path")

View File

@ -18,7 +18,6 @@ import de.bixilon.kutil.latch.CountUpAndDownLatch
import de.bixilon.kutil.string.StringUtil.formatPlaceholder
import de.bixilon.minosoft.assets.InvalidAssetException
import de.bixilon.minosoft.assets.util.FileAssetsUtil
import de.bixilon.minosoft.assets.util.FileUtil
import de.bixilon.minosoft.assets.util.FileUtil.readArchive
import de.bixilon.minosoft.assets.util.FileUtil.readZipArchive
import de.bixilon.minosoft.config.profile.profiles.resources.ResourcesProfile
@ -28,6 +27,9 @@ import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import de.bixilon.minosoft.util.KUtil.generalize
import de.bixilon.minosoft.util.KUtil.toResourceLocation
import de.bixilon.minosoft.util.json.Jackson
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import org.kamranzafar.jtar.TarEntry
import org.kamranzafar.jtar.TarHeader
import org.kamranzafar.jtar.TarOutputStream
@ -53,15 +55,15 @@ class JarAssetsManager(
override fun load(latch: CountUpAndDownLatch) {
check(!loaded) { "Already loaded!" }
val jarAssetFile = File(FileAssetsUtil.getPath(jarAssetsHash))
if (FileAssetsUtil.verifyAsset(jarAssetsHash, jarAssetFile, profile.verify)) {
val jarAssets = FileUtil.readFile(jarAssetFile).readArchive()
val jarAssets = FileAssetsUtil.readVerified(jarAssetsHash, profile.verify)?.readArchive()
if (jarAssets != null) {
for ((path, data) in jarAssets) {
this.jarAssets[path.removePrefix("assets/" + ProtocolDefinition.DEFAULT_NAMESPACE + "/")] = data
}
} else {
var clientJar = FileUtil.safeReadFile(File(FileAssetsUtil.getPath(clientJarHash)), false)?.readZipArchive()
var clientJar = FileAssetsUtil.readVerified(clientJarHash, profile.verify)?.readZipArchive()
if (clientJar == null) {
Log.log(LogMessageType.ASSETS, LogLevels.VERBOSE) { "Downloading minecraft jar ($clientJarHash)" }
val downloaded = FileAssetsUtil.downloadAndGetAsset(
profile.source.pistonObjects.formatPlaceholder(
"fullHash" to clientJarHash,

View File

@ -21,10 +21,11 @@ import de.bixilon.kutil.json.JsonUtil.asJsonObject
import de.bixilon.kutil.latch.CountUpAndDownLatch
import de.bixilon.kutil.primitive.LongUtil.toLong
import de.bixilon.kutil.string.StringUtil.formatPlaceholder
import de.bixilon.minosoft.assets.error.AssetCorruptedError
import de.bixilon.minosoft.assets.error.AssetNotFoundError
import de.bixilon.minosoft.assets.minecraft.MinecraftAssetsManager
import de.bixilon.minosoft.assets.util.FileAssetsUtil
import de.bixilon.minosoft.assets.util.FileAssetsUtil.toAssetName
import de.bixilon.minosoft.assets.util.FileUtil
import de.bixilon.minosoft.assets.util.FileUtil.readJsonObject
import de.bixilon.minosoft.config.StaticConfiguration
import de.bixilon.minosoft.config.profile.profiles.resources.ResourcesProfile
@ -35,7 +36,6 @@ import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
@ -55,6 +55,7 @@ class IndexAssetsManager(
private set
private fun downloadAssetsIndex(): Map<String, Any> {
Log.log(LogMessageType.ASSETS, LogLevels.VERBOSE) { "Downloading assets index ($indexHash)" }
return Jackson.MAPPER.readValue(
FileAssetsUtil.downloadAndGetAsset(
profile.source.mojangPackages.formatPlaceholder(
@ -84,7 +85,7 @@ class IndexAssetsManager(
override fun load(latch: CountUpAndDownLatch) {
check(!loaded) { "Already loaded!" }
var assets = FileUtil.safeReadFile(FileAssetsUtil.getPath(indexHash))?.readJsonObject() ?: downloadAssetsIndex()
var assets = FileAssetsUtil.readVerified(indexHash, verify)?.readJsonObject() ?: downloadAssetsIndex()
assets["objects"].let { assets = it.asJsonObject() }
val tasks = CountUpAndDownLatch(0)
@ -143,10 +144,10 @@ class IndexAssetsManager(
}
override fun get(path: ResourceLocation): InputStream {
return FileUtil.readFile(FileAssetsUtil.getPath(assets[path]?.hash ?: throw FileNotFoundException("Could not find asset $path")))
return FileAssetsUtil.readVerified(assets[path]?.hash ?: throw AssetNotFoundError(path), verify, hashType = FileAssetsUtil.HashTypes.SHA1) ?: throw AssetCorruptedError(path)
}
override fun getOrNull(path: ResourceLocation): InputStream? {
return FileUtil.readFile(FileAssetsUtil.getPath(assets[path]?.hash ?: return null))
return FileAssetsUtil.readVerified(assets[path]?.hash ?: return null, verify, hashType = FileAssetsUtil.HashTypes.SHA1) ?: throw AssetCorruptedError(path)
}
}

View File

@ -32,7 +32,7 @@ object AssetsPropertiesGenerator {
profile.verify = false
val (versionId, clientJarHash) = args
val assetsManager = JarAssetsManager("1233456789abcdef", clientJarHash, profile, Version(versionId, -1, -1, VersionTypes.APRIL_FOOL, emptyMap(), emptyMap()))
val assetsManager = JarAssetsManager("829c3804401b0727f70f73d4415e162400cbe57b", clientJarHash, profile, Version(versionId, -1, -1, VersionTypes.APRIL_FOOL, emptyMap(), emptyMap()))
try {
assetsManager.load(CountUpAndDownLatch(1))
} catch (exception: InvalidAssetException) {

View File

@ -16,9 +16,12 @@ package de.bixilon.minosoft.assets.util
import com.github.luben.zstd.ZstdInputStream
import com.github.luben.zstd.ZstdOutputStream
import de.bixilon.kutil.array.ByteArrayUtil.toHex
import de.bixilon.kutil.enums.EnumUtil
import de.bixilon.kutil.enums.ValuesEnum
import de.bixilon.kutil.hex.HexUtil.isHexString
import de.bixilon.kutil.random.RandomStringUtil.randomString
import de.bixilon.minosoft.assets.AssetsManager
import de.bixilon.minosoft.assets.util.FileAssetsUtil.HashTypes.Companion.hashType
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import de.bixilon.minosoft.terminal.RunConfiguration
@ -134,15 +137,7 @@ object FileAssetsUtil {
}
fun verifyAsset(hash: String, file: File = File(getPath(hash)), verify: Boolean, hashType: HashTypes = HashTypes.SHA256, compress: Boolean = true): Boolean {
if (!file.exists()) {
return false
}
if (!file.isFile) {
throw IllegalStateException("File is not a file: $file")
}
val size = file.length()
if (size < 0) {
file.delete()
if (!verifyAssetBasic(file)) {
return false
}
if (!verify) {
@ -152,10 +147,7 @@ object FileAssetsUtil {
try {
val digest = hashType.createDigest()
var input: InputStream = FileInputStream(file)
if (compress) {
input = ZstdInputStream(input)
}
val input = openStream(file, compress)
val buffer = ByteArray(ProtocolDefinition.DEFAULT_BUFFER_SIZE)
var length: Int
@ -177,6 +169,68 @@ object FileAssetsUtil {
}
}
private fun verifyAssetBasic(file: File): Boolean {
if (!file.exists()) {
return false
}
if (!file.isFile) {
throw IllegalStateException("File is not a file: $file")
}
val size = file.length()
if (size < 0) {
file.delete()
return false
}
return true
}
fun readVerified(hash: String, verify: Boolean, hashType: HashTypes = hash.hashType, compress: Boolean = true): InputStream? {
val file = File(getPath(hash))
if (!verifyAssetBasic(file)) {
return null
}
val stream = openStream(file, compress)
if (!verify) {
return stream
}
try {
val digest = hashType.createDigest()
val input = openStream(file, compress)
val output = ByteArrayOutputStream(if (compress) input.available() else maxOf(1000, input.available()))
val buffer = ByteArray(ProtocolDefinition.DEFAULT_BUFFER_SIZE)
var length: Int
while (true) {
length = input.read(buffer, 0, buffer.size)
if (length < 0) {
break
}
digest.update(buffer, 0, length)
output.write(buffer, 0, length)
}
val equals = hash == digest.digest().toHex()
if (!equals) {
file.delete()
return null
}
return ByteArrayInputStream(output.toByteArray())
} catch (exception: Throwable) {
file.delete()
return null
}
}
private fun openStream(file: File, compress: Boolean): InputStream {
var input: InputStream = FileInputStream(file)
if (compress) {
input = ZstdInputStream(input)
}
return input
}
enum class HashTypes(
val digestName: String,
val length: Int,
@ -188,5 +242,20 @@ object FileAssetsUtil {
fun createDigest(): MessageDigest {
return MessageDigest.getInstance(digestName)
}
companion object : ValuesEnum<HashTypes> {
override val VALUES: Array<HashTypes> = values()
override val NAME_MAP: Map<String, HashTypes> = EnumUtil.getEnumValues(VALUES)
val String.hashType: HashTypes
get() {
for (type in VALUES) {
if (this.length == type.length) {
return type
}
}
throw IllegalArgumentException("Can not determinate hash type: $this")
}
}
}
}