diff --git a/crypto/src/main/kotlin/org/openrs2/crypto/Xtea.kt b/crypto/src/main/kotlin/org/openrs2/crypto/Xtea.kt index 214387c5..8e46f34d 100644 --- a/crypto/src/main/kotlin/org/openrs2/crypto/Xtea.kt +++ b/crypto/src/main/kotlin/org/openrs2/crypto/Xtea.kt @@ -1,11 +1,12 @@ package org.openrs2.crypto import io.netty.buffer.ByteBuf +import java.security.SecureRandom private const val GOLDEN_RATIO = 0x9E3779B9.toInt() private const val ROUNDS = 32 -private const val BLOCK_SIZE = 8 -private const val BLOCK_SIZE_MASK = BLOCK_SIZE - 1 +public const val XTEA_BLOCK_SIZE: Int = 8 +private const val BLOCK_SIZE_MASK = XTEA_BLOCK_SIZE - 1 public data class XteaKey( public val k0: Int, @@ -35,6 +36,12 @@ public data class XteaKey( @JvmStatic public val ZERO: XteaKey = XteaKey(0, 0, 0, 0) + @JvmStatic + @JvmOverloads + public fun generate(r: SecureRandom = secureRandom): XteaKey { + return XteaKey(r.nextInt(), r.nextInt(), r.nextInt(), r.nextInt()) + } + @JvmStatic public fun fromIntArray(a: IntArray): XteaKey { require(a.size == 4) @@ -71,7 +78,7 @@ public fun ByteBuf.xteaEncrypt(index: Int, length: Int, key: XteaKey) { val k = key.toIntArray() val end = index + (length and BLOCK_SIZE_MASK.inv()) - for (i in index until end step BLOCK_SIZE) { + for (i in index until end step XTEA_BLOCK_SIZE) { var sum = 0 var v0 = getInt(i) var v1 = getInt(i + 4) @@ -91,7 +98,7 @@ public fun ByteBuf.xteaDecrypt(index: Int, length: Int, key: XteaKey) { val k = key.toIntArray() val end = index + (length and BLOCK_SIZE_MASK.inv()) - for (i in index until end step BLOCK_SIZE) { + for (i in index until end step XTEA_BLOCK_SIZE) { @Suppress("INTEGER_OVERFLOW") var sum = GOLDEN_RATIO * ROUNDS var v0 = getInt(i) diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt b/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt index 6243c51c..a0f96a45 100644 --- a/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt +++ b/protocol/src/main/kotlin/org/openrs2/protocol/ProtocolModule.kt @@ -13,6 +13,7 @@ import org.openrs2.protocol.login.downstream.Js5OkCodec import org.openrs2.protocol.login.downstream.LoginDownstream import org.openrs2.protocol.login.downstream.ServerFullCodec 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.InitCrossDomainConnectionCodec @@ -44,6 +45,7 @@ public object ProtocolModule : AbstractModule() { InitJaggrabConnectionCodec::class.java, CreateCheckDateOfBirthCountryCodec::class.java, CreateCheckNameCodec::class.java, + CreateAccountCodec::class.java, RequestWorldListCodec::class.java, CheckWorldSuitabilityCodec::class.java, InitCrossDomainConnectionCodec::class.java diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/CreateAccountCodec.kt b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/CreateAccountCodec.kt new file mode 100644 index 00000000..7c4c7f35 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/login/upstream/CreateAccountCodec.kt @@ -0,0 +1,132 @@ +package org.openrs2.protocol.login.upstream + +import io.netty.buffer.ByteBuf +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters +import org.openrs2.buffer.readString +import org.openrs2.buffer.use +import org.openrs2.buffer.writeString +import org.openrs2.crypto.Rsa +import org.openrs2.crypto.StreamCipher +import org.openrs2.crypto.XTEA_BLOCK_SIZE +import org.openrs2.crypto.XteaKey +import org.openrs2.crypto.publicKey +import org.openrs2.crypto.rsa +import org.openrs2.crypto.secureRandom +import org.openrs2.crypto.xteaDecrypt +import org.openrs2.crypto.xteaEncrypt +import org.openrs2.protocol.VariableShortPacketCodec +import org.openrs2.util.Base37 +import java.time.LocalDate +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +public class CreateAccountCodec @Inject constructor( + private val rsaKey: RSAPrivateCrtKeyParameters +) : VariableShortPacketCodec( + type = LoginRequest.CreateAccount::class.java, + opcode = 22 +) { + override fun decode(input: ByteBuf, cipher: StreamCipher): LoginRequest.CreateAccount { + val build = input.readShort().toInt() + + val ciphertextLen = input.readUnsignedByte().toInt() + val ciphertext = input.readSlice(ciphertextLen) + + ciphertext.rsa(rsaKey).use { plaintext -> + require(plaintext.readUnsignedByte().toInt() == Rsa.MAGIC) { + "Invalid RSA magic" + } + + val flags = plaintext.readUnsignedShort() + val gameNewsletters = (flags and FLAG_GAME_NEWSLETTERS) != 0 + val otherNewsletters = (flags and FLAG_OTHER_NEWSLETTERS) != 0 + val shareDetailsWithBusinessPartners = (flags and FLAG_SHARE_DETAILS_WITH_BUSINESS_PARTNERS) != 0 + + val username = Base37.decodeLowerCase(plaintext.readLong()) + val k0 = plaintext.readInt() + val password = plaintext.readString() + val k1 = plaintext.readInt() + val affiliate = plaintext.readUnsignedShort() + val day = plaintext.readUnsignedByte().toInt() + val month = plaintext.readUnsignedByte().toInt() + val k2 = plaintext.readInt() + val year = plaintext.readUnsignedShort() + val country = plaintext.readUnsignedShort() + val k3 = plaintext.readInt() + + val xteaKey = XteaKey(k0, k1, k2, k3) + input.xteaDecrypt(input.readerIndex(), input.readableBytes(), xteaKey) + + val email = input.readString() + + return LoginRequest.CreateAccount( + build, + gameNewsletters, + otherNewsletters, + shareDetailsWithBusinessPartners, + username, + password, + affiliate, + dateOfBirth = LocalDate.of(year, month + 1, day), + country, + email + ) + } + } + + override fun encode(input: LoginRequest.CreateAccount, output: ByteBuf, cipher: StreamCipher) { + val xteaKey = XteaKey.generate() + + output.writeShort(input.build) + + output.alloc().buffer().use { plaintext -> + plaintext.writeByte(Rsa.MAGIC) + + var flags = 0 + if (input.gameNewsletters) { + flags = flags or FLAG_GAME_NEWSLETTERS + } + if (input.otherNewsletters) { + flags = flags or FLAG_OTHER_NEWSLETTERS + } + if (input.shareDetailsWithBusinessPartners) { + flags = flags or FLAG_SHARE_DETAILS_WITH_BUSINESS_PARTNERS + } + plaintext.writeShort(flags) + + plaintext.writeLong(Base37.encode(input.username)) + plaintext.writeInt(xteaKey.k0) + plaintext.writeString(input.password) + plaintext.writeInt(xteaKey.k1) + plaintext.writeShort(input.affiliate) + plaintext.writeByte(input.dateOfBirth.dayOfMonth) + plaintext.writeByte(input.dateOfBirth.monthValue - 1) + plaintext.writeInt(xteaKey.k2) + plaintext.writeShort(input.dateOfBirth.year) + plaintext.writeShort(input.country) + plaintext.writeInt(xteaKey.k3) + + plaintext.rsa(rsaKey.publicKey).use { ciphertext -> + output.writeByte(ciphertext.readableBytes()) + output.writeBytes(ciphertext) + } + } + + val xteaIndex = output.writerIndex() + + output.writeString(input.email) + + while ((output.writerIndex() - xteaIndex) % XTEA_BLOCK_SIZE != 0) { + output.writeByte(secureRandom.nextInt()) + } + + output.xteaEncrypt(xteaIndex, output.writerIndex() - xteaIndex, xteaKey) + } + + private companion object { + private const val FLAG_GAME_NEWSLETTERS = 0x1 + private const val FLAG_OTHER_NEWSLETTERS = 0x2 + private const val FLAG_SHARE_DETAILS_WITH_BUSINESS_PARTNERS = 0x4 + } +} 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 a1b5c62c..a26ba32d 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 @@ -12,6 +12,18 @@ public sealed class LoginRequest : Packet { public val country: Int ) : LoginRequest() public data class CreateCheckName(public val username: String) : LoginRequest() + public data class CreateAccount( + public val build: Int, + public val gameNewsletters: Boolean, + public val otherNewsletters: Boolean, + public val shareDetailsWithBusinessPartners: Boolean, + public val username: String, + public val password: String, + public val affiliate: Int, + public val dateOfBirth: LocalDate, + public val country: Int, + public val email: String + ) : LoginRequest() public data class RequestWorldList(public val checksum: Int) : LoginRequest() public data class CheckWorldSuitability( public val build: Int,