From d96fce629957e1eb8435df53087ad91ed5250d68 Mon Sep 17 00:00:00 2001 From: Chord Date: Fri, 5 Feb 2016 03:19:13 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 11 + build.sbt | 37 ++ .../main/scala/psforever/IFinalizable.scala | 21 + .../psforever/crypto/CryptoInterface.scala | 228 +++++++++++ .../psforever/net/ControlPacketOpcode.scala | 68 +++ .../psforever/net/CryptoPacketOpcode.scala | 20 + .../psforever/net/GamePacketOpcode.scala | 45 ++ .../main/scala/psforever/net/PSPacket.scala | 386 ++++++++++++++++++ .../scala/psforever/net/PacketCoding.scala | 377 +++++++++++++++++ common/src/main/scala/sna/Library.scala | 60 +++ common/src/test/scala/PacketCodingTest.scala | 68 +++ project/build.properties | 1 + project/plugins.sbt | 3 + pslogin/src/main/scala/LoginSession.scala | 11 + .../src/main/scala/LoginSessionActor.scala | 250 ++++++++++++ pslogin/src/main/scala/PsLogin.scala | 22 + pslogin/src/main/scala/SessionRouter.scala | 38 ++ pslogin/src/main/scala/UdpListener.scala | 35 ++ .../psforever/crypto/CryptoStateManager.scala | 40 ++ .../src/test/scala/CryptoInterfaceTest.scala | 135 ++++++ pslogin/src/test/scala/CryptoPackets.scala | 133 ++++++ 21 files changed, 1989 insertions(+) create mode 100644 .gitignore create mode 100644 build.sbt create mode 100644 common/src/main/scala/psforever/IFinalizable.scala create mode 100644 common/src/main/scala/psforever/crypto/CryptoInterface.scala create mode 100644 common/src/main/scala/psforever/net/ControlPacketOpcode.scala create mode 100644 common/src/main/scala/psforever/net/CryptoPacketOpcode.scala create mode 100644 common/src/main/scala/psforever/net/GamePacketOpcode.scala create mode 100644 common/src/main/scala/psforever/net/PSPacket.scala create mode 100644 common/src/main/scala/psforever/net/PacketCoding.scala create mode 100644 common/src/main/scala/sna/Library.scala create mode 100644 common/src/test/scala/PacketCodingTest.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 pslogin/src/main/scala/LoginSession.scala create mode 100644 pslogin/src/main/scala/LoginSessionActor.scala create mode 100644 pslogin/src/main/scala/PsLogin.scala create mode 100644 pslogin/src/main/scala/SessionRouter.scala create mode 100644 pslogin/src/main/scala/UdpListener.scala create mode 100644 pslogin/src/main/scala/psforever/crypto/CryptoStateManager.scala create mode 100644 pslogin/src/test/scala/CryptoInterfaceTest.scala create mode 100644 pslogin/src/test/scala/CryptoPackets.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..6bd27d991 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +target +*.class +.idea/ +.idea_modules/ +tmp/ +out/ +*.iws +*.ipr +*.iml +*.swp +/*.csv diff --git a/build.sbt b/build.sbt new file mode 100644 index 000000000..d2b7cb461 --- /dev/null +++ b/build.sbt @@ -0,0 +1,37 @@ +import AssemblyKeys._ + +assemblySettings + + +lazy val commonSettings = Seq( + organization := "com.psforever", + version := "1.0", + jarName in assembly := "pslogin.jar", + scalaVersion := "2.11.7", + scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8"), + resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", + libraryDependencies := Seq( + "com.typesafe.akka" %% "akka-actor" % "2.3.11", + "com.typesafe.scala-logging" %% "scala-logging" % "3.1.0", + "org.specs2" %% "specs2-core" % "2.3.11" % "test", + "org.scodec" %% "scodec-core" % "1.8.3", + "org.scodec" %% "scodec-akka" % "0.1.0-SNAPSHOT", + "net.java.dev.jna" % "jna" % "4.2.1" + ) +) + +lazy val root = (project in file(".")). + settings(commonSettings: _*). + aggregate(pslogin, common) + +lazy val pslogin = (project in file("pslogin")). + settings(commonSettings: _*). + settings( + name := "pslogin" + ).settings(assemblySettings: _*).dependsOn(common) + +lazy val common = (project in file("common")). + settings(commonSettings: _*). + settings( + name := "common" + ) \ No newline at end of file diff --git a/common/src/main/scala/psforever/IFinalizable.scala b/common/src/main/scala/psforever/IFinalizable.scala new file mode 100644 index 000000000..8450fa1e7 --- /dev/null +++ b/common/src/main/scala/psforever/IFinalizable.scala @@ -0,0 +1,21 @@ +package psforever + +class ObjectFinalizedException(msg : String) extends Exception(msg) + +trait IFinalizable { + var closed = false + + def close = { + closed = true + } + + def assertNotClosed = { + if(closed) + throw new ObjectFinalizedException(this.getClass.getCanonicalName + ": already finalized. Cannot interact with object") + } + + override def finalize() = { + if(!closed) + println(this.getClass.getCanonicalName + ": class not closed. memory leaked") + } +} diff --git a/common/src/main/scala/psforever/crypto/CryptoInterface.scala b/common/src/main/scala/psforever/crypto/CryptoInterface.scala new file mode 100644 index 000000000..5388b47f3 --- /dev/null +++ b/common/src/main/scala/psforever/crypto/CryptoInterface.scala @@ -0,0 +1,228 @@ +package psforever.crypto + +import psforever.IFinalizable +import sna.Library +import com.sun.jna.Pointer +import scodec.bits.ByteVector + +object CryptoInterface { + final val libName = "pscrypto" + // TODO: make this cross platform + final val fullLibName = "lib" + libName + ".so" + final val psLib = new Library(libName) + final val RC5_BLOCK_SIZE = 8 + final val MD5_MAC_SIZE = 16 + + val functionsList = List( + "RC5_Init", + "RC5_Encrypt", + "RC5_Decrypt", + "DH_Start", + "DH_Start_Generate", + "DH_Agree", + "MD5_MAC", + "Free_DH", + "Free_RC5" + ) + + def initialize() : Unit = { + functionsList foreach psLib.prefetch + } + + def MD5MAC(key : ByteVector, message : ByteVector, bytesWanted : Int) : ByteVector = { + val out = Array.ofDim[Byte](bytesWanted) + + // WARNING BUG: the function must be cast to something (even if void) otherwise it doesnt work + val ret = psLib.MD5_MAC(key.toArray, key.length, message.toArray, message.length, out, out.length)[Boolean] + + if(!ret) + throw new Exception("MD5MAC failed to process") + + ByteVector(out) + } + + /** + * Checks if two MAC values are the same in constant time, preventing a timing attack for MAC forgery + * @param mac1 + * @param mac2 + */ + def verifyMAC(mac1 : ByteVector, mac2 : ByteVector) : Boolean = { + var okay = true + + if(mac1.length != mac2.length) + return false + + for(i <- 0 until mac1.length) { + okay = okay && mac1{i} == mac2{i} + } + + okay + } + + class CryptoDHState extends IFinalizable { + var started = false + // these types MUST be Arrays of bytes for JNA to work + val privateKey = Array.ofDim[Byte](16) + val publicKey = Array.ofDim[Byte](16) + val p = Array.ofDim[Byte](16) + val g = Array.ofDim[Byte](16) + var dhHandle = Pointer.NULL + + def start(modulus : ByteVector, generator : ByteVector) : Unit = { + assertNotClosed + + if(started) + throw new IllegalStateException("DH state has already been started") + + dhHandle = psLib.DH_Start(modulus.toArray, generator.toArray, privateKey, publicKey)[Pointer] + + if(dhHandle == Pointer.NULL) + throw new Exception("DH initialization failed!") + + modulus.copyToArray(p, 0) + generator.copyToArray(g, 0) + + started = true + } + + def start() : Unit = { + assertNotClosed + + if(started) + throw new IllegalStateException("DH state has already been started") + + dhHandle = psLib.DH_Start_Generate(privateKey, publicKey, p, g)[Pointer] + + if(dhHandle == Pointer.NULL) + throw new Exception("DH initialization failed!") + + started = true + } + + def agree(otherPublicKey : ByteVector) = { + if(!started) + throw new IllegalStateException("DH state has not been started") + + val agreedValue = Array.ofDim[Byte](16) + val agreed = psLib.DH_Agree(dhHandle, agreedValue, privateKey, otherPublicKey.toArray)[Boolean] + + if(!agreed) + throw new Exception("Failed to DH agree") + + ByteVector.view(agreedValue) + } + + private def checkAndReturnView(array : Array[Byte]) = { + if(!started) + throw new IllegalStateException("DH state has not been started") + + ByteVector.view(array) + } + + def getPrivateKey = { + checkAndReturnView(privateKey) + } + + def getPublicKey = { + checkAndReturnView(publicKey) + } + + def getModulus = { + checkAndReturnView(p) + } + + def getGenerator = { + checkAndReturnView(g) + } + + override def close = { + if(started) { + psLib.Free_DH(dhHandle)[Unit] + started = false + } + + super.close + } + } + + class CryptoState(val decryptionKey : ByteVector, + val encryptionKey : ByteVector) extends IFinalizable { + // Note that the keys must be returned as primitive Arrays for JNA to work + val encCryptoHandle = psLib.RC5_Init(encryptionKey.toArray, encryptionKey.length, true)[Pointer] + val decCryptoHandle = psLib.RC5_Init(decryptionKey.toArray, decryptionKey.length, false)[Pointer] + + if(encCryptoHandle == Pointer.NULL) + throw new Exception("Encryption initialization failed!") + + if(decCryptoHandle == Pointer.NULL) + throw new Exception("Decryption initialization failed!") + + def encrypt(plaintext : ByteVector) : ByteVector = { + if(plaintext.length % RC5_BLOCK_SIZE != 0) + throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary") + + val ciphertext = Array.ofDim[Byte](plaintext.length) + + val ret = psLib.RC5_Encrypt(encCryptoHandle, plaintext.toArray, plaintext.length, ciphertext)[Boolean] + + if(!ret) + throw new Exception("Failed to encrypt plaintext") + + ByteVector.view(ciphertext) + } + + def decrypt(ciphertext : ByteVector) : ByteVector = { + if(ciphertext.length % RC5_BLOCK_SIZE != 0) + throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary") + + val plaintext = Array.ofDim[Byte](ciphertext.length) + + val ret = psLib.RC5_Decrypt(decCryptoHandle, ciphertext.toArray, ciphertext.length, plaintext)[Boolean] + + if(!ret) + throw new Exception("Failed to decrypt ciphertext") + + ByteVector.view(plaintext) + } + + override def close = { + psLib.Free_RC5(encCryptoHandle)[Unit] + psLib.Free_RC5(decCryptoHandle)[Unit] + super.close + } + } + + class CryptoStateWithMAC(decryptionKey : ByteVector, + encryptionKey : ByteVector, + val decryptionMACKey : ByteVector, + val encryptionMACKey : ByteVector) extends CryptoState(decryptionKey, encryptionKey) { + /** + * Performs a MAC operation over the message. Used when encrypting packets + * @param message + * @return ByteVector + */ + def macForEncrypt(message : ByteVector) : ByteVector = { + MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE) + } + + /** + * Performs a MAC operation over the message. Used when verifying decrypted packets + * @param message + * @return ByteVector + */ + def macForDecrypt(message : ByteVector) : ByteVector = { + MD5MAC(decryptionMACKey, message, MD5_MAC_SIZE) + } + + /** + * MACs the plaintext message, encrypts it, and then returns the encrypted message with the + * MAC appended to the end. + * @param message Arbitrary set of bytes + * @return ByteVector + */ + def macAndEncrypt(message : ByteVector) : ByteVector = { + encrypt(message) ++ MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE) + } + } + +} diff --git a/common/src/main/scala/psforever/net/ControlPacketOpcode.scala b/common/src/main/scala/psforever/net/ControlPacketOpcode.scala new file mode 100644 index 000000000..7e6786063 --- /dev/null +++ b/common/src/main/scala/psforever/net/ControlPacketOpcode.scala @@ -0,0 +1,68 @@ +package psforever.net + +import scodec.bits.BitVector +import scodec.{Err, DecodeResult, Attempt, Codec} +import scodec.codecs._ + +object ControlPacketOpcode extends Enumeration { + type Type = Value + val + + // Opcodes should have a marker every 10 + // OPCODE 0 + HandleGamePacket, // a whoopsi case: not actually a control packet, but a game packet + ClientStart, // first packet ever sent during client connection + ServerStart, // second packet sent in response to ClientStart + MultiPacket, // used to send multiple packets with one UDP message (subpackets limited to <= 255) + Unknown4, + Unknown5, + Unknown6, + Unknown7, + Unknown8, + SlottedMetaPacket0, + + // OPCODE 10 + SlottedMetaPacket1, + SlottedMetaPacket2, + SlottedMetaPacket3, + SlottedMetaPacket4, + SlottedMetaPacket5, + SlottedMetaPacket6, + SlottedMetaPacket7, + RelatedA0, + RelatedA1, + RelatedA2, + + // OPCODE 20 + RelatedA3, + RelatedB0, + RelatedB1, + RelatedB2, + RelatedB3, + AggregatePacket, // same as MultiPacket, but with the ability to send extended length packets + Unknown26, + Unknown27, + Unknown28, + ConnectionClose, + + // OPCODE 30 + Unknown30 + = Value + + def getPacketDecoder(opcode : ControlPacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideControlPacket]] = opcode match { + case HandleGamePacket => psforever.net.HandleGamePacket.decode + case ServerStart => psforever.net.ServerStart.decode + case ClientStart => psforever.net.ClientStart.decode + case MultiPacket => psforever.net.MultiPacket.decode + case SlottedMetaPacket0 => psforever.net.SlottedMetaPacket.decode + case ConnectionClose => psforever.net.ConnectionClose.decode + case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for control packet ${opcode}")) + } + + val storageType = uint8L + + assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2), + this.getClass.getCanonicalName + ": maxId exceeds primitive type") + + implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType) +} diff --git a/common/src/main/scala/psforever/net/CryptoPacketOpcode.scala b/common/src/main/scala/psforever/net/CryptoPacketOpcode.scala new file mode 100644 index 000000000..ae6f3f7a8 --- /dev/null +++ b/common/src/main/scala/psforever/net/CryptoPacketOpcode.scala @@ -0,0 +1,20 @@ +package psforever.net + +import scodec.bits.BitVector +import scodec.{Err, DecodeResult, Attempt} + +// this isnt actually used as an opcode (i.e not serialized) +object CryptoPacketOpcode extends Enumeration { + type Type = Value + val Ignore, ClientChallengeXchg, ServerChallengeXchg, + ClientFinished, ServerFinished = Value + + def getPacketDecoder(opcode : CryptoPacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideCryptoPacket]] = opcode match { + case ClientChallengeXchg => psforever.net.ClientChallengeXchg.decode + case ServerChallengeXchg => psforever.net.ServerChallengeXchg.decode + case ServerFinished => psforever.net.ServerFinished.decode + case ClientFinished => psforever.net.ClientFinished.decode + case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for crypto packet ${opcode}") + .pushContext("get_marshaller")) + } +} diff --git a/common/src/main/scala/psforever/net/GamePacketOpcode.scala b/common/src/main/scala/psforever/net/GamePacketOpcode.scala new file mode 100644 index 000000000..29b5bacc1 --- /dev/null +++ b/common/src/main/scala/psforever/net/GamePacketOpcode.scala @@ -0,0 +1,45 @@ +package psforever.net + +import scodec.{Err, DecodeResult, Attempt, Codec} +import scodec.bits.BitVector +import scodec.codecs._ + +object GamePacketOpcode extends Enumeration { + type Type = Value + val + + // Opcodes should have a marker every 10 + // OPCODE 0 + Unknown0, + LoginMessage, + LoginRespMessage, + Unknown3, + ConnectToWorldMessage, + Unknown5, + UnknownMessage6, + UnknownMessage7, + PlayerStateMessage, + UnknownMessage9, + + // OPCODE 10 + HitHint, + DamageMessage, + DestroyMessage, + ReloadMessage, + MountVehicleMsg, + DismountVehicleMsg + = Value + + def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match { + case LoginMessage => psforever.net.LoginMessage.decode + case LoginRespMessage => psforever.net.LoginRespMessage.decode + case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for game packet ${opcode}")) + } + + val storageType = uint8L + + assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2), + this.getClass.getCanonicalName + ": maxId exceeds primitive type") + + implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType) +} diff --git a/common/src/main/scala/psforever/net/PSPacket.scala b/common/src/main/scala/psforever/net/PSPacket.scala new file mode 100644 index 000000000..b2705e963 --- /dev/null +++ b/common/src/main/scala/psforever/net/PSPacket.scala @@ -0,0 +1,386 @@ +package psforever.net + +import scodec.{DecodeResult, Err, Codec, Attempt} +import scodec.bits._ +import scodec.codecs._ +import scodec._ +import shapeless._ +import shapeless.ops.hlist.Prepend + +// Base packets +sealed trait PlanetSidePacket extends Serializable { + def encode : Attempt[BitVector] + def opcode : Enumeration#Value +} + +// Used by companion objects to create encoders and decoders +sealed trait Marshallable[T] { + implicit val codec : Codec[T] + def encode(a : T) : Attempt[BitVector] = codec.encode(a) + // assert that when decoding a marshallable type, that no bits are left over + def decode(a : BitVector) : Attempt[DecodeResult[T]] = codec.complete.decode(a) +} + +sealed trait PlanetSideGamePacket extends PlanetSidePacket { + def opcode : GamePacketOpcode.Type +} + +sealed trait PlanetSideControlPacket extends PlanetSidePacket { + def opcode : ControlPacketOpcode.Type +} + +sealed trait PlanetSideCryptoPacket extends PlanetSidePacket { + def opcode : CryptoPacketOpcode.Type +} + +// Crypto Packets +final case class ClientChallengeXchg(time : Long, challenge : ByteVector, p : ByteVector, g : ByteVector) + extends PlanetSideCryptoPacket { + def opcode = CryptoPacketOpcode.ClientChallengeXchg + def encode = ClientChallengeXchg.encode(this) +} + +object ClientChallengeXchg extends Marshallable[ClientChallengeXchg] { + implicit val codec: Codec[ClientChallengeXchg] = ( + ("unknown" | constant(1)) :: + ("unknown" | constant(1)) :: + ("client_time" | uint32L) :: + ("challenge" | bytes(12)) :: + ("end_chal?" | constant(0)) :: + ("objects?" | constant(1)) :: + ("object_type?" | constant(hex"0002".bits)) :: + ("unknown" | constant(hex"ff240000".bits)) :: + ("P_len" | constant(hex"1000".bits)) :: + ("P" | bytes(16)) :: + ("G_len" | constant(hex"1000".bits)) :: + ("G" | bytes(16)) :: + ("end?" | constant(0)) :: + ("end?" | constant(0)) :: + ("objects?" | constant(1)) :: + ("unknown" | constant(hex"03070000".bits)) :: + ("end?" | constant(0)) + ).as[ClientChallengeXchg] +} + +final case class ServerChallengeXchg(time : Long, challenge : ByteVector, pubKey : ByteVector) + extends PlanetSideCryptoPacket { + type Packet = ServerChallengeXchg + def opcode = CryptoPacketOpcode.ServerChallengeXchg + def encode = ServerChallengeXchg.encode(this) +} + +object ServerChallengeXchg extends Marshallable[ServerChallengeXchg] { + def getCompleteChallenge(time : Long, rest : ByteVector): ByteVector = + uint32L.encode(time).require.toByteVector ++ rest + + implicit val codec: Codec[ServerChallengeXchg] = ( + ("unknown" | constant(2)) :: + ("unknown" | constant(1)) :: + ("server_time" | uint32L) :: + ("challenge" | bytes(0xC)) :: + ("end?" | constant(0)) :: + ("objects" | constant(1)) :: + ("unknown" | constant(hex"03070000000c00".bits)) :: + ("pub_key_len" | constant(hex"1000")) :: + ("pub_key" | bytes(16)) :: + ("unknown" | constant(0x0e)) + ).as[ServerChallengeXchg] +} + +final case class ClientFinished(pubKey : ByteVector, challengeResult: ByteVector) + extends PlanetSideCryptoPacket { + type Packet = ClientFinished + def opcode = CryptoPacketOpcode.ClientFinished + def encode = ClientFinished.encode(this) +} + +object ClientFinished extends Marshallable[ClientFinished] { + implicit val codec : Codec[ClientFinished] = ( + ("obj_type?" | constant(hex"10".bits)) :: + ("pub_key_len" | constant(hex"1000")) :: + ("pub_key" | bytes(16)) :: + ("unknown" | constant(hex"0114".bits)) :: + ("challenge_result" | bytes(0xc)) + ).as[ClientFinished] +} + +final case class ServerFinished(challengeResult : ByteVector) + extends PlanetSideCryptoPacket { + type Packet = ServerFinished + def opcode = CryptoPacketOpcode.ServerFinished + def encode = ServerFinished.encode(this) +} + +object ServerFinished extends Marshallable[ServerFinished] { + implicit val codec : Codec[ServerFinished] = ( + ("unknown" | constant(hex"0114".bits)) :: + ("challenge_result" | bytes(0xc)) + ).as[ServerFinished] +} + +// Game Packets +final case class LoginMessage(majorVersion : Long, + minorVersion : Long, + buildDate : String, + username : String, + password : Option[String], + token : Option[String], + revision : Long) extends PlanetSideGamePacket { + require(majorVersion >= 0) + require(minorVersion >= 0) + require(revision >= 0) + require(password.isDefined ^ token.isDefined, "Either 'username' or 'token' must be set, but not both") + + def opcode = GamePacketOpcode.LoginMessage + def encode = LoginMessage.encode(this) +} + +object LoginMessage extends Marshallable[LoginMessage] { + private def username = PacketHelpers.encodedStringAligned(7) + private def password = PacketHelpers.encodedString + private def tokenPath = fixedSizeBytes(32, ascii) :: username + private def passwordPath = username :: password + + type Struct = String :: Option[String] :: Option[String] :: HNil + + implicit val credentialChoice : Codec[Struct] = { + type InStruct = Either[String :: String :: HNil, String :: String :: HNil] + + def from(a : InStruct) : Struct = a match { + case Left(username :: password :: HNil) => username :: Some(password) :: None :: HNil + case Right(token :: username :: HNil) => username :: None :: Some(token) :: HNil + } + + // serialization can fail if the user did not specify a token or password (or both) + def to(a : Struct) : InStruct = a match { + case username :: Some(password) :: None :: HNil => Left(username :: password :: HNil) + case username :: None :: Some(token) :: HNil => Right(token :: username :: HNil) + } + + either(bool, passwordPath, tokenPath).xmap[Struct](from, to) + } + + implicit val codec : Codec[LoginMessage] = ( + ("major_version" | uint32L) :: + ("minor_version" | uint32L) :: + ("build_date" | PacketHelpers.encodedString) :: + ( + // The :+ operator (and the parens) are required because we are adding an HList to an HList, + // not merely a value (like bool). Weird shit, but hey this works. + ("credential_choice" | credentialChoice) :+ + ("revision" | uint32L) + ) + ).as[LoginMessage] +} + +final case class LoginRespMessage(token : String, // printable ascii for 16 + unknown : ByteVector, // hex"00000000 18FABE0C 00000000 00000000" + error : Long, // 0 + stationError : Long, // 1 + subscriptionStatus : Long, // 2 or 5 + someToken : Long, // 685276011 + username : String, // the user + unk5 : Long, // 0 and unset bool + someBit : Boolean) extends PlanetSideGamePacket { + def opcode = GamePacketOpcode.LoginRespMessage + def encode = LoginRespMessage.encode(this) +} + +object LoginRespMessage extends Marshallable[LoginRespMessage] { + implicit val codec : Codec[LoginRespMessage] = ( + ("token" | fixedSizeBytes(16, ascii)) :: + ("unknown" | bytes(16)) :: + ("error" | uint32L) :: + ("station_error" | uint32L) :: + ("subscription_status" | uint32L) :: + ("unknown" | uint32L) :: + ("username" | PacketHelpers.encodedString) :: + ("unknown" | uint32L) :: + ("unknown" | byteAligned(bool)) + ).as[LoginRespMessage] +} + +final case class ConnectToWorldMessage(world : String) + extends PlanetSideGamePacket { + type Packet = ConnectToWorldMessage + def opcode = GamePacketOpcode.ConnectToWorldMessage + def encode = ConnectToWorldMessage.encode(this) +} + +object ConnectToWorldMessage extends Marshallable[ConnectToWorldMessage] { + implicit val codec : Codec[ConnectToWorldMessage] = ascii.as[ConnectToWorldMessage] +} + +// Control Packets +final case class HandleGamePacket(packet : ByteVector) + extends PlanetSideControlPacket { + def opcode = ControlPacketOpcode.HandleGamePacket + def encode = throw new Exception("This packet type should never be encoded") +} + +object HandleGamePacket extends Marshallable[HandleGamePacket] { + implicit val codec : Codec[HandleGamePacket] = bytes.as[HandleGamePacket].decodeOnly +} + +final case class ClientStart(clientNonce : Long) + extends PlanetSideControlPacket { + type Packet = ClientStart + def opcode = ControlPacketOpcode.ClientStart + def encode = ClientStart.encode(this) +} + +object ClientStart extends Marshallable[ClientStart] { + implicit val codec : Codec[ClientStart] = ( + ("unknown" | constant(hex"00000002".bits)) :: + ("client_nonce" | uint32L) :: + ("unknown" | constant(hex"000001f0".bits)) + ).as[ClientStart] +} + +final case class ServerStart(clientNonce : Long, serverNonce : Long) + extends PlanetSideControlPacket { + type Packet = ServerStart + def opcode = ControlPacketOpcode.ServerStart + def encode = ServerStart.encode(this) +} + +object ServerStart extends Marshallable[ServerStart] { + implicit val codec : Codec[ServerStart] = ( + ("client_nonce" | uint32L) :: + ("server_nonce" | uint32L) :: + ("unknown" | constant(hex"000000000001d300000002".bits)) + ).as[ServerStart] +} + +final case class MultiPacket(packets : Vector[ByteVector]) + extends PlanetSideControlPacket { + type Packet = MultiPacket + def opcode = ControlPacketOpcode.MultiPacket + def encode = MultiPacket.encode(this) +} + +object MultiPacket extends Marshallable[MultiPacket] { + implicit val codec : Codec[MultiPacket] = ("packets" | vector(variableSizeBytes(uint8L, bytes))).as[MultiPacket] +} + +final case class SlottedMetaPacket(/*slot : Int,*/ packet : ByteVector) + extends PlanetSideControlPacket { + type Packet = SlottedMetaPacket + + //assert(slot >= 0 && slot <= 7, "Slot number is out of range") + + def opcode = { + val base = ControlPacketOpcode.SlottedMetaPacket0.id + ControlPacketOpcode(base/* + slot*/) + } + + def encode = SlottedMetaPacket.encode(this) +} + +object SlottedMetaPacket extends Marshallable[SlottedMetaPacket] { + implicit val codec : Codec[SlottedMetaPacket] = ( + ("unknown" | constant(0)) :: + ("unknown" | constant(0)) :: + ("rest" | bytes) + ).as[SlottedMetaPacket] +} + +final case class ConnectionClose() + extends PlanetSideControlPacket { + type Packet = ConnectionClose + def opcode = ControlPacketOpcode.ConnectionClose + def encode = ConnectionClose.encode(this) +} + +object ConnectionClose extends Marshallable[ConnectionClose] { + implicit val codec: Codec[ConnectionClose] = PacketHelpers.emptyCodec(ConnectionClose()) +} + + +///////////////////////////////////////////////////////////////// + +// Packet typing +final case class PlanetSidePacketFlags(packetType : PacketType.Value, secured : Boolean) + +// Enumeration starts at 1 +object PacketType extends Enumeration(1) { + type Type = Value + val ResetSequence, Unknown2, Crypto, Normal = Value + + val storageType = uint4L + + assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2), + this.getClass.getCanonicalName + ": maxId exceeds primitive type") + + implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType) +} + +object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] { + implicit val codec : Codec[PlanetSidePacketFlags] = ( + ("packet_type" | PacketType.codec) :: + ("unused" | constant(bin"0")) :: + ("secured" | bool) :: + ("advanced" | constant(bin"1")) :: // we only support "advanced packets" + ("length_specified" | constant(bin"0")) // we DO NOT support this field + ).as[PlanetSidePacketFlags] +} + +////////////////////////////////////////////////// + +// TODO: figure out why I can't insert codecs without using a new case class +// Notes: https://mpilquist.github.io/blog/2013/06/09/scodec-part-3/ +// https://stackoverflow.com/questions/29585649/using-nested-case-classes-with-scodec +// https://gist.github.com/travisbrown/3945529 + +object PacketHelpers { + def emptyCodec[T](instance : T) = { + def to(pkt: T) = HNil + def from(a: HNil) = instance + Codec[HNil].xmap[T](from, to) + } + + def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = { + type Struct = Int :: HNil + val struct: Codec[Struct] = storageCodec.hlist + + // Assure that the enum will always be able to fit in a N-bit int + assert(enum.maxId <= Math.pow(storageCodec.sizeBound.exact.get, 2), + this.getClass.getCanonicalName + ": maxId exceeds primitive type") + + def to(pkt: E#Value): Struct = { + pkt.id :: HNil + } + + def from(struct: Struct): Attempt[E#Value] = struct match { + case enumVal :: HNil => + // verify that this int can match the enum + val first = enum.values.firstKey.id + val last = enum.maxId-1 + + if(enumVal >= first && enumVal <= last) + Attempt.successful(enum(enumVal)) + else + Attempt.failure(Err(s"Expected ${enum} with ID between [${first}, ${last}], but got '${enumVal}'")) + } + + struct.narrow[E#Value](from, to) + } + + // when the first bit of the byte is set, the size can be between [0, 127]. + // otherwise, it is between [128, 32767] and two bytes are used for encoding + // The magic in this is next level + private def encodedStringSize : Codec[Int] = either(bool, uint(15), uint(7)). + xmap[Int]( + (a : Either[Int, Int]) => a.fold[Int](a => a, a => a), + (a : Int) => if(a > 0x7f) Left(a) else Right(a) + ) + + private def encodedStringSizeWithPad(pad : Int) : Codec[Int] = either(bool, uint(15), uint(7)). + xmap[Int]( + (a : Either[Int, Int]) => a.fold[Int](a => a, a => a), + (a : Int) => if(a > 0x7f) Left(a) else Right(a) + ) <~ ignore(pad) + + def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii) + def encodedStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment), ascii) +} diff --git a/common/src/main/scala/psforever/net/PacketCoding.scala b/common/src/main/scala/psforever/net/PacketCoding.scala new file mode 100644 index 000000000..9fb8a3d70 --- /dev/null +++ b/common/src/main/scala/psforever/net/PacketCoding.scala @@ -0,0 +1,377 @@ +package psforever.net + +import psforever.crypto.CryptoInterface +import psforever.crypto.CryptoInterface._ +import scodec.Attempt.{Successful, Failure} +import scodec.bits._ +import scodec.{DecodeResult, Err, Attempt, Codec} +import scodec.codecs.{uint16L, uint8L, uint4L, bytes} + +// Packet containers +sealed trait PlanetSidePacketContainer + +// A sequence, encrypted opcode, encrypted payload, and MD5MAC plus padding +final case class EncryptedPacket(sequenceNumber : Int, + payload : ByteVector) extends PlanetSidePacketContainer + +// A sequence, and payload. Crypto packets have no discernible opcodes +final case class CryptoPacket(sequenceNumber : Int, + packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer + +final case class GamePacket(opcode : GamePacketOpcode.Value, + sequenceNumber : Int, + packet : PlanetSideGamePacket) extends PlanetSidePacketContainer + +// Just an opcode + payload (does not expect a response) +final case class ControlPacket(opcode : ControlPacketOpcode.Value, + packet : PlanetSideControlPacket) extends PlanetSidePacketContainer + +object PacketCoding { + final val PLANETSIDE_MIN_PACKET_SIZE = 2 + + def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = { + UnmarshalPacket(msg, CryptoPacketOpcode.Ignore) + } + + def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = { + // check for a minimum length + if(msg.length < PLANETSIDE_MIN_PACKET_SIZE) + return Attempt.failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes")) + + val firstByte = msg{0} + + firstByte match { + // drop the first byte as control packets dont need it + case 0x00 => unmarshalControlPacket(msg.drop(1)) + case _ => unmarshalFlaggedPacket(msg, cryptoState) // returns either EncryptedPacket or CryptoPacket + } + } + + def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = { + // check for a minimum length + if(msg.length < PLANETSIDE_MIN_PACKET_SIZE) + return Attempt.failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes")) + + val firstByte = msg{0} + + firstByte match { + // drop the first byte as control packets dont need it + case 0x00 => DecodeControlPacket(msg.drop(1)) + case _ => DecodeGamePacket(msg) + } + } + + def MarshalPacket(packet : PlanetSidePacketContainer) : Attempt[BitVector] = { + var flagsEncoded : BitVector = BitVector.empty + var seqEncoded : BitVector = BitVector.empty + var paddingEncoded : BitVector = BitVector.empty + var opcodeEncoded : BitVector = BitVector.empty + var payloadEncoded : BitVector = BitVector.empty + + packet match { + case GamePacket(opcode, seq, payload) => + val flags = PlanetSidePacketFlags(PacketType.Normal, secured = false) + flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require + + GamePacketOpcode.codec.encode(opcode) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) + case Successful(p) => opcodeEncoded = p + } + + uint16L.encode(seq) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $opcode: " + e.messageWithContext)) + case Successful(p) => seqEncoded = p + } + + encodePacket(payload) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext)) + case Successful(p) => payloadEncoded = p + } + case ControlPacket(opcode, payload) => + flagsEncoded = hex"00".bits + + ControlPacketOpcode.codec.encode(opcode) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) + case Successful(p) => opcodeEncoded = p + } + + encodePacket(payload) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext)) + case Successful(p) => payloadEncoded = p + } + case CryptoPacket(seq, payload) => + val flags = PlanetSidePacketFlags(PacketType.Crypto, secured = false) + flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require + + uint16L.encode(seq) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext)) + case Successful(p) => seqEncoded = p + } + + encodePacket(payload) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $payload: " + e.messageWithContext)) + case Successful(p) => payloadEncoded = p + } + case EncryptedPacket(seq, payload) => + val flags = PlanetSidePacketFlags(PacketType.Normal, secured = true) + flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require + + // encrypted packets need to be aligned to 4 bytes before encryption/decryption + // first byte are flags, second and third the sequence, and fourth is the pad + paddingEncoded = hex"00".bits + + uint16L.encode(seq) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext)) + case Successful(p) => seqEncoded = p + } + + payloadEncoded = payload.bits + } + + val finalPacket = flagsEncoded ++ seqEncoded ++ paddingEncoded ++ opcodeEncoded ++ payloadEncoded + Attempt.successful(finalPacket) + } + + def CreateControlPacket(packet : PlanetSideControlPacket) = ControlPacket(packet.opcode, packet) + def CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet) + def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet) + + ////////////////////////////////////////////////////////////////////////////// + + private def encodePacket(packet : PlanetSidePacket) : Attempt[BitVector] = packet.encode + + private def unmarshalFlaggedPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = { + val decodedFlags = Codec.decode[PlanetSidePacketFlags](BitVector(msg)) + + decodedFlags match { + case Failure(e) => + return Attempt.failure(Err("Failed to parse packet flags: " + e.message)) + case _ => + } + + val flags = decodedFlags.require.value + val rest = decodedFlags.require.remainder + val packetType = flags.packetType + + // perform a quick reject of weird packet types + packetType match { + case PacketType.Crypto => ; + case PacketType.Normal => ; + case default => + return Attempt.failure(Err("Unsupported packet type: " + flags.packetType.toString)) + } + + // we only support normal packets if they are encrypted + if(packetType == PacketType.Normal && !flags.secured) + return Attempt.failure(Err("Unsupported packet type: normal packets must be encryped")) + + // we only support crypto packets if they are not encrypted + if(packetType == PacketType.Crypto && flags.secured) + return Attempt.failure(Err("Unsupported packet type: crypto packets must be unencrypted")) + + // all packets have a two byte sequence ID + // TODO: make this a codec for reuse + val decodedSeq = uint16L.decode(rest) + + decodedSeq match { + case Failure(e) => + return Attempt.failure(Err("Failed to parse packet sequence number: " + e.message)) + case _ => + } + + val sequence = decodedSeq.require.value + var payload = decodedSeq.require.remainder.toByteVector + + // encrypted packets must be 4-byte aligned + if(flags.secured) { + payload = payload.drop(1) + } + + packetType match { + case PacketType.Crypto => + unmarshalCryptoPacket(cryptoState, sequence, payload) + case PacketType.Normal => + unmarshalEncryptedPacket(sequence, payload) + } + } + + private def unmarshalControlPacket(msg : ByteVector) : Attempt[ControlPacket] = { + val packet = DecodeControlPacket(msg) + + packet match { + case f @ Failure(e) => f + case Successful(p) => + Attempt.successful(CreateControlPacket(p)) + } + } + + def DecodeControlPacket(msg : ByteVector) : Attempt[PlanetSideControlPacket] = { + val opcode = ControlPacketOpcode.codec.decode(msg.bits) + + opcode match { + case Failure(e) => + return Attempt.failure(Err("Failed to decode control packet's opcode: " + e.message)) + case _ => + } + + val packet = ControlPacketOpcode.getPacketDecoder(opcode.require.value)(opcode.require.remainder) + + packet match { + case Failure(e) => + Attempt.failure(Err(f"Failed to parse control packet 0x${opcode.require.value.id}%02x: " + e.messageWithContext)) + case Successful(p) => Attempt.successful(p.value) + } + } + + private def unmarshalGamePacket(sequence : Int, msg : ByteVector) : Attempt[GamePacket] = { + val packet = DecodeGamePacket(msg) + + packet match { + case f @ Failure(e) => f + case Successful(p) => + Attempt.successful(CreateGamePacket(sequence, p)) + } + } + + def DecodeGamePacket(msg : ByteVector) : Attempt[PlanetSideGamePacket] = { + val opcode = GamePacketOpcode.codec.decode(msg.bits) + + opcode match { + case Failure(e) => + return Attempt.failure(Err("Failed to decode game packet's opcode: " + e.message)) + case _ => + } + + val packet = GamePacketOpcode.getPacketDecoder(opcode.require.value)(opcode.require.remainder) + + packet match { + case Failure(e) => + Attempt.failure(Err(f"Failed to parse game packet 0x${opcode.require.value.id}%02x: " + e.messageWithContext)) + case Successful(p) => Attempt.successful(p.value) + } + } + + private def unmarshalCryptoPacket(state : CryptoPacketOpcode.Type, sequence : Int, payload : ByteVector) : Attempt[CryptoPacket] = { + val packet = CryptoPacketOpcode.getPacketDecoder(state)(payload.bits) + + packet match { + case Successful(a) => + Attempt.successful(CryptoPacket(sequence, a.value)) + case Failure(e) => + Attempt.failure(e.pushContext("unmarshal_crypto_packet")) + } + } + + private def unmarshalEncryptedPacket(sequence : Int, payload : ByteVector) : Attempt[EncryptedPacket] = { + Attempt.successful(EncryptedPacket(sequence, payload)) + } + + /////////////////////////////////////////////////////////// + // Packet Crypto + /////////////////////////////////////////////////////////// + + def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = { + // TODO: this is bad. rework + var sequenceNumber = 0 + + val rawPacket : BitVector = packet match { + case GamePacket(opcode, seq, payload) => + val opcodeEncoded = GamePacketOpcode.codec.encode(opcode) + sequenceNumber = seq + + opcodeEncoded match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.message)) + case _ => + } + + encodePacket(payload) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext)) + case Successful(p) => opcodeEncoded.require ++ p + } + case ControlPacket(opcode, payload) => + val opcodeEncoded = ControlPacketOpcode.codec.encode(opcode) + + opcodeEncoded match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext)) + case _ => + } + + encodePacket(payload) match { + case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext)) + case Successful(p) => hex"00".bits ++ opcodeEncoded.require ++ p + } + case default => throw new IllegalArgumentException("Unsupported packet container type") + } + + val packetMac = crypto.macForEncrypt(rawPacket.toByteVector) + + // opcode, payload, and MAC + val packetNoPadding = rawPacket.toByteVector ++ packetMac + + val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE + + // minus 1 because of the actual byte telling of the padding, which always has to be there + val paddingNeeded = CryptoInterface.RC5_BLOCK_SIZE - remainder - 1 + val paddingEncoded = uint8L.encode(paddingNeeded).require + + val packetWithPadding = packetNoPadding ++ ByteVector.fill(paddingNeeded)(0x00) ++ paddingEncoded.toByteVector + + // raw packets plus MAC must be padded to the nearest 16 byte boundary + val encryptedPayload = crypto.encrypt(packetWithPadding) + + Attempt.successful(EncryptedPacket(sequenceNumber, encryptedPayload)) + } + + def decryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : EncryptedPacket) : Attempt[PlanetSidePacketContainer] = { + val payloadDecrypted = crypto.decrypt(packet.payload) + + // get the last byte which is the padding length + val payloadJustLen = payloadDecrypted.takeRight(1) + val padding = uint8L.decode(payloadJustLen.bits) + + padding match { + case Failure(e) => return Attempt.failure(Err("Failed to decode the encrypted padding length: " + e.message)) + case _ => + } + + val macSize = CryptoInterface.MD5_MAC_SIZE + val macDecoder = bytes(macSize) + val payloadNoPadding = payloadDecrypted.dropRight(1 + padding.require.value) + val payloadMac = payloadNoPadding.takeRight(macSize) + val payloadNoMac = payloadNoPadding.dropRight(macSize) + + /* + println("Payload: " + packet.payload) + println("DecPayload: " + payloadDecrypted) + println("DecPayloadNoLen: " + payloadJustLen) + println("Padding: " + padding.require.value) + println("NoPadding: " + payloadNoPadding) + println("Mac: " + payloadMac) + println("NoMac: " + payloadNoMac)*/ + + + val mac = macDecoder.decode(payloadMac.bits) + + mac match { + case Failure(e) => return Attempt.failure(Err("Failed to extract the encrypted MAC: " + e.message)) + case _ => + } + + val computedMac = crypto.macForDecrypt(payloadNoMac) + + // verify that the MAC matches + if(!CryptoInterface.verifyMAC(computedMac, mac.require.value)) + throw new SecurityException("Invalid packet MAC") + + if(payloadNoMac.length < PLANETSIDE_MIN_PACKET_SIZE) { + return Attempt.failure(Err(s"Decrypted packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes")) + } + + val firstByte = payloadNoMac{0} + + firstByte match { + case 0x00 => unmarshalControlPacket(payloadNoMac.drop(1)) + case _ => unmarshalGamePacket(packet.sequenceNumber, payloadNoMac) + } + } +} diff --git a/common/src/main/scala/sna/Library.scala b/common/src/main/scala/sna/Library.scala new file mode 100644 index 000000000..cf24cc010 --- /dev/null +++ b/common/src/main/scala/sna/Library.scala @@ -0,0 +1,60 @@ +/* Copyright (c) 2014 Sanjay Dasgupta, All Rights Reserved + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ + +package sna + +import com.sun.jna.{Function => JNAFunction} + +import scala.collection.mutable +import scala.language.dynamics + +class Library (val libName: String) extends Dynamic { + + class Invocation (val jnaFunction: JNAFunction, val args: Array[Object]) { + + // TODO: this does not call without a passed type parameter + def apply[R](implicit m: Manifest[R]): R = { + //println("invoking " + jnaFunction.getName + ". class " + m.runtimeClass.toString) + if (m.runtimeClass == classOf[Unit]) { + jnaFunction.invoke(args).asInstanceOf[R] + } else { + jnaFunction.invoke(m.runtimeClass, args).asInstanceOf[R] + } + } + + def as[R](implicit m: Manifest[R]) = apply[R](m) + def asInstanceOf[R](implicit m: Manifest[R]) = apply[R](m) + } + + def applyDynamic(functionName: String)(args: Any*) = { + new Invocation(loadFunction(functionName), args.map(_.asInstanceOf[Object]).toArray[Object]) + } + + private def loadFunction(functionName : String) : JNAFunction = { + var jnaFunction: JNAFunction = null + if (functionCache.contains(functionName)) { + jnaFunction = functionCache(functionName) + } else { + jnaFunction = JNAFunction.getFunction(libName, functionName) + functionCache(functionName) = jnaFunction + } + + jnaFunction + } + + def prefetch(functionName : String) : Unit = { + loadFunction(functionName) + } + + private val functionCache = mutable.Map.empty[String, JNAFunction] +} diff --git a/common/src/test/scala/PacketCodingTest.scala b/common/src/test/scala/PacketCodingTest.scala new file mode 100644 index 000000000..f819c9ee7 --- /dev/null +++ b/common/src/test/scala/PacketCodingTest.scala @@ -0,0 +1,68 @@ +import org.specs2.mutable._ +import psforever.crypto.CryptoInterface +import psforever.net._ +import scodec.bits._ + +class PacketCodingTest extends Specification { + /*def roundTrip[Container <: PlanetSidePacketContainer, Packet <: PlanetSidePacket](cont : Container, pkt : Packet) = { + + val filledContainer = cont match { + case x : ControlPacket => x.copy(packet = pkt.asInstanceOf[PlanetSideControlPacket]) + } + val pktEncoded = PacketCoding.MarshalPacket(ControlPacket(packetUnderTest.opcode, packetUnderTest)).require + val pktDecoded = PacketCoding.UnMarshalPacket(pkt.toByteVector).require.asInstanceOf[ControlPacket] + val recvPkt = decoded.packet.asInstanceOf[ServerStart] + + }*/ + + "Packet coding" should { + "correctly decode control packets" in { + val packet = PacketCoding.UnmarshalPacket(hex"0001 00000002 00261e27 000001f0").require + + packet.isInstanceOf[ControlPacket] mustEqual true + + val controlPacket = packet.asInstanceOf[ControlPacket] + controlPacket.opcode mustEqual ControlPacketOpcode.ClientStart + controlPacket.packet mustEqual ClientStart(656287232) + } + + "encode and decode to identical packets" in { + val clientNonce = 213129 + val serverNonce = 848483 + + val packetUnderTest = ServerStart(clientNonce, serverNonce) + val pkt = PacketCoding.MarshalPacket(ControlPacket(packetUnderTest.opcode, packetUnderTest)).require + + val decoded = PacketCoding.UnmarshalPacket(pkt.toByteVector).require.asInstanceOf[ControlPacket] + val recvPkt = decoded.packet.asInstanceOf[ServerStart] + + packetUnderTest mustEqual recvPkt + } + + "reject corrupted control packets" in { + val packet = PacketCoding.UnmarshalPacket(hex"0001 00001002 00261e27 004101f0") + + packet.isSuccessful mustEqual false + } + + "correctly decode crypto packets" in { + val packet = PacketCoding.UnmarshalPacket(hex"0001 00000002 00261e27 000001f0").require + + packet.isInstanceOf[ControlPacket] mustEqual true + + val controlPacket = packet.asInstanceOf[ControlPacket] + controlPacket.opcode mustEqual ControlPacketOpcode.ClientStart + controlPacket.packet mustEqual ClientStart(656287232) + } + + "reject bad packet types" in { + PacketCoding.UnmarshalPacket(hex"ff414141").isFailure mustEqual true + } + + "reject small packets" in { + PacketCoding.UnmarshalPacket(hex"00").isFailure mustEqual true + PacketCoding.UnmarshalPacket(hex"").isFailure mustEqual true + } + } + +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 000000000..d638b4f34 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 0.13.8 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 000000000..35619cb7e --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,3 @@ +logLevel := Level.Warn + +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2") \ No newline at end of file diff --git a/pslogin/src/main/scala/LoginSession.scala b/pslogin/src/main/scala/LoginSession.scala new file mode 100644 index 000000000..ea7841d14 --- /dev/null +++ b/pslogin/src/main/scala/LoginSession.scala @@ -0,0 +1,11 @@ +import java.net.InetSocketAddress + +import akka.actor.ActorRef +import scodec.bits.{BitVector, ByteVector} + +class LoginSession(id : Long, socket : ActorRef, address : InetSocketAddress) { + + def send(msg : BitVector) = { + socket ! SendPacket(msg.toByteVector, address) + } +} diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/pslogin/src/main/scala/LoginSessionActor.scala new file mode 100644 index 000000000..68af1ddc9 --- /dev/null +++ b/pslogin/src/main/scala/LoginSessionActor.scala @@ -0,0 +1,250 @@ +import akka.actor.{Actor, ActorLogging} +import psforever.crypto.CryptoInterface.{CryptoStateWithMAC, CryptoState} +import psforever.crypto.CryptoInterface +import psforever.net._ +import scodec.Attempt.{Successful, Failure} +import scodec.bits._ +import scodec.{Err, Attempt, Codec} +import scodec.codecs.{uint16L, uint8L, bytes} +import java.security.SecureRandom + +/*sealed trait SessionState extends Serializable +final case class NewSession() extends SessionState +final case class EstablishSecureChannel() extends SessionState +final case class SessionDead() extends SessionState*/ + +class LoginSessionActor(session : LoginSession) extends Actor with ActorLogging { + var cryptoDHState = new CryptoInterface.CryptoDHState() + var cryptoState : Option[CryptoInterface.CryptoStateWithMAC] = None + val random = new SecureRandom() + + // crypto handshake state + var serverChallenge = ByteVector.empty + var serverChallengeResult = ByteVector.empty + var serverMACBuffer = ByteVector.empty + + var clientPublicKey = ByteVector.empty + var clientChallenge = ByteVector.empty + var clientChallengeResult = ByteVector.empty + + def receive = clientStart + + def clientStart : Receive = { + case RawPacket(msg) => + PacketCoding.UnmarshalPacket(msg) match { + case Failure(e) => log.error("Could not decode packet: " + e) + case Successful(p) => + println("RECV: " + p) + + p match { + case ControlPacket(_, ClientStart(nonce)) => + sendResponse(PacketCoding.CreateControlPacket(ServerStart(nonce, Math.abs(random.nextInt())))) + context.become(clientXchg) + case default => + log.error("Unexpected packet type " + p) + } + } + case default => log.error(s"Invalid message received ${default}") + } + + def clientXchg : Receive = { + case RawPacket(msg) => + PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientChallengeXchg) match { + case Failure(e) => log.error("Could not decode packet: " + e) + case Successful(p) => + println("RECV: " + p) + + p match { + case CryptoPacket(seq, ClientChallengeXchg(time, challenge, p, g)) => + // initialize our crypto state from the client's P and G + cryptoDHState.start(p, g) + + // save the client challenge + clientChallenge = ServerChallengeXchg.getCompleteChallenge(time, challenge) + + // save the packet we got for a MAC check later. drop the first 3 bytes + serverMACBuffer ++= msg.drop(3) + + val serverTime = System.currentTimeMillis() / 1000L + val randomChallenge = getRandBytes(0xc) + + // store the complete server challenge for later + serverChallenge = ServerChallengeXchg.getCompleteChallenge(serverTime, randomChallenge) + + val packet = PacketCoding.CreateCryptoPacket(seq, + ServerChallengeXchg(serverTime, randomChallenge, cryptoDHState.getPublicKey)) + + val sentPacket = sendResponse(packet) + + // save the sent packet a MAC check + serverMACBuffer ++= sentPacket.drop(3) + + context.become(clientFinished) + case default => log.error("Unexpected packet type " + p) + } + } + case default => log.error(s"Invalid message received ${default}") + } + + def clientFinished : Receive = { + case RawPacket(msg) => + PacketCoding.UnmarshalPacket(msg, CryptoPacketOpcode.ClientFinished) match { + case Failure(e) => log.error("Could not decode packet: " + e) + case Successful(p) => + println("RECV: " + p) + + p match { + case CryptoPacket(seq, ClientFinished(clientPubKey, clientChalResult)) => + clientPublicKey = clientPubKey + clientChallengeResult = clientChalResult + + // save the packet we got for a MAC check later + serverMACBuffer ++= msg.drop(3) + + val agreedValue = cryptoDHState.agree(clientPublicKey) + + /*println("Agreed: " + agreedValue) + println(s"Client challenge: $clientChallenge")*/ + val agreedMessage = ByteVector("master secret".getBytes) ++ clientChallenge ++ + hex"00000000" ++ serverChallenge ++ hex"00000000" + + //println("In message: " + agreedMessage) + + val masterSecret = CryptoInterface.MD5MAC(agreedValue, + agreedMessage, + 20) + + //println("Master secret: " + masterSecret) + + serverChallengeResult = CryptoInterface.MD5MAC(masterSecret, + ByteVector("server finished".getBytes) ++ serverMACBuffer ++ hex"01", + 0xc) + + val clientChallengeResultCheck = CryptoInterface.MD5MAC(masterSecret, + ByteVector("client finished".getBytes) ++ serverMACBuffer ++ hex"01" ++ clientChallengeResult ++ hex"01", + 0xc) + + //println("Check result: " + CryptoInterface.verifyMAC(clientChallenge, clientChallengeResult)) + + val decExpansion = ByteVector("client expansion".getBytes) ++ hex"0000" ++ serverChallenge ++ + hex"00000000" ++ clientChallenge ++ hex"00000000" + + val encExpansion = ByteVector("server expansion".getBytes) ++ hex"0000" ++ serverChallenge ++ + hex"00000000" ++ clientChallenge ++ hex"00000000" + + /*println("DecExpansion: " + decExpansion) + println("EncExpansion: " + encExpansion)*/ + + // expand the encryption and decryption keys + // The first 20 bytes are for RC5, and the next 16 are for the MAC'ing keys + val expandedDecKey = CryptoInterface.MD5MAC(masterSecret, + decExpansion, + 0x40) // this is what is visible in IDA + + val expandedEncKey = CryptoInterface.MD5MAC(masterSecret, + encExpansion, + 0x40) + + val decKey = expandedDecKey.take(20) + val encKey = expandedEncKey.take(20) + val decMACKey = expandedDecKey.drop(20).take(16) + val encMACKey = expandedEncKey.drop(20).take(16) + + /*println("**** DecKey: " + decKey) + println("**** EncKey: " + encKey) + println("**** DecMacKey: " + decMACKey) + println("**** EncMacKey: " + encMACKey)*/ + + // spin up our encryption program + cryptoState = Some(new CryptoStateWithMAC(decKey, encKey, decMACKey, encMACKey)) + + val packet = PacketCoding.CreateCryptoPacket(seq, + ServerFinished(serverChallengeResult)) + + sendResponse(packet) + + context.become(established) + case default => failWithError("Unexpected packet type " + default) + } + } + case default => failWithError(s"Invalid message received ${default}") + } + + def established : Receive = { + case RawPacket(msg) => + PacketCoding.UnmarshalPacket(msg) match { + case Successful(p) => + p match { + case encPacket @ EncryptedPacket(seq, _) => + println("Decrypting packet..." + encPacket) + PacketCoding.decryptPacket(cryptoState.get, encPacket) match { + case Successful(packet) => + println("RECV[E]: " + packet) + + self ! packet + case Failure(e) => + println("Failed to decode encrypted packet: " + e) + } + case default => failWithError("Unexpected packet type " + default) + + } + case Failure(e) => println("Could not decode raw packet: " + e) + } + case ctrl @ ControlPacket(_, pkt) => pkt match { + case SlottedMetaPacket(innerPacket) => + PacketCoding.DecodePacket(innerPacket) match { + case Successful(p) => + println("RECV[INNER]: " + p) + + val packet = PacketCoding.encryptPacket(cryptoState.get, PacketCoding.CreateGamePacket(3, + LoginRespMessage("AAAABBBBCCCCDDDD", + hex"00000000 18FABE0C 00000000 00000000", + 0, 1, 2, 685276011, + "AAAAAAAA", 0, false + ))).require + + sendResponse(packet) + case Failure(e) => println("Failed to decode inner packet " + e) + } + } + case default => failWithError(s"Invalid message received ${default}") + } + + def failWithError(error : String) = { + log.error(error) + sendResponse(PacketCoding.CreateControlPacket(ConnectionClose())) + } + + def resetState() : Unit = { + context.become(receive) + + // reset the crypto primitives + cryptoDHState.close + cryptoDHState = new CryptoInterface.CryptoDHState() + + if(cryptoState.isDefined) { + cryptoState.get.close + cryptoState = None + } + + serverChallenge = ByteVector.empty + serverChallengeResult = ByteVector.empty + serverMACBuffer = ByteVector.empty + clientPublicKey = ByteVector.empty + clientChallenge = ByteVector.empty + clientChallengeResult = ByteVector.empty + } + + def sendResponse(cont : PlanetSidePacketContainer) : ByteVector = { + println("SEND: " + cont) + val pkt = PacketCoding.MarshalPacket(cont).require + session.send(pkt) + pkt.toByteVector + } + + def getRandBytes(amount : Int) : ByteVector = { + val array = Array.ofDim[Byte](amount) + random.nextBytes(array) + ByteVector.view(array) + } +} diff --git a/pslogin/src/main/scala/PsLogin.scala b/pslogin/src/main/scala/PsLogin.scala new file mode 100644 index 000000000..ebd7d71e1 --- /dev/null +++ b/pslogin/src/main/scala/PsLogin.scala @@ -0,0 +1,22 @@ +import akka.actor.{Props, ActorSystem} +import psforever.crypto.CryptoInterface + +object PsLogin { + def main(args : Array[String]) : Unit = { + println("PsLogin v0.1") + + try { + CryptoInterface.initialize() + println("Crypto initialized") + } + catch { + case e : UnsatisfiedLinkError => + println("Unable to initialize " + CryptoInterface.libName) + sys.exit(1) + } + + val system = ActorSystem("PsLogin") + val session = system.actorOf(Props[SessionRouter], "session-router") + val listener = system.actorOf(Props(new UdpListener(session)), "udp-listener") + } +} diff --git a/pslogin/src/main/scala/SessionRouter.scala b/pslogin/src/main/scala/SessionRouter.scala new file mode 100644 index 000000000..858104c18 --- /dev/null +++ b/pslogin/src/main/scala/SessionRouter.scala @@ -0,0 +1,38 @@ +import java.net.InetSocketAddress + +import akka.actor.{Props, ActorRef, ActorLogging, Actor} +import scodec.bits._ + +import scala.collection.mutable + +final case class RawPacket(data : ByteVector) + +class SessionRouter extends Actor with ActorLogging { + val sessions = mutable.Map[InetSocketAddress, ActorRef]() + var sessionId = 0L + + def receive = { + case ReceivedPacket(msg, from) => + if(sessions.contains(from)) { + sessions{from} ! RawPacket(msg) + } else { + log.info("New session from " + from.toString) + + val id = newSessionId + val loginSession = new LoginSession(id, sender(), from) + val ref = context.actorOf(Props(new LoginSessionActor(loginSession)), + "login-session" + id.toString) + + sessions{from} = ref + + ref ! RawPacket(msg) + } + case _ => log.error("Unknown message") + } + + def newSessionId = { + val oldId = sessionId + sessionId += 1 + oldId + } +} diff --git a/pslogin/src/main/scala/UdpListener.scala b/pslogin/src/main/scala/UdpListener.scala new file mode 100644 index 000000000..9ef9d749f --- /dev/null +++ b/pslogin/src/main/scala/UdpListener.scala @@ -0,0 +1,35 @@ +import java.net.InetSocketAddress + +import akka.actor.{ActorLogging, Actor, ActorRef} +import akka.io._ +import scodec.bits.ByteVector +import scodec.interop.akka._ + +final case class ReceivedPacket(msg : ByteVector, from : InetSocketAddress) +final case class SendPacket(msg : ByteVector, to : InetSocketAddress) + +class UdpListener(nextActor: ActorRef) extends Actor with ActorLogging { + import context.system + IO(Udp) ! Udp.Bind(self, new InetSocketAddress("localhost", 51000)) + + var bytesRecevied = 0L + var bytesSent = 0L + + def receive = { + case Udp.Bound(local) => + println("UDP bound: " + local) + context.become(ready(sender())) + } + + def ready(socket: ActorRef): Receive = { + case SendPacket(msg, to) => + bytesSent += msg.size + socket ! Udp.Send(msg.toByteString, to) + case Udp.Received(data, remote) => + bytesRecevied += data.size + nextActor ! ReceivedPacket(data.toByteVector, remote) + case Udp.Unbind => socket ! Udp.Unbind + case Udp.Unbound => context.stop(self) + case x : Any => log.error("Unhandled message: " + x.toString) + } +} \ No newline at end of file diff --git a/pslogin/src/main/scala/psforever/crypto/CryptoStateManager.scala b/pslogin/src/main/scala/psforever/crypto/CryptoStateManager.scala new file mode 100644 index 000000000..c760fdd0f --- /dev/null +++ b/pslogin/src/main/scala/psforever/crypto/CryptoStateManager.scala @@ -0,0 +1,40 @@ +package psforever.crypto + +import akka.actor.{Actor, ActorLogging, FSM} +import akka.util.ByteString +import scodec.Codec +import scodec.bits.ByteVector + +sealed trait CryptoState +final case class ClientStart() extends CryptoState +final case class ServerStart() extends CryptoState +final case class ClientChallengeXchg() extends CryptoState +final case class ServerChallengeXchg() extends CryptoState +final case class ClientFinished() extends CryptoState +final case class ServerFinished() extends CryptoState + +sealed trait CryptoData +final case class Uninitialized() extends CryptoData + +class CryptoStateManager extends Actor with ActorLogging with FSM[CryptoState, CryptoData] { + startWith(ClientStart(), Uninitialized()) + + when(ClientStart()) { + /*case Event(RawPacket(msg), _) => { + val decoded = Codec.decode[psforever.net.ClientStart](msg.bits) + try { + val packet = decoded.require.value + println("Got cNonce: " + packet.clientNonce) + } + catch { + case e : Exception => + println("Invalid packet: " + e.getMessage) + } + + stay + }*/ + case _ => stay + } + + initialize() +} diff --git a/pslogin/src/test/scala/CryptoInterfaceTest.scala b/pslogin/src/test/scala/CryptoInterfaceTest.scala new file mode 100644 index 000000000..ac5b3da2e --- /dev/null +++ b/pslogin/src/test/scala/CryptoInterfaceTest.scala @@ -0,0 +1,135 @@ +import org.specs2.mutable._ +import psforever.crypto.CryptoInterface +import psforever.crypto.CryptoInterface.{CryptoState, CryptoDHState} +import scodec.bits._ + +class CryptoInterfaceTest extends Specification { + "Crypto interface" should { + "correctly initialize" in { + CryptoInterface.initialize() + ok + } + + "encrypt and decrypt" in { + val key = hex"41414141" + val plaintext = ByteVector.fill(16)(0x42) + + val crypto = new CryptoInterface.CryptoState(key, key) + + val ciphertext = crypto.encrypt(plaintext) + val decrypted = crypto.decrypt(ciphertext) + + crypto.close + decrypted mustEqual plaintext + ciphertext mustNotEqual plaintext + } + + "encrypt and decrypt must handle no bytes" in { + val key = hex"41414141" + val empty = ByteVector.empty + + val crypto = new CryptoInterface.CryptoState(key, key) + + val ciphertext = crypto.encrypt(empty) + val decrypted = crypto.decrypt(ciphertext) + + crypto.close + + ciphertext mustEqual empty + decrypted mustEqual empty + } + + "encrypt and decrypt must only accept block aligned inputs" in { + val key = hex"41414141" + val badPad = ByteVector.fill(CryptoInterface.RC5_BLOCK_SIZE-1)('a') + + val crypto = new CryptoInterface.CryptoState(key, key) + + crypto.encrypt(badPad) must throwA[IllegalArgumentException] + crypto.decrypt(badPad) must throwA[IllegalArgumentException] + + crypto.close + ok + } + + "arrive at a shared secret" in { + val server = new CryptoInterface.CryptoDHState() + val client = new CryptoInterface.CryptoDHState() + + // 1. Client generates p, g, and its key pair + client.start() + + // 2. Client sends p and g to server who then generates a key pair as well + server.start(client.getModulus, client.getGenerator) + + // 3. Both parties come to a shared secret + val clientAgreed = client.agree(server.getPublicKey) + val serverAgreed = server.agree(client.getPublicKey) + + // Free resources + server.close + client.close + + clientAgreed mustEqual serverAgreed + } + + "must fail to agree on a secret with a bad public key" in { + val server = new CryptoInterface.CryptoDHState() + val client = new CryptoInterface.CryptoDHState() + + // 1. Client generates p, g, and its key pair + client.start() + + // 2. Client sends p and g to server who then generates a key pair as well + server.start(client.getModulus, client.getGenerator) + + // 3. Client agrees with a bad public key, so it must fail + val clientAgreed = client.agree(client.getPublicKey) + val serverAgreed = server.agree(client.getPublicKey) + + // Free resources + server.close + client.close + + clientAgreed mustNotEqual serverAgreed + } + + "MD5MAC correctly" in { + val key = hex"377b60f8790f91b35a9da82945743da9" + val message = ByteVector(Array[Byte]('m', 'a', 's', 't', 'e', 'r', ' ', 's', 'e', 'c', 'r', 'e', 't')) ++ + hex"b4aea1559444a20b6112a2892de40eac00000000c8aea155b53d187076b79abab59001b600000000" + val expected = hex"5aa15de41f5220cf5cca489155e1438c5aa15de4" + + val output = CryptoInterface.MD5MAC(key, message, expected.length) + + output mustEqual expected + } + + "safely handle multiple starts" in { + val dontCare = ByteVector.fill(16)(0x42) + var dh = new CryptoDHState() + + dh.start() + dh.start() must throwA[IllegalStateException] + dh.close + + dh = new CryptoDHState() + + ok + } + + "prevent function calls before initialization" in { + val dontCare = ByteVector.fill(16)(0x42) + val dh = new CryptoDHState() + + dh.getGenerator must throwA[IllegalStateException] + dh.getModulus must throwA[IllegalStateException] + dh.getPrivateKey must throwA[IllegalStateException] + dh.getPublicKey must throwA[IllegalStateException] + dh.agree(dontCare) must throwA[IllegalStateException] + dh.close + + ok + } + } +} diff --git a/pslogin/src/test/scala/CryptoPackets.scala b/pslogin/src/test/scala/CryptoPackets.scala new file mode 100644 index 000000000..917ca9196 --- /dev/null +++ b/pslogin/src/test/scala/CryptoPackets.scala @@ -0,0 +1,133 @@ +import org.specs2.mutable._ +import psforever.net._ +import scodec.Codec +import scodec.bits._ + +class CryptoPackets extends Specification { + + "PlanetSide crypto packet" in { + val cNonce = 656287232 + + "ClientStart" should { + val string = hex"0000000200261e27000001f0".bits + + "decode" in { + val res = Codec.decode[ClientStart](string) + res.isSuccessful mustEqual true + res.require.value.clientNonce mustEqual cNonce + } + + "encode" in { + val res = Codec.encode(ClientStart(cNonce)) + + res.require mustEqual string + } + } + + "ServerStart" should { + val sNonce = 3468803409L + val string = hex"00261e2751bdc1ce000000000001d300000002".bits + + "decode" in { + val res = Codec.decode[ServerStart](string) + val value = res.require.value + + value.clientNonce mustEqual cNonce + value.serverNonce mustEqual sNonce + } + + "encode" in { + val res = Codec.encode(ServerStart(cNonce, sNonce)) + + res.require mustEqual string + } + } + + "ClientChallengeXchg" should { + val time = hex"962d8453" + val timeDecoded = scodec.codecs.uint32L.decode(time.bits).require.value + val challenge = hex"24f5997c c7d16031 d1f567e9" + val p = hex"f57511eb 8e5d1efb 8b7f3287 d5a18b17" + val g = hex"00000000 00000000 00000000 00000002" + val string = (hex"0101" ++ time ++ challenge ++ hex"00 01 0002 ff 2400 00 1000" ++ + p ++ hex"1000" ++ g ++ hex"0000010307000000").bits + + "decode" in { + val res = Codec.decode[ClientChallengeXchg](string) + val value = res.require.value + + value.time mustEqual timeDecoded + value.challenge mustEqual challenge + value.p mustEqual p + value.g mustEqual g + } + + "encode" in { + val res = Codec.encode(ClientChallengeXchg(timeDecoded, challenge, p, g)) + + res.require mustEqual string + } + } + + "ServerChallengeXchg" should { + val time = hex"962d8453" + val timeDecoded = scodec.codecs.uint32L.decode(time.bits).require.value + val challenge = hex"1b0e6408 cd935ec2 429aeb58" + val pubKey = hex"51f83ce6 45e86c3e 79c8fc70 f6ddf14b" + val string = (hex"0201" ++ time ++ challenge ++ hex"00 01 03070000000c00 1000 " ++ pubKey ++ hex"0e").bits + + "decode" in { + val res = Codec.decode[ServerChallengeXchg](string) + val value = res.require.value + + value.time mustEqual timeDecoded + value.challenge mustEqual challenge + value.pubKey mustEqual pubKey + } + + "encode" in { + val res = Codec.encode(ServerChallengeXchg(timeDecoded, challenge, pubKey)) + + res.require mustEqual string + } + } + + "ClientFinished" should { + val challengeResult = hex"ea3cf05d a5cb4256 8bb91aa7" + val pubKey = hex"eddc35f2 52b02d0e 496ba273 54578e73" + val string = (hex"10 1000" ++ pubKey ++ hex"0114 " ++ challengeResult).bits + + "decode" in { + val res = Codec.decode[ClientFinished](string) + val value = res.require.value + + value.challengeResult mustEqual challengeResult + value.pubKey mustEqual pubKey + } + + "encode" in { + val res = Codec.encode(ClientFinished(pubKey, challengeResult)) + + res.require mustEqual string + } + } + + "ServerFinished" should { + val challengeResult = hex"d64ffb8e 526311b4 af46bece" + val string = (hex"0114" ++ challengeResult).bits + + "decode" in { + val res = Codec.decode[ServerFinished](string) + val value = res.require.value + + value.challengeResult mustEqual challengeResult + } + + "encode" in { + val res = Codec.encode(ServerFinished(challengeResult)) + + res.require mustEqual string + } + } + } +}