PocketMine-MP 5.39.3 git-66148f13a91e4af6778ba9f200ca25ad8a04a584
Loading...
Searching...
No Matches
AuthKeyProvider.php
1<?php
2
3/*
4 *
5 * ____ _ _ __ __ _ __ __ ____
6 * | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
7 * | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
8 * | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
9 * |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
10 *
11 * This program is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU Lesser General Public License as published by
13 * the Free Software Foundation, either version 3 of the License, or
14 * (at your option) any later version.
15 *
16 * @author PocketMine Team
17 * @link http://www.pocketmine.net/
18 *
19 *
20 */
21
22declare(strict_types=1);
23
24namespace pocketmine\network\mcpe\auth;
25
33use function array_keys;
34use function count;
35use function implode;
36use function time;
37
39 private const ALLOWED_REFRESH_INTERVAL = 30 * 60; // 30 minutes
40
41 private ?AuthKeyring $keyring = null;
42
44 private ?PromiseResolver $resolver = null;
45
46 private int $lastFetch = 0;
47
48 public function __construct(
49 private readonly \Logger $logger,
50 private readonly AsyncPool $asyncPool,
51 private readonly int $keyRefreshIntervalSeconds = self::ALLOWED_REFRESH_INTERVAL
52 ){}
53
60 public function getKey(string $keyId) : Promise{
62 $resolver = new PromiseResolver();
63
64 if(
65 $this->keyring === null || //we haven't fetched keys yet
66 ($this->keyring->getKey($keyId) === null && $this->lastFetch < time() - $this->keyRefreshIntervalSeconds) //we don't recognise this one & keys might be outdated
67 ){
68 //only refresh keys when we see one we don't recognise
69 $this->fetchKeys()->onCompletion(
70 onSuccess: fn(AuthKeyring $newKeyring) => $this->resolveKey($resolver, $newKeyring, $keyId),
71 onFailure: $resolver->reject(...)
72 );
73 }else{
74 $this->resolveKey($resolver, $this->keyring, $keyId);
75 }
76
77 return $resolver->getPromise();
78 }
79
83 private function resolveKey(PromiseResolver $resolver, AuthKeyring $keyring, string $keyId) : void{
84 $key = $keyring->getKey($keyId);
85 if($key === null){
86 $this->logger->debug("Key $keyId not recognised!");
87 $resolver->reject();
88 return;
89 }
90
91 $this->logger->debug("Key $keyId found in keychain");
92 $resolver->resolve([$keyring->getIssuer(), $key]);
93 }
94
99 private function onKeysFetched(?array $keys, string $issuer, ?array $errors) : void{
100 $resolver = $this->resolver;
101 if($resolver === null){
102 throw new AssumptionFailedError("Not expecting this to be called without a resolver present");
103 }
104 try{
105 if($errors !== null){
106 $this->logger->error("The following errors occurred while fetching new keys:\n\t- " . implode("\n\t-", $errors));
107 //we might've still succeeded in fetching keys even if there were errors, so don't return
108 }
109
110 if($keys === null){
111 $this->logger->critical("Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!");
112 $resolver->reject();
113 }else{
114 $pemKeys = [];
115 foreach($keys as $keyModel){
116 if($keyModel->use !== "sig" || $keyModel->kty !== "RSA"){
117 $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");
118 continue;
119 }
120 $derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e);
121
122 //make sure the key is valid
123 try{
124 JwtUtils::parseDerPublicKey($derKey);
125 }catch(JwtException $e){
126 $this->logger->error("Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage());
127 $this->logger->logException($e);
128 continue;
129 }
130
131 //retain PEM keys instead of OpenSSLAsymmetricKey since these are easier and cheaper to copy between threads
132 $pemKeys[$keyModel->kid] = $derKey;
133 }
134
135 if(count($keys) === 0){
136 $this->logger->critical("No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!");
137 $resolver->reject();
138 }else{
139 $this->logger->info("Successfully fetched " . count($keys) . " new authentication keys from issuer $issuer, key IDs: " . implode(", ", array_keys($pemKeys)));
140 $this->keyring = new AuthKeyring($issuer, $pemKeys);
141 $this->lastFetch = time();
142 $resolver->resolve($this->keyring);
143 }
144 }
145 }finally{
146 $this->resolver = null;
147 }
148 }
149
153 private function fetchKeys() : Promise{
154 if($this->resolver !== null){
155 $this->logger->debug("Key refresh was requested, but it's already in progress");
156 return $this->resolver->getPromise();
157 }
158
159 $this->logger->notice("Fetching new authentication keys");
160
162 $resolver = new PromiseResolver();
163 $this->resolver = $resolver;
164 //TODO: extract this so it can be polyfilled for unit testing
165 $this->asyncPool->submitTask(new FetchAuthKeysTask($this->onKeysFetched(...)));
166 return $this->resolver->getPromise();
167 }
168}