consolidate some JWT handling into one class

This commit is contained in:
Dylan K. Taylor 2020-05-06 21:32:22 +01:00
parent 5d154e43a9
commit ed757c7207
5 changed files with 78 additions and 47 deletions

View File

@ -0,0 +1,66 @@
<?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;
final class JwtUtils{
/**
* @return mixed[] array of claims
* @phpstan-return array<string, mixed>
*
* @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;
}
}

View File

@ -28,6 +28,7 @@ use Mdanter\Ecc\Crypto\Signature\Signature;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer;
use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\LoginPacket; use pocketmine\network\mcpe\protocol\LoginPacket;
use pocketmine\scheduler\AsyncTask; use pocketmine\scheduler\AsyncTask;
use function assert; use function assert;
@ -124,11 +125,11 @@ class ProcessLoginTask extends AsyncTask{
} }
//First link, check that it is self-signed //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"]; $currentPublicKey = $headers["x5u"];
} }
$plainSignature = self::b64UrlDecode($sigB64); $plainSignature = JwtUtils::b64UrlDecode($sigB64);
assert(strlen($plainSignature) === 96); assert(strlen($plainSignature) === 96);
[$rString, $sString] = str_split($plainSignature, 48); [$rString, $sString] = str_split($plainSignature, 48);
$sig = new Signature(gmp_init(bin2hex($rString), 16), gmp_init(bin2hex($sString), 16)); $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 $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(); $time = time();
if(isset($claims["nbf"]) and $claims["nbf"] > $time + self::CLOCK_DRIFT_MAX){ 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 $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{ public function onCompletion() : void{
/** /**
* @var \Closure $callback * @var \Closure $callback

View File

@ -29,15 +29,14 @@ use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer; use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer; use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer; use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
use pocketmine\network\mcpe\JwtUtils;
use function base64_encode; use function base64_encode;
use function gmp_strval; use function gmp_strval;
use function hex2bin; use function hex2bin;
use function json_encode; use function json_encode;
use function openssl_digest; use function openssl_digest;
use function openssl_sign; use function openssl_sign;
use function rtrim;
use function str_pad; use function str_pad;
use function strtr;
final class EncryptionUtils{ final class EncryptionUtils{
@ -45,10 +44,6 @@ final class EncryptionUtils{
//NOOP //NOOP
} }
private static function b64UrlEncode(string $str) : string{
return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
}
public static function generateSharedSecret(PrivateKeyInterface $localPriv, PublicKeyInterface $remotePub) : \GMP{ public static function generateSharedSecret(PrivateKeyInterface $localPriv, PublicKeyInterface $remotePub) : \GMP{
return $localPriv->createExchange($remotePub)->calculateSharedKey(); return $localPriv->createExchange($remotePub)->calculateSharedKey();
} }
@ -58,11 +53,11 @@ final class EncryptionUtils{
} }
public static function generateServerHandshakeJwt(PrivateKeyInterface $serverPriv, string $salt) : string{ 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())), "x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())),
"alg" => "ES384" "alg" => "ES384"
]) ])
) . "." . self::b64UrlEncode(json_encode([ ) . "." . JwtUtils::b64UrlEncode(json_encode([
"salt" => base64_encode($salt) "salt" => base64_encode($salt)
]) ])
); );
@ -70,7 +65,7 @@ final class EncryptionUtils{
openssl_sign($jwtBody, $sig, (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($serverPriv), OPENSSL_ALGO_SHA384); openssl_sign($jwtBody, $sig, (new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($serverPriv), OPENSSL_ALGO_SHA384);
$decodedSig = (new DerSignatureSerializer())->parse($sig); $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->getR(), 16), 96, "0", STR_PAD_LEFT)) .
hex2bin(str_pad(gmp_strval($decodedSig->getS(), 16), 96, "0", STR_PAD_LEFT)) hex2bin(str_pad(gmp_strval($decodedSig->getS(), 16), 96, "0", STR_PAD_LEFT))
); );

View File

@ -25,13 +25,13 @@ namespace pocketmine\network\mcpe\protocol;
#include <rules/DataPacket.h> #include <rules/DataPacket.h>
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\network\mcpe\protocol\serializer\NetworkBinaryStream; use pocketmine\network\mcpe\protocol\serializer\NetworkBinaryStream;
use pocketmine\network\mcpe\protocol\types\login\AuthenticationData; use pocketmine\network\mcpe\protocol\types\login\AuthenticationData;
use pocketmine\network\mcpe\protocol\types\login\ClientData; use pocketmine\network\mcpe\protocol\types\login\ClientData;
use pocketmine\network\mcpe\protocol\types\login\JwtChain; use pocketmine\network\mcpe\protocol\types\login\JwtChain;
use pocketmine\utils\BinaryDataException; use pocketmine\utils\BinaryDataException;
use pocketmine\utils\BinaryStream; use pocketmine\utils\BinaryStream;
use pocketmine\utils\Utils;
use function is_array; use function is_array;
use function json_decode; use function json_decode;
@ -83,7 +83,7 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
foreach($this->chainDataJwt->chain as $k => $chain){ foreach($this->chainDataJwt->chain as $k => $chain){
//validate every chain element //validate every chain element
try{ try{
$claims = Utils::getJwtClaims($chain); $claims = JwtUtils::getClaims($chain);
}catch(\UnexpectedValueException $e){ }catch(\UnexpectedValueException $e){
throw new PacketDecodeException($e->getMessage(), 0, $e); throw new PacketDecodeException($e->getMessage(), 0, $e);
} }
@ -112,7 +112,7 @@ class LoginPacket extends DataPacket implements ServerboundPacket{
$this->clientDataJwt = $buffer->get($buffer->getLInt()); $this->clientDataJwt = $buffer->get($buffer->getLInt());
try{ try{
$clientData = Utils::getJwtClaims($this->clientDataJwt); $clientData = JwtUtils::getClaims($this->clientDataJwt);
}catch(\UnexpectedValueException $e){ }catch(\UnexpectedValueException $e){
throw new PacketDecodeException($e->getMessage(), 0, $e); throw new PacketDecodeException($e->getMessage(), 0, $e);
} }

View File

@ -361,30 +361,6 @@ class Utils{
return $hash; return $hash;
} }
/**
* @return mixed[] array of claims
* @phpstan-return array<string, mixed>
*
* @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 * @param object $value
*/ */