diff --git a/src/pocketmine/network/mcpe/NetworkSession.php b/src/pocketmine/network/mcpe/NetworkSession.php index 84e8ae2b4..d84336379 100644 --- a/src/pocketmine/network/mcpe/NetworkSession.php +++ b/src/pocketmine/network/mcpe/NetworkSession.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\network\mcpe; +use Mdanter\Ecc\Crypto\Key\PublicKeyInterface; use pocketmine\entity\effect\EffectInstance; use pocketmine\entity\Human; use pocketmine\entity\Living; @@ -522,7 +523,10 @@ class NetworkSession{ }, $reason); } - public function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error) : bool{ + public function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error, PublicKeyInterface $clientPubKey) : void{ + if(!$this->connected){ + return; + } if($authenticated and $this->info->getXuid() === ""){ $error = "Expected XUID but none found"; } @@ -530,7 +534,7 @@ class NetworkSession{ if($error !== null){ $this->disconnect($this->server->getLanguage()->translateString("pocketmine.disconnect.invalidSession", [$error])); - return false; + return; } $this->authenticated = $authenticated; @@ -538,7 +542,7 @@ class NetworkSession{ if(!$this->authenticated){ if($authRequired){ $this->disconnect("disconnectionScreen.notAuthenticated"); - return false; + return; } if($this->info->getXuid() !== ""){ @@ -547,10 +551,19 @@ class NetworkSession{ } $this->logger->debug("Xbox Live authenticated: " . ($this->authenticated ? "YES" : "NO")); - return $this->manager->kickDuplicates($this); + if($this->manager->kickDuplicates($this)){ + if(NetworkCipher::$ENABLED){ + $this->server->getAsyncPool()->submitTask(new PrepareEncryptionTask($this, $clientPubKey)); + }else{ + $this->onLoginSuccess(); + } + } } public function enableEncryption(string $encryptionKey, string $handshakeJwt) : void{ + if(!$this->connected){ + return; + } $this->sendDataPacket(ServerToClientHandshakePacket::create($handshakeJwt), true); //make sure this gets sent before encryption is enabled $this->cipher = new NetworkCipher($encryptionKey); diff --git a/src/pocketmine/network/mcpe/PrepareEncryptionTask.php b/src/pocketmine/network/mcpe/PrepareEncryptionTask.php new file mode 100644 index 000000000..f67794ed7 --- /dev/null +++ b/src/pocketmine/network/mcpe/PrepareEncryptionTask.php @@ -0,0 +1,111 @@ +generator384()->createPrivateKey(); + } + + $this->serverPrivateKey = self::$SERVER_PRIVATE_KEY; + $this->clientPub = $clientPub; + $this->storeLocal(self::TLS_KEY_SESSION, $session); + } + + public function onRun() : void{ + $serverPriv = $this->serverPrivateKey; + $salt = random_bytes(16); + $sharedSecret = $serverPriv->createExchange($this->clientPub)->calculateSharedKey(); + + $this->aesKey = openssl_digest($salt . hex2bin(str_pad(gmp_strval($sharedSecret, 16), 96, "0", STR_PAD_LEFT)), 'sha256', true); + $this->handshakeJwt = $this->generateServerHandshakeJwt($serverPriv, $salt); + } + + private function generateServerHandshakeJwt(PrivateKeyInterface $serverPriv, string $salt) : string{ + $jwtBody = self::b64UrlEncode(json_encode([ + "x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())), + "alg" => "ES384" + ]) + ) . "." . self::b64UrlEncode(json_encode([ + "salt" => base64_encode($salt) + ]) + ); + + openssl_sign($jwtBody, $sig, (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($serverPriv), OPENSSL_ALGO_SHA384); + + $decodedSig = (new DerSignatureSerializer())->parse($sig); + $jwtSig = self::b64UrlEncode( + hex2bin(str_pad(gmp_strval($decodedSig->getR(), 16), 96, "0", STR_PAD_LEFT)) . + hex2bin(str_pad(gmp_strval($decodedSig->getS(), 16), 96, "0", STR_PAD_LEFT)) + ); + + return "$jwtBody.$jwtSig"; + } + + private static function b64UrlEncode(string $str) : string{ + return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); + } + + public function onCompletion() : void{ + /** @var NetworkSession $session */ + $session = $this->fetchLocal(self::TLS_KEY_SESSION); + $session->enableEncryption($this->aesKey, $this->handshakeJwt); + } +} diff --git a/src/pocketmine/network/mcpe/ProcessLoginTask.php b/src/pocketmine/network/mcpe/ProcessLoginTask.php index 909b5f522..8dbe3e314 100644 --- a/src/pocketmine/network/mcpe/ProcessLoginTask.php +++ b/src/pocketmine/network/mcpe/ProcessLoginTask.php @@ -23,12 +23,8 @@ declare(strict_types=1); namespace pocketmine\network\mcpe; -use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface; use Mdanter\Ecc\Crypto\Key\PublicKeyInterface; use Mdanter\Ecc\Crypto\Signature\Signature; -use Mdanter\Ecc\EccFactory; -use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer; -use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer; use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; @@ -36,26 +32,16 @@ use pocketmine\network\mcpe\protocol\LoginPacket; use pocketmine\scheduler\AsyncTask; use function assert; use function base64_decode; -use function base64_encode; use function bin2hex; use function explode; use function gmp_init; -use function gmp_strval; -use function hex2bin; use function json_decode; -use function json_encode; -use function openssl_digest; -use function openssl_sign; use function openssl_verify; -use function random_bytes; -use function rtrim; -use function str_pad; use function str_repeat; use function str_split; use function strlen; use function time; use const OPENSSL_ALGO_SHA384; -use const STR_PAD_LEFT; class ProcessLoginTask extends AsyncTask{ private const TLS_KEY_SESSION = "session"; @@ -64,9 +50,6 @@ class ProcessLoginTask extends AsyncTask{ private const CLOCK_DRIFT_MAX = 60; - /** @var PrivateKeyInterface|null */ - private static $SERVER_PRIVATE_KEY = null; - /** @var LoginPacket */ private $packet; @@ -86,52 +69,22 @@ class ProcessLoginTask extends AsyncTask{ /** @var bool */ private $authRequired; - /** - * @var bool - * Whether or not to enable encryption for the session that sent this login. - */ - private $useEncryption = true; + /** @var PublicKeyInterface|null */ + private $clientPublicKey = null; - /** @var PrivateKeyInterface|null */ - private $serverPrivateKey = null; - - /** @var string|null */ - private $aesKey = null; - /** @var string|null */ - private $handshakeJwt = null; - - public function __construct(NetworkSession $session, LoginPacket $packet, bool $authRequired, bool $useEncryption = true){ + public function __construct(NetworkSession $session, LoginPacket $packet, bool $authRequired){ $this->storeLocal(self::TLS_KEY_SESSION, $session); $this->packet = $packet; $this->authRequired = $authRequired; - $this->useEncryption = $useEncryption; - if($useEncryption){ - if(self::$SERVER_PRIVATE_KEY === null){ - self::$SERVER_PRIVATE_KEY = EccFactory::getNistCurves()->generator384()->createPrivateKey(); - } - - $this->serverPrivateKey = self::$SERVER_PRIVATE_KEY; - } } public function onRun() : void{ try{ - $clientPub = $this->validateChain(); + $this->clientPublicKey = $this->validateChain(); + $this->error = null; }catch(VerifyLoginException $e){ $this->error = $e->getMessage(); - return; } - - if($this->useEncryption){ - $serverPriv = $this->serverPrivateKey; - $salt = random_bytes(16); - $sharedSecret = $serverPriv->createExchange($clientPub)->calculateSharedKey(); - - $this->aesKey = openssl_digest($salt . hex2bin(str_pad(gmp_strval($sharedSecret, 16), 96, "0", STR_PAD_LEFT)), 'sha256', true); - $this->handshakeJwt = $this->generateServerHandshakeJwt($serverPriv, $salt); - } - - $this->error = null; } private function validateChain() : PublicKeyInterface{ @@ -210,27 +163,6 @@ class ProcessLoginTask extends AsyncTask{ $currentPublicKey = $claims["identityPublicKey"] ?? null; //if there are further links, the next link should be signed with this } - private function generateServerHandshakeJwt(PrivateKeyInterface $serverPriv, string $salt) : string{ - $jwtBody = self::b64UrlEncode(json_encode([ - "x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())), - "alg" => "ES384" - ]) - ) . "." . self::b64UrlEncode(json_encode([ - "salt" => base64_encode($salt) - ]) - ); - - openssl_sign($jwtBody, $sig, (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($serverPriv), OPENSSL_ALGO_SHA384); - - $decodedSig = (new DerSignatureSerializer())->parse($sig); - $jwtSig = self::b64UrlEncode( - hex2bin(str_pad(gmp_strval($decodedSig->getR(), 16), 96, "0", STR_PAD_LEFT)) . - hex2bin(str_pad(gmp_strval($decodedSig->getS(), 16), 96, "0", STR_PAD_LEFT)) - ); - - return "$jwtBody.$jwtSig"; - } - private static function b64UrlDecode(string $str) : string{ if(($len = strlen($str) % 4) !== 0){ $str .= str_repeat('=', 4 - $len); @@ -238,21 +170,9 @@ class ProcessLoginTask extends AsyncTask{ return base64_decode(strtr($str, '-_', '+/'), true); } - private static function b64UrlEncode(string $str) : string{ - return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); - } - public function onCompletion() : void{ /** @var NetworkSession $session */ $session = $this->fetchLocal(self::TLS_KEY_SESSION); - if(!$session->isConnected()){ - $session->getLogger()->debug("Disconnected before login could be verified"); - }elseif($session->setAuthenticationStatus($this->authenticated, $this->authRequired, $this->error)){ - if(!$this->useEncryption){ - $session->onLoginSuccess(); - }else{ - $session->enableEncryption($this->aesKey, $this->handshakeJwt); - } - } + $session->setAuthenticationStatus($this->authenticated, $this->authRequired, $this->error, $this->clientPublicKey); } } diff --git a/src/pocketmine/network/mcpe/handler/LoginPacketHandler.php b/src/pocketmine/network/mcpe/handler/LoginPacketHandler.php index 76bd38f46..d34db5126 100644 --- a/src/pocketmine/network/mcpe/handler/LoginPacketHandler.php +++ b/src/pocketmine/network/mcpe/handler/LoginPacketHandler.php @@ -25,7 +25,6 @@ namespace pocketmine\network\mcpe\handler; use pocketmine\entity\Skin; use pocketmine\event\player\PlayerPreLoginEvent; -use pocketmine\network\mcpe\NetworkCipher; use pocketmine\network\mcpe\NetworkSession; use pocketmine\network\mcpe\ProcessLoginTask; use pocketmine\network\mcpe\protocol\LoginPacket; @@ -133,7 +132,7 @@ class LoginPacketHandler extends PacketHandler{ * @throws \InvalidArgumentException */ protected function processLogin(LoginPacket $packet, bool $authRequired) : void{ - $this->server->getAsyncPool()->submitTask(new ProcessLoginTask($this->session, $packet, $authRequired, NetworkCipher::$ENABLED)); + $this->server->getAsyncPool()->submitTask(new ProcessLoginTask($this->session, $packet, $authRequired)); $this->session->setHandler(NullPacketHandler::getInstance()); //drop packets received during login verification }