diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWork.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWork.kt new file mode 100644 index 000000000..6e3da07e5 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWork.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.loginprot.incoming.pow + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeVerifier + +/** + * Proof of work is a procedure during login to attempt to throttle login requests from a single source, + * by requiring them to do CPU-bound work before accepting the login. + * @property challengeType the type of the challenge to require the client to solve + * @property challengeVerifier the verifier of that challenge, to ensure the client did complete + * the world successfully + */ +@Suppress("MemberVisibilityCanBePrivate") +public class ProofOfWork, in MetaData : ChallengeMetaData>( + public val challengeType: T, + public val challengeVerifier: ChallengeVerifier, +) diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWorkProvider.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWorkProvider.kt new file mode 100644 index 000000000..bcd6d4b8e --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWorkProvider.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.loginprot.incoming.pow + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType + +/** + * An interface to return proof of work implementations based on the input ip. + */ +public fun interface ProofOfWorkProvider, in MetaData : ChallengeMetaData> { + /** + * Provides a proof of work instance for a given [ip]. + * @param ip the IP from which the client is connecting. + * @return a proof of work instance that the client needs to solve. + */ + public fun provide(ip: Int): ProofOfWork +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/SingleTypeProofOfWorkProvider.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/SingleTypeProofOfWorkProvider.kt new file mode 100644 index 000000000..241a8265d --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/SingleTypeProofOfWorkProvider.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.loginprot.incoming.pow + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeGenerator +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaDataProvider +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeVerifier + +/** + * A single type proof of work provider is used to always return proof of work instances + * of a single specific type. + * @property metaDataProvider the provider used to return instances of metadata for the + * challenges. + * @property challengeGenerator the generator that will create a new proof of work challenge + * based on the input metadata. + * @property challengeVerifier the verifier that will check if the answer sent by the client + * is correct. + */ +public class SingleTypeProofOfWorkProvider, in MetaData : ChallengeMetaData>( + private val metaDataProvider: ChallengeMetaDataProvider, + private val challengeGenerator: ChallengeGenerator, + private val challengeVerifier: ChallengeVerifier, +) : ProofOfWorkProvider { + override fun provide(ip: Int): ProofOfWork { + val metadata = metaDataProvider.provide(ip) + val challenge = challengeGenerator.generate(metadata) + return ProofOfWork(challenge, challengeVerifier) + } +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeGenerator.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeGenerator.kt new file mode 100644 index 000000000..3533b70e7 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeGenerator.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +/** + * A challenge generator used to construct a challenge out of the provided metadata. + */ +public fun interface ChallengeGenerator> { + /** + * A function to generate a challenge out of the provided metadata. + * @param input the metadata input necessary to generate a challenge + * @return a challenge generated out of the metadata + */ + public fun generate(input: MetaData): Type +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaData.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaData.kt new file mode 100644 index 000000000..388bb05c0 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaData.kt @@ -0,0 +1,6 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +/** + * A common binding interface for metadata necessary to pass into the challenge constructors. + */ +public interface ChallengeMetaData diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaDataProvider.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaDataProvider.kt new file mode 100644 index 000000000..90ab8ecb3 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaDataProvider.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +/** + * A challenge metadata provider is used to generate a metadata necessary to construct a challenge. + */ +public fun interface ChallengeMetaDataProvider { + /** + * Provides a metadata instance for a challenge, using the ip as the input parameter. + * @param ip the IP from which the user is connecting to the server. + * This is provided in case an implementation which scales with the number of requests + * from a given host is desired. + * @return the metadata object necessary to construct a challenge. + */ + public fun provide(ip: Int): T +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeType.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeType.kt new file mode 100644 index 000000000..8b531c327 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeType.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +/** + * A common binding interface for challenge types. + * Currently, the client only supports SHA-256 as a challenge, but it is set up to + * support other types with ease. + * @param MetaData the metadata necessary to construct a challenge of this type. + * @property id the id of the challenge, used by the client to identify what challenge + * solver to use. + * @property resultSize the number of bytes the server must have in the socket before + * it can attempt to verify the challenge. + */ +public interface ChallengeType { + public val id: Int + public val resultSize: Int +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeVerifier.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeVerifier.kt new file mode 100644 index 000000000..91cafbaf4 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeVerifier.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +import net.rsprot.buffer.JagByteBuf + +/** + * A challenge verifier is used to check the work that the client did. + * The general idea here is that the client has to perform the work N times, where N is + * pseudo-random, while the server only has to do that same work one time - to verify the + * result that the client sent. The complexity of the work to perform is configurable by the + * server. + * @param T the challenge type to verify + */ +public interface ChallengeVerifier> { + /** + * Verifies the work performed by the client. + * @param result the byte buffer containing the result sent by the client. + * @param challenge the challenge to verify using the [result] provided. + * @return whether the challenge is solved using the [result] provided. + */ + public fun verify( + result: JagByteBuf, + challenge: T, + ): Boolean +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeWorker.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeWorker.kt new file mode 100644 index 000000000..bed044789 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeWorker.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +import net.rsprot.buffer.JagByteBuf +import java.util.concurrent.CompletableFuture + +/** + * A worker is used to perform the verifications of the data sent by the client for our + * proof of work requests. While the work itself is relatively cheap, servers may wish + * to perform the work on other threads - this interface allows doing that. + */ +public interface ChallengeWorker { + /** + * Verifies the result sent by the client. + * @param result the byte buffer containing the result data sent by the client + * @param challenge the challenge the client had to solve + * @param verifier the verifier used to check the work done by the client for out challenge + * @return a future object containing the result of the work, or an exception. + * If the future doesn't return immediately, there will be a 30-second timeout applied to it, + * after which the work will be concluded failed. + */ + public fun , V : ChallengeVerifier> verify( + result: JagByteBuf, + challenge: T, + verifier: V, + ): CompletableFuture +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/DefaultChallengeWorker.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/DefaultChallengeWorker.kt new file mode 100644 index 000000000..936bdeddc --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/DefaultChallengeWorker.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +import net.rsprot.buffer.JagByteBuf +import java.util.concurrent.CompletableFuture + +/** + * The default challenge worker will perform the work on the calling thread. + * The SHA-256 challenges are fairly inexpensive and the overhead of switching threads + * is similar to the work itself done. + */ +public data object DefaultChallengeWorker : ChallengeWorker { + override fun , V : ChallengeVerifier> verify( + result: JagByteBuf, + challenge: T, + verifier: V, + ): CompletableFuture { + return try { + CompletableFuture.completedFuture(verifier.verify(result, challenge)) + } catch (t: Throwable) { + CompletableFuture.failedFuture(t) + } + } +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ChallengeGenerator.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ChallengeGenerator.kt new file mode 100644 index 000000000..e45a295f8 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ChallengeGenerator.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeGenerator +import java.math.BigInteger +import kotlin.random.Random + +/** + * The default SHA-256 challenge generator is used to generate challenges which align + * up with what OldSchool RuneScape is generating, which is a combination of epoch time millis, + * the world id and a 495-byte [BigInteger] that is turned into a hexadecimal string, + * which will have a length of 1004 or 1005 characters, depending on if the [BigInteger] was negative. + */ +public class DefaultSha256ChallengeGenerator : + ChallengeGenerator { + override fun generate(input: Sha256MetaData): Sha256Challenge { + val randomData = Random.Default.nextBytes(RANDOM_DATA_LENGTH) + val hexSalt = BigInteger(randomData).toString(HEX_RADIX) + val salt = + java.lang.Long.toHexString(input.epochTimeMillis) + + Integer.toHexString(input.world) + + hexSalt + return Sha256Challenge( + input.unknown, + input.difficulty, + salt, + ) + } + + private companion object { + private const val RANDOM_DATA_LENGTH: Int = 495 + private const val HEX_RADIX: Int = 16 + } +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256MetaDataProvider.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256MetaDataProvider.kt new file mode 100644 index 000000000..5f581be42 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256MetaDataProvider.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaDataProvider + +/** + * The default SHA-256 metadata provider will return a metadata object + * that matches what OldSchool RuneScape sends. + * @property world the world that the client is connecting to. + */ +public class DefaultSha256MetaDataProvider( + private val world: Int, +) : ChallengeMetaDataProvider { + override fun provide(ip: Int): Sha256MetaData { + return Sha256MetaData(world) + } +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ProofOfWorkProvider.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ProofOfWorkProvider.kt new file mode 100644 index 000000000..c9119b4ea --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ProofOfWorkProvider.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.ProofOfWorkProvider +import net.rsprot.protocol.loginprot.incoming.pow.SingleTypeProofOfWorkProvider + +/** + * A value class to wrap the properties of a SHA-256 into a single instance. + * @property provider the SHA-256 proof of work provider. + */ +@Suppress("MemberVisibilityCanBePrivate") +@JvmInline +public value class DefaultSha256ProofOfWorkProvider private constructor( + public val provider: SingleTypeProofOfWorkProvider, +) : ProofOfWorkProvider by provider { + public constructor( + world: Int, + ) : this( + SingleTypeProofOfWorkProvider( + DefaultSha256MetaDataProvider(world), + DefaultSha256ChallengeGenerator(), + Sha256ChallengeVerifier(), + ), + ) +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256Challenge.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256Challenge.kt new file mode 100644 index 000000000..cf11c53a4 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256Challenge.kt @@ -0,0 +1,75 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType + +/** + * A SHA-256 challenge is a challenge which forces the client to find a hash which + * has at least [difficulty] number of leading zero bits in the hash. + * As hashing returns pseudo-random results, as a general rule of thumb, the work + * needed to solve a challenge doubles with each difficulty increase, since each + * bit can be either true or false, and the solution must have at least [difficulty] + * amount of false (zero) bits. + * Since the requirement is that there are at least [difficulty] amount of leading + * zero bits, these challenges aren't constrained to only having a single successful + * answer. + * @property unknown an unknown byte value that is appended to the start of each + * base string that needs hashing. The value of this byte is **always** one in OldSchool + * RuneScape. + * @property difficulty the difficulty of the challenge, as explained above, is the number + * of leading zero bits the hash must have for it to be considered successful. + * The default difficulty in OldSchool RuneScape is 18 as of writing this. + * When Proof of Work was first introduced, this value was 16. + * It is possible that the value gets dynamically increased as the pressure increases, + * or if there are a lot of requests from a single IP. + * @property salt the salt string that is the bulk of the input to hash. + * @property id the id of the challenge as identified by the client. + * @property resultSize the number of bytes the server must have in its socket after sending + * a SHA-256 challenge request, in order to attempt to verify it. + */ +public class Sha256Challenge( + public val unknown: Int, + public val difficulty: Int, + public val salt: String, +) : ChallengeType { + override val id: Int + get() = 0 + override val resultSize: Int + get() = Long.SIZE_BYTES + + /** + * Gets the base string that is part of the input for the hash. + * A long will be appended to this base string at the end, which will additionally + * be the solution to the challenge. The full string of baseString + the long is what + * must result in [difficulty] number of leading zero bits after having been hashed. + * @return the base string used for the hashing input. + */ + public fun getBaseString(): String { + return Integer.toHexString(this.unknown) + Integer.toHexString(this.difficulty) + this.salt + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Sha256Challenge) return false + + if (unknown != other.unknown) return false + if (difficulty != other.difficulty) return false + if (salt != other.salt) return false + + return true + } + + override fun hashCode(): Int { + var result = unknown + result = 31 * result + difficulty + result = 31 * result + salt.hashCode() + return result + } + + override fun toString(): String { + return "Sha256Challenge(" + + "unknown=$unknown, " + + "difficulty=$difficulty, " + + "salt='$salt'" + + ")" + } +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256ChallengeVerifier.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256ChallengeVerifier.kt new file mode 100644 index 000000000..d76ab774e --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256ChallengeVerifier.kt @@ -0,0 +1,84 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeVerifier +import net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing.DefaultSha256MessageDigestHashFunction +import net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing.Sha256HashFunction + +/** + * The SHA-256 challenge verifier is a replica of the client's implementation of the + * SHA-256 proof of work verifier. + * @property hashFunction the function used to hash the input bytes, with the default + * implementation being the same as the client - making a new MessageDigest object + * for each hash. These objects are fairly cheap, though. + */ +public class Sha256ChallengeVerifier( + private val hashFunction: Sha256HashFunction = DefaultSha256MessageDigestHashFunction, +) : ChallengeVerifier { + override fun verify( + result: JagByteBuf, + challenge: Sha256Challenge, + ): Boolean { + val value = result.g8() + val baseString = challenge.getBaseString() + val builder = StringBuilder(baseString) + builder.append(java.lang.Long.toHexString(value)) + val utf8ByteArray = + builder + .toString() + .toByteArray(Charsets.UTF_8) + val hash = hashFunction.hash(utf8ByteArray) + return leadingZeros(hash) >= challenge.difficulty + } + + /** + * Counts the number of leading zero bits in the [byteArray]. + * @param byteArray the byte array to check for leading zero bits. + * @return the number of leading zero bits in the byte array. + */ + private fun leadingZeros(byteArray: ByteArray): Int { + var numBits = 0 + for (byte in byteArray) { + val bitCount = leadingZeros(byte) + numBits += bitCount + if (bitCount != Byte.SIZE_BITS) { + break + } + } + return numBits + } + + /** + * Gets the number of leading zero bits in the provided [byte]. + * @return the number of leading zero bits in the byte. + */ + private fun leadingZeros(byte: Byte): Int { + var value = byte.toInt() and 0xFF + if (value == 0) { + return Byte.SIZE_BITS + } + var numBits = 0 + while (value and 0x80 == 0) { + numBits++ + value = value shl 1 + } + return numBits + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Sha256ChallengeVerifier) return false + + if (hashFunction != other.hashFunction) return false + + return true + } + + override fun hashCode(): Int { + return hashFunction.hashCode() + } + + override fun toString(): String { + return "Sha256ChallengeVerifier(hashFunction=$hashFunction)" + } +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256MetaData.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256MetaData.kt new file mode 100644 index 000000000..d075abea6 --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256MetaData.kt @@ -0,0 +1,53 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData + +/** + * The SHA-256 metadata is what the default SHA-256 implementation requires in order + * to construct new challenges. + * @property world the world that the client is connecting to. The world id is the second argument + * to the string that will be hashed. + * @property difficulty the difficulty of the challenge, which is the number of leading zero bits + * that the hash must have for it to be considered successful. + * @property epochTimeMillis the epoch time milliseconds when the request was made. + * This value is the very first section of the hash input. + * @property unknown an unknown byte value - this value is always one in OldSchool RuneScape; it is + * unclear what it is meant to represent. + */ +public class Sha256MetaData + @JvmOverloads + public constructor( + public val world: Int, + public val difficulty: Int = 18, + public val epochTimeMillis: Long = System.currentTimeMillis(), + public val unknown: Int = 1, + ) : ChallengeMetaData { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Sha256MetaData) return false + + if (world != other.world) return false + if (difficulty != other.difficulty) return false + if (epochTimeMillis != other.epochTimeMillis) return false + if (unknown != other.unknown) return false + + return true + } + + override fun hashCode(): Int { + var result = world + result = 31 * result + difficulty + result = 31 * result + epochTimeMillis.hashCode() + result = 31 * result + unknown + return result + } + + override fun toString(): String { + return "Sha256MetaData(" + + "world=$world, " + + "difficulty=$difficulty, " + + "epochTimeMillis=$epochTimeMillis, " + + "unknown=$unknown" + + ")" + } + } diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/DefaultSha256MessageDigestHashFunction.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/DefaultSha256MessageDigestHashFunction.kt new file mode 100644 index 000000000..6f7862b9f --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/DefaultSha256MessageDigestHashFunction.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing + +import java.security.MessageDigest + +/** + * The default SHA-256 hash function using the [MessageDigest] implementation. + * Each hash request will generate a new instance of the [MessageDigest] object, + * as these implementations are not thread safe. + * These [MessageDigest] instances however are relatively cheap to construct. + */ +public data object DefaultSha256MessageDigestHashFunction : Sha256HashFunction { + override fun hash(input: ByteArray): ByteArray { + val messageDigest = MessageDigest.getInstance("SHA-256") + messageDigest.update(input) + return messageDigest.digest() + } +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/Sha256HashFunction.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/Sha256HashFunction.kt new file mode 100644 index 000000000..3bd9bf6fe --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/Sha256HashFunction.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing + +/** + * A SHA-256 hash function is a function used to turn the input bytes into a valid SHA-256 hash. + */ +public interface Sha256HashFunction { + /** + * The hash function takes an [input] byte array and turns it into a valid SHA-256 hash. + * @param input the input byte array to be hashed. + * @return the SHA-256 hash. + */ + public fun hash(input: ByteArray): ByteArray +} diff --git a/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/ThreadLocalSha256MessageDigestHashFunction.kt b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/ThreadLocalSha256MessageDigestHashFunction.kt new file mode 100644 index 000000000..b8b5d01ee --- /dev/null +++ b/protocol/osrs-221-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/ThreadLocalSha256MessageDigestHashFunction.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing + +import java.security.MessageDigest + +/** + * A SHA-256 hash function using the [MessageDigest] implementation. + * Unlike the default implementation, this one will utilize a thread-local implementation + * of the [MessageDigest] instances, which are all reset before use. + * + * @property digesters the thread-local message digest instances of SHA-256. + */ +public data object ThreadLocalSha256MessageDigestHashFunction : Sha256HashFunction { + private val digesters = + ThreadLocal.withInitial { + MessageDigest.getInstance("SHA-256") + } + + override fun hash(input: ByteArray): ByteArray { + val messageDigest = digesters.get() + messageDigest.reset() + messageDigest.update(input) + return messageDigest.digest() + } +}