diff --git a/src/network/mcpe/JwtUtils.php b/src/network/mcpe/JwtUtils.php new file mode 100644 index 000000000..1ef186ef2 --- /dev/null +++ b/src/network/mcpe/JwtUtils.php @@ -0,0 +1,66 @@ + + * + * @throws \UnexpectedValueException + */ + public static function getClaims(string $token) : array{ + $v = explode(".", $token); + if(count($v) !== 3){ + throw new \UnexpectedValueException("Expected exactly 3 JWT parts, got " . count($v)); + } + $payloadB64 = $v[1]; + $payloadJSON = self::b64UrlDecode($payloadB64); + if($payloadJSON === false){ + throw new \UnexpectedValueException("Invalid base64 JWT payload"); + } + $result = json_decode($payloadJSON, true); + if(!is_array($result)){ + throw new \UnexpectedValueException("Failed to decode JWT payload JSON: " . json_last_error_msg()); + } + + return $result; + } + + public static function b64UrlEncode(string $str) : string{ + return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); + } + + public static function b64UrlDecode(string $str) : string{ + if(($len = strlen($str) % 4) !== 0){ + $str .= str_repeat('=', 4 - $len); + } + $decoded = base64_decode(strtr($str, '-_', '+/'), true); + if($decoded === false){ + throw new \UnexpectedValueException("Malformed base64url encoded payload could not be decoded"); + } + return $decoded; + } +} diff --git a/src/network/mcpe/auth/ProcessLoginTask.php b/src/network/mcpe/auth/ProcessLoginTask.php index 73c0b31b6..3fcc730a1 100644 --- a/src/network/mcpe/auth/ProcessLoginTask.php +++ b/src/network/mcpe/auth/ProcessLoginTask.php @@ -28,6 +28,7 @@ use Mdanter\Ecc\Crypto\Signature\Signature; use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer; use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; +use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\protocol\LoginPacket; use pocketmine\scheduler\AsyncTask; use function assert; @@ -124,11 +125,11 @@ class ProcessLoginTask extends AsyncTask{ } //First link, check that it is self-signed - $headers = json_decode(self::b64UrlDecode($headB64), true); + $headers = json_decode(JwtUtils::b64UrlDecode($headB64), true); $currentPublicKey = $headers["x5u"]; } - $plainSignature = self::b64UrlDecode($sigB64); + $plainSignature = JwtUtils::b64UrlDecode($sigB64); assert(strlen($plainSignature) === 96); [$rString, $sString] = str_split($plainSignature, 48); $sig = new Signature(gmp_init(bin2hex($rString), 16), gmp_init(bin2hex($sString), 16)); @@ -149,7 +150,7 @@ class ProcessLoginTask extends AsyncTask{ $this->authenticated = true; //we're signed into xbox live } - $claims = json_decode(self::b64UrlDecode($payloadB64), true); + $claims = json_decode(JwtUtils::b64UrlDecode($payloadB64), true); $time = time(); if(isset($claims["nbf"]) and $claims["nbf"] > $time + self::CLOCK_DRIFT_MAX){ @@ -163,13 +164,6 @@ class ProcessLoginTask extends AsyncTask{ $currentPublicKey = $claims["identityPublicKey"] ?? null; //if there are further links, the next link should be signed with this } - private static function b64UrlDecode(string $str) : string{ - if(($len = strlen($str) % 4) !== 0){ - $str .= str_repeat('=', 4 - $len); - } - return base64_decode(strtr($str, '-_', '+/'), true); - } - public function onCompletion() : void{ /** * @var \Closure $callback diff --git a/src/network/mcpe/encryption/EncryptionUtils.php b/src/network/mcpe/encryption/EncryptionUtils.php index 38fe0e95a..a396c8ab1 100644 --- a/src/network/mcpe/encryption/EncryptionUtils.php +++ b/src/network/mcpe/encryption/EncryptionUtils.php @@ -29,15 +29,14 @@ use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer; use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; +use pocketmine\network\mcpe\JwtUtils; use function base64_encode; use function gmp_strval; use function hex2bin; use function json_encode; use function openssl_digest; use function openssl_sign; -use function rtrim; use function str_pad; -use function strtr; final class EncryptionUtils{ @@ -45,10 +44,6 @@ final class EncryptionUtils{ //NOOP } - private static function b64UrlEncode(string $str) : string{ - return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); - } - public static function generateSharedSecret(PrivateKeyInterface $localPriv, PublicKeyInterface $remotePub) : \GMP{ return $localPriv->createExchange($remotePub)->calculateSharedKey(); } @@ -58,11 +53,11 @@ final class EncryptionUtils{ } public static function generateServerHandshakeJwt(PrivateKeyInterface $serverPriv, string $salt) : string{ - $jwtBody = self::b64UrlEncode(json_encode([ + $jwtBody = JwtUtils::b64UrlEncode(json_encode([ "x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())), "alg" => "ES384" ]) - ) . "." . self::b64UrlEncode(json_encode([ + ) . "." . JwtUtils::b64UrlEncode(json_encode([ "salt" => base64_encode($salt) ]) ); @@ -70,11 +65,11 @@ final class EncryptionUtils{ openssl_sign($jwtBody, $sig, (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($serverPriv), OPENSSL_ALGO_SHA384); $decodedSig = (new DerSignatureSerializer())->parse($sig); - $jwtSig = self::b64UrlEncode( + $jwtSig = JwtUtils::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"; } -} \ No newline at end of file +} diff --git a/src/network/mcpe/protocol/LoginPacket.php b/src/network/mcpe/protocol/LoginPacket.php index ca86b98b3..6ca62209a 100644 --- a/src/network/mcpe/protocol/LoginPacket.php +++ b/src/network/mcpe/protocol/LoginPacket.php @@ -25,13 +25,13 @@ namespace pocketmine\network\mcpe\protocol; #include +use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\protocol\serializer\NetworkBinaryStream; use pocketmine\network\mcpe\protocol\types\login\AuthenticationData; use pocketmine\network\mcpe\protocol\types\login\ClientData; use pocketmine\network\mcpe\protocol\types\login\JwtChain; use pocketmine\utils\BinaryDataException; use pocketmine\utils\BinaryStream; -use pocketmine\utils\Utils; use function is_array; use function json_decode; @@ -83,7 +83,7 @@ class LoginPacket extends DataPacket implements ServerboundPacket{ foreach($this->chainDataJwt->chain as $k => $chain){ //validate every chain element try{ - $claims = Utils::getJwtClaims($chain); + $claims = JwtUtils::getClaims($chain); }catch(\UnexpectedValueException $e){ throw new PacketDecodeException($e->getMessage(), 0, $e); } @@ -112,7 +112,7 @@ class LoginPacket extends DataPacket implements ServerboundPacket{ $this->clientDataJwt = $buffer->get($buffer->getLInt()); try{ - $clientData = Utils::getJwtClaims($this->clientDataJwt); + $clientData = JwtUtils::getClaims($this->clientDataJwt); }catch(\UnexpectedValueException $e){ throw new PacketDecodeException($e->getMessage(), 0, $e); } diff --git a/src/utils/Utils.php b/src/utils/Utils.php index 1536baca6..4fe1cd973 100644 --- a/src/utils/Utils.php +++ b/src/utils/Utils.php @@ -361,30 +361,6 @@ class Utils{ return $hash; } - /** - * @return mixed[] array of claims - * @phpstan-return array - * - * @throws \UnexpectedValueException - */ - public static function getJwtClaims(string $token) : array{ - $v = explode(".", $token); - if(count($v) !== 3){ - throw new \UnexpectedValueException("Expected exactly 3 JWT parts, got " . count($v)); - } - $payloadB64 = $v[1]; - $payloadJSON = base64_decode(strtr($payloadB64, '-_', '+/'), true); - if($payloadJSON === false){ - throw new \UnexpectedValueException("Invalid base64 JWT payload"); - } - $result = json_decode($payloadJSON, true); - if(!is_array($result)){ - throw new \UnexpectedValueException("Failed to decode JWT payload JSON: " . json_last_error_msg()); - } - - return $result; - } - /** * @param object $value */