mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-03-08 06:30:26 +00:00
Initial Commit
This commit is contained in:
commit
d96fce6299
21 changed files with 1989 additions and 0 deletions
21
common/src/main/scala/psforever/IFinalizable.scala
Normal file
21
common/src/main/scala/psforever/IFinalizable.scala
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package psforever
|
||||
|
||||
class ObjectFinalizedException(msg : String) extends Exception(msg)
|
||||
|
||||
trait IFinalizable {
|
||||
var closed = false
|
||||
|
||||
def close = {
|
||||
closed = true
|
||||
}
|
||||
|
||||
def assertNotClosed = {
|
||||
if(closed)
|
||||
throw new ObjectFinalizedException(this.getClass.getCanonicalName + ": already finalized. Cannot interact with object")
|
||||
}
|
||||
|
||||
override def finalize() = {
|
||||
if(!closed)
|
||||
println(this.getClass.getCanonicalName + ": class not closed. memory leaked")
|
||||
}
|
||||
}
|
||||
228
common/src/main/scala/psforever/crypto/CryptoInterface.scala
Normal file
228
common/src/main/scala/psforever/crypto/CryptoInterface.scala
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
package psforever.crypto
|
||||
|
||||
import psforever.IFinalizable
|
||||
import sna.Library
|
||||
import com.sun.jna.Pointer
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
object CryptoInterface {
|
||||
final val libName = "pscrypto"
|
||||
// TODO: make this cross platform
|
||||
final val fullLibName = "lib" + libName + ".so"
|
||||
final val psLib = new Library(libName)
|
||||
final val RC5_BLOCK_SIZE = 8
|
||||
final val MD5_MAC_SIZE = 16
|
||||
|
||||
val functionsList = List(
|
||||
"RC5_Init",
|
||||
"RC5_Encrypt",
|
||||
"RC5_Decrypt",
|
||||
"DH_Start",
|
||||
"DH_Start_Generate",
|
||||
"DH_Agree",
|
||||
"MD5_MAC",
|
||||
"Free_DH",
|
||||
"Free_RC5"
|
||||
)
|
||||
|
||||
def initialize() : Unit = {
|
||||
functionsList foreach psLib.prefetch
|
||||
}
|
||||
|
||||
def MD5MAC(key : ByteVector, message : ByteVector, bytesWanted : Int) : ByteVector = {
|
||||
val out = Array.ofDim[Byte](bytesWanted)
|
||||
|
||||
// WARNING BUG: the function must be cast to something (even if void) otherwise it doesnt work
|
||||
val ret = psLib.MD5_MAC(key.toArray, key.length, message.toArray, message.length, out, out.length)[Boolean]
|
||||
|
||||
if(!ret)
|
||||
throw new Exception("MD5MAC failed to process")
|
||||
|
||||
ByteVector(out)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two MAC values are the same in constant time, preventing a timing attack for MAC forgery
|
||||
* @param mac1
|
||||
* @param mac2
|
||||
*/
|
||||
def verifyMAC(mac1 : ByteVector, mac2 : ByteVector) : Boolean = {
|
||||
var okay = true
|
||||
|
||||
if(mac1.length != mac2.length)
|
||||
return false
|
||||
|
||||
for(i <- 0 until mac1.length) {
|
||||
okay = okay && mac1{i} == mac2{i}
|
||||
}
|
||||
|
||||
okay
|
||||
}
|
||||
|
||||
class CryptoDHState extends IFinalizable {
|
||||
var started = false
|
||||
// these types MUST be Arrays of bytes for JNA to work
|
||||
val privateKey = Array.ofDim[Byte](16)
|
||||
val publicKey = Array.ofDim[Byte](16)
|
||||
val p = Array.ofDim[Byte](16)
|
||||
val g = Array.ofDim[Byte](16)
|
||||
var dhHandle = Pointer.NULL
|
||||
|
||||
def start(modulus : ByteVector, generator : ByteVector) : Unit = {
|
||||
assertNotClosed
|
||||
|
||||
if(started)
|
||||
throw new IllegalStateException("DH state has already been started")
|
||||
|
||||
dhHandle = psLib.DH_Start(modulus.toArray, generator.toArray, privateKey, publicKey)[Pointer]
|
||||
|
||||
if(dhHandle == Pointer.NULL)
|
||||
throw new Exception("DH initialization failed!")
|
||||
|
||||
modulus.copyToArray(p, 0)
|
||||
generator.copyToArray(g, 0)
|
||||
|
||||
started = true
|
||||
}
|
||||
|
||||
def start() : Unit = {
|
||||
assertNotClosed
|
||||
|
||||
if(started)
|
||||
throw new IllegalStateException("DH state has already been started")
|
||||
|
||||
dhHandle = psLib.DH_Start_Generate(privateKey, publicKey, p, g)[Pointer]
|
||||
|
||||
if(dhHandle == Pointer.NULL)
|
||||
throw new Exception("DH initialization failed!")
|
||||
|
||||
started = true
|
||||
}
|
||||
|
||||
def agree(otherPublicKey : ByteVector) = {
|
||||
if(!started)
|
||||
throw new IllegalStateException("DH state has not been started")
|
||||
|
||||
val agreedValue = Array.ofDim[Byte](16)
|
||||
val agreed = psLib.DH_Agree(dhHandle, agreedValue, privateKey, otherPublicKey.toArray)[Boolean]
|
||||
|
||||
if(!agreed)
|
||||
throw new Exception("Failed to DH agree")
|
||||
|
||||
ByteVector.view(agreedValue)
|
||||
}
|
||||
|
||||
private def checkAndReturnView(array : Array[Byte]) = {
|
||||
if(!started)
|
||||
throw new IllegalStateException("DH state has not been started")
|
||||
|
||||
ByteVector.view(array)
|
||||
}
|
||||
|
||||
def getPrivateKey = {
|
||||
checkAndReturnView(privateKey)
|
||||
}
|
||||
|
||||
def getPublicKey = {
|
||||
checkAndReturnView(publicKey)
|
||||
}
|
||||
|
||||
def getModulus = {
|
||||
checkAndReturnView(p)
|
||||
}
|
||||
|
||||
def getGenerator = {
|
||||
checkAndReturnView(g)
|
||||
}
|
||||
|
||||
override def close = {
|
||||
if(started) {
|
||||
psLib.Free_DH(dhHandle)[Unit]
|
||||
started = false
|
||||
}
|
||||
|
||||
super.close
|
||||
}
|
||||
}
|
||||
|
||||
class CryptoState(val decryptionKey : ByteVector,
|
||||
val encryptionKey : ByteVector) extends IFinalizable {
|
||||
// Note that the keys must be returned as primitive Arrays for JNA to work
|
||||
val encCryptoHandle = psLib.RC5_Init(encryptionKey.toArray, encryptionKey.length, true)[Pointer]
|
||||
val decCryptoHandle = psLib.RC5_Init(decryptionKey.toArray, decryptionKey.length, false)[Pointer]
|
||||
|
||||
if(encCryptoHandle == Pointer.NULL)
|
||||
throw new Exception("Encryption initialization failed!")
|
||||
|
||||
if(decCryptoHandle == Pointer.NULL)
|
||||
throw new Exception("Decryption initialization failed!")
|
||||
|
||||
def encrypt(plaintext : ByteVector) : ByteVector = {
|
||||
if(plaintext.length % RC5_BLOCK_SIZE != 0)
|
||||
throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary")
|
||||
|
||||
val ciphertext = Array.ofDim[Byte](plaintext.length)
|
||||
|
||||
val ret = psLib.RC5_Encrypt(encCryptoHandle, plaintext.toArray, plaintext.length, ciphertext)[Boolean]
|
||||
|
||||
if(!ret)
|
||||
throw new Exception("Failed to encrypt plaintext")
|
||||
|
||||
ByteVector.view(ciphertext)
|
||||
}
|
||||
|
||||
def decrypt(ciphertext : ByteVector) : ByteVector = {
|
||||
if(ciphertext.length % RC5_BLOCK_SIZE != 0)
|
||||
throw new IllegalArgumentException(s"input must be padded to the nearest $RC5_BLOCK_SIZE byte boundary")
|
||||
|
||||
val plaintext = Array.ofDim[Byte](ciphertext.length)
|
||||
|
||||
val ret = psLib.RC5_Decrypt(decCryptoHandle, ciphertext.toArray, ciphertext.length, plaintext)[Boolean]
|
||||
|
||||
if(!ret)
|
||||
throw new Exception("Failed to decrypt ciphertext")
|
||||
|
||||
ByteVector.view(plaintext)
|
||||
}
|
||||
|
||||
override def close = {
|
||||
psLib.Free_RC5(encCryptoHandle)[Unit]
|
||||
psLib.Free_RC5(decCryptoHandle)[Unit]
|
||||
super.close
|
||||
}
|
||||
}
|
||||
|
||||
class CryptoStateWithMAC(decryptionKey : ByteVector,
|
||||
encryptionKey : ByteVector,
|
||||
val decryptionMACKey : ByteVector,
|
||||
val encryptionMACKey : ByteVector) extends CryptoState(decryptionKey, encryptionKey) {
|
||||
/**
|
||||
* Performs a MAC operation over the message. Used when encrypting packets
|
||||
* @param message
|
||||
* @return ByteVector
|
||||
*/
|
||||
def macForEncrypt(message : ByteVector) : ByteVector = {
|
||||
MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a MAC operation over the message. Used when verifying decrypted packets
|
||||
* @param message
|
||||
* @return ByteVector
|
||||
*/
|
||||
def macForDecrypt(message : ByteVector) : ByteVector = {
|
||||
MD5MAC(decryptionMACKey, message, MD5_MAC_SIZE)
|
||||
}
|
||||
|
||||
/**
|
||||
* MACs the plaintext message, encrypts it, and then returns the encrypted message with the
|
||||
* MAC appended to the end.
|
||||
* @param message Arbitrary set of bytes
|
||||
* @return ByteVector
|
||||
*/
|
||||
def macAndEncrypt(message : ByteVector) : ByteVector = {
|
||||
encrypt(message) ++ MD5MAC(encryptionMACKey, message, MD5_MAC_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package psforever.net
|
||||
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Err, DecodeResult, Attempt, Codec}
|
||||
import scodec.codecs._
|
||||
|
||||
object ControlPacketOpcode extends Enumeration {
|
||||
type Type = Value
|
||||
val
|
||||
|
||||
// Opcodes should have a marker every 10
|
||||
// OPCODE 0
|
||||
HandleGamePacket, // a whoopsi case: not actually a control packet, but a game packet
|
||||
ClientStart, // first packet ever sent during client connection
|
||||
ServerStart, // second packet sent in response to ClientStart
|
||||
MultiPacket, // used to send multiple packets with one UDP message (subpackets limited to <= 255)
|
||||
Unknown4,
|
||||
Unknown5,
|
||||
Unknown6,
|
||||
Unknown7,
|
||||
Unknown8,
|
||||
SlottedMetaPacket0,
|
||||
|
||||
// OPCODE 10
|
||||
SlottedMetaPacket1,
|
||||
SlottedMetaPacket2,
|
||||
SlottedMetaPacket3,
|
||||
SlottedMetaPacket4,
|
||||
SlottedMetaPacket5,
|
||||
SlottedMetaPacket6,
|
||||
SlottedMetaPacket7,
|
||||
RelatedA0,
|
||||
RelatedA1,
|
||||
RelatedA2,
|
||||
|
||||
// OPCODE 20
|
||||
RelatedA3,
|
||||
RelatedB0,
|
||||
RelatedB1,
|
||||
RelatedB2,
|
||||
RelatedB3,
|
||||
AggregatePacket, // same as MultiPacket, but with the ability to send extended length packets
|
||||
Unknown26,
|
||||
Unknown27,
|
||||
Unknown28,
|
||||
ConnectionClose,
|
||||
|
||||
// OPCODE 30
|
||||
Unknown30
|
||||
= Value
|
||||
|
||||
def getPacketDecoder(opcode : ControlPacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideControlPacket]] = opcode match {
|
||||
case HandleGamePacket => psforever.net.HandleGamePacket.decode
|
||||
case ServerStart => psforever.net.ServerStart.decode
|
||||
case ClientStart => psforever.net.ClientStart.decode
|
||||
case MultiPacket => psforever.net.MultiPacket.decode
|
||||
case SlottedMetaPacket0 => psforever.net.SlottedMetaPacket.decode
|
||||
case ConnectionClose => psforever.net.ConnectionClose.decode
|
||||
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for control packet ${opcode}"))
|
||||
}
|
||||
|
||||
val storageType = uint8L
|
||||
|
||||
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
|
||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
||||
|
||||
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
|
||||
}
|
||||
20
common/src/main/scala/psforever/net/CryptoPacketOpcode.scala
Normal file
20
common/src/main/scala/psforever/net/CryptoPacketOpcode.scala
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package psforever.net
|
||||
|
||||
import scodec.bits.BitVector
|
||||
import scodec.{Err, DecodeResult, Attempt}
|
||||
|
||||
// this isnt actually used as an opcode (i.e not serialized)
|
||||
object CryptoPacketOpcode extends Enumeration {
|
||||
type Type = Value
|
||||
val Ignore, ClientChallengeXchg, ServerChallengeXchg,
|
||||
ClientFinished, ServerFinished = Value
|
||||
|
||||
def getPacketDecoder(opcode : CryptoPacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideCryptoPacket]] = opcode match {
|
||||
case ClientChallengeXchg => psforever.net.ClientChallengeXchg.decode
|
||||
case ServerChallengeXchg => psforever.net.ServerChallengeXchg.decode
|
||||
case ServerFinished => psforever.net.ServerFinished.decode
|
||||
case ClientFinished => psforever.net.ClientFinished.decode
|
||||
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for crypto packet ${opcode}")
|
||||
.pushContext("get_marshaller"))
|
||||
}
|
||||
}
|
||||
45
common/src/main/scala/psforever/net/GamePacketOpcode.scala
Normal file
45
common/src/main/scala/psforever/net/GamePacketOpcode.scala
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package psforever.net
|
||||
|
||||
import scodec.{Err, DecodeResult, Attempt, Codec}
|
||||
import scodec.bits.BitVector
|
||||
import scodec.codecs._
|
||||
|
||||
object GamePacketOpcode extends Enumeration {
|
||||
type Type = Value
|
||||
val
|
||||
|
||||
// Opcodes should have a marker every 10
|
||||
// OPCODE 0
|
||||
Unknown0,
|
||||
LoginMessage,
|
||||
LoginRespMessage,
|
||||
Unknown3,
|
||||
ConnectToWorldMessage,
|
||||
Unknown5,
|
||||
UnknownMessage6,
|
||||
UnknownMessage7,
|
||||
PlayerStateMessage,
|
||||
UnknownMessage9,
|
||||
|
||||
// OPCODE 10
|
||||
HitHint,
|
||||
DamageMessage,
|
||||
DestroyMessage,
|
||||
ReloadMessage,
|
||||
MountVehicleMsg,
|
||||
DismountVehicleMsg
|
||||
= Value
|
||||
|
||||
def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match {
|
||||
case LoginMessage => psforever.net.LoginMessage.decode
|
||||
case LoginRespMessage => psforever.net.LoginRespMessage.decode
|
||||
case default => (a : BitVector) => Attempt.failure(Err(s"Could not find a marshaller for game packet ${opcode}"))
|
||||
}
|
||||
|
||||
val storageType = uint8L
|
||||
|
||||
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
|
||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
||||
|
||||
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
|
||||
}
|
||||
386
common/src/main/scala/psforever/net/PSPacket.scala
Normal file
386
common/src/main/scala/psforever/net/PSPacket.scala
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
package psforever.net
|
||||
|
||||
import scodec.{DecodeResult, Err, Codec, Attempt}
|
||||
import scodec.bits._
|
||||
import scodec.codecs._
|
||||
import scodec._
|
||||
import shapeless._
|
||||
import shapeless.ops.hlist.Prepend
|
||||
|
||||
// Base packets
|
||||
sealed trait PlanetSidePacket extends Serializable {
|
||||
def encode : Attempt[BitVector]
|
||||
def opcode : Enumeration#Value
|
||||
}
|
||||
|
||||
// Used by companion objects to create encoders and decoders
|
||||
sealed trait Marshallable[T] {
|
||||
implicit val codec : Codec[T]
|
||||
def encode(a : T) : Attempt[BitVector] = codec.encode(a)
|
||||
// assert that when decoding a marshallable type, that no bits are left over
|
||||
def decode(a : BitVector) : Attempt[DecodeResult[T]] = codec.complete.decode(a)
|
||||
}
|
||||
|
||||
sealed trait PlanetSideGamePacket extends PlanetSidePacket {
|
||||
def opcode : GamePacketOpcode.Type
|
||||
}
|
||||
|
||||
sealed trait PlanetSideControlPacket extends PlanetSidePacket {
|
||||
def opcode : ControlPacketOpcode.Type
|
||||
}
|
||||
|
||||
sealed trait PlanetSideCryptoPacket extends PlanetSidePacket {
|
||||
def opcode : CryptoPacketOpcode.Type
|
||||
}
|
||||
|
||||
// Crypto Packets
|
||||
final case class ClientChallengeXchg(time : Long, challenge : ByteVector, p : ByteVector, g : ByteVector)
|
||||
extends PlanetSideCryptoPacket {
|
||||
def opcode = CryptoPacketOpcode.ClientChallengeXchg
|
||||
def encode = ClientChallengeXchg.encode(this)
|
||||
}
|
||||
|
||||
object ClientChallengeXchg extends Marshallable[ClientChallengeXchg] {
|
||||
implicit val codec: Codec[ClientChallengeXchg] = (
|
||||
("unknown" | constant(1)) ::
|
||||
("unknown" | constant(1)) ::
|
||||
("client_time" | uint32L) ::
|
||||
("challenge" | bytes(12)) ::
|
||||
("end_chal?" | constant(0)) ::
|
||||
("objects?" | constant(1)) ::
|
||||
("object_type?" | constant(hex"0002".bits)) ::
|
||||
("unknown" | constant(hex"ff240000".bits)) ::
|
||||
("P_len" | constant(hex"1000".bits)) ::
|
||||
("P" | bytes(16)) ::
|
||||
("G_len" | constant(hex"1000".bits)) ::
|
||||
("G" | bytes(16)) ::
|
||||
("end?" | constant(0)) ::
|
||||
("end?" | constant(0)) ::
|
||||
("objects?" | constant(1)) ::
|
||||
("unknown" | constant(hex"03070000".bits)) ::
|
||||
("end?" | constant(0))
|
||||
).as[ClientChallengeXchg]
|
||||
}
|
||||
|
||||
final case class ServerChallengeXchg(time : Long, challenge : ByteVector, pubKey : ByteVector)
|
||||
extends PlanetSideCryptoPacket {
|
||||
type Packet = ServerChallengeXchg
|
||||
def opcode = CryptoPacketOpcode.ServerChallengeXchg
|
||||
def encode = ServerChallengeXchg.encode(this)
|
||||
}
|
||||
|
||||
object ServerChallengeXchg extends Marshallable[ServerChallengeXchg] {
|
||||
def getCompleteChallenge(time : Long, rest : ByteVector): ByteVector =
|
||||
uint32L.encode(time).require.toByteVector ++ rest
|
||||
|
||||
implicit val codec: Codec[ServerChallengeXchg] = (
|
||||
("unknown" | constant(2)) ::
|
||||
("unknown" | constant(1)) ::
|
||||
("server_time" | uint32L) ::
|
||||
("challenge" | bytes(0xC)) ::
|
||||
("end?" | constant(0)) ::
|
||||
("objects" | constant(1)) ::
|
||||
("unknown" | constant(hex"03070000000c00".bits)) ::
|
||||
("pub_key_len" | constant(hex"1000")) ::
|
||||
("pub_key" | bytes(16)) ::
|
||||
("unknown" | constant(0x0e))
|
||||
).as[ServerChallengeXchg]
|
||||
}
|
||||
|
||||
final case class ClientFinished(pubKey : ByteVector, challengeResult: ByteVector)
|
||||
extends PlanetSideCryptoPacket {
|
||||
type Packet = ClientFinished
|
||||
def opcode = CryptoPacketOpcode.ClientFinished
|
||||
def encode = ClientFinished.encode(this)
|
||||
}
|
||||
|
||||
object ClientFinished extends Marshallable[ClientFinished] {
|
||||
implicit val codec : Codec[ClientFinished] = (
|
||||
("obj_type?" | constant(hex"10".bits)) ::
|
||||
("pub_key_len" | constant(hex"1000")) ::
|
||||
("pub_key" | bytes(16)) ::
|
||||
("unknown" | constant(hex"0114".bits)) ::
|
||||
("challenge_result" | bytes(0xc))
|
||||
).as[ClientFinished]
|
||||
}
|
||||
|
||||
final case class ServerFinished(challengeResult : ByteVector)
|
||||
extends PlanetSideCryptoPacket {
|
||||
type Packet = ServerFinished
|
||||
def opcode = CryptoPacketOpcode.ServerFinished
|
||||
def encode = ServerFinished.encode(this)
|
||||
}
|
||||
|
||||
object ServerFinished extends Marshallable[ServerFinished] {
|
||||
implicit val codec : Codec[ServerFinished] = (
|
||||
("unknown" | constant(hex"0114".bits)) ::
|
||||
("challenge_result" | bytes(0xc))
|
||||
).as[ServerFinished]
|
||||
}
|
||||
|
||||
// Game Packets
|
||||
final case class LoginMessage(majorVersion : Long,
|
||||
minorVersion : Long,
|
||||
buildDate : String,
|
||||
username : String,
|
||||
password : Option[String],
|
||||
token : Option[String],
|
||||
revision : Long) extends PlanetSideGamePacket {
|
||||
require(majorVersion >= 0)
|
||||
require(minorVersion >= 0)
|
||||
require(revision >= 0)
|
||||
require(password.isDefined ^ token.isDefined, "Either 'username' or 'token' must be set, but not both")
|
||||
|
||||
def opcode = GamePacketOpcode.LoginMessage
|
||||
def encode = LoginMessage.encode(this)
|
||||
}
|
||||
|
||||
object LoginMessage extends Marshallable[LoginMessage] {
|
||||
private def username = PacketHelpers.encodedStringAligned(7)
|
||||
private def password = PacketHelpers.encodedString
|
||||
private def tokenPath = fixedSizeBytes(32, ascii) :: username
|
||||
private def passwordPath = username :: password
|
||||
|
||||
type Struct = String :: Option[String] :: Option[String] :: HNil
|
||||
|
||||
implicit val credentialChoice : Codec[Struct] = {
|
||||
type InStruct = Either[String :: String :: HNil, String :: String :: HNil]
|
||||
|
||||
def from(a : InStruct) : Struct = a match {
|
||||
case Left(username :: password :: HNil) => username :: Some(password) :: None :: HNil
|
||||
case Right(token :: username :: HNil) => username :: None :: Some(token) :: HNil
|
||||
}
|
||||
|
||||
// serialization can fail if the user did not specify a token or password (or both)
|
||||
def to(a : Struct) : InStruct = a match {
|
||||
case username :: Some(password) :: None :: HNil => Left(username :: password :: HNil)
|
||||
case username :: None :: Some(token) :: HNil => Right(token :: username :: HNil)
|
||||
}
|
||||
|
||||
either(bool, passwordPath, tokenPath).xmap[Struct](from, to)
|
||||
}
|
||||
|
||||
implicit val codec : Codec[LoginMessage] = (
|
||||
("major_version" | uint32L) ::
|
||||
("minor_version" | uint32L) ::
|
||||
("build_date" | PacketHelpers.encodedString) ::
|
||||
(
|
||||
// The :+ operator (and the parens) are required because we are adding an HList to an HList,
|
||||
// not merely a value (like bool). Weird shit, but hey this works.
|
||||
("credential_choice" | credentialChoice) :+
|
||||
("revision" | uint32L)
|
||||
)
|
||||
).as[LoginMessage]
|
||||
}
|
||||
|
||||
final case class LoginRespMessage(token : String, // printable ascii for 16
|
||||
unknown : ByteVector, // hex"00000000 18FABE0C 00000000 00000000"
|
||||
error : Long, // 0
|
||||
stationError : Long, // 1
|
||||
subscriptionStatus : Long, // 2 or 5
|
||||
someToken : Long, // 685276011
|
||||
username : String, // the user
|
||||
unk5 : Long, // 0 and unset bool
|
||||
someBit : Boolean) extends PlanetSideGamePacket {
|
||||
def opcode = GamePacketOpcode.LoginRespMessage
|
||||
def encode = LoginRespMessage.encode(this)
|
||||
}
|
||||
|
||||
object LoginRespMessage extends Marshallable[LoginRespMessage] {
|
||||
implicit val codec : Codec[LoginRespMessage] = (
|
||||
("token" | fixedSizeBytes(16, ascii)) ::
|
||||
("unknown" | bytes(16)) ::
|
||||
("error" | uint32L) ::
|
||||
("station_error" | uint32L) ::
|
||||
("subscription_status" | uint32L) ::
|
||||
("unknown" | uint32L) ::
|
||||
("username" | PacketHelpers.encodedString) ::
|
||||
("unknown" | uint32L) ::
|
||||
("unknown" | byteAligned(bool))
|
||||
).as[LoginRespMessage]
|
||||
}
|
||||
|
||||
final case class ConnectToWorldMessage(world : String)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = ConnectToWorldMessage
|
||||
def opcode = GamePacketOpcode.ConnectToWorldMessage
|
||||
def encode = ConnectToWorldMessage.encode(this)
|
||||
}
|
||||
|
||||
object ConnectToWorldMessage extends Marshallable[ConnectToWorldMessage] {
|
||||
implicit val codec : Codec[ConnectToWorldMessage] = ascii.as[ConnectToWorldMessage]
|
||||
}
|
||||
|
||||
// Control Packets
|
||||
final case class HandleGamePacket(packet : ByteVector)
|
||||
extends PlanetSideControlPacket {
|
||||
def opcode = ControlPacketOpcode.HandleGamePacket
|
||||
def encode = throw new Exception("This packet type should never be encoded")
|
||||
}
|
||||
|
||||
object HandleGamePacket extends Marshallable[HandleGamePacket] {
|
||||
implicit val codec : Codec[HandleGamePacket] = bytes.as[HandleGamePacket].decodeOnly
|
||||
}
|
||||
|
||||
final case class ClientStart(clientNonce : Long)
|
||||
extends PlanetSideControlPacket {
|
||||
type Packet = ClientStart
|
||||
def opcode = ControlPacketOpcode.ClientStart
|
||||
def encode = ClientStart.encode(this)
|
||||
}
|
||||
|
||||
object ClientStart extends Marshallable[ClientStart] {
|
||||
implicit val codec : Codec[ClientStart] = (
|
||||
("unknown" | constant(hex"00000002".bits)) ::
|
||||
("client_nonce" | uint32L) ::
|
||||
("unknown" | constant(hex"000001f0".bits))
|
||||
).as[ClientStart]
|
||||
}
|
||||
|
||||
final case class ServerStart(clientNonce : Long, serverNonce : Long)
|
||||
extends PlanetSideControlPacket {
|
||||
type Packet = ServerStart
|
||||
def opcode = ControlPacketOpcode.ServerStart
|
||||
def encode = ServerStart.encode(this)
|
||||
}
|
||||
|
||||
object ServerStart extends Marshallable[ServerStart] {
|
||||
implicit val codec : Codec[ServerStart] = (
|
||||
("client_nonce" | uint32L) ::
|
||||
("server_nonce" | uint32L) ::
|
||||
("unknown" | constant(hex"000000000001d300000002".bits))
|
||||
).as[ServerStart]
|
||||
}
|
||||
|
||||
final case class MultiPacket(packets : Vector[ByteVector])
|
||||
extends PlanetSideControlPacket {
|
||||
type Packet = MultiPacket
|
||||
def opcode = ControlPacketOpcode.MultiPacket
|
||||
def encode = MultiPacket.encode(this)
|
||||
}
|
||||
|
||||
object MultiPacket extends Marshallable[MultiPacket] {
|
||||
implicit val codec : Codec[MultiPacket] = ("packets" | vector(variableSizeBytes(uint8L, bytes))).as[MultiPacket]
|
||||
}
|
||||
|
||||
final case class SlottedMetaPacket(/*slot : Int,*/ packet : ByteVector)
|
||||
extends PlanetSideControlPacket {
|
||||
type Packet = SlottedMetaPacket
|
||||
|
||||
//assert(slot >= 0 && slot <= 7, "Slot number is out of range")
|
||||
|
||||
def opcode = {
|
||||
val base = ControlPacketOpcode.SlottedMetaPacket0.id
|
||||
ControlPacketOpcode(base/* + slot*/)
|
||||
}
|
||||
|
||||
def encode = SlottedMetaPacket.encode(this)
|
||||
}
|
||||
|
||||
object SlottedMetaPacket extends Marshallable[SlottedMetaPacket] {
|
||||
implicit val codec : Codec[SlottedMetaPacket] = (
|
||||
("unknown" | constant(0)) ::
|
||||
("unknown" | constant(0)) ::
|
||||
("rest" | bytes)
|
||||
).as[SlottedMetaPacket]
|
||||
}
|
||||
|
||||
final case class ConnectionClose()
|
||||
extends PlanetSideControlPacket {
|
||||
type Packet = ConnectionClose
|
||||
def opcode = ControlPacketOpcode.ConnectionClose
|
||||
def encode = ConnectionClose.encode(this)
|
||||
}
|
||||
|
||||
object ConnectionClose extends Marshallable[ConnectionClose] {
|
||||
implicit val codec: Codec[ConnectionClose] = PacketHelpers.emptyCodec(ConnectionClose())
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
|
||||
// Packet typing
|
||||
final case class PlanetSidePacketFlags(packetType : PacketType.Value, secured : Boolean)
|
||||
|
||||
// Enumeration starts at 1
|
||||
object PacketType extends Enumeration(1) {
|
||||
type Type = Value
|
||||
val ResetSequence, Unknown2, Crypto, Normal = Value
|
||||
|
||||
val storageType = uint4L
|
||||
|
||||
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
|
||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
||||
|
||||
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, storageType)
|
||||
}
|
||||
|
||||
object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
|
||||
implicit val codec : Codec[PlanetSidePacketFlags] = (
|
||||
("packet_type" | PacketType.codec) ::
|
||||
("unused" | constant(bin"0")) ::
|
||||
("secured" | bool) ::
|
||||
("advanced" | constant(bin"1")) :: // we only support "advanced packets"
|
||||
("length_specified" | constant(bin"0")) // we DO NOT support this field
|
||||
).as[PlanetSidePacketFlags]
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
// TODO: figure out why I can't insert codecs without using a new case class
|
||||
// Notes: https://mpilquist.github.io/blog/2013/06/09/scodec-part-3/
|
||||
// https://stackoverflow.com/questions/29585649/using-nested-case-classes-with-scodec
|
||||
// https://gist.github.com/travisbrown/3945529
|
||||
|
||||
object PacketHelpers {
|
||||
def emptyCodec[T](instance : T) = {
|
||||
def to(pkt: T) = HNil
|
||||
def from(a: HNil) = instance
|
||||
Codec[HNil].xmap[T](from, to)
|
||||
}
|
||||
|
||||
def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = {
|
||||
type Struct = Int :: HNil
|
||||
val struct: Codec[Struct] = storageCodec.hlist
|
||||
|
||||
// Assure that the enum will always be able to fit in a N-bit int
|
||||
assert(enum.maxId <= Math.pow(storageCodec.sizeBound.exact.get, 2),
|
||||
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
|
||||
|
||||
def to(pkt: E#Value): Struct = {
|
||||
pkt.id :: HNil
|
||||
}
|
||||
|
||||
def from(struct: Struct): Attempt[E#Value] = struct match {
|
||||
case enumVal :: HNil =>
|
||||
// verify that this int can match the enum
|
||||
val first = enum.values.firstKey.id
|
||||
val last = enum.maxId-1
|
||||
|
||||
if(enumVal >= first && enumVal <= last)
|
||||
Attempt.successful(enum(enumVal))
|
||||
else
|
||||
Attempt.failure(Err(s"Expected ${enum} with ID between [${first}, ${last}], but got '${enumVal}'"))
|
||||
}
|
||||
|
||||
struct.narrow[E#Value](from, to)
|
||||
}
|
||||
|
||||
// when the first bit of the byte is set, the size can be between [0, 127].
|
||||
// otherwise, it is between [128, 32767] and two bytes are used for encoding
|
||||
// The magic in this is next level
|
||||
private def encodedStringSize : Codec[Int] = either(bool, uint(15), uint(7)).
|
||||
xmap[Int](
|
||||
(a : Either[Int, Int]) => a.fold[Int](a => a, a => a),
|
||||
(a : Int) => if(a > 0x7f) Left(a) else Right(a)
|
||||
)
|
||||
|
||||
private def encodedStringSizeWithPad(pad : Int) : Codec[Int] = either(bool, uint(15), uint(7)).
|
||||
xmap[Int](
|
||||
(a : Either[Int, Int]) => a.fold[Int](a => a, a => a),
|
||||
(a : Int) => if(a > 0x7f) Left(a) else Right(a)
|
||||
) <~ ignore(pad)
|
||||
|
||||
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
|
||||
def encodedStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment), ascii)
|
||||
}
|
||||
377
common/src/main/scala/psforever/net/PacketCoding.scala
Normal file
377
common/src/main/scala/psforever/net/PacketCoding.scala
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
package psforever.net
|
||||
|
||||
import psforever.crypto.CryptoInterface
|
||||
import psforever.crypto.CryptoInterface._
|
||||
import scodec.Attempt.{Successful, Failure}
|
||||
import scodec.bits._
|
||||
import scodec.{DecodeResult, Err, Attempt, Codec}
|
||||
import scodec.codecs.{uint16L, uint8L, uint4L, bytes}
|
||||
|
||||
// Packet containers
|
||||
sealed trait PlanetSidePacketContainer
|
||||
|
||||
// A sequence, encrypted opcode, encrypted payload, and MD5MAC plus padding
|
||||
final case class EncryptedPacket(sequenceNumber : Int,
|
||||
payload : ByteVector) extends PlanetSidePacketContainer
|
||||
|
||||
// A sequence, and payload. Crypto packets have no discernible opcodes
|
||||
final case class CryptoPacket(sequenceNumber : Int,
|
||||
packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer
|
||||
|
||||
final case class GamePacket(opcode : GamePacketOpcode.Value,
|
||||
sequenceNumber : Int,
|
||||
packet : PlanetSideGamePacket) extends PlanetSidePacketContainer
|
||||
|
||||
// Just an opcode + payload (does not expect a response)
|
||||
final case class ControlPacket(opcode : ControlPacketOpcode.Value,
|
||||
packet : PlanetSideControlPacket) extends PlanetSidePacketContainer
|
||||
|
||||
object PacketCoding {
|
||||
final val PLANETSIDE_MIN_PACKET_SIZE = 2
|
||||
|
||||
def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = {
|
||||
UnmarshalPacket(msg, CryptoPacketOpcode.Ignore)
|
||||
}
|
||||
|
||||
def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = {
|
||||
// check for a minimum length
|
||||
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
||||
return Attempt.failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes"))
|
||||
|
||||
val firstByte = msg{0}
|
||||
|
||||
firstByte match {
|
||||
// drop the first byte as control packets dont need it
|
||||
case 0x00 => unmarshalControlPacket(msg.drop(1))
|
||||
case _ => unmarshalFlaggedPacket(msg, cryptoState) // returns either EncryptedPacket or CryptoPacket
|
||||
}
|
||||
}
|
||||
|
||||
def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = {
|
||||
// check for a minimum length
|
||||
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
|
||||
return Attempt.failure(Err(s"Packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes"))
|
||||
|
||||
val firstByte = msg{0}
|
||||
|
||||
firstByte match {
|
||||
// drop the first byte as control packets dont need it
|
||||
case 0x00 => DecodeControlPacket(msg.drop(1))
|
||||
case _ => DecodeGamePacket(msg)
|
||||
}
|
||||
}
|
||||
|
||||
def MarshalPacket(packet : PlanetSidePacketContainer) : Attempt[BitVector] = {
|
||||
var flagsEncoded : BitVector = BitVector.empty
|
||||
var seqEncoded : BitVector = BitVector.empty
|
||||
var paddingEncoded : BitVector = BitVector.empty
|
||||
var opcodeEncoded : BitVector = BitVector.empty
|
||||
var payloadEncoded : BitVector = BitVector.empty
|
||||
|
||||
packet match {
|
||||
case GamePacket(opcode, seq, payload) =>
|
||||
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = false)
|
||||
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
||||
|
||||
GamePacketOpcode.codec.encode(opcode) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => opcodeEncoded = p
|
||||
}
|
||||
|
||||
uint16L.encode(seq) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => seqEncoded = p
|
||||
}
|
||||
|
||||
encodePacket(payload) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => payloadEncoded = p
|
||||
}
|
||||
case ControlPacket(opcode, payload) =>
|
||||
flagsEncoded = hex"00".bits
|
||||
|
||||
ControlPacketOpcode.codec.encode(opcode) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => opcodeEncoded = p
|
||||
}
|
||||
|
||||
encodePacket(payload) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => payloadEncoded = p
|
||||
}
|
||||
case CryptoPacket(seq, payload) =>
|
||||
val flags = PlanetSidePacketFlags(PacketType.Crypto, secured = false)
|
||||
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
||||
|
||||
uint16L.encode(seq) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext))
|
||||
case Successful(p) => seqEncoded = p
|
||||
}
|
||||
|
||||
encodePacket(payload) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $payload: " + e.messageWithContext))
|
||||
case Successful(p) => payloadEncoded = p
|
||||
}
|
||||
case EncryptedPacket(seq, payload) =>
|
||||
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = true)
|
||||
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
|
||||
|
||||
// encrypted packets need to be aligned to 4 bytes before encryption/decryption
|
||||
// first byte are flags, second and third the sequence, and fourth is the pad
|
||||
paddingEncoded = hex"00".bits
|
||||
|
||||
uint16L.encode(seq) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $payload: " + e.messageWithContext))
|
||||
case Successful(p) => seqEncoded = p
|
||||
}
|
||||
|
||||
payloadEncoded = payload.bits
|
||||
}
|
||||
|
||||
val finalPacket = flagsEncoded ++ seqEncoded ++ paddingEncoded ++ opcodeEncoded ++ payloadEncoded
|
||||
Attempt.successful(finalPacket)
|
||||
}
|
||||
|
||||
def CreateControlPacket(packet : PlanetSideControlPacket) = ControlPacket(packet.opcode, packet)
|
||||
def CreateCryptoPacket(sequence : Int, packet : PlanetSideCryptoPacket) = CryptoPacket(sequence, packet)
|
||||
def CreateGamePacket(sequence : Int, packet : PlanetSideGamePacket) = GamePacket(packet.opcode, sequence, packet)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private def encodePacket(packet : PlanetSidePacket) : Attempt[BitVector] = packet.encode
|
||||
|
||||
private def unmarshalFlaggedPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = {
|
||||
val decodedFlags = Codec.decode[PlanetSidePacketFlags](BitVector(msg))
|
||||
|
||||
decodedFlags match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err("Failed to parse packet flags: " + e.message))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
val flags = decodedFlags.require.value
|
||||
val rest = decodedFlags.require.remainder
|
||||
val packetType = flags.packetType
|
||||
|
||||
// perform a quick reject of weird packet types
|
||||
packetType match {
|
||||
case PacketType.Crypto => ;
|
||||
case PacketType.Normal => ;
|
||||
case default =>
|
||||
return Attempt.failure(Err("Unsupported packet type: " + flags.packetType.toString))
|
||||
}
|
||||
|
||||
// we only support normal packets if they are encrypted
|
||||
if(packetType == PacketType.Normal && !flags.secured)
|
||||
return Attempt.failure(Err("Unsupported packet type: normal packets must be encryped"))
|
||||
|
||||
// we only support crypto packets if they are not encrypted
|
||||
if(packetType == PacketType.Crypto && flags.secured)
|
||||
return Attempt.failure(Err("Unsupported packet type: crypto packets must be unencrypted"))
|
||||
|
||||
// all packets have a two byte sequence ID
|
||||
// TODO: make this a codec for reuse
|
||||
val decodedSeq = uint16L.decode(rest)
|
||||
|
||||
decodedSeq match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err("Failed to parse packet sequence number: " + e.message))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
val sequence = decodedSeq.require.value
|
||||
var payload = decodedSeq.require.remainder.toByteVector
|
||||
|
||||
// encrypted packets must be 4-byte aligned
|
||||
if(flags.secured) {
|
||||
payload = payload.drop(1)
|
||||
}
|
||||
|
||||
packetType match {
|
||||
case PacketType.Crypto =>
|
||||
unmarshalCryptoPacket(cryptoState, sequence, payload)
|
||||
case PacketType.Normal =>
|
||||
unmarshalEncryptedPacket(sequence, payload)
|
||||
}
|
||||
}
|
||||
|
||||
private def unmarshalControlPacket(msg : ByteVector) : Attempt[ControlPacket] = {
|
||||
val packet = DecodeControlPacket(msg)
|
||||
|
||||
packet match {
|
||||
case f @ Failure(e) => f
|
||||
case Successful(p) =>
|
||||
Attempt.successful(CreateControlPacket(p))
|
||||
}
|
||||
}
|
||||
|
||||
def DecodeControlPacket(msg : ByteVector) : Attempt[PlanetSideControlPacket] = {
|
||||
val opcode = ControlPacketOpcode.codec.decode(msg.bits)
|
||||
|
||||
opcode match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err("Failed to decode control packet's opcode: " + e.message))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
val packet = ControlPacketOpcode.getPacketDecoder(opcode.require.value)(opcode.require.remainder)
|
||||
|
||||
packet match {
|
||||
case Failure(e) =>
|
||||
Attempt.failure(Err(f"Failed to parse control packet 0x${opcode.require.value.id}%02x: " + e.messageWithContext))
|
||||
case Successful(p) => Attempt.successful(p.value)
|
||||
}
|
||||
}
|
||||
|
||||
private def unmarshalGamePacket(sequence : Int, msg : ByteVector) : Attempt[GamePacket] = {
|
||||
val packet = DecodeGamePacket(msg)
|
||||
|
||||
packet match {
|
||||
case f @ Failure(e) => f
|
||||
case Successful(p) =>
|
||||
Attempt.successful(CreateGamePacket(sequence, p))
|
||||
}
|
||||
}
|
||||
|
||||
def DecodeGamePacket(msg : ByteVector) : Attempt[PlanetSideGamePacket] = {
|
||||
val opcode = GamePacketOpcode.codec.decode(msg.bits)
|
||||
|
||||
opcode match {
|
||||
case Failure(e) =>
|
||||
return Attempt.failure(Err("Failed to decode game packet's opcode: " + e.message))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
val packet = GamePacketOpcode.getPacketDecoder(opcode.require.value)(opcode.require.remainder)
|
||||
|
||||
packet match {
|
||||
case Failure(e) =>
|
||||
Attempt.failure(Err(f"Failed to parse game packet 0x${opcode.require.value.id}%02x: " + e.messageWithContext))
|
||||
case Successful(p) => Attempt.successful(p.value)
|
||||
}
|
||||
}
|
||||
|
||||
private def unmarshalCryptoPacket(state : CryptoPacketOpcode.Type, sequence : Int, payload : ByteVector) : Attempt[CryptoPacket] = {
|
||||
val packet = CryptoPacketOpcode.getPacketDecoder(state)(payload.bits)
|
||||
|
||||
packet match {
|
||||
case Successful(a) =>
|
||||
Attempt.successful(CryptoPacket(sequence, a.value))
|
||||
case Failure(e) =>
|
||||
Attempt.failure(e.pushContext("unmarshal_crypto_packet"))
|
||||
}
|
||||
}
|
||||
|
||||
private def unmarshalEncryptedPacket(sequence : Int, payload : ByteVector) : Attempt[EncryptedPacket] = {
|
||||
Attempt.successful(EncryptedPacket(sequence, payload))
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Packet Crypto
|
||||
///////////////////////////////////////////////////////////
|
||||
|
||||
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = {
|
||||
// TODO: this is bad. rework
|
||||
var sequenceNumber = 0
|
||||
|
||||
val rawPacket : BitVector = packet match {
|
||||
case GamePacket(opcode, seq, payload) =>
|
||||
val opcodeEncoded = GamePacketOpcode.codec.encode(opcode)
|
||||
sequenceNumber = seq
|
||||
|
||||
opcodeEncoded match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.message))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
encodePacket(payload) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => opcodeEncoded.require ++ p
|
||||
}
|
||||
case ControlPacket(opcode, payload) =>
|
||||
val opcodeEncoded = ControlPacketOpcode.codec.encode(opcode)
|
||||
|
||||
opcodeEncoded match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in packet $opcode: " + e.messageWithContext))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
encodePacket(payload) match {
|
||||
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal packet $opcode: " + e.messageWithContext))
|
||||
case Successful(p) => hex"00".bits ++ opcodeEncoded.require ++ p
|
||||
}
|
||||
case default => throw new IllegalArgumentException("Unsupported packet container type")
|
||||
}
|
||||
|
||||
val packetMac = crypto.macForEncrypt(rawPacket.toByteVector)
|
||||
|
||||
// opcode, payload, and MAC
|
||||
val packetNoPadding = rawPacket.toByteVector ++ packetMac
|
||||
|
||||
val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE
|
||||
|
||||
// minus 1 because of the actual byte telling of the padding, which always has to be there
|
||||
val paddingNeeded = CryptoInterface.RC5_BLOCK_SIZE - remainder - 1
|
||||
val paddingEncoded = uint8L.encode(paddingNeeded).require
|
||||
|
||||
val packetWithPadding = packetNoPadding ++ ByteVector.fill(paddingNeeded)(0x00) ++ paddingEncoded.toByteVector
|
||||
|
||||
// raw packets plus MAC must be padded to the nearest 16 byte boundary
|
||||
val encryptedPayload = crypto.encrypt(packetWithPadding)
|
||||
|
||||
Attempt.successful(EncryptedPacket(sequenceNumber, encryptedPayload))
|
||||
}
|
||||
|
||||
def decryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : EncryptedPacket) : Attempt[PlanetSidePacketContainer] = {
|
||||
val payloadDecrypted = crypto.decrypt(packet.payload)
|
||||
|
||||
// get the last byte which is the padding length
|
||||
val payloadJustLen = payloadDecrypted.takeRight(1)
|
||||
val padding = uint8L.decode(payloadJustLen.bits)
|
||||
|
||||
padding match {
|
||||
case Failure(e) => return Attempt.failure(Err("Failed to decode the encrypted padding length: " + e.message))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
val macSize = CryptoInterface.MD5_MAC_SIZE
|
||||
val macDecoder = bytes(macSize)
|
||||
val payloadNoPadding = payloadDecrypted.dropRight(1 + padding.require.value)
|
||||
val payloadMac = payloadNoPadding.takeRight(macSize)
|
||||
val payloadNoMac = payloadNoPadding.dropRight(macSize)
|
||||
|
||||
/*
|
||||
println("Payload: " + packet.payload)
|
||||
println("DecPayload: " + payloadDecrypted)
|
||||
println("DecPayloadNoLen: " + payloadJustLen)
|
||||
println("Padding: " + padding.require.value)
|
||||
println("NoPadding: " + payloadNoPadding)
|
||||
println("Mac: " + payloadMac)
|
||||
println("NoMac: " + payloadNoMac)*/
|
||||
|
||||
|
||||
val mac = macDecoder.decode(payloadMac.bits)
|
||||
|
||||
mac match {
|
||||
case Failure(e) => return Attempt.failure(Err("Failed to extract the encrypted MAC: " + e.message))
|
||||
case _ =>
|
||||
}
|
||||
|
||||
val computedMac = crypto.macForDecrypt(payloadNoMac)
|
||||
|
||||
// verify that the MAC matches
|
||||
if(!CryptoInterface.verifyMAC(computedMac, mac.require.value))
|
||||
throw new SecurityException("Invalid packet MAC")
|
||||
|
||||
if(payloadNoMac.length < PLANETSIDE_MIN_PACKET_SIZE) {
|
||||
return Attempt.failure(Err(s"Decrypted packet does not meet the minimum length of $PLANETSIDE_MIN_PACKET_SIZE bytes"))
|
||||
}
|
||||
|
||||
val firstByte = payloadNoMac{0}
|
||||
|
||||
firstByte match {
|
||||
case 0x00 => unmarshalControlPacket(payloadNoMac.drop(1))
|
||||
case _ => unmarshalGamePacket(packet.sequenceNumber, payloadNoMac)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
common/src/main/scala/sna/Library.scala
Normal file
60
common/src/main/scala/sna/Library.scala
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* Copyright (c) 2014 Sanjay Dasgupta, All Rights Reserved
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* Lesser General Public License for more details.
|
||||
*/
|
||||
|
||||
package sna
|
||||
|
||||
import com.sun.jna.{Function => JNAFunction}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.language.dynamics
|
||||
|
||||
class Library (val libName: String) extends Dynamic {
|
||||
|
||||
class Invocation (val jnaFunction: JNAFunction, val args: Array[Object]) {
|
||||
|
||||
// TODO: this does not call without a passed type parameter
|
||||
def apply[R](implicit m: Manifest[R]): R = {
|
||||
//println("invoking " + jnaFunction.getName + ". class " + m.runtimeClass.toString)
|
||||
if (m.runtimeClass == classOf[Unit]) {
|
||||
jnaFunction.invoke(args).asInstanceOf[R]
|
||||
} else {
|
||||
jnaFunction.invoke(m.runtimeClass, args).asInstanceOf[R]
|
||||
}
|
||||
}
|
||||
|
||||
def as[R](implicit m: Manifest[R]) = apply[R](m)
|
||||
def asInstanceOf[R](implicit m: Manifest[R]) = apply[R](m)
|
||||
}
|
||||
|
||||
def applyDynamic(functionName: String)(args: Any*) = {
|
||||
new Invocation(loadFunction(functionName), args.map(_.asInstanceOf[Object]).toArray[Object])
|
||||
}
|
||||
|
||||
private def loadFunction(functionName : String) : JNAFunction = {
|
||||
var jnaFunction: JNAFunction = null
|
||||
if (functionCache.contains(functionName)) {
|
||||
jnaFunction = functionCache(functionName)
|
||||
} else {
|
||||
jnaFunction = JNAFunction.getFunction(libName, functionName)
|
||||
functionCache(functionName) = jnaFunction
|
||||
}
|
||||
|
||||
jnaFunction
|
||||
}
|
||||
|
||||
def prefetch(functionName : String) : Unit = {
|
||||
loadFunction(functionName)
|
||||
}
|
||||
|
||||
private val functionCache = mutable.Map.empty[String, JNAFunction]
|
||||
}
|
||||
68
common/src/test/scala/PacketCodingTest.scala
Normal file
68
common/src/test/scala/PacketCodingTest.scala
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import org.specs2.mutable._
|
||||
import psforever.crypto.CryptoInterface
|
||||
import psforever.net._
|
||||
import scodec.bits._
|
||||
|
||||
class PacketCodingTest extends Specification {
|
||||
/*def roundTrip[Container <: PlanetSidePacketContainer, Packet <: PlanetSidePacket](cont : Container, pkt : Packet) = {
|
||||
|
||||
val filledContainer = cont match {
|
||||
case x : ControlPacket => x.copy(packet = pkt.asInstanceOf[PlanetSideControlPacket])
|
||||
}
|
||||
val pktEncoded = PacketCoding.MarshalPacket(ControlPacket(packetUnderTest.opcode, packetUnderTest)).require
|
||||
val pktDecoded = PacketCoding.UnMarshalPacket(pkt.toByteVector).require.asInstanceOf[ControlPacket]
|
||||
val recvPkt = decoded.packet.asInstanceOf[ServerStart]
|
||||
|
||||
}*/
|
||||
|
||||
"Packet coding" should {
|
||||
"correctly decode control packets" in {
|
||||
val packet = PacketCoding.UnmarshalPacket(hex"0001 00000002 00261e27 000001f0").require
|
||||
|
||||
packet.isInstanceOf[ControlPacket] mustEqual true
|
||||
|
||||
val controlPacket = packet.asInstanceOf[ControlPacket]
|
||||
controlPacket.opcode mustEqual ControlPacketOpcode.ClientStart
|
||||
controlPacket.packet mustEqual ClientStart(656287232)
|
||||
}
|
||||
|
||||
"encode and decode to identical packets" in {
|
||||
val clientNonce = 213129
|
||||
val serverNonce = 848483
|
||||
|
||||
val packetUnderTest = ServerStart(clientNonce, serverNonce)
|
||||
val pkt = PacketCoding.MarshalPacket(ControlPacket(packetUnderTest.opcode, packetUnderTest)).require
|
||||
|
||||
val decoded = PacketCoding.UnmarshalPacket(pkt.toByteVector).require.asInstanceOf[ControlPacket]
|
||||
val recvPkt = decoded.packet.asInstanceOf[ServerStart]
|
||||
|
||||
packetUnderTest mustEqual recvPkt
|
||||
}
|
||||
|
||||
"reject corrupted control packets" in {
|
||||
val packet = PacketCoding.UnmarshalPacket(hex"0001 00001002 00261e27 004101f0")
|
||||
|
||||
packet.isSuccessful mustEqual false
|
||||
}
|
||||
|
||||
"correctly decode crypto packets" in {
|
||||
val packet = PacketCoding.UnmarshalPacket(hex"0001 00000002 00261e27 000001f0").require
|
||||
|
||||
packet.isInstanceOf[ControlPacket] mustEqual true
|
||||
|
||||
val controlPacket = packet.asInstanceOf[ControlPacket]
|
||||
controlPacket.opcode mustEqual ControlPacketOpcode.ClientStart
|
||||
controlPacket.packet mustEqual ClientStart(656287232)
|
||||
}
|
||||
|
||||
"reject bad packet types" in {
|
||||
PacketCoding.UnmarshalPacket(hex"ff414141").isFailure mustEqual true
|
||||
}
|
||||
|
||||
"reject small packets" in {
|
||||
PacketCoding.UnmarshalPacket(hex"00").isFailure mustEqual true
|
||||
PacketCoding.UnmarshalPacket(hex"").isFailure mustEqual true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue