Initial Commit

This commit is contained in:
Chord 2016-02-05 03:19:13 -05:00
commit d96fce6299
21 changed files with 1989 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
target
*.class
.idea/
.idea_modules/
tmp/
out/
*.iws
*.ipr
*.iml
*.swp
/*.csv

37
build.sbt Normal file
View 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"
)

View 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")
}
}

View 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)
}
}
}

View file

@ -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)
}

View 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"))
}
}

View 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)
}

View 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)
}

View 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)
}
}
}

View 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]
}

View 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
View file

@ -0,0 +1 @@
sbt.version = 0.13.8

3
project/plugins.sbt Normal file
View file

@ -0,0 +1,3 @@
logLevel := Level.Warn
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2")

View 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)
}
}

View 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)
}
}

View 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")
}
}

View 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
}
}

View 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)
}
}

View file

@ -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()
}

View 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
}
}
}

View 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
}
}
}
}