PSF-LoginServer/src/test/scala/CryptoTest.scala
Jakob Gillich 407429ee21 Networking
The game uses a UDP-based protocol. Unlike TCP, UDP does not guarantee that
packets arrive, or that they arrive in the correct order. For this reason,
the game protocol implements those features using the following:

* All packets have a sequence number that is utilized for reordering
* Important packets are wrapped in a SlottedMetaPacket with a subslot number
* RelatedA packets ae used to request lost packets using the subslot number
* RelatedB packets are used to confirm received SlottedMetaPackets

All of these go both ways, server <-> client. We used to only partially
implement these features: Outgoing packet bundles used SMPs and could be
resent, but not all packets were bundled and there was no logic for requesting
lost packets from the client and there was no packet reordering, which resulted
in dire consequences in the case of packet loss (zoning failures, crashes and many
other odd bugs). This patch addresses all of these issues.

* Packet bundling: Packets are now automatically bundled and sent as
  SlottedMetaPackets using a recurring timer. All manual bundling functionality
  was removed.

* Packet reordering: Incoming packets, if received out of order, are stashed and
  reordered. The maximum wait time for reordering is 20ms.

* Packet requesting: Missing SlottedMetaPackets are requested from the client.

* PacketCoding refactor: Dropped confusing packet container types. Fixes #5.

* Crypto rewrite: PSCrypto is based on a ancient buggy version of cryptopp.
  Updating to a current version was not possible because it removed the
  MD5-MAC algorithm. For more details, see Md5Mac.scala.
  This patch replaces PSCrypto with native Scala code.

* Added two new actors:
  * SocketActor: A simple typed UDP socket actor
  * MiddlewareActor: The old session pipeline greatly simplified into a
    typed actor that does most of the things mentioned above.

* Begun work on a headless client

* Fixed anniversary gun breaking stamina regen

* Resolved a few sentry errors
2020-09-26 23:58:09 +02:00

106 lines
3.5 KiB
Scala

// Copyright (c) 2017 PSForever
import java.security.{SecureRandom, Security}
import javax.crypto.spec.SecretKeySpec
import org.specs2.mutable._
import net.psforever.packet.PacketCoding
import net.psforever.packet.PacketCoding.CryptoCoding
import net.psforever.packet.control.{HandleGamePacket, SlottedMetaPacket}
import net.psforever.packet.game.PlanetsideAttributeMessage
import net.psforever.types.PlanetSideGUID
import net.psforever.util.{DiffieHellman, Md5Mac}
import org.bouncycastle.jce.provider.BouncyCastleProvider
import scodec.Attempt.Failure
import scodec.Err
import scodec.bits._
class CryptoTest extends Specification {
Security.addProvider(new BouncyCastleProvider)
args(stopOnFail = true)
"Crypto" should {
"encrypt and decrypt" in {
val key = hex"41414141414141414141414141414141"
val keySpec = new SecretKeySpec(key.take(20).toArray, "RC5")
val plaintext = ByteVector.fill(32)(0x42)
val crypto = CryptoCoding(keySpec, keySpec, key, key)
val ciphertext = crypto.encrypt(plaintext).require
val decrypted = crypto.decrypt(ciphertext).require
decrypted mustEqual plaintext
ciphertext mustNotEqual plaintext
}
"encrypt and decrypt must only accept block aligned inputs" in {
val key = hex"41414141414141414141414141414141"
val keySpec = new SecretKeySpec(key.take(20).toArray, "RC5")
val badPad = ByteVector.fill(PacketCoding.RC5_BLOCK_SIZE - 1)('a')
val crypto = CryptoCoding(keySpec, keySpec, key, key)
//crypto.encrypt(badPad) must throwA[javax.crypto.IllegalBlockSizeException]
crypto.decrypt(badPad) mustEqual Failure(Err("data not block size aligned"))
}
"encrypt and decrypt packet" in {
val key = hex"41414141414141414141414141414141"
val keySpec = new SecretKeySpec(key.take(20).toArray, "RC5")
val crypto = CryptoCoding(keySpec, keySpec, key, key)
val packet =
SlottedMetaPacket(
0,
5,
PacketCoding
.encodePacket(
HandleGamePacket(
PacketCoding.encodePacket(PlanetsideAttributeMessage(PlanetSideGUID(0), 0, 0L)).require.toByteVector
)
)
.require
.bytes
)
val encrypted = PacketCoding.marshalPacket(packet, Some(10), Some(crypto)).require
println(s"encrypted ${encrypted}")
val (decryptedPacket, sequence) = PacketCoding.unmarshalPacket(encrypted.bytes, Some(crypto)).require
decryptedPacket mustEqual packet
sequence must beSome(10)
}
"MD5MAC" 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 message2 = ByteVector.view((0 until 64).map(_.toByte).toArray)
val expected = hex"5aa15de41f5220cf5cca489155e1438c5aa15de4"
val mac = new Md5Mac(key)
mac.update(message)
mac.doFinal(20) mustEqual expected
val mac2 = new Md5Mac(key)
mac2.update(message2)
mac.update(message2)
mac.doFinal() mustEqual mac2.doFinal()
(0 to 20).map(_ => mac.updateFinal(message, 20) mustEqual expected)
}
"DH" in {
val p = BigInt(128, new SecureRandom()).toByteArray
val g = Array(1.toByte)
val bob = new DiffieHellman(p, g)
val alice = new DiffieHellman(p, g)
bob.agree(alice.publicKey) mustEqual alice.agree(bob.publicKey)
}
}
}