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
11 changed files with 186 additions and 154 deletions

View File

@ -23,14 +23,15 @@ declare(strict_types=1);
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 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{
@ -39,18 +40,30 @@ final class EncryptionUtils{
//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{
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(
[
"x5u" => base64_encode((new DerPublicKeySerializer())->serialize($serverPriv->getPublicKey())),
"x5u" => base64_encode($derPublicKey),
"alg" => "ES384"
],
[

View File

@ -23,50 +23,65 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\encryption;
use Mdanter\Ecc\Crypto\Key\PrivateKeyInterface;
use Mdanter\Ecc\Crypto\Key\PublicKeyInterface;
use Mdanter\Ecc\EccFactory;
use pocketmine\network\mcpe\JwtUtils;
use pocketmine\scheduler\AsyncTask;
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 const TLS_KEY_ON_COMPLETION = "completion";
/** @var PrivateKeyInterface|null */
/** @var resource|null */
private static $SERVER_PRIVATE_KEY = null;
/** @var PrivateKeyInterface */
/** @var string */
private $serverPrivateKey;
/** @var string|null */
private $aesKey = null;
/** @var string|null */
private $handshakeJwt = null;
/** @var PublicKeyInterface */
/** @var string */
private $clientPub;
/**
* @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){
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->storeLocal(self::TLS_KEY_ON_COMPLETION, $onCompletion);
}
public function onRun() : void{
$serverPriv = $this->serverPrivateKey;
$sharedSecret = EncryptionUtils::generateSharedSecret($serverPriv, $this->clientPub);
/** @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() : void{