<?php

/**
 * Pure-PHP implementation of SSHv2.
 *
 * PHP version 8.1+
 *
 * Here are some examples of how to use this library:
 * <code>
 * <?php
 *    include 'vendor/autoload.php';
 *
 *    $ssh = new \phpseclib4\Net\SSH2('www.domain.tld');
 *    if (!$ssh->login('username', 'password')) {
 *        exit('Login Failed');
 *    }
 *
 *    echo $ssh->exec('pwd');
 *    echo $ssh->exec('ls -la');
 * ?>
 * </code>
 *
 * <code>
 * <?php
 *    include 'vendor/autoload.php';
 *
 *    $key = \phpseclib4\Crypt\PublicKeyLoader::load('...', '(optional) password');
 *
 *    $ssh = new \phpseclib4\Net\SSH2('www.domain.tld');
 *    if (!$ssh->login('username', $key)) {
 *        exit('Login Failed');
 *    }
 *
 *    echo $ssh->read('username@username:~$');
 *    $ssh->write("ls -la\n");
 *    echo $ssh->read('username@username:~$');
 * ?>
 * </code>
 *
 * @author    Jim Wigginton <terrafrost@php.net>
 * @copyright 2007-2026 Jim Wigginton
 * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
 * @link      https://phpseclib.com/
 */

declare(strict_types=1);

namespace phpseclib4\Net;

use phpseclib4\Common\Functions\Strings;
use phpseclib4\Crypt\{
    Blowfish,
    ChaCha20,
    DH,
    DSA,
    EC,
    Hash,
    RC4,
    RSA,
    Rijndael,
    TripleDES,
    Twofish
};
use phpseclib4\Crypt\Common\{PrivateKey, SymmetricKey};
use phpseclib4\Exception\{ConnectionClosedException,
    InvalidArgumentException,
    InvalidModeException,
    InvalidPacketLengthException,
    InvalidStateException,
    LengthException,
    NoSupportedAlgorithmsException,
    ServiceUnavailableException,
    TimeoutException,
    UnexpectedSSHMessageException,
    UnexpectedValueException,
    UnsupportedAlgorithmException,
    UnsupportedCurveException,
    UnsupportedValueException};
use phpseclib4\Math\BigInteger;
use phpseclib4\Net\SSH2\{
    ChannelConnectionFailureReason,
    DisconnectReason,
    MessageType,
    MessageTypeExtra,
    TerminalMode
};
use phpseclib4\System\SSH\Agent;

/**
 * Pure-PHP implementation of SSHv2.
 *
 * @author  Jim Wigginton <terrafrost@php.net>
 */
class SSH2
{
    /**#@+
     * Compression Types
     *
     */
    /**
     * No compression
     */
    public const NET_SSH2_COMPRESSION_NONE = 1;
    /**
     * zlib compression
     */
    public const NET_SSH2_COMPRESSION_ZLIB = 2;
    /**
     * zlib@openssh.com
     */
    public const NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH = 3;
    /**#@-*/

    // Execution Bitmap Masks
    public const MASK_CONSTRUCTOR   = 0x00000001;
    public const MASK_CONNECTED     = 0x00000002;
    public const MASK_LOGIN_REQ     = 0x00000004;
    public const MASK_LOGIN         = 0x00000008;
    public const MASK_SHELL         = 0x00000010;
    public const MASK_DISCONNECT    = 0x00000020;

    /*
     * Channel constants
     *
     * RFC4254 refers not to client and server channels but rather to sender and recipient channels.  we don't refer
     * to them in that way because RFC4254 toggles the meaning. the client sends a SSH_MSG_CHANNEL_OPEN message with
     * a sender channel and the server sends a SSH_MSG_CHANNEL_OPEN_CONFIRMATION in response, with a sender and a
     * recipient channel.  at first glance, you might conclude that SSH_MSG_CHANNEL_OPEN_CONFIRMATION's sender channel
     * would be the same thing as SSH_MSG_CHANNEL_OPEN's sender channel, but it's not, per this snippet:
     *     The 'recipient channel' is the channel number given in the original
     *     open request, and 'sender channel' is the channel number allocated by
     *     the other side.
     *
     * @see \phpseclib4\Net\SSH2::send_channel_packet()
     * @see \phpseclib4\Net\SSH2::get_channel_packet()
     */
    public const CHANNEL_EXEC          = 1; // PuTTy uses 0x100
    public const CHANNEL_SHELL         = 2;
    public const CHANNEL_SUBSYSTEM     = 3;
    public const CHANNEL_AGENT_FORWARD = 4;
    public const CHANNEL_KEEP_ALIVE    = 5;

    /**
     * Returns the message numbers
     *
     * @see \phpseclib4\Net\SSH2::getLog()
     */
    public const LOG_SIMPLE = 1;
    /**
     * Returns the message content
     *
     * @see \phpseclib4\Net\SSH2::getLog()
     */
    public const LOG_COMPLEX = 2;
    /**
     * Outputs the content real-time
     */
    public const LOG_REALTIME = 3;
    /**
     * Dumps the content real-time to a file
     */
    public const LOG_REALTIME_FILE = 4;
    /**
     * Outputs the message numbers real-time
     */
    public const LOG_SIMPLE_REALTIME = 5;
    /*
     * Dumps the message numbers real-time
     */
    public const LOG_REALTIME_SIMPLE = 5;
    /**
     * Make sure that the log never gets larger than this
     *
     * @see \phpseclib4\Net\SSH2::getLog()
     */
    public const LOG_MAX_SIZE = 1048576; // 1024 * 1024

    /**
     * Returns when a string matching $expect exactly is found
     *
     * @see \phpseclib4\Net\SSH2::read()
     */
    public const READ_SIMPLE = 1;
    /**
     * Returns when a string matching the regular expression $expect is found
     *
     * @see \phpseclib4\Net\SSH2::read()
     */
    public const READ_REGEX = 2;
    /**
     * Returns whenever a data packet is received.
     *
     * Some data packets may only contain a single character so it may be necessary
     * to call read() multiple times when using this option
     *
     * @see \phpseclib4\Net\SSH2::read()
     */
    public const READ_NEXT = 3;

    /**
     * The SSH identifier
     */
    private string $identifier;

    /**
     * The Socket Object
     *
     * @var resource|closed-resource|null
     */
    public mixed $fsock = null;

    /**
     * Execution Bitmap
     *
     * The bits that are set represent functions that have been called already.  This is used to determine
     * if a requisite function has been successfully executed.  If not, an error should be thrown.
     */
    protected int $bitmap = 0;

    /**
     * Error information
     *
     * @see self::getErrors()
     * @see self::getLastError()
     */
    private array $errors = [];

    /**
     * Server Identifier
     *
     * @see self::getServerIdentification()
     */
    protected string $server_identifier;

    /**
     * Key Exchange Algorithms
     *
     * @see self::getKexAlgorithims()
     */
    private array $kex_algorithms;

    /**
     * Key Exchange Algorithm
     *
     * @see self::getMethodsNegotiated()
     */
    private ?string $kex_algorithm;

    /**
     * Minimum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods
     *
     * @see self::_key_exchange()
     */
    private int $kex_dh_group_size_min = 1536;

    /**
     * Preferred Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods
     *
     * @see self::_key_exchange()
     */
    private int $kex_dh_group_size_preferred = 2048;

    /**
     * Maximum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods
     *
     * @see self::_key_exchange()
     */
    private int $kex_dh_group_size_max = 4096;

    /**
     * Server Host Key Algorithms
     *
     * @see self::getServerHostKeyAlgorithms()
     */
    private array $server_host_key_algorithms;

    /**
     * Supported Private Key Algorithms
     *
     * In theory this should be the same as the Server Host Key Algorithms but, in practice,
     * some servers (eg. Azure) will support rsa-sha2-512 as a server host key algorithm but
     * not a private key algorithm
     *
     * @see self::privatekey_login()
     */
    private array $supported_private_key_algorithms;

    /**
     * Encryption Algorithms: Client to Server
     *
     * @see self::getEncryptionAlgorithmsClient2Server()
     */
    private array $encryption_algorithms_client_to_server;

    /**
     * Encryption Algorithms: Server to Client
     *
     * @see self::getEncryptionAlgorithmsServer2Client()
     */
    private array $encryption_algorithms_server_to_client;

    /**
     * MAC Algorithms: Client to Server
     *
     * @see self::getMACAlgorithmsClient2Server()
     */
    private array $mac_algorithms_client_to_server;

    /**
     * MAC Algorithms: Server to Client
     *
     * @see self::getMACAlgorithmsServer2Client()
     */
    private array $mac_algorithms_server_to_client;

    /**
     * Compression Algorithms: Client to Server
     *
     * @see self::getCompressionAlgorithmsClient2Server()
     */
    private array $compression_algorithms_client_to_server;

    /**
     * Compression Algorithms: Server to Client
     *
     * @see self::getCompressionAlgorithmsServer2Client()
     */
    private array $compression_algorithms_server_to_client;

    /**
     * Languages: Server to Client
     */
    private array $languages_server_to_client;

    /**
     * Languages: Client to Server
     */
    private array $languages_client_to_server;

    /**
     * Preferred Algorithms
     *
     * @see self::setPreferredAlgorithms()
     */
    private array $preferred = [];

    /**
     * Block Size for Server to Client Encryption
     *
     * "Note that the length of the concatenation of 'packet_length',
     *  'padding_length', 'payload', and 'random padding' MUST be a multiple
     *  of the cipher block size or 8, whichever is larger.  This constraint
     *  MUST be enforced, even when using stream ciphers."
     *
     *  -- http://tools.ietf.org/html/rfc4253#section-6
     *
     * @see self::__construct()
     * @see self::send_binary_packet()
     */
    private int $encrypt_block_size = 8;

    /**
     * Block Size for Client to Server Encryption
     *
     * @see self::__construct()
     * @see self::get_binary_packet()
     */
    private int $decrypt_block_size = 8;

    /**
     * Server to Client Encryption Object
     *
     * @see self::get_binary_packet()
     */
    private ?SymmetricKey $decrypt = null;

    /**
     * Decryption Algorithm Name
     */
    private string $decryptName;

    /**
     * Decryption Invocation Counter
     *
     * Used by GCM
     */
    private ?string $decryptInvocationCounter;

    /**
     * Fixed Part of Nonce
     *
     * Used by GCM
     */
    private ?string $decryptFixedPart;

    /**
     * Server to Client Length Encryption Object
     *
     * @see self::get_binary_packet()
     */
    private SymmetricKey $lengthDecrypt;

    /**
     * Client to Server Encryption Object
     *
     * @see self::send_binary_packet()
     */
    private ?SymmetricKey $encrypt = null;

    /**
     * Encryption Algorithm Name
     */
    private string $encryptName;

    /**
     * Encryption Invocation Counter
     *
     * Used by GCM
     */
    private string $encryptInvocationCounter;

    /**
     * Fixed Part of Nonce
     *
     * Used by GCM
     */
    private string $encryptFixedPart;

    /**
     * Client to Server Length Encryption Object
     *
     * @see self::send_binary_packet()
     */
    private SymmetricKey $lengthEncrypt;

    /**
     * Client to Server HMAC Object
     *
     * @see self::send_binary_packet()
     */
    private Hash|\stdClass|null $hmac_create = null;

    /**
     * Client to Server HMAC Name
     */
    private string $hmac_create_name;

    /**
     * Client to Server ETM
     */
    private bool $hmac_create_etm = false;

    /**
     * Server to Client HMAC Object
     *
     * @see self::get_binary_packet()
     */
    private Hash|\stdClass|null $hmac_check = null;

    /**
     * Server to Client HMAC Name
     */
    private string $hmac_check_name;

    /**
     * Server to Client ETM
     */
    private bool $hmac_check_etm;

    /**
     * Size of server to client HMAC
     *
     * We need to know how big the HMAC will be for the server to client direction so that we know how many bytes to read.
     * For the client to server side, the HMAC object will make the HMAC as long as it needs to be.  All we need to do is
     * append it.
     *
     * @see self::get_binary_packet()
     */
    private ?int $hmac_size = null;

    /**
     * Server Public Host Key
     *
     * @see self::getServerPublicHostKey()
     */
    private string $server_public_host_key;

    /**
     * Session identifier
     *
     * "The exchange hash H from the first key exchange is additionally
     *  used as the session identifier, which is a unique identifier for
     *  this connection."
     *
     *  -- http://tools.ietf.org/html/rfc4253#section-7.2
     *
     * @see self::key_exchange()
     */
    private ?string $session_id = null;

    /**
     * Exchange hash
     *
     * The current exchange hash
     *
     * @see self::key_exchange()
     */
    private string $exchange_hash;

    /**
     * Send Sequence Number
     *
     * See 'Section 6.4.  Data Integrity' of rfc4253 for more info.
     *
     * @see self::send_binary_packet()
     */
    private int $send_seq_no = 0;

    /**
     * Get Sequence Number
     *
     * See 'Section 6.4.  Data Integrity' of rfc4253 for more info.
     *
     * @see self::get_binary_packet()
     */
    private int $get_seq_no = 0;

    /**
     * Server Channels
     *
     * Maps client channels to server channels
     *
     * @see self::get_channel_packet()
     * @see self::exec()
     */
    protected array $server_channels = [];

    /**
     * Channel Read Buffers
     *
     * If a client requests a packet from one channel but receives two packets from another those packets should
     * be placed in a buffer
     *
     * @see self::get_channel_packet()
     * @see self::exec()
     */
    private array $channel_buffers = [];

    /**
     * Channel Write Buffers
     *
     * If a client sends a packet and receives a timeout error mid-transmission, buffer the data written so it
     * can be de-duplicated upon resuming write
     *
     * @see self::send_channel_packet()
     */
    private array $channel_buffers_write = [];

    /**
     * Channel Status
     *
     * Contains the type of the last sent message
     *
     * @see self::get_channel_packet()
     */
    protected array $channel_status = [];

    /**
     * The identifier of the interactive channel which was opened most recently
     *
     * @see self::getInteractiveChannelId()
     */
    private int $channel_id_last_interactive = 0;

    /**
     * Packet Size
     *
     * Maximum packet size indexed by channel
     *
     * @see self::send_channel_packet()
     */
    protected array $packet_size_client_to_server = [];

    /**
     * Message Number Log
     *
     * @see self::getLog()
     */
    private array $message_number_log = [];

    /**
     * Message Log
     *
     * @see self::getLog()
     */
    private array $message_log = [];

    /**
     * The Window Size
     *
     * Bytes the other party can send before it must wait for the window to be adjusted (0x7FFFFFFF = 2GB)
     *
     * @see self::send_channel_packet()
     * @see self::exec()
     */
    protected int $window_size = 0x7FFFFFFF;

    /**
     * What we resize the window to
     *
     * When PuTTY resizes the window it doesn't add an additional 0x7FFFFFFF bytes - it adds 0x40000000 bytes.
     * Some SFTP clients (GoAnywhere) don't support adding 0x7FFFFFFF to the window size after the fact so
     * we'll just do what PuTTY does
     *
     * @see self::_send_channel_packet()
     * @see self::exec()
     */
    private int $window_resize = 0x40000000;

    /**
     * Window size, server to client
     *
     * Window size indexed by channel
     *
     * @see self::send_channel_packet()
     */
    protected array $window_size_server_to_client = [];

    /**
     * Window size, client to server
     *
     * Window size indexed by channel
     *
     * @see self::get_channel_packet()
     */
    private array $window_size_client_to_server = [];

    /**
     * Server signature
     *
     * Verified against $this->session_id
     *
     * @see self::getServerPublicHostKey()
     */
    private string $signature = '';

    /**
     * Server signature format
     *
     * ssh-rsa or ssh-dss.
     *
     * @see self::getServerPublicHostKey()
     */
    private string $signature_format = '';

    /**
     * Interactive Buffer
     *
     * @see self::read()
     */
    private string $interactiveBuffer = '';

    /**
     * Current log size
     *
     * Should never exceed self::LOG_MAX_SIZE
     *
     * @see self::_send_binary_packet()
     * @see self::_get_binary_packet()
     */
    private int $log_size = 0;

    /**
     * Timeout
     *
     * Is overwritten by the constructor BUT if you're mocking the object
     * the constructor may be bypassed, hence our setting it to -1 initially
     *
     * @see SSH2::setTimeout()
     */
    protected int $timeout = -1;

    /**
     * Current Timeout
     *
     * @see SSH2::get_channel_packet()
     */
    protected int|float|null $curTimeout = null;

    /**
     * Keep Alive Interval
     *
     * @see self::setKeepAlive()
     */
    private ?int $keepAlive = null;

    /**
     * Real-time log file pointer
     *
     * @see self::_append_log()
     * @var resource|closed-resource
     */
    private $realtime_log_file;

    /**
     * Real-time log file size
     *
     * @see self::_append_log()
     */
    private int $realtime_log_size = 0;

    /**
     * Has the signature been validated?
     *
     * @see self::getServerPublicHostKey()
     */
    private bool $signature_validated = false;

    /**
     * Real-time log file wrap boolean
     *
     * @see self::_append_log()
     */
    private bool $realtime_log_wrap = false;

    /**
     * Flag to suppress stderr from output
     *
     * @see self::enableQuietMode()
     */
    private bool $quiet_mode = false;

    /**
     * Time of last read/write network activity
     */
    private ?float $last_packet = null;

    /**
     * Exit status returned from ssh if any
     */
    private int $exit_status;

    /**
     * Flag to request a PTY when using exec()
     *
     * @see self::enablePTY()
     */
    private bool $request_pty = false;

    /**
     * Contents of stdError
     */
    private string $stdErrorLog;

    /**
     * The Last Interactive Response
     *
     * @see self::_keyboard_interactive_process()
     */
    private string $last_interactive_response = '';

    /**
     * Keyboard Interactive Request / Responses
     *
     * @see self::_keyboard_interactive_process()
     */
    private array $keyboard_requests_responses = [];

    /**
     * Banner Message
     *
     * Quoting from the RFC, "in some jurisdictions, sending a warning message before
     * authentication may be relevant for getting legal protection."
     *
     * @see self::_filter()
     * @see self::getBannerMessage()
     */
    private string $banner_message = '';

    /**
     * Did read() timeout or return normally?
     *
     * @see self::isTimeout()
     */
    protected bool $is_timeout = false;

    /**
     * Log Boundary
     *
     * @see self::_format_log()
     */
    private string $log_boundary = ':';

    /**
     * Log Long Width
     *
     * @see self::_format_log()
     */
    private int $log_long_width = 65;

    /**
     * Log Short Width
     *
     * @see self::_format_log()
     */
    private int $log_short_width = 16;

    /**
     * Hostname
     *
     * @see self::__construct()
     * @see self::_connect()
     */
    private string $host;

    /**
     * Port Number
     *
     * @see self::__construct()
     * @see self::connect()
     */
    private int $port;

    /**
     * Number of columns for terminal window size
     *
     * @see self::getWindowColumns()
     * @see self::setWindowColumns()
     * @see self::setWindowSize()
     */
    private int $windowColumns = 80;

    /**
     * Number of columns for terminal window size
     *
     * @see self::getWindowRows()
     * @see self::setWindowRows()
     * @see self::setWindowSize()
     */
    private int $windowRows = 24;

    /**
     * Crypto Engine
     *
     * @see self::setCryptoEngine()
     * @see self::_key_exchange()
     */
    private static ?string $crypto_engine = null;

    /**
     * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario
     */
    private Agent $agent;

