[packet] VNLWorldStatusMessage

Added VNL packet type from IDA. Moved definition in to its own file.
Refactored PacketCoding MarshalPacket. The whole structure needs a
rework.

Now able to get a PlanetSide client to the server screen with a server
of choice.
This commit is contained in:
Chord 2016-03-04 13:00:03 -05:00
parent 1643ecc1dd
commit b8ff34c0f9
10 changed files with 363 additions and 102 deletions

View file

@ -59,10 +59,5 @@ object ControlPacketOpcode extends Enumeration {
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)
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L)
}

View file

@ -8,14 +8,14 @@ object GamePacketOpcode extends Enumeration {
type Type = Value
val
// Opcodes should have a marker every 10
// Opcodes should have a marker every 10 (decimal)
// OPCODE 0
Unknown0,
LoginMessage,
LoginRespMessage,
Unknown3,
ConnectToWorldMessage,
Unknown5,
VNLWorldStatusMessage,
UnknownMessage6,
UnknownMessage7,
PlayerStateMessage,
@ -33,13 +33,9 @@ object GamePacketOpcode extends Enumeration {
def getPacketDecoder(opcode : GamePacketOpcode.Type) : (BitVector) => Attempt[DecodeResult[PlanetSideGamePacket]] = opcode match {
case LoginMessage => psforever.net.LoginMessage.decode
case LoginRespMessage => psforever.net.LoginRespMessage.decode
case VNLWorldStatusMessage => psforever.net.VNLWorldStatusMessage.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)
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint8L)
}

View file

