Use OpenSSL for ECDH during client login, drop mdanter/ecc (#4328)

This brings a significant performance improvement to login sequence handling, reducing CPU cost of `PrepareEncryptionTask` by over 90% and `ProcessLoginTask` by over 60%. It also allows us to shed a dependency.
This commit is contained in:
Dylan T 2021-07-22 23:04:00 +01:00 committed by GitHub
parent 83016a97bd
commit 0eb4231b51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 186 additions and 154 deletions

View File

@ -32,7 +32,7 @@
"ext-zlib": ">=1.2.11", "ext-zlib": ">=1.2.11",
"composer-runtime-api": "^2.0", "composer-runtime-api": "^2.0",
"adhocore/json-comment": "^1.1", "adhocore/json-comment": "^1.1",
"mdanter/ecc": "^1.0", "fgrosse/phpasn1": "^2.3",
"netresearch/jsonmapper": "^4.0", "netresearch/jsonmapper": "^4.0",
"pocketmine/bedrock-protocol": "dev-master#88ae308a03e8e61ccfdddd42623efabe9e772b42", "pocketmine/bedrock-protocol": "dev-master#88ae308a03e8e61ccfdddd42623efabe9e772b42",
"pocketmine/binaryutils": "^0.2.1", "pocketmine/binaryutils": "^0.2.1",

78
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "7c8ebad0871e3c90e8894476e3c5b925", "content-hash": "49005f17832ef5949b4a2ac04cd1ee93",
"packages": [ "packages": [
{ {
"name": "adhocore/json-comment", "name": "adhocore/json-comment",
@ -192,82 +192,6 @@
}, },
"time": "2021-04-24T19:01:55+00:00" "time": "2021-04-24T19:01:55+00:00"
}, },
{
"name": "mdanter/ecc",
"version": "v1.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpecc/phpecc.git",
"reference": "34e2eec096bf3dcda814e8f66dd91ae87a2db7cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpecc/phpecc/zipball/34e2eec096bf3dcda814e8f66dd91ae87a2db7cd",
"reference": "34e2eec096bf3dcda814e8f66dd91ae87a2db7cd",
"shasum": ""
},
"require": {
"ext-gmp": "*",
"fgrosse/phpasn1": "^2.0",
"php": "^7.0||^8.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0||^8.0||^9.0",
"squizlabs/php_codesniffer": "^2.0",
"symfony/yaml": "^2.6|^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mdanter\\Ecc\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matyas Danter",
"homepage": "http://matejdanter.com/",
"role": "Author"
},
{
"name": "Thibaud Fabre",
"email": "thibaud@aztech.io",
"homepage": "http://aztech.io",
"role": "Maintainer"
},
{
"name": "Thomas Kerin",
"email": "afk11@users.noreply.github.com",
"role": "Maintainer"
}
],
"description": "PHP Elliptic Curve Cryptography library",
"homepage": "https://github.com/phpecc/phpecc",
"keywords": [
"Diffie",
"ECDSA",
"Hellman",
"curve",
"ecdh",
"elliptic",
"nistp192",
"nistp224",
"nistp256",
"nistp384",
"nistp521",
"phpecc",
"secp256k1",
"secp256r1"
],
"support": {
"issues": "https://github.com/phpecc/phpecc/issues",
"source": "https://github.com/phpecc/phpecc/tree/v1.0.0"
},
"time": "2021-01-16T19:42:14+00:00"
},
{ {
"name": "netresearch/jsonmapper", "name": "netresearch/jsonmapper",
"version": "v4.0.0", "version": "v4.0.0",

View File

@ -46,6 +46,7 @@ parameters:
- tests/phpstan/stubs/JsonMapper.stub - tests/phpstan/stubs/JsonMapper.stub
- tests/phpstan/stubs/pthreads.stub - tests/phpstan/stubs/pthreads.stub
- tests/phpstan/stubs/leveldb.stub - tests/phpstan/stubs/leveldb.stub
- tests/phpstan/stubs/phpasn1.stub
reportUnmatchedIgnoredErrors: false #no other way to silence platform-specific non-warnings reportUnmatchedIgnoredErrors: false #no other way to silence platform-specific non-warnings
staticReflectionClassNamePatterns: staticReflectionClassNamePatterns:
- "#^COM$#" - "#^COM$#"

View File

@ -23,36 +23,39 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface; use FG\ASN1\Exception\ParserException;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface; use FG\ASN1\Universal\Integer;
use Mdanter\Ecc\Crypto\Signature\Signature; use FG\ASN1\Universal\Sequence;
use Mdanter\Ecc\Serializer\PrivateKey\DerPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PrivateKey\PemPrivateKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
use Mdanter\Ecc\Serializer\PublicKey\PemPublicKeySerializer;
use Mdanter\Ecc\Serializer\Signature\DerSignatureSerializer;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use function base64_decode; use function base64_decode;
use function base64_encode; use function base64_encode;
use function bin2hex;
use function count; use function count;
use function explode; use function explode;
use function gmp_export;
use function gmp_import;
use function gmp_init; use function gmp_init;
use function gmp_strval; use function gmp_strval;
use function hex2bin;
use function is_array; use function is_array;
use function json_decode; use function json_decode;
use function json_encode; use function json_encode;
use function json_last_error_msg; use function json_last_error_msg;
use function openssl_error_string; use function openssl_error_string;
use function openssl_pkey_get_details;
use function openssl_pkey_get_public;
use function openssl_sign; use function openssl_sign;
use function openssl_verify; use function openssl_verify;
use function preg_match;
use function rtrim; use function rtrim;
use function sprintf;
use function str_pad; use function str_pad;
use function str_repeat; use function str_repeat;
use function str_replace;
use function str_split; use function str_split;
use function strlen; use function strlen;
use function strtr; use function strtr;
use const GMP_BIG_ENDIAN;
use const GMP_MSW_FIRST;
use const JSON_THROW_ON_ERROR;
use const OPENSSL_ALGO_SHA384; use const OPENSSL_ALGO_SHA384;
use const STR_PAD_LEFT; use const STR_PAD_LEFT;
@ -94,9 +97,11 @@ final class JwtUtils{
} }
/** /**
* @param resource $signingKey
*
* @throws JwtException * @throws JwtException
*/ */
public static function verify(string $jwt, PublicKeyInterface $signingKey) : bool{ public static function verify(string $jwt, $signingKey) : bool{
[$header, $body, $signature] = self::split($jwt); [$header, $body, $signature] = self::split($jwt);
$plainSignature = self::b64UrlDecode($signature); $plainSignature = self::b64UrlDecode($signature);
@ -105,12 +110,17 @@ final class JwtUtils{
} }
[$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)); $convert = fn(string $str) => gmp_strval(gmp_import($str, 1, GMP_BIG_ENDIAN | GMP_MSW_FIRST), 10);
$sequence = new Sequence(
new Integer($convert($rString)),
new Integer($convert($sString))
);
$v = openssl_verify( $v = openssl_verify(
$header . '.' . $body, $header . '.' . $body,
(new DerSignatureSerializer())->serialize($sig), $sequence->getBinary(),
(new PemPublicKeySerializer(new DerPublicKeySerializer()))->serialize($signingKey), $signingKey,
OPENSSL_ALGO_SHA384 OPENSSL_ALGO_SHA384
); );
switch($v){ switch($v){
@ -122,24 +132,43 @@ final class JwtUtils{
} }
/** /**
* @param resource $signingKey
*
* @phpstan-param array<string, mixed> $header * @phpstan-param array<string, mixed> $header
* @phpstan-param array<string, mixed> $claims * @phpstan-param array<string, mixed> $claims
*/ */
public static function create(array $header, array $claims, PrivateKeyInterface $signingKey) : string{ public static function create(array $header, array $claims, $signingKey) : string{
$jwtBody = JwtUtils::b64UrlEncode(json_encode($header)) . "." . JwtUtils::b64UrlEncode(json_encode($claims)); $jwtBody = JwtUtils::b64UrlEncode(json_encode($header, JSON_THROW_ON_ERROR)) . "." . JwtUtils::b64UrlEncode(json_encode($claims, JSON_THROW_ON_ERROR));
openssl_sign( openssl_sign(
$jwtBody, $jwtBody,
$sig, $rawDerSig,
(new PemPrivateKeySerializer(new DerPrivateKeySerializer()))->serialize($signingKey), $signingKey,
OPENSSL_ALGO_SHA384 OPENSSL_ALGO_SHA384
); );
$decodedSig = (new DerSignatureSerializer())->parse($sig); try{
$jwtSig = JwtUtils::b64UrlEncode( $asnObject = Sequence::fromBinary($rawDerSig);
hex2bin(str_pad(gmp_strval($decodedSig->getR(), 16), 96, "0", STR_PAD_LEFT)) . }catch(ParserException $e){
hex2bin(str_pad(gmp_strval($decodedSig->getS(), 16), 96, "0", STR_PAD_LEFT)) throw new AssumptionFailedError("Failed to parse OpenSSL signature: " . $e->getMessage(), 0, $e);
}
if(count($asnObject) !== 2){
throw new AssumptionFailedError("OpenSSL produced invalid signature, expected exactly 2 parts");
}
[$r, $s] = [$asnObject[0], $asnObject[1]];
if(!($r instanceof Integer) || !($s instanceof Integer)){
throw new AssumptionFailedError("OpenSSL produced invalid signature, expected 2 INTEGER parts");
}
$rString = $r->getContent();
$sString = $s->getContent();
$toBinary = fn($str) => str_pad(
gmp_export(gmp_init($str, 10), 1, GMP_BIG_ENDIAN | GMP_MSW_FIRST),
48,
"\x00",
STR_PAD_LEFT
); );
$jwtSig = JwtUtils::b64UrlEncode($toBinary($rString) . $toBinary($sString));
return "$jwtBody.$jwtSig"; return "$jwtBody.$jwtSig";
} }
@ -158,4 +187,35 @@ final class JwtUtils{
} }
return $decoded; return $decoded;
} }
/**
* @param resource $opensslKey
*/
public static function emitDerPublicKey($opensslKey) : string{
$details = openssl_pkey_get_details($opensslKey);
if($details === false){
throw new AssumptionFailedError("Failed to get details from OpenSSL key resource");
}
/** @var string $pemKey */
$pemKey = $details['key'];
if(preg_match("@^-----BEGIN[A-Z\d ]+PUBLIC KEY-----\n([A-Za-z\d+/\n]+)\n-----END[A-Z\d ]+PUBLIC KEY-----\n$@", $pemKey, $matches) === 1){
$derKey = base64_decode(str_replace("\n", "", $matches[1]), true);
if($derKey !== false){
return $derKey;
}
}
throw new AssumptionFailedError("OpenSSL resource contains invalid public key");
}
/**
* @return resource
*/
public static function parseDerPublicKey(string $derKey){
$signingKeyOpenSSL = openssl_pkey_get_public(sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", base64_encode($derKey)));
if($signingKeyOpenSSL === false){
throw new JwtException("OpenSSL failed to parse key: " . openssl_error_string());
}
return $signingKeyOpenSSL;
}
} }

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe; namespace pocketmine\network\mcpe;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
use pocketmine\data\bedrock\EffectIdMap; use pocketmine\data\bedrock\EffectIdMap;
use pocketmine\entity\Attribute; use pocketmine\entity\Attribute;
use pocketmine\entity\effect\EffectInstance; use pocketmine\entity\effect\EffectInstance;
@ -203,7 +202,7 @@ class NetworkSession{
$this->logger->info("Player: " . TextFormat::AQUA . $info->getUsername() . TextFormat::RESET); $this->logger->info("Player: " . TextFormat::AQUA . $info->getUsername() . TextFormat::RESET);
$this->logger->setPrefix($this->getLogPrefix()); $this->logger->setPrefix($this->getLogPrefix());
}, },
function(bool $isAuthenticated, bool $authRequired, ?string $error, ?PublicKeyInterface $clientPubKey) : void{ function(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
$this->setAuthenticationStatus($isAuthenticated, $authRequired, $error, $clientPubKey); $this->setAuthenticationStatus($isAuthenticated, $authRequired, $error, $clientPubKey);
} }
)); ));
@ -570,7 +569,7 @@ class NetworkSession{
}, $reason); }, $reason);
} }
private function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error, ?PublicKeyInterface $clientPubKey) : void{ private function setAuthenticationStatus(bool $authenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void{
if(!$this->connected){ if(!$this->connected){
return; return;
} }

View File

@ -23,9 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\auth; namespace pocketmine\network\mcpe\auth;
use FG\ASN1\Exception\ParserException;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
use pocketmine\lang\KnownTranslationKeys; use pocketmine\lang\KnownTranslationKeys;
use pocketmine\network\mcpe\JwtException; use pocketmine\network\mcpe\JwtException;
use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\JwtUtils;
@ -35,6 +32,8 @@ use pocketmine\scheduler\AsyncTask;
use function base64_decode; use function base64_decode;
use function igbinary_serialize; use function igbinary_serialize;
use function igbinary_unserialize; use function igbinary_unserialize;
use function openssl_error_string;
use function openssl_free_key;
use function time; use function time;
class ProcessLoginTask extends AsyncTask{ class ProcessLoginTask extends AsyncTask{
@ -65,12 +64,12 @@ class ProcessLoginTask extends AsyncTask{
/** @var bool */ /** @var bool */
private $authRequired; private $authRequired;
/** @var PublicKeyInterface|null */ /** @var string|null */
private $clientPublicKey = null; private $clientPublicKey = null;
/** /**
* @param string[] $chainJwts * @param string[] $chainJwts
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?PublicKeyInterface $clientPublicKey) : void $onCompletion * @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPublicKey) : void $onCompletion
*/ */
public function __construct(array $chainJwts, string $clientDataJwt, bool $authRequired, \Closure $onCompletion){ public function __construct(array $chainJwts, string $clientDataJwt, bool $authRequired, \Closure $onCompletion){
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
@ -88,7 +87,7 @@ class ProcessLoginTask extends AsyncTask{
} }
} }
private function validateChain() : PublicKeyInterface{ private function validateChain() : string{
/** @var string[] $chain */ /** @var string[] $chain */
$chain = igbinary_unserialize($this->chain); $chain = igbinary_unserialize($this->chain);
@ -107,7 +106,7 @@ class ProcessLoginTask extends AsyncTask{
$this->validateToken($this->clientDataJwt, $currentKey); $this->validateToken($this->clientDataJwt, $currentKey);
return (new DerPublicKeySerializer())->parse(base64_decode($clientKey, true)); return $clientKey;
} }
/** /**
@ -132,38 +131,36 @@ class ProcessLoginTask extends AsyncTask{
throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), 0, $e); throw new VerifyLoginException("Invalid JWT header: " . $e->getMessage(), 0, $e);
} }
$headerDerKey = base64_decode($headers->x5u, true);
if($headerDerKey === false){
throw new VerifyLoginException("Invalid JWT public key: base64 decoding error decoding x5u");
}
if($currentPublicKey === null){ if($currentPublicKey === null){
if(!$first){ if(!$first){
throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY); throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_MISSINGKEY);
} }
}elseif($headerDerKey !== $currentPublicKey){
//First link, check that it is self-signed
$currentPublicKey = $headers->x5u;
}elseif($headers->x5u !== $currentPublicKey){
//Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway //Fast path: if the header key doesn't match what we expected, the signature isn't going to validate anyway
throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE); throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
} }
$derPublicKeySerializer = new DerPublicKeySerializer(); try{
$rawPublicKey = base64_decode($currentPublicKey, true); $signingKeyOpenSSL = JwtUtils::parseDerPublicKey($headerDerKey);
if($rawPublicKey === false){ }catch(JwtException $e){
throw new VerifyLoginException("Failed to decode base64'd public key"); throw new VerifyLoginException("Invalid JWT public key: " . openssl_error_string());
} }
try{ try{
$signingKey = $derPublicKeySerializer->parse($rawPublicKey); if(!JwtUtils::verify($jwt, $signingKeyOpenSSL)){
}catch(\RuntimeException | ParserException $e){
throw new VerifyLoginException("Failed to parse DER public key: " . $e->getMessage(), 0, $e);
}
try{
if(!JwtUtils::verify($jwt, $signingKey)){
throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE); throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_BADSIGNATURE);
} }
}catch(JwtException $e){ }catch(JwtException $e){
throw new VerifyLoginException($e->getMessage(), 0, $e); throw new VerifyLoginException($e->getMessage(), 0, $e);
} }
if($currentPublicKey === self::MOJANG_ROOT_PUBLIC_KEY){ openssl_free_key($signingKeyOpenSSL);
if($headers->x5u === self::MOJANG_ROOT_PUBLIC_KEY){
$this->authenticated = true; //we're signed into xbox live $this->authenticated = true; //we're signed into xbox live
} }
@ -188,13 +185,19 @@ class ProcessLoginTask extends AsyncTask{
throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE); throw new VerifyLoginException("%" . KnownTranslationKeys::POCKETMINE_DISCONNECT_INVALIDSESSION_TOOLATE);
} }
$currentPublicKey = $claims->identityPublicKey ?? null; //if there are further links, the next link should be signed with this if(isset($claims->identityPublicKey)){
$identityPublicKey = base64_decode($claims->identityPublicKey, true);
if($identityPublicKey === false){
throw new VerifyLoginException("Invalid identityPublicKey: base64 error decoding");
}
$currentPublicKey = $identityPublicKey; //if there are further links, the next link should be signed with this
}
} }
public function onCompletion() : void{ public function onCompletion() : void{
/** /**
* @var \Closure $callback * @var \Closure $callback
* @phpstan-var \Closure(bool, bool, ?string, ?PublicKeyInterface) : void $callback * @phpstan-var \Closure(bool, bool, ?string, ?string) : void $callback
*/ */
$callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION); $callback = $this->fetchLocal(self::TLS_KEY_ON_COMPLETION);
$callback($this->authenticated, $this->authRequired, $this->error, $this->clientPublicKey); $callback($this->authenticated, $this->authRequired, $this->error, $this->clientPublicKey);

View File

@ -23,14 +23,15 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\encryption; namespace pocketmine\network\mcpe\encryption;
use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
use Mdanter\Ecc\Serializer\PublicKey\DerPublicKeySerializer;
use pocketmine\network\mcpe\JwtUtils; use pocketmine\network\mcpe\JwtUtils;
use function base64_encode; use function base64_encode;
use function bin2hex;
use function gmp_init;
use function gmp_strval; use function gmp_strval;
use function hex2bin; use function hex2bin;
use function openssl_digest; use function openssl_digest;
use function openssl_error_string;
use function openssl_pkey_derive;
use function str_pad; use function str_pad;
final class EncryptionUtils{ final class EncryptionUtils{
@ -39,18 +40,30 @@ final class EncryptionUtils{
//NOOP //NOOP
} }
public static function generateSharedSecret(PrivateKeyInterface $localPriv, PublicKeyInterface $remotePub) : \GMP{ /**
return $localPriv->createExchange($remotePub)->calculateSharedKey(); * @param resource $localPriv
* @param resource $remotePub
*/
public static function generateSharedSecret($localPriv, $remotePub) : \GMP{
$hexSecret = openssl_pkey_derive($remotePub, $localPriv, 48);
if($hexSecret === false){
throw new \InvalidArgumentException("Failed to derive shared secret: " . openssl_error_string());
}
return gmp_init(bin2hex($hexSecret), 16);
} }
public static function generateKey(\GMP $secret, string $salt) : string{ public static function generateKey(\GMP $secret, string $salt) : string{
return openssl_digest($salt . hex2bin(str_pad(gmp_strval($secret, 16), 96, "0", STR_PAD_LEFT)), 'sha256', true); return openssl_digest($salt . hex2bin(str_pad(gmp_strval($secret, 16), 96, "0", STR_PAD_LEFT)), 'sha256', true);
} }
public static function generateServerHandshakeJwt(PrivateKeyInterface $serverPriv, string $salt) : string{ /**
* @param resource $serverPriv
*/
public static function generateServerHandshakeJwt($serverPriv, string $salt) : string{
$derPublicKey = JwtUtils::emitDerPublicKey($serverPriv);
return JwtUtils::create( return JwtUtils::create(
[ [
"x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())), "x5u" => base64_encode($derPublicKey),
"alg" => "ES384" "alg" => "ES384"
], ],
[ [

View File

@ -23,50 +23,65 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\encryption; namespace pocketmine\network\mcpe\encryption;
use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface; use pocketmine\network\mcpe\JwtUtils;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
use Mdanter\Ecc\EccFactory;
use pocketmine\scheduler\AsyncTask; use pocketmine\scheduler\AsyncTask;
use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\AssumptionFailedError;
use function igbinary_serialize;
use function igbinary_unserialize;
use function openssl_error_string;
use function openssl_free_key;
use function openssl_pkey_get_details;
use function openssl_pkey_new;
use function random_bytes; use function random_bytes;
class PrepareEncryptionTask extends AsyncTask{ class PrepareEncryptionTask extends AsyncTask{
private const TLS_KEY_ON_COMPLETION = "completion"; private const TLS_KEY_ON_COMPLETION = "completion";
/** @var PrivateKeyInterface|null */ /** @var resource|null */
private static $SERVER_PRIVATE_KEY = null; private static $SERVER_PRIVATE_KEY = null;
/** @var PrivateKeyInterface */ /** @var string */
private $serverPrivateKey; private $serverPrivateKey;
/** @var string|null */ /** @var string|null */
private $aesKey = null; private $aesKey = null;
/** @var string|null */ /** @var string|null */
private $handshakeJwt = null; private $handshakeJwt = null;
/** @var PublicKeyInterface */ /** @var string */
private $clientPub; private $clientPub;
/** /**
* @phpstan-param \Closure(string $encryptionKey, string $handshakeJwt) : void $onCompletion * @phpstan-param \Closure(string $encryptionKey, string $handshakeJwt) : void $onCompletion
*/ */
public function __construct(PublicKeyInterface $clientPub, \Closure $onCompletion){ public function __construct(string $clientPub, \Closure $onCompletion){
if(self::$SERVER_PRIVATE_KEY === null){ if(self::$SERVER_PRIVATE_KEY === null){
self::$SERVER_PRIVATE_KEY = EccFactory::getNistCurves()->generator384()->createPrivateKey(); $serverPrivateKey = openssl_pkey_new(["ec" => ["curve_name" => "secp384r1"]]);
if($serverPrivateKey === false){
throw new \RuntimeException("openssl_pkey_new() failed: " . openssl_error_string());
}
self::$SERVER_PRIVATE_KEY = $serverPrivateKey;
} }
$this->serverPrivateKey = self::$SERVER_PRIVATE_KEY; $this->serverPrivateKey = igbinary_serialize(openssl_pkey_get_details(self::$SERVER_PRIVATE_KEY));
$this->clientPub = $clientPub; $this->clientPub = $clientPub;
$this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion); $this->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
} }
public function onRun() : void{ public function onRun() : void{
$serverPriv = $this->serverPrivateKey; /** @var mixed[] $serverPrivDetails */
$sharedSecret = EncryptionUtils::generateSharedSecret($serverPriv, $this->clientPub); $serverPrivDetails = igbinary_unserialize($this->serverPrivateKey);
$serverPriv = openssl_pkey_new($serverPrivDetails);
if($serverPriv === false) throw new AssumptionFailedError("Failed to restore server signing key from details");
$clientPub = JwtUtils::parseDerPublicKey($this->clientPub);
$sharedSecret = EncryptionUtils::generateSharedSecret($serverPriv, $clientPub);
$salt = random_bytes(16); $salt = random_bytes(16);
$this->aesKey = EncryptionUtils::generateKey($sharedSecret, $salt); $this->aesKey = EncryptionUtils::generateKey($sharedSecret, $salt);
$this->handshakeJwt = EncryptionUtils::generateServerHandshakeJwt($serverPriv, $salt); $this->handshakeJwt = EncryptionUtils::generateServerHandshakeJwt($serverPriv, $salt);
openssl_free_key($serverPriv);
openssl_free_key($clientPub);
} }
public function onCompletion() : void{ public function onCompletion() : void{

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler; namespace pocketmine\network\mcpe\handler;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
use pocketmine\entity\InvalidSkinException; use pocketmine\entity\InvalidSkinException;
use pocketmine\event\player\PlayerPreLoginEvent; use pocketmine\event\player\PlayerPreLoginEvent;
use pocketmine\lang\KnownTranslationKeys; use pocketmine\lang\KnownTranslationKeys;
@ -63,13 +62,13 @@ class LoginPacketHandler extends PacketHandler{
private $playerInfoConsumer; private $playerInfoConsumer;
/** /**
* @var \Closure * @var \Closure
* @phpstan-var \Closure(bool, bool, ?string, ?PublicKeyInterface) : void * @phpstan-var \Closure(bool, bool, ?string, ?string) : void
*/ */
private $authCallback; private $authCallback;
/** /**
* @phpstan-param \Closure(PlayerInfo) : void $playerInfoConsumer * @phpstan-param \Closure(PlayerInfo) : void $playerInfoConsumer
* @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?PublicKeyInterface $clientPubKey) : void $authCallback * @phpstan-param \Closure(bool $isAuthenticated, bool $authRequired, ?string $error, ?string $clientPubKey) : void $authCallback
*/ */
public function __construct(Server $server, NetworkSession $session, \Closure $playerInfoConsumer, \Closure $authCallback){ public function __construct(Server $server, NetworkSession $session, \Closure $playerInfoConsumer, \Closure $authCallback){
$this->session = $session; $this->session = $session;
@ -78,10 +77,6 @@ class LoginPacketHandler extends PacketHandler{
$this->authCallback = $authCallback; $this->authCallback = $authCallback;
} }
private static function dummy() : void{
echo PublicKeyInterface::class; //this prevents the import getting removed by tools that don't understand phpstan
}
public function handleLogin(LoginPacket $packet) : bool{ public function handleLogin(LoginPacket $packet) : bool{
if(!$this->isCompatibleProtocol($packet->protocol)){ if(!$this->isCompatibleProtocol($packet->protocol)){
$this->session->sendDataPacket(PlayStatusPacket::create($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true); $this->session->sendDataPacket(PlayStatusPacket::create($packet->protocol < ProtocolInfo::CURRENT_PROTOCOL ? PlayStatusPacket::LOGIN_FAILED_CLIENT : PlayStatusPacket::LOGIN_FAILED_SERVER), true);

View File

@ -136,7 +136,7 @@ parameters:
path: ../../../src/network/mcpe/NetworkSession.php path: ../../../src/network/mcpe/NetworkSession.php
- -
message: "#^Parameter \\#1 \\$clientPub of class pocketmine\\\\network\\\\mcpe\\\\encryption\\\\PrepareEncryptionTask constructor expects Mdanter\\\\Ecc\\\\Crypto\\\\Key\\\\PublicKeyInterface, Mdanter\\\\Ecc\\\\Crypto\\\\Key\\\\PublicKeyInterface\\|null given\\.$#" message: "#^Parameter \\#1 \\$clientPub of class pocketmine\\\\network\\\\mcpe\\\\encryption\\\\PrepareEncryptionTask constructor expects string, string\\|null given\\.$#"
count: 1 count: 1
path: ../../../src/network/mcpe/NetworkSession.php path: ../../../src/network/mcpe/NetworkSession.php

View File

@ -0,0 +1,22 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
class Integer
{
/**
* @param int|string $value
*/
public function __construct($value){}
/** @return int|string */
public function getContent(){}
}