    /**
     * Connection storage to replicate ssh2 extension functionality:
     * {@link http://php.net/manual/en/wrappers.ssh2.php#refsect1-wrappers.ssh2-examples}
     *
     * @var array<string, \WeakReference<SSH2>>
     */
    private static array $connections;

    /**
     * Send the identification string first?
     */
    private bool $send_id_string_first = true;

    /**
     * Send the key exchange initiation packet first?
     */
    private bool $send_kex_first = true;

    /**
     * Some versions of OpenSSH incorrectly calculate the key size
     */
    private bool $bad_key_size_fix = false;

    /**
     * Should we try to re-connect to re-establish keys?
     */
    private bool $login_credentials_finalized = false;

    /**
     * Binary Packet Buffer
     */
    private ?object $binary_packet_buffer = null;

    /**
     * Authentication Credentials
     */
    protected array $auth = [];

    /**
     * Terminal
     */
    private string $term = 'vt100';

    /**
     * The authentication methods that may productively continue authentication.
     *
     * @see https://tools.ietf.org/html/rfc4252#section-5.1
     */
    private ?array $auth_methods_to_continue = null;

    /**
     * Compression method
     */
    private int $compress = self::NET_SSH2_COMPRESSION_NONE;

    /**
     * Decompression method
     */
    private int $decompress = self::NET_SSH2_COMPRESSION_NONE;

    /**
     * Compression context
     */
    private \DeflateContext $compress_context;

    /**
     * Decompression context
     */
    private \InflateContext $decompress_context;

    /**
     * Regenerate Compression Context
     */
    private bool $regenerate_compression_context = false;

    /**
     * Regenerate Decompression Context
     */
    private bool $regenerate_decompression_context = false;

    /**
     * Smart multi-factor authentication flag
     */
    private bool $smartMFA = true;

    /**
     * How many channels are currently opened
     */
    private int $channelCount = 0;

    /**
     * Does the server support multiple channels? If not then error out
     * when multiple channels are attempted to be opened
     */
    private bool $errorOnMultipleChannels;

    /**
     * Bytes Transferred Since Last Key Exchange
     *
     * Includes outbound and inbound totals
     */
    private int $bytesTransferredSinceLastKEX = 0;

    /**
     * After how many transferred byte should phpseclib initiate a key re-exchange?
     */
    private int $doKeyReexchangeAfterXBytes = 1024 * 1024 * 1024;

    /**
     * Has a key re-exchange been initialized?
     */
    private bool $keyExchangeInProgress = false;

    /**
     * KEX Buffer
     *
     * If we're in the middle of a key exchange we want to buffer any additional packets we get until
     * the key exchange is over
     *
     * @see self::_get_binary_packet()
     * @see self::_key_exchange()
     * @see self::exec()
     */
    private array $kex_buffer = [];

    /**
     * Strict KEX Flag
     *
     * If kex-strict-s-v00@openssh.com is present in the first KEX packet it need not
     * be present in subsequent packet
     *
     * @see self::_key_exchange()
     * @see self::exec()
     */
    private bool $strict_kex_flag = false;

    /**
     * Default Constructor.
     *
     * $host can either be a string, representing the host, or a stream resource.
     * If $host is a stream resource then $port doesn't do anything, altho $timeout
     * still will be used
     *
     * @see self::login()
     * @param string|resource $host
     */
    public function __construct(mixed $host, int $port = 22, int $timeout = 10)
    {
        self::$connections[$this->getResourceId()] = \WeakReference::create($this);

        $this->timeout = $timeout;

        if (is_resource($host)) {
            $this->fsock = $host;
            return;
        }

        if (Strings::is_stringable($host)) {
            $this->host = (string) $host;
            $this->port = $port;
        }
    }

