mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-19 18:14:44 +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