22declare(strict_types=1);
24namespace pocketmine\network\mcpe\auth;
34use
function base64_decode;
35use
function igbinary_serialize;
36use
function igbinary_unserialize;
40 private const TLS_KEY_ON_COMPLETION =
"completion";
47 public const MOJANG_ROOT_PUBLIC_KEY =
"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
49 private const CLOCK_DRIFT_MAX = 60;
51 private string $chain;
65 private bool $authenticated =
false;
66 private ?
string $clientPublicKey =
null;
74 private string $clientDataJwt,
75 private bool $authRequired,
76 \Closure $onCompletion
78 $this->
storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
79 $this->chain = igbinary_serialize($chainJwts);
84 $this->clientPublicKey = $this->validateChain();
87 $disconnectMessage = $e->getDisconnectMessage();
92 private function validateChain() : string{
94 $chain = igbinary_unserialize($this->chain);
99 foreach($chain as $jwt){
100 $this->validateToken($jwt, $currentKey, $first);
107 $clientKey = $currentKey;
109 $this->validateToken($this->clientDataJwt, $currentKey);
117 private function validateToken(
string $jwt, ?
string &$currentPublicKey,
bool $first =
false) : void{
119 [$headersArray, $claimsArray, ] = JwtUtils::parse($jwt);
120 }
catch(JwtException $e){
121 throw new VerifyLoginException(
"Failed to parse JWT: " . $e->getMessage(),
null, 0, $e);
124 $mapper = new \JsonMapper();
125 $mapper->bExceptionOnMissingData =
true;
126 $mapper->bExceptionOnUndefinedProperty =
true;
127 $mapper->bStrictObjectTypeChecking =
true;
128 $mapper->bEnforceMapType =
false;
132 $headers = $mapper->map($headersArray,
new JwtHeader());
134 throw new VerifyLoginException(
"Invalid JWT header: " . $e->getMessage(),
null, 0, $e);
137 $headerDerKey = base64_decode($headers->x5u,
true);
138 if($headerDerKey ===
false){
139 throw new VerifyLoginException(
"Invalid JWT public key: base64 decoding error decoding x5u");
142 if($currentPublicKey ===
null){
144 throw new VerifyLoginException(
"Missing JWT public key", KnownTranslationFactory::pocketmine_disconnect_invalidSession_missingKey());
146 }elseif($headerDerKey !== $currentPublicKey){
148 throw new VerifyLoginException(
"Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
152 $signingKeyOpenSSL = JwtUtils::parseDerPublicKey($headerDerKey);
153 }
catch(JwtException $e){
154 throw new VerifyLoginException(
"Invalid JWT public key: " . $e->getMessage(),
null, 0, $e);
157 if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
158 throw new VerifyLoginException(
"Invalid JWT signature", KnownTranslationFactory::pocketmine_disconnect_invalidSession_badSignature());
160 }
catch(JwtException $e){
161 throw new VerifyLoginException($e->getMessage(),
null, 0, $e);
164 if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
165 $this->authenticated =
true;
168 $mapper = new \JsonMapper();
169 $mapper->bExceptionOnUndefinedProperty =
false;
170 $mapper->bExceptionOnMissingData =
true;
171 $mapper->bStrictObjectTypeChecking =
true;
172 $mapper->bEnforceMapType =
false;
173 $mapper->bRemoveUndefinedAttributes =
true;
176 $claims = $mapper->map($claimsArray,
new JwtChainLinkBody());
178 throw new VerifyLoginException(
"Invalid chain link body: " . $e->getMessage(),
null, 0, $e);
182 if(isset($claims->nbf) && $claims->nbf > $time + self::CLOCK_DRIFT_MAX){
183 throw new VerifyLoginException(
"JWT not yet valid", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooEarly());
186 if(isset($claims->exp) && $claims->exp < $time - self::CLOCK_DRIFT_MAX){
187 throw new VerifyLoginException(
"JWT expired", KnownTranslationFactory::pocketmine_disconnect_invalidSession_tooLate());
190 if(isset($claims->identityPublicKey)){
191 $identityPublicKey = base64_decode($claims->identityPublicKey,
true);
192 if($identityPublicKey ===
false){
193 throw new VerifyLoginException(
"Invalid identityPublicKey: base64 error decoding");
197 JwtUtils::parseDerPublicKey($identityPublicKey);
198 }
catch(JwtException $e){
199 throw new VerifyLoginException(
"Invalid identityPublicKey: " . $e->getMessage(),
null, 0, $e);
201 $currentPublicKey = $identityPublicKey;
210 $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
211 $callback($this->authenticated, $this->authRequired, $this->error instanceof
NonThreadSafeValue ? $this->error->
deserialize() : $this->error, $this->clientPublicKey);
__construct(array $chainJwts, private string $clientDataJwt, private bool $authRequired, \Closure $onCompletion)
const MOJANG_ROOT_PUBLIC_KEY
storeLocal(string $key, mixed $complexData)