From 6d43e0392eb2877c5683ec24fdbde578430e7ddf Mon Sep 17 00:00:00 2001 From: Graham Date: Tue, 29 Aug 2023 17:17:02 +0100 Subject: [PATCH] Add initial GAMELOGIC packet support Signed-off-by: Graham --- .../game/net/login/LoginChannelHandler.kt | 15 ++ .../org/openrs2/protocol/ProtocolModule.kt | 4 + .../protocol/common/AntiAliasingMode.kt | 19 +++ .../openrs2/protocol/common/DisplayMode.kt | 20 +++ .../kotlin/org/openrs2/protocol/common/Uid.kt | 30 ++++ .../protocol/login/upstream/GameLoginCodec.kt | 24 +++ .../login/upstream/GameLoginPayload.kt | 143 ++++++++++++++++++ .../login/upstream/GameReconnectCodec.kt | 24 +++ .../protocol/login/upstream/LoginRequest.kt | 2 + 9 files changed, 281 insertions(+) create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/common/AntiAliasingMode.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/common/DisplayMode.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/common/Uid.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginCodec.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginPayload.kt create mode 100644 protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameReconnectCodec.kt diff --git a/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt b/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt index 75315f9797..ba8a824b59 100644 --- a/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt +++ b/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.openrs2.buffer.copiedBuffer +import org.openrs2.cache.Js5MasterIndex import org.openrs2.conf.CountryCode import org.openrs2.crypto.secureRandom import org.openrs2.game.cluster.Cluster @@ -44,6 +45,7 @@ import org.openrs2.protocol.js5.downstream.Js5ResponseEncoder import org.openrs2.protocol.js5.downstream.XorDecoder import org.openrs2.protocol.js5.upstream.Js5RequestDecoder import org.openrs2.protocol.login.downstream.LoginResponse +import org.openrs2.protocol.login.upstream.GameLoginPayload import org.openrs2.protocol.login.upstream.LoginRequest import org.openrs2.protocol.world.downstream.WorldListDownstream import org.openrs2.protocol.world.downstream.WorldListResponse @@ -53,6 +55,7 @@ import java.time.LocalDate public class LoginChannelHandler @Inject constructor( private val cluster: Cluster, private val store: PlayerStore, + js5MasterIndex: Js5MasterIndex, private val js5HandlerProvider: Provider, private val jaggrabHandler: JaggrabChannelHandler, @CreateDownstream @@ -63,6 +66,7 @@ public class LoginChannelHandler @Inject constructor( private val worldListDownstreamProtocol: Protocol ) : SimpleChannelInboundHandler(LoginRequest::class.java) { private lateinit var scope: CoroutineScope + private val js5ArchiveChecksums = js5MasterIndex.entries.map(Js5MasterIndex.Entry::checksum) private var usernameHash = 0 private var serverKey = 0L @@ -86,7 +90,9 @@ public class LoginChannelHandler @Inject constructor( when (msg) { is LoginRequest.InitGameConnection -> handleInitGameConnection(ctx, msg) is LoginRequest.InitJs5RemoteConnection -> handleInitJs5RemoteConnection(ctx, msg) + is LoginRequest.GameLogin -> handleGameLogin(ctx, msg.payload, reconnect = false) is LoginRequest.InitJaggrabConnection -> handleInitJaggrabConnection(ctx) + is LoginRequest.GameReconnect -> handleGameLogin(ctx, msg.payload, reconnect = true) is LoginRequest.CreateCheckDateOfBirthCountry -> handleCreateCheckDateOfBirthCountry(ctx, msg) is LoginRequest.CreateCheckName -> handleCreateCheckName(ctx, msg) is LoginRequest.CreateAccount -> handleCreateAccount(ctx, msg) @@ -129,6 +135,15 @@ public class LoginChannelHandler @Inject constructor( } } + private fun handleGameLogin(ctx: ChannelHandlerContext, msg: GameLoginPayload, reconnect: Boolean) { + if (msg.build != BUILD || msg.js5ArchiveChecksums != js5ArchiveChecksums) { + ctx.write(LoginResponse.ClientOutOfDate).addListener(ChannelFutureListener.CLOSE) + return + } + + // TODO + } + private fun handleInitJaggrabConnection(ctx: ChannelHandlerContext) { ctx.pipeline().addLast( DelimiterBasedFrameDecoder(JAGGRAB_MAX_FRAME_LENGTH, JAGGRAB_DELIMITER), diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt b/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt index 378ca7509d..2dd60206b2 100644 --- a/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt +++ b/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt @@ -71,6 +71,8 @@ import org.openrs2.protocol.login.upstream.CheckWorldSuitabilityCodec import org.openrs2.protocol.login.upstream.CreateAccountCodec import org.openrs2.protocol.login.upstream.CreateCheckDateOfBirthCountryCodec import org.openrs2.protocol.login.upstream.CreateCheckNameCodec +import org.openrs2.protocol.login.upstream.GameLoginCodec +import org.openrs2.protocol.login.upstream.GameReconnectCodec import org.openrs2.protocol.login.upstream.InitCrossDomainConnectionCodec import org.openrs2.protocol.login.upstream.InitGameConnectionCodec import org.openrs2.protocol.login.upstream.InitJaggrabConnectionCodec @@ -125,7 +127,9 @@ public object ProtocolModule : AbstractModule() { LoginUpstream::class.java, InitGameConnectionCodec::class.java, InitJs5RemoteConnectionCodec::class.java, + GameLoginCodec::class.java, InitJaggrabConnectionCodec::class.java, + GameReconnectCodec::class.java, CreateCheckDateOfBirthCountryCodec::class.java, CreateCheckNameCodec::class.java, CreateAccountCodec::class.java, diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/common/AntiAliasingMode.kt b/protocol/src/main/kotlin/org/openrs2/protocol/common/AntiAliasingMode.kt new file mode 100644 index 0000000000..203c59bf99 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/common/AntiAliasingMode.kt @@ -0,0 +1,19 @@ +package org.openrs2.protocol.common + +public enum class AntiAliasingMode { + NONE, + X2, + X4; + + public companion object { + private val values = values() + + public fun fromOrdinal(ordinal: Int): AntiAliasingMode? { + return if (ordinal >= 0 && ordinal < values.size) { + values[ordinal] + } else { + null + } + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/common/DisplayMode.kt b/protocol/src/main/kotlin/org/openrs2/protocol/common/DisplayMode.kt new file mode 100644 index 0000000000..fde492f8c2 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/common/DisplayMode.kt @@ -0,0 +1,20 @@ +package org.openrs2.protocol.common + +public enum class DisplayMode { + SD, + HD_SMALL, + HD, + HD_FULLSCREEN; + + public companion object { + private val values = values() + + public fun fromOrdinal(ordinal: Int): DisplayMode? { + return if (ordinal >= 0 && ordinal < values.size) { + values[ordinal] + } else { + null + } + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/common/Uid.kt b/protocol/src/main/kotlin/org/openrs2/protocol/common/Uid.kt new file mode 100644 index 0000000000..24ac693b10 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/common/Uid.kt @@ -0,0 +1,30 @@ +package org.openrs2.protocol.common + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufUtil + +public class Uid private constructor( + private val bytes: ByteArray, +) { + init { + require(bytes.size == LENGTH) + } + + public fun write(buf: ByteBuf) { + buf.writeBytes(bytes) + } + + public override fun toString(): String { + return ByteBufUtil.hexDump(bytes) + } + + public companion object { + public const val LENGTH: Int = 24 + + public fun read(buf: ByteBuf): Uid { + val bytes = ByteArray(LENGTH) + buf.readBytes(bytes) + return Uid(bytes) + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginCodec.kt b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginCodec.kt new file mode 100644 index 0000000000..8c694bd299 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginCodec.kt @@ -0,0 +1,24 @@ +package org.openrs2.protocol.login.upstream + +import io.netty.buffer.ByteBuf +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters +import org.openrs2.crypto.StreamCipher +import org.openrs2.protocol.VariableShortPacketCodec + +@Singleton +public class GameLoginCodec @Inject constructor( + private val key: RSAPrivateCrtKeyParameters, +) : VariableShortPacketCodec( + type = LoginRequest.GameLogin::class.java, + opcode = 16, +) { + override fun decode(input: ByteBuf, cipher: StreamCipher): LoginRequest.GameLogin { + return LoginRequest.GameLogin(GameLoginPayload.read(input, key)) + } + + override fun encode(input: LoginRequest.GameLogin, output: ByteBuf, cipher: StreamCipher) { + input.payload.write(output, key) + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginPayload.kt b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginPayload.kt new file mode 100644 index 0000000000..0277e62ac2 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameLoginPayload.kt @@ -0,0 +1,143 @@ +package org.openrs2.protocol.login.upstream + +import io.netty.buffer.ByteBuf +import org.bouncycastle.crypto.params.RSAKeyParameters +import org.openrs2.buffer.readString +import org.openrs2.buffer.use +import org.openrs2.buffer.writeString +import org.openrs2.crypto.Rsa +import org.openrs2.crypto.XteaKey +import org.openrs2.crypto.rsa +import org.openrs2.protocol.common.AntiAliasingMode +import org.openrs2.protocol.common.DisplayMode +import org.openrs2.protocol.common.Uid +import org.openrs2.util.Base37 + +public data class GameLoginPayload( + val build: Int, + val advertSuppressed: Boolean, + val clientSigned: Boolean, + val displayMode: DisplayMode, + val canvasWidth: Int, + val canvasHeight: Int, + val antiAliasingMode: AntiAliasingMode, + val uid: Uid, + val siteSettings: String, + val affiliate: Int, + val detailOptions: Int, + val verifyId: Int, + val js5ArchiveChecksums: List, + // TODO(gpe): XteaKey needs a better name, as it represents an ISAAC key here + val key: XteaKey, + val username: String, + val password: String, +) { + init { + require(js5ArchiveChecksums.size == JS5_ARCHIVES) + } + + internal fun write(buf: ByteBuf, rsaKey: RSAKeyParameters) { + buf.writeInt(build) + buf.writeByte(0) + buf.writeBoolean(advertSuppressed) + buf.writeBoolean(clientSigned) + buf.writeByte(displayMode.ordinal) + buf.writeShort(canvasWidth) + buf.writeShort(canvasHeight) + buf.writeByte(antiAliasingMode.ordinal) + uid.write(buf) + buf.writeString(siteSettings) + buf.writeInt(affiliate) + buf.writeInt(detailOptions) + buf.writeShort(verifyId) + + for (checksum in js5ArchiveChecksums) { + buf.writeInt(checksum) + } + + buf.alloc().buffer().use { plaintext -> + plaintext.writeByte(Rsa.MAGIC) + plaintext.writeInt(key.k0) + plaintext.writeInt(key.k1) + plaintext.writeInt(key.k2) + plaintext.writeInt(key.k3) + plaintext.writeLong(Base37.encode(username)) + plaintext.writeString(password) + + plaintext.rsa(rsaKey).use { ciphertext -> + buf.writeByte(ciphertext.readableBytes()) + buf.writeBytes(ciphertext) + } + } + } + + public companion object { + public const val JS5_ARCHIVES: Int = 29 + + internal fun read(buf: ByteBuf, rsaKey: RSAKeyParameters): GameLoginPayload { + val build = buf.readInt() + + require(buf.readUnsignedByte().toInt() == 0) { + "Unknown byte is non-zero" + } + + val advertSuppressed = buf.readBoolean() + val clientSigned = buf.readBoolean() + + val displayMode = DisplayMode.fromOrdinal(buf.readUnsignedByte().toInt()) + ?: throw IllegalArgumentException("Invalid DisplayMode") + + val canvasWidth = buf.readUnsignedShort() + val canvasHeight = buf.readUnsignedShort() + + val antiAliasingMode = AntiAliasingMode.fromOrdinal(buf.readUnsignedByte().toInt()) + ?: throw IllegalArgumentException("Invalid AntiAliasingMode") + + val uid = Uid.read(buf) + val siteSettings = buf.readString() + val affiliate = buf.readInt() + val detailOptions = buf.readInt() + val verifyId = buf.readUnsignedShort() + + val js5ArchiveChecksums = mutableListOf() + for (i in 0 until JS5_ARCHIVES) { + js5ArchiveChecksums += buf.readInt() + } + + val ciphertextLen = buf.readUnsignedByte().toInt() + val ciphertext = buf.readSlice(ciphertextLen) + + ciphertext.rsa(rsaKey).use { plaintext -> + require(plaintext.readUnsignedByte().toInt() == Rsa.MAGIC) { + "Invalid RSA magic" + } + + val k0 = plaintext.readInt() + val k1 = plaintext.readInt() + val k2 = plaintext.readInt() + val k3 = plaintext.readInt() + val username = plaintext.readLong() + val password = plaintext.readString() + + return GameLoginPayload( + build, + advertSuppressed, + clientSigned, + displayMode, + canvasWidth, + canvasHeight, + antiAliasingMode, + uid, + siteSettings, + affiliate, + detailOptions, + verifyId, + js5ArchiveChecksums, + XteaKey(k0, k1, k2, k3), + Base37.decodeLowerCase(username), + password, + ) + } + } + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameReconnectCodec.kt b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameReconnectCodec.kt new file mode 100644 index 0000000000..27b8d22fc3 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/GameReconnectCodec.kt @@ -0,0 +1,24 @@ +package org.openrs2.protocol.login.upstream + +import io.netty.buffer.ByteBuf +import jakarta.inject.Inject +import jakarta.inject.Singleton +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters +import org.openrs2.crypto.StreamCipher +import org.openrs2.protocol.VariableShortPacketCodec + +@Singleton +public class GameReconnectCodec @Inject constructor( + private val key: RSAPrivateCrtKeyParameters, +) : VariableShortPacketCodec( + type = LoginRequest.GameReconnect::class.java, + opcode = 18, +) { + override fun decode(input: ByteBuf, cipher: StreamCipher): LoginRequest.GameReconnect { + return LoginRequest.GameReconnect(GameLoginPayload.read(input, key)) + } + + override fun encode(input: LoginRequest.GameReconnect, output: ByteBuf, cipher: StreamCipher) { + input.payload.write(output, key) + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/LoginRequest.kt b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/LoginRequest.kt index 214bde3ed5..a6f8a8937b 100644 --- a/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/LoginRequest.kt +++ b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/LoginRequest.kt @@ -5,7 +5,9 @@ import org.openrs2.protocol.Packet public sealed class LoginRequest : Packet { public data class InitGameConnection(public val usernameHash: Int) : LoginRequest() public data class InitJs5RemoteConnection(public val build: Int) : LoginRequest() + public data class GameLogin(public val payload: GameLoginPayload) : LoginRequest() public object InitJaggrabConnection : LoginRequest() + public data class GameReconnect(public val payload: GameLoginPayload) : LoginRequest() public data class CreateCheckDateOfBirthCountry( public val year: Int, public val month: Int,