mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-04-20 16:00:20 +00:00
Quick and dirty backport of encryption, preserving BC
This commit is contained in:
parent
b33a75a6d1
commit
d28be4eaf2
@ -8,6 +8,7 @@
|
||||
"php": "^8.0",
|
||||
"php-64bit": "*",
|
||||
"ext-chunkutils2": "^0.3.1",
|
||||
"ext-crypto": "^0.3.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-date": "*",
|
||||
@ -27,6 +28,7 @@
|
||||
"ext-zlib": ">=1.2.11",
|
||||
"composer-runtime-api": "^2.0",
|
||||
"adhocore/json-comment": "^1.1",
|
||||
"fgrosse/phpasn1": "^2.3",
|
||||
"pocketmine/binaryutils": "^0.1.9",
|
||||
"pocketmine/callback-validator": "^1.0.2",
|
||||
"pocketmine/classloader": "^0.1.0",
|
||||
|
78
composer.lock
generated
78
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "9fe058549206174b6c62b2a41685083c",
|
||||
"content-hash": "4ee772232d0936f6f9eda5d54ec2462d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adhocore/json-comment",
|
||||
@ -61,6 +61,81 @@
|
||||
],
|
||||
"time": "2021-04-09T03:06:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fgrosse/phpasn1",
|
||||
"version": "v2.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/fgrosse/PHPASN1.git",
|
||||
"reference": "eef488991d53e58e60c9554b09b1201ca5ba9296"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/eef488991d53e58e60c9554b09b1201ca5ba9296",
|
||||
"reference": "eef488991d53e58e60c9554b09b1201ca5ba9296",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "~2.0",
|
||||
"phpunit/phpunit": "^6.3 || ^7.0 || ^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "BCmath is the fallback extension for big integer calculations",
|
||||
"ext-curl": "For loading OID information from the web if they have not bee defined statically",
|
||||
"ext-gmp": "GMP is the preferred extension for big integer calculations",
|
||||
"phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FG\\": "lib/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Friedrich Große",
|
||||
"email": "friedrich.grosse@gmail.com",
|
||||
"homepage": "https://github.com/FGrosse",
|
||||
"role": "Author"
|
||||
},
|
||||
{
|
||||
"name": "All contributors",
|
||||
"homepage": "https://github.com/FGrosse/PHPASN1/contributors"
|
||||
}
|
||||
],
|
||||
"description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.",
|
||||
"homepage": "https://github.com/FGrosse/PHPASN1",
|
||||
"keywords": [
|
||||
"DER",
|
||||
"asn.1",
|
||||
"asn1",
|
||||
"ber",
|
||||
"binary",
|
||||
"decoding",
|
||||
"encoding",
|
||||
"x.509",
|
||||
"x.690",
|
||||
"x509",
|
||||
"x690"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/fgrosse/PHPASN1/issues",
|
||||
"source": "https://github.com/fgrosse/PHPASN1/tree/v2.4.0"
|
||||
},
|
||||
"time": "2021-12-11T12:41:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pocketmine/binaryutils",
|
||||
"version": "0.1.13",
|
||||
@ -2762,6 +2837,7 @@
|
||||
"php": "^8.0",
|
||||
"php-64bit": "*",
|
||||
"ext-chunkutils2": "^0.3.1",
|
||||
"ext-crypto": "^0.3.1",
|
||||
"ext-ctype": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-date": "*",
|
||||
|
@ -102,6 +102,8 @@ use pocketmine\nbt\tag\DoubleTag;
|
||||
use pocketmine\nbt\tag\ListTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\mcpe\convert\ItemTypeDictionary;
|
||||
use pocketmine\network\mcpe\encryption\EncryptionContext;
|
||||
use pocketmine\network\mcpe\encryption\PrepareEncryptionTask;
|
||||
use pocketmine\network\mcpe\PlayerNetworkSessionAdapter;
|
||||
use pocketmine\network\mcpe\protocol\ActorEventPacket;
|
||||
use pocketmine\network\mcpe\protocol\AdventureSettingsPacket;
|
||||
@ -139,6 +141,7 @@ use pocketmine\network\mcpe\protocol\ResourcePackDataInfoPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePacksInfoPacket;
|
||||
use pocketmine\network\mcpe\protocol\ResourcePackStackPacket;
|
||||
use pocketmine\network\mcpe\protocol\RespawnPacket;
|
||||
use pocketmine\network\mcpe\protocol\ServerToClientHandshakePacket;
|
||||
use pocketmine\network\mcpe\protocol\SetPlayerGameTypePacket;
|
||||
use pocketmine\network\mcpe\protocol\SetSpawnPositionPacket;
|
||||
use pocketmine\network\mcpe\protocol\SetTitlePacket;
|
||||
@ -184,6 +187,7 @@ use pocketmine\tile\ItemFrame;
|
||||
use pocketmine\tile\Spawnable;
|
||||
use pocketmine\tile\Tile;
|
||||
use pocketmine\timings\Timings;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use pocketmine\utils\TextFormat;
|
||||
use pocketmine\utils\UUID;
|
||||
use function abs;
|
||||
@ -285,6 +289,8 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
/** @var DataPacket[] */
|
||||
private $batchedPackets = [];
|
||||
|
||||
private ?EncryptionContext $cipher = null;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
* Last measurement of player's latency in milliseconds.
|
||||
@ -300,6 +306,8 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
/** @var bool */
|
||||
private $seenLoginPacket = false;
|
||||
/** @var bool */
|
||||
private $awaitingEncryptionHandshake = false;
|
||||
/** @var bool */
|
||||
private $resourcePacksDone = false;
|
||||
|
||||
/** @var bool */
|
||||
@ -2073,9 +2081,49 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
$this->xuid = $xuid;
|
||||
}
|
||||
|
||||
//TODO: encryption
|
||||
$identityPublicKey = base64_decode($packet->identityPublicKey, true);
|
||||
if($identityPublicKey === false){
|
||||
//if this is invalid it should have borked VerifyLoginTask
|
||||
throw new AssumptionFailedError("We should never have reached here if the key is invalid");
|
||||
}
|
||||
|
||||
if(EncryptionContext::$ENABLED){
|
||||
$this->server->getAsyncPool()->submitTask(new PrepareEncryptionTask(
|
||||
$identityPublicKey,
|
||||
function(string $encryptionKey, string $handshakeJwt) : void{
|
||||
if(!$this->isConnected()){
|
||||
return;
|
||||
}
|
||||
|
||||
$pk = new ServerToClientHandshakePacket();
|
||||
$pk->jwt = $handshakeJwt;
|
||||
$this->sendDataPacket($pk, false, true); //make sure this gets sent before encryption is enabled
|
||||
|
||||
$this->awaitingEncryptionHandshake = true;
|
||||
|
||||
$this->cipher = EncryptionContext::fakeGCM($encryptionKey);
|
||||
|
||||
$this->server->getLogger()->debug("Enabled encryption for " . $this->username);
|
||||
}
|
||||
));
|
||||
}else{
|
||||
$this->processLogin();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function onEncryptionHandshake() : bool{
|
||||
if(!$this->awaitingEncryptionHandshake){
|
||||
return false;
|
||||
}
|
||||
$this->awaitingEncryptionHandshake = false;
|
||||
|
||||
$this->server->getLogger()->debug("Encryption handshake completed for " . $this->username);
|
||||
|
||||
$this->processLogin();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3434,6 +3482,13 @@ class Player extends Human implements CommandSender, ChunkLoader, IPlayer{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getCipher() : ?EncryptionContext{
|
||||
return $this->cipher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
|
@ -71,6 +71,7 @@ use pocketmine\nbt\tag\ShortTag;
|
||||
use pocketmine\nbt\tag\StringTag;
|
||||
use pocketmine\network\AdvancedSourceInterface;
|
||||
use pocketmine\network\CompressBatchedTask;
|
||||
use pocketmine\network\mcpe\encryption\EncryptionContext;
|
||||
use pocketmine\network\mcpe\protocol\BatchPacket;
|
||||
use pocketmine\network\mcpe\protocol\DataPacket;
|
||||
use pocketmine\network\mcpe\protocol\PlayerListPacket;
|
||||
@ -1406,6 +1407,8 @@ class Server{
|
||||
}
|
||||
$this->networkCompressionAsync = (bool) $this->getProperty("network.async-compression", true);
|
||||
|
||||
EncryptionContext::$ENABLED = (bool) $this->getProperty("network.enable-encryption", true);
|
||||
|
||||
$this->doTitleTick = ((bool) $this->getProperty("console.title-tick", true)) && Terminal::hasFormattingCodes();
|
||||
|
||||
$consoleSender = new ConsoleCommandSender();
|
||||
|
28
src/pocketmine/network/mcpe/JwtException.php
Normal file
28
src/pocketmine/network/mcpe/JwtException.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?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 JwtException extends \RuntimeException{
|
||||
|
||||
}
|
211
src/pocketmine/network/mcpe/JwtUtils.php
Normal file
211
src/pocketmine/network/mcpe/JwtUtils.php
Normal file
@ -0,0 +1,211 @@
|
||||
<?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;
|
||||
|
||||
use FG\ASN1\Exception\ParserException;
|
||||
use FG\ASN1\Universal\Integer;
|
||||
use FG\ASN1\Universal\Sequence;
|
||||
use pocketmine\utils\AssumptionFailedError;
|
||||
use function base64_decode;
|
||||
use function base64_encode;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function gmp_export;
|
||||
use function gmp_import;
|
||||
use function gmp_init;
|
||||
use function gmp_strval;
|
||||
use function is_array;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function json_last_error_msg;
|
||||
use function openssl_error_string;
|
||||
use function openssl_pkey_get_details;
|
||||
use function openssl_pkey_get_public;
|
||||
use function openssl_sign;
|
||||
use function openssl_verify;
|
||||
use function preg_match;
|
||||
use function rtrim;
|
||||
use function sprintf;
|
||||
use function str_pad;
|
||||
use function str_repeat;
|
||||
use function str_replace;
|
||||
use function str_split;
|
||||
use function strlen;
|
||||
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 STR_PAD_LEFT;
|
||||
|
||||
final class JwtUtils{
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @phpstan-return array{string, string, string}
|
||||
* @throws JwtException
|
||||
*/
|
||||
public static function split(string $jwt) : array{
|
||||
$v = explode(".", $jwt);
|
||||
if(count($v) !== 3){
|
||||
throw new JwtException("Expected exactly 3 JWT parts, got " . count($v));
|
||||
}
|
||||
return [$v[0], $v[1], $v[2]]; //workaround phpstan bug
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: replace this result with an object
|
||||
*
|
||||
* @return mixed[]
|
||||
* @phpstan-return array{mixed[], mixed[], string}
|
||||
*
|
||||
* @throws JwtException
|
||||
*/
|
||||
public static function parse(string $token) : array{
|
||||
$v = self::split($token);
|
||||
$header = json_decode(self::b64UrlDecode($v[0]), true);
|
||||
if(!is_array($header)){
|
||||
throw new JwtException("Failed to decode JWT header JSON: " . json_last_error_msg());
|
||||
}
|
||||
$body = json_decode(self::b64UrlDecode($v[1]), true);
|
||||
if(!is_array($body)){
|
||||
throw new JwtException("Failed to decode JWT payload JSON: " . json_last_error_msg());
|
||||
}
|
||||
$signature = self::b64UrlDecode($v[2]);
|
||||
return [$header, $body, $signature];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JwtException
|
||||
*/
|
||||
public static function verify(string $jwt, \OpenSSLAsymmetricKey $signingKey) : bool{
|
||||
[$header, $body, $signature] = self::split($jwt);
|
||||
|
||||
$plainSignature = self::b64UrlDecode($signature);
|
||||
if(strlen($plainSignature) !== 96){
|
||||
throw new JwtException("JWT signature has unexpected length, expected 96, got " . strlen($plainSignature));
|
||||
}
|
||||
|
||||
[$rString, $sString] = str_split($plainSignature, 48);
|
||||
$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(
|
||||
$header . '.' . $body,
|
||||
$sequence->getBinary(),
|
||||
$signingKey,
|
||||
OPENSSL_ALGO_SHA384
|
||||
);
|
||||
switch($v){
|
||||
case 0: return false;
|
||||
case 1: return true;
|
||||
case -1: throw new JwtException("Error verifying JWT signature: " . openssl_error_string());
|
||||
default: throw new AssumptionFailedError("openssl_verify() should only return -1, 0 or 1");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array<string, mixed> $header
|
||||
* @phpstan-param array<string, mixed> $claims
|
||||
*/
|
||||
public static function create(array $header, array $claims, \OpenSSLAsymmetricKey $signingKey) : string{
|
||||
$jwtBody = JwtUtils::b64UrlEncode(json_encode($header, JSON_THROW_ON_ERROR)) . "." . JwtUtils::b64UrlEncode(json_encode($claims, JSON_THROW_ON_ERROR));
|
||||
|
||||
openssl_sign(
|
||||
$jwtBody,
|
||||
$rawDerSig,
|
||||
$signingKey,
|
||||
OPENSSL_ALGO_SHA384
|
||||
);
|
||||
|
||||
try{
|
||||
$asnObject = Sequence::fromBinary($rawDerSig);
|
||||
}catch(ParserException $e){
|
||||
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";
|
||||
}
|
||||
|
||||
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 JwtException("Malformed base64url encoded payload could not be decoded");
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
public static function emitDerPublicKey(\OpenSSLAsymmetricKey $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");
|
||||
}
|
||||
|
||||
public static function parseDerPublicKey(string $derKey) : \OpenSSLAsymmetricKey{
|
||||
$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;
|
||||
}
|
||||
}
|
@ -118,7 +118,7 @@ class PlayerNetworkSessionAdapter extends NetworkSession{
|
||||
}
|
||||
|
||||
public function handleClientToServerHandshake(ClientToServerHandshakePacket $packet) : bool{
|
||||
return false; //TODO
|
||||
return $this->player->onEncryptionHandshake();
|
||||
}
|
||||
|
||||
public function handleResourcePackClientResponse(ResourcePackClientResponsePacket $packet) : bool{
|
||||
|
@ -45,6 +45,7 @@ use function get_class;
|
||||
use function implode;
|
||||
use function rtrim;
|
||||
use function spl_object_hash;
|
||||
use function substr;
|
||||
use function unserialize;
|
||||
use const PTHREADS_INHERIT_CONSTANTS;
|
||||
|
||||
@ -55,6 +56,8 @@ class RakLibInterface implements ServerInstance, AdvancedSourceInterface{
|
||||
*/
|
||||
private const MCPE_RAKNET_PROTOCOL_VERSION = 10;
|
||||
|
||||
private const MCPE_RAKNET_PACKET_ID = "\xfe";
|
||||
|
||||
/** @var Server */
|
||||
private $server;
|
||||
|
||||
@ -163,9 +166,18 @@ class RakLibInterface implements ServerInstance, AdvancedSourceInterface{
|
||||
//get this now for blocking in case the player was closed before the exception was raised
|
||||
$player = $this->players[$identifier];
|
||||
$address = $player->getAddress();
|
||||
|
||||
try{
|
||||
if($packet->buffer !== ""){
|
||||
$pk = new BatchPacket($packet->buffer);
|
||||
if($packet->buffer[0] !== self::MCPE_RAKNET_PACKET_ID){
|
||||
throw new \UnexpectedValueException("Unexpected non-FE packet");
|
||||
}
|
||||
|
||||
$cipher = $player->getCipher();
|
||||
$buffer = substr($packet->buffer, 1);
|
||||
$buffer = $cipher !== null ? $cipher->decrypt($buffer) : $buffer;
|
||||
|
||||
$pk = new BatchPacket(self::MCPE_RAKNET_PACKET_ID . $buffer);
|
||||
$player->handleDataPacket($pk);
|
||||
}
|
||||
}catch(\Throwable $e){
|
||||
@ -245,17 +257,21 @@ class RakLibInterface implements ServerInstance, AdvancedSourceInterface{
|
||||
}
|
||||
|
||||
if($packet instanceof BatchPacket){
|
||||
$cipher = $player->getCipher();
|
||||
$rawBuffer = substr($packet->buffer, 1);
|
||||
$buffer = self::MCPE_RAKNET_PACKET_ID . ($cipher !== null ? $cipher->encrypt($rawBuffer) : $rawBuffer);
|
||||
|
||||
if($needACK){
|
||||
$pk = new EncapsulatedPacket();
|
||||
$pk->identifierACK = $this->identifiersACK[$identifier]++;
|
||||
$pk->buffer = $packet->buffer;
|
||||
$pk->buffer = $buffer;
|
||||
$pk->reliability = PacketReliability::RELIABLE_ORDERED;
|
||||
$pk->orderChannel = 0;
|
||||
}else{
|
||||
if(!isset($packet->__encapsulatedPacket)){
|
||||
$packet->__encapsulatedPacket = new CachedEncapsulatedPacket;
|
||||
$packet->__encapsulatedPacket->identifierACK = null;
|
||||
$packet->__encapsulatedPacket->buffer = $packet->buffer;
|
||||
$packet->__encapsulatedPacket->buffer = $buffer;
|
||||
$packet->__encapsulatedPacket->reliability = PacketReliability::RELIABLE_ORDERED;
|
||||
$packet->__encapsulatedPacket->orderChannel = 0;
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
<?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\encryption;
|
||||
|
||||
final class DecryptionException extends \RuntimeException{
|
||||
|
||||
}
|
119
src/pocketmine/network/mcpe/encryption/EncryptionContext.php
Normal file
119
src/pocketmine/network/mcpe/encryption/EncryptionContext.php
Normal file
@ -0,0 +1,119 @@
|
||||
<?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\encryption;
|
||||
|
||||
use Crypto\Cipher;
|
||||
use pocketmine\utils\Binary;
|
||||
use function bin2hex;
|
||||
use function openssl_digest;
|
||||
use function openssl_error_string;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
class EncryptionContext{
|
||||
private const CHECKSUM_ALGO = "sha256";
|
||||
|
||||
/** @var bool */
|
||||
public static $ENABLED = true;
|
||||
|
||||
/** @var string */
|
||||
private $key;
|
||||
|
||||
/** @var Cipher */
|
||||
private $decryptCipher;
|
||||
|
||||
/** @var int */
|
||||
private $decryptCounter = 0;
|
||||
/** @var Cipher */
|
||||
private $encryptCipher;
|
||||
/** @var int */
|
||||
private $encryptCounter = 0;
|
||||
|
||||
public function __construct(string $encryptionKey, string $algorithm, string $iv){
|
||||
$this->key = $encryptionKey;
|
||||
|
||||
$this->decryptCipher = new Cipher($algorithm);
|
||||
$this->decryptCipher->decryptInit($this->key, $iv);
|
||||
|
||||
$this->encryptCipher = new Cipher($algorithm);
|
||||
$this->encryptCipher->encryptInit($this->key, $iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an EncryptionContext suitable for decrypting Minecraft packets from 1.16.200 and up.
|
||||
*
|
||||
* MCPE uses GCM, but without the auth tag, which defeats the whole purpose of using GCM.
|
||||
* GCM is just a wrapper around CTR which adds the auth tag, so CTR can replace GCM for this case.
|
||||
* However, since GCM passes only the first 12 bytes of the IV followed by 0002, we must do the same for
|
||||
* compatibility with MCPE.
|
||||
* In PM, we could skip this and just use GCM directly (since we use OpenSSL), but this way is more portable, and
|
||||
* better for developers who come digging in the PM code looking for answers.
|
||||
*/
|
||||
public static function fakeGCM(string $encryptionKey) : self{
|
||||
return new EncryptionContext(
|
||||
$encryptionKey,
|
||||
"AES-256-CTR",
|
||||
substr($encryptionKey, 0, 12) . "\x00\x00\x00\x02"
|
||||
);
|
||||
}
|
||||
|
||||
public static function cfb8(string $encryptionKey) : self{
|
||||
return new EncryptionContext(
|
||||
$encryptionKey,
|
||||
"AES-256-CFB8",
|
||||
substr($encryptionKey, 0, 16)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DecryptionException
|
||||
*/
|
||||
public function decrypt(string $encrypted) : string{
|
||||
if(strlen($encrypted) < 9){
|
||||
throw new DecryptionException("Payload is too short");
|
||||
}
|
||||
$decrypted = $this->decryptCipher->decryptUpdate($encrypted);
|
||||
$payload = substr($decrypted, 0, -8);
|
||||
|
||||
$packetCounter = $this->decryptCounter++;
|
||||
|
||||
if(($expected = $this->calculateChecksum($packetCounter, $payload)) !== ($actual = substr($decrypted, -8))){
|
||||
throw new DecryptionException("Encrypted packet $packetCounter has invalid checksum (expected " . bin2hex($expected) . ", got " . bin2hex($actual) . ")");
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
public function encrypt(string $payload) : string{
|
||||
return $this->encryptCipher->encryptUpdate($payload . $this->calculateChecksum($this->encryptCounter++, $payload));
|
||||
}
|
||||
|
||||
private function calculateChecksum(int $counter, string $payload) : string{
|
||||
$hash = openssl_digest(Binary::writeLLong($counter) . $payload . $this->key, self::CHECKSUM_ALGO, true);
|
||||
if($hash === false){
|
||||
throw new \RuntimeException("openssl_digest() error: " . openssl_error_string());
|
||||
}
|
||||
return substr($hash, 0, 8);
|
||||
}
|
||||
}
|
68
src/pocketmine/network/mcpe/encryption/EncryptionUtils.php
Normal file
68
src/pocketmine/network/mcpe/encryption/EncryptionUtils.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?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\encryption;
|
||||
|
||||
use pocketmine\network\mcpe\JwtUtils;
|
||||
use function base64_encode;
|
||||
use function bin2hex;
|
||||
use function gmp_init;
|
||||
use function gmp_strval;
|
||||
use function hex2bin;
|
||||
use function openssl_digest;
|
||||
use function openssl_error_string;
|
||||
use function openssl_pkey_derive;
|
||||
use function str_pad;
|
||||
|
||||
final class EncryptionUtils{
|
||||
|
||||
private function __construct(){
|
||||
//NOOP
|
||||
}
|
||||
|
||||
public static function generateSharedSecret(\OpenSSLAsymmetricKey $localPriv, \OpenSSLAsymmetricKey $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{
|
||||
return openssl_digest($salt . hex2bin(str_pad(gmp_strval($secret, 16), 96, "0", STR_PAD_LEFT)), 'sha256', true);
|
||||
}
|
||||
|
||||
public static function generateServerHandshakeJwt(\OpenSSLAsymmetricKey $serverPriv, string $salt) : string{
|
||||
$derPublicKey = JwtUtils::emitDerPublicKey($serverPriv);
|
||||
return JwtUtils::create(
|
||||
[
|
||||
"x5u" => base64_encode($derPublicKey),
|
||||
"alg" => "ES384"
|
||||
],
|
||||
[
|
||||
"salt" => base64_encode($salt)
|
||||
],
|
||||
$serverPriv
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
<?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\encryption;
|
||||
|
||||
use pocketmine\network\mcpe\JwtUtils;
|
||||
use pocketmine\scheduler\AsyncTask;
|
||||
use pocketmine\Server;
|
||||
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;
|
||||
|
||||
class PrepareEncryptionTask extends AsyncTask{
|
||||
|
||||
private static ?\OpenSSLAsymmetricKey $SERVER_PRIVATE_KEY = null;
|
||||
|
||||
/** @var string */
|
||||
private $serverPrivateKey;
|
||||
|
||||
/** @var string|null */
|
||||
private $aesKey = null;
|
||||
/** @var string|null */
|
||||
private $handshakeJwt = null;
|
||||
/** @var string */
|
||||
private $clientPub;
|
||||
|
||||
/**
|
||||
* @phpstan-param \Closure(string $encryptionKey, string $handshakeJwt) : void $onCompletion
|
||||
*/
|
||||
public function __construct(string $clientPub, \Closure $onCompletion){
|
||||
if(self::$SERVER_PRIVATE_KEY === null){
|
||||
$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 = igbinary_serialize(openssl_pkey_get_details(self::$SERVER_PRIVATE_KEY));
|
||||
$this->clientPub = $clientPub;
|
||||
$this->storeLocal($onCompletion);
|
||||
}
|
||||
|
||||
public function onRun() : void{
|
||||
/** @var mixed[] $serverPrivDetails */
|
||||
$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);
|
||||
$this->aesKey = EncryptionUtils::generateKey($sharedSecret, $salt);
|
||||
$this->handshakeJwt = EncryptionUtils::generateServerHandshakeJwt($serverPriv, $salt);
|
||||
|
||||
@openssl_free_key($serverPriv);
|
||||
@openssl_free_key($clientPub);
|
||||
}
|
||||
|
||||
public function onCompletion(Server $server) : void{
|
||||
/**
|
||||
* @var \Closure $callback
|
||||
* @phpstan-var \Closure(string $encryptionKey, string $handshakeJwt) : void $callback
|
||||
*/
|
||||
$callback = $this->fetchLocal();
|
||||
if($this->aesKey === null || $this->handshakeJwt === null){
|
||||
throw new AssumptionFailedError("Something strange happened here ...");
|
||||
}
|
||||
$callback($this->aesKey, $this->handshakeJwt);
|
||||
}
|
||||
}
|
@ -92,6 +92,9 @@ network:
|
||||
#Maximum size in bytes of packets sent over the network (default 1492 bytes). Packets larger than this will be
|
||||
#fragmented or split into smaller parts. Clients can request MTU sizes up to but not more than this number.
|
||||
max-mtu-size: 1492
|
||||
#Enable encryption of Minecraft network traffic. This has an impact on performance, but prevents hackers from stealing sessions and pretending to be other players.
|
||||
#DO NOT DISABLE THIS unless you understand the risks involved.
|
||||
enable-encryption: true
|
||||
|
||||
debug:
|
||||
#If > 1, it will show debug messages in the console
|
||||
|
Loading…
x
Reference in New Issue
Block a user