Quick and dirty backport of encryption, preserving BC

This commit is contained in:
Dylan K. Taylor 2022-01-21 23:05:21 +00:00
parent b33a75a6d1
commit d28be4eaf2
No known key found for this signature in database
GPG Key ID: 8927471A91CAFD3D
13 changed files with 711 additions and 6 deletions

View File

@ -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
View File

@ -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": "*",

View File

@ -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
*/

View File

@ -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();

View 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{
}

View 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;
}
}

View File

@ -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{

View File

@ -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;
}

View 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\encryption;
final class DecryptionException extends \RuntimeException{
}

View 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);
}
}

View 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
);
}
}

View File

@ -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);
}
}

View File

@ -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