PocketMine-MP 5.35.1 git-e32e836dad793a3a3c8ddd8927c00e112b1e576a
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 if($errors !== null){
105 $this->logger->error("The following errors occurred while fetching new keys:\n\t- " . implode("\n\t-", $errors));
106 //we might've still succeeded in fetching keys even if there were errors, so don't return
107 }
108
109 if($keys === null){
110 $this->logger->critical("Failed to fetch authentication keys from Mojang's API. Xbox players may not be able to authenticate!");
111 $resolver->reject();
112 }else{
113 $pemKeys = [];
114 foreach($keys as $keyModel){
115 if($keyModel->use !== "sig" || $keyModel->kty !== "RSA"){
116 $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");
117 continue;
118 }
119 $derKey = JwtUtils::rsaPublicKeyModExpToDer($keyModel->n, $keyModel->e);
120
121 //make sure the key is valid
122 try{
123 JwtUtils::parseDerPublicKey($derKey);
124 }catch(JwtException $e){
125 $this->logger->error("Failed to parse RSA public key for key ID $keyModel->kid: " . $e->getMessage());
126 $this->logger->logException($e);
127 continue;
128 }
129
130 //retain PEM keys instead of OpenSSLAsymmetricKey since these are easier and cheaper to copy between threads
131 $pemKeys[$keyModel->kid] = $derKey;
132 }
133
134 if(count($keys) === 0){
135 $this->logger->critical("No valid authentication keys returned by Mojang's API. Xbox players may not be able to authenticate!");
136 $resolver->reject();
137 }else{
138 $this->logger->info("Successfully fetched " . count($keys) . " new authentication keys from issuer $issuer, key IDs: " . implode(", ", array_keys($pemKeys)));
139 $this->keyring = new AuthKeyring($issuer, $pemKeys);
140 $this->lastFetch = time();
141 $resolver->resolve($this->keyring);
142 }
143 }
144 }
145
149 private function fetchKeys() : Promise{
150 if($this->resolver !== null){
151 $this->logger->debug("Key refresh was requested, but it's already in progress");
152 return $this->resolver->getPromise();
153 }
154
155 $this->logger->notice("Fetching new authentication keys");
156
158 $resolver = new PromiseResolver();
159 $this->resolver = $resolver;
160 //TODO: extract this so it can be polyfilled for unit testing
161 $this->asyncPool->submitTask(new FetchAuthKeysTask($this->onKeysFetched(...)));
162 return $this->resolver->getPromise();
163 }
164}