    /**
     * Set Crypto Engine Mode
     *
     * Possible $engine values:
     * OpenSSL, Eval, PHP
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public static function setCryptoEngine(string $engine): void
    {
        self::$crypto_engine = $engine;
    }

    /**
     * Send Identification String First
     *
     * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established,
     * both sides MUST send an identification string". It does not say which side sends it first. In
     * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function sendIdentificationStringFirst(): void
    {
        $this->send_id_string_first = true;
    }

    /**
     * Send Identification String Last
     *
     * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established,
     * both sides MUST send an identification string". It does not say which side sends it first. In
     * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function sendIdentificationStringLast(): void
    {
        $this->send_id_string_first = false;
    }

    /**
     * Send SSH_MSG_KEXINIT First
     *
     * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending
     * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory
     * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function sendKEXINITFirst(): void
    {
        $this->send_kex_first = true;
    }

    /**
     * Send SSH_MSG_KEXINIT Last
     *
     * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending
     * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory
     * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function sendKEXINITLast(): void
    {
        $this->send_kex_first = false;
    }

    /**
     * stream_select wrapper
     *
     * Quoting https://stackoverflow.com/a/14262151/569976,
     * "The general approach to `EINTR` is to simply handle the error and retry the operation again"
     *
     * This wrapper does that loop
     */
    private static function stream_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, ?int $microseconds = null): int|false
    {
        $remaining = $seconds + $microseconds / 1000000;
        $start = microtime(true);
        while (true) {
            $result = @stream_select($read, $write, $except, $seconds, $microseconds);
            if ($result !== false) {
                return $result;
            }
            $elapsed = microtime(true) - $start;
            $seconds = (int) ($remaining - floor($elapsed));
            $microseconds = (int) (1000000 * ($remaining - $seconds));
            if ($elapsed >= $remaining) {
                return false;
            }
        }
    }

    /**
     * Connect to an SSHv2 server
     */
    private function connect(): void
    {
        if ($this->bitmap & self::MASK_CONSTRUCTOR) {
            return;
        }

        $this->bitmap |= self::MASK_CONSTRUCTOR;

        $this->curTimeout = $this->timeout;

        if (!is_resource($this->fsock)) {
            $start = microtime(true);
            // with stream_select a timeout of 0 means that no timeout takes place;
            // with fsockopen a timeout of 0 means that you instantly timeout
            // to resolve this incompatibility a timeout of 100,000 will be used for fsockopen if timeout is 0
            $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout == 0 ? 100000 : $this->curTimeout);
            if (!$this->fsock) {
                $host = $this->host . ':' . $this->port;
                throw new ServiceUnavailableException(rtrim("Cannot connect to $host. Error $errno. $errstr"));
            }
            $elapsed = microtime(true) - $start;

            if ($this->curTimeout) {
                $this->curTimeout -= $elapsed;
                if ($this->curTimeout < 0) {
                    throw new TimeoutException('Connection timed out whilst attempting to open socket connection');
                }
            }

            if (defined('NET_SSH2_LOGGING')) {
                $this->append_log('(fsockopen took ' . round($elapsed, 4) . 's)', '');
            }
        }

        $this->identifier = $this->generate_identifier();

        if ($this->send_id_string_first) {
            $start = microtime(true);
            fwrite($this->fsock, $this->identifier . "\r\n");
            $elapsed = round(microtime(true) - $start, 4);
            if (defined('NET_SSH2_LOGGING')) {
                $this->append_log("-> (network: $elapsed)", $this->identifier . "\r\n");
            }
        }

        /* According to the SSH2 specs,

          "The server MAY send other lines of data before sending the version
           string.  Each line SHOULD be terminated by a Carriage Return and Line
           Feed.  Such lines MUST NOT begin with "SSH-", and SHOULD be encoded
           in ISO-10646 UTF-8 [RFC3629] (language is not specified).  Clients
           MUST be able to process such lines." */
        $data = '';
        $totalElapsed = 0;
        while (!feof($this->fsock) && !preg_match('#(.*)^(SSH-(\d\.\d+).*)#ms', $data, $matches)) {
            $line = '';
            while (true) {
                if ($this->curTimeout) {
                    if ($this->curTimeout < 0) {
                        throw new TimeoutException('Connection timed out whilst receiving server identification string');
                    }
                    $read = [$this->fsock];
                    $write = $except = null;
                    $start = microtime(true);
                    $sec = (int) floor($this->curTimeout);
                    $usec = (int) (1000000 * ($this->curTimeout - $sec));
                    if (static::stream_select($read, $write, $except, $sec, $usec) === false) {
                        throw new TimeoutException('Connection timed out whilst receiving server identification string');
                    }
                    $elapsed = microtime(true) - $start;
                    $totalElapsed += $elapsed;
                    $this->curTimeout -= $elapsed;
                }

                $temp = stream_get_line($this->fsock, 255, "\n");
                if ($temp === false) {
                    throw new UnexpectedValueException('Error reading SSH identification string; are you sure you\'re connecting to an SSH server?');
                }

                $line .= $temp;
                if (strlen($temp) == 255) {
                    continue;
                }

                $line .= "\n";

                break;
            }

            $data .= $line;
        }

        if (defined('NET_SSH2_LOGGING')) {
            $this->append_log('<- (network: ' . round($totalElapsed, 4) . ')', $data);
        }

        if (feof($this->fsock)) {
            $this->bitmap = 0;
            throw new ConnectionClosedException('Connection closed by server; are you sure you\'re connected to an SSH server?');
        }

        $extra = $matches[1];

        // earlier the SSH specs were quoted.
        // "The server MAY send other lines of data before sending the version string." they said.
        // the implication of this is that the lines of data before the server string are *not* a part of it
        // getting this right is important because the correct server identifier needs to be fed into the
        // exchange hash for the shared keys to be calculated correctly
        $data = explode("\r\n", trim($data, "\r\n"));
        $this->server_identifier = $data[count($data) - 1];
        if (strlen($extra)) {
            $this->errors[] = $data;
        }

        if (version_compare($matches[3], '1.99', '<')) {
            $this->bitmap = 0;
            throw new UnsupportedValueException("Cannot connect to SSH $matches[3] servers");
        }

        // Ubuntu's OpenSSH from 5.8 to 6.9 didn't work with multiple channels. see
        // https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/1334916 for more info.
        // https://lists.ubuntu.com/archives/oneiric-changes/2011-July/005772.html discusses
        // when consolekit was incorporated.
        // https://marc.info/?l=openssh-unix-dev&m=163409903417589&w=2 discusses some of the
        // issues with how Ubuntu incorporated consolekit
        $pattern = '#^SSH-2\.0-OpenSSH_([\d.]+)[^ ]* Ubuntu-.*$#';
        $match = preg_match($pattern, $this->server_identifier, $matches);
        $match = $match && version_compare('5.8', $matches[1], '<=');
        $match = $match && version_compare('6.9', $matches[1], '>=');
        $this->errorOnMultipleChannels = $match;

        if (!$this->send_id_string_first) {
            $start = microtime(true);
            fwrite($this->fsock, $this->identifier . "\r\n");
            $elapsed = round(microtime(true) - $start, 4);
            if (defined('NET_SSH2_LOGGING')) {
                $this->append_log("-> (network: $elapsed)", $this->identifier . "\r\n");
            }
        }

        $this->last_packet = microtime(true);

        if (!$this->send_kex_first) {
            $response = $this->get_binary_packet_or_close(MessageType::KEXINIT);
            $this->key_exchange($response);
        }

        if ($this->send_kex_first) {
            $this->key_exchange();
        }

        $this->bitmap |= self::MASK_CONNECTED;
    }

    /**
     * Generates the SSH identifier
     *
     * You should overwrite this method in your own class if you want to use another identifier
     */
    private function generate_identifier(): string
    {
        $identifier = 'SSH-2.0-phpseclib_4.0';

        $ext = [];
        if (extension_loaded('sodium')) {
            $ext[] = 'libsodium';
        }

        if (extension_loaded('openssl')) {
            $ext[] = 'openssl';
        }

        if (extension_loaded('gmp')) {
            $ext[] = 'gmp';
        } elseif (extension_loaded('bcmath')) {
            $ext[] = 'bcmath';
        }

        if (!empty($ext)) {
            $identifier .= ' (' . implode(', ', $ext) . ')';
        }

        return $identifier;
    }

    /**
     * Key Exchange
     */
    private function key_exchange(?string $kexinit_payload_server = null): bool
    {
        $this->bytesTransferredSinceLastKEX = 0;

        $preferred = $this->preferred;
        // for the initial key exchange $send_kex is true (no key re-exchange has been started)
        // for phpseclib initiated key exchanges $send_kex is false
        $send_kex = !$this->keyExchangeInProgress;
        $this->keyExchangeInProgress = true;

        $kex_algorithms = $preferred['kex'] ??
            SSH2::getSupportedKEXAlgorithms();
        $server_host_key_algorithms = $preferred['hostkey'] ??
            SSH2::getSupportedHostKeyAlgorithms();
        $s2c_encryption_algorithms = $preferred['server_to_client']['crypt'] ??
            SSH2::getSupportedEncryptionAlgorithms();
        $c2s_encryption_algorithms = $preferred['client_to_server']['crypt'] ??
            SSH2::getSupportedEncryptionAlgorithms();
        $s2c_mac_algorithms = $preferred['server_to_client']['mac'] ??
            SSH2::getSupportedMACAlgorithms();
        $c2s_mac_algorithms = $preferred['client_to_server']['mac'] ??
            SSH2::getSupportedMACAlgorithms();
        $s2c_compression_algorithms = $preferred['server_to_client']['comp'] ??
            SSH2::getSupportedCompressionAlgorithms();
        $c2s_compression_algorithms = $preferred['client_to_server']['comp'] ??
            SSH2::getSupportedCompressionAlgorithms();

        $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c', 'kex-strict-c-v00@openssh.com']);

        // some SSH servers have buggy implementations of some of the above algorithms
        switch (true) {
            case $this->server_identifier == 'SSH-2.0-SSHD':
            case substr($this->server_identifier, 0, 13) == 'SSH-2.0-DLINK':
                if (!isset($preferred['server_to_client']['mac'])) {
                    $s2c_mac_algorithms = array_values(array_diff(
                        $s2c_mac_algorithms,
                        ['hmac-sha1-96', 'hmac-md5-96']
                    ));
                }
                if (!isset($preferred['client_to_server']['mac'])) {
                    $c2s_mac_algorithms = array_values(array_diff(
                        $c2s_mac_algorithms,
                        ['hmac-sha1-96', 'hmac-md5-96']
                    ));
                }
                break;
            case substr($this->server_identifier, 0, 24) == 'SSH-2.0-TurboFTP_SERVER_':
                if (!isset($preferred['server_to_client']['crypt'])) {
                    $s2c_encryption_algorithms = array_values(array_diff(
                        $s2c_encryption_algorithms,
                        ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com']
                    ));
                }
                if (!isset($preferred['client_to_server']['crypt'])) {
                    $c2s_encryption_algorithms = array_values(array_diff(
                        $c2s_encryption_algorithms,
                        ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com']
                    ));
                }
        }

        $client_cookie = random_bytes(16);

        $kexinit_payload_client = pack('Ca*', MessageType::KEXINIT, $client_cookie);
        $kexinit_payload_client .= Strings::packSSH2(
            'L10bN',
            $kex_algorithms,
            $server_host_key_algorithms,
            $c2s_encryption_algorithms,
            $s2c_encryption_algorithms,
            $c2s_mac_algorithms,
            $s2c_mac_algorithms,
            $c2s_compression_algorithms,
            $s2c_compression_algorithms,
            [], // language, client to server
            [], // language, server to client
            false, // first_kex_packet_follows
            0 // reserved for future extension
        );

        if (!isset($kexinit_payload_server) && $send_kex) {
            $this->send_binary_packet($kexinit_payload_client);

            while (true) {
                $kexinit_payload_server = $this->get_binary_packet();
                switch (ord($kexinit_payload_server[0])) {
                    case MessageType::KEXINIT:
                        break 2;
                    case MessageType::DISCONNECT:
                        $this->handleDisconnect($kexinit_payload_server);
                }
                $this->kex_buffer[] = $kexinit_payload_server;
            }

            $send_kex = false;
        }

        $response = $kexinit_payload_server;
        Strings::shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT)
        Strings::shift($response, 16); // skip past the server cookie

        [
            $this->kex_algorithms,
            $this->server_host_key_algorithms,
            $this->encryption_algorithms_client_to_server,
            $this->encryption_algorithms_server_to_client,
            $this->mac_algorithms_client_to_server,
            $this->mac_algorithms_server_to_client,
            $this->compression_algorithms_client_to_server,
            $this->compression_algorithms_server_to_client,
            $this->languages_client_to_server,
            $this->languages_server_to_client,
            //$first_kex_packet_follows
        ] = Strings::unpackSSH2('L10C', $response);
        if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) {
            if (!isset($this->session_id)) {
                // [kex-strict-s-v00@openssh.com is] only valid in the initial SSH2_MSG_KEXINIT and MUST be ignored
                // if [it is] present in subsequent SSH2_MSG_KEXINIT packets
                $this->strict_kex_flag = true;
                if (count($this->kex_buffer)) {
                    throw new UnexpectedValueException('Possible Terrapin Attack detected');
                }
            }
        }

        $this->supported_private_key_algorithms = $this->server_host_key_algorithms;

        if ($send_kex) {
            $this->send_binary_packet($kexinit_payload_client);
        }

        // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange

        // we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the
        // diffie-hellman key exchange as fast as possible
        $decrypt = self::array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client);
        if (!$decrypt || ($decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt)) === null) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible server to client encryption algorithms found');
        }

        $encrypt = self::array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server);
        if (!$encrypt || ($encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt)) === null) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible client to server encryption algorithms found');
        }

        // through diffie-hellman key exchange a symmetric key is obtained
        $this->kex_algorithm = self::array_intersect_first($kex_algorithms, $this->kex_algorithms);
        if (!$this->kex_algorithm) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible key exchange algorithms found');
        }

        $server_host_key_algorithm = self::array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms);
        if (!$server_host_key_algorithm) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible server host key algorithms found');
        }

        $mac_algorithm_out = self::array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server);
        if (!$mac_algorithm_out) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible client to server message authentication algorithms found');
        }

        $mac_algorithm_in = self::array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client);
        if (!$mac_algorithm_in) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible server to client message authentication algorithms found');
        }

        $compression_map = [
            'none' => self::NET_SSH2_COMPRESSION_NONE,
            'zlib' => self::NET_SSH2_COMPRESSION_ZLIB,
            'zlib@openssh.com' => self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH,
        ];

        $compression_algorithm_in = self::array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client);
        if (!$compression_algorithm_in) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible server to client compression algorithms found');
        }
        $this->decompress = $compression_map[$compression_algorithm_in];

        $compression_algorithm_out = self::array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server);
        if (!$compression_algorithm_out) {
            $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
            throw new NoSupportedAlgorithmsException('No compatible client to server compression algorithms found');
        }
        $this->compress = $compression_map[$compression_algorithm_out];

        $kexHash = match ($this->kex_algorithm) {
            'diffie-hellman-group15-sha512',
            'diffie-hellman-group16-sha512',
            'diffie-hellman-group17-sha512',
            'diffie-hellman-group18-sha512',
            'ecdh-sha2-nistp521'
                => new Hash('sha512'),
            'ecdh-sha2-nistp384'
                => new Hash('sha384'),
            'diffie-hellman-group-exchange-sha256',
            'diffie-hellman-group14-sha256',
            'ecdh-sha2-nistp256',
            'curve25519-sha256@libssh.org',
            'curve25519-sha256'
                => new Hash('sha256'),
            default
                => new Hash('sha1')
        };

        // Only relevant in diffie-hellman-group-exchange-sha{1,256}, otherwise empty.

        $exchange_hash_rfc4419 = '';

        if (str_starts_with($this->kex_algorithm, 'curve25519-sha256') || str_starts_with($this->kex_algorithm, 'ecdh-sha2-nistp')) {
            $curve = str_starts_with($this->kex_algorithm, 'curve25519-sha256') ?
                'Curve25519' :
                substr($this->kex_algorithm, 10);
            $ourPrivate = EC::createKey($curve);
            $ourPublicBytes = $ourPrivate->getPublicKey()->getEncodedCoordinates();
            $clientKexInitMessage = MessageTypeExtra::KEX_ECDH_INIT;
            $serverKexReplyMessage = MessageTypeExtra::KEX_ECDH_REPLY;
        } else {
            if (str_starts_with($this->kex_algorithm, 'diffie-hellman-group-exchange')) {
                $dh_group_sizes_packed = pack(
                    'NNN',
                    $this->kex_dh_group_size_min,
                    $this->kex_dh_group_size_preferred,
                    $this->kex_dh_group_size_max
                );
                $packet = pack(
                    'Ca*',
                    MessageTypeExtra::KEXDH_GEX_REQUEST,
                    $dh_group_sizes_packed
                );
                $this->send_binary_packet($packet);
                $this->updateLogHistory('UNKNOWN (34)', 'SSH_MSG_KEXDH_GEX_REQUEST');

                $response = $this->get_binary_packet_or_close(MessageTypeExtra::KEXDH_GEX_GROUP);
                [, $primeBytes, $gBytes] = Strings::unpackSSH2('Css', $response);
                $this->updateLogHistory('UNKNOWN (31)', 'SSH_MSG_KEXDH_GEX_GROUP');

                $prime = new BigInteger($primeBytes, -256);
                $g = new BigInteger($gBytes, -256);

                $exchange_hash_rfc4419 = $dh_group_sizes_packed . Strings::packSSH2(
                    'ss',
                    $primeBytes,
                    $gBytes
                );

                $params = DH::createParameters($prime, $g);
                $clientKexInitMessage = MessageTypeExtra::KEXDH_GEX_INIT;
                $serverKexReplyMessage = MessageTypeExtra::KEXDH_GEX_REPLY;
            } else {
                $params = DH::createParameters($this->kex_algorithm);
                $clientKexInitMessage = MessageType::KEXDH_INIT;
                $serverKexReplyMessage = MessageType::KEXDH_REPLY;
            }

            $keyLength = min($kexHash->getLengthInBytes(), max($encryptKeyLength, $decryptKeyLength));

            $ourPrivate = DH::createKey($params, 16 * $keyLength); // 2 * 8 * $keyLength
            $ourPublic = $ourPrivate->getPublicKey()->toBigInteger();
            $ourPublicBytes = $ourPublic->toBytes(true);
        }

        $data = pack('CNa*', $clientKexInitMessage, strlen($ourPublicBytes), $ourPublicBytes);

        $this->send_binary_packet($data);

        switch ($clientKexInitMessage) {
            case MessageTypeExtra::KEX_ECDH_INIT:
                $this->updateLogHistory('SSH_MSG_KEXDH_INIT', 'SSH_MSG_KEX_ECDH_INIT');
                break;
            case MessageTypeExtra::KEXDH_GEX_INIT:
                $this->updateLogHistory('UNKNOWN (32)', 'SSH_MSG_KEXDH_GEX_INIT');
        }

        $response = $this->get_binary_packet_or_close($serverKexReplyMessage);

        [
            , // $type (which was already checked with get_binary_packet_or_close()
            $server_public_host_key,
            $theirPublicBytes,
            $this->signature
        ] = Strings::unpackSSH2('Csss', $response);

        switch ($serverKexReplyMessage) {
            case MessageTypeExtra::KEX_ECDH_REPLY:
                $this->updateLogHistory('SSH_MSG_KEXDH_REPLY', 'SSH_MSG_KEX_ECDH_REPLY');
                break;
            case MessageTypeExtra::KEXDH_GEX_REPLY:
                $this->updateLogHistory('UNKNOWN (33)', 'SSH_MSG_KEXDH_GEX_REPLY');
        }

        $this->server_public_host_key = $server_public_host_key;
        [$public_key_format] = Strings::unpackSSH2('s', $server_public_host_key);
        if (strlen($this->signature) < 4) {
            throw new LengthException('The signature needs at least four bytes');
        }
        $temp = unpack('Nlength', substr($this->signature, 0, 4));
        $this->signature_format = substr($this->signature, 4, $temp['length']);

        $keyBytes = DH::computeSecret($ourPrivate, $theirPublicBytes);
        if (($keyBytes & "\xFF\x80") === "\x00\x00") {
            $keyBytes = substr($keyBytes, 1);
        } elseif (($keyBytes[0] & "\x80") === "\x80") {
            $keyBytes = "\0$keyBytes";
        }

        $this->exchange_hash = Strings::packSSH2(
            's5',
            $this->identifier,
            $this->server_identifier,
            $kexinit_payload_client,
            $kexinit_payload_server,
            $this->server_public_host_key
        );
        $this->exchange_hash .= $exchange_hash_rfc4419;
        $this->exchange_hash .= Strings::packSSH2(
            's3',
            $ourPublicBytes,
            $theirPublicBytes,
            $keyBytes
        );

        $this->exchange_hash = $kexHash->hash($this->exchange_hash);
        $this->session_id ??= $this->exchange_hash;

        $expected_key_format = match ($server_host_key_algorithm) {
            'rsa-sha2-256', 'rsa-sha2-512' => 'ssh-rsa',
            default => $server_host_key_algorithm
        };

        if ($public_key_format != $expected_key_format || $this->signature_format != $server_host_key_algorithm) {
            switch (true) {
                case $this->signature_format == $server_host_key_algorithm:
                case $server_host_key_algorithm != 'rsa-sha2-256' && $server_host_key_algorithm != 'rsa-sha2-512':
                case $this->signature_format != 'ssh-rsa':
                    $this->disconnect_helper(DisconnectReason::HOST_KEY_NOT_VERIFIABLE);
                    throw new UnexpectedValueException('Server Host Key Algorithm Mismatch (' . $this->signature_format . ' vs ' . $server_host_key_algorithm . ')');
            }
        }

        $packet = pack('C', MessageType::NEWKEYS);
        $this->send_binary_packet($packet);
        $this->get_binary_packet_or_close(MessageType::NEWKEYS);

        $this->keyExchangeInProgress = false;

        if ($this->strict_kex_flag) {
            $this->get_seq_no = $this->send_seq_no = 0;
        }

        $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes);

        $this->encrypt = self::encryption_algorithm_to_crypt_instance($encrypt);
        if ($this->encrypt) {
            if (self::$crypto_engine) {
                $this->encrypt->setPreferredEngine(self::$crypto_engine);
            }
            if ($this->encrypt->getBlockLengthInBytes()) {
                $this->encrypt_block_size = $this->encrypt->getBlockLengthInBytes();
            }
            $this->encrypt->disablePadding();

            if ($this->encrypt->usesIV()) {
                $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id);
                while ($this->encrypt_block_size > strlen($iv)) {
                    $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv);
                }
                $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size));
            }

            switch ($encrypt) {
                case 'aes128-gcm@openssh.com':
                case 'aes256-gcm@openssh.com':
                    $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id);
                    $this->encryptFixedPart = substr($nonce, 0, 4);
                    $this->encryptInvocationCounter = substr($nonce, 4, 8);
                    // no break
                case 'chacha20-poly1305@openssh.com':
                    break;
                default:
                    $this->encrypt->enableContinuousBuffer();
            }

            $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id);
            while ($encryptKeyLength > strlen($key)) {
                $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key);
            }
            switch ($encrypt) {
                case 'chacha20-poly1305@openssh.com':
                    $encryptKeyLength = 32;
                    $this->lengthEncrypt = self::encryption_algorithm_to_crypt_instance($encrypt);
                    $this->lengthEncrypt->setKey(substr($key, 32, 32));
            }
            $this->encrypt->setKey(substr($key, 0, $encryptKeyLength));
            $this->encryptName = $encrypt;
        }

        $this->decrypt = self::encryption_algorithm_to_crypt_instance($decrypt);
        if ($this->decrypt) {
            if (self::$crypto_engine) {
                $this->decrypt->setPreferredEngine(self::$crypto_engine);
            }
            if ($this->decrypt->getBlockLengthInBytes()) {
                $this->decrypt_block_size = $this->decrypt->getBlockLengthInBytes();
            }
            $this->decrypt->disablePadding();

            if ($this->decrypt->usesIV()) {
                $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id);
                while ($this->decrypt_block_size > strlen($iv)) {
                    $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv);
                }
                $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size));
            }

            switch ($decrypt) {
                case 'aes128-gcm@openssh.com':
                case 'aes256-gcm@openssh.com':
                    // see https://tools.ietf.org/html/rfc5647#section-7.1
                    $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id);
                    $this->decryptFixedPart = substr($nonce, 0, 4);
                    $this->decryptInvocationCounter = substr($nonce, 4, 8);
                    // no break
                case 'chacha20-poly1305@openssh.com':
                    break;
                default:
                    $this->decrypt->enableContinuousBuffer();
            }

            $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id);
            while ($decryptKeyLength > strlen($key)) {
                $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key);
            }
            switch ($decrypt) {
                case 'chacha20-poly1305@openssh.com':
                    $decryptKeyLength = 32;
                    $this->lengthDecrypt = self::encryption_algorithm_to_crypt_instance($decrypt);
                    $this->lengthDecrypt->setKey(substr($key, 32, 32));
            }
            $this->decrypt->setKey(substr($key, 0, $decryptKeyLength));
            $this->decryptName = $decrypt;
        }

        /* The "arcfour128" algorithm is the RC4 cipher, as described in
           [SCHNEIER], using a 128-bit key.  The first 1536 bytes of keystream
           generated by the cipher MUST be discarded, and the first byte of the
           first encrypted packet MUST be encrypted using the 1537th byte of
           keystream.

           -- http://tools.ietf.org/html/rfc4345#section-4 */
        if ($encrypt == 'arcfour128' || $encrypt == 'arcfour256') {
            $this->encrypt->encrypt(str_repeat("\0", 1536));
        }
        if ($decrypt == 'arcfour128' || $decrypt == 'arcfour256') {
            $this->decrypt->decrypt(str_repeat("\0", 1536));
        }

        if (!$this->encrypt->usesNonce()) {
            [$this->hmac_create, $createKeyLength] = self::mac_algorithm_to_hash_instance($mac_algorithm_out);
        } else {
            $this->hmac_create = new \stdClass();
            $this->hmac_create_name = $mac_algorithm_out;
            //$mac_algorithm_out = 'none';
            $createKeyLength = 0;
        }

        if ($this->hmac_create instanceof Hash) {
            $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'E' . $this->session_id);
            while ($createKeyLength > strlen($key)) {
                $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key);
            }
            $this->hmac_create->setKey(substr($key, 0, $createKeyLength));
            $this->hmac_create_name = $mac_algorithm_out;
            $this->hmac_create_etm = (bool) preg_match('#-etm@openssh\.com$#', $mac_algorithm_out);
        }

        if (!$this->decrypt->usesNonce()) {
            [$this->hmac_check, $checkKeyLength] = self::mac_algorithm_to_hash_instance($mac_algorithm_in);
            $this->hmac_size = $this->hmac_check->getLengthInBytes();
        } else {
            $this->hmac_check = new \stdClass();
            $this->hmac_check_name = $mac_algorithm_in;
            //$mac_algorithm_in = 'none';
            $checkKeyLength = 0;
            $this->hmac_size = 0;
        }

        if ($this->hmac_check instanceof Hash) {
            $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'F' . $this->session_id);
            while ($checkKeyLength > strlen($key)) {
                $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key);
            }
            $this->hmac_check->setKey(substr($key, 0, $checkKeyLength));
            $this->hmac_check_name = $mac_algorithm_in;
            $this->hmac_check_etm = (bool) preg_match('#-etm@openssh\.com$#', $mac_algorithm_in);
        }

        $this->regenerate_compression_context = $this->regenerate_decompression_context = true;

        return true;
    }

    /**
     * Maps an encryption algorithm name to the number of key bytes.
     *
     * @param string $algorithm Name of the encryption algorithm
     * @return int|null Number of bytes as an integer or null for unknown
     */
    private function encryption_algorithm_to_key_size(string $algorithm): int
    {
        if ($this->bad_key_size_fix && self::bad_algorithm_candidate($algorithm)) {
            return 16;
        }

        return match ($algorithm) {
            'none'
                => 0,
            'aes128-gcm@openssh.com',
            'aes128-cbc',
            'aes128-ctr',
            'arcfour',
            'arcfour128',
            'blowfish-cbc',
            'blowfish-ctr',
            'twofish128-cbc',
            'twofish128-ctr'
                => 16,
            '3des-cbc',
            '3des-ctr',
            'aes192-cbc',
            'aes192-ctr',
            'twofish192-cbc',
            'twofish192-ctr'
                => 24,
            'aes256-gcm@openssh.com',
            'aes256-cbc',
            'aes256-ctr',
            'arcfour256',
            'twofish-cbc',
            'twofish256-cbc',
            'twofish256-ctr'
                => 32,
            'chacha20-poly1305@openssh.com'
                => 64
        };
    }

    /**
     * Maps an encryption algorithm name to an instance of a subclass of
     * \phpseclib4\Crypt\Common\SymmetricKey.
     */
    private static function encryption_algorithm_to_crypt_instance(string $algorithm): ?SymmetricKey
    {
        return match ($algorithm) {
            '3des-cbc' => new TripleDES('cbc'),
            '3des-ctr' => new TripleDES('cbc'),
            'aes256-cbc', 'aes192-cbc', 'aes128-cbc' => new Rijndael('cbc'),
            'aes256-ctr', 'aes192-ctr', 'aes128-ctr' => new Rijndael('ctr'),
            'blowfish-cbc' => new Blowfish('cbc'),
            'blowfish-ctr' => new Blowfish('ctr'),
            'twofish128-cbc', 'twofish192-cbc', 'twofish256-cbc', 'twofish-cbc' => new Twofish('cbc'),
            'twofish128-ctr', 'twofish192-ctr', 'twofish256-ctr' => new Twofish('ctr'),
            'arcfour', 'arcfour128', 'arcfour256' => new RC4(),
            'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com' => new Rijndael('gcm'),
            'chacha20-poly1305@openssh.com' => new ChaCha20(),
            default => null
        };
    }

    /**
     * Maps a hash algorithm name to an instance of a subclass of
     * \phpseclib4\Crypt\Hash.
     *
     * @return array{Hash, int}|null
     */
    private static function mac_algorithm_to_hash_instance(string $algorithm): ?array
    {
        return match ($algorithm) {
            'umac-64@openssh.com', 'umac-64-etm@openssh.com' => [new Hash('umac-64'), 16],
            'umac-128@openssh.com', 'umac-128-etm@openssh.com' => [new Hash('umac-128'), 16],
            'hmac-sha2-512', 'hmac-sha2-512-etm@openssh.com' => [new Hash('sha512'), 64],
            'hmac-sha2-256', 'hmac-sha2-256-etm@openssh.com' => [new Hash('sha256'), 32],
            'hmac-sha1', 'hmac-sha1-etm@openssh.com' => [new Hash('sha1'), 20],
            'hmac-sha1-96' => [new Hash('sha1-96'), 20],
            'hmac-md5' => [new Hash('md5'), 16],
            'hmac-md5-96' => [new Hash('md5-96'), 16],
            default => null
        };
    }

    /**
     * Tests whether or not proposed algorithm has a potential for issues
     *
     * @link https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/ssh2-aesctr-openssh.html
     * @link https://bugzilla.mindrot.org/show_bug.cgi?id=1291
     * @param string $algorithm Name of the encryption algorithm
     */
    private static function bad_algorithm_candidate(string $algorithm): bool
    {
        return match ($algorithm) {
            'arcfour256', 'aes192-ctr', 'aes256-ctr' => true,
            default => false
        };
    }

    /**
     * Login
     *
     * The $password parameter can be a plaintext password, a \phpseclib4\Crypt\RSA|EC|DSA object, a \phpseclib4\System\SSH\Agent object or an array
     *
     * @see self::_login()
     */
    public function login(string $username, #[\SensitiveParameter] string|PrivateKey|array|Agent ...$args): bool
    {
        if (!($this->bitmap & self::MASK_CONSTRUCTOR)) {
            $this->connect();
        }

        if (!$this->login_credentials_finalized) {
            $this->auth[] = [$username, ...$args];
        }

        // try logging with 'none' as an authentication method first since that's what
        // PuTTY does
        if (
            substr($this->server_identifier ?: '', 0, 15) !== 'SSH-2.0-CoreFTP' &&
            $this->auth_methods_to_continue === null
        ) {
            if ($this->sublogin($username)) {
                return true;
            }
            if (!count($args)) {
                return false;
            }
        }
        return $this->sublogin($username, ...$args);
    }

    /**
     * Login Helper
     *
     * @see self::_login_helper()
     */
    protected function sublogin(string $username, #[\SensitiveParameter] string|PrivateKey|array|Agent ...$args): bool
    {
        if (empty($args)) {
            return $this->login_helper($username);
        }

        while (count($args)) {
            if (!$this->auth_methods_to_continue || !$this->smartMFA) {
                $newargs = $args;
                $args = [];
            } else {
                $newargs = [];
                foreach ($this->auth_methods_to_continue as $method) {
                    switch ($method) {
                        case 'publickey':
                            foreach ($args as $key => $arg) {
                                if ($arg instanceof PrivateKey || $arg instanceof Agent) {
                                    $newargs[] = $arg;
                                    unset($args[$key]);
                                    break;
                                }
                            }
                            break;
                        case 'keyboard-interactive':
                            $hasArray = $hasString = false;
                            foreach ($args as $arg) {
                                if ($hasArray || is_array($arg)) {
                                    $hasArray = true;
                                    break;
                                }
                                if ($hasString || is_string($arg)) {
                                    $hasString = true;
                                    break;
                                }
                            }
                            if ($hasArray && $hasString) {
                                foreach ($args as $arg) {
                                    if (is_array($arg)) {
                                        $newargs[] = $arg;
                                        break 2;
                                    }
                                }
                            }
                            // no break
                        case 'password':
                            foreach ($args as $key => $arg) {
                                $newargs[] = $arg;
                                unset($args[$key]);
                                break;
                            }
                    }
                }
            }

            if (!count($newargs)) {
                return $this->login_helper($username);
            }

            foreach ($newargs as $arg) {
                if ($this->login_helper($username, $arg)) {
                    $this->login_credentials_finalized = true;
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Login Helper
     *
     * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis}
     *           by sending dummy SSH_MSG_IGNORE messages.}
     */
    private function login_helper(
        string $username,
        #[\SensitiveParameter] string|PrivateKey|array|Agent|null $password = null
    ): bool {
        if (!($this->bitmap & self::MASK_LOGIN_REQ)) {
            $packet = Strings::packSSH2('Cs', MessageType::SERVICE_REQUEST, 'ssh-userauth');
            $this->send_binary_packet($packet);

            try {
                $response = $this->get_binary_packet_or_close(MessageType::SERVICE_ACCEPT);
            } catch (InvalidPacketLengthException $e) {
                // the first opportunity to encounter the "bad key size" error
                if (!$this->bad_key_size_fix && isset($this->decryptName) && self::bad_algorithm_candidate($this->decryptName)) {
                    // bad_key_size_fix is only ever re-assigned to true here
                    // retry the connection with that new setting but we'll
                    // only try it once.
                    $this->bad_key_size_fix = true;
                    return $this->reconnect();
                }
                throw $e;
            }

            [, $service] = Strings::unpackSSH2('Cs', $response);

            if ($service != 'ssh-userauth') {
                $this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
                throw new UnexpectedSSHMessageException('Expected SSH_MSG_SERVICE_ACCEPT');
            }
            $this->bitmap |= self::MASK_LOGIN_REQ;
        }

        if (strlen($this->last_interactive_response)) {
            return !is_string($password) && !is_array($password) ? false : $this->keyboard_interactive_process($password);
        }

        if ($password instanceof PrivateKey) {
            return $this->privatekey_login($username, $password);
        }

        if ($password instanceof Agent) {
            return $this->ssh_agent_login($username, $password);
        }

        if (is_array($password)) {
            if ($this->keyboard_interactive_login($username, $password)) {
                $this->bitmap |= self::MASK_LOGIN;
                return true;
            }
            return false;
        }

        if (!isset($password)) {
            $packet = Strings::packSSH2(
                'Cs3',
                MessageType::USERAUTH_REQUEST,
                $username,
                'ssh-connection',
                'none'
            );

            $this->send_binary_packet($packet);

            $response = $this->get_binary_packet_or_close();

            [$type] = Strings::unpackSSH2('C', $response);
            switch ($type) {
                case MessageType::USERAUTH_SUCCESS:
                    $this->bitmap |= self::MASK_LOGIN;
                    return true;
                case MessageType::USERAUTH_FAILURE:
                    [$auth_methods] = Strings::unpackSSH2('L', $response);
                    $this->auth_methods_to_continue = $auth_methods;
                    // no break
                default:
                    return false;
            }
        }

        $packet = Strings::packSSH2(
            'Cs3bs',
            MessageType::USERAUTH_REQUEST,
            $username,
            'ssh-connection',
            'password',
            false,
            $password
        );

        // remove the username and password from the logged packet
        if (!defined('NET_SSH2_LOGGING')) {
            $logged = null;
        } else {
            $logged = Strings::packSSH2(
                'Cs3bs',
                MessageType::USERAUTH_REQUEST,
                $username,
                'ssh-connection',
                'password',
                false,
                'password'
            );
        }

        $this->send_binary_packet($packet, $logged);

        $response = $this->get_binary_packet_or_close();
        [$type] = Strings::unpackSSH2('C', $response);

        switch ($type) {
            case MessageTypeExtra::USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed
                $this->updateLogHistory('SSH_MSG_USERAUTH_INFO_REQUEST', 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ');

                [$message] = Strings::unpackSSH2('s', $response);
                $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . $message;

                $this->disconnect_helper(DisconnectReason::AUTH_CANCELLED_BY_USER);
                return false;
            case MessageType::USERAUTH_FAILURE:
                // can we use keyboard-interactive authentication?  if not then either the login is bad or the server employees
                // multi-factor authentication
                [$auth_methods, $partial_success] = Strings::unpackSSH2('Lb', $response);
                $this->auth_methods_to_continue = $auth_methods;
                if (!$partial_success && in_array('keyboard-interactive', $auth_methods)) {
                    if ($this->keyboard_interactive_login($username, $password)) {
                        $this->bitmap |= self::MASK_LOGIN;
                        return true;
                    }
                    return false;
                }
                return false;
            case MessageType::USERAUTH_SUCCESS:
                $this->bitmap |= self::MASK_LOGIN;
                return true;
        }

        return false;
    }

    /**
     * Login via keyboard-interactive authentication
     *
     * See {@link http://tools.ietf.org/html/rfc4256 RFC4256} for details.  This is not a full-featured keyboard-interactive authenticator.
     */
    private function keyboard_interactive_login(string $username, #[\SensitiveParameter] string|array $password): bool
    {
        $packet = Strings::packSSH2(
            'Cs5',
            MessageType::USERAUTH_REQUEST,
            $username,
            'ssh-connection',
            'keyboard-interactive',
            '', // language tag
            '' // submethods
        );
        $this->send_binary_packet($packet);

        return $this->keyboard_interactive_process($password);
    }

    /**
     * Handle the keyboard-interactive requests / responses.
     */
    private function keyboard_interactive_process(string|array ...$responses): bool
    {
        if (strlen($this->last_interactive_response)) {
            $response = $this->last_interactive_response;
        } else {
            $orig = $response = $this->get_binary_packet_or_close();
        }

        [$type] = Strings::unpackSSH2('C', $response);
        switch ($type) {
            case MessageType::USERAUTH_INFO_REQUEST:
                [
                    , // name; may be empty
                    , // instruction; may be empty
                    , // language tag; may be empty
                    $num_prompts
                ] = Strings::unpackSSH2('s3N', $response);

                for ($i = 0; $i < count($responses); $i++) {
                    if (is_array($responses[$i])) {
                        foreach ($responses[$i] as $key => $value) {
                            $this->keyboard_requests_responses[$key] = $value;
                        }
                        unset($responses[$i]);
                    }
                }
                $responses = array_values($responses);

                if (isset($this->keyboard_requests_responses)) {
                    for ($i = 0; $i < $num_prompts; $i++) {
                        [
                            $prompt, // prompt - ie. "Password: "; must not be empty
                            // echo
                        ] = Strings::unpackSSH2('sC', $response);
                        foreach ($this->keyboard_requests_responses as $key => $value) {
                            if (substr($prompt, 0, strlen($key)) == $key) {
                                $responses[] = $value;
                                break;
                            }
                        }
                    }
                }

                // see http://tools.ietf.org/html/rfc4256#section-3.2
                if (strlen($this->last_interactive_response)) {
                    $this->last_interactive_response = '';
                } else {
                    $this->updateLogHistory('UNKNOWN (60)', 'SSH_MSG_USERAUTH_INFO_REQUEST');
                }

                if (!count($responses) && $num_prompts) {
                    $this->last_interactive_response = $orig;
                    return false;
                }

                /*
                   After obtaining the requested information from the user, the client
                   MUST respond with an SSH_MSG_USERAUTH_INFO_RESPONSE message.
                */
                // see http://tools.ietf.org/html/rfc4256#section-3.4
                $packet = $logged = pack('CN', MessageType::USERAUTH_INFO_RESPONSE, count($responses));
                for ($i = 0; $i < count($responses); $i++) {
                    $packet .= Strings::packSSH2('s', $responses[$i]);
                    $logged .= Strings::packSSH2('s', 'dummy-answer');
                }

                $this->send_binary_packet($packet, $logged);

                $this->updateLogHistory('UNKNOWN (61)', 'SSH_MSG_USERAUTH_INFO_RESPONSE');

                /*
                   After receiving the response, the server MUST send either an
                   SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_USERAUTH_FAILURE, or another
                   SSH_MSG_USERAUTH_INFO_REQUEST message.
                */
                // maybe phpseclib should force close the connection after x request / responses?  unless something like that is done
                // there could be an infinite loop of request / responses.
                return $this->keyboard_interactive_process();
            case MessageType::USERAUTH_SUCCESS:
                return true;
            case MessageType::USERAUTH_FAILURE:
                [$auth_methods] = Strings::unpackSSH2('L', $response);
                $this->auth_methods_to_continue = $auth_methods;
                return false;
        }

        return false;
    }

    /**
     * Login with an ssh-agent provided key
     */
    private function ssh_agent_login(string $username, Agent $agent): bool
    {
        $this->agent = $agent;
        $keys = $agent->requestIdentities();
        $orig_algorithms = $this->supported_private_key_algorithms;
        foreach ($keys as $key) {
            if ($this->privatekey_login($username, $key)) {
                return true;
            }
            $this->supported_private_key_algorithms = $orig_algorithms;
        }

        return false;
    }

    /**
     * Login with an RSA private key
     *
     * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis}
     *           by sending dummy SSH_MSG_IGNORE messages.}
     */
    private function privatekey_login(string $username, #[\SensitiveParameter] PrivateKey $privatekey): bool
    {
        $publickey = $privatekey->getPublicKey();

        if ($publickey instanceof RSA) {
            $privatekey = $privatekey->withPadding(RSA::SIGNATURE_PKCS1);
            $algos = ['rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa'];
            if (isset($this->preferred['hostkey'])) {
                $algos = array_intersect($algos, $this->preferred['hostkey']);
            }
            $algo = self::array_intersect_first($algos, $this->supported_private_key_algorithms);
            switch ($algo) {
                case 'rsa-sha2-512':
                    $hash = 'sha512';
                    $signatureType = 'rsa-sha2-512';
                    break;
                case 'rsa-sha2-256':
                    $hash = 'sha256';
                    $signatureType = 'rsa-sha2-256';
                    break;
                //case 'ssh-rsa':
                default:
                    $hash = 'sha1';
                    $signatureType = 'ssh-rsa';
            }
        } elseif ($publickey instanceof EC) {
            $privatekey = $privatekey->withSignatureFormat('SSH2');
            $curveName = $privatekey->getCurve();
            switch ($curveName) {
                case 'Ed25519':
                    $hash = 'sha512';
                    $signatureType = 'ssh-ed25519';
                    break;
                case 'secp256r1': // nistp256
                    $hash = 'sha256';
                    $signatureType = 'ecdsa-sha2-nistp256';
                    break;
                case 'secp384r1': // nistp384
                    $hash = 'sha384';
                    $signatureType = 'ecdsa-sha2-nistp384';
                    break;
                case 'secp521r1': // nistp521
                    $hash = 'sha512';
                    $signatureType = 'ecdsa-sha2-nistp521';
                    break;
                default:
                    if (is_array($curveName)) {
                        throw new UnsupportedCurveException('Specified Curves are not supported by SSH2');
                    }
                    throw new UnsupportedCurveException('Named Curve of ' . $curveName . ' is not supported by phpseclib4\'s SSH2 implementation');
            }
        } elseif ($publickey instanceof DSA) {
            $privatekey = $privatekey->withSignatureFormat('SSH2');
            $hash = 'sha1';
            $signatureType = 'ssh-dss';
        } else {
            throw new UnsupportedAlgorithmException('Please use either an RSA key, an EC one or a DSA key');
        }

        $publickeyStr = $publickey->toString('OpenSSH', ['binary' => true]);

        $part1 = Strings::packSSH2(
            'Csss',
            MessageType::USERAUTH_REQUEST,
            $username,
            'ssh-connection',
            'publickey'
        );
        $part2 = Strings::packSSH2('ss', $signatureType, $publickeyStr);

        $packet = $part1 . chr(0) . $part2;
        $this->send_binary_packet($packet);

        $response = $this->get_binary_packet_or_close(
            MessageType::USERAUTH_SUCCESS,
            MessageType::USERAUTH_FAILURE,
            MessageTypeExtra::USERAUTH_PK_OK
        );

        [$type] = Strings::unpackSSH2('C', $response);
        switch ($type) {
            case MessageType::USERAUTH_FAILURE:
                [$auth_methods] = Strings::unpackSSH2('L', $response);
                if (in_array('publickey', $auth_methods) && substr($signatureType, 0, 9) == 'rsa-sha2-') {
                    $this->supported_private_key_algorithms = array_diff($this->supported_private_key_algorithms, ['rsa-sha2-256', 'rsa-sha2-512']);
                    return $this->privatekey_login($username, $privatekey);
                }
                $this->auth_methods_to_continue = $auth_methods;
                $this->errors[] = 'SSH_MSG_USERAUTH_FAILURE';
                return false;
            case MessageTypeExtra::USERAUTH_PK_OK:
                // we'll just take it on faith that the public key blob and the public key algorithm name are as
                // they should be
                $this->updateLogHistory('SSH_MSG_USERAUTH_INFO_REQUEST', 'SSH_MSG_USERAUTH_PK_OK');
                break;
            case MessageType::USERAUTH_SUCCESS:
                $this->bitmap |= self::MASK_LOGIN;
                return true;
        }

        $packet = $part1 . chr(1) . $part2;
        $privatekey = $privatekey->withHash($hash);
        $signature = $privatekey->sign(Strings::packSSH2('s', $this->session_id) . $packet);
        if ($publickey instanceof RSA) {
            $signature = Strings::packSSH2('ss', $signatureType, $signature);
        }
        $packet .= Strings::packSSH2('s', $signature);

        $this->send_binary_packet($packet);

        $response = $this->get_binary_packet_or_close(
            MessageType::USERAUTH_SUCCESS,
            MessageType::USERAUTH_FAILURE
        );

        [$type] = Strings::unpackSSH2('C', $response);
        switch ($type) {
            case MessageType::USERAUTH_SUCCESS:
                $this->bitmap |= self::MASK_LOGIN;
                return true;
            //case MessageType::USERAUTH_FAILURE:
            default:
                // either the login is bad or the server employs multi-factor authentication
                [$auth_methods] = Strings::unpackSSH2('L', $response);
                $this->auth_methods_to_continue = $auth_methods;
                return false;
        }
    }

    /**
     * Return the currently configured timeout
     */
    public function getTimeout(): int
    {
        return $this->timeout;
    }

    /**
     * Set Timeout
     *
     * $ssh->exec('ping 127.0.0.1'); on a Linux host will never return and will run indefinitely.  setTimeout() makes it so it'll timeout.
     * Setting $timeout to false or 0 will revert to the default socket timeout.
     */
    public function setTimeout(int $timeout): void
    {
        $this->timeout = $this->curTimeout = $timeout;
    }

    /**
     * Set Keep Alive
     *
     * Sends an SSH2_MSG_IGNORE message every x seconds, if x is a positive non-zero number.
     */
    public function setKeepAlive(int $interval): void
    {
        $this->keepAlive = $interval;
    }

    /**
     * Get the output from stdError
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function getStdError(): string
    {
        return $this->stdErrorLog;
    }

    protected function initExec(string $command): void
    {
        $this->curTimeout = $this->timeout;
        $this->is_timeout = false;
        $this->stdErrorLog = '';

        //if ($this->isPTYOpen()) {
        //    throw new InvalidStateException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.');
        //}

        $this->open_channel(self::CHANNEL_EXEC);

        if ($this->request_pty === true) {
            $terminal_modes = pack('C', TerminalMode::TTY_OP_END);
            $packet = Strings::packSSH2(
                'CNsCsN4s',
                MessageType::CHANNEL_REQUEST,
                $this->server_channels[self::CHANNEL_EXEC],
                'pty-req',
                1,
                $this->term,
                $this->windowColumns,
                $this->windowRows,
                0,
                0,
                $terminal_modes
            );

            $this->send_binary_packet($packet);

            $this->channel_status[self::CHANNEL_EXEC] = MessageType::CHANNEL_REQUEST;
            if (!$this->get_channel_packet(self::CHANNEL_EXEC)) {
                $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
                throw new UnexpectedValueException('Unable to request pseudo-terminal');
            }
        }

        // sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things
        // down.  the one place where it might be desirable is if you're doing something like \phpseclib4\Net\SSH2::exec('ping localhost &').
        // with a pty-req SSH_MSG_CHANNEL_REQUEST, exec() will return immediately and the ping process will then
        // then immediately terminate.  without such a request exec() will loop indefinitely.  the ping process won't end but
        // neither will your script.

        // although, in theory, the size of SSH_MSG_CHANNEL_REQUEST could exceed the maximum packet size established by
        // SSH_MSG_CHANNEL_OPEN_CONFIRMATION, RFC4254#section-5.1 states that the "maximum packet size" refers to the
        // "maximum size of an individual data packet". ie. SSH_MSG_CHANNEL_DATA.  RFC4254#section-5.2 corroborates.
        $packet = Strings::packSSH2(
            'CNsCs',
            MessageType::CHANNEL_REQUEST,
            $this->server_channels[self::CHANNEL_EXEC],
            'exec',
            1,
            $command
        );
        $this->send_binary_packet($packet);

        $this->channel_status[self::CHANNEL_EXEC] = MessageType::CHANNEL_REQUEST;

        if (!$this->get_channel_packet(self::CHANNEL_EXEC)) {
            $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
            throw new UnexpectedValueException('Unable to execute command');
        }

        $this->channel_status[self::CHANNEL_EXEC] = MessageType::CHANNEL_DATA;
    }

    /**
     * Execute Command
     *
     * If $callback is set to false then \phpseclib4\Net\SSH2::get_channel_packet(self::CHANNEL_EXEC) will need to be called manually.
     * In all likelihood, this is not a feature you want to be taking advantage of (SCP.php uses it)
     *
     * @psalm-return ($callback is callable ? null : string)
     */
    public function exec(string $command, ?\Closure $callback = null): ?string
    {
        if (!$this->isAuthenticated()) {
            throw new InvalidStateException('Please login before attempting to call exec()');
        }

        $this->initExec($command);

        if ($this->request_pty === true) {
            $this->channel_id_last_interactive = self::CHANNEL_EXEC;
            return null;
        }

        $output = '';
        while (true) {
            $temp = $this->get_channel_packet(self::CHANNEL_EXEC);
            if ($temp === true) {
                return $callback ? null : $output;
            }
            if ($callback) {
                if ($callback($temp) === true) {
                    $this->close_channel(self::CHANNEL_EXEC);
                    return null;
                }
            } else {
                $output .= $temp;
            }
        }
    }

    /**
     * How many channels are currently open?
     */
    public function getOpenChannelCount(): int
    {
        return $this->channelCount;
    }

    /**
     * Opens a channel
     *
     * @psalm-suppress InvalidReturnType
     */
    protected function open_channel(int $channel, bool $skip_extended = false): bool
    {
        if (isset($this->channel_status[$channel])) {
            throw new InvalidStateException('Please close the channel (' . $channel . ') before trying to open it again');
        }

        $this->channelCount++;

        if ($this->channelCount > 1 && $this->errorOnMultipleChannels) {
            throw new UnsupportedValueException("Ubuntu's OpenSSH from 5.8 to 6.9 doesn't work with multiple channels");
        }

        // RFC4254 defines the (client) window size as "bytes the other party can send before it must wait for the window to
        // be adjusted".  0x7FFFFFFF is, at 2GB, the max size.  technically, it should probably be decremented, but,
        // honestly, if you're transferring more than 2GB, you probably shouldn't be using phpseclib, anyway.
        // see http://tools.ietf.org/html/rfc4254#section-5.2 for more info
        $this->window_size_server_to_client[$channel] = $this->window_size;
        // 0x8000 is the maximum max packet size, per http://tools.ietf.org/html/rfc4253#section-6.1, although since PuTTy
        // uses 0x4000, that's what will be used here, as well.
        $packet_size = 0x4000;

        $packet = Strings::packSSH2(
            'CsN3',
            MessageType::CHANNEL_OPEN,
            'session',
            $channel,
            $this->window_size_server_to_client[$channel],
            $packet_size
        );

        $this->send_binary_packet($packet);

        $this->channel_status[$channel] = MessageType::CHANNEL_OPEN;

        /** @psalm-suppress InvalidReturnStatement */
        return $this->get_channel_packet($channel, $skip_extended);
    }

    /**
     * Creates an interactive shell
     *
     * Returns bool(true) if the shell was opened.
     * Returns bool(false) if the shell was already open.
     *
     * @see self::isShellOpen()
     * @see self::read()
     * @see self::write()
     */
    public function openShell(): void
    {
        if (!$this->isAuthenticated()) {
            throw new InvalidStateException('Operation disallowed prior to login()');
        }

        $this->open_channel(self::CHANNEL_SHELL);

        $terminal_modes = pack('C', TerminalMode::TTY_OP_END);
        $packet = Strings::packSSH2(
            'CNsbsN4s',
            MessageType::CHANNEL_REQUEST,
            $this->server_channels[self::CHANNEL_SHELL],
            'pty-req',
            true, // want reply
            $this->term,
            $this->windowColumns,
            $this->windowRows,
            0,
            0,
            $terminal_modes
        );

        $this->send_binary_packet($packet);

        $this->channel_status[self::CHANNEL_SHELL] = MessageType::CHANNEL_REQUEST;

        if (!$this->get_channel_packet(self::CHANNEL_SHELL)) {
            throw new UnexpectedValueException('Unable to request pty');
        }

        $packet = Strings::packSSH2(
            'CNsb',
            MessageType::CHANNEL_REQUEST,
            $this->server_channels[self::CHANNEL_SHELL],
            'shell',
            true // want reply
        );
        $this->send_binary_packet($packet);

        $response = $this->get_channel_packet(self::CHANNEL_SHELL);
        if ($response === false) {
            throw new UnexpectedValueException('Unable to request shell');
        }

        $this->channel_status[self::CHANNEL_SHELL] = MessageType::CHANNEL_DATA;

        $this->channel_id_last_interactive = self::CHANNEL_SHELL;

        $this->bitmap |= self::MASK_SHELL;
    }

    /**
     * Return the channel to be used with read(), write(), and reset(), if none were specified
     * @deprecated for lack of transparency in intended channel target, to be potentially replaced
     *             with method which guarantees open-ness of all yielded channels and throws
     *             error for multiple open channels
     * @see self::read()
     * @see self::write()
     */
    private function get_interactive_channel(): int
    {
        switch (true) {
            case $this->is_channel_status_data(self::CHANNEL_SUBSYSTEM):
                return self::CHANNEL_SUBSYSTEM;
            case $this->is_channel_status_data(self::CHANNEL_EXEC):
                return self::CHANNEL_EXEC;
            default:
                return self::CHANNEL_SHELL;
        }
    }

    /**
     * Indicates the DATA status on the given channel
     */
    private function is_channel_status_data(int $channel): bool
    {
        return isset($this->channel_status[$channel]) && $this->channel_status[$channel] == MessageType::CHANNEL_DATA;
    }

    /**
     * Return an available open channel
     */
    private function get_open_channel(): ?int
    {
        $channel = self::CHANNEL_EXEC;
        do {
            if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == MessageType::CHANNEL_OPEN) {
                return $channel;
            }
        } while ($channel++ < self::CHANNEL_SUBSYSTEM);

        return null;
    }

    /**
     * Request agent forwarding of remote server
     */
    public function requestAgentForwarding(): bool
    {
        $request_channel = $this->get_open_channel();
        if ($request_channel === null) {
            return false;
        }

        $packet = Strings::packSSH2(
            'CNsC',
            MessageType::CHANNEL_REQUEST,
            $this->server_channels[$request_channel],
            'auth-agent-req@openssh.com',
            1
        );

        $this->channel_status[$request_channel] = MessageType::CHANNEL_REQUEST;

        $this->send_binary_packet($packet);

        if (!$this->get_channel_packet($request_channel)) {
            return false;
        }

        $this->channel_status[$request_channel] = MessageType::CHANNEL_OPEN;

        return true;
    }

    /**
     * Returns the output of an interactive shell
     *
     * Returns when there's a match for $expect, which can take the form of a string literal or,
     * if $mode == self::READ_REGEX, a regular expression.
     *
     * If not specifying a channel, an open interactive channel will be selected, or, if there are
     * no open channels, an interactive shell will be created. If there are multiple open
     * interactive channels, a legacy behavior will apply in which channel selection prioritizes
     * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive
     * channels, callers are discouraged from relying on this legacy behavior and should specify
     * the intended channel.
     *
     * @see self::write()
     */
    public function read(string $expect = '', int $mode = self::READ_SIMPLE, ?int $channel = null): string|bool
    {
        if (!$this->isAuthenticated()) {
            throw new InvalidStateException('Operation disallowed prior to login()');
        }

        $this->curTimeout = $this->timeout;
        $this->is_timeout = false;

        if ($channel === null) {
            $channel = $this->get_interactive_channel();
        }

        if (!$this->is_channel_status_data($channel) && empty($this->channel_buffers[$channel])) {
            if ($channel != self::CHANNEL_SHELL) {
                throw new InvalidStateException('Data is not available on channel');
            }
            $this->openShell();
        }

        if ($mode == self::READ_NEXT) {
            return $this->get_channel_packet($channel);
        }

        $match = $expect;
        while (true) {
            if ($mode == self::READ_REGEX) {
                preg_match($expect, substr($this->interactiveBuffer, -1024), $matches);
                $match = $matches[0] ?? '';
            }
            $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false;
            if ($pos !== false) {
                return Strings::shift($this->interactiveBuffer, $pos + strlen($match));
            }
            $response = $this->get_channel_packet($channel);
            if ($response === true) {
                return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer));
            }

            $this->interactiveBuffer .= $response;
        }
    }

    /**
     * Inputs a command into an interactive shell.
     *
     * If not specifying a channel, an open interactive channel will be selected, or, if there are
     * no open channels, an interactive shell will be created. If there are multiple open
     * interactive channels, a legacy behavior will apply in which channel selection prioritizes
     * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive
     * channels, callers are discouraged from relying on this legacy behavior and should specify
     * the intended channel.
     *
     * @param int|null $channel Channel id returned by self::getInteractiveChannelId()
     * @see SSH2::read()
     */
    public function write(string $cmd, ?int $channel = null): void
    {
        if (!$this->isAuthenticated()) {
            throw new InvalidStateException('Operation disallowed prior to login()');
        }

        if ($channel === null) {
            $channel = $this->get_interactive_channel();
        }

        if (!$this->is_channel_status_data($channel)) {
            if ($channel != self::CHANNEL_SHELL) {
                throw new UnexpectedValueException('Data is not available on channel');
            }
            $this->openShell();
        }

        $this->curTimeout = $this->timeout;
        $this->is_timeout = false;
        $this->send_channel_packet($channel, $cmd);
    }

    /**
     * Start a subsystem.
     *
     * Right now only one subsystem at a time is supported. To support multiple subsystem's stopSubsystem() could accept
     * a string that contained the name of the subsystem, but at that point, only one subsystem of each type could be opened.
     * To support multiple subsystem's of the same name maybe it'd be best if startSubsystem() generated a new channel id and
     * returns that and then that that was passed into stopSubsystem() but that'll be saved for a future date and implemented
     * if there's sufficient demand for such a feature.
     *
     * @see self::stopSubsystem()
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function startSubsystem(string $subsystem): bool
    {
        $this->open_channel(self::CHANNEL_SUBSYSTEM);

        $packet = Strings::packSSH2(
            'CNsCs',
            MessageType::CHANNEL_REQUEST,
            $this->server_channels[self::CHANNEL_SUBSYSTEM],
            'subsystem',
            1,
            $subsystem
        );
        $this->send_binary_packet($packet);

        $this->channel_status[self::CHANNEL_SUBSYSTEM] = MessageType::CHANNEL_REQUEST;

        if (!$this->get_channel_packet(self::CHANNEL_SUBSYSTEM)) {
            return false;
        }

        $this->channel_status[self::CHANNEL_SUBSYSTEM] = MessageType::CHANNEL_DATA;

        $this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM;

        return true;
    }

    /**
     * Stops a subsystem.
     *
     * @see self::startSubsystem()
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function stopSubsystem(): void
    {
        if ($this->isInteractiveChannelOpen(self::CHANNEL_SUBSYSTEM)) {
            $this->close_channel(self::CHANNEL_SUBSYSTEM);
        }
    }

    /**
     * Closes a channel
     *
     * If read() timed out you might want to just close the channel and have it auto-restart on the next read() call
     *
     * If not specifying a channel, an open interactive channel will be selected. If there are
     * multiple open interactive channels, a legacy behavior will apply in which channel selection
     * prioritizes an active subsystem, the exec pty, and, lastly, the shell. If using multiple
     * interactive channels, callers are discouraged from relying on this legacy behavior and
     * should specify the intended channel.
     *
     * @param int|null $channel Channel id returned by self::getInteractiveChannelId()
     */
    public function reset(?int $channel = null): void
    {
        if ($channel === null) {
            $channel = $this->get_interactive_channel();
        }
        if ($this->isInteractiveChannelOpen($channel)) {
            $this->close_channel($channel);
        }
    }

    /**
     * Send EOF on a channel
     *
     * Sends an EOF to the stream; this is typically used to close standard
     * input, while keeping output and error alive.
     *
     * @param int|null $channel Channel id returned by self::getInteractiveChannelId()
     * @return void
     */
    public function sendEOF(?int $channel = null): void
    {
        if ($channel === null) {
            $channel = $this->get_interactive_channel();
        }

        $excludeStatuses = [MessageType::CHANNEL_EOF, MessageType::CHANNEL_CLOSE];
        if (isset($this->channel_status[$channel]) && !in_array($this->channel_status[$channel], $excludeStatuses)) {
            $this->send_binary_packet(pack('CN', MessageType::CHANNEL_EOF, $this->server_channels[$channel]));
        }
    }

    /**
     * Is timeout?
     *
     * Did exec() or read() return because they timed out or because they encountered the end?
     */
    public function isTimeout(): bool
    {
        return $this->is_timeout;
    }

    /**
     * Disconnect
     */
    public function disconnect(): void
    {
        $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
        if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) {
            fclose($this->realtime_log_file);
        }
        unset(self::$connections[$this->getResourceId()]);
    }

    /**
     * Destructor.
     *
     * Will be called, automatically, if you're supporting just PHP5.  If you're supporting PHP4, you'll need to call
     * disconnect().
     */
    public function __destruct()
    {
        $this->disconnect();
    }

    /**
     * Is the connection still active?
     *
     * $level has 3x possible values:
     * 0 (default): phpseclib takes a passive approach to see if the connection is still active by calling feof()
     *    on the socket
     * 1: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_IGNORE
     *    packet that doesn't require a response
     * 2: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_CHANNEL_OPEN
     *    packet and imediately trying to close that channel. some routers, in particular, however, will only let you
     *    open one channel, so this approach could yield false positives
     */
    public function isConnected(int $level = 0): bool
    {
        if ($level < 0 || $level > 2) {
            throw new InvalidArgumentException('$level must be 0, 1 or 2');
        }

        if ($level == 0) {
            return ($this->bitmap & self::MASK_CONNECTED) && is_resource($this->fsock) && !feof($this->fsock);
        }
        try {
            if ($level == 1) {
                $this->send_binary_packet(pack('CN', MessageType::IGNORE, 0));
            } else {
                $this->open_channel(self::CHANNEL_KEEP_ALIVE);
                $this->close_channel(self::CHANNEL_KEEP_ALIVE);
            }
            return true;
        } catch (\Exception) {
            return false;
        }
    }

    /**
     * Have you successfully been logged in?
     */
    public function isAuthenticated(): bool
    {
        return (bool) ($this->bitmap & self::MASK_LOGIN);
    }

    /**
     * Is the interactive shell active?
     */
    public function isShellOpen(): bool
    {
        return $this->isInteractiveChannelOpen(self::CHANNEL_SHELL);
    }

    /**
     * Is the exec pty active?
     */
    public function isPTYOpen(): bool
    {
        return $this->isInteractiveChannelOpen(self::CHANNEL_EXEC);
    }

    /**
     * Is the given interactive channel active?
     *
     * @param int $channel Channel id returned by self::getInteractiveChannelId()
     */
    public function isInteractiveChannelOpen(int $channel): bool
    {
        return $this->isAuthenticated() && $this->is_channel_status_data($channel);
    }

    /**
     * Returns a channel identifier, presently of the last interactive channel opened, regardless of current status.
     * Returns 0 if no interactive channel has been opened.
     *
     * @see self::isInteractiveChannelOpen()
     */
    public function getInteractiveChannelId(): int
    {
        return $this->channel_id_last_interactive;
    }

    /**
     * Pings a server connection, or tries to reconnect if the connection has gone down
     *
     * Inspired by http://php.net/manual/en/mysqli.ping.php
     */
    public function ping(): bool
    {
        if (!$this->isAuthenticated()) {
            if (!empty($this->auth)) {
                return $this->reconnect();
            }
            return false;
        }

        try {
            $this->open_channel(self::CHANNEL_KEEP_ALIVE);
        } catch (InvalidStateException) {
            return $this->reconnect();
        }

        $this->close_channel(self::CHANNEL_KEEP_ALIVE);
        return true;
    }

    /**
     * In situ reconnect method
     *
     * @return boolean
     */
    private function reconnect(): bool
    {
        $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
        $this->connect();
        foreach ($this->auth as $auth) {
            $result = $this->login(...$auth);
        }
        return $result;
    }

    /**
     * Resets a connection for re-use
     */
    protected function reset_connection(): void
    {
        if (is_resource($this->fsock) && get_resource_type($this->fsock) === 'stream') {
            fclose($this->fsock);
        }
        $this->fsock = null;
        $this->bitmap = 0;
        $this->binary_packet_buffer = null;
        $this->decrypt = $this->encrypt = null;
        $this->decrypt_block_size = $this->encrypt_block_size = 8;
        $this->hmac_check = $this->hmac_create = null;
        $this->hmac_size = null;
        $this->session_id = null;
        $this->get_seq_no = $this->send_seq_no = 0;
        $this->channel_status = [];
        $this->channel_id_last_interactive = 0;
        $this->channel_buffers = [];
        $this->channel_buffers_write = [];
    }

    /**
     * @return int[] second and microsecond stream timeout options based on user-requested timeout and keep-alive, or the default socket timeout by default, which mirrors PHP socket streams.
     */
    private function get_stream_timeout(): array
    {
        $sec = (int) ini_get('default_socket_timeout');
        $usec = 0;
        if ($this->curTimeout > 0) {
            $sec = (int) floor($this->curTimeout);
            $usec = (int) (1000000 * ($this->curTimeout - $sec));
        }
        if ($this->keepAlive > 0) {
            $elapsed = microtime(true) - $this->last_packet;
            $timeout = max($this->keepAlive - $elapsed, 0);
            if (!$this->curTimeout || $timeout < $this->curTimeout) {
                $sec = (int) floor($timeout);
                $usec = (int) (1000000 * ($timeout - $sec));
            }
        }
        return [$sec, $usec];
    }

    /**
     * Retrieves the next packet with added timeout and type handling
     */
    private function get_binary_packet_or_close(int ...$message_types): string
    {
        try {
            $packet = $this->get_binary_packet();
            if (count($message_types) > 0 && !in_array(ord($packet[0]), $message_types)) {
                $this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
                throw new UnexpectedSSHMessageException('Bad message type. Expected: #'
                    . implode(', #', $message_types) . '. Got: #' . ord($packet[0]));
            }
            return $packet;
        } catch (TimeoutException) {
            $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
            throw new TimeoutException('Connection closed due to timeout');
        }
    }

    /**
     * Gets Binary Packets
     *
     * See '6. Binary Packet Protocol' of rfc4253 for more info.
     *
     * @see self::_send_binary_packet()
     * @throws TimeoutException If user requested timeout was reached while waiting for next packet
     * @throws ConnectionClosedException If an error has occurred preventing read of the next packet
     */
    private function get_binary_packet(): string
    {
        if (!is_resource($this->fsock)) {
            throw new InvalidArgumentException('fsock is not a resource.');
        }
        if (!$this->keyExchangeInProgress && count($this->kex_buffer)) {
            return $this->filter(array_shift($this->kex_buffer));
        }
        if ($this->binary_packet_buffer == null) {
            // buffer the packet to permit continued reads across timeouts
            $this->binary_packet_buffer = (object) [
                'read_time' => 0, // the time to read the packet from the socket
                'raw' => '', // the raw payload read from the socket
                'plain' => '', // the packet in plain text, excluding packet_length header
                'packet_length' => null, // the packet_length value pulled from the payload
                'size' => $this->decrypt_block_size, // the total size of this packet to be read from the socket
                                                     // initialize to read single block until packet_length is available
            ];
        }
        $packet = $this->binary_packet_buffer;
        while (strlen($packet->raw) < $packet->size) {
            if (feof($this->fsock)) {
                $this->disconnect_helper(DisconnectReason::CONNECTION_LOST);
                throw new ConnectionClosedException('Connection closed by server');
            }
            if ($this->curTimeout < 0) {
                $this->is_timeout = true;
                throw new TimeoutException('Timed out waiting for server');
            }
            $this->send_keep_alive();

            [$sec, $usec] = $this->get_stream_timeout();
            stream_set_timeout($this->fsock, $sec, $usec);
            $start = microtime(true);
            $raw = stream_get_contents($this->fsock, $packet->size - strlen($packet->raw));
            $elapsed = microtime(true) - $start;
            $packet->read_time += $elapsed;
            if ($this->curTimeout > 0) {
                $this->curTimeout -= $elapsed;
            }
            if ($raw === false) {
                $this->disconnect_helper(DisconnectReason::CONNECTION_LOST);
                throw new ConnectionClosedException('Connection closed by server');
            } elseif (!strlen($raw)) {
                continue;
            }
            $packet->raw .= $raw;
            if (!$packet->packet_length) {
                $this->get_binary_packet_size($packet);
            }
        }

        if (strlen($packet->raw) != $packet->size) {
            throw new UnexpectedValueException('Size of packet was not expected length');
        }
        // destroy buffer as packet represents the entire payload and should be processed in full
        $this->binary_packet_buffer = null;
        // copy the raw payload, so as not to destroy original
        $raw = $packet->raw;
        if ($this->hmac_check instanceof Hash) {
            $hmac = Strings::pop($raw, $this->hmac_size);
        }
        $packet_length_header_size = 4;
        if ($this->decrypt) {
            switch ($this->decryptName) {
                case 'aes128-gcm@openssh.com':
                case 'aes256-gcm@openssh.com':
                    $this->decrypt->setNonce(
                        $this->decryptFixedPart .
                        $this->decryptInvocationCounter
                    );
                    Strings::increment_str($this->decryptInvocationCounter);
                    $this->decrypt->setAAD(Strings::shift($raw, $packet_length_header_size));
                    $this->decrypt->setTag(Strings::pop($raw, $this->decrypt_block_size));
                    $packet->plain = $this->decrypt->decrypt($raw);
                    break;
                case 'chacha20-poly1305@openssh.com':
                    // This should be impossible, but we are checking anyway to narrow the type for Psalm.
                    if (!($this->decrypt instanceof ChaCha20)) {
                        throw new InvalidStateException('$this->decrypt is not a ' . ChaCha20::class);
                    }
                    $this->decrypt->setNonce(pack('N2', 0, $this->get_seq_no));
                    $this->decrypt->setCounter(0);
                    // this is the same approach that's implemented in Salsa20::createPoly1305Key()
                    // but we don't want to use the same AEAD construction that RFC8439 describes
                    // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305())
                    $this->decrypt->setPoly1305Key(
                        $this->decrypt->encrypt(str_repeat("\0", 32))
                    );
                    $this->decrypt->setAAD(Strings::shift($raw, $packet_length_header_size));
                    $this->decrypt->setCounter(1);
                    $this->decrypt->setTag(Strings::pop($raw, 16));
                    $packet->plain = $this->decrypt->decrypt($raw);
                    break;
                default:
                    if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) {
                        // first block was already decrypted for contained packet_length header
                        Strings::shift($raw, $this->decrypt_block_size);
                        if (strlen($raw) > 0) {
                            $packet->plain .= $this->decrypt->decrypt($raw);
                        }
                    } else {
                        Strings::shift($raw, $packet_length_header_size);
                        $packet->plain = $this->decrypt->decrypt($raw);
                    }
                    break;
            }
        } else {
            Strings::shift($raw, $packet_length_header_size);
            $packet->plain = $raw;
        }
        if ($this->hmac_check instanceof Hash) {
            $reconstructed = !$this->hmac_check_etm ?
                pack('Na*', $packet->packet_length, $packet->plain) :
                substr($packet->raw, 0, -$this->hmac_size);
            if (($this->hmac_check->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') {
                $this->hmac_check->setNonce("\0\0\0\0" . pack('N', $this->get_seq_no));
                if (!hash_equals($hmac, $this->hmac_check->hash($reconstructed))) {
                    $this->disconnect_helper(DisconnectReason::MAC_ERROR);
                    throw new UnexpectedValueException('Invalid UMAC');
                }
            } else {
                if (!hash_equals($hmac, $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed)))) {
                    $this->disconnect_helper(DisconnectReason::MAC_ERROR);
                    throw new UnexpectedValueException('Invalid HMAC');
                }
            }
        }

        $payload = $packet->plain;
        ['padding_length' => $padding_length] = unpack('Cpadding_length', Strings::shift($payload, 1));
        if ($padding_length > 0) {
            Strings::pop($payload, $padding_length);
        }

        if (!$this->keyExchangeInProgress) {
            $this->bytesTransferredSinceLastKEX += $packet->packet_length + $padding_length + 5;
        }

        if (empty($payload)) {
            $this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
            throw new UnexpectedValueException('Plaintext is too short');
        }

        switch ($this->decompress) {
            case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH:
                if (!$this->isAuthenticated()) {
                    break;
                }
                // no break
            case self::NET_SSH2_COMPRESSION_ZLIB:
                if ($this->regenerate_decompression_context) {
                    $this->regenerate_decompression_context = false;

                    $cmf = ord($payload[0]);
                    $cm = $cmf & 0x0F;
                    if ($cm != 8) { // deflate
                        throw new UnsupportedAlgorithmException("Only CM = 8 ('deflate') is supported ($cm)");
                    }
                    $cinfo = ($cmf & 0xF0) >> 4;
                    if ($cinfo > 7) {
                        throw new UnexpectedValueException("CINFO above 7 is not allowed ($cinfo)");
                    }
                    //$windowSize = 1 << ($cinfo + 8);

                    $flg = ord($payload[1]);
                    //$fcheck = $flg & 0x0F;

                    if ((($cmf << 8) | $flg) % 31) {
                        throw new UnexpectedValueException('fcheck failed');
                    }
                    //$fdict = boolval($flg & 0x20);
                    //$flevel = ($flg & 0xC0) >> 6;

                    $this->decompress_context = inflate_init(ZLIB_ENCODING_RAW, ['window' => $cinfo + 8]);
                    $payload = substr($payload, 2);
                }
                if ($this->decompress_context) {
                    $payload = inflate_add($this->decompress_context, $payload, ZLIB_PARTIAL_FLUSH);
                }
        }

        $this->get_seq_no++;

        if (defined('NET_SSH2_LOGGING')) {
            $current = microtime(true);
            /** @psalm-suppress InvalidArrayAccess */
            $message_number = sprintf(
                '<- %s (since last: %s, network: %ss)',
                ($constantName = MessageType::findConstantNameByValue($value = ord($payload[0])))
                    ? "SSH_MSG_$constantName"
                    : "UNKNOWN ($value)",
                round($current - $this->last_packet, 4),
                round($packet->read_time, 4)
            );
            $this->append_log($message_number, $payload);
        }
        $this->last_packet = microtime(true);

        if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) {
            $this->key_exchange();
        }

        return $this->filter($payload);
    }

    /**
     * @param object $packet The packet object being constructed, passed by reference
     *        The size, packet_length, and plain properties of this object may be modified in processing
     */
    private function get_binary_packet_size(object &$packet): void
    {
        $packet_length_header_size = 4;
        if (strlen($packet->raw) < $packet_length_header_size) {
            return;
        }
        $added_validation_length = 0; // indicates when the packet length header is included when validating packet length against block size
        if ($this->decrypt) {
            switch ($this->decryptName) {
                case 'aes128-gcm@openssh.com':
                case 'aes256-gcm@openssh.com':
                    ['packet_length' => $packet_length] = unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size));
                    $packet->size = $packet_length_header_size + $packet_length + $this->decrypt_block_size; // expect tag
                    break;
                case 'chacha20-poly1305@openssh.com':
                    $this->lengthDecrypt->setNonce(pack('N2', 0, $this->get_seq_no));
                    $packet_length_header = $this->lengthDecrypt->decrypt(substr($packet->raw, 0, $packet_length_header_size));
                    ['packet_length' => $packet_length] = unpack('Npacket_length', $packet_length_header);
                    $packet->size = $packet_length_header_size + $packet_length + 16; // expect tag
                    break;
                default:
                    if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) {
                        if (strlen($packet->raw) < $this->decrypt_block_size) {
                            return;
                        }
                        $packet->plain = $this->decrypt->decrypt(substr($packet->raw, 0, $this->decrypt_block_size));
                        ['packet_length' => $packet_length] = unpack('Npacket_length', Strings::shift($packet->plain, $packet_length_header_size));
                        $packet->size = $packet_length_header_size + $packet_length;
                        $added_validation_length = $packet_length_header_size;
                    } else {
                        ['packet_length' => $packet_length] = unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size));
                        $packet->size = $packet_length_header_size + $packet_length;
                    }
                    break;
            }
        } else {
            ['packet_length' => $packet_length] = unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size));
            $packet->size = $packet_length_header_size + $packet_length;
            $added_validation_length = $packet_length_header_size;
        }
        // quoting <http://tools.ietf.org/html/rfc4253#section-6.1>,
        // "implementations SHOULD check that the packet length is reasonable"
        // PuTTY uses 0x9000 as the actual max packet size and so to shall we
        if (
            $packet_length <= 0 || $packet_length > 0x9000
            || ($packet_length + $added_validation_length) % $this->decrypt_block_size != 0
        ) {
            $this->disconnect_helper(DisconnectReason::PROTOCOL_ERROR);
            throw new InvalidPacketLengthException('Invalid packet length');
        }
        if ($this->hmac_check instanceof Hash) {
            $packet->size += $this->hmac_size;
        }
        $packet->packet_length = $packet_length;
    }

    /**
     * Handle Disconnect
     *
     * Because some binary packets need to be ignored...
     *
     * @see self::filter()
     * @see self::key_exchange()
     */
    private function handleDisconnect($payload): void
    {
        Strings::shift($payload, 1);
        [$reason_code, $message] = Strings::unpackSSH2('Ns', $payload);

        $this->errors[] = 'SSH_MSG_DISCONNECT: ' .
            DisconnectReason::findConstantNameByValue($reason_code) .
            "\r\n$message";
        $this->disconnect_helper(DisconnectReason::CONNECTION_LOST);
        throw new ConnectionClosedException('Connection closed by server');
    }

    /**
     * Filter Binary Packets
     *
     * Because some binary packets need to be ignored...
     *
     * @see self::_get_binary_packet()
     */
    private function filter(string $payload): string
    {
        if (ord($payload[0]) == MessageType::DISCONNECT) {
            $this->handleDisconnect($payload);
        }

        if (!isset($this->session_id) && $this->keyExchangeInProgress) {
            return $payload;
        }

        switch (ord($payload[0])) {
            case MessageType::IGNORE:
                $payload = $this->get_binary_packet();
                break;
            case MessageType::DEBUG:
                Strings::shift($payload, 2); // second byte is "always_display"
                [$message] = Strings::unpackSSH2('s', $payload);
                $this->errors[] = "SSH_MSG_DEBUG: $message";
                $payload = $this->get_binary_packet();
                break;
            case MessageType::UNIMPLEMENTED:
                break;
            case MessageType::KEXINIT:
                // this is here for server key re-exchanges after the initial key exchange
                if (!$this->keyExchangeInProgress && isset($this->session_id)) {
                    if (!$this->key_exchange($payload)) {
                        $this->disconnect_helper(DisconnectReason::KEY_EXCHANGE_FAILED);
                        throw new ConnectionClosedException('Key exchange failed');
                    }
                    $payload = $this->get_binary_packet();
                }
                break;
            case MessageType::EXT_INFO:
                Strings::shift($payload, 1);
                [$nr_extensions] = Strings::unpackSSH2('N', $payload);
                for ($i = 0; $i < $nr_extensions; $i++) {
                    [$extension_name, $extension_value] = Strings::unpackSSH2('ss', $payload);
                    if ($extension_name == 'server-sig-algs') {
                        $this->supported_private_key_algorithms = explode(',', $extension_value);
                    }
                }
                $payload = $this->get_binary_packet();
        }

        /*
           Once a party has sent a SSH_MSG_KEXINIT message for key exchange or
           re-exchange, until it has sent a SSH_MSG_NEWKEYS message (Section
           7.3), it MUST NOT send any messages other than:

           o  Transport layer generic messages (1 to 19) (but
              SSH_MSG_SERVICE_REQUEST and SSH_MSG_SERVICE_ACCEPT MUST NOT be
              sent);

           o  Algorithm negotiation messages (20 to 29) (but further
              SSH_MSG_KEXINIT messages MUST NOT be sent);

           o  Specific key exchange method messages (30 to 49).

           -- https://www.rfc-editor.org/rfc/rfc4253#section-7.1
        */
        if ($this->keyExchangeInProgress) {
            return $payload;
        }

        // see http://tools.ietf.org/html/rfc4252#section-5.4; only called when the encryption has been activated and when we haven't already logged in
        if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && ord($payload[0]) == MessageType::USERAUTH_BANNER) {
            Strings::shift($payload, 1);
            [$this->banner_message] = Strings::unpackSSH2('s', $payload);
            $payload = $this->get_binary_packet();
        }

        // only called when we've already logged in
        if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) {
            switch (ord($payload[0])) {
                case MessageType::CHANNEL_REQUEST:
                    if (strlen($payload) == 31) {
                        [
                            'channel' => $channel,
                            'length' => $length
                        ] = unpack('cpacket_type/Nchannel/Nlength', $payload);
                        if (substr($payload, 9, $length) == 'keepalive@openssh.com' && isset($this->server_channels[$channel])) {
                            if (ord(substr($payload, 9 + $length))) { // want reply
                                $this->send_binary_packet(pack('CN', MessageType::CHANNEL_SUCCESS, $this->server_channels[$channel]));
                            }
                            $payload = $this->get_binary_packet();
                        }
                    }
                    break;
                case MessageType::GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4
                    Strings::shift($payload, 1);
                    [$request_name, $want_reply] = Strings::unpackSSH2('sb', $payload);
                    $this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name";
                    if ($want_reply) {
                        $this->send_binary_packet(pack('C', MessageType::REQUEST_FAILURE));
                    }
                    $payload = $this->get_binary_packet();
                    break;
                case MessageType::CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1
                    Strings::shift($payload, 1);
                    [$data, $server_channel] = Strings::unpackSSH2('sN', $payload);
                    switch ($data) {
                        case 'auth-agent':
                        case 'auth-agent@openssh.com':
                            if (isset($this->agent)) {
                                $new_channel = self::CHANNEL_AGENT_FORWARD;

                                [
                                    $remote_window_size,
                                    $remote_maximum_packet_size
                                ] = Strings::unpackSSH2('NN', $payload);

                                $this->packet_size_client_to_server[$new_channel] = $remote_window_size;
                                $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size;
                                $this->window_size_client_to_server[$new_channel] = $this->window_size;

                                $packet_size = 0x4000;

                                $packet = pack(
                                    'CN4',
                                    MessageType::CHANNEL_OPEN_CONFIRMATION,
                                    $server_channel,
                                    $new_channel,
                                    $packet_size,
                                    $packet_size
                                );

                                $this->server_channels[$new_channel] = $server_channel;
                                $this->channel_status[$new_channel] = MessageType::CHANNEL_OPEN_CONFIRMATION;
                                $this->send_binary_packet($packet);
                            }
                            break;
                        default:
                            $packet = Strings::packSSH2(
                                'CN2ss',
                                MessageType::CHANNEL_OPEN_FAILURE,
                                $server_channel,
                                ChannelConnectionFailureReason::ADMINISTRATIVELY_PROHIBITED,
                                '', // description
                                '' // language tag
                            );

                            $this->send_binary_packet($packet);
                    }

                    $payload = $this->get_binary_packet();
            }
        }

        return $payload;
    }

    /**
     * Enable Quiet Mode
     *
     * Suppress stderr from output
     */
    public function enableQuietMode(): void
    {
        $this->quiet_mode = true;
    }

    /**
     * Disable Quiet Mode
     *
     * Show stderr in output
     */
    public function disableQuietMode(): void
    {
        $this->quiet_mode = false;
    }

    /**
     * Returns whether Quiet Mode is enabled or not
     *
     * @see self::enableQuietMode()
     * @see self::disableQuietMode()
     */
    public function isQuietModeEnabled(): bool
    {
        return $this->quiet_mode;
    }

    /**
     * Enable request-pty when using exec()
     */
    public function enablePTY(): void
    {
        $this->request_pty = true;
    }

    /**
     * Disable request-pty when using exec()
     */
    public function disablePTY(): void
    {
        if ($this->isPTYOpen()) {
            $this->close_channel(self::CHANNEL_EXEC);
        }
        $this->request_pty = false;
    }

    /**
     * Returns whether request-pty is enabled or not
     *
     * @see self::enablePTY()
     * @see self::disablePTY()
     */
    public function isPTYEnabled(): bool
    {
        return $this->request_pty;
    }

    /**
     * Gets channel data
     *
     * Returns the data as a string. bool(true) is returned if:
     *
     * - the server closes the channel
     * - if the connection times out
     * - if a window adjust packet is received on the given negated client channel
     * - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION
     * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS
     * - if the channel status is CHANNEL_CLOSE and the response was CHANNEL_CLOSE
     *
     * bool(false) is returned if:
     *
     * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_FAILURE
     *
     * @param int $client_channel Specifies the channel to return data for, and data received
     *        on other channels is buffered. The respective negative value of a channel is
     *        also supported for the case that the caller is awaiting adjustment of the data
     *        window, and where data received on that respective channel is also buffered.
     */
    protected function get_channel_packet(int $client_channel, bool $skip_extended = false): string|bool
    {
        if (!empty($this->channel_buffers[$client_channel])) {
            switch ($this->channel_status[$client_channel] ?? null) {
                case MessageType::CHANNEL_REQUEST:
                    foreach ($this->channel_buffers[$client_channel] as $i => $packet) {
                        switch (ord($packet[0])) {
                            case MessageType::CHANNEL_SUCCESS:
                            case MessageType::CHANNEL_FAILURE:
                                unset($this->channel_buffers[$client_channel][$i]);
                                return substr($packet, 1);
                        }
                    }
                    break;
                default:
                    return substr(array_shift($this->channel_buffers[$client_channel]), 1);
            }
        }

        while (true) {
            try {
                $response = $this->get_binary_packet();
            } catch (TimeoutException) {
                return true;
            }
            [$type] = Strings::unpackSSH2('C', $response);
            if (strlen($response) >= 4) {
                [$channel] = Strings::unpackSSH2('N', $response);
            }

            // will not be setup yet on incoming channel open request
            if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) {
                $this->window_size_server_to_client[$channel] -= strlen($response);

                // resize the window, if appropriate
                if ($this->window_size_server_to_client[$channel] < 0) {
                // PuTTY does something more analogous to the following:
                //if ($this->window_size_server_to_client[$channel] < 0x3FFFFFFF) {
                    $packet = pack('CNN', MessageType::CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_resize);
                    $this->send_binary_packet($packet);
                    $this->window_size_server_to_client[$channel] += $this->window_resize;
                }

                switch ($type) {
                    case MessageType::CHANNEL_WINDOW_ADJUST:
                        [$window_size] = Strings::unpackSSH2('N', $response);
                        $this->window_size_client_to_server[$channel] += $window_size;
                        if ($channel == -$client_channel) {
                            return true;
                        }

                        continue 2;
                    case MessageType::CHANNEL_EXTENDED_DATA:
                        /*
                        if ($client_channel == self::CHANNEL_EXEC) {
                            $this->send_channel_packet($client_channel, chr(0));
                        }
                        */
                        // currently, there's only one possible value for $data_type_code: NET_SSH2_EXTENDED_DATA_STDERR
                        [$data_type_code, $data] = Strings::unpackSSH2('Ns', $response);
                        unset($data_type_code); // suppress psalm error

                        $this->stdErrorLog .= $data;
                        if ($skip_extended || $this->quiet_mode) {
                            continue 2;
                        }
                        if ($client_channel == $channel && $this->channel_status[$channel] == MessageType::CHANNEL_DATA) {
                            return $data;
                        }
                        $this->channel_buffers[$channel][] = chr($type) . $data;

                        continue 2;
                    case MessageType::CHANNEL_REQUEST:
                        if (!isset($this->channel_status[$channel])) {
                            continue 2;
                        }
                        [$value] = Strings::unpackSSH2('s', $response);
                        switch ($value) {
                            case 'exit-signal':
                                [
                                    , // FALSE
                                    $signal_name,
                                    , // core dumped
                                    $error_message
                                ] = Strings::unpackSSH2('bsbs', $response);

                                $this->errors[] = "SSH_MSG_CHANNEL_REQUEST (exit-signal): $signal_name";
                                if (strlen($error_message)) {
                                    $this->errors[count($this->errors) - 1] .= "\r\n$error_message";
                                }
                                if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] != MessageType::CHANNEL_CLOSE) {
                                    if ($this->channel_status[$channel] != MessageType::CHANNEL_EOF) {
                                        $this->send_binary_packet(pack('CN', MessageType::CHANNEL_EOF, $this->server_channels[$channel]));
                                    }
                                    $this->send_binary_packet(pack('CN', MessageType::CHANNEL_CLOSE, $this->server_channels[$channel]));

                                    $this->channel_status[$channel] = MessageType::CHANNEL_CLOSE;
                                }
                                continue 3;
                            case 'exit-status':
                                [, $this->exit_status] = Strings::unpackSSH2('CN', $response);

                                // "The client MAY ignore these messages."
                                // -- http://tools.ietf.org/html/rfc4254#section-6.10

                                continue 3;
                            default:
                                [$want_reply] = Strings::unpackSSH2('b', $response);
                                if ($want_reply) {
                                    // "If the request is not recognized or is not supported for the channel,
                                    //  SSH_MSG_CHANNEL_FAILURE is returned."
                                    // -- https://datatracker.ietf.org/doc/html/rfc4254#page-10
                                    $this->send_binary_packet(pack('CN', MessageType::CHANNEL_FAILURE, $this->server_channels[$channel]));
                                }
                                continue 3;
                        }
                }

                switch ($this->channel_status[$channel]) {
                    case MessageType::CHANNEL_OPEN:
                        switch ($type) {
                            case MessageType::CHANNEL_OPEN_CONFIRMATION:
                                [
                                    $this->server_channels[$channel],
                                    $window_size,
                                    $this->packet_size_client_to_server[$channel]
                                ] = Strings::unpackSSH2('NNN', $response);

                                if ($window_size < 0) {
                                    $window_size &= 0x7FFFFFFF;
                                    $window_size += 0x80000000;
                                }
                                $this->window_size_client_to_server[$channel] = $window_size;
                                $result = $client_channel == $channel ? true : $this->get_channel_packet($client_channel, $skip_extended);
                                $this->on_channel_open();
                                return $result;
                            case MessageType::CHANNEL_OPEN_FAILURE:
                                $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
                                throw new ConnectionClosedException('Unable to open channel');
                            default:
                                if ($client_channel == $channel) {
                                    $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
                                    throw new ConnectionClosedException('Unexpected response to open request');
                                }
                                return $this->get_channel_packet($client_channel, $skip_extended);
                        }
                        /** @psalm-suppress UnevaluatedCode */
                        break;
                    case MessageType::CHANNEL_REQUEST:
                        switch ($type) {
                            case MessageType::CHANNEL_SUCCESS:
                                return true;
                            case MessageType::CHANNEL_FAILURE:
                                return false;
                            case MessageType::CHANNEL_DATA:
                                [$data] = Strings::unpackSSH2('s', $response);
                                $this->channel_buffers[$channel][] = chr($type) . $data;
                                return $this->get_channel_packet($client_channel, $skip_extended);
                            default:
                                $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
                                throw new ConnectionClosedException('Unable to fulfill channel request');
                        }
                        // no break
                    case MessageType::CHANNEL_CLOSE:
                        if ($client_channel == $channel && $type == MessageType::CHANNEL_CLOSE) {
                            return true;
                        }
                        return $this->get_channel_packet($client_channel, $skip_extended);
                }
            }

            // ie. $this->channel_status[$channel] == MessageType::CHANNEL_DATA

            switch ($type) {
                case MessageType::CHANNEL_DATA:
                    /*
                    if ($channel == self::CHANNEL_EXEC) {
                        // SCP requires null packets, such as this, be sent.  further, in the case of the ssh.com SSH server
                        // this actually seems to make things twice as fast.  more to the point, the message right after
                        // SSH_MSG_CHANNEL_DATA (usually SSH_MSG_IGNORE) won't block for as long as it would have otherwise.
                        // in OpenSSH it slows things down but only by a couple thousandths of a second.
                        $this->send_channel_packet($channel, chr(0));
                    }
                    */
                    [$data] = Strings::unpackSSH2('s', $response);

                    if ($channel == self::CHANNEL_AGENT_FORWARD) {
                        $agent_response = $this->agent->forwardData($data);
                        if (isset($agent_response)) {
                            $this->send_channel_packet($channel, $agent_response);
                        }
                        break;
                    }

                    if ($client_channel == $channel) {
                        return $data;
                    }
                    $this->channel_buffers[$channel][] = chr($type) . $data;
                    break;
                case MessageType::CHANNEL_CLOSE:
                    $this->curTimeout = 5;

                    $this->close_channel_bitmap($channel);

                    if ($this->channel_status[$channel] != MessageType::CHANNEL_CLOSE) {
                        $this->send_binary_packet(pack('CN', MessageType::CHANNEL_CLOSE, $this->server_channels[$channel]));
                    }

                    unset($this->channel_status[$channel]);
                    $this->channelCount--;

                    if ($client_channel == $channel) {
                        return true;
                    }
                    // no break
                case MessageType::CHANNEL_EOF:
                    break;
                default:
                    $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
                    throw new UnexpectedSSHMessageException("Error reading channel data ($type)");
            }
        }
    }

    /**
     * Sends Binary Packets
     *
     * See '6. Binary Packet Protocol' of rfc4253 for more info.
     *
     * @see self::_get_binary_packet()
     */
    protected function send_binary_packet(
        #[\SensitiveParameter] string $data,
        ?string $logged = null
    ): void {
        if (!is_resource($this->fsock) || feof($this->fsock)) {
            $this->disconnect_helper(DisconnectReason::CONNECTION_LOST);
            throw new ConnectionClosedException('Connection closed prematurely');
        }

        if (!isset($logged)) {
            $logged = $data;
        }

        switch ($this->compress) {
            case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH:
                if (!$this->isAuthenticated()) {
                    break;
                }
                // no break
            case self::NET_SSH2_COMPRESSION_ZLIB:
                if (!$this->regenerate_compression_context) {
                    $header = '';
                } else {
                    $this->regenerate_compression_context = false;
                    $this->compress_context = deflate_init(ZLIB_ENCODING_RAW, ['window' => 15]);
                    $header = "\x78\x9C";
                }
                if ($this->compress_context) {
                    $data = $header . deflate_add($this->compress_context, $data, ZLIB_PARTIAL_FLUSH);
                }
        }

        // 4 (packet length) + 1 (padding length) + 4 (minimal padding amount) == 9
        $packet_length = strlen($data) + 9;
        if ($this->encrypt && $this->encrypt->usesNonce()) {
            $packet_length -= 4;
        }
        // round up to the nearest $this->encrypt_block_size
        $packet_length += (($this->encrypt_block_size - 1) * $packet_length) % $this->encrypt_block_size;
        // subtracting strlen($data) is obvious - subtracting 5 is necessary because of packet_length and padding_length
        $padding_length = $packet_length - strlen($data) - 5;
        switch (true) {
            case $this->encrypt && $this->encrypt->usesNonce():
            case $this->hmac_create instanceof Hash && $this->hmac_create_etm:
                $padding_length += 4;
                $packet_length += 4;
        }

        // SSH binary packet padding is always 4-255 bytes; the block-rounding above
        // guarantees $padding_length >= 4. Psalm can't track that through the modular
        // arithmetic, so it sees int<min,max>.
        /** @psalm-suppress InvalidArgument */
        $padding = random_bytes($padding_length);

        // we subtract 4 from packet_length because the packet_length field isn't supposed to include itself
        $packet = pack('NCa*', $packet_length - 4, $padding_length, $data . $padding);

        $hmac = '';
        if ($this->hmac_create instanceof Hash && !$this->hmac_create_etm) {
            if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') {
                $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no));
                $hmac = $this->hmac_create->hash($packet);
            } else {
                $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet));
            }
        }

        if ($this->encrypt) {
            switch ($this->encryptName) {
                case 'aes128-gcm@openssh.com':
                case 'aes256-gcm@openssh.com':
                    $this->encrypt->setNonce(
                        $this->encryptFixedPart .
                        $this->encryptInvocationCounter
                    );
                    Strings::increment_str($this->encryptInvocationCounter);
                    $this->encrypt->setAAD($temp = ($packet & "\xFF\xFF\xFF\xFF"));
                    $packet = $temp . $this->encrypt->encrypt(substr($packet, 4));
                    break;
                case 'chacha20-poly1305@openssh.com':
                    // This should be impossible, but we are checking anyway to narrow the type for Psalm.
                    if (!($this->encrypt instanceof ChaCha20)) {
                        throw new InvalidStateException('$this->encrypt is not an instance of ' . ChaCha20::class);
                    }

                    $nonce = pack('N2', 0, $this->send_seq_no);

                    $this->encrypt->setNonce($nonce);
                    $this->lengthEncrypt->setNonce($nonce);

                    $length = $this->lengthEncrypt->encrypt($packet & "\xFF\xFF\xFF\xFF");

                    $this->encrypt->setCounter(0);
                    // this is the same approach that's implemented in Salsa20::createPoly1305Key()
                    // but we don't want to use the same AEAD construction that RFC8439 describes
                    // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305())
                    $this->encrypt->setPoly1305Key(
                        $this->encrypt->encrypt(str_repeat("\0", 32))
                    );
                    $this->encrypt->setAAD($length);
                    $this->encrypt->setCounter(1);
                    $packet = $length . $this->encrypt->encrypt(substr($packet, 4));
                    break;
                default:
                    $packet = $this->hmac_create instanceof Hash && $this->hmac_create_etm ?
                        ($packet & "\xFF\xFF\xFF\xFF") . $this->encrypt->encrypt(substr($packet, 4)) :
                        $this->encrypt->encrypt($packet);
            }
        }

        if ($this->hmac_create instanceof Hash && $this->hmac_create_etm) {
            if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') {
                $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no));
                $hmac = $this->hmac_create->hash($packet);
            } else {
                $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet));
            }
        }

        $this->send_seq_no++;

        $packet .= $this->encrypt && $this->encrypt->usesNonce() ? $this->encrypt->getTag() : $hmac;

        if (!$this->keyExchangeInProgress) {
            $this->bytesTransferredSinceLastKEX += strlen($packet);
        }

        $start = microtime(true);
        $sent = @fwrite($this->fsock, $packet);
        $stop = microtime(true);

        if (defined('NET_SSH2_LOGGING')) {
            $current = microtime(true);
            $message_number = sprintf(
                '-> %s (since last: %s, network: %ss)',
                ($constantName = MessageType::findConstantNameByValue($value = ord($logged[0]), false))
                    ? "SSH_MSG_$constantName"
                    : "UNKNOWN ($value)",
                round($current - $this->last_packet, 4),
                round($stop - $start, 4)
            );
            $this->append_log($message_number, $logged);
        }
        $this->last_packet = microtime(true);

        if (strlen($packet) != $sent) {
            $this->disconnect_helper(DisconnectReason::BY_APPLICATION);
            $message = $sent === false ?
                'Unable to write ' . strlen($packet) . ' bytes' :
                "Only $sent of " . strlen($packet) . " bytes were sent";
            throw new ConnectionClosedException($message);
        }

        if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) {
            $this->key_exchange();
        }
    }

    /**
     * Sends a keep-alive message, if keep-alive is enabled and interval is met
     */
    private function send_keep_alive(): void
    {
        if ($this->bitmap & self::MASK_CONNECTED) {
            $elapsed = microtime(true) - $this->last_packet;
            if ($this->keepAlive > 0 && $elapsed >= $this->keepAlive) {
                $this->send_binary_packet(pack('CN', MessageType::IGNORE, 0));
            }
        }
    }

    /**
     * Logs data packets
     *
     * Makes sure that only the last 1MB worth of packets will be logged
     */
    private function append_log(string $message_number, string $message): void
    {
        if (!defined('NET_SSH2_LOGGING')) {
            throw new InvalidModeException('Logging is not enabled');
        }
        $this->append_log_helper(
            NET_SSH2_LOGGING,
            $message_number,
            $message,
            $this->message_number_log,
            $this->message_log,
            $this->log_size,
            $this->realtime_log_file,
            $this->realtime_log_wrap,
            $this->realtime_log_size
        );
    }

    /**
     * Logs data packet helper
     *
     * @param resource &$realtime_log_file
     */
    protected function append_log_helper(int $constant, string $message_number, string $message, array &$message_number_log, array &$message_log, ?int &$log_size, mixed &$realtime_log_file, ?bool &$realtime_log_wrap, ?int &$realtime_log_size): void
    {
        // remove the byte identifying the message type from all but the first two messages (ie. the identification strings)
        if (!in_array(substr($message_number, 0, 4), ['<- (', '-> (']) && strlen($message_number) > 2 && substr($message_number, 3, 8) != 'SSH_FXP_') {
            Strings::shift($message);
        }

        switch ($constant) {
            // useful for benchmarks
            case self::LOG_SIMPLE:
                $message_number_log[] = $message_number;
                break;
            case self::LOG_SIMPLE_REALTIME:
            //case self::LOG_REALTIME_SIMPLE:
                echo $message_number;
                echo PHP_SAPI == 'cli' ? "\r\n" : '<br>';
                @flush();
                @ob_flush();
                break;
            // the most useful log for SSH2
            case self::LOG_COMPLEX:
                $message_number_log[] = $message_number;
                $log_size += strlen($message);
                $message_log[] = $message;
                while ($log_size > self::LOG_MAX_SIZE) {
                    $log_size -= strlen(array_shift($message_log));
                    array_shift($message_number_log);
                }
                break;
            // dump the output out realtime; packets may be interspersed with non packets,
            // passwords won't be filtered out and select other packets may not be correctly
            // identified
            case self::LOG_REALTIME:
                switch (PHP_SAPI) {
                    case 'cli':
                        $start = $stop = "\r\n";
                        break;
                    default:
                        $start = '<pre>';
                        $stop = '</pre>';
                }
                echo $start . $this->format_log([$message], [$message_number]) . $stop;
                @flush();
                @ob_flush();
                break;
            // basically the same thing as self::LOG_REALTIME with the caveat that NET_SSH2_LOG_REALTIME_FILENAME
            // needs to be defined and that the resultant log file will be capped out at self::LOG_MAX_SIZE.
            // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily
            // at the beginning of the file
            case self::LOG_REALTIME_FILE:
                if (!isset($realtime_log_file)) {
                    if (!defined('NET_SSH2_LOG_REALTIME_FILENAME')) {
                        throw new InvalidModeException('NET_SSH2_LOG_REALTIME_FILENAME must be defined');
                    }
                    // PHP doesn't seem to like using constants in fopen()
                    $filename = NET_SSH2_LOG_REALTIME_FILENAME;
                    $fp = fopen($filename, 'w');
                    $realtime_log_file = $fp;
                }
                if (!is_resource($realtime_log_file)) {
                    break;
                }
                $entry = $this->format_log([$message], [$message_number]);
                if ($realtime_log_wrap) {
                    $temp = "<<< START >>>\r\n";
                    $entry .= $temp;
                    fseek($realtime_log_file, ftell($realtime_log_file) - strlen($temp));
                }
                $realtime_log_size += strlen($entry);
                if ($realtime_log_size > self::LOG_MAX_SIZE) {
                    fseek($realtime_log_file, 0);
                    $realtime_log_size = strlen($entry);
                    $realtime_log_wrap = true;
                }
                fwrite($realtime_log_file, $entry);
        }
    }

    /**
     * Sends channel data
     *
     * Spans multiple SSH_MSG_CHANNEL_DATAs if appropriate
     */
    protected function send_channel_packet(
        int $client_channel,
        #[\SensitiveParameter] string $data
    ): void {
        if (
            isset($this->channel_buffers_write[$client_channel])
            && str_starts_with($data, $this->channel_buffers_write[$client_channel])
        ) {
            // if buffer holds identical initial data content, resume send from the unmatched data portion
            $data = substr($data, strlen($this->channel_buffers_write[$client_channel]));
        } else {
            $this->channel_buffers_write[$client_channel] = '';
        }
        while (strlen($data)) {
            if (!$this->window_size_client_to_server[$client_channel]) {
                // using an invalid channel will let the buffers be built up for the valid channels
                $this->get_channel_packet(-$client_channel);
                if ($this->isTimeout()) {
                    throw new TimeoutException('Timed out waiting for server');
                } elseif (!$this->window_size_client_to_server[$client_channel]) {
                    throw new UnexpectedValueException('Data window was not adjusted');
                }
            }

            /* The maximum amount of data allowed is determined by the maximum
               packet size for the channel, and the current window size, whichever
               is smaller.
                 -- http://tools.ietf.org/html/rfc4254#section-5.2 */
            $max_size = min(
                $this->packet_size_client_to_server[$client_channel],
                $this->window_size_client_to_server[$client_channel]
            );

            $temp = Strings::shift($data, $max_size);
            $packet = Strings::packSSH2(
                'CNs',
                MessageType::CHANNEL_DATA,
                $this->server_channels[$client_channel],
                $temp
            );
            $this->window_size_client_to_server[$client_channel] -= strlen($temp);
            $this->send_binary_packet($packet);
            $this->channel_buffers_write[$client_channel] .= $temp;
        }
        unset($this->channel_buffers_write[$client_channel]);
    }

    /**
     * Closes and flushes a channel
     *
     * \phpseclib4\Net\SSH2 doesn't properly close most channels.  For exec() channels are normally closed by the server
     * and for SFTP channels are presumably closed when the client disconnects.  This functions is intended
     * for SCP more than anything.
     */
    protected function close_channel(int $client_channel): void
    {
        // see http://tools.ietf.org/html/rfc4254#section-5.3

        if ($this->channel_status[$client_channel] != MessageType::CHANNEL_EOF) {
            $this->send_binary_packet(pack('CN', MessageType::CHANNEL_EOF, $this->server_channels[$client_channel]));
        }
        $this->send_binary_packet(pack('CN', MessageType::CHANNEL_CLOSE, $this->server_channels[$client_channel]));

        $this->channel_status[$client_channel] = MessageType::CHANNEL_CLOSE;

        $this->channelCount--;

        $this->curTimeout = 5;
        while (!is_bool($this->get_channel_packet($client_channel))) {
        }

        unset($this->channel_status[$client_channel]);

        $this->close_channel_bitmap($client_channel);
    }

    /**
     * Maintains execution state bitmap in response to channel closure
     */
    private function close_channel_bitmap(int $client_channel): void
    {
        switch ($client_channel) {
            case self::CHANNEL_SHELL:
                // Shell status has been maintained in the bitmap for backwards
                //  compatibility sake, but can be removed going forward
                if ($this->bitmap & self::MASK_SHELL) {
                    $this->bitmap &= ~self::MASK_SHELL;
                }
                break;
        }
    }

    /**
     * Disconnect
     *
     * @return false
     */
    protected function disconnect_helper(int $reason): bool
    {
        if ($this->bitmap & self::MASK_DISCONNECT) {
            // Disregard subsequent disconnect requests
            return false;
        }
        $this->bitmap |= self::MASK_DISCONNECT;
        if ($this->isConnected()) {
            $data = Strings::packSSH2('CNss', MessageType::DISCONNECT, $reason, '', '');
            try {
                $this->send_binary_packet($data);
            } catch (\Exception) {
            }
        }

        $this->reset_connection();

        return false;
    }

    /**
     * Returns a log of the packets that have been sent and received.
     *
     * Returns a string if NET_SSH2_LOGGING == self::LOG_COMPLEX, an array if NET_SSH2_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SSH2_LOGGING')
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function getLog(): array|string|null
    {
        if (!defined('NET_SSH2_LOGGING')) {
            return null;
        }

        switch (NET_SSH2_LOGGING) {
            case self::LOG_SIMPLE:
                return $this->message_number_log;
            case self::LOG_COMPLEX:
                $log = $this->format_log($this->message_log, $this->message_number_log);
                return PHP_SAPI == 'cli' ? $log : '<pre>' . $log . '</pre>';
            default:
                return null;
        }
    }

    /**
     * Formats a log for printing
     */
    protected function format_log(array $message_log, array $message_number_log): string
    {
        $output = '';
        for ($i = 0; $i < count($message_log); $i++) {
            $output .= $message_number_log[$i];
            $current_log = $message_log[$i];
            $j = 0;
            if (strlen($current_log)) {
                $output .= "\r\n";
            }
            do {
                if (strlen($current_log)) {
                    $output .= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0  ';
                }
                $fragment = Strings::shift($current_log, $this->log_short_width);
                $hex = substr(preg_replace_callback('#.#s', fn ($matches) => $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT), $fragment), strlen($this->log_boundary));
                // replace non ASCII printable characters with dots
                // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters
                // also replace < with a . since < messes up the output on web browsers
                $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment);
                $output .= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n";
                $j++;
            } while (strlen($current_log));
            $output .= "\r\n";
        }

        return $output;
    }

    /**
     * Helper function for agent->on_channel_open()
     *
     * Used when channels are created to inform agent
     * of said channel opening. Must be called after
     * channel open confirmation received
     */
    private function on_channel_open(): void
    {
        if (isset($this->agent)) {
            $this->agent->registerChannelOpen($this);
        }
    }

    private static function array_intersect_first(array $array1, array $array2): ?string
    {
        foreach ($array1 as $value) {
            if (in_array($value, $array2)) {
                return $value;
            }
        }
        return null;
    }

    /**
     * Return the server identification.
     */
    public function getServerIdentification(): string
    {
        $this->connect();

        return $this->server_identifier;
    }

    /**
     * Returns a list of algorithms the server supports
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function getServerAlgorithms(): array
    {
        $this->connect();

        return [
            'kex' => $this->kex_algorithms,
            'hostkey' => $this->server_host_key_algorithms,
            'client_to_server' => [
                'crypt' => $this->encryption_algorithms_client_to_server,
                'mac' => $this->mac_algorithms_client_to_server,
                'comp' => $this->compression_algorithms_client_to_server,
                'lang' => $this->languages_client_to_server,
            ],
            'server_to_client' => [
                'crypt' => $this->encryption_algorithms_server_to_client,
                'mac' => $this->mac_algorithms_server_to_client,
                'comp' => $this->compression_algorithms_server_to_client,
                'lang' => $this->languages_server_to_client,
            ],
        ];
    }

    /**
     * Returns a list of KEX algorithms that phpseclib supports
     */
    public static function getSupportedKEXAlgorithms(): array
    {
        $kex_algorithms = [
            // Elliptic Curve Diffie-Hellman Key Agreement (ECDH) using
            // Curve25519. See doc/curve25519-sha256@libssh.org.txt in the
            // libssh repository for more information.
            'curve25519-sha256',
            'curve25519-sha256@libssh.org',

            'ecdh-sha2-nistp256', // RFC 5656
            'ecdh-sha2-nistp384', // RFC 5656
            'ecdh-sha2-nistp521', // RFC 5656

            'diffie-hellman-group-exchange-sha256',// RFC 4419
            'diffie-hellman-group-exchange-sha1',  // RFC 4419

            // Diffie-Hellman Key Agreement (DH) using integer modulo prime
            // groups.
            'diffie-hellman-group14-sha256',
            'diffie-hellman-group14-sha1', // REQUIRED
            'diffie-hellman-group15-sha512',
            'diffie-hellman-group16-sha512',
            'diffie-hellman-group17-sha512',
            'diffie-hellman-group18-sha512',

            'diffie-hellman-group1-sha1', // REQUIRED
        ];

        return $kex_algorithms;
    }

    /**
     * Returns a list of host key algorithms that phpseclib supports
     */
    public static function getSupportedHostKeyAlgorithms(): array
    {
        return [
            'ssh-ed25519', // https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-02
            'ecdsa-sha2-nistp256', // RFC 5656
            'ecdsa-sha2-nistp384', // RFC 5656
            'ecdsa-sha2-nistp521', // RFC 5656
            'rsa-sha2-256', // RFC 8332
            'rsa-sha2-512', // RFC 8332
            'ssh-rsa', // RECOMMENDED  sign   Raw RSA Key
            'ssh-dss',  // REQUIRED     sign   Raw DSS Key
        ];
    }

    /**
     * Returns a list of symmetric key algorithms that phpseclib supports
     */
    public static function getSupportedEncryptionAlgorithms(): array
    {
        $algos = [
            // from <https://tools.ietf.org/html/rfc5647>:
            'aes128-gcm@openssh.com',
            'aes256-gcm@openssh.com',

            // from <http://tools.ietf.org/html/rfc4345#section-4>:
            'arcfour256',
            'arcfour128',

            //'arcfour',      // OPTIONAL          the ARCFOUR stream cipher with a 128-bit key

            // CTR modes from <http://tools.ietf.org/html/rfc4344#section-4>:
            'aes128-ctr',     // RECOMMENDED       AES (Rijndael) in SDCTR mode, with 128-bit key
            'aes192-ctr',     // RECOMMENDED       AES with 192-bit key
            'aes256-ctr',     // RECOMMENDED       AES with 256-bit key

            // from <https://github.com/openssh/openssh-portable/blob/001aa55/PROTOCOL.chacha20poly1305>:
            // one of the big benefits of chacha20-poly1305 is speed. the problem is...
            // libsodium doesn't generate the poly1305 keys in the way ssh does and openssl's PHP bindings don't even
            // seem to support poly1305 currently. so even if libsodium or openssl are being used for the chacha20
            // part, pure-PHP has to be used for the poly1305 part and that's gonna cause a big slow down.
            // speed-wise it winds up being faster to use AES (when openssl is available) and some HMAC
            // (which is always gonna be super fast to compute thanks to the hash extension, which
            // "is bundled and compiled into PHP by default")
            'chacha20-poly1305@openssh.com',

            'twofish128-ctr', // OPTIONAL          Twofish in SDCTR mode, with 128-bit key
            'twofish192-ctr', // OPTIONAL          Twofish with 192-bit key
            'twofish256-ctr', // OPTIONAL          Twofish with 256-bit key

            'aes128-cbc',     // RECOMMENDED       AES with a 128-bit key
            'aes192-cbc',     // OPTIONAL          AES with a 192-bit key
            'aes256-cbc',     // OPTIONAL          AES in CBC mode, with a 256-bit key

            'twofish128-cbc', // OPTIONAL          Twofish with a 128-bit key
            'twofish192-cbc', // OPTIONAL          Twofish with a 192-bit key
            'twofish256-cbc',
            'twofish-cbc',    // OPTIONAL          alias for "twofish256-cbc"
                              //                   (this is being retained for historical reasons)

            'blowfish-ctr',   // OPTIONAL          Blowfish in SDCTR mode

            'blowfish-cbc',   // OPTIONAL          Blowfish in CBC mode

            '3des-ctr',       // RECOMMENDED       Three-key 3DES in SDCTR mode

            '3des-cbc',       // REQUIRED          three-key 3DES in CBC mode

             //'none'           // OPTIONAL          no encryption; NOT RECOMMENDED
        ];

        if (self::$crypto_engine) {
            $engines = [self::$crypto_engine];
        } else {
            $engines = [
                'libsodium',
                'OpenSSL (AEAD)',
                'OpenSSL',
                'Eval',
                'PHP',
            ];
        }

        $ciphers = [];

        foreach ($engines as $engine) {
            foreach ($algos as $algo) {
                $obj = self::encryption_algorithm_to_crypt_instance($algo);
                if ($obj instanceof Rijndael) {
                    $obj->setKeyLength((int) preg_replace('#[^\d]#', '', $algo));
                }
                switch ($algo) {
                    // Eval engines do not exist for ChaCha20 or RC4 because they would not benefit from one.
                    // to benefit from an Eval engine they'd need to loop a variable amount of times, they'd
                    // need to do table lookups (eg. sbox subsitutions). ChaCha20 doesn't do either because
                    // it's a so-called ARX cipher, meaning that the only operations it does are add (A), rotate (R)
                    // and XOR (X). RC4 does do table lookups but being a stream cipher it works differently than
                    // block ciphers. with RC4 you XOR the plaintext against a keystream and the keystream changes
                    // as you encrypt stuff. the only table lookups are made against this keystream and thus table
                    // lookups are kinda unavoidable. with AES and DES, however, the table lookups that are done
                    // are done against substitution boxes (sboxes), which are invariant.

                    // OpenSSL can't be used as an engine, either, because OpenSSL doesn't support continuous buffers
                    // as SSH2 uses and altho you can emulate a continuous buffer with block ciphers you can't do so
                    // with stream ciphers. As for ChaCha20...  for the ChaCha20 part OpenSSL could prob be used but
                    // the big slow down isn't with ChaCha20 - it's with Poly1305. SSH constructs the key for that
                    // differently than how OpenSSL does it (OpenSSL does it as the RFC describes, SSH doesn't).

                    // libsodium can't be used because it doesn't support RC4 and it doesn't construct the Poly1305
                    // keys in the same way that SSH does

                    // mcrypt could prob be used for RC4 but mcrypt hasn't been included in PHP core for yearss
                    case 'chacha20-poly1305@openssh.com':
                    case 'arcfour128':
                    case 'arcfour256':
                        if ($engine != 'PHP') {
                            continue 2;
                        }
                        break;
                    case 'aes128-gcm@openssh.com':
                    case 'aes256-gcm@openssh.com':
                        if ($engine == 'OpenSSL') {
                            continue 2;
                        }
                        $obj->setNonce('dummydummydu');
                }
                if ($obj->isValidEngine($engine)) {
                    $algos = array_diff($algos, [$algo]);
                    $ciphers[] = $algo;
                }
            }
        }

        return $ciphers;
    }

    /**
     * Returns a list of MAC algorithms that phpseclib supports
     */
    public static function getSupportedMACAlgorithms(): array
    {
        return [
            'hmac-sha2-256-etm@openssh.com',
            'hmac-sha2-512-etm@openssh.com',
            'hmac-sha1-etm@openssh.com',

            // from <http://www.ietf.org/rfc/rfc6668.txt>:
            'hmac-sha2-256',// RECOMMENDED     HMAC-SHA256 (digest length = key length = 32)
            'hmac-sha2-512',// OPTIONAL        HMAC-SHA512 (digest length = key length = 64)

            'hmac-sha1-96', // RECOMMENDED     first 96 bits of HMAC-SHA1 (digest length = 12, key length = 20)
            'hmac-sha1',    // REQUIRED        HMAC-SHA1 (digest length = key length = 20)
            'hmac-md5-96',  // OPTIONAL        first 96 bits of HMAC-MD5 (digest length = 12, key length = 16)
            'hmac-md5',     // OPTIONAL        HMAC-MD5 (digest length = key length = 16)

            'umac-64-etm@openssh.com',
            'umac-128-etm@openssh.com',

            // from <https://tools.ietf.org/html/draft-miller-secsh-umac-01>:
            'umac-64@openssh.com',
            'umac-128@openssh.com',

            //'none'          // OPTIONAL        no MAC; NOT RECOMMENDED
        ];
    }

    /**
     * Returns a list of compression algorithms that phpseclib supports
     */
    public static function getSupportedCompressionAlgorithms(): array
    {
        $algos = ['none']; // REQUIRED        no compression
        if (function_exists('deflate_init')) {
            $algos[] = 'zlib@openssh.com'; // https://datatracker.ietf.org/doc/html/draft-miller-secsh-compression-delayed
            $algos[] = 'zlib';
        }
        return $algos;
    }

    /**
     * Return list of negotiated algorithms
     *
     * Uses the same format as https://www.php.net/ssh2-methods-negotiated
     */
    public function getAlgorithmsNegotiated(): array
    {
        $this->connect();

        $compression_map = [
            self::NET_SSH2_COMPRESSION_NONE => 'none',
            self::NET_SSH2_COMPRESSION_ZLIB => 'zlib',
            self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH => 'zlib@openssh.com',
        ];

        return [
            'kex' => $this->kex_algorithm,
            'hostkey' => $this->signature_format,
            'client_to_server' => [
                'crypt' => $this->encryptName,
                'mac' => $this->hmac_create_name,
                'comp' => $compression_map[$this->compress],
            ],
            'server_to_client' => [
                'crypt' => $this->decryptName,
                'mac' => $this->hmac_check_name,
                'comp' => $compression_map[$this->decompress],
            ],
        ];
    }

    /**
     * Force multiple channels (even if phpseclib has decided to disable them)
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function forceMultipleChannels(): void
    {
        $this->errorOnMultipleChannels = false;
    }

    /**
     * Allows you to set the terminal
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function setTerminal(string $term): void
    {
        $this->term = $term;
    }

    /**
     * Accepts an associative array with up to four parameters as described at
     * <https://www.php.net/manual/en/function.ssh2-connect.php>
     */
    public function setPreferredAlgorithms(array $methods): void
    {
        $keys = ['client_to_server', 'server_to_client'];

        if (isset($methods['kex']) && is_string($methods['kex'])) {
            $methods['kex'] = explode(',', $methods['kex']);
        }

        if (isset($methods['hostkey']) && is_string($methods['hostkey'])) {
            $methods['hostkey'] = explode(',', $methods['hostkey']);
        }

        foreach ($keys as $key) {
            if (isset($methods[$key])) {
                $a = &$methods[$key];
                if (isset($a['crypt']) && is_string($a['crypt'])) {
                    $a['crypt'] = explode(',', $a['crypt']);
                }
                if (isset($a['comp']) && is_string($a['comp'])) {
                    $a['comp'] = explode(',', $a['comp']);
                }
                if (isset($a['mac']) && is_string($a['mac'])) {
                    $a['mac'] = explode(',', $a['mac']);
                }
            }
        }

        $preferred = $methods;

        if (isset($preferred['kex'])) {
            $preferred['kex'] = array_intersect(
                $preferred['kex'],
                static::getSupportedKEXAlgorithms()
            );
        }

        if (isset($preferred['hostkey'])) {
            $preferred['hostkey'] = array_intersect(
                $preferred['hostkey'],
                static::getSupportedHostKeyAlgorithms()
            );
        }

        foreach ($keys as $key) {
            if (isset($preferred[$key])) {
                $a = &$preferred[$key];
                if (isset($a['crypt'])) {
                    $a['crypt'] = array_intersect(
                        $a['crypt'],
                        static::getSupportedEncryptionAlgorithms()
                    );
                }
                if (isset($a['comp'])) {
                    $a['comp'] = array_intersect(
                        $a['comp'],
                        static::getSupportedCompressionAlgorithms()
                    );
                }
                if (isset($a['mac'])) {
                    $a['mac'] = array_intersect(
                        $a['mac'],
                        static::getSupportedMACAlgorithms()
                    );
                }
            }
        }

        $keys = [
            'kex',
            'hostkey',
            'client_to_server/crypt',
            'client_to_server/comp',
            'client_to_server/mac',
            'server_to_client/crypt',
            'server_to_client/comp',
            'server_to_client/mac',
        ];
        foreach ($keys as $key) {
            $p = $preferred;
            $m = $methods;

            $subkeys = explode('/', $key);
            foreach ($subkeys as $subkey) {
                if (!isset($p[$subkey])) {
                    continue 2;
                }
                $p = $p[$subkey];
                $m = $m[$subkey];
            }

            if (count($p) != count($m)) {
                $diff = array_diff($m, $p);
                $msg = count($diff) == 1 ?
                    ' is not a supported algorithm' :
                    ' are not supported algorithms';
                throw new UnsupportedAlgorithmException(implode(', ', $diff) . $msg);
            }
        }

        $this->preferred = $preferred;
    }

    /**
     * Returns the banner message.
     *
     * Quoting from the RFC, "in some jurisdictions, sending a warning message before
     * authentication may be relevant for getting legal protection."
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function getBannerMessage(): string
    {
        return $this->banner_message;
    }

    /**
     * Returns the server public host key.
     *
     * Caching this the first time you connect to a server and checking the result on subsequent connections
     * is recommended.  Returns false if the server signature is not signed correctly with the public host key.
     */
    public function getServerPublicHostKey(): ?string
    {
        if (!($this->bitmap & self::MASK_CONSTRUCTOR)) {
            $this->connect();
        }

        $signature = $this->signature;
        $server_public_host_key = base64_encode($this->server_public_host_key);

        if ($this->signature_validated) {
            return $this->bitmap ?
                $this->signature_format . ' ' . $server_public_host_key :
                null;
        }

        $this->signature_validated = true;

        switch ($this->signature_format) {
            case 'ssh-ed25519':
            case 'ecdsa-sha2-nistp256':
            case 'ecdsa-sha2-nistp384':
            case 'ecdsa-sha2-nistp521':
                $key = EC::loadFormat('OpenSSH', $server_public_host_key)
                    ->withSignatureFormat('SSH2');
                $hash = match ($this->signature_format) {
                    'ssh-ed25519' => 'sha512',
                    'ecdsa-sha2-nistp256' => 'sha256',
                    'ecdsa-sha2-nistp384' => 'sha384',
                    'ecdsa-sha2-nistp521' => 'sha512'
                };
                $key = $key->withHash($hash);
                break;
            case 'ssh-dss':
                $key = DSA::loadFormat('OpenSSH', $server_public_host_key)
                    ->withSignatureFormat('SSH2')
                    ->withHash('sha1');
                break;
            case 'ssh-rsa':
            case 'rsa-sha2-256':
            case 'rsa-sha2-512':
                // could be ssh-rsa, rsa-sha2-256, rsa-sha2-512
                // we don't check here because we already checked in key_exchange
                // some signatures have the type embedded within the message and some don't
                [, $signature] = Strings::unpackSSH2('ss', $signature);

                $key = RSA::loadFormat('OpenSSH', $server_public_host_key)
                    ->withPadding(RSA::SIGNATURE_PKCS1);
                $hash = match ($this->signature_format) {
                    'rsa-sha2-512' => 'sha512',
                    'rsa-sha2-256' => 'sha256',
                    default => 'sha1'
                };
                $key = $key->withHash($hash);
                break;
            default:
                $this->disconnect_helper(DisconnectReason::HOST_KEY_NOT_VERIFIABLE);
                throw new NoSupportedAlgorithmsException('Unsupported signature format');
        }

        if (!$key->verify($this->exchange_hash, $signature)) {
            $this->disconnect_helper(DisconnectReason::HOST_KEY_NOT_VERIFIABLE);
            return null;
        };

        return $this->signature_format . ' ' . $server_public_host_key;
    }

    /**
     * Returns the exit status of an SSH command or false
     */
    public function getExitStatus(): ?int
    {
        if (!isset($this->exit_status)) {
            return null;
        }
        return $this->exit_status;
    }

    /**
     * Returns the number of columns for the terminal window size.
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function getWindowColumns(): int
    {
        return $this->windowColumns;
    }

    /**
     * Returns the number of rows for the terminal window size.
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function getWindowRows(): int
    {
        return $this->windowRows;
    }

    /**
     * Sets the number of columns for the terminal window size.
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function setWindowColumns(int $value): void
    {
        $this->windowColumns = $value;
    }

    /**
     * Sets the number of rows for the terminal window size.
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function setWindowRows(int $value): void
    {
        $this->windowRows = $value;
    }

    /**
     * Sets the number of columns and rows for the terminal window size.
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function setWindowSize(int $columns = 80, int $rows = 24): void
    {
        $this->windowColumns = $columns;
        $this->windowRows = $rows;
    }

    /**
     * To String Magic Method
     */
    public function __toString(): string
    {
        return $this->getResourceId();
    }

    /**
     * Get Resource ID
     *
     * We use {} because that symbols should not be in URL according to
     * {@link http://tools.ietf.org/html/rfc3986#section-2 RFC}.
     * It will safe us from any conflicts, because otherwise regexp will
     * match all alphanumeric domains.
     */
    public function getResourceId(): string
    {
        return '{' . spl_object_hash($this) . '}';
    }

    public static function getConnectionByResourceId(string $id): ?SSH2
    {
        if (array_key_exists($id, self::$connections)) {
            /**
             * @psalm-ignore-var
             * @var SSH2|null $ssh2
             */
            $ssh2 =  self::$connections[$id]->get();
            return $ssh2;
        }
        return null;
    }

    /**
     * Return all excising connections
     *
     * @return array<string, SSH2>
     */
    public static function getConnections(): array
    {
        $temp = [];
        foreach (self::$connections as $key => $ref) {
            $conn = $ref->get();
            if (isset($conn)) {
                $temp[$key] = $conn;
            }
        }
        return $temp;
    }

    /**
     * Update packet types in log history
     */
    private function updateLogHistory(string $old, string $new): void
    {
        if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == self::LOG_COMPLEX) {
            $this->message_number_log[count($this->message_number_log) - 1] = str_replace(
                $old,
                $new,
                $this->message_number_log[count($this->message_number_log) - 1]
            );
        }
    }

    /**
     * Return the list of authentication methods that may productively continue authentication.
     *
     * @see https://tools.ietf.org/html/rfc4252#section-5.1
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function getAuthMethodsToContinue(): ?array
    {
        return $this->auth_methods_to_continue;
    }

    /**
     * Enables "smart" multi-factor authentication (MFA)
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function enableSmartMFA(): void
    {
        $this->smartMFA = true;
    }

    /**
     * Disables "smart" multi-factor authentication (MFA)
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function disableSmartMFA(): void
    {
        $this->smartMFA = false;
    }

    /**
     * How many bytes until the next key re-exchange?
     *
     * @psalm-suppress PossiblyUnusedMethod
     */
    public function setBytesUntilKeyReexchange(int $bytes): void
    {
        $this->doKeyReexchangeAfterXBytes = $bytes;
    }
}