@ -1,5 +1,7 @@
package psforever.net
import java.nio.charset.Charset
import scodec.{DecodeResult, Err, Codec, Attempt}
import scodec.bits._
import scodec.codecs._
@ -14,22 +16,22 @@ sealed trait PlanetSidePacket extends Serializable {
}
// Used by companion objects to create encoders and decoders
sealed trait Marshallable[T] {
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)
def decode(a : BitVector) : Attempt[DecodeResult[T]] = codec.decode(a)
}
sealed trait PlanetSideGamePacket extends PlanetSidePacket {
trait PlanetSideGamePacket extends PlanetSidePacket {
def opcode : GamePacketOpcode.Type
}
sealed trait PlanetSideControlPacket extends PlanetSidePacket {
trait PlanetSideControlPacket extends PlanetSidePacket {
def opcode : ControlPacketOpcode.Type
}
sealed trait PlanetSideCryptoPacket extends PlanetSidePacket {
trait PlanetSideCryptoPacket extends PlanetSidePacket {
def opcode : CryptoPacketOpcode.Type
}
@ -143,6 +145,17 @@ object LoginMessage extends Marshallable[LoginMessage] {
type Struct = String :: Option[String] :: Option[String] :: HNil
/* Okay, okay, here's what's happening here:
PlanetSide's *wonderful* packet design reuses packets for different encodings.
What we have here is that depending on a boolean in the LoginPacket, we will either
be decoding a username & password OR a token & username. Yeah...so this doesn't
really fit in to a fixed packet decoding scheme.
The below code abstracts away from this by using pattern matching.
The scodec specific part is the either(...) Codec, which decodes one bit and chooses
Left or Right depending on it.
*/
implicit val credentialChoice : Codec[Struct] = {
type InStruct = Either[String :: String :: HNil, String :: String :: HNil]
@ -307,12 +320,7 @@ 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)
implicit val codec: Codec[this.Value] = PacketHelpers.createEnumerationCodec(this, uint4L)
}
object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
@ -327,10 +335,16 @@ object PlanetSidePacketFlags extends Marshallable[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
/*class MarshallableEnum[+T] extends Enumeration {
type StorageType = Codec[Int]
implicit val storageType : StorageType = uint8
assert(maxId <= Math.pow(storageType.sizeBound.exact.get, 2),
this.getClass.getCanonicalName + ": maxId exceeds primitive type")
implicit val codec: Codec[T] = PacketHelpers.createEnumerationCodec(this, storageType)
}*/
object PacketHelpers {
def emptyCodec[T](instance : T) = {
@ -339,13 +353,14 @@ object PacketHelpers {
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")
enum.getClass.getCanonicalName + ": maxId exceeds primitive type")
def to(pkt: E#Value): Struct = {
pkt.id :: HNil
@ -372,15 +387,47 @@ object PacketHelpers {
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)
(a : Int) =>
// if the specified goes above 0x7f (127) then we need two bytes to represent it
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)
/*private def encodedStringSizeWithLimit(limit : Int) : Codec[Int] = {
either(bool, uint(15), uint(7)).
exmap[Int](
(a : Either[Int, Int]) => {
val result = a.fold[Int](a => a, a => a)
if(result > limit)
Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit"))
else
Attempt.successful(result)
},
(a : Int) => {
if(a > limit)
return Attempt.failure(Err("adsf"))
//return Left(Attempt.failure(Err(s"Encoded string exceeded byte limit of $limit")))
if(a > 0x7f)
return Attempt.successful(Left(a))
else
Right(a)
}
)
}*/
private def encodedStringSizeWithPad(pad : Int) : Codec[Int] = encodedStringSize <~ ignore(pad)
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
//def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii)
def encodedStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment), ascii)
/// Variable for the charset that planetside uses for unicode
val utf16 = string(Charset.forName("UTF-16LE"))
/// An encoded *wide* string is twice the length of the given encoded size and half of the length of the
/// input string. We use xmap to transform the encodedString codec as this change is just a division and multiply
def encodedWideString : Codec[String] = variableSizeBytes(encodedStringSize.xmap(
insize => insize*2,
outSize => outSize/2), utf16)
}

View file

@ -7,32 +7,41 @@ import scodec.bits._
import scodec.{DecodeResult, Err, Attempt, Codec}
import scodec.codecs.{uint16L, uint8L, uint4L, bytes}
// Packet containers
/// Packet container base trait
sealed trait PlanetSidePacketContainer
// A sequence, encrypted opcode, encrypted payload, and MD5MAC plus padding
/// A sequence, encrypted opcode, encrypted payload, and implicit MD5MAC plus padding
final case class EncryptedPacket(sequenceNumber : Int,
payload : ByteVector) extends PlanetSidePacketContainer
// A sequence, and payload. Crypto packets have no discernible opcodes
/// A sequence, and payload. Crypto packets have no discernible opcodes an rely off of implicit
/// state to decode properly
final case class CryptoPacket(sequenceNumber : Int,
packet : PlanetSideCryptoPacket) extends PlanetSidePacketContainer
/// A sequenced game packet with an opcode and payload
final case class GamePacket(opcode : GamePacketOpcode.Value,
sequenceNumber : Int,
packet : PlanetSideGamePacket) extends PlanetSidePacketContainer
// Just an opcode + payload (does not expect a response)
/// Just an opcode + payload
final case class ControlPacket(opcode : ControlPacketOpcode.Value,
packet : PlanetSideControlPacket) extends PlanetSidePacketContainer
object PacketCoding {
/// A lower bound on the packet size
final val PLANETSIDE_MIN_PACKET_SIZE = 2
def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = {
UnmarshalPacket(msg, CryptoPacketOpcode.Ignore)
}
/**
* Given a full and complete planetside packet as it would be sent on the wire, attempt to
* decode it given an optional header and required payload. This function does all of the
* hard work of making decisions along the way in order to decode a planetside packet to
* completion.
* @param msg the raw packet
* @param cryptoState the current state of the connection's crypto. This is only used when decoding
* crypto packets as they do not have opcodes
* @return PlanetSidePacketContainer
*/
def UnmarshalPacket(msg : ByteVector, cryptoState : CryptoPacketOpcode.Type) : Attempt[PlanetSidePacketContainer] = {
// check for a minimum length
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
@ -47,6 +56,22 @@ object PacketCoding {
}
}
/**
* Helper function to decode a packet without specifying a crypto packet state.
* Mostly used when there is no crypto state available, such as tests.
* @param msg packet data bytes
* @return PlanetSidePacketContainer
*/
def UnmarshalPacket(msg : ByteVector) : Attempt[PlanetSidePacketContainer] = {
UnmarshalPacket(msg, CryptoPacketOpcode.Ignore)
}
/**
* Similar to UnmarshalPacket, but does not process any packet header and does not support
* decoding of crypto packets. Mostly used in tests.
* @param msg raw, unencrypted packet
* @return PlanetSidePacket
*/
def DecodePacket(msg : ByteVector) : Attempt[PlanetSidePacket] = {
// check for a minimum length
if(msg.length < PLANETSIDE_MIN_PACKET_SIZE)
@ -68,70 +93,110 @@ object PacketCoding {
var opcodeEncoded : BitVector = BitVector.empty
var payloadEncoded : BitVector = BitVector.empty
var controlPacket = false
var sequenceNum = 0
// packet flags
var hasFlags = true
var secured = false
var packetType = PacketType.Crypto
packet match {
case GamePacket(opcode, seq, payload) =>
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = false)
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
secured = false
packetType = PacketType.Normal
sequenceNum = seq
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))
EncodePacket(payload) match {
case f @ Failure(e) => return f
case Successful(p) => payloadEncoded = p
}
case ControlPacket(opcode, payload) =>
flagsEncoded = hex"00".bits
controlPacket = true
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))
EncodePacket(payload) match {
case f @ Failure(e) => return f
case Successful(p) => payloadEncoded = p
}
case CryptoPacket(seq, payload) =>
val flags = PlanetSidePacketFlags(PacketType.Crypto, secured = false)
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
secured = false
packetType = PacketType.Crypto
sequenceNum = seq
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))
EncodePacket(payload) match {
case f @ Failure(e) => return f
case Successful(p) => payloadEncoded = p
}
case EncryptedPacket(seq, payload) =>
val flags = PlanetSidePacketFlags(PacketType.Normal, secured = true)
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
secured = true
packetType = PacketType.Normal
sequenceNum = seq
// 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 flags = PlanetSidePacketFlags(packetType, secured = secured)
// crypto packets DONT have flags
if(!controlPacket) {
flagsEncoded = PlanetSidePacketFlags.codec.encode(flags).require
uint16L.encode(sequenceNum) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal sequence in packet $packet: " + e.messageWithContext))
case Successful(p) => seqEncoded = p
}
}
val finalPacket = flagsEncoded ++ seqEncoded ++ paddingEncoded ++ opcodeEncoded ++ payloadEncoded
Attempt.successful(finalPacket)
}
def EncodePacket(packet : PlanetSideControlPacket) : Attempt[BitVector] = {
val opcode = packet.opcode
var opcodeEncoded = BitVector.empty
var payloadEncoded = BitVector.empty
ControlPacketOpcode.codec.encode(opcode) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in control packet $opcode: " + e.messageWithContext))
case Successful(p) => opcodeEncoded = p
}
encodePacket(packet) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal control packet $packet: " + e.messageWithContext))
case Successful(p) => payloadEncoded = p
}
Attempt.Successful(hex"00".bits ++ opcodeEncoded ++ payloadEncoded)
}
def EncodePacket(packet : PlanetSideCryptoPacket) : Attempt[BitVector] = {
encodePacket(packet) match {
case Failure(e) => Attempt.failure(Err(s"Failed to marshal crypto packet $packet: " + e.messageWithContext))
case s @ Successful(p) => s
}
}
def EncodePacket(packet : PlanetSideGamePacket) : Attempt[BitVector] = {
val opcode = packet.opcode
var opcodeEncoded = BitVector.empty
var payloadEncoded = BitVector.empty
GamePacketOpcode.codec.encode(opcode) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal opcode in game packet $opcode: " + e.messageWithContext))
case Successful(p) => opcodeEncoded = p
}
encodePacket(packet) match {
case Failure(e) => return Attempt.failure(Err(s"Failed to marshal game packet $packet: " + e.messageWithContext))
case Successful(p) => payloadEncoded = p
}
Attempt.Successful(opcodeEncoded ++ payloadEncoded)
}
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)
@ -199,6 +264,7 @@ object PacketCoding {
val packet = DecodeControlPacket(msg)
packet match {
// just return the failure
case f @ Failure(e) => f
case Successful(p) =>
Attempt.successful(CreateControlPacket(p))
@ -271,7 +337,7 @@ object PacketCoding {
///////////////////////////////////////////////////////////
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, packet : PlanetSidePacketContainer) : Attempt[EncryptedPacket] = {
// TODO: this is bad. rework
// TODO XXX: this is bad. rework
var sequenceNumber = 0
val rawPacket : BitVector = packet match {
@ -303,10 +369,14 @@ object PacketCoding {
case default => throw new IllegalArgumentException("Unsupported packet container type")
}
val packetMac = crypto.macForEncrypt(rawPacket.toByteVector)
encryptPacket(crypto, sequenceNumber, rawPacket.toByteVector)
}
def encryptPacket(crypto : CryptoInterface.CryptoStateWithMAC, sequenceNumber : Int, rawPacket : ByteVector) : Attempt[EncryptedPacket] = {
val packetMac = crypto.macForEncrypt(rawPacket)
// opcode, payload, and MAC
val packetNoPadding = rawPacket.toByteVector ++ packetMac
val packetNoPadding = rawPacket ++ packetMac
val remainder = packetNoPadding.length % CryptoInterface.RC5_BLOCK_SIZE

View file

@ -0,0 +1,89 @@
// Copyright (c) 2016 PSForever.net to present
package psforever.net
import scodec._
import scodec.bits._
import scodec.codecs._
import shapeless._
object WorldStatus extends Enumeration {
type Type = Value
val Up, Down, Locked, Full = Value
}
object ServerType extends Enumeration {
type Type = Value
val Development, Beta, Released = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
}
object EmpireNeed extends Enumeration {
type Type = Value
val TR, NC, VS = Value
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint2L)
}
final case class WorldInformation(name : String, status : WorldStatus.Value,
serverType : ServerType.Value, empireNeed : EmpireNeed.Value)
final case class VNLWorldStatusMessage(welcomeMessage : String, worlds : Vector[WorldInformation])
extends PlanetSideGamePacket {
type Packet = VNLWorldStatusMessage
def opcode = GamePacketOpcode.VNLWorldStatusMessage
def encode = VNLWorldStatusMessage.encode(this)
}
object VNLWorldStatusMessage extends Marshallable[VNLWorldStatusMessage] {
type InStruct = WorldStatus.Value :: ServerType.Value :: HNil
type OutStruct = Int :: ServerType.Value :: Int :: HNil
implicit val statusCodec : Codec[InStruct] = {
def from(a : InStruct) : OutStruct = a match {
case status :: svrType :: HNil =>
status match {
case WorldStatus.Down =>
0 :: svrType :: 2 :: HNil
case WorldStatus.Locked =>
0 :: svrType :: 1 :: HNil
case WorldStatus.Up =>
1 :: svrType :: 0 :: HNil
case WorldStatus.Full =>
5 :: svrType :: 0 :: HNil
}
}
def to(a : OutStruct) : InStruct = a match {
case status2 :: svrType :: status1 :: HNil =>
if(status1 == 0) {
if(status2 >= 5) {
WorldStatus.Full :: svrType :: HNil
} else {
WorldStatus.Up :: svrType :: HNil
}
} else {
if(status1 != 1)
WorldStatus.Down :: svrType :: HNil
else
WorldStatus.Locked :: svrType :: HNil
}
}
(("status2" | uint16L) ::
("server_type" | ServerType.codec) ::
("status1" | uint8L)).xmap(to, from)
}
implicit val codec : Codec[VNLWorldStatusMessage] = (
("welcome_message" | PacketHelpers.encodedWideString) ::
("worlds" | vectorOfN(uint8L, (
// XXX: this needs to be limited to 0x20 bytes
// XXX: this needs to be byte aligned, but not sure how to do this
("world_name" | PacketHelpers.encodedString) :: (
("status_and_type" | statusCodec) :+
("unknown" | constant(hex"01459e25403775")) :+
("empire_need" | EmpireNeed.codec)
)
).as[WorldInformation]
))).as[VNLWorldStatusMessage]
}

View file

@ -0,0 +1,135 @@
import org.specs2.mutable._
import psforever.crypto.CryptoInterface
import psforever.crypto.CryptoInterface.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 CryptoPacketTest 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
}
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright (c) 2016 PSForever.net to present
import org.specs2.mutable._
import psforever.net._
import scodec.bits._
class GamePacketTest extends Specification {
"PlanetSide game packet" in {
val cNonce = 656287232
"VNLWorldStatusMessage" should {
val string = hex"0597570065006c0063006f006d006500200074006f00200050006c0061006e00650074005300690064006500210020000186" ++
hex"67656d696e69" ++ hex"0100 01 00 01459e2540 3775" ++ bin"01".toByteVector
"decode" in {
PacketCoding.DecodePacket(string).require match {
case VNLWorldStatusMessage(message, worlds) =>
worlds.length mustEqual 1
message mustEqual "Welcome to PlanetSide! "
worlds{0}.name mustEqual "gemini"
worlds{0}.empireNeed mustEqual EmpireNeed.NC
worlds{0}.status mustEqual WorldStatus.Up
case default =>
true mustEqual false
}
}
"encode" in {
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
Vector(WorldInformation("gemini", WorldStatus.Up, ServerType.Beta, EmpireNeed.NC)))
//0100 04 00 01459e2540377540
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string
}
"encode and decode multiple worlds" in {
val msg = VNLWorldStatusMessage("Welcome to PlanetSide! ",
Vector(
WorldInformation("PSForever1", WorldStatus.Up, ServerType.Released, EmpireNeed.NC),
WorldInformation("PSForever2", WorldStatus.Down, ServerType.Beta, EmpireNeed.TR)
))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
println(pkt)
true mustEqual true
}
}
}
}

View file

@ -1,5 +1,4 @@
import org.specs2.mutable._
import psforever.crypto.CryptoInterface
import psforever.net._
import scodec.bits._