mirror of
https://gitlab.bixilon.de/bixilon/minosoft.git
synced 2025-09-16 19:05:02 -04:00
assets: allow sha256 hashes, check if hash is hex asset (security), save player textures
This commit is contained in:
parent
f8f45818cd
commit
e7f3ac8ecd
@ -78,6 +78,6 @@ class DirectoryAssetsManager(
|
||||
if (path !in assets) {
|
||||
return null
|
||||
}
|
||||
return FileUtil.saveReadFile(path.filePath, false)
|
||||
return FileUtil.safeReadFile(path.filePath, false)
|
||||
}
|
||||
}
|
||||
|
@ -49,13 +49,13 @@ class JarAssetsManager(
|
||||
check(!loaded) { "Already loaded!" }
|
||||
|
||||
val jarAssetFile = File(FileAssetsUtil.getPath(jarAssetsHash))
|
||||
if (FileAssetsUtil.verifyAsset(jarAssetsHash, jarAssetFile, profile.verify)) {
|
||||
if (FileAssetsUtil.verifyAsset(jarAssetsHash, jarAssetFile, profile.verify, FileAssetsUtil.HashTypes.SHA1)) {
|
||||
val jarAssets = FileUtil.readFile(jarAssetFile).readArchive()
|
||||
for ((path, data) in jarAssets) {
|
||||
this.jarAssets[path.removePrefix("assets/" + ProtocolDefinition.DEFAULT_NAMESPACE + "/")] = data
|
||||
}
|
||||
} else {
|
||||
var clientJar = FileUtil.saveReadFile(File(FileAssetsUtil.getPath(clientJarHash)), false)?.readZipArchive()
|
||||
var clientJar = FileUtil.safeReadFile(File(FileAssetsUtil.getPath(clientJarHash)), false)?.readZipArchive()
|
||||
if (clientJar == null) {
|
||||
val downloaded = FileAssetsUtil.downloadAndGetAsset(Util.formatString(profile.source.launcherPackages, mapOf(
|
||||
"fullHash" to clientJarHash,
|
||||
|
@ -59,12 +59,12 @@ class IndexAssetsManager(
|
||||
mapOf(
|
||||
"fullHash" to indexHash,
|
||||
"filename" to "$assetsVersion.json",
|
||||
))).second, Jackson.JSON_MAP_TYPE)
|
||||
)), hashType = FileAssetsUtil.HashTypes.SHA1).second, Jackson.JSON_MAP_TYPE)
|
||||
}
|
||||
|
||||
fun verifyAsset(hash: String) {
|
||||
val file = File(FileAssetsUtil.getPath(hash))
|
||||
if (FileAssetsUtil.verifyAsset(hash, file, verify)) {
|
||||
if (FileAssetsUtil.verifyAsset(hash, file, verify, hashType = FileAssetsUtil.HashTypes.SHA1)) {
|
||||
return
|
||||
}
|
||||
val url = Util.formatString(profile.source.minecraftResources,
|
||||
@ -73,7 +73,7 @@ class IndexAssetsManager(
|
||||
"fullHash" to hash,
|
||||
))
|
||||
Log.log(LogMessageType.ASSETS, LogLevels.VERBOSE) { "Downloading asset $url" }
|
||||
val downloadedHash = FileAssetsUtil.downloadAsset(url)
|
||||
val downloadedHash = FileAssetsUtil.downloadAsset(url, hashType = FileAssetsUtil.HashTypes.SHA1)
|
||||
if (downloadedHash != hash) {
|
||||
throw IOException("Verification of asset $hash failed!")
|
||||
}
|
||||
@ -82,7 +82,7 @@ class IndexAssetsManager(
|
||||
override fun load(latch: CountUpAndDownLatch) {
|
||||
check(!loaded) { "Already loaded!" }
|
||||
|
||||
var assets = FileUtil.saveReadFile(FileAssetsUtil.getPath(indexHash))?.readJsonObject() ?: downloadAssetsIndex()
|
||||
var assets = FileUtil.safeReadFile(FileAssetsUtil.getPath(indexHash))?.readJsonObject() ?: downloadAssetsIndex()
|
||||
|
||||
assets["objects"].let { assets = it.asCompound() }
|
||||
val tasks = CountUpAndDownLatch(0)
|
||||
|
@ -18,6 +18,7 @@ import com.github.luben.zstd.ZstdOutputStream
|
||||
import de.bixilon.minosoft.data.registries.ResourceLocation
|
||||
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
|
||||
import de.bixilon.minosoft.terminal.RunConfiguration
|
||||
import de.bixilon.minosoft.util.KUtil.isHexString
|
||||
import de.bixilon.minosoft.util.Util
|
||||
import java.io.*
|
||||
import java.net.URL
|
||||
@ -29,18 +30,21 @@ object FileAssetsUtil {
|
||||
private val BASE_PATH = RunConfiguration.HOME_DIRECTORY + "assets/objects/"
|
||||
|
||||
fun getPath(hash: String): String {
|
||||
if (!hash.isHexString) {
|
||||
throw IllegalArgumentException("String is not a hex string. Invalid data or manipulated?")
|
||||
}
|
||||
return BASE_PATH + hash.substring(0, 2) + "/" + hash
|
||||
}
|
||||
|
||||
fun downloadAsset(url: String, compress: Boolean = true): String {
|
||||
return saveAndGet(URL(url).openStream(), compress, false).first
|
||||
fun downloadAsset(url: String, compress: Boolean = true, hashType: HashTypes = HashTypes.SHA256): String {
|
||||
return saveAndGet(URL(url).openStream(), compress, false, hashType).first
|
||||
}
|
||||
|
||||
fun downloadAndGetAsset(url: String, compress: Boolean = true): Pair<String, ByteArray> {
|
||||
return saveAndGet(URL(url).openStream(), compress, true)
|
||||
fun downloadAndGetAsset(url: String, compress: Boolean = true, hashType: HashTypes = HashTypes.SHA256): Pair<String, ByteArray> {
|
||||
return saveAndGet(URL(url).openStream(), compress, true, hashType)
|
||||
}
|
||||
|
||||
fun saveAndGet(stream: InputStream, compress: Boolean = true, get: Boolean = true): Pair<String, ByteArray> {
|
||||
fun saveAndGet(stream: InputStream, compress: Boolean = true, get: Boolean = true, hashType: HashTypes = HashTypes.SHA256): Pair<String, ByteArray> {
|
||||
var tempFile: File
|
||||
do {
|
||||
tempFile = File(RunConfiguration.TEMPORARY_FOLDER + Util.generateRandomString(32))
|
||||
@ -56,7 +60,7 @@ object FileAssetsUtil {
|
||||
} else {
|
||||
ByteArrayOutputStream(0)
|
||||
}
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
val digest = hashType.createDigest()
|
||||
var output: OutputStream = FileOutputStream(tempFile)
|
||||
if (compress) {
|
||||
output = ZstdOutputStream(output)
|
||||
@ -97,16 +101,16 @@ object FileAssetsUtil {
|
||||
return Pair(hash, returnStream.toByteArray())
|
||||
}
|
||||
|
||||
fun saveAsset(stream: InputStream, compress: Boolean = true): String {
|
||||
return saveAndGet(stream, compress, false).first
|
||||
fun saveAsset(stream: InputStream, compress: Boolean = true, hashType: HashTypes = HashTypes.SHA256): String {
|
||||
return saveAndGet(stream, compress, false, hashType).first
|
||||
}
|
||||
|
||||
fun saveAsset(data: ByteArray, compress: Boolean = true): String {
|
||||
return saveAndGet(ByteArrayInputStream(data), compress, false).first
|
||||
fun saveAsset(data: ByteArray, compress: Boolean = true, hashType: HashTypes = HashTypes.SHA256): String {
|
||||
return saveAndGet(ByteArrayInputStream(data), compress, false, hashType).first
|
||||
}
|
||||
|
||||
fun saveAndGetAsset(data: ByteArray, compress: Boolean = true): Pair<String, ByteArray> {
|
||||
return saveAndGet(ByteArrayInputStream(data), compress, false)
|
||||
fun saveAndGetAsset(data: ByteArray, hashType: HashTypes = HashTypes.SHA256, compress: Boolean = true): Pair<String, ByteArray> {
|
||||
return saveAndGet(ByteArrayInputStream(data), compress, false, hashType)
|
||||
}
|
||||
|
||||
fun String.toAssetName(verifyPrefix: Boolean = true): ResourceLocation? {
|
||||
@ -120,7 +124,7 @@ object FileAssetsUtil {
|
||||
return ResourceLocation(split[0], split[1])
|
||||
}
|
||||
|
||||
fun verifyAsset(hash: String, file: File = File(getPath(hash)), verify: Boolean, compress: Boolean = true): Boolean {
|
||||
fun verifyAsset(hash: String, file: File = File(getPath(hash)), verify: Boolean, hashType: HashTypes = HashTypes.SHA256, compress: Boolean = true): Boolean {
|
||||
if (!file.exists()) {
|
||||
return false
|
||||
}
|
||||
@ -137,7 +141,7 @@ object FileAssetsUtil {
|
||||
}
|
||||
|
||||
try {
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
val digest = hashType.createDigest()
|
||||
|
||||
var input: InputStream = FileInputStream(file)
|
||||
if (compress) {
|
||||
@ -163,4 +167,17 @@ object FileAssetsUtil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
enum class HashTypes(
|
||||
val digestName: String,
|
||||
val length: Int,
|
||||
) {
|
||||
SHA1("SHA-1", 40),
|
||||
SHA256("SHA-256", 64),
|
||||
;
|
||||
|
||||
fun createDigest(): MessageDigest {
|
||||
return MessageDigest.getInstance(digestName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,11 +29,11 @@ import java.util.zip.ZipInputStream
|
||||
|
||||
object FileUtil {
|
||||
|
||||
fun saveReadFile(path: String, compressed: Boolean = true): InputStream? {
|
||||
return saveReadFile(File(path), compressed)
|
||||
fun safeReadFile(path: String, compressed: Boolean = true): InputStream? {
|
||||
return safeReadFile(File(path), compressed)
|
||||
}
|
||||
|
||||
fun saveReadFile(file: File, compressed: Boolean = true): InputStream? {
|
||||
fun safeReadFile(file: File, compressed: Boolean = true): InputStream? {
|
||||
if (!file.exists()) {
|
||||
return null
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ package de.bixilon.minosoft.config.profile.profiles.eros.server.entries
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import de.bixilon.minosoft.assets.util.FileAssetsUtil
|
||||
import de.bixilon.minosoft.config.profile.profiles.eros.ErosProfileManager.backingDelegate
|
||||
import de.bixilon.minosoft.config.profile.profiles.eros.ErosProfileManager.delegate
|
||||
import de.bixilon.minosoft.config.profile.profiles.eros.ErosProfileManager.mapDelegate
|
||||
@ -56,5 +57,5 @@ class Server(
|
||||
var forcedVersion by backingDelegate(getter = { Versions[_forcedVersion] }, setter = { _forcedVersion = it?.name })
|
||||
|
||||
@get:JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
var faviconHash: String? by delegate(null) { if (it != null) check(it.length == 40) { "Not a valid sha1 hash!" } }
|
||||
var faviconHash: String? by delegate(null) { if (it != null) check(it.length == FileAssetsUtil.HashTypes.SHA256.length) { "Not a valid sha256 hash!" } }
|
||||
}
|
||||
|
@ -1,19 +1,43 @@
|
||||
package de.bixilon.minosoft.data.player.properties.textures
|
||||
|
||||
import de.bixilon.minosoft.assets.util.FileAssetsUtil
|
||||
import de.bixilon.minosoft.assets.util.FileUtil
|
||||
import de.bixilon.minosoft.util.KUtil.check
|
||||
import java.net.URL
|
||||
|
||||
open class PlayerTexture(
|
||||
val url: URL,
|
||||
) {
|
||||
var data: ByteArray? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
url.check()
|
||||
|
||||
check(urlMatches(url, ALLOWED_DOMAINS) && !urlMatches(url, BLOCKED_DOMAINS)) { "URL hostname is not allowed!" }
|
||||
}
|
||||
|
||||
fun read(): ByteArray {
|
||||
val sha256 = when (url.host) {
|
||||
"textures.minecraft.net" -> url.file.split("/").last()
|
||||
else -> TODO("Can not get texture identifier!")
|
||||
}
|
||||
val file = FileUtil.safeReadFile(FileAssetsUtil.getPath(sha256), true)?.let {
|
||||
val data = it.readAllBytes()
|
||||
this.data = data
|
||||
return data
|
||||
}
|
||||
|
||||
val input = url.openStream()
|
||||
if (input.available() > MAX_TEXTURE_SIZE) {
|
||||
throw IllegalStateException("Texture is too big!")
|
||||
}
|
||||
val data = FileAssetsUtil.saveAndGet(input)
|
||||
return data.second
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_TEXTURE_SIZE = 64 * 64 * 3 + 100 // width * height * rgb + some padding
|
||||
private val ALLOWED_DOMAINS = arrayOf(".minecraft.net", ".mojang.com")
|
||||
private val BLOCKED_DOMAINS = arrayOf("bugs.mojang.com", "education.minecraft.net", "feedback.minecraft.net")
|
||||
|
||||
|
@ -39,7 +39,7 @@ object RegistriesLoader {
|
||||
"hashPrefix" to hash.substring(0, 2),
|
||||
"fullHash" to hash,
|
||||
)
|
||||
), false)
|
||||
), false, hashType = FileAssetsUtil.HashTypes.SHA1)
|
||||
if (savedHash.first != hash) {
|
||||
throw IllegalStateException("Data mismatch, expected $hash, got ${savedHash.first}")
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ object FaviconManager {
|
||||
return null
|
||||
}
|
||||
|
||||
fun Server.saveFavicon(favicon: ByteArray?, faviconHash: String = Util.sha1(favicon)) {
|
||||
fun Server.saveFavicon(favicon: ByteArray?, faviconHash: String = Util.sha256(favicon)) {
|
||||
if (this.faviconHash == faviconHash) {
|
||||
return
|
||||
}
|
||||
|
@ -564,4 +564,14 @@ object KUtil {
|
||||
fun URL.check() {
|
||||
check(this.protocol == "http" || this.protocol == "https") { "Url is not a web address" }
|
||||
}
|
||||
|
||||
val String.isHexString: Boolean
|
||||
get() {
|
||||
for (digit in toCharArray()) {
|
||||
if (digit !in '0'..'9' && digit !in 'a'..'f') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -139,6 +139,16 @@ public final class Util {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String sha256(byte[] data) {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
|
||||
try {
|
||||
return sha256(inputStream);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String sha1(File file) throws IOException {
|
||||
return sha1(new FileInputStream(file));
|
||||
}
|
||||
@ -149,15 +159,27 @@ public final class Util {
|
||||
|
||||
public static String sha1(InputStream inputStream) throws IOException {
|
||||
try {
|
||||
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
|
||||
crypt.reset();
|
||||
return hash(MessageDigest.getInstance("SHA-1"), inputStream);
|
||||
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[ProtocolDefinition.DEFAULT_BUFFER_SIZE];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
||||
crypt.update(buffer, 0, length);
|
||||
}
|
||||
return byteArrayToHexString(crypt.digest());
|
||||
private static String hash(MessageDigest digest, InputStream inputStream) throws IOException {
|
||||
digest.reset();
|
||||
|
||||
byte[] buffer = new byte[ProtocolDefinition.DEFAULT_BUFFER_SIZE];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) {
|
||||
digest.update(buffer, 0, length);
|
||||
}
|
||||
return byteArrayToHexString(digest.digest());
|
||||
}
|
||||
|
||||
public static String sha256(InputStream inputStream) throws IOException {
|
||||
try {
|
||||
return hash(MessageDigest.getInstance("SHA-256"), inputStream);
|
||||
} catch (NoSuchAlgorithmException | FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
|
Loading…
x
Reference in New Issue
Block a user