From 45ab18088a9b3e8001eea68645665fc1ef04a3fa Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 13 Oct 2023 19:33:16 +0100 Subject: [PATCH] Add a Hasher class This abstraction, which includes ByteBuf-compatible methods, also fixes the invalid-hash bug from ChunkTile.computeDataHash(), since it's not hashing the whole underlying array, only the written bytes. --- .../minecraft/mapsync/common/Cartography.java | 5 +- .../mapsync/common/data/ChunkTile.java | 15 --- .../mapsync/common/net/SyncClient.java | 17 ++- .../mapsync/common/utils/Hasher.java | 116 ++++++++++++++++++ 4 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 mod/common/src/main/java/gjum/minecraft/mapsync/common/utils/Hasher.java diff --git a/mod/common/src/main/java/gjum/minecraft/mapsync/common/Cartography.java b/mod/common/src/main/java/gjum/minecraft/mapsync/common/Cartography.java index bc6fe2d5..2c856ea9 100644 --- a/mod/common/src/main/java/gjum/minecraft/mapsync/common/Cartography.java +++ b/mod/common/src/main/java/gjum/minecraft/mapsync/common/Cartography.java @@ -1,6 +1,7 @@ package gjum.minecraft.mapsync.common; import gjum.minecraft.mapsync.common.data.*; +import gjum.minecraft.mapsync.common.utils.Hasher; import io.netty.buffer.Unpooled; import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; @@ -31,7 +32,9 @@ public static ChunkTile chunkTileFromLevel(Level level, int cx, int cz) { // TODO speedup: don't serialize twice (once here, once later when writing to network) var columnsBuf = Unpooled.buffer(); ChunkTile.writeColumns(columns, columnsBuf); - byte[] dataHash = ChunkTile.computeDataHash(columnsBuf); + final byte[] dataHash = Hasher.sha1() + .update(columnsBuf) + .generateHash(); return new ChunkTile(dimension, cx, cz, timestamp, dataVersion, dataHash, columns); } diff --git a/mod/common/src/main/java/gjum/minecraft/mapsync/common/data/ChunkTile.java b/mod/common/src/main/java/gjum/minecraft/mapsync/common/data/ChunkTile.java index 4ac1b5e4..1542c8da 100644 --- a/mod/common/src/main/java/gjum/minecraft/mapsync/common/data/ChunkTile.java +++ b/mod/common/src/main/java/gjum/minecraft/mapsync/common/data/ChunkTile.java @@ -9,9 +9,6 @@ import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - public record ChunkTile( ResourceKey dimension, int x, int z, @@ -66,16 +63,4 @@ public static ChunkTile fromBuf(ByteBuf buf) { } return new ChunkTile(dimension, x, z, timestamp, dataVersion, hash, columns); } - - public static byte[] computeDataHash(ByteBuf columns) { - try { - // SHA-1 is faster than SHA-256, and other algorithms are not required to be implemented in every JVM - MessageDigest md = MessageDigest.getInstance("SHA-1"); - md.update(columns.array()); - return md.digest(); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - return new byte[]{}; - } - } } diff --git a/mod/common/src/main/java/gjum/minecraft/mapsync/common/net/SyncClient.java b/mod/common/src/main/java/gjum/minecraft/mapsync/common/net/SyncClient.java index 693fd17b..40d84312 100644 --- a/mod/common/src/main/java/gjum/minecraft/mapsync/common/net/SyncClient.java +++ b/mod/common/src/main/java/gjum/minecraft/mapsync/common/net/SyncClient.java @@ -5,6 +5,7 @@ import gjum.minecraft.mapsync.common.net.encryption.EncryptionDecoder; import gjum.minecraft.mapsync.common.net.encryption.EncryptionEncoder; import gjum.minecraft.mapsync.common.net.packet.*; +import gjum.minecraft.mapsync.common.utils.Hasher; import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; @@ -244,16 +245,12 @@ void setUpEncryption(ChannelHandlerContext ctx, ClientboundEncryptionRequestPack byte[] sharedSecret = new byte[16]; ThreadLocalRandom.current().nextBytes(sharedSecret); - String shaHex; - try { - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.update(sharedSecret); - digest.update(packet.publicKey.getEncoded()); - // note that this is different from minecraft (we get no negative hashes) - shaHex = HexFormat.of().formatHex(digest.digest()); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + // note that this is different from minecraft (we get no negative hashes) + final String shaHex = HexFormat.of().formatHex(Hasher.sha1() + .update(sharedSecret) + .update(packet.publicKey.getEncoded()) + .generateHash() + ); User session = Minecraft.getInstance().getUser(); Minecraft.getInstance().getMinecraftSessionService().joinServer( diff --git a/mod/common/src/main/java/gjum/minecraft/mapsync/common/utils/Hasher.java b/mod/common/src/main/java/gjum/minecraft/mapsync/common/utils/Hasher.java new file mode 100644 index 00000000..dc8468a3 --- /dev/null +++ b/mod/common/src/main/java/gjum/minecraft/mapsync/common/utils/Hasher.java @@ -0,0 +1,116 @@ +package gjum.minecraft.mapsync.common.utils; + +import io.netty.buffer.ByteBuf; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +public final class Hasher { + private final MessageDigest messageDigest; + + private Hasher( + final @NotNull MessageDigest messageDigest + ) { + this.messageDigest = Objects.requireNonNull(messageDigest); + } + + /** + * Updates the digest with a single byte. + */ + public @NotNull Hasher update( + final byte input + ) { + this.messageDigest.update((byte) input); + return this; + } + + /** + * Updates the digest with an entire byte array. + */ + public @NotNull Hasher update( + final byte @NotNull [] input + ) { + this.messageDigest.update((byte[]) input); + return this; + } + + /** + * Updates the digest with a byte-array slice, defined by the given offset and length. + */ + public @NotNull Hasher update( + final byte @NotNull [] input, + final int offset, + final int length + ) { + this.messageDigest.update((byte[]) input, offset, length); + return this; + } + + /** + * Updates the digest with a ByteBuffer slice, using {@link ByteBuffer#position()} as the offset and + * {@link ByteBuffer#remaining()} as the length. If you have been writing to this ByteBuffer, you may wish to + * {@link ByteBuffer#flip()} it first before passing it into this method. + */ + public @NotNull Hasher update( + final @NotNull ByteBuffer input + ) { + this.messageDigest.update((ByteBuffer) input); + return this; + } + + /** + * Updates the digest with a ByteBuffer slice, defined by the given offset and length. + */ + public @NotNull Hasher update( + final @NotNull ByteBuffer input, + final int offset, + final int length + ) { + return update((ByteBuffer) input.slice(offset, length)); + } + + /** + * Updates the digest with a ByteBuf slice, using {@link ByteBuf#readerIndex()} as the offset and + * {@link ByteBuf#readableBytes()} as the length. + */ + public @NotNull Hasher update( + final @NotNull ByteBuf input + ) { + update((ByteBuffer) input.nioBuffer()); + return this; + } + + /** + * Updates the digest with a ByteBuf slice, defined by the given offset and length. + */ + public @NotNull Hasher update( + final @NotNull ByteBuf input, + final int offset, + final int length + ) { + update((ByteBuffer) input.nioBuffer(offset, length)); + return this; + } + + public byte @NotNull [] generateHash() { + return this.messageDigest.digest(); + } + + /** + * Since every implementation of Java is required to support SHA-1 + * (source) + * it's a safe bet that the algorithm exists. + */ + public static @NotNull Hasher sha1() { + final MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-1"); + } + catch (final NoSuchAlgorithmException thrown) { + throw new IllegalStateException("This should never happen!", thrown); + } + return new Hasher(messageDigest); + } +}