mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-19 12:25:12 -04:00
properly verify assets, GH-6
This commit is contained in:
parent
8bde0bc65e
commit
6f071c81d0
@ -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")
|
@ -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")
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user