mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
Initial Commit
This commit is contained in:
commit
d96fce6299
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
target
|
||||
*.class
|
||||
.idea/
|
||||
.idea_modules/
|
||||
tmp/
|
||||
out/
|
||||
*.iws
|
||||
*.ipr
|
||||
*.iml
|
||||
*.swp
|
||||
/*.csv
|
||||
37
build.sbt
Normal file
37
build.sbt
Normal file
|
|
@ -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"
|
||||
)
|
||||
21
common/src/main/scala/psforever/IFinalizable.scala
Normal file
21
common/src/main/scala/psforever/IFinalizable.scala
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
228
common/src/main/scala/psforever/crypto/CryptoInterface.scala
Normal file
228
common/src/main/scala/psforever/crypto/CryptoInterface.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
20
common/src/main/scala/psforever/net/CryptoPacketOpcode.scala
Normal file
20
common/src/main/scala/psforever/net/CryptoPacketOpcode.scala
Normal file
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
45
common/src/main/scala/psforever/net/GamePacketOpcode.scala
Normal file
45
common/src/main/scala/psforever/net/GamePacketOpcode.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
386
common/src/main/scala/psforever/net/PSPacket.scala
Normal file
386
common/src/main/scala/psforever/net/PSPacket.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
377
common/src/main/scala/psforever/net/PacketCoding.scala
Normal file
377
common/src/main/scala/psforever/net/PacketCoding.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
common/src/main/scala/sna/Library.scala
Normal file
60
common/src/main/scala/sna/Library.scala
Normal file
|
|
@ -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]
|
||||
}
|
||||
68
common/src/test/scala/PacketCodingTest.scala
Normal file
68
common/src/test/scala/PacketCodingTest.scala
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
1
project/build.properties
Normal file
1
project/build.properties
Normal file
|
|
@ -0,0 +1 @@
|
|||
sbt.version = 0.13.8
|
||||
3
project/plugins.sbt
Normal file
3
project/plugins.sbt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
logLevel := Level.Warn
|
||||
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")
|
||||
11
pslogin/src/main/scala/LoginSession.scala
Normal file
11
pslogin/src/main/scala/LoginSession.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
250
pslogin/src/main/scala/LoginSessionActor.scala
Normal file
250
pslogin/src/main/scala/LoginSessionActor.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
22
pslogin/src/main/scala/PsLogin.scala
Normal file
22
pslogin/src/main/scala/PsLogin.scala
Normal file
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
38
pslogin/src/main/scala/SessionRouter.scala
Normal file
38
pslogin/src/main/scala/SessionRouter.scala
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
35
pslogin/src/main/scala/UdpListener.scala
Normal file
35
pslogin/src/main/scala/UdpListener.scala
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
135
pslogin/src/test/scala/CryptoInterfaceTest.scala
Normal file
135
pslogin/src/test/scala/CryptoInterfaceTest.scala
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
133
pslogin/src/test/scala/CryptoPackets.scala
Normal file
133
pslogin/src/test/scala/CryptoPackets.scala
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue