Compare commits

..

11 Commits

25 changed files with 1079 additions and 468 deletions

View File

@ -73,7 +73,7 @@ PocketMine-MP is free, but it requires a lot of time and effort from unpaid volu
You can support development using the following methods:
- [Patreon](https://www.patreon.com/pocketminemp)
- Bitcoin (BTC): `bc1q2v5ngyf8ugyd55kqa9ep35g2rv342ueqm6ks33`
- Bitcoin (BTC): `171u8K9e4FtU6j3e5sqNoxKUgEw9qWQdRV`
- Stellar Lumens (XLM): `GAAC5WZ33HCTE3BFJFZJXONMEIBNHFLBXM2HJVAZHXXPYA3HP5XPPS7T`
Thanks for your support!

View File

@ -29,9 +29,6 @@ use pocketmine\data\bedrock\block\BlockStateStringValues;
use pocketmine\data\bedrock\block\BlockTypeNames;
use pocketmine\errorhandler\ErrorToExceptionHandler;
use pocketmine\nbt\NbtException;
use pocketmine\nbt\tag\ByteTag;
use pocketmine\nbt\tag\IntTag;
use pocketmine\nbt\tag\StringTag;
use pocketmine\network\mcpe\convert\BlockStateDictionary;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Utils;
@ -81,9 +78,6 @@ function generateBlockPaletteReport(array $states) : BlockPaletteReport{
$name = $stateData->getName();
$result->seenTypes[$name] = $name;
foreach(Utils::stringifyKeys($stateData->getStates()) as $k => $v){
if(!$v instanceof ByteTag && !$v instanceof IntTag && !$v instanceof StringTag){
throw new AssumptionFailedError("Assumed all state tags should be TAG_Byte, TAG_Int or TAG_String, but found $k ($v) on block $name");
}
$result->seenStateValues[$k][$v->getValue()] = $v->getValue();
asort($result->seenStateValues[$k]);
}

View File

@ -133,15 +133,3 @@ Released 31st August 2025.
## Fixes
- Fixed banners placed in prior versions getting their tiles deleted (due to missing `Type` tags).
# 5.33.2
Released 16th September 2025.
## General
- PHP 8.4 has now been added to the test matrix.
## Fixes
- Fixed PHP 8.4 deprecation notice in `AsyncGeneratorExecutor`.
- Fixed inventory windows breaking if a window is sent while the player has the chat window open (e.g. quickly pressing E followed by / could trigger this).
- Fixed `BlockBreakEvent` being called twice in creative if cancelled.
- Reduced block lag when towering and other situations where the placed block might intersect with the player's AABB. Block lag may still appear on higher latency clients - this is still being worked on.

View File

@ -36,7 +36,7 @@
"pocketmine/bedrock-block-upgrade-schema": "~5.1.0+bedrock-1.21.60",
"pocketmine/bedrock-data": "~6.0.0+bedrock-1.21.100",
"pocketmine/bedrock-item-upgrade-schema": "~1.15.0+bedrock-1.21.100",
"pocketmine/bedrock-protocol": "~40.0.0+bedrock-1.21.100",
"pocketmine/bedrock-protocol": "~41.0.0+bedrock-1.21.100",
"pocketmine/binaryutils": "^0.2.1",
"pocketmine/callback-validator": "^1.0.2",
"pocketmine/color": "^0.3.0",
@ -52,7 +52,7 @@
"symfony/filesystem": "~6.4.0"
},
"require-dev": {
"phpstan/phpstan": "2.1.25",
"phpstan/phpstan": "2.1.17",
"phpstan/phpstan-phpunit": "^2.0.0",
"phpstan/phpstan-strict-rules": "^2.0.0",
"phpunit/phpunit": "^10.5.24"

90
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "008c888b5812dda09a0ec6e425453153",
"content-hash": "7bf7cd54642c2d65ecdfdcb28f3a64a8",
"packages": [
{
"name": "adhocore/json-comment",
@ -256,16 +256,16 @@
},
{
"name": "pocketmine/bedrock-protocol",
"version": "40.0.0+bedrock-1.21.100",
"version": "41.0.0+bedrock-1.21.100",
"source": {
"type": "git",
"url": "https://github.com/pmmp/BedrockProtocol.git",
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca"
"reference": "920ac291fe1b0143b2ebc90b3374ddab0b8531bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"reference": "5e95cab3a6e6c24920e0c25ca4aaf887ed4b70ca",
"url": "https://api.github.com/repos/pmmp/BedrockProtocol/zipball/920ac291fe1b0143b2ebc90b3374ddab0b8531bf",
"reference": "920ac291fe1b0143b2ebc90b3374ddab0b8531bf",
"shasum": ""
},
"require": {
@ -296,9 +296,9 @@
"description": "An implementation of the Minecraft: Bedrock Edition protocol in PHP",
"support": {
"issues": "https://github.com/pmmp/BedrockProtocol/issues",
"source": "https://github.com/pmmp/BedrockProtocol/tree/40.0.0+bedrock-1.21.100"
"source": "https://github.com/pmmp/BedrockProtocol/tree/41.0.0+bedrock-1.21.100"
},
"time": "2025-08-06T15:13:45+00:00"
"time": "2025-09-09T20:52:18+00:00"
},
{
"name": "pocketmine/binaryutils",
@ -818,20 +818,20 @@
},
{
"name": "ramsey/uuid",
"version": "4.9.1",
"version": "4.9.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0",
"shasum": ""
},
"require": {
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@ -890,9 +890,9 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
"source": "https://github.com/ramsey/uuid/tree/4.9.1"
"source": "https://github.com/ramsey/uuid/tree/4.9.0"
},
"time": "2025-09-04T20:59:21+00:00"
"time": "2025-06-25T14:20:11+00:00"
},
{
"name": "symfony/filesystem",
@ -1204,16 +1204,16 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.25",
"version": "2.1.17",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "4087d28bd252895874e174d65e26b2c202ed893a"
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4087d28bd252895874e174d65e26b2c202ed893a",
"reference": "4087d28bd252895874e174d65e26b2c202ed893a",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053",
"reference": "89b5ef665716fa2a52ecd2633f21007a6a349053",
"shasum": ""
},
"require": {
@ -1258,25 +1258,25 @@
"type": "github"
}
],
"time": "2025-09-12T14:26:42+00:00"
"time": "2025-05-21T20:55:28+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
"version": "2.0.7",
"version": "2.0.6",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan-phpunit.git",
"reference": "9a9b161baee88a5f5c58d816943cff354ff233dc"
"reference": "6b92469f8a7995e626da3aa487099617b8dfa260"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/9a9b161baee88a5f5c58d816943cff354ff233dc",
"reference": "9a9b161baee88a5f5c58d816943cff354ff233dc",
"url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/6b92469f8a7995e626da3aa487099617b8dfa260",
"reference": "6b92469f8a7995e626da3aa487099617b8dfa260",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"phpstan/phpstan": "^2.1.18"
"phpstan/phpstan": "^2.0.4"
},
"conflict": {
"phpunit/phpunit": "<7.0"
@ -1309,9 +1309,9 @@
"description": "PHPUnit extensions and rules for PHPStan",
"support": {
"issues": "https://github.com/phpstan/phpstan-phpunit/issues",
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.7"
"source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.6"
},
"time": "2025-07-13T11:31:46+00:00"
"time": "2025-03-26T12:47:06+00:00"
},
{
"name": "phpstan/phpstan-strict-rules",
@ -1684,16 +1684,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.55",
"version": "10.5.53",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "4b2d546b336876bd9562f24641b08a25335b06b6"
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b2d546b336876bd9562f24641b08a25335b06b6",
"reference": "4b2d546b336876bd9562f24641b08a25335b06b6",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653",
"reference": "32768472ebfb6969e6c7399f1c7b09009723f653",
"shasum": ""
},
"require": {
@ -1714,7 +1714,7 @@
"phpunit/php-timer": "^6.0.0",
"sebastian/cli-parser": "^2.0.1",
"sebastian/code-unit": "^2.0.0",
"sebastian/comparator": "^5.0.4",
"sebastian/comparator": "^5.0.3",
"sebastian/diff": "^5.1.1",
"sebastian/environment": "^6.1.0",
"sebastian/exporter": "^5.1.2",
@ -1765,7 +1765,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.55"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53"
},
"funding": [
{
@ -1789,7 +1789,7 @@
"type": "tidelift"
}
],
"time": "2025-09-14T06:19:20+00:00"
"time": "2025-08-20T14:40:06+00:00"
},
{
"name": "sebastian/cli-parser",
@ -1961,16 +1961,16 @@
},
{
"name": "sebastian/comparator",
"version": "5.0.4",
"version": "5.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e"
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
"reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e",
"shasum": ""
},
"require": {
@ -2026,27 +2026,15 @@
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"security": "https://github.com/sebastianbergmann/comparator/security/policy",
"source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4"
"source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
},
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{
"url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
"type": "tidelift"
}
],
"time": "2025-09-07T05:25:07+00:00"
"time": "2024-10-18T14:56:07+00:00"
},
{
"name": "sebastian/complexity",

View File

@ -50,6 +50,7 @@ use pocketmine\lang\Language;
use pocketmine\lang\LanguageNotFoundException;
use pocketmine\lang\Translatable;
use pocketmine\nbt\tag\CompoundTag;
use pocketmine\network\mcpe\auth\AuthKeyProvider;
use pocketmine\network\mcpe\compression\CompressBatchPromise;
use pocketmine\network\mcpe\compression\CompressBatchTask;
use pocketmine\network\mcpe\compression\Compressor;
@ -270,6 +271,7 @@ class Server{
private int $maxPlayers;
private bool $onlineMode = true;
private AuthKeyProvider $authKeyProvider;
private Network $network;
private bool $networkCompressionAsync = true;
@ -982,6 +984,8 @@ class Server{
$this->logger->warning($this->language->translate(KnownTranslationFactory::pocketmine_server_authProperty_disabled()));
}
$this->authKeyProvider = new AuthKeyProvider(new \PrefixedLogger($this->logger, "Minecraft Auth Key Provider"), $this->asyncPool);
if($this->configGroup->getConfigBool(ServerProperties::HARDCORE, false) && $this->getDifficulty() < World::DIFFICULTY_HARD){
$this->configGroup->setConfigInt(ServerProperties::DIFFICULTY, World::DIFFICULTY_HARD);
}
@ -1800,6 +1804,10 @@ class Server{
return $this->forceLanguage;
}
public function getAuthKeyProvider() : AuthKeyProvider{
return $this->authKeyProvider;
}
public function getNetwork() : Network{
return $this->network;
}

View File

@ -31,7 +31,7 @@ use function str_repeat;
final class VersionInfo{
public const NAME = "PocketMine-MP";
public const BASE_VERSION = "5.33.3";
public const BASE_VERSION = "5.33.2";
public const IS_DEVELOPMENT_BUILD = true;
public const BUILD_CHANNEL = "stable";

View File

@ -278,10 +278,6 @@ final class BlockStateUpgrader{
private function applyPropertyFlattened(BlockStateUpgradeSchemaFlattenInfo $flattenInfo, string $oldName, array $states) : array{
$flattenedValue = $states[$flattenInfo->flattenedProperty] ?? null;
$expectedType = $flattenInfo->flattenedPropertyType;
if($expectedType === null){
//TODO: we can't make this non-nullable in a patch release
throw new AssumptionFailedError("We never give this null");
}
if(!$flattenedValue instanceof $expectedType){
//flattened property is not of the expected type, so this transformation is not applicable
return [$oldName, $states];

View File

@ -64,7 +64,6 @@ use pocketmine\network\mcpe\protocol\types\inventory\WindowTypes;
use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\utils\ObjectSet;
use function array_fill_keys;
use function array_keys;
@ -420,15 +419,6 @@ class InventoryManager{
}
public function onClientRemoveWindow(int $id) : void{
if(Binary::signByte($id) === ContainerIds::NONE){ //TODO: REMOVE signByte() once BedrockProtocol + ext-encoding are implemented
//TODO: HACK! Since 1.21.100 (and probably earlier), the client will send -1 to close windows that it can't
//view for some reason, e.g. if the chat window was already open. This is pretty awkward, since it means
//that we can only assume it refers to the most recently sent window, and if we don't handle it,
//InventoryManager will never get the green light to send subsequent windows, which breaks inventory UIs.
//Fortunately, we already wait for close acks anyway, so the window ID is technically useless...?
$this->session->getLogger()->debug("Client rejected opening of a window, assuming it was $this->lastInventoryNetworkId");
$id = $this->lastInventoryNetworkId;
}
if($id === $this->lastInventoryNetworkId){
if(isset($this->networkIdToInventoryMap[$id]) && $id !== $this->pendingCloseWindowId){
$this->remove($id);

View File

@ -24,6 +24,7 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe;
use pocketmine\utils\AssumptionFailedError;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use function base64_decode;
@ -32,6 +33,7 @@ use function bin2hex;
use function chr;
use function count;
use function explode;
use function hex2bin;
use function is_array;
use function json_decode;
use function json_encode;
@ -54,6 +56,7 @@ use function strlen;
use function strtr;
use function substr;
use const JSON_THROW_ON_ERROR;
use const OPENSSL_ALGO_SHA256;
use const OPENSSL_ALGO_SHA384;
use const STR_PAD_LEFT;
@ -170,17 +173,17 @@ final class JwtUtils{
/**
* @throws JwtException
*/
public static function verify(string $jwt, \OpenSSLAsymmetricKey $signingKey) : bool{
public static function verify(string $jwt, string $signingKeyDer, bool $ec) : bool{
[$header, $body, $signature] = self::split($jwt);
$rawSignature = self::b64UrlDecode($signature);
$derSignature = self::rawSignatureToDer($rawSignature);
$derSignature = $ec ? self::rawSignatureToDer($rawSignature) : $rawSignature;
$v = openssl_verify(
$header . '.' . $body,
$derSignature,
$signingKey,
self::SIGNATURE_ALGORITHM
self::derPublicKeyToPem($signingKeyDer),
$ec ? self::SIGNATURE_ALGORITHM : OPENSSL_ALGO_SHA256
);
switch($v){
case 0: return false;
@ -238,22 +241,56 @@ final class JwtUtils{
throw new AssumptionFailedError("OpenSSL resource contains invalid public key");
}
/**
* DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
* {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
*/
private static function encodeDerLength(int $length) : string{
if ($length <= 0x7F) {
return chr($length);
}
$lengthBytes = ltrim(Binary::writeInt($length), "\x00");
return chr(0x80 | strlen($lengthBytes)) . $lengthBytes;
}
private static function encodeDerBytes(int $tag, string $data) : string{
return chr($tag) . self::encodeDerLength(strlen($data)) . $data;
}
public static function parseDerPublicKey(string $derKey) : \OpenSSLAsymmetricKey{
$signingKeyOpenSSL = openssl_pkey_get_public(sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey)));
$signingKeyOpenSSL = openssl_pkey_get_public(self::derPublicKeyToPem($derKey));
if($signingKeyOpenSSL === false){
throw new JwtException("OpenSSL failed to parse key: " . openssl_error_string());
}
$details = openssl_pkey_get_details($signingKeyOpenSSL);
if($details === false){
throw new JwtException("OpenSSL failed to get details from key: " . openssl_error_string());
}
if(!isset($details['ec']['curve_name'])){
throw new JwtException("Expected an EC key");
}
$curve = $details['ec']['curve_name'];
if($curve !== self::BEDROCK_SIGNING_KEY_CURVE_NAME){
throw new JwtException("Key must belong to curve " . self::BEDROCK_SIGNING_KEY_CURVE_NAME . ", got $curve");
}
return $signingKeyOpenSSL;
}
public static function derPublicKeyToPem(string $derKey) : string{
return sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey));
}
/**
* Create a public key represented in DER format from RSA modulus and exponent information
*
* @param string $nBase64 The RSA modulus encoded in Base64
* @param string $eBase64 The RSA exponent encoded in Base64
*/
public static function rsaPublicKeyModExpToDer(string $nBase64, string $eBase64) : string{
$mod = self::b64UrlDecode($nBase64);
$exp = self::b64UrlDecode($eBase64);
$modulus = self::encodeDerBytes(2, $mod);
$publicExponent = self::encodeDerBytes(2, $exp);
$rsaPublicKey = self::encodeDerBytes(48, $modulus . $publicExponent);
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
$rsaOID = hex2bin('300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
$rsaPublicKey = chr(0) . $rsaPublicKey;
$rsaPublicKey = self::encodeDerBytes(3, $rsaPublicKey);
return self::encodeDerBytes(48, $rsaOID . $rsaPublicKey);
}
}

View File

@ -0,0 +1,165 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\JwtBodyRfc7519;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthJwtBody;
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtBody;
use pocketmine\network\mcpe\protocol\types\login\SelfSignedJwtHeader;
use function base64_decode;
use function time;
final class AuthJwtHelper{
public const MOJANG_AUDIENCE = "api://auth-minecraft-services/multiplayer";
private const CLOCK_DRIFT_MAX = 60;
/**
* @throws VerifyLoginException if the token is expired or not yet valid
*/
private static function checkExpiry(JwtBodyRfc7519 $claims) : void{
$time = time();
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
}
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
}
}
/**
* @throws VerifyLoginException if errors are encountered
*/
public static function validateOpenIdAuthToken(string $jwt, string $signingKeyDer, string $issuer, string $audience) : XboxAuthJwtBody{
try{
if(!JwtUtils::verify($jwt, $signingKeyDer, ec: false)){
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
try{
[, $claimsArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
//nasty dynamic new for JsonMapper
$claims = $mapper->map($claimsArray, new XboxAuthJwtBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
if(!isset($claims->iss) || $claims->iss !== $issuer){
throw new VerifyLoginException("Invalid JWT issuer");
}
if(!isset($claims->aud) || $claims->aud !== $audience){
throw new VerifyLoginException("Invalid JWT audience");
}
self::checkExpiry($claims);
return $claims;
}
/**
* @throws VerifyLoginException if errors are encountered
*/
public static function validateLegacyAuthToken(string $jwt, ?string $expectedKeyDer) : LegacyAuthJwtBody{
self::validateSelfSignedToken($jwt, $expectedKeyDer);
//TODO: this parses the JWT twice and throws away a bunch of parts, optimize this
[, $claimsArray, ] = JwtUtils::parse($jwt);
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var LegacyAuthJwtBody $claims */
$claims = $mapper->map($claimsArray, new LegacyAuthJwtBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
self::checkExpiry($claims);
return $claims;
}
public static function validateSelfSignedToken(string $jwt, ?string $expectedKeyDer) : void{
try{
[$headersArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
try{
/** @var SelfSignedJwtHeader $headers */
$headers = $mapper->map($headersArray, new SelfSignedJwtHeader());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
}
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($expectedKeyDer !== null && $headerDerKey !== $expectedKeyDer){
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
try{
if(!JwtUtils::verify($jwt, $headerDerKey, ec: true)){
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
}
}

View File

@ -0,0 +1,164 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceKey;
use pocketmine\promise\Promise;
use pocketmine\promise\PromiseResolver;
use pocketmine\scheduler\AsyncPool;
use pocketmine\utils\AssumptionFailedError;
use function array_keys;
use function count;
use function implode;
use function time;
class AuthKeyProvider{
private const ALLOWED_REFRESH_INTERVAL = 30 * 60; // 30 minutes
private ?AuthKeyring $keyring = null;
/** @phpstan-var PromiseResolver<AuthKeyring> */
private ?PromiseResolver $resolver = null;
private int $lastFetch = 0;
public function __construct(
private readonly \Logger $logger,
private readonly AsyncPool $asyncPool,
private readonly int $keyRefreshIntervalSeconds = self::ALLOWED_REFRESH_INTERVAL
){}
/**
* Fetches the key for the given key ID.
* The promise will be resolved with an array of [issuer, pemPublicKey].
*
* @phpstan-return Promise<array{string, string}>
*/
public function getKey(string $keyId) : Promise{
/** @phpstan-var PromiseResolver<array{string, string}> $resolver */
$resolver = new PromiseResolver();
if(
$this->keyring === null || //we haven't fetched keys yet
($this->keyring->getKey($keyId) === null && $this->lastFetch < time() - $this->keyRefreshIntervalSeconds) //we don't recognise this one & keys might be outdated
){
//only refresh keys when we see one we don't recognise
$this->fetchKeys()->onCompletion(
onSuccess: fn(AuthKeyring $newKeyring) => $this->resolveKey($resolver, $newKeyring, $keyId),
onFailure: $resolver->reject(...)
);
}else{
$this->resolveKey($resolver, $this->keyring, $keyId);
}
return $resolver->getPromise();
}
/**
* @phpstan-param PromiseResolver<array{string, string}> $resolver
*/
private function resolveKey(PromiseResolver $resolver, AuthKeyring $keyring, string $keyId) : void{
$key = $keyring->getKey($keyId);
if($key === null){
$this->logger->debug("Key $keyId not recognised!");
$resolver->reject();
return;
}
$this->logger->debug("Key $keyId found in keychain");
$resolver->resolve([$keyring->getIssuer(), $key]);
}
/**
* @phpstan-param array<string, AuthServiceKey>|null $keys
* @phpstan-param string[]|null $errors
*/
private function onKeysFetched(?array $keys, string $issuer, ?array $errors) : void{
$resolver = $this->resolver;
if($resolver === null){
throw new AssumptionFailedError("Not expecting this to be called without a resolver present");
}
if($errors !== null){
$this->logger->error("The following errors occurred while fetching new keys:\n\t- " . implode("\n\t-", $errors));
//we might've still succeeded in fetching keys even if there were errors, so don't return
}
if($keys === null){
$this->logger->critical("Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!");
$resolver->reject();
}else{
$pemKeys = [];
foreach($keys as $keyModel){
if($keyModel->use !== "sig" || $keyModel->kty !== "RSA"){
$this->logger->error("Key ID $keyModel->kid doesn't have the expected properties: expected use=sig, kty=RSA, got use=$keyModel->use, kty=$keyModel->kty");
continue;
}
$derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e);
//make sure the key is valid
try{
JwtUtils::parseDerPublicKey($derKey);
}catch(JwtException $e){
$this->logger->error("Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage());
$this->logger->logException($e);
continue;
}
//retain PEM keys instead of OpenSSLAsymmetricKey since these are easier and cheaper to copy between threads
$pemKeys[$keyModel->kid] = $derKey;
}
if(count($keys) === 0){
$this->logger->critical("No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!");
$resolver->reject();
}else{
$this->logger->info("Successfully fetched " . count($keys) . " new authentication keys from issuer $issuer, key IDs: " . implode(", ", array_keys($pemKeys)));
$this->keyring = new AuthKeyring($issuer, $pemKeys);
$this->lastFetch = time();
$resolver->resolve($this->keyring);
}
}
}
/**
* @phpstan-return Promise<AuthKeyring>
*/
private function fetchKeys() : Promise{
if($this->resolver !== null){
$this->logger->debug("Key refresh was requested, but it's already in progress");
return $this->resolver->getPromise();
}
$this->logger->notice("Fetching new authentication keys");
/** @phpstan-var PromiseResolver<AuthKeyring> $resolver */
$resolver = new PromiseResolver();
$this->resolver = $resolver;
//TODO: extract this so it can be polyfilled for unit testing
$this->asyncPool->submitTask(new FetchAuthKeysTask($this->onKeysFetched(...)));
return $this->resolver->getPromise();
}
}

View File

@ -0,0 +1,45 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
final class AuthKeyring{
/**
* @param string[] $keys
* @phpstan-param array<string, string> $keys
*/
public function __construct(
private string $issuer,
private array $keys
){}
public function getIssuer() : string{ return $this->issuer; }
/**
* Returns a (raw) DER public key associated with the given key ID
*/
public function getKey(string $keyId) : ?string{
return $this->keys[$keyId] ?? null;
}
}

View File

@ -0,0 +1,209 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\network\mcpe\protocol\ProtocolInfo;
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceKey;
use pocketmine\network\mcpe\protocol\types\login\openid\api\AuthServiceOpenIdConfiguration;
use pocketmine\network\mcpe\protocol\types\login\openid\api\MinecraftServicesDiscovery;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use pocketmine\utils\Internet;
use function gettype;
use function is_array;
use function is_object;
use function json_decode;
use const JSON_THROW_ON_ERROR;
class FetchAuthKeysTask extends AsyncTask{
private const KEYS_ON_COMPLETION = "completion";
private const MINECRAFT_SERVICES_DISCOVERY_URL = "https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/" . ProtocolInfo::MINECRAFT_VERSION_NETWORK;
private const AUTHORIZATION_SERVICE_URI_FALLBACK = "https://authorization.franchise.minecraft-services.net";
private const AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH = "/.well-known/openid-configuration";
private const AUTHORIZATION_SERVICE_KEYS_PATH = "/.well-known/keys";
/** @phpstan-var ?NonThreadSafeValue<array<string, AuthServiceKey>> */
private ?NonThreadSafeValue $keys = null;
private string $issuer;
/** @phpstan-var ?NonThreadSafeValue<non-empty-array<string>> */
private ?NonThreadSafeValue $errors = null;
/**
* @phpstan-param \Closure(?array<string, AuthServiceKey> $keys, string $issuer, ?string[] $errors) : void $onCompletion
*/
public function __construct(
\Closure $onCompletion
){
$this->storeLocal(self::KEYS_ON_COMPLETION, $onCompletion);
}
public function onRun() : void{
/** @var string[] $errors */
$errors = [];
try{
$authServiceUri = $this->getAuthServiceURI();
}catch(\RuntimeException $e){
$errors[] = $e->getMessage();
$authServiceUri = self::AUTHORIZATION_SERVICE_URI_FALLBACK;
}
try {
$openIdConfig = $this->getOpenIdConfiguration($authServiceUri);
$jwksUri = $openIdConfig->jwks_uri;
$this->issuer = $openIdConfig->issuer;
} catch (\RuntimeException $e) {
$errors[] = $e->getMessage();
$jwksUri = $authServiceUri . self::AUTHORIZATION_SERVICE_KEYS_PATH;
$this->issuer = $authServiceUri;
}
try{
$this->keys = new NonThreadSafeValue($this->getKeys($jwksUri));
}catch(\RuntimeException $e){
$errors[] = $e->getMessage();
}
$this->errors = $errors === [] ? null : new NonThreadSafeValue($errors);
}
private function getAuthServiceURI() : string{
$result = Internet::getURL(self::MINECRAFT_SERVICES_DISCOVERY_URL);
if($result === null || $result->getCode() !== 200){
throw new \RuntimeException("Failed to fetch Minecraft services discovery document");
}
try{
$json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw new \RuntimeException($e->getMessage(), 0, $e);
}
if(!is_object($json)){
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var MinecraftServicesDiscovery $discovery */
$discovery = $mapper->map($json, new MinecraftServicesDiscovery());
}catch(\JsonMapper_Exception $e){
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
}
return $discovery->result->serviceEnvironments->auth->prod->serviceUri;
}
private function getOpenIdConfiguration(string $authServiceUri) : AuthServiceOpenIdConfiguration{
$result = Internet::getURL($authServiceUri . self::AUTHORIZATION_SERVICE_OPENID_CONFIGURATION_PATH);
if($result === null || $result->getCode() !== 200){
throw new \RuntimeException("Failed to fetch OpenID configuration from authorization service");
}
try{
$json = json_decode($result->getBody(), false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw new \RuntimeException($e->getMessage(), 0, $e);
}
if(!is_object($json)){
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var AuthServiceOpenIdConfiguration $configuration */
$configuration = $mapper->map($json, new AuthServiceOpenIdConfiguration());
}catch(\JsonMapper_Exception $e){
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
}
return $configuration;
}
/**
* @return array<string, AuthServiceKey> keys indexed by key ID
*/
private function getKeys(string $jwksUri) : array{
$result = Internet::getURL($jwksUri);
if($result === null || $result->getCode() !== 200){
return throw new \RuntimeException("Failed to fetch keys from authorization service");
}
try{
$json = json_decode($result->getBody(), true, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw new \RuntimeException($e->getMessage(), 0, $e);
}
if(!is_array($json) || !isset($json["keys"]) || !is_array($keysArray = $json["keys"])){
throw new \RuntimeException("Unexpected root type of schema file " . gettype($json) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
$keys = [];
foreach($keysArray as $keyJson){
if(!is_array($keyJson)){
throw new \RuntimeException("Unexpected key type in schema file: " . gettype($keyJson) . ", expected object");
}
try{
/** @var AuthServiceKey $key */
$key = $mapper->map($keyJson, new AuthServiceKey());
$keys[$key->kid] = $key;
}catch(\JsonMapper_Exception $e){
throw new \RuntimeException("Invalid schema file: " . $e->getMessage(), 0, $e);
}
}
return $keys;
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(?AuthServiceKey[] $keys, string $issuer, ?string[] $errors) : void $callback
*/
$callback = $this->fetchLocal(self::KEYS_ON_COMPLETION);
$callback($this->keys?->deserialize(), $this->issuer, $this->errors?->deserialize());
}
}

View File

@ -0,0 +1,121 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use pocketmine\utils\AssumptionFailedError;
use function base64_decode;
use function igbinary_serialize;
use function igbinary_unserialize;
class ProcessLegacyLoginTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion";
/**
* New Mojang root auth key. Mojang notified third-party developers of this change prior to the release of 1.20.0.
* Expectations were that this would be used starting a "couple of weeks" after the release, but as of 2023-07-01,
* it has not yet been deployed.
*/
public const LEGACY_MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
private string $chain;
/**
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
*/
private NonThreadSafeValue|string|null $error = "Unknown";
/** Whether the player has a certificate chain link signed by the given root public key. */
private bool $authenticated = false;
private ?string $clientPublicKeyDer = null;
/**
* @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
array $chainJwts,
private string $clientDataJwt,
private ?string $rootAuthKeyDer,
private bool $authRequired,
\Closure $onCompletion
){
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
$this->chain = igbinary_serialize($chainJwts) ?? throw new AssumptionFailedError("This should never fail");
}
public function onRun() : void{
try{
$this->clientPublicKeyDer = $this->validateChain();
AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $this->clientPublicKeyDer);
$this->error = null;
}catch(VerifyLoginException $e){
$disconnectMessage = $e->getDisconnectMessage();
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
}
}
private function validateChain() : string{
/** @var string[] $chain */
$chain = igbinary_unserialize($this->chain);
$identityPublicKeyDer = null;
foreach($chain as $jwt){
$claims = AuthJwtHelper::validateLegacyAuthToken($jwt, $identityPublicKeyDer);
if($this->rootAuthKeyDer !== null && $identityPublicKeyDer === $this->rootAuthKeyDer){
$this->authenticated = true; //we're signed into xbox live, according to this root key
}
if(!isset($claims->identityPublicKey)){
throw new VerifyLoginException("Missing identityPublicKey in chain link", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
}
$identityPublicKey = base64_decode($claims->identityPublicKey, true);
if($identityPublicKey === false){
throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding");
}
$identityPublicKeyDer = $identityPublicKey;
}
if($identityPublicKeyDer === null){
throw new VerifyLoginException("No authentication chain links provided");
}
return $identityPublicKeyDer;
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer);
}
}

View File

@ -1,213 +0,0 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\types\login\JwtChainLinkBody;
use pocketmine\network\mcpe\protocol\types\login\JwtHeader;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use function base64_decode;
use function igbinary_serialize;
use function igbinary_unserialize;
use function time;
class ProcessLoginTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion";
/**
* New Mojang root auth key. Mojang notified third-party developers of this change prior to the release of 1.20.0.
* Expectations were that this would be used starting a "couple of weeks" after the release, but as of 2023-07-01,
* it has not yet been deployed.
*/
public const MOJANG_ROOT_PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
private const CLOCK_DRIFT_MAX = 60;
private string $chain;
/**
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
*/
private NonThreadSafeValue|string|null $error = "Unknown";
/**
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
* root public key.
*/
private bool $authenticated = false;
private ?string $clientPublicKey = null;
/**
* @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
array $chainJwts,
private string $clientDataJwt,
private bool $authRequired,
\Closure $onCompletion
){
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
$this->chain = igbinary_serialize($chainJwts);
}
public function onRun() : void{
try{
$this->clientPublicKey = $this->validateChain();
$this->error = null;
}catch(VerifyLoginException $e){
$disconnectMessage = $e->getDisconnectMessage();
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
}
}
private function validateChain() : string{
/** @var string[] $chain */
$chain = igbinary_unserialize($this->chain);
$currentKey = null;
$first = true;
foreach($chain as $jwt){
$this->validateToken($jwt, $currentKey, $first);
if($first){
$first = false;
}
}
/** @var string $clientKey */
$clientKey = $currentKey;
$this->validateToken($this->clientDataJwt, $currentKey);
return $clientKey;
}
/**
* @throws VerifyLoginException if errors are encountered
*/
private function validateToken(string $jwt, ?string &$currentPublicKey, bool $first = false) : void{
try{
[$headersArray, $claimsArray, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw new VerifyLoginException("Failed to parse JWT: " . $e->getMessage(), null, 0, $e);
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
try{
/** @var JwtHeader $headers */
$headers = $mapper->map($headersArray, new JwtHeader());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), null, 0, $e);
}
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($currentPublicKey === null){
if(!$first){
throw new VerifyLoginException("Missing JWT public key", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
}
}elseif($headerDerKey !== $currentPublicKey){
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
try{
$signingKeyOpenSSL = JwtUtils::parseDerPublicKey($headerDerKey);
}catch(JwtException $e){
throw new VerifyLoginException("Invalid JWT public key: " . $e->getMessage(), null, 0, $e);
}
try{
if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
throw new VerifyLoginException("Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
}
}catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), null, 0, $e);
}
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
$this->authenticated = true; //we're signed into xbox live
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnUndefinedProperty = false; //we only care about the properties we're using in this case
$mapper->bExceptionOnMissingData = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
$mapper->bRemoveUndefinedAttributes = true;
try{
/** @var JwtChainLinkBody $claims */
$claims = $mapper->map($claimsArray, new JwtChainLinkBody());
}catch(\JsonMapper_Exception $e){
throw new VerifyLoginException("Invalid chain link body: " . $e->getMessage(), null, 0, $e);
}
$time = time();
if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
}
if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
throw new VerifyLoginException("JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
}
if(isset($claims->identityPublicKey)){
$identityPublicKey = base64_decode($claims->identityPublicKey, true);
if($identityPublicKey === false){
throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding");
}
try{
//verify key format and parameters
JwtUtils::parseDerPublicKey($identityPublicKey);
}catch(JwtException $e){
throw new VerifyLoginException("Invalid identityPublicKey: " . $e->getMessage(), null, 0, $e);
}
$currentPublicKey = $identityPublicKey; //if there are further links, the next link should be signed with this
}
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKey);
}
}

View File

@ -0,0 +1,98 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\auth;
use pocketmine\lang\Translatable;
use pocketmine\scheduler\AsyncTask;
use pocketmine\thread\NonThreadSafeValue;
use function base64_decode;
class ProcessOpenIdLoginTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion";
public const MOJANG_AUDIENCE = "api://auth-minecraft-services/multiplayer";
/**
* Whether the keychain signatures were validated correctly. This will be set to an error message if any link in the
* keychain is invalid for whatever reason (bad signature, not in nbf-exp window, etc). If this is non-null, the
* keychain might have been tampered with. The player will always be disconnected if this is non-null.
*
* @phpstan-var NonThreadSafeValue<Translatable>|string|null
*/
private NonThreadSafeValue|string|null $error = "Unknown";
/**
* Whether the player is logged into Xbox Live. This is true if any link in the keychain is signed with the Mojang
* root public key.
*/
private bool $authenticated = false;
private ?string $clientPublicKeyDer = null;
/**
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, Translatable|string|null $error, ?string $clientPublicKey) : void $onCompletion
*/
public function __construct(
private string $jwt,
private string $issuer,
private string $mojangPublicKeyDer,
private string $clientDataJwt,
private bool $authRequired,
\Closure $onCompletion
){
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
}
public function onRun() : void{
try{
$this->clientPublicKeyDer = $this->validateChain();
$this->error = null;
}catch(VerifyLoginException $e){
$disconnectMessage = $e->getDisconnectMessage();
$this->error = $disconnectMessage instanceof Translatable ? new NonThreadSafeValue($disconnectMessage) : $disconnectMessage;
}
}
private function validateChain() : string{
$claims = AuthJwtHelper::validateOpenIdAuthToken($this->jwt, $this->mojangPublicKeyDer, issuer: $this->issuer, audience: self::MOJANG_AUDIENCE);
//validateToken will throw if the JWT is not valid
$this->authenticated = true;
$clientDerKey = base64_decode($claims->cpk, strict: true);
if($clientDerKey === false){
throw new VerifyLoginException("Invalid client public key: base64 error decoding");
}
//no further validation needed - OpenSSL will bail if the key is invalid
AuthJwtHelper::validateSelfSignedToken($this->clientDataJwt, $clientDerKey);
return $clientDerKey;
}
public function onCompletion() : void{
/**
* @var \Closure $callback
* @phpstan-var \Closure(bool, bool, Translatable|string|null, ?string) : void $callback
*/
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error instanceof NonThreadSafeValue ? $this->error->deserialize() : $this->error, $this->clientPublicKeyDer);
}
}

View File

@ -89,7 +89,6 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds;
use pocketmine\network\mcpe\protocol\types\inventory\MismatchTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\NetworkInventoryAction;
use pocketmine\network\mcpe\protocol\types\inventory\NormalTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\PredictedResult;
use pocketmine\network\mcpe\protocol\types\inventory\ReleaseItemTransactionData;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\ItemStackRequest;
use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResponse;
@ -499,13 +498,11 @@ class InGamePacketHandler extends PacketHandler{
$blockPos = $data->getBlockPosition();
$vBlockPos = new Vector3($blockPos->getX(), $blockPos->getY(), $blockPos->getZ());
$this->player->interactBlock($vBlockPos, $data->getFace(), $clickPos);
if($data->getClientInteractPrediction() === PredictedResult::SUCCESS){
//always sync this in case plugins caused a different result than the client expected
//we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating
//more information up the stack. For now I think this is good enough.
//if only the client would tell us what blocks it thinks changed...
$this->syncBlocksNearby($vBlockPos, $data->getFace());
}
//always sync this in case plugins caused a different result than the client expected
//we *could* try to enhance detection of plugin-altered behaviour, but this would require propagating
//more information up the stack. For now I think this is good enough.
//if only the client would tell us what blocks it thinks changed...
$this->syncBlocksNearby($vBlockPos, $data->getFace());
return true;
case UseItemTransactionData::ACTION_CLICK_AIR:
if($this->player->isUsingItem()){
@ -720,8 +717,7 @@ class InGamePacketHandler extends PacketHandler{
case PlayerAction::INTERACT_BLOCK: //TODO: ignored (for now)
break;
case PlayerAction::CREATIVE_PLAYER_DESTROY_BLOCK:
//in server auth block breaking, we get PREDICT_DESTROY_BLOCK anyway, so this action is redundant
break;
//TODO: do we need to handle this?
case PlayerAction::PREDICT_DESTROY_BLOCK:
self::validateFacing($face);
if(!$this->player->breakBlock($pos)){

View File

@ -27,27 +27,35 @@ use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerPreLoginEvent;
use pocketmine\lang\KnownTranslationFactory;
use pocketmine\lang\Translatable;
use pocketmine\network\mcpe\auth\ProcessLoginTask;
use pocketmine\network\mcpe\auth\ProcessLegacyLoginTask;
use pocketmine\network\mcpe\auth\ProcessOpenIdLoginTask;
use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\NetworkSession;
use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationInfo;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationType;
use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\ClientDataToSkinDataHelper;
use pocketmine\network\mcpe\protocol\types\login\JwtChain;
use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientData;
use pocketmine\network\mcpe\protocol\types\login\clientdata\ClientDataToSkinDataHelper;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthChain;
use pocketmine\network\mcpe\protocol\types\login\legacy\LegacyAuthIdentityData;
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtBody;
use pocketmine\network\mcpe\protocol\types\login\openid\XboxAuthJwtHeader;
use pocketmine\network\PacketHandlingException;
use pocketmine\player\Player;
use pocketmine\player\PlayerInfo;
use pocketmine\player\XboxLivePlayerInfo;
use pocketmine\Server;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use function chr;
use function count;
use function gettype;
use function is_array;
use function is_object;
use function json_decode;
use function md5;
use function ord;
use const JSON_THROW_ON_ERROR;
/**
@ -65,15 +73,95 @@ class LoginPacketHandler extends PacketHandler{
private \Closure $authCallback
){}
private static function calculateUuidFromXuid(string $xuid) : UuidInterface{
$hash = md5("pocket-auth-1-xuid:" . $xuid, binary: true);
$hash[6] = chr((ord($hash[6]) & 0x0f) | 0x30); // set version to 3
$hash[8] = chr((ord($hash[8]) & 0x3f) | 0x80); // set variant to RFC 4122
return Uuid::fromBytes($hash);
}
public function handleLogin(LoginPacket $packet) : bool{
$authInfo = $this->parseAuthInfo($packet->authInfoJson);
$jwtChain = $this->parseJwtChain($authInfo->Certificate);
$extraData = $this->fetchAuthData($jwtChain);
if(!Player::isValidUserName($extraData->displayName)){
if($authInfo->AuthenticationType === AuthenticationType::FULL->value){
try{
[$headerArray, $claimsArray,] = JwtUtils::parse($authInfo->Token);
}catch(JwtException $e){
throw PacketHandlingException::wrap($e, "Error parsing authentication token");
}
$header = $this->mapXboxTokenHeader($headerArray);
$claims = $this->mapXboxTokenBody($claimsArray);
$legacyUuid = self::calculateUuidFromXuid($claims->xid);
$username = $claims->xname;
$xuid = $claims->xid;
$authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
if($authRequired === null){
//plugin cancelled
return true;
}
$this->processOpenIdLogin($authInfo->Token, $header->kid, $packet->clientDataJwt, $authRequired);
}elseif($authInfo->AuthenticationType === AuthenticationType::SELF_SIGNED->value){
try{
$chainData = json_decode($authInfo->Certificate, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate chain");
}
if(!is_object($chainData)){
throw new PacketHandlingException("Unexpected type for self-signed certificate chain: " . gettype($chainData) . ", expected object");
}
try{
$chain = $this->defaultJsonMapper()->map($chainData, new LegacyAuthChain());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate chain");
}
if(count($chain->chain) > 1 || !isset($chain->chain[0])){
throw new PacketHandlingException("Expected exactly one certificate in self-signed certificate chain, got " . count($chain->chain));
}
try{
[, $claimsArray, ] = JwtUtils::parse($chain->chain[0]);
}catch(JwtException $e){
throw PacketHandlingException::wrap($e, "Error parsing self-signed certificate");
}
if(!isset($claimsArray["extraData"]) || !is_array($claimsArray["extraData"])){
throw new PacketHandlingException("Expected \"extraData\" to be present in self-signed certificate");
}
try{
$claims = $this->defaultJsonMapper()->map($claimsArray["extraData"], new LegacyAuthIdentityData());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e, "Error mapping self-signed certificate extraData");
}
if(!Uuid::isValid($claims->identity)){
throw new PacketHandlingException("Invalid UUID string in self-signed certificate: " . $claims->identity);
}
$legacyUuid = Uuid::fromString($claims->identity);
$username = $claims->displayName;
$xuid = "";
$authRequired = $this->processLoginCommon($packet, $username, $legacyUuid, $xuid);
if($authRequired === null){
//plugin cancelled
return true;
}
$this->processSelfSignedLogin($chain->chain, $packet->clientDataJwt, $authRequired);
}else{
throw new PacketHandlingException("Unsupported authentication type: $authInfo->AuthenticationType");
}
return true;
}
private function processLoginCommon(LoginPacket $packet, string $username, UuidInterface $legacyUuid, string $xuid) : ?bool{
if(!Player::isValidUserName($username)){
$this->session->disconnectWithError(KnownTranslationFactory::disconnectionScreen_invalidName());
return true;
return null;
}
$clientData = $this->parseClientData($packet->clientDataJwt);
@ -86,32 +174,25 @@ class LoginPacketHandler extends PacketHandler{
disconnectScreenMessage: KnownTranslationFactory::disconnectionScreen_invalidSkin()
);
return true;
return null;
}
if(!Uuid::isValid($extraData->identity)){
throw new PacketHandlingException("Invalid login UUID");
}
$uuid = Uuid::fromString($extraData->identity);
$arrClientData = (array) $clientData;
$arrClientData["TitleID"] = $extraData->titleId;
if($extraData->XUID !== ""){
if($xuid !== ""){
$playerInfo = new XboxLivePlayerInfo(
$extraData->XUID,
$extraData->displayName,
$uuid,
$xuid,
$username,
$legacyUuid,
$skin,
$clientData->LanguageCode,
$arrClientData
(array) $clientData
);
}else{
$playerInfo = new PlayerInfo(
$extraData->displayName,
$uuid,
$username,
$legacyUuid,
$skin,
$clientData->LanguageCode,
$arrClientData
(array) $clientData
);
}
($this->playerInfoConsumer)($playerInfo);
@ -144,12 +225,10 @@ class LoginPacketHandler extends PacketHandler{
$ev->call();
if(!$ev->isAllowed()){
$this->session->disconnect($ev->getFinalDisconnectReason(), $ev->getFinalDisconnectScreenMessage());
return true;
return null;
}
$this->processLogin($authInfo->Token, AuthenticationType::from($authInfo->AuthenticationType), $jwtChain->chain, $packet->clientDataJwt, $ev->isAuthRequired());
return true;
return $ev->isAuthRequired();
}
/**
@ -162,13 +241,10 @@ class LoginPacketHandler extends PacketHandler{
throw PacketHandlingException::wrap($e);
}
if(!is_object($authInfoJson)){
throw new \RuntimeException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
throw new PacketHandlingException("Unexpected type for auth info data: " . gettype($authInfoJson) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper = $this->defaultJsonMapper();
try{
$clientData = $mapper->map($authInfoJson, new AuthenticationInfo());
}catch(\JsonMapper_Exception $e){
@ -178,68 +254,31 @@ class LoginPacketHandler extends PacketHandler{
}
/**
* @param array<string, mixed> $headerArray
* @throws PacketHandlingException
*/
protected function parseJwtChain(string $chainDataJwt) : JwtChain{
protected function mapXboxTokenHeader(array $headerArray) : XboxAuthJwtHeader{
$mapper = $this->defaultJsonMapper();
try{
$jwtChainJson = json_decode($chainDataJwt, associative: false, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
throw PacketHandlingException::wrap($e);
}
if(!is_object($jwtChainJson)){
throw new \RuntimeException("Unexpected type for JWT chain data: " . gettype($jwtChainJson) . ", expected object");
}
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{
$clientData = $mapper->map($jwtChainJson, new JwtChain());
$header = $mapper->map($headerArray, new XboxAuthJwtHeader());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
return $clientData;
return $header;
}
/**
* @param array<string, mixed> $bodyArray
* @throws PacketHandlingException
*/
protected function fetchAuthData(JwtChain $chain) : AuthenticationData{
/** @var AuthenticationData|null $extraData */
$extraData = null;
foreach($chain->chain as $jwt){
//validate every chain element
try{
[, $claims, ] = JwtUtils::parse($jwt);
}catch(JwtException $e){
throw PacketHandlingException::wrap($e);
}
if(isset($claims["extraData"])){
if($extraData !== null){
throw new PacketHandlingException("Found 'extraData' more than once in chainData");
}
if(!is_array($claims["extraData"])){
throw new PacketHandlingException("'extraData' key should be an array");
}
$mapper = new \JsonMapper();
$mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
try{
/** @var AuthenticationData $extraData */
$extraData = $mapper->map($claims["extraData"], new AuthenticationData());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
}
protected function mapXboxTokenBody(array $bodyArray) : XboxAuthJwtBody{
$mapper = $this->defaultJsonMapper();
try{
$header = $mapper->map($bodyArray, new XboxAuthJwtBody());
}catch(\JsonMapper_Exception $e){
throw PacketHandlingException::wrap($e);
}
if($extraData === null){
throw new PacketHandlingException("'extraData' not found in chain data");
}
return $extraData;
return $header;
}
/**
@ -252,11 +291,7 @@ class LoginPacketHandler extends PacketHandler{
throw PacketHandlingException::wrap($e);
}
$mapper = new \JsonMapper();
$mapper->bEnforceMapType = false; //TODO: we don't really need this as an array, but right now we don't have enough models
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper = $this->defaultJsonMapper();
try{
$clientData = $mapper->map($clientDataClaims, new ClientData());
}catch(\JsonMapper_Exception $e){
@ -269,15 +304,37 @@ class LoginPacketHandler extends PacketHandler{
* TODO: This is separated for the purposes of allowing plugins (like Specter) to hack it and bypass authentication.
* In the future this won't be necessary.
*
* @param null|string[] $legacyCertificate
*
* @throws \InvalidArgumentException
*/
protected function processLogin(string $token, AuthenticationType $authType, ?array $legacyCertificate, string $clientData, bool $authRequired) : void{
if($legacyCertificate === null){
throw new PacketHandlingException("Legacy certificate cannot be null");
}
$this->server->getAsyncPool()->submitTask(new ProcessLoginTask($legacyCertificate, $clientData, $authRequired, $this->authCallback));
protected function processOpenIdLogin(string $token, string $keyId, string $clientData, bool $authRequired) : void{
$this->session->setHandler(null); //drop packets received during login verification
$authKeyProvider = $this->server->getAuthKeyProvider();
$authKeyProvider->getKey($keyId)->onCompletion(
function(array $issuerAndKey) use ($token, $clientData, $authRequired) : void{
[$issuer, $mojangPublicKeyPem] = $issuerAndKey;
$this->server->getAsyncPool()->submitTask(new ProcessOpenIdLoginTask($token, $issuer, $mojangPublicKeyPem, $clientData, $authRequired, $this->authCallback));
},
fn() => ($this->authCallback)(false, $authRequired, "Unrecognized authentication key ID: $keyId", null)
);
}
/**
* @param string[] $legacyCertificate
*/
protected function processSelfSignedLogin(array $legacyCertificate, string $clientDataJwt, bool $authRequired) : void{
$this->session->setHandler(null); //drop packets received during login verification
$this->server->getAsyncPool()->submitTask(new ProcessLegacyLoginTask($legacyCertificate, $clientDataJwt, rootAuthKeyDer: null, authRequired: $authRequired, onCompletion: $this->authCallback));
}
private function defaultJsonMapper() : \JsonMapper{
$mapper = new \JsonMapper();
$mapper->bExceptionOnMissingData = true;
$mapper->bExceptionOnUndefinedProperty = true;
$mapper->bStrictObjectTypeChecking = true;
$mapper->bEnforceMapType = false;
return $mapper;
}
}

View File

@ -870,12 +870,6 @@ parameters:
count: 1
path: ../../../src/network/mcpe/NetworkSession.php
-
message: '#^Property pocketmine\\network\\mcpe\\auth\\ProcessLoginTask\:\:\$chain \(string\) does not accept string\|null\.$#'
identifier: assign.propertyType
count: 1
path: ../../../src/network/mcpe/auth/ProcessLoginTask.php
-
message: '#^Parameter \#1 \$result of method pocketmine\\network\\mcpe\\compression\\CompressBatchPromise\:\:resolve\(\) expects string, mixed given\.$#'
identifier: argument.type
@ -948,12 +942,6 @@ parameters:
count: 1
path: ../../../src/plugin/PluginDescription.php
-
message: '#^Property pocketmine\\plugin\\PluginDescription\:\:\$srcNamespacePrefix \(string\) does not accept mixed\.$#'
identifier: assign.propertyType
count: 1
path: ../../../src/plugin/PluginDescription.php
-
message: '#^Cannot call method addChild\(\) on pocketmine\\permission\\Permission\|null\.$#'
identifier: method.nonObject
@ -996,12 +984,6 @@ parameters:
count: 1
path: ../../../src/scheduler/TaskScheduler.php
-
message: '#^Possibly invalid array key type mixed\.$#'
identifier: offsetAccess.invalidOffset
count: 1
path: ../../../src/thread/ThreadManager.php
-
message: '#^Cannot access offset string on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible

View File

@ -192,6 +192,18 @@ parameters:
count: 1
path: ../../../src/network/mcpe/cache/CraftingDataCache.php
-
message: '#^Method pocketmine\\network\\mcpe\\compression\\ZlibCompressor\:\:getNetworkId\(\) never returns 1 so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: ../../../src/network/mcpe/compression/ZlibCompressor.php
-
message: '#^Method pocketmine\\network\\mcpe\\compression\\ZlibCompressor\:\:getNetworkId\(\) never returns 255 so it can be removed from the return type\.$#'
identifier: return.unusedType
count: 1
path: ../../../src/network/mcpe/compression/ZlibCompressor.php
-
message: '#^Parameter \#1 \$states of class pocketmine\\network\\mcpe\\convert\\BlockStateDictionary constructor expects list\<pocketmine\\network\\mcpe\\convert\\BlockStateDictionaryEntry\>, array\<int\<0, max\>, pocketmine\\network\\mcpe\\convert\\BlockStateDictionaryEntry\> given\.$#'
identifier: argument.type

View File

@ -30,26 +30,3 @@ parameters:
count: 4
path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q00 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q01 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q10 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php
-
message: '#^Parameter \$q11 of static method pocketmine\\world\\generator\\noise\\Noise\:\:bilinearLerp\(\) expects float, float\|null given\.$#'
identifier: argument.type
count: 1
path: ../../../src/world/generator/noise/Noise.php

View File

@ -47,7 +47,7 @@ final class CloningRegistryTraitTest extends TestCase{
public function testGetAllClone() : void{
$list1 = TestCloningRegistry::getAll();
$list2 = TestCloningRegistry::getAll();
foreach(Utils::stringifyKeys($list1) as $k => $member){
foreach(Utils::promoteKeys($list1) as $k => $member){
self::assertNotSame($member, $list2[$k], "VanillaBlocks ought to clone its members");
}
}

View File

@ -354,9 +354,6 @@ function processStateGroup(string $oldName, array $upgradeTable, BlockStateUpgra
* @param string[] $strings
*/
function findCommonPrefix(array $strings) : string{
if(count($strings) === 0){
return "";
}
sort($strings, SORT_STRING);
$first = $strings[array_key_first($strings)];