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

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