[securemail] Update Composer dependency ahead of release
[friendica-addons.git/.git] / securemail / vendor / phpseclib / phpseclib / phpseclib / Net / SSH2.php
index d613456..eee2e10 100644 (file)
@@ -3,14 +3,14 @@
 /**
  * Pure-PHP implementation of SSHv2.
  *
- * PHP versions 4 and 5
+ * PHP version 5
  *
  * Here are some examples of how to use this library:
  * <code>
  * <?php
- *    include 'Net/SSH2.php';
+ *    include 'vendor/autoload.php';
  *
- *    $ssh = new Net_SSH2('www.domain.tld');
+ *    $ssh = new \phpseclib3\Net\SSH2('www.domain.tld');
  *    if (!$ssh->login('username', 'password')) {
  *        exit('Login Failed');
  *    }
  *
  * <code>
  * <?php
- *    include 'Crypt/RSA.php';
- *    include 'Net/SSH2.php';
+ *    include 'vendor/autoload.php';
  *
- *    $key = new Crypt_RSA();
- *    //$key->setPassword('whatever');
- *    $key->loadKey(file_get_contents('privatekey'));
+ *    $key = \phpseclib3\Crypt\PublicKeyLoader::load('...', '(optional) password');
  *
- *    $ssh = new Net_SSH2('www.domain.tld');
+ *    $ssh = new \phpseclib3\Net\SSH2('www.domain.tld');
  *    if (!$ssh->login('username', $key)) {
  *        exit('Login Failed');
  *    }
  * ?>
  * </code>
  *
- * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @category  Net
- * @package   Net_SSH2
  * @author    Jim Wigginton <terrafrost@php.net>
  * @copyright 2007 Jim Wigginton
  * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
  * @link      http://phpseclib.sourceforge.net
  */
 
-/**#@+
- * Execution Bitmap Masks
- *
- * @see Net_SSH2::bitmap
- * @access private
- */
-define('NET_SSH2_MASK_CONSTRUCTOR',   0x00000001);
-define('NET_SSH2_MASK_CONNECTED',     0x00000002);
-define('NET_SSH2_MASK_LOGIN_REQ',     0x00000004);
-define('NET_SSH2_MASK_LOGIN',         0x00000008);
-define('NET_SSH2_MASK_SHELL',         0x00000010);
-define('NET_SSH2_MASK_WINDOW_ADJUST', 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
- * recepient 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 snipet:
- *     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 Net_SSH2::_send_channel_packet()
- * @see Net_SSH2::_get_channel_packet()
- * @access private
- */
-define('NET_SSH2_CHANNEL_EXEC',      0); // PuTTy uses 0x100
-define('NET_SSH2_CHANNEL_SHELL',     1);
-define('NET_SSH2_CHANNEL_SUBSYSTEM', 2);
-/**#@-*/
-
-/**#@+
- * @access public
- * @see Net_SSH2::getLog()
- */
-/**
- * Returns the message numbers
- */
-define('NET_SSH2_LOG_SIMPLE',  1);
-/**
- * Returns the message content
- */
-define('NET_SSH2_LOG_COMPLEX', 2);
-/**
- * Outputs the content real-time
- */
-define('NET_SSH2_LOG_REALTIME', 3);
-/**
- * Dumps the content real-time to a file
- */
-define('NET_SSH2_LOG_REALTIME_FILE', 4);
-/**#@-*/
-
-/**#@+
- * @access public
- * @see Net_SSH2::read()
- */
-/**
- * Returns when a string matching $expect exactly is found
- */
-define('NET_SSH2_READ_SIMPLE',  1);
-/**
- * Returns when a string matching the regular expression $expect is found
- */
-define('NET_SSH2_READ_REGEX', 2);
-/**
- * Make sure that the log never gets larger than this
- */
-define('NET_SSH2_LOG_MAX_SIZE', 1024 * 1024);
-/**#@-*/
+namespace phpseclib3\Net;
+
+use phpseclib3\Common\Functions\Strings;
+use phpseclib3\Crypt\Blowfish;
+use phpseclib3\Crypt\ChaCha20;
+use phpseclib3\Crypt\Common\AsymmetricKey;
+use phpseclib3\Crypt\Common\PrivateKey;
+use phpseclib3\Crypt\Common\PublicKey;
+use phpseclib3\Crypt\Common\SymmetricKey;
+use phpseclib3\Crypt\DH;
+use phpseclib3\Crypt\DSA;
+use phpseclib3\Crypt\EC;
+use phpseclib3\Crypt\Hash;
+use phpseclib3\Crypt\Random;
+use phpseclib3\Crypt\RC4;
+use phpseclib3\Crypt\Rijndael;
+use phpseclib3\Crypt\RSA;
+use phpseclib3\Crypt\TripleDES; // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification.
+use phpseclib3\Crypt\Twofish;
+use phpseclib3\Exception\ConnectionClosedException;
+use phpseclib3\Exception\InsufficientSetupException;
+use phpseclib3\Exception\NoSupportedAlgorithmsException;
+use phpseclib3\Exception\UnableToConnectException;
+use phpseclib3\Exception\UnsupportedAlgorithmException;
+use phpseclib3\Exception\UnsupportedCurveException;
+use phpseclib3\Math\BigInteger;
+use phpseclib3\System\SSH\Agent;
 
 /**
  * Pure-PHP implementation of SSHv2.
  *
- * @package Net_SSH2
  * @author  Jim Wigginton <terrafrost@php.net>
- * @access  public
  */
-class Net_SSH2
+class SSH2
 {
+    /**#@+
+     * Compression Types
+     *
+     */
+    /**
+     * No compression
+     */
+    const NET_SSH2_COMPRESSION_NONE = 1;
+    /**
+     * zlib compression
+     */
+    const NET_SSH2_COMPRESSION_ZLIB = 2;
+    /**
+     * zlib@openssh.com
+     */
+    const NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH = 3;
+    /**#@-*/
+
+    // Execution Bitmap Masks
+    const MASK_CONSTRUCTOR   = 0x00000001;
+    const MASK_CONNECTED     = 0x00000002;
+    const MASK_LOGIN_REQ     = 0x00000004;
+    const MASK_LOGIN         = 0x00000008;
+    const MASK_SHELL         = 0x00000010;
+    const MASK_WINDOW_ADJUST = 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 \phpseclib3\Net\SSH2::send_channel_packet()
+     * @see \phpseclib3\Net\SSH2::get_channel_packet()
+     */
+    const CHANNEL_EXEC          = 1; // PuTTy uses 0x100
+    const CHANNEL_SHELL         = 2;
+    const CHANNEL_SUBSYSTEM     = 3;
+    const CHANNEL_AGENT_FORWARD = 4;
+    const CHANNEL_KEEP_ALIVE    = 5;
+
+    /**
+     * Returns the message numbers
+     *
+     * @see \phpseclib3\Net\SSH2::getLog()
+     */
+    const LOG_SIMPLE = 1;
+    /**
+     * Returns the message content
+     *
+     * @see \phpseclib3\Net\SSH2::getLog()
+     */
+    const LOG_COMPLEX = 2;
+    /**
+     * Outputs the content real-time
+     */
+    const LOG_REALTIME = 3;
+    /**
+     * Dumps the content real-time to a file
+     */
+    const LOG_REALTIME_FILE = 4;
+    /**
+     * Outputs the message numbers real-time
+     */
+    const LOG_SIMPLE_REALTIME = 5;
+    /**
+     * Make sure that the log never gets larger than this
+     *
+     * @see \phpseclib3\Net\SSH2::getLog()
+     */
+    const LOG_MAX_SIZE = 1048576; // 1024 * 1024
+
+    /**
+     * Returns when a string matching $expect exactly is found
+     *
+     * @see \phpseclib3\Net\SSH2::read()
+     */
+    const READ_SIMPLE = 1;
+    /**
+     * Returns when a string matching the regular expression $expect is found
+     *
+     * @see \phpseclib3\Net\SSH2::read()
+     */
+    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 \phpseclib3\Net\SSH2::read()
+     */
+    const READ_NEXT = 3;
+
     /**
      * The SSH identifier
      *
-     * @var String
-     * @access private
+     * @var string
      */
-    var $identifier;
+    private $identifier;
 
     /**
      * The Socket Object
      *
-     * @var Object
-     * @access private
+     * @var resource|closed-resource|null
      */
-    var $fsock;
+    public $fsock;
 
     /**
      * Execution Bitmap
@@ -172,119 +198,158 @@ class Net_SSH2
      * 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.
      *
-     * @var Integer
-     * @access private
+     * @var int
      */
-    var $bitmap = 0;
+    protected $bitmap = 0;
 
     /**
      * Error information
      *
-     * @see Net_SSH2::getErrors()
-     * @see Net_SSH2::getLastError()
-     * @var String
-     * @access private
+     * @see self::getErrors()
+     * @see self::getLastError()
+     * @var array
      */
-    var $errors = array();
+    private $errors = [];
 
     /**
      * Server Identifier
      *
-     * @see Net_SSH2::getServerIdentification()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getServerIdentification()
+     * @var string|false
      */
-    var $server_identifier = false;
+    protected $server_identifier = false;
 
     /**
      * Key Exchange Algorithms
      *
-     * @see Net_SSH2::getKexAlgorithims()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getKexAlgorithims()
+     * @var array|false
+     */
+    private $kex_algorithms = false;
+
+    /**
+     * Key Exchange Algorithm
+     *
+     * @see self::getMethodsNegotiated()
+     * @var string|false
+     */
+    private $kex_algorithm = false;
+
+    /**
+     * Minimum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods
+     *
+     * @see self::_key_exchange()
+     * @var int
      */
-    var $kex_algorithms = false;
+    private $kex_dh_group_size_min = 1536;
+
+    /**
+     * Preferred Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods
+     *
+     * @see self::_key_exchange()
+     * @var int
+     */
+    private $kex_dh_group_size_preferred = 2048;
+
+    /**
+     * Maximum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods
+     *
+     * @see self::_key_exchange()
+     * @var int
+     */
+    private $kex_dh_group_size_max = 4096;
 
     /**
      * Server Host Key Algorithms
      *
-     * @see Net_SSH2::getServerHostKeyAlgorithms()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getServerHostKeyAlgorithms()
+     * @var array|false
+     */
+    private $server_host_key_algorithms = false;
+
+    /**
+     * 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()
+     * @var array|false
      */
-    var $server_host_key_algorithms = false;
+    private $supported_private_key_algorithms = false;
 
     /**
      * Encryption Algorithms: Client to Server
      *
-     * @see Net_SSH2::getEncryptionAlgorithmsClient2Server()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getEncryptionAlgorithmsClient2Server()
+     * @var array|false
      */
-    var $encryption_algorithms_client_to_server = false;
+    private $encryption_algorithms_client_to_server = false;
 
     /**
      * Encryption Algorithms: Server to Client
      *
-     * @see Net_SSH2::getEncryptionAlgorithmsServer2Client()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getEncryptionAlgorithmsServer2Client()
+     * @var array|false
      */
-    var $encryption_algorithms_server_to_client = false;
+    private $encryption_algorithms_server_to_client = false;
 
     /**
      * MAC Algorithms: Client to Server
      *
-     * @see Net_SSH2::getMACAlgorithmsClient2Server()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getMACAlgorithmsClient2Server()
+     * @var array|false
      */
-    var $mac_algorithms_client_to_server = false;
+    private $mac_algorithms_client_to_server = false;
 
     /**
      * MAC Algorithms: Server to Client
      *
-     * @see Net_SSH2::getMACAlgorithmsServer2Client()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getMACAlgorithmsServer2Client()
+     * @var array|false
      */
-    var $mac_algorithms_server_to_client = false;
+    private $mac_algorithms_server_to_client = false;
 
     /**
      * Compression Algorithms: Client to Server
      *
-     * @see Net_SSH2::getCompressionAlgorithmsClient2Server()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getCompressionAlgorithmsClient2Server()
+     * @var array|false
      */
-    var $compression_algorithms_client_to_server = false;
+    private $compression_algorithms_client_to_server = false;
 
     /**
      * Compression Algorithms: Server to Client
      *
-     * @see Net_SSH2::getCompressionAlgorithmsServer2Client()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getCompressionAlgorithmsServer2Client()
+     * @var array|false
      */
-    var $compression_algorithms_server_to_client = false;
+    private $compression_algorithms_server_to_client = false;
 
     /**
      * Languages: Server to Client
      *
-     * @see Net_SSH2::getLanguagesServer2Client()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getLanguagesServer2Client()
+     * @var array|false
      */
-    var $languages_server_to_client = false;
+    private $languages_server_to_client = false;
 
     /**
      * Languages: Client to Server
      *
-     * @see Net_SSH2::getLanguagesClient2Server()
-     * @var mixed false or Array
-     * @access private
+     * @see self::getLanguagesClient2Server()
+     * @var array|false
+     */
+    private $languages_client_to_server = false;
+
+    /**
+     * Preferred Algorithms
+     *
+     * @see self::setPreferredAlgorithms()
+     * @var array
      */
-    var $languages_client_to_server = false;
+    private $preferred = [];
 
     /**
      * Block Size for Server to Client Encryption
@@ -296,58 +361,146 @@ class Net_SSH2
      *
      *  -- http://tools.ietf.org/html/rfc4253#section-6
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @see Net_SSH2::_send_binary_packet()
-     * @var Integer
-     * @access private
+     * @see self::__construct()
+     * @see self::_send_binary_packet()
+     * @var int
      */
-    var $encrypt_block_size = 8;
+    private $encrypt_block_size = 8;
 
     /**
      * Block Size for Client to Server Encryption
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @see Net_SSH2::_get_binary_packet()
-     * @var Integer
-     * @access private
+     * @see self::__construct()
+     * @see self::_get_binary_packet()
+     * @var int
      */
-    var $decrypt_block_size = 8;
+    private $decrypt_block_size = 8;
 
     /**
      * Server to Client Encryption Object
      *
-     * @see Net_SSH2::_get_binary_packet()
-     * @var Object
-     * @access private
+     * @see self::_get_binary_packet()
+     * @var SymmetricKey|false
+     */
+    private $decrypt = false;
+
+    /**
+     * Decryption Algorithm Name
+     *
+     * @var string|null
+     */
+    private $decryptName;
+
+    /**
+     * Decryption Invocation Counter
+     *
+     * Used by GCM
+     *
+     * @var string|null
+     */
+    private $decryptInvocationCounter;
+
+    /**
+     * Fixed Part of Nonce
+     *
+     * Used by GCM
+     *
+     * @var string|null
+     */
+    private $decryptFixedPart;
+
+    /**
+     * Server to Client Length Encryption Object
+     *
+     * @see self::_get_binary_packet()
+     * @var object
      */
-    var $decrypt = false;
+    private $lengthDecrypt = false;
 
     /**
      * Client to Server Encryption Object
      *
-     * @see Net_SSH2::_send_binary_packet()
-     * @var Object
-     * @access private
+     * @see self::_send_binary_packet()
+     * @var SymmetricKey|false
      */
-    var $encrypt = false;
+    private $encrypt = false;
+
+    /**
+     * Encryption Algorithm Name
+     *
+     * @var string|null
+     */
+    private $encryptName;
+
+    /**
+     * Encryption Invocation Counter
+     *
+     * Used by GCM
+     *
+     * @var string|null
+     */
+    private $encryptInvocationCounter;
+
+    /**
+     * Fixed Part of Nonce
+     *
+     * Used by GCM
+     *
+     * @var string|null
+     */
+    private $encryptFixedPart;
+
+    /**
+     * Client to Server Length Encryption Object
+     *
+     * @see self::_send_binary_packet()
+     * @var object
+     */
+    private $lengthEncrypt = false;
 
     /**
      * Client to Server HMAC Object
      *
-     * @see Net_SSH2::_send_binary_packet()
-     * @var Object
-     * @access private
+     * @see self::_send_binary_packet()
+     * @var object
+     */
+    private $hmac_create = false;
+
+    /**
+     * Client to Server HMAC Name
+     *
+     * @var string|false
+     */
+    private $hmac_create_name;
+
+    /**
+     * Client to Server ETM
+     *
+     * @var int|false
      */
-    var $hmac_create = false;
+    private $hmac_create_etm;
 
     /**
      * Server to Client HMAC Object
      *
-     * @see Net_SSH2::_get_binary_packet()
-     * @var Object
-     * @access private
+     * @see self::_get_binary_packet()
+     * @var object
+     */
+    private $hmac_check = false;
+
+    /**
+     * Server to Client HMAC Name
+     *
+     * @var string|false
      */
-    var $hmac_check = false;
+    private $hmac_check_name;
+
+    /**
+     * Server to Client ETM
+     *
+     * @var int|false
+     */
+    private $hmac_check_etm;
 
     /**
      * Size of server to client HMAC
@@ -356,23 +509,21 @@ class Net_SSH2
      * 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 Net_SSH2::_get_binary_packet()
-     * @var Integer
-     * @access private
+     * @see self::_get_binary_packet()
+     * @var int
      */
-    var $hmac_size = false;
+    private $hmac_size = false;
 
     /**
      * Server Public Host Key
      *
-     * @see Net_SSH2::getServerPublicHostKey()
-     * @var String
-     * @access private
+     * @see self::getServerPublicHostKey()
+     * @var string
      */
-    var $server_public_host_key;
+    private $server_public_host_key;
 
     /**
-     * Session identifer
+     * Session identifier
      *
      * "The exchange hash H from the first key exchange is additionally
      *  used as the session identifier, which is a unique identifier for
@@ -380,103 +531,98 @@ class Net_SSH2
      *
      *  -- http://tools.ietf.org/html/rfc4253#section-7.2
      *
-     * @see Net_SSH2::_key_exchange()
-     * @var String
-     * @access private
+     * @see self::_key_exchange()
+     * @var string
      */
-    var $session_id = false;
+    private $session_id = false;
 
     /**
      * Exchange hash
      *
      * The current exchange hash
      *
-     * @see Net_SSH2::_key_exchange()
-     * @var String
-     * @access private
+     * @see self::_key_exchange()
+     * @var string
      */
-    var $exchange_hash = false;
+    private $exchange_hash = false;
 
     /**
      * Message Numbers
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @var Array
+     * @see self::__construct()
+     * @var array
      * @access private
      */
-    var $message_numbers = array();
+    private static $message_numbers = [];
 
     /**
      * Disconnection Message 'reason codes' defined in RFC4253
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @var Array
+     * @see self::__construct()
+     * @var array
      * @access private
      */
-    var $disconnect_reasons = array();
+    private static $disconnect_reasons = [];
 
     /**
      * SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @var Array
+     * @see self::__construct()
+     * @var array
      * @access private
      */
-    var $channel_open_failure_reasons = array();
+    private static $channel_open_failure_reasons = [];
 
     /**
      * Terminal Modes
      *
      * @link http://tools.ietf.org/html/rfc4254#section-8
-     * @see Net_SSH2::Net_SSH2()
-     * @var Array
+     * @see self::__construct()
+     * @var array
      * @access private
      */
-    var $terminal_modes = array();
+    private static $terminal_modes = [];
 
     /**
      * SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes
      *
      * @link http://tools.ietf.org/html/rfc4254#section-5.2
-     * @see Net_SSH2::Net_SSH2()
-     * @var Array
+     * @see self::__construct()
+     * @var array
      * @access private
      */
-    var $channel_extended_data_type_codes = array();
+    private static $channel_extended_data_type_codes = [];
 
     /**
      * Send Sequence Number
      *
      * See 'Section 6.4.  Data Integrity' of rfc4253 for more info.
      *
-     * @see Net_SSH2::_send_binary_packet()
-     * @var Integer
-     * @access private
+     * @see self::_send_binary_packet()
+     * @var int
      */
-    var $send_seq_no = 0;
+    private $send_seq_no = 0;
 
     /**
      * Get Sequence Number
      *
      * See 'Section 6.4.  Data Integrity' of rfc4253 for more info.
      *
-     * @see Net_SSH2::_get_binary_packet()
-     * @var Integer
-     * @access private
+     * @see self::_get_binary_packet()
+     * @var int
      */
-    var $get_seq_no = 0;
+    private $get_seq_no = 0;
 
     /**
      * Server Channels
      *
      * Maps client channels to server channels
      *
-     * @see Net_SSH2::_get_channel_packet()
-     * @see Net_SSH2::exec()
-     * @var Array
-     * @access private
+     * @see self::get_channel_packet()
+     * @see self::exec()
+     * @var array
      */
-    var $server_channels = array();
+    protected $server_channels = [];
 
     /**
      * Channel Buffers
@@ -484,255 +630,243 @@ class Net_SSH2
      * If a client requests a packet from one channel but receives two packets from another those packets should
      * be placed in a buffer
      *
-     * @see Net_SSH2::_get_channel_packet()
-     * @see Net_SSH2::exec()
-     * @var Array
-     * @access private
+     * @see self::get_channel_packet()
+     * @see self::exec()
+     * @var array
      */
-    var $channel_buffers = array();
+    private $channel_buffers = [];
 
     /**
      * Channel Status
      *
      * Contains the type of the last sent message
      *
-     * @see Net_SSH2::_get_channel_packet()
-     * @var Array
-     * @access private
+     * @see self::get_channel_packet()
+     * @var array
+     */
+    protected $channel_status = [];
+
+    /**
+     * The identifier of the interactive channel which was opened most recently
+     *
+     * @see self::getInteractiveChannelId()
+     * @var int
      */
-    var $channel_status = array();
+    private $channel_id_last_interactive = 0;
 
     /**
      * Packet Size
      *
      * Maximum packet size indexed by channel
      *
-     * @see Net_SSH2::_send_channel_packet()
-     * @var Array
-     * @access private
+     * @see self::send_channel_packet()
+     * @var array
      */
-    var $packet_size_client_to_server = array();
+    private $packet_size_client_to_server = [];
 
     /**
      * Message Number Log
      *
-     * @see Net_SSH2::getLog()
-     * @var Array
-     * @access private
+     * @see self::getLog()
+     * @var array
      */
-    var $message_number_log = array();
+    private $message_number_log = [];
 
     /**
      * Message Log
      *
-     * @see Net_SSH2::getLog()
-     * @var Array
-     * @access private
+     * @see self::getLog()
+     * @var array
      */
-    var $message_log = array();
+    private $message_log = [];
 
     /**
      * The Window Size
      *
      * Bytes the other party can send before it must wait for the window to be adjusted (0x7FFFFFFF = 2GB)
      *
-     * @var Integer
-     * @see Net_SSH2::_send_channel_packet()
-     * @see Net_SSH2::exec()
-     * @access private
+     * @var int
+     * @see self::send_channel_packet()
+     * @see self::exec()
+     */
+    protected $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
+     *
+     * @var int
+     * @see self::_send_channel_packet()
+     * @see self::exec()
      */
-    var $window_size = 0x7FFFFFFF;
+    private $window_resize = 0x40000000;
 
     /**
      * Window size, server to client
      *
      * Window size indexed by channel
      *
-     * @see Net_SSH2::_send_channel_packet()
-     * @var Array
-     * @access private
+     * @see self::send_channel_packet()
+     * @var array
      */
-    var $window_size_server_to_client = array();
+    protected $window_size_server_to_client = [];
 
     /**
      * Window size, client to server
      *
      * Window size indexed by channel
      *
-     * @see Net_SSH2::_get_channel_packet()
-     * @var Array
-     * @access private
+     * @see self::get_channel_packet()
+     * @var array
      */
-    var $window_size_client_to_server = array();
+    private $window_size_client_to_server = [];
 
     /**
      * Server signature
      *
      * Verified against $this->session_id
      *
-     * @see Net_SSH2::getServerPublicHostKey()
-     * @var String
-     * @access private
+     * @see self::getServerPublicHostKey()
+     * @var string
      */
-    var $signature = '';
+    private $signature = '';
 
     /**
      * Server signature format
      *
      * ssh-rsa or ssh-dss.
      *
-     * @see Net_SSH2::getServerPublicHostKey()
-     * @var String
-     * @access private
+     * @see self::getServerPublicHostKey()
+     * @var string
      */
-    var $signature_format = '';
+    private $signature_format = '';
 
     /**
      * Interactive Buffer
      *
-     * @see Net_SSH2::read()
-     * @var Array
-     * @access private
+     * @see self::read()
+     * @var string
      */
-    var $interactiveBuffer = '';
+    private $interactiveBuffer = '';
 
     /**
      * Current log size
      *
-     * Should never exceed NET_SSH2_LOG_MAX_SIZE
+     * Should never exceed self::LOG_MAX_SIZE
      *
-     * @see Net_SSH2::_send_binary_packet()
-     * @see Net_SSH2::_get_binary_packet()
-     * @var Integer
-     * @access private
+     * @see self::_send_binary_packet()
+     * @see self::_get_binary_packet()
+     * @var int
      */
-    var $log_size;
+    private $log_size;
 
     /**
      * Timeout
      *
-     * @see Net_SSH2::setTimeout()
-     * @access private
+     * @see self::setTimeout()
      */
-    var $timeout;
+    protected $timeout;
 
     /**
      * Current Timeout
      *
-     * @see Net_SSH2::_get_channel_packet()
-     * @access private
+     * @see self::get_channel_packet()
+     */
+    protected $curTimeout;
+
+    /**
+     * Keep Alive Interval
+     *
+     * @see self::setKeepAlive()
      */
-    var $curTimeout;
+    private $keepAlive;
 
     /**
      * Real-time log file pointer
      *
-     * @see Net_SSH2::_append_log()
-     * @var Resource
-     * @access private
+     * @see self::_append_log()
+     * @var resource|closed-resource
      */
-    var $realtime_log_file;
+    private $realtime_log_file;
 
     /**
      * Real-time log file size
      *
-     * @see Net_SSH2::_append_log()
-     * @var Integer
-     * @access private
+     * @see self::_append_log()
+     * @var int
      */
-    var $realtime_log_size;
+    private $realtime_log_size;
 
     /**
      * Has the signature been validated?
      *
-     * @see Net_SSH2::getServerPublicHostKey()
-     * @var Boolean
-     * @access private
+     * @see self::getServerPublicHostKey()
+     * @var bool
      */
-    var $signature_validated = false;
+    private $signature_validated = false;
 
     /**
      * Real-time log file wrap boolean
      *
-     * @see Net_SSH2::_append_log()
-     * @access private
+     * @see self::_append_log()
+     * @var bool
      */
-    var $realtime_log_wrap;
+    private $realtime_log_wrap;
 
     /**
      * Flag to suppress stderr from output
      *
-     * @see Net_SSH2::enableQuietMode()
-     * @access private
+     * @see self::enableQuietMode()
      */
-    var $quiet_mode = false;
+    private $quiet_mode = false;
 
     /**
      * Time of first network activity
      *
-     * @var Integer
-     * @access private
+     * @var float
      */
-    var $last_packet;
+    private $last_packet;
 
     /**
      * Exit status returned from ssh if any
      *
-     * @var Integer
-     * @access private
+     * @var int
      */
-    var $exit_status;
+    private $exit_status;
 
     /**
      * Flag to request a PTY when using exec()
      *
-     * @var Boolean
-     * @see Net_SSH2::enablePTY()
-     * @access private
-     */
-    var $request_pty = false;
-
-    /**
-     * Flag set while exec() is running when using enablePTY()
-     *
-     * @var Boolean
-     * @access private
-     */
-    var $in_request_pty_exec = false;
-
-    /**
-     * Flag set after startSubsystem() is called
-     *
-     * @var Boolean
-     * @access private
+     * @var bool
+     * @see self::enablePTY()
      */
-    var $in_subsystem;
+    private $request_pty = false;
 
     /**
      * Contents of stdError
      *
-     * @var String
-     * @access private
+     * @var string
      */
-    var $stdErrorLog;
+    private $stdErrorLog;
 
     /**
      * The Last Interactive Response
      *
-     * @see Net_SSH2::_keyboard_interactive_process()
-     * @var String
-     * @access private
+     * @see self::_keyboard_interactive_process()
+     * @var string
      */
-    var $last_interactive_response = '';
+    private $last_interactive_response = '';
 
     /**
      * Keyboard Interactive Request / Responses
      *
-     * @see Net_SSH2::_keyboard_interactive_process()
-     * @var Array
-     * @access private
+     * @see self::_keyboard_interactive_process()
+     * @var array
      */
-    var $keyboard_requests_responses = array();
+    private $keyboard_requests_responses = [];
 
     /**
      * Banner Message
@@ -740,252 +874,494 @@ class Net_SSH2
      * Quoting from the RFC, "in some jurisdictions, sending a warning message before
      * authentication may be relevant for getting legal protection."
      *
-     * @see Net_SSH2::_filter()
-     * @see Net_SSH2::getBannerMessage()
-     * @var String
-     * @access private
+     * @see self::_filter()
+     * @see self::getBannerMessage()
+     * @var string
      */
-    var $banner_message = '';
+    private $banner_message = '';
 
     /**
      * Did read() timeout or return normally?
      *
-     * @see Net_SSH2::isTimeout()
-     * @var Boolean
-     * @access private
+     * @see self::isTimeout()
+     * @var bool
      */
-    var $is_timeout = false;
+    private $is_timeout = false;
 
     /**
      * Log Boundary
      *
-     * @see Net_SSH2::_format_log()
-     * @var String
-     * @access private
+     * @see self::_format_log()
+     * @var string
      */
-    var $log_boundary = ':';
+    private $log_boundary = ':';
 
     /**
      * Log Long Width
      *
-     * @see Net_SSH2::_format_log()
-     * @var Integer
-     * @access private
+     * @see self::_format_log()
+     * @var int
      */
-    var $log_long_width = 65;
+    private $log_long_width = 65;
 
     /**
      * Log Short Width
      *
-     * @see Net_SSH2::_format_log()
-     * @var Integer
-     * @access private
+     * @see self::_format_log()
+     * @var int
      */
-    var $log_short_width = 16;
+    private $log_short_width = 16;
 
     /**
      * Hostname
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @see Net_SSH2::_connect()
-     * @var String
-     * @access private
+     * @see self::__construct()
+     * @see self::_connect()
+     * @var string
      */
-    var $host;
+    private $host;
 
     /**
      * Port Number
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @see Net_SSH2::_connect()
-     * @var Integer
-     * @access private
+     * @see self::__construct()
+     * @see self::_connect()
+     * @var int
      */
-    var $port;
+    private $port;
 
     /**
-     * Timeout for initial connection
-     *
-     * Set by the constructor call. Calling setTimeout() is optional. If it's not called functions like
-     * exec() won't timeout unless some PHP setting forces it too. The timeout specified in the constructor,
-     * however, is non-optional. There will be a timeout, whether or not you set it. If you don't it'll be
-     * 10 seconds. It is used by fsockopen() and the initial stream_select in that function.
+     * Number of columns for terminal window size
      *
-     * @see Net_SSH2::Net_SSH2()
-     * @see Net_SSH2::_connect()
-     * @var Integer
-     * @access private
+     * @see self::getWindowColumns()
+     * @see self::setWindowColumns()
+     * @see self::setWindowSize()
+     * @var int
      */
-    var $connectionTimeout;
+    private $windowColumns = 80;
 
     /**
      * Number of columns for terminal window size
      *
-     * @see Net_SSH2::getWindowColumns()
-     * @see Net_SSH2::setWindowColumns()
-     * @see Net_SSH2::setWindowSize()
-     * @var Integer
-     * @access private
+     * @see self::getWindowRows()
+     * @see self::setWindowRows()
+     * @see self::setWindowSize()
+     * @var int
      */
-    var $windowColumns = 80;
+    private $windowRows = 24;
 
     /**
-     * Number of columns for terminal window size
+     * Crypto Engine
      *
-     * @see Net_SSH2::getWindowRows()
-     * @see Net_SSH2::setWindowRows()
-     * @see Net_SSH2::setWindowSize()
-     * @var Integer
-     * @access private
+     * @see self::setCryptoEngine()
+     * @see self::_key_exchange()
+     * @var int
      */
-    var $windowRows = 24;
+    private static $crypto_engine = false;
 
     /**
-     * Default Constructor.
+     * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario
      *
-     * @param String $host
-     * @param optional Integer $port
-     * @param optional Integer $timeout
-     * @see Net_SSH2::login()
-     * @return Net_SSH2
-     * @access public
-     */
-    function Net_SSH2($host, $port = 22, $timeout = 10)
-    {
-        // Include Math_BigInteger
-        // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification.
-        if (!class_exists('Math_BigInteger')) {
-            include_once 'Math/BigInteger.php';
-        }
-
-        if (!function_exists('crypt_random_string')) {
-            include_once 'Crypt/Random.php';
-        }
-
-        if (!class_exists('Crypt_Hash')) {
-            include_once 'Crypt/Hash.php';
-        }
-
-        $this->message_numbers = array(
-            1 => 'NET_SSH2_MSG_DISCONNECT',
-            2 => 'NET_SSH2_MSG_IGNORE',
-            3 => 'NET_SSH2_MSG_UNIMPLEMENTED',
-            4 => 'NET_SSH2_MSG_DEBUG',
-            5 => 'NET_SSH2_MSG_SERVICE_REQUEST',
-            6 => 'NET_SSH2_MSG_SERVICE_ACCEPT',
-            20 => 'NET_SSH2_MSG_KEXINIT',
-            21 => 'NET_SSH2_MSG_NEWKEYS',
-            30 => 'NET_SSH2_MSG_KEXDH_INIT',
-            31 => 'NET_SSH2_MSG_KEXDH_REPLY',
-            50 => 'NET_SSH2_MSG_USERAUTH_REQUEST',
-            51 => 'NET_SSH2_MSG_USERAUTH_FAILURE',
-            52 => 'NET_SSH2_MSG_USERAUTH_SUCCESS',
-            53 => 'NET_SSH2_MSG_USERAUTH_BANNER',
-
-            80 => 'NET_SSH2_MSG_GLOBAL_REQUEST',
-            81 => 'NET_SSH2_MSG_REQUEST_SUCCESS',
-            82 => 'NET_SSH2_MSG_REQUEST_FAILURE',
-            90 => 'NET_SSH2_MSG_CHANNEL_OPEN',
-            91 => 'NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION',
-            92 => 'NET_SSH2_MSG_CHANNEL_OPEN_FAILURE',
-            93 => 'NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST',
-            94 => 'NET_SSH2_MSG_CHANNEL_DATA',
-            95 => 'NET_SSH2_MSG_CHANNEL_EXTENDED_DATA',
-            96 => 'NET_SSH2_MSG_CHANNEL_EOF',
-            97 => 'NET_SSH2_MSG_CHANNEL_CLOSE',
-            98 => 'NET_SSH2_MSG_CHANNEL_REQUEST',
-            99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS',
-            100 => 'NET_SSH2_MSG_CHANNEL_FAILURE'
-        );
-        $this->disconnect_reasons = array(
-            1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT',
-            2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR',
-            3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED',
-            4 => 'NET_SSH2_DISCONNECT_RESERVED',
-            5 => 'NET_SSH2_DISCONNECT_MAC_ERROR',
-            6 => 'NET_SSH2_DISCONNECT_COMPRESSION_ERROR',
-            7 => 'NET_SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE',
-            8 => 'NET_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED',
-            9 => 'NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE',
-            10 => 'NET_SSH2_DISCONNECT_CONNECTION_LOST',
-            11 => 'NET_SSH2_DISCONNECT_BY_APPLICATION',
-            12 => 'NET_SSH2_DISCONNECT_TOO_MANY_CONNECTIONS',
-            13 => 'NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER',
-            14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE',
-            15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME'
-        );
-        $this->channel_open_failure_reasons = array(
-            1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED'
-        );
-        $this->terminal_modes = array(
-            0 => 'NET_SSH2_TTY_OP_END'
-        );
-        $this->channel_extended_data_type_codes = array(
-            1 => 'NET_SSH2_EXTENDED_DATA_STDERR'
-        );
-
-        $this->_define_array(
-            $this->message_numbers,
-            $this->disconnect_reasons,
-            $this->channel_open_failure_reasons,
-            $this->terminal_modes,
-            $this->channel_extended_data_type_codes,
-            array(60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'),
-            array(60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'),
-            array(60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST',
-                  61 => 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE')
-        );
+     * @var Agent
+     */
+    private $agent;
 
-        $this->host = $host;
-        $this->port = $port;
-        $this->connectionTimeout = $timeout;
-    }
+    /**
+     * Connection storage to replicates ssh2 extension functionality:
+     * {@link http://php.net/manual/en/wrappers.ssh2.php#refsect1-wrappers.ssh2-examples}
+     *
+     * @var array<string, SSH2|\WeakReference<SSH2>>
+     */
+    private static $connections;
 
     /**
-     * Connect to an SSHv2 server
+     * Send the identification string first?
      *
-     * @return Boolean
-     * @access private
+     * @var bool
      */
-    function _connect()
-    {
-        if ($this->bitmap & NET_SSH2_MASK_CONSTRUCTOR) {
-            return false;
-        }
+    private $send_id_string_first = true;
 
-        $this->bitmap |= NET_SSH2_MASK_CONSTRUCTOR;
+    /**
+     * Send the key exchange initiation packet first?
+     *
+     * @var bool
+     */
+    private $send_kex_first = true;
 
-        $timeout = $this->connectionTimeout;
-        $host = $this->host . ':' . $this->port;
+    /**
+     * Some versions of OpenSSH incorrectly calculate the key size
+     *
+     * @var bool
+     */
+    private $bad_key_size_fix = false;
 
-        $this->last_packet = strtok(microtime(), ' ') + strtok(''); // == microtime(true) in PHP5
+    /**
+     * Should we try to re-connect to re-establish keys?
+     *
+     * @var bool
+     */
+    private $retry_connect = false;
 
-        $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838
-        $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $timeout);
-        if (!$this->fsock) {
-            user_error(rtrim("Cannot connect to $host. Error $errno. $errstr"));
-            return false;
+    /**
+     * Binary Packet Buffer
+     *
+     * @var string|false
+     */
+    private $binary_packet_buffer = false;
+
+    /**
+     * Preferred Signature Format
+     *
+     * @var string|false
+     */
+    protected $preferred_signature_format = false;
+
+    /**
+     * Authentication Credentials
+     *
+     * @var array
+     */
+    protected $auth = [];
+
+    /**
+     * Terminal
+     *
+     * @var string
+     */
+    private $term = 'vt100';
+
+    /**
+     * The authentication methods that may productively continue authentication.
+     *
+     * @see https://tools.ietf.org/html/rfc4252#section-5.1
+     * @var array|null
+     */
+    private $auth_methods_to_continue = null;
+
+    /**
+     * Compression method
+     *
+     * @var int
+     */
+    private $compress = self::NET_SSH2_COMPRESSION_NONE;
+
+    /**
+     * Decompression method
+     *
+     * @var int
+     */
+    private $decompress = self::NET_SSH2_COMPRESSION_NONE;
+
+    /**
+     * Compression context
+     *
+     * @var resource|false|null
+     */
+    private $compress_context;
+
+    /**
+     * Decompression context
+     *
+     * @var resource|object
+     */
+    private $decompress_context;
+
+    /**
+     * Regenerate Compression Context
+     *
+     * @var bool
+     */
+    private $regenerate_compression_context = false;
+
+    /**
+     * Regenerate Decompression Context
+     *
+     * @var bool
+     */
+    private $regenerate_decompression_context = false;
+
+    /**
+     * Smart multi-factor authentication flag
+     *
+     * @var bool
+     */
+    private $smartMFA = true;
+
+    /**
+     * How many channels are currently opened
+     *
+     * @var int
+     */
+    private $channelCount = 0;
+
+    /**
+     * Does the server support multiple channels? If not then error out
+     * when multiple channels are attempted to be opened
+     *
+     * @var bool
+     */
+    private $errorOnMultipleChannels;
+
+    /**
+     * Terrapin Countermeasure
+     *
+     * "During initial KEX, terminate the connection if any unexpected or out-of-sequence packet is received"
+     * -- https://github.com/openssh/openssh-portable/commit/1edb00c58f8a6875fad6a497aa2bacf37f9e6cd5
+     *
+     * @var int
+     */
+    private $extra_packets;
+
+    /**
+     * 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
+     *
+     * @param mixed $host
+     * @param int $port
+     * @param int $timeout
+     * @see self::login()
+     */
+    public function __construct($host, $port = 22, $timeout = 10)
+    {
+        if (empty(self::$message_numbers)) {
+            self::$message_numbers = [
+                1 => 'NET_SSH2_MSG_DISCONNECT',
+                2 => 'NET_SSH2_MSG_IGNORE',
+                3 => 'NET_SSH2_MSG_UNIMPLEMENTED',
+                4 => 'NET_SSH2_MSG_DEBUG',
+                5 => 'NET_SSH2_MSG_SERVICE_REQUEST',
+                6 => 'NET_SSH2_MSG_SERVICE_ACCEPT',
+                7 => 'NET_SSH2_MSG_EXT_INFO', // RFC 8308
+                20 => 'NET_SSH2_MSG_KEXINIT',
+                21 => 'NET_SSH2_MSG_NEWKEYS',
+                30 => 'NET_SSH2_MSG_KEXDH_INIT',
+                31 => 'NET_SSH2_MSG_KEXDH_REPLY',
+                50 => 'NET_SSH2_MSG_USERAUTH_REQUEST',
+                51 => 'NET_SSH2_MSG_USERAUTH_FAILURE',
+                52 => 'NET_SSH2_MSG_USERAUTH_SUCCESS',
+                53 => 'NET_SSH2_MSG_USERAUTH_BANNER',
+
+                80 => 'NET_SSH2_MSG_GLOBAL_REQUEST',
+                81 => 'NET_SSH2_MSG_REQUEST_SUCCESS',
+                82 => 'NET_SSH2_MSG_REQUEST_FAILURE',
+                90 => 'NET_SSH2_MSG_CHANNEL_OPEN',
+                91 => 'NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION',
+                92 => 'NET_SSH2_MSG_CHANNEL_OPEN_FAILURE',
+                93 => 'NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST',
+                94 => 'NET_SSH2_MSG_CHANNEL_DATA',
+                95 => 'NET_SSH2_MSG_CHANNEL_EXTENDED_DATA',
+                96 => 'NET_SSH2_MSG_CHANNEL_EOF',
+                97 => 'NET_SSH2_MSG_CHANNEL_CLOSE',
+                98 => 'NET_SSH2_MSG_CHANNEL_REQUEST',
+                99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS',
+                100 => 'NET_SSH2_MSG_CHANNEL_FAILURE'
+            ];
+            self::$disconnect_reasons = [
+                1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT',
+                2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR',
+                3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED',
+                4 => 'NET_SSH2_DISCONNECT_RESERVED',
+                5 => 'NET_SSH2_DISCONNECT_MAC_ERROR',
+                6 => 'NET_SSH2_DISCONNECT_COMPRESSION_ERROR',
+                7 => 'NET_SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE',
+                8 => 'NET_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED',
+                9 => 'NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE',
+                10 => 'NET_SSH2_DISCONNECT_CONNECTION_LOST',
+                11 => 'NET_SSH2_DISCONNECT_BY_APPLICATION',
+                12 => 'NET_SSH2_DISCONNECT_TOO_MANY_CONNECTIONS',
+                13 => 'NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER',
+                14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE',
+                15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME'
+            ];
+            self::$channel_open_failure_reasons = [
+                1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED'
+            ];
+            self::$terminal_modes = [
+                0 => 'NET_SSH2_TTY_OP_END'
+            ];
+            self::$channel_extended_data_type_codes = [
+                1 => 'NET_SSH2_EXTENDED_DATA_STDERR'
+            ];
+
+            self::define_array(
+                self::$message_numbers,
+                self::$disconnect_reasons,
+                self::$channel_open_failure_reasons,
+                self::$terminal_modes,
+                self::$channel_extended_data_type_codes,
+                [60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'],
+                [60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'],
+                [60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST',
+                      61 => 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'],
+                // RFC 4419 - diffie-hellman-group-exchange-sha{1,256}
+                [30 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST_OLD',
+                      31 => 'NET_SSH2_MSG_KEXDH_GEX_GROUP',
+                      32 => 'NET_SSH2_MSG_KEXDH_GEX_INIT',
+                      33 => 'NET_SSH2_MSG_KEXDH_GEX_REPLY',
+                      34 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'],
+                // RFC 5656 - Elliptic Curves (for curve25519-sha256@libssh.org)
+                [30 => 'NET_SSH2_MSG_KEX_ECDH_INIT',
+                      31 => 'NET_SSH2_MSG_KEX_ECDH_REPLY']
+            );
         }
-        $elapsed = strtok(microtime(), ' ') + strtok('') - $start;
 
-        $timeout-= $elapsed;
+        /**
+         * Typehint is required due to a bug in Psalm: https://github.com/vimeo/psalm/issues/7508
+         * @var \WeakReference<SSH2>|SSH2
+         */
+        self::$connections[$this->getResourceId()] = class_exists('WeakReference')
+            ? \WeakReference::create($this)
+            : $this;
 
-        if ($timeout <= 0) {
-            user_error("Cannot connect to $host. Timeout error");
-            return false;
+        $this->timeout = $timeout;
+
+        if (is_resource($host)) {
+            $this->fsock = $host;
+            return;
+        }
+
+        if (Strings::is_stringable($host)) {
+            $this->host = $host;
+            $this->port = $port;
+        }
+    }
+
+    /**
+     * Set Crypto Engine Mode
+     *
+     * Possible $engine values:
+     * OpenSSL, mcrypt, Eval, PHP
+     *
+     * @param int $engine
+     */
+    public static function setCryptoEngine($engine)
+    {
+        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
+     *
+     */
+    public function sendIdentificationStringFirst()
+    {
+        $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
+     *
+     */
+    public function sendIdentificationStringLast()
+    {
+        $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
+     *
+     */
+    public function sendKEXINITFirst()
+    {
+        $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
+     *
+     */
+    public function sendKEXINITLast()
+    {
+        $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(&$read, &$write, &$except, $seconds, $microseconds = null)
+    {
+        $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
+     *
+     * @throws \UnexpectedValueException on receipt of unexpected packets
+     * @throws \RuntimeException on other errors
+     */
+    private function connect()
+    {
+        if ($this->bitmap & self::MASK_CONSTRUCTOR) {
+            return;
         }
 
-        $read = array($this->fsock);
-        $write = $except = null;
+        $this->bitmap |= self::MASK_CONSTRUCTOR;
+
+        $this->curTimeout = $this->timeout;
 
-        $sec = floor($timeout);
-        $usec = 1000000 * ($timeout - $sec);
+        $this->last_packet = microtime(true);
+
+        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 UnableToConnectException(rtrim("Cannot connect to $host. Error $errno. $errstr"));
+            }
+            $elapsed = microtime(true) - $start;
 
-        // on windows this returns a "Warning: Invalid CRT parameters detected" error
-        // the !count() is done as a workaround for <https://bugs.php.net/42682>
-        if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) {
-            user_error("Cannot connect to $host. Banner timeout");
-            return false;
+            if ($this->curTimeout) {
+                $this->curTimeout -= $elapsed;
+                if ($this->curTimeout < 0) {
+                    throw new \RuntimeException('Connection timed out whilst attempting to open socket connection');
+                }
+            }
+        }
+
+        $this->identifier = $this->generate_identifier();
+
+        if ($this->send_id_string_first) {
+            fputs($this->fsock, $this->identifier . "\r\n");
         }
 
         /* According to the SSH2 specs,
@@ -995,56 +1371,106 @@ class Net_SSH2
            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." */
-        $temp = '';
-        $extra = '';
-        while (!feof($this->fsock) && !preg_match('#^SSH-(\d\.\d+)#', $temp, $matches)) {
-            if (substr($temp, -2) == "\r\n") {
-                $extra.= $temp;
-                $temp = '';
+        $data = '';
+        while (!feof($this->fsock) && !preg_match('#(.*)^(SSH-(\d\.\d+).*)#ms', $data, $matches)) {
+            $line = '';
+            while (true) {
+                if ($this->curTimeout) {
+                    if ($this->curTimeout < 0) {
+                        throw new \RuntimeException('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 \RuntimeException('Connection timed out whilst receiving server identification string');
+                    }
+                    $elapsed = microtime(true) - $start;
+                    $this->curTimeout -= $elapsed;
+                }
+
+                $temp = stream_get_line($this->fsock, 255, "\n");
+                if ($temp === false) {
+                    throw new \RuntimeException('Error reading from socket');
+                }
+                if (strlen($temp) == 255) {
+                    continue;
+                }
+
+                $line .= "$temp\n";
+
+                // quoting RFC4253, "Implementers who wish to maintain
+                // compatibility with older, undocumented versions of this protocol may
+                // want to process the identification string without expecting the
+                // presence of the carriage return character for reasons described in
+                // Section 5 of this document."
+
+                //if (substr($line, -2) == "\r\n") {
+                //    break;
+                //}
+
+                break;
             }
-            $temp.= fgets($this->fsock, 255);
+
+            $data .= $line;
         }
 
         if (feof($this->fsock)) {
-            user_error('Connection closed by server');
-            return false;
+            $this->bitmap = 0;
+            throw new ConnectionClosedException('Connection closed by server');
         }
 
-        $this->identifier = $this->_generate_identifier();
+        $extra = $matches[1];
 
         if (defined('NET_SSH2_LOGGING')) {
-            $this->_append_log('<-', $extra . $temp);
-            $this->_append_log('->', $this->identifier . "\r\n");
+            $this->append_log('<-', $matches[0]);
+            $this->append_log('->', $this->identifier . "\r\n");
         }
 
         $this->server_identifier = trim($temp, "\r\n");
         if (strlen($extra)) {
-            $this->errors[] = utf8_decode($extra);
+            $this->errors[] = $data;
         }
 
-        if ($matches[1] != '1.99' && $matches[1] != '2.0') {
-            user_error("Cannot connect to SSH $matches[1] servers");
-            return false;
+        if (version_compare($matches[3], '1.99', '<')) {
+            $this->bitmap = 0;
+            throw new UnableToConnectException("Cannot connect to SSH $matches[3] servers");
         }
 
-        fputs($this->fsock, $this->identifier . "\r\n");
+        // 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;
 
-        $response = $this->_get_binary_packet();
-        if ($response === false) {
-            user_error('Connection closed by server');
-            return false;
+        if (!$this->send_id_string_first) {
+            fputs($this->fsock, $this->identifier . "\r\n");
         }
 
-        if (ord($response[0]) != NET_SSH2_MSG_KEXINIT) {
-            user_error('Expected SSH_MSG_KEXINIT');
-            return false;
+        if (!$this->send_kex_first) {
+            $response = $this->get_binary_packet();
+
+            if (is_bool($response) || !strlen($response) || ord($response[0]) != NET_SSH2_MSG_KEXINIT) {
+                $this->bitmap = 0;
+                throw new \UnexpectedValueException('Expected SSH_MSG_KEXINIT');
+            }
+
+            $this->key_exchange($response);
         }
 
-        if (!$this->_key_exchange($response)) {
-            return false;
+        if ($this->send_kex_first) {
+            $this->key_exchange();
         }
 
-        $this->bitmap|= NET_SSH2_MASK_CONNECTED;
+        $this->bitmap |= self::MASK_CONNECTED;
 
         return true;
     }
@@ -1054,15 +1480,20 @@ class Net_SSH2
      *
      * You should overwrite this method in your own class if you want to use another identifier
      *
-     * @access protected
-     * @return String
+     * @return string
      */
-    function _generate_identifier()
+    private function generate_identifier()
     {
-        $identifier = 'SSH-2.0-phpseclib_0.3';
+        $identifier = 'SSH-2.0-phpseclib_3.0';
+
+        $ext = [];
+        if (extension_loaded('sodium')) {
+            $ext[] = 'libsodium';
+        }
 
-        $ext = array();
-        if (extension_loaded('mcrypt')) {
+        if (extension_loaded('openssl')) {
+            $ext[] = 'openssl';
+        } elseif (extension_loaded('mcrypt')) {
             $ext[] = 'mcrypt';
         }
 
@@ -1082,372 +1513,350 @@ class Net_SSH2
     /**
      * Key Exchange
      *
-     * @param String $kexinit_payload_server
-     * @access private
+     * @return bool
+     * @param string|bool $kexinit_payload_server optional
+     * @throws \UnexpectedValueException on receipt of unexpected packets
+     * @throws \RuntimeException on other errors
+     * @throws \phpseclib3\Exception\NoSupportedAlgorithmsException when none of the algorithms phpseclib has loaded are compatible
      */
-    function _key_exchange($kexinit_payload_server)
+    private function key_exchange($kexinit_payload_server = false)
     {
-        static $kex_algorithms = array(
-            'diffie-hellman-group1-sha1', // REQUIRED
-            'diffie-hellman-group14-sha1' // REQUIRED
-        );
-
-        static $server_host_key_algorithms = array(
-            'ssh-rsa', // RECOMMENDED  sign   Raw RSA Key
-            'ssh-dss'  // REQUIRED     sign   Raw DSS Key
-        );
+        $preferred = $this->preferred;
+        $send_kex = true;
+
+        $kex_algorithms = isset($preferred['kex']) ?
+            $preferred['kex'] :
+            SSH2::getSupportedKEXAlgorithms();
+        $server_host_key_algorithms = isset($preferred['hostkey']) ?
+            $preferred['hostkey'] :
+            SSH2::getSupportedHostKeyAlgorithms();
+        $s2c_encryption_algorithms = isset($preferred['server_to_client']['crypt']) ?
+            $preferred['server_to_client']['crypt'] :
+            SSH2::getSupportedEncryptionAlgorithms();
+        $c2s_encryption_algorithms = isset($preferred['client_to_server']['crypt']) ?
+            $preferred['client_to_server']['crypt'] :
+            SSH2::getSupportedEncryptionAlgorithms();
+        $s2c_mac_algorithms = isset($preferred['server_to_client']['mac']) ?
+            $preferred['server_to_client']['mac'] :
+            SSH2::getSupportedMACAlgorithms();
+        $c2s_mac_algorithms = isset($preferred['client_to_server']['mac']) ?
+            $preferred['client_to_server']['mac'] :
+            SSH2::getSupportedMACAlgorithms();
+        $s2c_compression_algorithms = isset($preferred['server_to_client']['comp']) ?
+            $preferred['server_to_client']['comp'] :
+            SSH2::getSupportedCompressionAlgorithms();
+        $c2s_compression_algorithms = isset($preferred['client_to_server']['comp']) ?
+            $preferred['client_to_server']['comp'] :
+            SSH2::getSupportedCompressionAlgorithms();
+
+        $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c', 'kex-strict-c-v00@openssh.com']);
 
-        static $encryption_algorithms = false;
-        if ($encryption_algorithms === false) {
-            $encryption_algorithms = array(
-                // 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
-
-                '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 (phpseclib_resolve_include_path('Crypt/RC4.php') === false) {
-                $encryption_algorithms = array_diff(
-                    $encryption_algorithms,
-                    array('arcfour256', 'arcfour128', 'arcfour')
-                );
-            }
-            if (phpseclib_resolve_include_path('Crypt/Rijndael.php') === false) {
-                $encryption_algorithms = array_diff(
-                    $encryption_algorithms,
-                    array('aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-cbc', 'aes192-cbc', 'aes256-cbc')
-                );
-            }
-            if (phpseclib_resolve_include_path('Crypt/Twofish.php') === false) {
-                $encryption_algorithms = array_diff(
-                    $encryption_algorithms,
-                    array('twofish128-ctr', 'twofish192-ctr', 'twofish256-ctr', 'twofish128-cbc', 'twofish192-cbc', 'twofish256-cbc', 'twofish-cbc')
-                );
-            }
-            if (phpseclib_resolve_include_path('Crypt/Blowfish.php') === false) {
-                $encryption_algorithms = array_diff(
-                    $encryption_algorithms,
-                    array('blowfish-ctr', 'blowfish-cbc')
-                );
-            }
-            if (phpseclib_resolve_include_path('Crypt/TripleDES.php') === false) {
-                $encryption_algorithms = array_diff(
-                    $encryption_algorithms,
-                    array('3des-ctr', '3des-cbc')
-                );
-            }
-            $encryption_algorithms = array_values($encryption_algorithms);
+        // 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']
+                    ));
+                }
         }
 
-        $mac_algorithms = array(
-            // from <http://www.ietf.org/rfc/rfc6668.txt>:
-            'hmac-sha2-256',// RECOMMENDED     HMAC-SHA256 (digest length = key length = 32)
-
-            '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)
-            //'none'          // OPTIONAL        no MAC; NOT RECOMMENDED
+        $client_cookie = Random::string(16);
+
+        $kexinit_payload_client = pack('Ca*', NET_SSH2_MSG_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
         );
 
-        static $compression_algorithms = array(
-            'none'   // REQUIRED        no compression
-            //'zlib' // OPTIONAL        ZLIB (LZ77) compression
-        );
+        if ($kexinit_payload_server === false) {
+            $this->send_binary_packet($kexinit_payload_client);
 
-        // some SSH servers have buggy implementations of some of the above algorithms
-        switch ($this->server_identifier) {
-            case 'SSH-2.0-SSHD':
-                $mac_algorithms = array_values(array_diff(
-                    $mac_algorithms,
-                    array('hmac-sha1-96', 'hmac-md5-96')
-                ));
-        }
+            $this->extra_packets = 0;
+            $kexinit_payload_server = $this->get_binary_packet();
 
-        static $str_kex_algorithms, $str_server_host_key_algorithms,
-               $encryption_algorithms_server_to_client, $mac_algorithms_server_to_client, $compression_algorithms_server_to_client,
-               $encryption_algorithms_client_to_server, $mac_algorithms_client_to_server, $compression_algorithms_client_to_server;
+            if (
+                is_bool($kexinit_payload_server)
+                || !strlen($kexinit_payload_server)
+                || ord($kexinit_payload_server[0]) != NET_SSH2_MSG_KEXINIT
+            ) {
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR);
+                throw new \UnexpectedValueException('Expected SSH_MSG_KEXINIT');
+            }
 
-        if (empty($str_kex_algorithms)) {
-            $str_kex_algorithms = implode(',', $kex_algorithms);
-            $str_server_host_key_algorithms = implode(',', $server_host_key_algorithms);
-            $encryption_algorithms_server_to_client = $encryption_algorithms_client_to_server = implode(',', $encryption_algorithms);
-            $mac_algorithms_server_to_client = $mac_algorithms_client_to_server = implode(',', $mac_algorithms);
-            $compression_algorithms_server_to_client = $compression_algorithms_client_to_server = implode(',', $compression_algorithms);
+            $send_kex = false;
         }
 
-        $client_cookie = crypt_random_string(16);
-
         $response = $kexinit_payload_server;
-        $this->_string_shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT)
-        $server_cookie = $this->_string_shift($response, 16);
-
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->kex_algorithms = explode(',', $this->_string_shift($response, $temp['length']));
-
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->server_host_key_algorithms = explode(',', $this->_string_shift($response, $temp['length']));
-
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->encryption_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length']));
-
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->encryption_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length']));
-
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->mac_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length']));
+        Strings::shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT)
+        $server_cookie = Strings::shift($response, 16);
+
+        list(
+            $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 ($this->session_id === false && $this->extra_packets) {
+                throw new \UnexpectedValueException('Possible Terrapin Attack detected');
+            }
+        }
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->mac_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length']));
+        $this->supported_private_key_algorithms = $this->server_host_key_algorithms;
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->compression_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length']));
+        if ($send_kex) {
+            $this->send_binary_packet($kexinit_payload_client);
+        }
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->compression_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length']));
+        // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->languages_client_to_server = explode(',', $this->_string_shift($response, $temp['length']));
+        // 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);
+        $decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt);
+        if ($decryptKeyLength === null) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible server to client encryption algorithms found');
+        }
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->languages_server_to_client = explode(',', $this->_string_shift($response, $temp['length']));
+        $encrypt = self::array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server);
+        $encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt);
+        if ($encryptKeyLength === null) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible client to server encryption algorithms found');
+        }
 
-        extract(unpack('Cfirst_kex_packet_follows', $this->_string_shift($response, 1)));
-        $first_kex_packet_follows = $first_kex_packet_follows != 0;
+        // 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 === false) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible key exchange algorithms found');
+        }
 
-        // the sending of SSH2_MSG_KEXINIT could go in one of two places.  this is the second place.
-        $kexinit_payload_client = pack('Ca*Na*Na*Na*Na*Na*Na*Na*Na*Na*Na*CN',
-            NET_SSH2_MSG_KEXINIT, $client_cookie, strlen($str_kex_algorithms), $str_kex_algorithms,
-            strlen($str_server_host_key_algorithms), $str_server_host_key_algorithms, strlen($encryption_algorithms_client_to_server),
-            $encryption_algorithms_client_to_server, strlen($encryption_algorithms_server_to_client), $encryption_algorithms_server_to_client,
-            strlen($mac_algorithms_client_to_server), $mac_algorithms_client_to_server, strlen($mac_algorithms_server_to_client),
-            $mac_algorithms_server_to_client, strlen($compression_algorithms_client_to_server), $compression_algorithms_client_to_server,
-            strlen($compression_algorithms_server_to_client), $compression_algorithms_server_to_client, 0, '', 0, '',
-            0, 0
-        );
+        $server_host_key_algorithm = self::array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms);
+        if ($server_host_key_algorithm === false) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible server host key algorithms found');
+        }
 
-        if (!$this->_send_binary_packet($kexinit_payload_client)) {
-            return false;
+        $mac_algorithm_out = self::array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server);
+        if ($mac_algorithm_out === false) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible client to server message authentication algorithms found');
         }
-        // here ends the second place.
 
-        // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange
-        for ($i = 0; $i < count($encryption_algorithms) && !in_array($encryption_algorithms[$i], $this->encryption_algorithms_server_to_client); $i++);
-        if ($i == count($encryption_algorithms)) {
-            user_error('No compatible server to client encryption algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+        $mac_algorithm_in = self::array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client);
+        if ($mac_algorithm_in === false) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible server to client message authentication algorithms found');
         }
 
-        // 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 = $encryption_algorithms[$i];
-        switch ($decrypt) {
-            case '3des-cbc':
-            case '3des-ctr':
-                $decryptKeyLength = 24; // eg. 192 / 8
-                break;
-            case 'aes256-cbc':
-            case 'aes256-ctr':
-            case 'twofish-cbc':
-            case 'twofish256-cbc':
-            case 'twofish256-ctr':
-                $decryptKeyLength = 32; // eg. 256 / 8
-                break;
-            case 'aes192-cbc':
-            case 'aes192-ctr':
-            case 'twofish192-cbc':
-            case 'twofish192-ctr':
-                $decryptKeyLength = 24; // eg. 192 / 8
-                break;
-            case 'aes128-cbc':
-            case 'aes128-ctr':
-            case 'twofish128-cbc':
-            case 'twofish128-ctr':
-            case 'blowfish-cbc':
-            case 'blowfish-ctr':
-                $decryptKeyLength = 16; // eg. 128 / 8
-                break;
-            case 'arcfour':
-            case 'arcfour128':
-                $decryptKeyLength = 16; // eg. 128 / 8
-                break;
-            case 'arcfour256':
-                $decryptKeyLength = 32; // eg. 128 / 8
-                break;
-            case 'none';
-                $decryptKeyLength = 0;
+        $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 === false) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible server to client compression algorithms found');
         }
+        $this->decompress = $compression_map[$compression_algorithm_in];
 
-        for ($i = 0; $i < count($encryption_algorithms) && !in_array($encryption_algorithms[$i], $this->encryption_algorithms_client_to_server); $i++);
-        if ($i == count($encryption_algorithms)) {
-            user_error('No compatible client to server encryption algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+        $compression_algorithm_out = self::array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server);
+        if ($compression_algorithm_out === false) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+            throw new NoSupportedAlgorithmsException('No compatible client to server compression algorithms found');
         }
+        $this->compress = $compression_map[$compression_algorithm_out];
 
-        $encrypt = $encryption_algorithms[$i];
-        switch ($encrypt) {
-            case '3des-cbc':
-            case '3des-ctr':
-                $encryptKeyLength = 24;
-                break;
-            case 'aes256-cbc':
-            case 'aes256-ctr':
-            case 'twofish-cbc':
-            case 'twofish256-cbc':
-            case 'twofish256-ctr':
-                $encryptKeyLength = 32;
+        switch ($this->kex_algorithm) {
+            case 'diffie-hellman-group15-sha512':
+            case 'diffie-hellman-group16-sha512':
+            case 'diffie-hellman-group17-sha512':
+            case 'diffie-hellman-group18-sha512':
+            case 'ecdh-sha2-nistp521':
+                $kexHash = new Hash('sha512');
                 break;
-            case 'aes192-cbc':
-            case 'aes192-ctr':
-            case 'twofish192-cbc':
-            case 'twofish192-ctr':
-                $encryptKeyLength = 24;
-                break;
-            case 'aes128-cbc':
-            case 'aes128-ctr':
-            case 'twofish128-cbc':
-            case 'twofish128-ctr':
-            case 'blowfish-cbc':
-            case 'blowfish-ctr':
-                $encryptKeyLength = 16;
+            case 'ecdh-sha2-nistp384':
+                $kexHash = new Hash('sha384');
                 break;
-            case 'arcfour':
-            case 'arcfour128':
-                $encryptKeyLength = 16;
-                break;
-            case 'arcfour256':
-                $encryptKeyLength = 32;
+            case 'diffie-hellman-group-exchange-sha256':
+            case 'diffie-hellman-group14-sha256':
+            case 'ecdh-sha2-nistp256':
+            case 'curve25519-sha256@libssh.org':
+            case 'curve25519-sha256':
+                $kexHash = new Hash('sha256');
                 break;
-            case 'none';
-                $encryptKeyLength = 0;
+            default:
+                $kexHash = new Hash('sha1');
         }
 
-        $keyLength = $decryptKeyLength > $encryptKeyLength ? $decryptKeyLength : $encryptKeyLength;
+        // Only relevant in diffie-hellman-group-exchange-sha{1,256}, otherwise empty.
 
-        // through diffie-hellman key exchange a symmetric key is obtained
-        for ($i = 0; $i < count($kex_algorithms) && !in_array($kex_algorithms[$i], $this->kex_algorithms); $i++);
-        if ($i == count($kex_algorithms)) {
-            user_error('No compatible key exchange algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
-        }
-
-        switch ($kex_algorithms[$i]) {
-            // see http://tools.ietf.org/html/rfc2409#section-6.2 and
-            // http://tools.ietf.org/html/rfc2412, appendex E
-            case 'diffie-hellman-group1-sha1':
-                $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' .
-                         '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' .
-                         '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' .
-                         'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF';
-                break;
-            // see http://tools.ietf.org/html/rfc3526#section-3
-            case 'diffie-hellman-group14-sha1':
-                $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' .
-                         '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' .
-                         '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' .
-                         'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' .
-                         '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' .
-                         '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' .
-                         'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' .
-                         '3995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF';
-                break;
-        }
+        $exchange_hash_rfc4419 = '';
 
-        // For both diffie-hellman-group1-sha1 and diffie-hellman-group14-sha1
-        // the generator field element is 2 (decimal) and the hash function is sha1.
-        $g = new Math_BigInteger(2);
-        $prime = new Math_BigInteger($prime, 16);
-        $kexHash = new Crypt_Hash('sha1');
-        //$q = $p->bitwise_rightShift(1);
+        if (strpos($this->kex_algorithm, 'curve25519-sha256') === 0 || strpos($this->kex_algorithm, 'ecdh-sha2-nistp') === 0) {
+            $curve = strpos($this->kex_algorithm, 'curve25519-sha256') === 0 ?
+                'Curve25519' :
+                substr($this->kex_algorithm, 10);
+            $ourPrivate = EC::createKey($curve);
+            $ourPublicBytes = $ourPrivate->getPublicKey()->getEncodedCoordinates();
+            $clientKexInitMessage = 'NET_SSH2_MSG_KEX_ECDH_INIT';
+            $serverKexReplyMessage = 'NET_SSH2_MSG_KEX_ECDH_REPLY';
+        } else {
+            if (strpos($this->kex_algorithm, 'diffie-hellman-group-exchange') === 0) {
+                $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*',
+                    NET_SSH2_MSG_KEXDH_GEX_REQUEST,
+                    $dh_group_sizes_packed
+                );
+                $this->send_binary_packet($packet);
+                $this->updateLogHistory('UNKNOWN (34)', 'NET_SSH2_MSG_KEXDH_GEX_REQUEST');
 
-        /* To increase the speed of the key exchange, both client and server may
-           reduce the size of their private exponents.  It should be at least
-           twice as long as the key material that is generated from the shared
-           secret.  For more details, see the paper by van Oorschot and Wiener
-           [VAN-OORSCHOT].
+                $response = $this->get_binary_packet();
 
-           -- http://tools.ietf.org/html/rfc4419#section-6.2 */
-        $one = new Math_BigInteger(1);
-        $keyLength = min($keyLength, $kexHash->getLength());
-        $max = $one->bitwise_leftShift(16 * $keyLength); // 2 * 8 * $keyLength
-        $max = $max->subtract($one);
+                list($type, $primeBytes, $gBytes) = Strings::unpackSSH2('Css', $response);
+                if ($type != NET_SSH2_MSG_KEXDH_GEX_GROUP) {
+                    $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR);
+                    throw new \UnexpectedValueException('Expected SSH_MSG_KEX_DH_GEX_GROUP');
+                }
+                $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_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
+                );
 
-        $x = $one->random($one, $max);
-        $e = $g->modPow($x, $prime);
+                $params = DH::createParameters($prime, $g);
+                $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_GEX_INIT';
+                $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_GEX_REPLY';
+            } else {
+                $params = DH::createParameters($this->kex_algorithm);
+                $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_INIT';
+                $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_REPLY';
+            }
 
-        $eBytes = $e->toBytes(true);
-        $data = pack('CNa*', NET_SSH2_MSG_KEXDH_INIT, strlen($eBytes), $eBytes);
+            $keyLength = min($kexHash->getLengthInBytes(), max($encryptKeyLength, $decryptKeyLength));
 
-        if (!$this->_send_binary_packet($data)) {
-            user_error('Connection closed by server');
-            return false;
+            $ourPrivate = DH::createKey($params, 16 * $keyLength); // 2 * 8 * $keyLength
+            $ourPublic = $ourPrivate->getPublicKey()->toBigInteger();
+            $ourPublicBytes = $ourPublic->toBytes(true);
         }
 
-        $response = $this->_get_binary_packet();
-        if ($response === false) {
-            user_error('Connection closed by server');
-            return false;
-        }
-        extract(unpack('Ctype', $this->_string_shift($response, 1)));
+        $data = pack('CNa*', constant($clientKexInitMessage), strlen($ourPublicBytes), $ourPublicBytes);
 
-        if ($type != NET_SSH2_MSG_KEXDH_REPLY) {
-            user_error('Expected SSH_MSG_KEXDH_REPLY');
-            return false;
-        }
+        $this->send_binary_packet($data);
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->server_public_host_key = $server_public_host_key = $this->_string_shift($response, $temp['length']);
+        switch ($clientKexInitMessage) {
+            case 'NET_SSH2_MSG_KEX_ECDH_INIT':
+                $this->updateLogHistory('NET_SSH2_MSG_KEXDH_INIT', 'NET_SSH2_MSG_KEX_ECDH_INIT');
+                break;
+            case 'NET_SSH2_MSG_KEXDH_GEX_INIT':
+                $this->updateLogHistory('UNKNOWN (32)', 'NET_SSH2_MSG_KEXDH_GEX_INIT');
+        }
 
-        $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4));
-        $public_key_format = $this->_string_shift($server_public_host_key, $temp['length']);
+        $response = $this->get_binary_packet();
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $fBytes = $this->_string_shift($response, $temp['length']);
-        $f = new Math_BigInteger($fBytes, -256);
+        list(
+            $type,
+            $server_public_host_key,
+            $theirPublicBytes,
+            $this->signature
+        ) = Strings::unpackSSH2('Csss', $response);
 
-        $temp = unpack('Nlength', $this->_string_shift($response, 4));
-        $this->signature = $this->_string_shift($response, $temp['length']);
+        if ($type != constant($serverKexReplyMessage)) {
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR);
+            throw new \UnexpectedValueException("Expected $serverKexReplyMessage");
+        }
+        switch ($serverKexReplyMessage) {
+            case 'NET_SSH2_MSG_KEX_ECDH_REPLY':
+                $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEX_ECDH_REPLY');
+                break;
+            case 'NET_SSH2_MSG_KEXDH_GEX_REPLY':
+                $this->updateLogHistory('UNKNOWN (33)', 'NET_SSH2_MSG_KEXDH_GEX_REPLY');
+        }
 
-        $temp = unpack('Nlength', $this->_string_shift($this->signature, 4));
-        $this->signature_format = $this->_string_shift($this->signature, $temp['length']);
+        $this->server_public_host_key = $server_public_host_key;
+        list($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']);
 
-        $key = $f->modPow($x, $prime);
-        $keyBytes = $key->toBytes(true);
+        $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 = pack('Na*Na*Na*Na*Na*Na*Na*Na*',
-            strlen($this->identifier), $this->identifier, strlen($this->server_identifier), $this->server_identifier,
-            strlen($kexinit_payload_client), $kexinit_payload_client, strlen($kexinit_payload_server),
-            $kexinit_payload_server, strlen($this->server_public_host_key), $this->server_public_host_key, strlen($eBytes),
-            $eBytes, strlen($fBytes), $fBytes, strlen($keyBytes), $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);
@@ -1456,227 +1865,136 @@ class Net_SSH2
             $this->session_id = $this->exchange_hash;
         }
 
-        for ($i = 0; $i < count($server_host_key_algorithms) && !in_array($server_host_key_algorithms[$i], $this->server_host_key_algorithms); $i++);
-        if ($i == count($server_host_key_algorithms)) {
-            user_error('No compatible server host key algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+        switch ($server_host_key_algorithm) {
+            case 'rsa-sha2-256':
+            case 'rsa-sha2-512':
+            //case 'ssh-rsa':
+                $expected_key_format = 'ssh-rsa';
+                break;
+            default:
+                $expected_key_format = $server_host_key_algorithm;
         }
-
-        if ($public_key_format != $server_host_key_algorithms[$i] || $this->signature_format != $server_host_key_algorithms[$i]) {
-            user_error('Server Host Key Algorithm Mismatch');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+        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(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
+                    throw new \RuntimeException('Server Host Key Algorithm Mismatch (' . $this->signature_format . ' vs ' . $server_host_key_algorithm . ')');
+            }
         }
 
-        $packet = pack('C',
-            NET_SSH2_MSG_NEWKEYS
-        );
-
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
-        }
+        $packet = pack('C', NET_SSH2_MSG_NEWKEYS);
+        $this->send_binary_packet($packet);
 
-        $response = $this->_get_binary_packet();
+        $response = $this->get_binary_packet();
 
         if ($response === false) {
-            user_error('Connection closed by server');
-            return false;
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
+            throw new ConnectionClosedException('Connection closed by server');
         }
 
-        extract(unpack('Ctype', $this->_string_shift($response, 1)));
-
+        list($type) = Strings::unpackSSH2('C', $response);
         if ($type != NET_SSH2_MSG_NEWKEYS) {
-            user_error('Expected SSH_MSG_NEWKEYS');
-            return false;
-        }
-
-        switch ($encrypt) {
-            case '3des-cbc':
-                if (!class_exists('Crypt_TripleDES')) {
-                    include_once 'Crypt/TripleDES.php';
-                }
-                $this->encrypt = new Crypt_TripleDES();
-                // $this->encrypt_block_size = 64 / 8 == the default
-                break;
-            case '3des-ctr':
-                if (!class_exists('Crypt_TripleDES')) {
-                    include_once 'Crypt/TripleDES.php';
-                }
-                $this->encrypt = new Crypt_TripleDES(CRYPT_DES_MODE_CTR);
-                // $this->encrypt_block_size = 64 / 8 == the default
-                break;
-            case 'aes256-cbc':
-            case 'aes192-cbc':
-            case 'aes128-cbc':
-                if (!class_exists('Crypt_Rijndael')) {
-                    include_once 'Crypt/Rijndael.php';
-                }
-                $this->encrypt = new Crypt_Rijndael();
-                $this->encrypt_block_size = 16; // eg. 128 / 8
-                break;
-            case 'aes256-ctr':
-            case 'aes192-ctr':
-            case 'aes128-ctr':
-                if (!class_exists('Crypt_Rijndael')) {
-                    include_once 'Crypt/Rijndael.php';
-                }
-                $this->encrypt = new Crypt_Rijndael(CRYPT_RIJNDAEL_MODE_CTR);
-                $this->encrypt_block_size = 16; // eg. 128 / 8
-                break;
-            case 'blowfish-cbc':
-                if (!class_exists('Crypt_Blowfish')) {
-                    include_once 'Crypt/Blowfish.php';
-                }
-                $this->encrypt = new Crypt_Blowfish();
-                $this->encrypt_block_size = 8;
-                break;
-            case 'blowfish-ctr':
-                if (!class_exists('Crypt_Blowfish')) {
-                    include_once 'Crypt/Blowfish.php';
-                }
-                $this->encrypt = new Crypt_Blowfish(CRYPT_BLOWFISH_MODE_CTR);
-                $this->encrypt_block_size = 8;
-                break;
-            case 'twofish128-cbc':
-            case 'twofish192-cbc':
-            case 'twofish256-cbc':
-            case 'twofish-cbc':
-                if (!class_exists('Crypt_Twofish')) {
-                    include_once 'Crypt/Twofish.php';
-                }
-                $this->encrypt = new Crypt_Twofish();
-                $this->encrypt_block_size = 16;
-                break;
-            case 'twofish128-ctr':
-            case 'twofish192-ctr':
-            case 'twofish256-ctr':
-                if (!class_exists('Crypt_Twofish')) {
-                    include_once 'Crypt/Twofish.php';
-                }
-                $this->encrypt = new Crypt_Twofish(CRYPT_TWOFISH_MODE_CTR);
-                $this->encrypt_block_size = 16;
-                break;
-            case 'arcfour':
-            case 'arcfour128':
-            case 'arcfour256':
-                if (!class_exists('Crypt_RC4')) {
-                    include_once 'Crypt/RC4.php';
-                }
-                $this->encrypt = new Crypt_RC4();
-                break;
-            case 'none';
-                //$this->encrypt = new Crypt_Null();
+            $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR);
+            throw new \UnexpectedValueException('Expected SSH_MSG_NEWKEYS');
         }
 
-        switch ($decrypt) {
-            case '3des-cbc':
-                if (!class_exists('Crypt_TripleDES')) {
-                    include_once 'Crypt/TripleDES.php';
-                }
-                $this->decrypt = new Crypt_TripleDES();
-                break;
-            case '3des-ctr':
-                if (!class_exists('Crypt_TripleDES')) {
-                    include_once 'Crypt/TripleDES.php';
-                }
-                $this->decrypt = new Crypt_TripleDES(CRYPT_DES_MODE_CTR);
-                break;
-            case 'aes256-cbc':
-            case 'aes192-cbc':
-            case 'aes128-cbc':
-                if (!class_exists('Crypt_Rijndael')) {
-                    include_once 'Crypt/Rijndael.php';
-                }
-                $this->decrypt = new Crypt_Rijndael();
-                $this->decrypt_block_size = 16;
-                break;
-            case 'aes256-ctr':
-            case 'aes192-ctr':
-            case 'aes128-ctr':
-                if (!class_exists('Crypt_Rijndael')) {
-                    include_once 'Crypt/Rijndael.php';
-                }
-                $this->decrypt = new Crypt_Rijndael(CRYPT_RIJNDAEL_MODE_CTR);
-                $this->decrypt_block_size = 16;
-                break;
-            case 'blowfish-cbc':
-                if (!class_exists('Crypt_Blowfish')) {
-                    include_once 'Crypt/Blowfish.php';
-                }
-                $this->decrypt = new Crypt_Blowfish();
-                $this->decrypt_block_size = 8;
-                break;
-            case 'blowfish-ctr':
-                if (!class_exists('Crypt_Blowfish')) {
-                    include_once 'Crypt/Blowfish.php';
-                }
-                $this->decrypt = new Crypt_Blowfish(CRYPT_BLOWFISH_MODE_CTR);
-                $this->decrypt_block_size = 8;
-                break;
-            case 'twofish128-cbc':
-            case 'twofish192-cbc':
-            case 'twofish256-cbc':
-            case 'twofish-cbc':
-                if (!class_exists('Crypt_Twofish')) {
-                    include_once 'Crypt/Twofish.php';
-                }
-                $this->decrypt = new Crypt_Twofish();
-                $this->decrypt_block_size = 16;
-                break;
-            case 'twofish128-ctr':
-            case 'twofish192-ctr':
-            case 'twofish256-ctr':
-                if (!class_exists('Crypt_Twofish')) {
-                    include_once 'Crypt/Twofish.php';
-                }
-                $this->decrypt = new Crypt_Twofish(CRYPT_TWOFISH_MODE_CTR);
-                $this->decrypt_block_size = 16;
-                break;
-            case 'arcfour':
-            case 'arcfour128':
-            case 'arcfour256':
-                if (!class_exists('Crypt_RC4')) {
-                    include_once 'Crypt/RC4.php';
-                }
-                $this->decrypt = new Crypt_RC4();
-                break;
-            case 'none';
-                //$this->decrypt = new Crypt_Null();
+        if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) {
+            $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) {
-            $this->encrypt->enableContinuousBuffer();
+            if (self::$crypto_engine) {
+                $this->encrypt->setPreferredEngine(self::$crypto_engine);
+            }
+            if ($this->encrypt->getBlockLengthInBytes()) {
+                $this->encrypt_block_size = $this->encrypt->getBlockLengthInBytes();
+            }
             $this->encrypt->disablePadding();
 
-            $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);
+            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);
+                    // fall-through
+                case 'chacha20-poly1305@openssh.com':
+                    break;
+                default:
+                    $this->encrypt->enableContinuousBuffer();
             }
-            $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size));
 
             $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id);
             while ($encryptKeyLength > strlen($key)) {
-                $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $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) {
-            $this->decrypt->enableContinuousBuffer();
+            if (self::$crypto_engine) {
+                $this->decrypt->setPreferredEngine(self::$crypto_engine);
+            }
+            if ($this->decrypt->getBlockLengthInBytes()) {
+                $this->decrypt_block_size = $this->decrypt->getBlockLengthInBytes();
+            }
             $this->decrypt->disablePadding();
 
-            $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);
+            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);
+                    // fall-through
+                case 'chacha20-poly1305@openssh.com':
+                    break;
+                default:
+                    $this->decrypt->enableContinuousBuffer();
             }
-            $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size));
 
             $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id);
             while ($decryptKeyLength > strlen($key)) {
-                $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $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
@@ -1693,143 +2011,316 @@ class Net_SSH2
             $this->decrypt->decrypt(str_repeat("\0", 1536));
         }
 
-        for ($i = 0; $i < count($mac_algorithms) && !in_array($mac_algorithms[$i], $this->mac_algorithms_client_to_server); $i++);
-        if ($i == count($mac_algorithms)) {
-            user_error('No compatible client to server message authentication algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+        if (!$this->encrypt->usesNonce()) {
+            list($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;
         }
 
-        $createKeyLength = 0; // ie. $mac_algorithms[$i] == 'none'
-        switch ($mac_algorithms[$i]) {
-            case 'hmac-sha2-256':
-                $this->hmac_create = new Crypt_Hash('sha256');
-                $createKeyLength = 32;
-                break;
-            case 'hmac-sha1':
-                $this->hmac_create = new Crypt_Hash('sha1');
-                $createKeyLength = 20;
-                break;
-            case 'hmac-sha1-96':
-                $this->hmac_create = new Crypt_Hash('sha1-96');
-                $createKeyLength = 20;
-                break;
-            case 'hmac-md5':
-                $this->hmac_create = new Crypt_Hash('md5');
-                $createKeyLength = 16;
-                break;
-            case 'hmac-md5-96':
-                $this->hmac_create = new Crypt_Hash('md5-96');
-                $createKeyLength = 16;
+        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 = preg_match('#-etm@openssh\.com$#', $mac_algorithm_out);
         }
 
-        for ($i = 0; $i < count($mac_algorithms) && !in_array($mac_algorithms[$i], $this->mac_algorithms_server_to_client); $i++);
-        if ($i == count($mac_algorithms)) {
-            user_error('No compatible server to client message authentication algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+        if (!$this->decrypt->usesNonce()) {
+            list($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;
         }
 
-        $checkKeyLength = 0;
-        $this->hmac_size = 0;
-        switch ($mac_algorithms[$i]) {
-            case 'hmac-sha2-256':
-                $this->hmac_check = new Crypt_Hash('sha256');
-                $checkKeyLength = 32;
-                $this->hmac_size = 32;
-                break;
-            case 'hmac-sha1':
-                $this->hmac_check = new Crypt_Hash('sha1');
-                $checkKeyLength = 20;
-                $this->hmac_size = 20;
-                break;
-            case 'hmac-sha1-96':
-                $this->hmac_check = new Crypt_Hash('sha1-96');
-                $checkKeyLength = 20;
-                $this->hmac_size = 12;
-                break;
-            case 'hmac-md5':
-                $this->hmac_check = new Crypt_Hash('md5');
-                $checkKeyLength = 16;
-                $this->hmac_size = 16;
-                break;
-            case 'hmac-md5-96':
-                $this->hmac_check = new Crypt_Hash('md5-96');
-                $checkKeyLength = 16;
-                $this->hmac_size = 12;
+        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 = preg_match('#-etm@openssh\.com$#', $mac_algorithm_in);
         }
 
-        $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'E' . $this->session_id);
-        while ($createKeyLength > strlen($key)) {
-            $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key);
+        $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($algorithm)
+    {
+        if ($this->bad_key_size_fix && self::bad_algorithm_candidate($algorithm)) {
+            return 16;
         }
-        $this->hmac_create->setKey(substr($key, 0, $createKeyLength));
 
-        $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'F' . $this->session_id);
-        while ($checkKeyLength > strlen($key)) {
-            $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key);
+        switch ($algorithm) {
+            case 'none':
+                return 0;
+            case 'aes128-gcm@openssh.com':
+            case 'aes128-cbc':
+            case 'aes128-ctr':
+            case 'arcfour':
+            case 'arcfour128':
+            case 'blowfish-cbc':
+            case 'blowfish-ctr':
+            case 'twofish128-cbc':
+            case 'twofish128-ctr':
+                return 16;
+            case '3des-cbc':
+            case '3des-ctr':
+            case 'aes192-cbc':
+            case 'aes192-ctr':
+            case 'twofish192-cbc':
+            case 'twofish192-ctr':
+                return 24;
+            case 'aes256-gcm@openssh.com':
+            case 'aes256-cbc':
+            case 'aes256-ctr':
+            case 'arcfour256':
+            case 'twofish-cbc':
+            case 'twofish256-cbc':
+            case 'twofish256-ctr':
+                return 32;
+            case 'chacha20-poly1305@openssh.com':
+                return 64;
         }
-        $this->hmac_check->setKey(substr($key, 0, $checkKeyLength));
+        return null;
+    }
+
+    /**
+     * Maps an encryption algorithm name to an instance of a subclass of
+     * \phpseclib3\Crypt\Common\SymmetricKey.
+     *
+     * @param string $algorithm Name of the encryption algorithm
+     * @return SymmetricKey|null
+     */
+    private static function encryption_algorithm_to_crypt_instance($algorithm)
+    {
+        switch ($algorithm) {
+            case '3des-cbc':
+                return new TripleDES('cbc');
+            case '3des-ctr':
+                return new TripleDES('ctr');
+            case 'aes256-cbc':
+            case 'aes192-cbc':
+            case 'aes128-cbc':
+                return new Rijndael('cbc');
+            case 'aes256-ctr':
+            case 'aes192-ctr':
+            case 'aes128-ctr':
+                return new Rijndael('ctr');
+            case 'blowfish-cbc':
+                return new Blowfish('cbc');
+            case 'blowfish-ctr':
+                return new Blowfish('ctr');
+            case 'twofish128-cbc':
+            case 'twofish192-cbc':
+            case 'twofish256-cbc':
+            case 'twofish-cbc':
+                return new Twofish('cbc');
+            case 'twofish128-ctr':
+            case 'twofish192-ctr':
+            case 'twofish256-ctr':
+                return new Twofish('ctr');
+            case 'arcfour':
+            case 'arcfour128':
+            case 'arcfour256':
+                return new RC4();
+            case 'aes128-gcm@openssh.com':
+            case 'aes256-gcm@openssh.com':
+                return new Rijndael('gcm');
+            case 'chacha20-poly1305@openssh.com':
+                return new ChaCha20();
+        }
+        return null;
+    }
 
-        for ($i = 0; $i < count($compression_algorithms) && !in_array($compression_algorithms[$i], $this->compression_algorithms_server_to_client); $i++);
-        if ($i == count($compression_algorithms)) {
-            user_error('No compatible server to client compression algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+    /**
+     * Maps an encryption algorithm name to an instance of a subclass of
+     * \phpseclib3\Crypt\Hash.
+     *
+     * @param string $algorithm Name of the encryption algorithm
+     * @return array{Hash, int}|null
+     */
+    private static function mac_algorithm_to_hash_instance($algorithm)
+    {
+        switch ($algorithm) {
+            case 'umac-64@openssh.com':
+            case 'umac-64-etm@openssh.com':
+                return [new Hash('umac-64'), 16];
+            case 'umac-128@openssh.com':
+            case 'umac-128-etm@openssh.com':
+                return [new Hash('umac-128'), 16];
+            case 'hmac-sha2-512':
+            case 'hmac-sha2-512-etm@openssh.com':
+                return [new Hash('sha512'), 64];
+            case 'hmac-sha2-256':
+            case 'hmac-sha2-256-etm@openssh.com':
+                return [new Hash('sha256'), 32];
+            case 'hmac-sha1':
+            case 'hmac-sha1-etm@openssh.com':
+                return [new Hash('sha1'), 20];
+            case 'hmac-sha1-96':
+                return [new Hash('sha1-96'), 20];
+            case 'hmac-md5':
+                return [new Hash('md5'), 16];
+            case 'hmac-md5-96':
+                return [new Hash('md5-96'), 16];
         }
-        $this->decompress = $compression_algorithms[$i] == 'zlib';
+    }
 
-        for ($i = 0; $i < count($compression_algorithms) && !in_array($compression_algorithms[$i], $this->compression_algorithms_client_to_server); $i++);
-        if ($i == count($compression_algorithms)) {
-            user_error('No compatible client to server compression algorithms found');
-            return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+    /*
+     * 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
+     * @return bool
+     */
+    private static function bad_algorithm_candidate($algorithm)
+    {
+        switch ($algorithm) {
+            case 'arcfour256':
+            case 'aes192-ctr':
+            case 'aes256-ctr':
+                return true;
         }
-        $this->compress = $compression_algorithms[$i] == 'zlib';
 
-        return true;
+        return false;
     }
 
     /**
      * Login
      *
-     * The $password parameter can be a plaintext password, a Crypt_RSA object or an array
+     * The $password parameter can be a plaintext password, a \phpseclib3\Crypt\RSA|EC|DSA object, a \phpseclib3\System\SSH\Agent object or an array
      *
-     * @param String $username
-     * @param Mixed $password
-     * @param Mixed $...
-     * @return Boolean
-     * @see _login
-     * @access public
+     * @param string $username
+     * @param string|PrivateKey|array[]|Agent|null ...$args
+     * @return bool
+     * @see self::_login()
      */
-    function login($username)
+    public function login($username, ...$args)
     {
-        $args = func_get_args();
-        return call_user_func_array(array(&$this, '_login'), $args);
+        if (!$this->retry_connect) {
+            $this->auth[] = func_get_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
      *
-     * @param String $username
-     * @param Mixed $password
-     * @param Mixed $...
-     * @return Boolean
-     * @see _login_helper
-     * @access private
+     * @param string $username
+     * @param string|PrivateKey|array[]|Agent|null ...$args
+     * @return bool
+     * @see self::_login_helper()
      */
-    function _login($username)
+    protected function sublogin($username, ...$args)
     {
-        if (!($this->bitmap & NET_SSH2_MASK_CONSTRUCTOR)) {
-            if (!$this->_connect()) {
-                return false;
-            }
+        if (!($this->bitmap & self::MASK_CONSTRUCTOR)) {
+            $this->connect();
         }
 
-        $args = array_slice(func_get_args(), 1);
         if (empty($args)) {
-            return $this->_login_helper($username);
+            return $this->login_helper($username);
         }
 
         foreach ($args as $arg) {
-            if ($this->_login_helper($username, $arg)) {
-                return true;
+            switch (true) {
+                case $arg instanceof PublicKey:
+                    throw new \UnexpectedValueException('A PublicKey object was passed to the login method instead of a PrivateKey object');
+                case $arg instanceof PrivateKey:
+                case $arg instanceof Agent:
+                case is_array($arg):
+                case Strings::is_stringable($arg):
+                    break;
+                default:
+                    throw new \UnexpectedValueException('$password needs to either be an instance of \phpseclib3\Crypt\Common\PrivateKey, \System\SSH\Agent, an array or a string');
+            }
+        }
+
+        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 || Strings::is_stringable($arg)) {
+                                    $hasString = true;
+                                    break;
+                                }
+                            }
+                            if ($hasArray && $hasString) {
+                                foreach ($args as $key => $arg) {
+                                    if (is_array($arg)) {
+                                        $newargs[] = $arg;
+                                        break 2;
+                                    }
+                                }
+                            }
+                            // fall-through
+                        case 'password':
+                            foreach ($args as $key => $arg) {
+                                $newargs[] = $arg;
+                                unset($args[$key]);
+                                break;
+                            }
+                    }
+                }
+            }
+
+            if (!count($newargs)) {
+                return false;
+            }
+
+            foreach ($newargs as $arg) {
+                if ($this->login_helper($username, $arg)) {
+                    return true;
+                }
             }
         }
         return false;
@@ -1838,146 +2329,163 @@ class Net_SSH2
     /**
      * Login Helper
      *
-     * @param String $username
-     * @param optional String $password
-     * @return Boolean
-     * @access private
-     * @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.
+     * {@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.}
+     *
+     * @param string $username
+     * @param string|AsymmetricKey|array[]|Agent|null ...$args
+     * @return bool
+     * @throws \UnexpectedValueException on receipt of unexpected packets
+     * @throws \RuntimeException on other errors
      */
-    function _login_helper($username, $password = null)
+    private function login_helper($username, $password = null)
     {
-        if (!($this->bitmap & NET_SSH2_MASK_CONNECTED)) {
+        if (!($this->bitmap & self::MASK_CONNECTED)) {
             return false;
         }
 
-        if (!($this->bitmap & NET_SSH2_MASK_LOGIN_REQ)) {
-            $packet = pack('CNa*',
-                NET_SSH2_MSG_SERVICE_REQUEST, strlen('ssh-userauth'), 'ssh-userauth'
-            );
+        if (!($this->bitmap & self::MASK_LOGIN_REQ)) {
+            $packet = Strings::packSSH2('Cs', NET_SSH2_MSG_SERVICE_REQUEST, 'ssh-userauth');
+            $this->send_binary_packet($packet);
 
-            if (!$this->_send_binary_packet($packet)) {
-                return false;
+            try {
+                $response = $this->get_binary_packet();
+            } catch (\Exception $e) {
+                if ($this->retry_connect) {
+                    $this->retry_connect = false;
+                    $this->connect();
+                    return $this->login_helper($username, $password);
+                }
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
+                throw $e;
             }
 
-            $response = $this->_get_binary_packet();
-            if ($response === false) {
-                user_error('Connection closed by server');
-                return false;
+            list($type) = Strings::unpackSSH2('C', $response);
+
+            if ($type == NET_SSH2_MSG_EXT_INFO) {
+                list($nr_extensions) = Strings::unpackSSH2('N', $response);
+                for ($i = 0; $i < $nr_extensions; $i++) {
+                    list($extension_name, $extension_value) = Strings::unpackSSH2('ss', $response);
+                    if ($extension_name == 'server-sig-algs') {
+                        $this->supported_private_key_algorithms = explode(',', $extension_value);
+                    }
+                }
+
+                $response = $this->get_binary_packet();
+                list($type) = Strings::unpackSSH2('C', $response);
             }
 
-            extract(unpack('Ctype', $this->_string_shift($response, 1)));
+            list($service) = Strings::unpackSSH2('s', $response);
 
-            if ($type != NET_SSH2_MSG_SERVICE_ACCEPT) {
-                user_error('Expected SSH_MSG_SERVICE_ACCEPT');
-                return false;
+            if ($type != NET_SSH2_MSG_SERVICE_ACCEPT || $service != 'ssh-userauth') {
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR);
+                throw new \UnexpectedValueException('Expected SSH_MSG_SERVICE_ACCEPT');
             }
-            $this->bitmap |= NET_SSH2_MASK_LOGIN_REQ;
+            $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);
+            return !Strings::is_stringable($password) && !is_array($password) ? false : $this->keyboard_interactive_process($password);
         }
 
-        // although PHP5's get_class() preserves the case, PHP4's does not
-        if (is_object($password)) {
-            switch (strtolower(get_class($password))) {
-                case 'crypt_rsa':
-                    return $this->_privatekey_login($username, $password);
-                case 'system_ssh_agent':
-                    return $this->_ssh_agent_login($username, $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 |= NET_SSH2_MASK_LOGIN;
+            if ($this->keyboard_interactive_login($username, $password)) {
+                $this->bitmap |= self::MASK_LOGIN;
                 return true;
             }
             return false;
         }
 
         if (!isset($password)) {
-            $packet = pack('CNa*Na*Na*',
-                NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection',
-                strlen('none'), 'none'
+            $packet = Strings::packSSH2(
+                'Cs3',
+                NET_SSH2_MSG_USERAUTH_REQUEST,
+                $username,
+                'ssh-connection',
+                'none'
             );
 
-            if (!$this->_send_binary_packet($packet)) {
-                return false;
-            }
-
-            $response = $this->_get_binary_packet();
-            if ($response === false) {
-                user_error('Connection closed by server');
-                return false;
-            }
+            $this->send_binary_packet($packet);
 
-            extract(unpack('Ctype', $this->_string_shift($response, 1)));
+            $response = $this->get_binary_packet();
 
+            list($type) = Strings::unpackSSH2('C', $response);
             switch ($type) {
                 case NET_SSH2_MSG_USERAUTH_SUCCESS:
-                    $this->bitmap |= NET_SSH2_MASK_LOGIN;
+                    $this->bitmap |= self::MASK_LOGIN;
                     return true;
-                //case NET_SSH2_MSG_USERAUTH_FAILURE:
+                case NET_SSH2_MSG_USERAUTH_FAILURE:
+                    list($auth_methods) = Strings::unpackSSH2('L', $response);
+                    $this->auth_methods_to_continue = $auth_methods;
+                    // fall-through
                 default:
                     return false;
             }
         }
 
-        $packet = pack('CNa*Na*Na*CNa*',
-            NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection',
-            strlen('password'), 'password', 0, strlen($password), $password
+        $packet = Strings::packSSH2(
+            'Cs3bs',
+            NET_SSH2_MSG_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 = pack('CNa*Na*Na*CNa*',
-                NET_SSH2_MSG_USERAUTH_REQUEST, strlen('username'), 'username', strlen('ssh-connection'), 'ssh-connection',
-                strlen('password'), 'password', 0, strlen('password'), 'password'
+            $logged = Strings::packSSH2(
+                'Cs3bs',
+                NET_SSH2_MSG_USERAUTH_REQUEST,
+                $username,
+                'ssh-connection',
+                'password',
+                false,
+                'password'
             );
         }
 
-        if (!$this->_send_binary_packet($packet, $logged)) {
-            return false;
-        }
+        $this->send_binary_packet($packet, $logged);
 
-        $response = $this->_get_binary_packet();
+        $response = $this->get_binary_packet();
         if ($response === false) {
-            user_error('Connection closed by server');
             return false;
         }
-
-        extract(unpack('Ctype', $this->_string_shift($response, 1)));
-
+        list($type) = Strings::unpackSSH2('C', $response);
         switch ($type) {
             case NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed
-                if (defined('NET_SSH2_LOGGING')) {
-                    $this->message_number_log[count($this->message_number_log) - 1] = 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ';
-                }
-                extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . utf8_decode($this->_string_shift($response, $length));
-                return $this->_disconnect(NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER);
+                $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ');
+
+                list($message) = Strings::unpackSSH2('s', $response);
+                $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . $message;
+
+                return $this->disconnect_helper(NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER);
             case NET_SSH2_MSG_USERAUTH_FAILURE:
                 // can we use keyboard-interactive authentication?  if not then either the login is bad or the server employees
                 // multi-factor authentication
-                extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                $auth_methods = explode(',', $this->_string_shift($response, $length));
-                extract(unpack('Cpartial_success', $this->_string_shift($response, 1)));
-                $partial_success = $partial_success != 0;
-
+                list($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 |= NET_SSH2_MASK_LOGIN;
+                    if ($this->keyboard_interactive_login($username, $password)) {
+                        $this->bitmap |= self::MASK_LOGIN;
                         return true;
                     }
                     return false;
                 }
                 return false;
             case NET_SSH2_MSG_USERAUTH_SUCCESS:
-                $this->bitmap |= NET_SSH2_MASK_LOGIN;
+                $this->bitmap |= self::MASK_LOGIN;
                 return true;
         }
 
@@ -1989,57 +2497,50 @@ class Net_SSH2
      *
      * See {@link http://tools.ietf.org/html/rfc4256 RFC4256} for details.  This is not a full-featured keyboard-interactive authenticator.
      *
-     * @param String $username
-     * @param String $password
-     * @return Boolean
-     * @access private
+     * @param string $username
+     * @param string|array $password
+     * @return bool
      */
-    function _keyboard_interactive_login($username, $password)
+    private function keyboard_interactive_login($username, $password)
     {
-        $packet = pack('CNa*Na*Na*Na*Na*',
-            NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection',
-            strlen('keyboard-interactive'), 'keyboard-interactive', 0, '', 0, ''
+        $packet = Strings::packSSH2(
+            'Cs5',
+            NET_SSH2_MSG_USERAUTH_REQUEST,
+            $username,
+            'ssh-connection',
+            'keyboard-interactive',
+            '', // language tag
+            '' // submethods
         );
+        $this->send_binary_packet($packet);
 
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
-        }
-
-        return $this->_keyboard_interactive_process($password);
+        return $this->keyboard_interactive_process($password);
     }
 
     /**
      * Handle the keyboard-interactive requests / responses.
      *
-     * @param String $responses...
-     * @return Boolean
-     * @access private
+     * @param string|array ...$responses
+     * @return bool
+     * @throws \RuntimeException on connection error
      */
-    function _keyboard_interactive_process()
+    private function keyboard_interactive_process(...$responses)
     {
-        $responses = func_get_args();
-
         if (strlen($this->last_interactive_response)) {
             $response = $this->last_interactive_response;
         } else {
-            $orig = $response = $this->_get_binary_packet();
-            if ($response === false) {
-                user_error('Connection closed by server');
-                return false;
-            }
+            $orig = $response = $this->get_binary_packet();
         }
 
-        extract(unpack('Ctype', $this->_string_shift($response, 1)));
-
+        list($type) = Strings::unpackSSH2('C', $response);
         switch ($type) {
             case NET_SSH2_MSG_USERAUTH_INFO_REQUEST:
-                extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                $this->_string_shift($response, $length); // name; may be empty
-                extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                $this->_string_shift($response, $length); // instruction; may be empty
-                extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                $this->_string_shift($response, $length); // language tag; may be empty
-                extract(unpack('Nnum_prompts', $this->_string_shift($response, 4)));
+                list(
+                    , // 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])) {
@@ -2053,10 +2554,10 @@ class Net_SSH2
 
                 if (isset($this->keyboard_requests_responses)) {
                     for ($i = 0; $i < $num_prompts; $i++) {
-                        extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                        // prompt - ie. "Password: "; must not be empty
-                        $prompt = $this->_string_shift($response, $length);
-                        //$echo = $this->_string_shift($response) != chr(0);
+                        list(
+                            $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;
@@ -2069,12 +2570,8 @@ class Net_SSH2
                 // see http://tools.ietf.org/html/rfc4256#section-3.2
                 if (strlen($this->last_interactive_response)) {
                     $this->last_interactive_response = '';
-                } else if (defined('NET_SSH2_LOGGING')) {
-                    $this->message_number_log[count($this->message_number_log) - 1] = str_replace(
-                        'UNKNOWN',
-                        'NET_SSH2_MSG_USERAUTH_INFO_REQUEST',
-                        $this->message_number_log[count($this->message_number_log) - 1]
-                    );
+                } else {
+                    $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST');
                 }
 
                 if (!count($responses) && $num_prompts) {
@@ -2089,21 +2586,13 @@ class Net_SSH2
                 // see http://tools.ietf.org/html/rfc4256#section-3.4
                 $packet = $logged = pack('CN', NET_SSH2_MSG_USERAUTH_INFO_RESPONSE, count($responses));
                 for ($i = 0; $i < count($responses); $i++) {
-                    $packet.= pack('Na*', strlen($responses[$i]), $responses[$i]);
-                    $logged.= pack('Na*', strlen('dummy-answer'), 'dummy-answer');
+                    $packet .= Strings::packSSH2('s', $responses[$i]);
+                    $logged .= Strings::packSSH2('s', 'dummy-answer');
                 }
 
-                if (!$this->_send_binary_packet($packet, $logged)) {
-                    return false;
-                }
+                $this->send_binary_packet($packet, $logged);
 
-                if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == NET_SSH2_LOG_COMPLEX) {
-                    $this->message_number_log[count($this->message_number_log) - 1] = str_replace(
-                        'UNKNOWN',
-                        'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE',
-                        $this->message_number_log[count($this->message_number_log) - 1]
-                    );
-                }
+                $this->updateLogHistory('UNKNOWN (61)', 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE');
 
                 /*
                    After receiving the response, the server MUST send either an
@@ -2112,10 +2601,12 @@ class Net_SSH2
                 */
                 // 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();
+                return $this->keyboard_interactive_process();
             case NET_SSH2_MSG_USERAUTH_SUCCESS:
                 return true;
             case NET_SSH2_MSG_USERAUTH_FAILURE:
+                list($auth_methods) = Strings::unpackSSH2('L', $response);
+                $this->auth_methods_to_continue = $auth_methods;
                 return false;
         }
 
@@ -2125,16 +2616,16 @@ class Net_SSH2
     /**
      * Login with an ssh-agent provided key
      *
-     * @param String $username
-     * @param System_SSH_Agent $agent
-     * @return Boolean
-     * @access private
+     * @param string $username
+     * @param \phpseclib3\System\SSH\Agent $agent
+     * @return bool
      */
-    function _ssh_agent_login($username, $agent)
+    private function ssh_agent_login($username, Agent $agent)
     {
+        $this->agent = $agent;
         $keys = $agent->requestIdentities();
         foreach ($keys as $key) {
-            if ($this->_privatekey_login($username, $key)) {
+            if ($this->privatekey_login($username, $key)) {
                 return true;
             }
         }
@@ -2145,93 +2636,149 @@ class Net_SSH2
     /**
      * Login with an RSA private key
      *
-     * @param String $username
-     * @param Crypt_RSA $password
-     * @return Boolean
-     * @access private
-     * @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.
+     * {@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.}
+     *
+     * @param string $username
+     * @param \phpseclib3\Crypt\Common\PrivateKey $privatekey
+     * @return bool
+     * @throws \RuntimeException on connection error
      */
-    function _privatekey_login($username, $privatekey)
+    private function privatekey_login($username, PrivateKey $privatekey)
     {
-        // see http://tools.ietf.org/html/rfc4253#page-15
-        $publickey = $privatekey->getPublicKey(CRYPT_RSA_PUBLIC_FORMAT_RAW);
-        if ($publickey === false) {
-            return false;
+        $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 phpseclib3\'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');
         }
 
-        $publickey = array(
-            'e' => $publickey['e']->toBytes(true),
-            'n' => $publickey['n']->toBytes(true)
-        );
-        $publickey = pack('Na*Na*Na*',
-            strlen('ssh-rsa'), 'ssh-rsa', strlen($publickey['e']), $publickey['e'], strlen($publickey['n']), $publickey['n']
-        );
+        $publickeyStr = $publickey->toString('OpenSSH', ['binary' => true]);
 
-        $part1 = pack('CNa*Na*Na*',
-            NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection',
-            strlen('publickey'), 'publickey'
+        $part1 = Strings::packSSH2(
+            'Csss',
+            NET_SSH2_MSG_USERAUTH_REQUEST,
+            $username,
+            'ssh-connection',
+            'publickey'
         );
-        $part2 = pack('Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($publickey), $publickey);
+        $part2 = Strings::packSSH2('ss', $signatureType, $publickeyStr);
 
         $packet = $part1 . chr(0) . $part2;
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
-        }
-
-        $response = $this->_get_binary_packet();
-        if ($response === false) {
-            user_error('Connection closed by server');
-            return false;
-        }
+        $this->send_binary_packet($packet);
 
-        extract(unpack('Ctype', $this->_string_shift($response, 1)));
+        $response = $this->get_binary_packet();
 
+        list($type) = Strings::unpackSSH2('C', $response);
         switch ($type) {
             case NET_SSH2_MSG_USERAUTH_FAILURE:
-                extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                $this->errors[] = 'SSH_MSG_USERAUTH_FAILURE: ' . $this->_string_shift($response, $length);
+                list($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 NET_SSH2_MSG_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
-                if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == NET_SSH2_LOG_COMPLEX) {
-                    $this->message_number_log[count($this->message_number_log) - 1] = str_replace(
-                        'UNKNOWN',
-                        'NET_SSH2_MSG_USERAUTH_PK_OK',
-                        $this->message_number_log[count($this->message_number_log) - 1]
-                    );
-                }
+                $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PK_OK');
+                break;
+            case NET_SSH2_MSG_USERAUTH_SUCCESS:
+                $this->bitmap |= self::MASK_LOGIN;
+                return true;
+            default:
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                throw new ConnectionClosedException('Unexpected response to publickey authentication pt 1');
         }
 
         $packet = $part1 . chr(1) . $part2;
-        $privatekey->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
-        $signature = $privatekey->sign(pack('Na*a*', strlen($this->session_id), $this->session_id, $packet));
-        $signature = pack('Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($signature), $signature);
-        $packet.= pack('Na*', strlen($signature), $signature);
-
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
+        $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);
 
-        $response = $this->_get_binary_packet();
-        if ($response === false) {
-            user_error('Connection closed by server');
-            return false;
-        }
+        $this->send_binary_packet($packet);
 
-        extract(unpack('Ctype', $this->_string_shift($response, 1)));
+        $response = $this->get_binary_packet();
 
+        list($type) = Strings::unpackSSH2('C', $response);
         switch ($type) {
             case NET_SSH2_MSG_USERAUTH_FAILURE:
                 // either the login is bad or the server employs multi-factor authentication
+                list($auth_methods) = Strings::unpackSSH2('L', $response);
+                $this->auth_methods_to_continue = $auth_methods;
                 return false;
             case NET_SSH2_MSG_USERAUTH_SUCCESS:
-                $this->bitmap |= NET_SSH2_MASK_LOGIN;
+                $this->bitmap |= self::MASK_LOGIN;
                 return true;
         }
 
-        return false;
+        $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+        throw new ConnectionClosedException('Unexpected response to publickey authentication pt 2');
+    }
+
+    /**
+     * Return the currently configured timeout
+     *
+     * @return int
+     */
+    public function getTimeout()
+    {
+        return $this->timeout;
     }
 
     /**
@@ -2240,20 +2787,30 @@ class Net_SSH2
      * $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 mean there is no timeout.
      *
-     * @param Mixed $timeout
-     * @access public
+     * @param mixed $timeout
      */
-    function setTimeout($timeout)
+    public function setTimeout($timeout)
     {
         $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.
+     *
+     * @param int $interval
+     */
+    public function setKeepAlive($interval)
+    {
+        $this->keepAlive = $interval;
+    }
+
     /**
      * Get the output from stdError
      *
-     * @access public
      */
-    function getStdError()
+    public function getStdError()
     {
         return $this->stdErrorLog;
     }
@@ -2261,77 +2818,57 @@ class Net_SSH2
     /**
      * Execute Command
      *
-     * If $callback is set to false then Net_SSH2::_get_channel_packet(NET_SSH2_CHANNEL_EXEC) will need to be called manually.
+     * If $callback is set to false then \phpseclib3\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.
      *
-     * @param String $command
-     * @param optional Callback $callback
-     * @return String
-     * @access public
+     * @param string $command
+     * @return string|bool
+     * @psalm-return ($callback is callable ? bool : string|bool)
+     * @throws \RuntimeException on connection error
      */
-    function exec($command, $callback = null)
+    public function exec($command, callable $callback = null)
     {
         $this->curTimeout = $this->timeout;
         $this->is_timeout = false;
         $this->stdErrorLog = '';
 
-        if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
-            return false;
-        }
-
-        // 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 transfering 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[NET_SSH2_CHANNEL_EXEC] = $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 = pack('CNa*N3',
-            NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_EXEC, $this->window_size_server_to_client[NET_SSH2_CHANNEL_EXEC], $packet_size);
-
-        if (!$this->_send_binary_packet($packet)) {
+        if (!$this->isAuthenticated()) {
             return false;
         }
 
-        $this->channel_status[NET_SSH2_CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_OPEN;
+        //if ($this->isPTYOpen()) {
+        //    throw new \RuntimeException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.');
+        //}
 
-        $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_EXEC);
-        if ($response === false) {
-            return false;
-        }
+        $this->open_channel(self::CHANNEL_EXEC);
 
         if ($this->request_pty === true) {
             $terminal_modes = pack('C', NET_SSH2_TTY_OP_END);
-            $packet = pack('CNNa*CNa*N5a*',
-                NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_EXEC], strlen('pty-req'), 'pty-req', 1, strlen('vt100'), 'vt100',
-                $this->windowColumns, $this->windowRows, 0, 0, strlen($terminal_modes), $terminal_modes);
-
-            if (!$this->_send_binary_packet($packet)) {
-                return false;
-            }
-            $response = $this->_get_binary_packet();
-            if ($response === false) {
-                user_error('Connection closed by server');
-                return false;
-            }
+            $packet = Strings::packSSH2(
+                'CNsCsN4s',
+                NET_SSH2_MSG_CHANNEL_REQUEST,
+                $this->server_channels[self::CHANNEL_EXEC],
+                'pty-req',
+                1,
+                $this->term,
+                $this->windowColumns,
+                $this->windowRows,
+                0,
+                0,
+                $terminal_modes
+            );
 
-            list(, $type) = unpack('C', $this->_string_shift($response, 1));
+            $this->send_binary_packet($packet);
 
-            switch ($type) {
-                case NET_SSH2_MSG_CHANNEL_SUCCESS:
-                    break;
-                case NET_SSH2_MSG_CHANNEL_FAILURE:
-                default:
-                    user_error('Unable to request pseudo-terminal');
-                    return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
+            $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST;
+            if (!$this->get_channel_packet(self::CHANNEL_EXEC)) {
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                throw new \RuntimeException('Unable to request pseudo-terminal');
             }
-            $this->in_request_pty_exec = true;
         }
 
         // 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 Net_SSH2::exec('ping localhost &').
+        // down.  the one place where it might be desirable is if you're doing something like \phpseclib3\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.
@@ -2339,28 +2876,32 @@ class Net_SSH2
         // 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 = pack('CNNa*CNa*',
-            NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_EXEC], strlen('exec'), 'exec', 1, strlen($command), $command);
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
-        }
+        $packet = Strings::packSSH2(
+            'CNsCs',
+            NET_SSH2_MSG_CHANNEL_REQUEST,
+            $this->server_channels[self::CHANNEL_EXEC],
+            'exec',
+            1,
+            $command
+        );
+        $this->send_binary_packet($packet);
 
-        $this->channel_status[NET_SSH2_CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST;
+        $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST;
 
-        $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_EXEC);
-        if ($response === false) {
+        if (!$this->get_channel_packet(self::CHANNEL_EXEC)) {
             return false;
         }
 
-        $this->channel_status[NET_SSH2_CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA;
+        $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA;
 
-        if ($callback === false || $this->in_request_pty_exec) {
+        if ($this->request_pty === true) {
+            $this->channel_id_last_interactive = self::CHANNEL_EXEC;
             return true;
         }
 
         $output = '';
         while (true) {
-            $temp = $this->_get_channel_packet(NET_SSH2_CHANNEL_EXEC);
+            $temp = $this->get_channel_packet(self::CHANNEL_EXEC);
             switch (true) {
                 case $temp === true:
                     return is_callable($callback) ? true : $output;
@@ -2368,185 +2909,322 @@ class Net_SSH2
                     return false;
                 default:
                     if (is_callable($callback)) {
-                        if (call_user_func($callback, $temp) === true) {
-                            $this->_close_channel(NET_SSH2_CHANNEL_EXEC);
+                        if ($callback($temp) === true) {
+                            $this->close_channel(self::CHANNEL_EXEC);
                             return true;
                         }
                     } else {
-                        $output.= $temp;
+                        $output .= $temp;
                     }
             }
         }
     }
 
     /**
-     * Creates an interactive shell
+     * How many channels are currently open?
      *
-     * @see Net_SSH2::read()
-     * @see Net_SSH2::write()
-     * @return Boolean
-     * @access private
+     * @return int
      */
-    function _initShell()
+    public function getOpenChannelCount()
     {
-        if ($this->in_request_pty_exec === true) {
-            return true;
+        return $this->channelCount;
+    }
+
+    /**
+     * Opens a channel
+     *
+     * @param string $channel
+     * @param bool $skip_extended
+     * @return bool
+     */
+    protected function open_channel($channel, $skip_extended = false)
+    {
+        if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_CLOSE) {
+            throw new \RuntimeException('Please close the channel (' . $channel . ') before trying to open it again');
+        }
+
+        $this->channelCount++;
+
+        if ($this->channelCount > 1 && $this->errorOnMultipleChannels) {
+            throw new \RuntimeException("Ubuntu's OpenSSH from 5.8 to 6.9 doesn't work with multiple channels");
         }
 
-        $this->window_size_server_to_client[NET_SSH2_CHANNEL_SHELL] = $this->window_size;
+        // 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 = pack('CNa*N3',
-            NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_SHELL, $this->window_size_server_to_client[NET_SSH2_CHANNEL_SHELL], $packet_size);
+        $packet = Strings::packSSH2(
+            'CsN3',
+            NET_SSH2_MSG_CHANNEL_OPEN,
+            'session',
+            $channel,
+            $this->window_size_server_to_client[$channel],
+            $packet_size
+        );
+
+        $this->send_binary_packet($packet);
 
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
-        }
+        $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_OPEN;
 
-        $this->channel_status[NET_SSH2_CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_OPEN;
+        return $this->get_channel_packet($channel, $skip_extended);
+    }
 
-        $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SHELL);
-        if ($response === false) {
-            return false;
+    /**
+     * 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()
+     * @return bool
+     * @throws InsufficientSetupException if not authenticated
+     * @throws \UnexpectedValueException on receipt of unexpected packets
+     * @throws \RuntimeException on other errors
+     */
+    public function openShell()
+    {
+        if (!$this->isAuthenticated()) {
+            throw new InsufficientSetupException('Operation disallowed prior to login()');
         }
 
+        $this->open_channel(self::CHANNEL_SHELL);
+
         $terminal_modes = pack('C', NET_SSH2_TTY_OP_END);
-        $packet = pack('CNNa*CNa*N5a*',
-            NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_SHELL], strlen('pty-req'), 'pty-req', 1, strlen('vt100'), 'vt100',
-            $this->windowColumns, $this->windowRows, 0, 0, strlen($terminal_modes), $terminal_modes);
+        $packet = Strings::packSSH2(
+            'CNsbsN4s',
+            NET_SSH2_MSG_CHANNEL_REQUEST,
+            $this->server_channels[self::CHANNEL_SHELL],
+            'pty-req',
+            true, // want reply
+            $this->term,
+            $this->windowColumns,
+            $this->windowRows,
+            0,
+            0,
+            $terminal_modes
+        );
 
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
+        $this->send_binary_packet($packet);
+
+        $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_REQUEST;
+
+        if (!$this->get_channel_packet(self::CHANNEL_SHELL)) {
+            throw new \RuntimeException('Unable to request pty');
         }
 
-        $response = $this->_get_binary_packet();
+        $packet = Strings::packSSH2(
+            'CNsb',
+            NET_SSH2_MSG_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) {
-            user_error('Connection closed by server');
-            return false;
+            throw new \RuntimeException('Unable to request shell');
         }
 
-        list(, $type) = unpack('C', $this->_string_shift($response, 1));
+        $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA;
 
-        switch ($type) {
-            case NET_SSH2_MSG_CHANNEL_SUCCESS:
-            // if a pty can't be opened maybe commands can still be executed
-            case NET_SSH2_MSG_CHANNEL_FAILURE:
-                break;
-            default:
-                user_error('Unable to request pseudo-terminal');
-                return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
-        }
+        $this->channel_id_last_interactive = self::CHANNEL_SHELL;
 
-        $packet = pack('CNNa*C',
-            NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_SHELL], strlen('shell'), 'shell', 1);
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
-        }
+        $this->bitmap |= self::MASK_SHELL;
 
-        $this->channel_status[NET_SSH2_CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_REQUEST;
+        return true;
+    }
 
-        $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SHELL);
-        if ($response === false) {
-            return false;
+    /**
+     * 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()
+     * @return int
+     */
+    private function get_interactive_channel()
+    {
+        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;
         }
+    }
 
-        $this->channel_status[NET_SSH2_CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA;
+    /**
+     * Indicates the DATA status on the given channel
+     *
+     * @param int $channel The channel number to evaluate
+     * @return bool
+     */
+    private function is_channel_status_data($channel)
+    {
+        return isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA;
+    }
 
-        $this->bitmap |= NET_SSH2_MASK_SHELL;
+    /**
+     * Return an available open channel
+     *
+     * @return int
+     */
+    private function get_open_channel()
+    {
+        $channel = self::CHANNEL_EXEC;
+        do {
+            if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) {
+                return $channel;
+            }
+        } while ($channel++ < self::CHANNEL_SUBSYSTEM);
 
-        return true;
+        return false;
     }
 
     /**
-     * Return the channel to be used with read() / write()
+     * Request agent forwarding of remote server
      *
-     * @see Net_SSH2::read()
-     * @see Net_SSH2::write()
-     * @return Integer
-     * @access public
+     * @return bool
      */
-    function _get_interactive_channel()
+    public function requestAgentForwarding()
     {
-        switch (true) {
-            case $this->in_subsystem:
-                return NET_SSH2_CHANNEL_SUBSYSTEM;
-            case $this->in_request_pty_exec:
-                return NET_SSH2_CHANNEL_EXEC;
-            default:
-                return NET_SSH2_CHANNEL_SHELL;
+        $request_channel = $this->get_open_channel();
+        if ($request_channel === false) {
+            return false;
+        }
+
+        $packet = Strings::packSSH2(
+            'CNsC',
+            NET_SSH2_MSG_CHANNEL_REQUEST,
+            $this->server_channels[$request_channel],
+            'auth-agent-req@openssh.com',
+            1
+        );
+
+        $this->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST;
+
+        $this->send_binary_packet($packet);
+
+        if (!$this->get_channel_packet($request_channel)) {
+            return false;
         }
+
+        $this->channel_status[$request_channel] = NET_SSH2_MSG_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 == NET_SSH2_READ_REGEX, a regular expression.
-     *
-     * @see Net_SSH2::write()
-     * @param String $expect
-     * @param Integer $mode
-     * @return String
-     * @access public
-     */
-    function read($expect = '', $mode = NET_SSH2_READ_SIMPLE)
+     * 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()
+     * @param string $expect
+     * @param int $mode One of the self::READ_* constants
+     * @param int|null $channel Channel id returned by self::getInteractiveChannelId()
+     * @return string|bool|null
+     * @throws \RuntimeException on connection error
+     * @throws InsufficientSetupException on unexpected channel status, possibly due to closure
+     */
+    public function read($expect = '', $mode = self::READ_SIMPLE, $channel = null)
     {
+        if (!$this->isAuthenticated()) {
+            throw new InsufficientSetupException('Operation disallowed prior to login()');
+        }
+
         $this->curTimeout = $this->timeout;
         $this->is_timeout = false;
 
-        if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
-            user_error('Operation disallowed prior to login()');
-            return false;
+        if ($channel === null) {
+            $channel = $this->get_interactive_channel();
         }
 
-        if (!($this->bitmap & NET_SSH2_MASK_SHELL) && !$this->_initShell()) {
-            user_error('Unable to initiate an interactive shell session');
-            return false;
+        if (!$this->is_channel_status_data($channel) && empty($this->channel_buffers[$channel])) {
+            if ($channel != self::CHANNEL_SHELL) {
+                throw new InsufficientSetupException('Data is not available on channel');
+            } elseif (!$this->openShell()) {
+                throw new \RuntimeException('Unable to initiate an interactive shell session');
+            }
         }
 
-        $channel = $this->_get_interactive_channel();
+        if ($mode == self::READ_NEXT) {
+            return $this->get_channel_packet($channel);
+        }
 
         $match = $expect;
         while (true) {
-            if ($mode == NET_SSH2_READ_REGEX) {
-                preg_match($expect, $this->interactiveBuffer, $matches);
+            if ($mode == self::READ_REGEX) {
+                preg_match($expect, substr($this->interactiveBuffer, -1024), $matches);
                 $match = isset($matches[0]) ? $matches[0] : '';
             }
             $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false;
             if ($pos !== false) {
-                return $this->_string_shift($this->interactiveBuffer, $pos + strlen($match));
+                return Strings::shift($this->interactiveBuffer, $pos + strlen($match));
             }
-            $response = $this->_get_channel_packet($channel);
-            if (is_bool($response)) {
-                $this->in_request_pty_exec = false;
-                return $response ? $this->_string_shift($this->interactiveBuffer, strlen($this->interactiveBuffer)) : false;
+            $response = $this->get_channel_packet($channel);
+            if ($response === true) {
+                return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer));
             }
 
-            $this->interactiveBuffer.= $response;
+            $this->interactiveBuffer .= $response;
         }
     }
 
     /**
      * Inputs a command into an interactive shell.
      *
-     * @see Net_SSH2::read()
-     * @param String $cmd
-     * @return Boolean
-     * @access public
-     */
-    function write($cmd)
+     * 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 SSH2::read()
+     * @param string $cmd
+     * @param int|null $channel Channel id returned by self::getInteractiveChannelId()
+     * @return void
+     * @throws \RuntimeException on connection error
+     * @throws InsufficientSetupException on unexpected channel status, possibly due to closure
+     */
+    public function write($cmd, $channel = null)
     {
-        if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
-            user_error('Operation disallowed prior to login()');
-            return false;
+        if (!$this->isAuthenticated()) {
+            throw new InsufficientSetupException('Operation disallowed prior to login()');
         }
 
-        if (!($this->bitmap & NET_SSH2_MASK_SHELL) && !$this->_initShell()) {
-            user_error('Unable to initiate an interactive shell session');
-            return false;
+        if ($channel === null) {
+            $channel = $this->get_interactive_channel();
         }
 
-        return $this->_send_channel_packet($this->_get_interactive_channel(), $cmd);
+        if (!$this->is_channel_status_data($channel)) {
+            if ($channel != self::CHANNEL_SHELL) {
+                throw new InsufficientSetupException('Data is not available on channel');
+            } elseif (!$this->openShell()) {
+                throw new \RuntimeException('Unable to initiate an interactive shell session');
+            }
+        }
+
+        $this->send_channel_packet($channel, $cmd);
     }
 
     /**
@@ -2558,124 +3236,260 @@ class Net_SSH2
      * 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 Net_SSH2::stopSubsystem()
-     * @param String $subsystem
-     * @return Boolean
-     * @access public
+     * @see self::stopSubsystem()
+     * @param string $subsystem
+     * @return bool
      */
-    function startSubsystem($subsystem)
+    public function startSubsystem($subsystem)
     {
-        $this->window_size_server_to_client[NET_SSH2_CHANNEL_SUBSYSTEM] = $this->window_size;
+        $this->open_channel(self::CHANNEL_SUBSYSTEM);
+
+        $packet = Strings::packSSH2(
+            'CNsCs',
+            NET_SSH2_MSG_CHANNEL_REQUEST,
+            $this->server_channels[self::CHANNEL_SUBSYSTEM],
+            'subsystem',
+            1,
+            $subsystem
+        );
+        $this->send_binary_packet($packet);
 
-        $packet = pack('CNa*N3',
-            NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_SUBSYSTEM, $this->window_size, 0x4000);
+        $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_REQUEST;
 
-        if (!$this->_send_binary_packet($packet)) {
+        if (!$this->get_channel_packet(self::CHANNEL_SUBSYSTEM)) {
             return false;
         }
 
-        $this->channel_status[NET_SSH2_CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_OPEN;
+        $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA;
 
-        $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SUBSYSTEM);
-        if ($response === false) {
-            return false;
-        }
+        $this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM;
 
-        $packet = pack('CNNa*CNa*',
-            NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_SUBSYSTEM], strlen('subsystem'), 'subsystem', 1, strlen($subsystem), $subsystem);
-        if (!$this->_send_binary_packet($packet)) {
-            return false;
+        return true;
+    }
+
+    /**
+     * Stops a subsystem.
+     *
+     * @see self::startSubsystem()
+     * @return bool
+     */
+    public function stopSubsystem()
+    {
+        if ($this->isInteractiveChannelOpen(self::CHANNEL_SUBSYSTEM)) {
+            $this->close_channel(self::CHANNEL_SUBSYSTEM);
         }
+        return true;
+    }
 
-        $this->channel_status[NET_SSH2_CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_REQUEST;
+    /**
+     * 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()
+     * @return void
+     */
+    public function reset($channel = null)
+    {
+        if ($channel === null) {
+            $channel = $this->get_interactive_channel();
+        }
+        if ($this->isInteractiveChannelOpen($channel)) {
+            $this->close_channel($channel);
+        }
+    }
 
-        $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SUBSYSTEM);
+    /**
+     * Is timeout?
+     *
+     * Did exec() or read() return because they timed out or because they encountered the end?
+     *
+     */
+    public function isTimeout()
+    {
+        return $this->is_timeout;
+    }
 
-        if ($response === false) {
-           return false;
+    /**
+     * Disconnect
+     *
+     */
+    public function disconnect()
+    {
+        $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+        if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) {
+            fclose($this->realtime_log_file);
         }
+        unset(self::$connections[$this->getResourceId()]);
+    }
 
-        $this->channel_status[NET_SSH2_CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA;
+    /**
+     * 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();
+    }
 
-        $this->bitmap |= NET_SSH2_MASK_SHELL;
-        $this->in_subsystem = true;
+    /**
+     * 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
+     *
+     * @param int $level
+     * @return bool
+     */
+    public function isConnected($level = 0)
+    {
+        if (!is_int($level) || $level < 0 || $level > 2) {
+            throw new \InvalidArgumentException('$level must be 0, 1 or 2');
+        }
 
-        return true;
+        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', NET_SSH2_MSG_IGNORE, 0));
+            } else {
+                $this->open_channel(self::CHANNEL_KEEP_ALIVE);
+                $this->close_channel(self::CHANNEL_KEEP_ALIVE);
+            }
+            return true;
+        } catch (\Exception $e) {
+            return false;
+        }
     }
 
     /**
-     * Stops a subsystem.
+     * Have you successfully been logged in?
      *
-     * @see Net_SSH2::startSubsystem()
-     * @return Boolean
-     * @access public
+     * @return bool
      */
-    function stopSubsystem()
+    public function isAuthenticated()
     {
-        $this->in_subsystem = false;
-        $this->_close_channel(NET_SSH2_CHANNEL_SUBSYSTEM);
-        return true;
+        return (bool) ($this->bitmap & self::MASK_LOGIN);
     }
 
     /**
-     * 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
+     * Is the interactive shell active?
      *
-     * @access public
+     * @return bool
      */
-    function reset()
+    public function isShellOpen()
     {
-        $this->_close_channel($this->_get_interactive_channel());
+        return $this->isInteractiveChannelOpen(self::CHANNEL_SHELL);
     }
 
     /**
-     * Is timeout?
+     * Is the exec pty active?
      *
-     * Did exec() or read() return because they timed out or because they encountered the end?
+     * @return bool
+     */
+    public function isPTYOpen()
+    {
+        return $this->isInteractiveChannelOpen(self::CHANNEL_EXEC);
+    }
+
+    /**
+     * Is the given interactive channel active?
      *
-     * @access public
+     * @param int $channel Channel id returned by self::getInteractiveChannelId()
+     * @return bool
      */
-    function isTimeout()
+    public function isInteractiveChannelOpen($channel)
     {
-        return $this->is_timeout;
+        return $this->isAuthenticated() && $this->is_channel_status_data($channel);
     }
 
     /**
-     * Disconnect
+     * Returns a channel identifier, presently of the last interactive channel opened, regardless of current status.
+     * Returns 0 if no interactive channel has been opened.
      *
-     * @access public
+     * @see self::isInteractiveChannelOpen()
+     * @return int
      */
-    function disconnect()
+    public function getInteractiveChannelId()
     {
-        $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
-        if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) {
-            fclose($this->realtime_log_file);
-        }
+        return $this->channel_id_last_interactive;
     }
 
     /**
-     * Destructor.
+     * Pings a server connection, or tries to reconnect if the connection has gone down
      *
-     * Will be called, automatically, if you're supporting just PHP5.  If you're supporting PHP4, you'll need to call
-     * disconnect().
+     * Inspired by http://php.net/manual/en/mysqli.ping.php
      *
-     * @access public
+     * @return bool
      */
-    function __destruct()
+    public function ping()
     {
-        $this->disconnect();
+        if (!$this->isAuthenticated()) {
+            if (!empty($this->auth)) {
+                return $this->reconnect();
+            }
+            return false;
+        }
+
+        try {
+            $this->open_channel(self::CHANNEL_KEEP_ALIVE);
+        } catch (\RuntimeException $e) {
+            return $this->reconnect();
+        }
+
+        $this->close_channel(self::CHANNEL_KEEP_ALIVE);
+        return true;
     }
 
     /**
-     * Is the connection still active?
+     * In situ reconnect method
      *
      * @return boolean
-     * @access public
      */
-    function isConnected()
+    private function reconnect()
+    {
+        $this->reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST);
+        $this->retry_connect = true;
+        $this->connect();
+        foreach ($this->auth as $auth) {
+            $result = $this->login(...$auth);
+        }
+        return $result;
+    }
+
+    /**
+     * Resets a connection for re-use
+     *
+     * @param int $reason
+     */
+    protected function reset_connection($reason)
     {
-        return (bool) ($this->bitmap & NET_SSH2_MASK_CONNECTED);
+        $this->disconnect_helper($reason);
+        $this->decrypt = $this->encrypt = false;
+        $this->decrypt_block_size = $this->encrypt_block_size = 8;
+        $this->hmac_check = $this->hmac_create = false;
+        $this->hmac_size = false;
+        $this->session_id = false;
+        $this->retry_connect = true;
+        $this->get_seq_no = $this->send_seq_no = 0;
+        $this->channel_status = [];
+        $this->channel_id_last_interactive = 0;
     }
 
     /**
@@ -2683,92 +3497,311 @@ class Net_SSH2
      *
      * See '6. Binary Packet Protocol' of rfc4253 for more info.
      *
-     * @see Net_SSH2::_send_binary_packet()
-     * @return String
-     * @access private
+     * @see self::_send_binary_packet()
+     * @param bool $skip_channel_filter
+     * @return bool|string
      */
-    function _get_binary_packet()
+    private function get_binary_packet($skip_channel_filter = false)
     {
+        if ($skip_channel_filter) {
+            if (!is_resource($this->fsock)) {
+                throw new \InvalidArgumentException('fsock is not a resource.');
+            }
+            $read = [$this->fsock];
+            $write = $except = null;
+
+            if (!$this->curTimeout) {
+                if ($this->keepAlive <= 0) {
+                    static::stream_select($read, $write, $except, null);
+                } else {
+                    if (!static::stream_select($read, $write, $except, $this->keepAlive)) {
+                        $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0));
+                        return $this->get_binary_packet(true);
+                    }
+                }
+            } else {
+                if ($this->curTimeout < 0) {
+                    $this->is_timeout = true;
+                    return true;
+                }
+
+                $start = microtime(true);
+
+                if ($this->keepAlive > 0 && $this->keepAlive < $this->curTimeout) {
+                    if (!static::stream_select($read, $write, $except, $this->keepAlive)) {
+                        $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0));
+                        $elapsed = microtime(true) - $start;
+                        $this->curTimeout -= $elapsed;
+                        return $this->get_binary_packet(true);
+                    }
+                    $elapsed = microtime(true) - $start;
+                    $this->curTimeout -= $elapsed;
+                }
+
+                $sec = (int) floor($this->curTimeout);
+                $usec = (int) (1000000 * ($this->curTimeout - $sec));
+
+                // this can return a "stream_select(): unable to select [4]: Interrupted system call" error
+                if (!static::stream_select($read, $write, $except, $sec, $usec)) {
+                    $this->is_timeout = true;
+                    return true;
+                }
+                $elapsed = microtime(true) - $start;
+                $this->curTimeout -= $elapsed;
+            }
+        }
+
         if (!is_resource($this->fsock) || feof($this->fsock)) {
-            user_error('Connection closed prematurely');
             $this->bitmap = 0;
-            return false;
+            $str = 'Connection closed (by server) prematurely';
+            if (isset($elapsed)) {
+                $str .= ' ' . $elapsed . 's';
+            }
+            throw new ConnectionClosedException($str);
         }
 
-        $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838
-        $raw = fread($this->fsock, $this->decrypt_block_size);
+        $start = microtime(true);
+        if ($this->curTimeout) {
+            $sec = (int) floor($this->curTimeout);
+            $usec = (int) (1000000 * ($this->curTimeout - $sec));
+            stream_set_timeout($this->fsock, $sec, $usec);
+        }
+        $raw = stream_get_contents($this->fsock, $this->decrypt_block_size);
 
         if (!strlen($raw)) {
-            return '';
+            $this->bitmap = 0;
+            throw new ConnectionClosedException('No data received from server');
         }
 
-        if ($this->decrypt !== false) {
-            $raw = $this->decrypt->decrypt($raw);
-        }
-        if ($raw === false) {
-            user_error('Unable to decrypt content');
-            return false;
-        }
+        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($temp = Strings::shift($raw, 4));
+                    extract(unpack('Npacket_length', $temp));
+                    /**
+                     * @var integer $packet_length
+                     */
+
+                    $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
+                    $stop = microtime(true);
+                    $tag = stream_get_contents($this->fsock, $this->decrypt_block_size);
+                    $this->decrypt->setTag($tag);
+                    $raw = $this->decrypt->decrypt($raw);
+                    $raw = $temp . $raw;
+                    $remaining_length = 0;
+                    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 \LogicException('$this->decrypt is not a ' . ChaCha20::class);
+                    }
 
-        extract(unpack('Npacket_length/Cpadding_length', $this->_string_shift($raw, 5)));
+                    $nonce = pack('N2', 0, $this->get_seq_no);
+
+                    $this->lengthDecrypt->setNonce($nonce);
+                    $temp = $this->lengthDecrypt->decrypt($aad = Strings::shift($raw, 4));
+                    extract(unpack('Npacket_length', $temp));
+                    /**
+                     * @var integer $packet_length
+                     */
+
+                    $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
+                    $stop = microtime(true);
+                    $tag = stream_get_contents($this->fsock, 16);
+
+                    $this->decrypt->setNonce($nonce);
+                    $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($aad);
+                    $this->decrypt->setCounter(1);
+                    $this->decrypt->setTag($tag);
+                    $raw = $this->decrypt->decrypt($raw);
+                    $raw = $temp . $raw;
+                    $remaining_length = 0;
+                    break;
+                default:
+                    if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) {
+                        $raw = $this->decrypt->decrypt($raw);
+                        break;
+                    }
+                    extract(unpack('Npacket_length', $temp = Strings::shift($raw, 4)));
+                    /**
+                     * @var integer $packet_length
+                     */
+                    $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
+                    $stop = microtime(true);
+                    $encrypted = $temp . $raw;
+                    $raw = $temp . $this->decrypt->decrypt($raw);
+                    $remaining_length = 0;
+            }
+        }
 
-        $remaining_length = $packet_length + 4 - $this->decrypt_block_size;
+        if (strlen($raw) < 5) {
+            $this->bitmap = 0;
+            throw new \RuntimeException('Plaintext is too short');
+        }
+        extract(unpack('Npacket_length/Cpadding_length', Strings::shift($raw, 5)));
+        /**
+         * @var integer $packet_length
+         * @var integer $padding_length
+         */
 
-        // 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 ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) {
-            user_error('Invalid size');
-            return false;
+        if (!isset($remaining_length)) {
+            $remaining_length = $packet_length + 4 - $this->decrypt_block_size;
         }
 
-        $buffer = '';
-        while ($remaining_length > 0) {
-            $temp = fread($this->fsock, $remaining_length);
-            if ($temp === false || feof($this->fsock)) {
-                user_error('Error reading from socket');
-                $this->bitmap = 0;
-                return false;
-            }
-            $buffer.= $temp;
-            $remaining_length-= strlen($temp);
+        $buffer = $this->read_remaining_bytes($remaining_length);
+
+        if (!isset($stop)) {
+            $stop = microtime(true);
         }
-        $stop = strtok(microtime(), ' ') + strtok('');
         if (strlen($buffer)) {
-            $raw.= $this->decrypt !== false ? $this->decrypt->decrypt($buffer) : $buffer;
+            $raw .= $this->decrypt ? $this->decrypt->decrypt($buffer) : $buffer;
         }
 
-        $payload = $this->_string_shift($raw, $packet_length - $padding_length - 1);
-        $padding = $this->_string_shift($raw, $padding_length); // should leave $raw empty
+        $payload = Strings::shift($raw, $packet_length - $padding_length - 1);
+        $padding = Strings::shift($raw, $padding_length); // should leave $raw empty
 
-        if ($this->hmac_check !== false) {
-            $hmac = fread($this->fsock, $this->hmac_size);
+        if ($this->hmac_check instanceof Hash) {
+            $hmac = stream_get_contents($this->fsock, $this->hmac_size);
             if ($hmac === false || strlen($hmac) != $this->hmac_size) {
-                user_error('Error reading socket');
-                $this->bitmap = 0;
-                return false;
-            } elseif ($hmac != $this->hmac_check->hash(pack('NNCa*', $this->get_seq_no, $packet_length, $padding_length, $payload . $padding))) {
-                user_error('Invalid HMAC');
-                return false;
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR);
+                throw new \RuntimeException('Error reading socket');
+            }
+
+            $reconstructed = !$this->hmac_check_etm ?
+                pack('NCa*', $packet_length, $padding_length, $payload . $padding) :
+                $encrypted;
+            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 ($hmac != $this->hmac_check->hash($reconstructed)) {
+                    $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR);
+                    throw new \RuntimeException('Invalid UMAC');
+                }
+            } else {
+                if ($hmac != $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed))) {
+                    $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR);
+                    throw new \RuntimeException('Invalid HMAC');
+                }
             }
         }
 
-        //if ($this->decompress) {
-        //    $payload = gzinflate(substr($payload, 2));
-        //}
+        switch ($this->decompress) {
+            case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH:
+                if (!$this->isAuthenticated()) {
+                    break;
+                }
+                // fall-through
+            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
+                        user_error("Only CM = 8 ('deflate') is supported ($cm)");
+                    }
+                    $cinfo = ($cmf & 0xF0) >> 4;
+                    if ($cinfo > 7) {
+                        user_error("CINFO above 7 is not allowed ($cinfo)");
+                    }
+                    $windowSize = 1 << ($cinfo + 8);
+
+                    $flg = ord($payload[1]);
+                    //$fcheck = $flg && 0x0F;
+                    if ((($cmf << 8) | $flg) % 31) {
+                        user_error('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 = strtok(microtime(), ' ') + strtok('');
-            $message_number = isset($this->message_numbers[ord($payload[0])]) ? $this->message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')';
+            $current = microtime(true);
+            $message_number = isset(self::$message_numbers[ord($payload[0])]) ? self::$message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')';
             $message_number = '<- ' . $message_number .
                               ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)';
-            $this->_append_log($message_number, $payload);
+            $this->append_log($message_number, $payload);
             $this->last_packet = $current;
         }
 
-        return $this->_filter($payload);
+        return $this->filter($payload, $skip_channel_filter);
+    }
+
+    /**
+     * Read Remaining Bytes
+     *
+     * @see self::get_binary_packet()
+     * @param int $remaining_length
+     * @return string
+     */
+    private function read_remaining_bytes($remaining_length)
+    {
+        if (!$remaining_length) {
+            return '';
+        }
+
+        $adjustLength = false;
+        if ($this->decrypt) {
+            switch (true) {
+                case $this->decryptName == 'aes128-gcm@openssh.com':
+                case $this->decryptName == 'aes256-gcm@openssh.com':
+                case $this->decryptName == 'chacha20-poly1305@openssh.com':
+                case $this->hmac_check instanceof Hash && $this->hmac_check_etm:
+                    $remaining_length += $this->decrypt_block_size - 4;
+                    $adjustLength = true;
+            }
+        }
+
+        // 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
+        // don't do this when GCM mode is used since GCM mode doesn't encrypt the length
+        if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) {
+            if (!$this->bad_key_size_fix && self::bad_algorithm_candidate($this->decrypt ? $this->decryptName : '') && !($this->bitmap & SSH2::MASK_LOGIN)) {
+                $this->bad_key_size_fix = true;
+                $this->reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
+                return false;
+            }
+            throw new \RuntimeException('Invalid size');
+        }
+
+        if ($adjustLength) {
+            $remaining_length -= $this->decrypt_block_size - 4;
+        }
+
+        $buffer = '';
+        while ($remaining_length > 0) {
+            $temp = stream_get_contents($this->fsock, $remaining_length);
+            if ($temp === false || feof($this->fsock)) {
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
+                throw new \RuntimeException('Error reading from socket');
+            }
+            $buffer .= $temp;
+            $remaining_length -= strlen($temp);
+        }
+
+        return $buffer;
     }
 
     /**
@@ -2776,86 +3809,152 @@ class Net_SSH2
      *
      * Because some binary packets need to be ignored...
      *
-     * @see Net_SSH2::_get_binary_packet()
-     * @return String
-     * @access private
+     * @see self::_get_binary_packet()
+     * @param string $payload
+     * @param bool $skip_channel_filter
+     * @return string|bool
      */
-    function _filter($payload)
+    private function filter($payload, $skip_channel_filter)
     {
         switch (ord($payload[0])) {
             case NET_SSH2_MSG_DISCONNECT:
-                $this->_string_shift($payload, 1);
-                extract(unpack('Nreason_code/Nlength', $this->_string_shift($payload, 8)));
-                $this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n" . utf8_decode($this->_string_shift($payload, $length));
+                Strings::shift($payload, 1);
+                list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload);
+                $this->errors[] = 'SSH_MSG_DISCONNECT: ' . self::$disconnect_reasons[$reason_code] . "\r\n$message";
                 $this->bitmap = 0;
                 return false;
             case NET_SSH2_MSG_IGNORE:
-                $payload = $this->_get_binary_packet();
+                $this->extra_packets++;
+                $payload = $this->get_binary_packet($skip_channel_filter);
                 break;
             case NET_SSH2_MSG_DEBUG:
-                $this->_string_shift($payload, 2);
-                extract(unpack('Nlength', $this->_string_shift($payload, 4)));
-                $this->errors[] = 'SSH_MSG_DEBUG: ' . utf8_decode($this->_string_shift($payload, $length));
-                $payload = $this->_get_binary_packet();
+                $this->extra_packets++;
+                Strings::shift($payload, 2); // second byte is "always_display"
+                list($message) = Strings::unpackSSH2('s', $payload);
+                $this->errors[] = "SSH_MSG_DEBUG: $message";
+                $payload = $this->get_binary_packet($skip_channel_filter);
                 break;
             case NET_SSH2_MSG_UNIMPLEMENTED:
                 return false;
             case NET_SSH2_MSG_KEXINIT:
+                // this is here for key re-exchanges after the initial key exchange
                 if ($this->session_id !== false) {
-                    if (!$this->_key_exchange($payload)) {
+                    if (!$this->key_exchange($payload)) {
                         $this->bitmap = 0;
                         return false;
                     }
-                    $payload = $this->_get_binary_packet();
+                    $payload = $this->get_binary_packet($skip_channel_filter);
                 }
         }
 
         // 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 & NET_SSH2_MASK_CONNECTED) && !($this->bitmap & NET_SSH2_MASK_LOGIN) && ord($payload[0]) == NET_SSH2_MSG_USERAUTH_BANNER) {
-            $this->_string_shift($payload, 1);
-            extract(unpack('Nlength', $this->_string_shift($payload, 4)));
-            $this->banner_message = utf8_decode($this->_string_shift($payload, $length));
-            $payload = $this->_get_binary_packet();
+        if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && !is_bool($payload) && ord($payload[0]) == NET_SSH2_MSG_USERAUTH_BANNER) {
+            Strings::shift($payload, 1);
+            list($this->banner_message) = Strings::unpackSSH2('s', $payload);
+            $payload = $this->get_binary_packet();
         }
 
         // only called when we've already logged in
-        if (($this->bitmap & NET_SSH2_MASK_CONNECTED) && ($this->bitmap & NET_SSH2_MASK_LOGIN)) {
+        if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) {
+            if (is_bool($payload)) {
+                return $payload;
+            }
+
             switch (ord($payload[0])) {
+                case NET_SSH2_MSG_CHANNEL_REQUEST:
+                    if (strlen($payload) == 31) {
+                        extract(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', NET_SSH2_MSG_CHANNEL_SUCCESS, $this->server_channels[$channel]));
+                            }
+                            $payload = $this->get_binary_packet($skip_channel_filter);
+                        }
+                    }
+                    break;
+                case NET_SSH2_MSG_CHANNEL_DATA:
+                case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA:
+                case NET_SSH2_MSG_CHANNEL_CLOSE:
+                case NET_SSH2_MSG_CHANNEL_EOF:
+                    if (!$skip_channel_filter && !empty($this->server_channels)) {
+                        $this->binary_packet_buffer = $payload;
+                        $this->get_channel_packet(true);
+                        $payload = $this->get_binary_packet();
+                    }
+                    break;
                 case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4
-                    $this->_string_shift($payload, 1);
-                    extract(unpack('Nlength', $this->_string_shift($payload)));
-                    $this->errors[] = 'SSH_MSG_GLOBAL_REQUEST: ' . utf8_decode($this->_string_shift($payload, $length));
-
-                    if (!$this->_send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE))) {
-                        return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                    Strings::shift($payload, 1);
+                    list($request_name) = Strings::unpackSSH2('s', $payload);
+                    $this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name";
+
+                    try {
+                        $this->send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE));
+                    } catch (\RuntimeException $e) {
+                        return $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
                     }
 
-                    $payload = $this->_get_binary_packet();
+                    $payload = $this->get_binary_packet($skip_channel_filter);
                     break;
                 case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1
-                    $this->_string_shift($payload, 1);
-                    extract(unpack('Nlength', $this->_string_shift($payload, 4)));
-                    $this->errors[] = 'SSH_MSG_CHANNEL_OPEN: ' . utf8_decode($this->_string_shift($payload, $length));
-
-                    $this->_string_shift($payload, 4); // skip over client channel
-                    extract(unpack('Nserver_channel', $this->_string_shift($payload, 4)));
-
-                    $packet = pack('CN3a*Na*',
-                        NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, '');
-
-                    if (!$this->_send_binary_packet($packet)) {
-                        return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                    Strings::shift($payload, 1);
+                    list($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;
+
+                                list(
+                                    $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',
+                                    NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION,
+                                    $server_channel,
+                                    $new_channel,
+                                    $packet_size,
+                                    $packet_size
+                                );
+
+                                $this->server_channels[$new_channel] = $server_channel;
+                                $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION;
+                                $this->send_binary_packet($packet);
+                            }
+                            break;
+                        default:
+                            $packet = Strings::packSSH2(
+                                'CN2ss',
+                                NET_SSH2_MSG_CHANNEL_OPEN_FAILURE,
+                                $server_channel,
+                                NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED,
+                                '', // description
+                                '' // language tag
+                            );
+
+                            try {
+                                $this->send_binary_packet($packet);
+                            } catch (\RuntimeException $e) {
+                                return $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                            }
                     }
 
-                    $payload = $this->_get_binary_packet();
+                    $payload = $this->get_binary_packet($skip_channel_filter);
                     break;
                 case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST:
-                    $this->_string_shift($payload, 1);
-                    extract(unpack('Nchannel', $this->_string_shift($payload, 4)));
-                    extract(unpack('Nwindow_size', $this->_string_shift($payload, 4)));
-                    $this->window_size_client_to_server[$channel]+= $window_size;
+                    Strings::shift($payload, 1);
+                    list($channel, $window_size) = Strings::unpackSSH2('NN', $payload);
 
-                    $payload = ($this->bitmap & NET_SSH2_MASK_WINDOW_ADJUST) ? true : $this->_get_binary_packet();
+                    $this->window_size_client_to_server[$channel] += $window_size;
+
+                    $payload = ($this->bitmap & self::MASK_WINDOW_ADJUST) ? true : $this->get_binary_packet($skip_channel_filter);
             }
         }
 
@@ -2867,9 +3966,8 @@ class Net_SSH2
      *
      * Suppress stderr from output
      *
-     * @access public
      */
-    function enableQuietMode()
+    public function enableQuietMode()
     {
         $this->quiet_mode = true;
     }
@@ -2879,9 +3977,8 @@ class Net_SSH2
      *
      * Show stderr in output
      *
-     * @access public
      */
-    function disableQuietMode()
+    public function disableQuietMode()
     {
         $this->quiet_mode = false;
     }
@@ -2889,13 +3986,11 @@ class Net_SSH2
     /**
      * Returns whether Quiet Mode is enabled or not
      *
-     * @see Net_SSH2::enableQuietMode()
-     * @see Net_SSH2::disableQuietMode()
-     *
-     * @access public
-     * @return boolean
+     * @see self::enableQuietMode()
+     * @see self::disableQuietMode()
+     * @return bool
      */
-    function isQuietModeEnabled()
+    public function isQuietModeEnabled()
     {
         return $this->quiet_mode;
     }
@@ -2903,9 +3998,8 @@ class Net_SSH2
     /**
      * Enable request-pty when using exec()
      *
-     * @access public
      */
-    function enablePTY()
+    public function enablePTY()
     {
         $this->request_pty = true;
     }
@@ -2913,23 +4007,23 @@ class Net_SSH2
     /**
      * Disable request-pty when using exec()
      *
-     * @access public
      */
-    function disablePTY()
+    public function disablePTY()
     {
+        if ($this->isPTYOpen()) {
+            $this->close_channel(self::CHANNEL_EXEC);
+        }
         $this->request_pty = false;
     }
 
     /**
      * Returns whether request-pty is enabled or not
      *
-     * @see Net_SSH2::enablePTY()
-     * @see Net_SSH2::disablePTY()
-     *
-     * @access public
-     * @return boolean
+     * @see self::enablePTY()
+     * @see self::disablePTY()
+     * @return bool
      */
-    function isPTYEnabled()
+    public function isPTYEnabled()
     {
         return $this->request_pty;
     }
@@ -2937,94 +4031,185 @@ class Net_SSH2
     /**
      * Gets channel data
      *
-     * Returns the data as a string if it's available and false if not.
+     * Returns the data as a string. bool(true) is returned if:
      *
-     * @param $client_channel
-     * @return Mixed
-     * @access private
+     * - the server closes the channel
+     * - if the connection times out
+     * - 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
+     * @param bool $skip_extended
+     * @return mixed
+     * @throws \RuntimeException on connection error
      */
-    function _get_channel_packet($client_channel, $skip_extended = false)
+    protected function get_channel_packet($client_channel, $skip_extended = false)
     {
         if (!empty($this->channel_buffers[$client_channel])) {
-            return array_shift($this->channel_buffers[$client_channel]);
+            switch ($this->channel_status[$client_channel]) {
+                case NET_SSH2_MSG_CHANNEL_REQUEST:
+                    foreach ($this->channel_buffers[$client_channel] as $i => $packet) {
+                        switch (ord($packet[0])) {
+                            case NET_SSH2_MSG_CHANNEL_SUCCESS:
+                            case NET_SSH2_MSG_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) {
-            if ($this->curTimeout) {
-                if ($this->curTimeout < 0) {
-                    $this->is_timeout = true;
+            if ($this->binary_packet_buffer !== false) {
+                $response = $this->binary_packet_buffer;
+                $this->binary_packet_buffer = false;
+            } else {
+                $response = $this->get_binary_packet(true);
+                if ($response === true && $this->is_timeout) {
+                    if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) {
+                        $this->close_channel($client_channel);
+                    }
                     return true;
                 }
-
-                $read = array($this->fsock);
-                $write = $except = null;
-
-                $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838
-                $sec = floor($this->curTimeout);
-                $usec = 1000000 * ($this->curTimeout - $sec);
-                // on windows this returns a "Warning: Invalid CRT parameters detected" error
-                if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) {
-                    $this->is_timeout = true;
-                    return true;
+                if ($response === false) {
+                    $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
+                    throw new ConnectionClosedException('Connection closed by server');
                 }
-                $elapsed = strtok(microtime(), ' ') + strtok('') - $start;
-                $this->curTimeout-= $elapsed;
             }
 
-            $response = $this->_get_binary_packet();
-            if ($response === false) {
-                user_error('Connection closed by server');
-                return false;
-            }
             if ($client_channel == -1 && $response === true) {
                 return true;
             }
-            if (!strlen($response)) {
-                return '';
-            }
-
-            extract(unpack('Ctype/Nchannel', $this->_string_shift($response, 5)));
+            list($type, $channel) = Strings::unpackSSH2('CN', $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', NET_SSH2_MSG_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;
+                }
 
-            $this->window_size_server_to_client[$channel]-= strlen($response);
+                switch ($type) {
+                    case NET_SSH2_MSG_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
+                        list($data_type_code, $data) = Strings::unpackSSH2('Ns', $response);
+                        $this->stdErrorLog .= $data;
+                        if ($skip_extended || $this->quiet_mode) {
+                            continue 2;
+                        }
+                        if ($client_channel == $channel && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA) {
+                            return $data;
+                        }
+                        $this->channel_buffers[$channel][] = chr($type) . $data;
 
-            // resize the window, if appropriate
-            if ($this->window_size_server_to_client[$channel] < 0) {
-                $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size);
-                if (!$this->_send_binary_packet($packet)) {
-                    return false;
+                        continue 2;
+                    case NET_SSH2_MSG_CHANNEL_REQUEST:
+                        if ($this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_CLOSE) {
+                            continue 2;
+                        }
+                        list($value) = Strings::unpackSSH2('s', $response);
+                        switch ($value) {
+                            case 'exit-signal':
+                                list(
+                                    , // 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";
+                                }
+
+                                $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel]));
+                                $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel]));
+
+                                $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_EOF;
+
+                                continue 3;
+                            case 'exit-status':
+                                list(, $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:
+                                // "Some systems may not implement signals, in which case they SHOULD ignore this message."
+                                //  -- http://tools.ietf.org/html/rfc4254#section-6.9
+                                continue 3;
+                        }
                 }
-                $this->window_size_server_to_client[$channel]+= $this->window_size;
-            }
-
-            switch ($this->channel_status[$channel]) {
-                case NET_SSH2_MSG_CHANNEL_OPEN:
-                    switch ($type) {
-                        case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
-                            extract(unpack('Nserver_channel', $this->_string_shift($response, 4)));
-                            $this->server_channels[$channel] = $server_channel;
-                            extract(unpack('Nwindow_size', $this->_string_shift($response, 4)));
-                            $this->window_size_client_to_server[$channel] = $window_size;
-                            $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4));
-                            $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server'];
-                            return $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended);
-                        //case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE:
-                        default:
-                            user_error('Unable to open channel');
-                            return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
-                    }
-                    break;
-                case NET_SSH2_MSG_CHANNEL_REQUEST:
-                    switch ($type) {
-                        case NET_SSH2_MSG_CHANNEL_SUCCESS:
+
+                switch ($this->channel_status[$channel]) {
+                    case NET_SSH2_MSG_CHANNEL_OPEN:
+                        switch ($type) {
+                            case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION:
+                                list(
+                                    $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 NET_SSH2_MSG_CHANNEL_OPEN_FAILURE:
+                                $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                                throw new \RuntimeException('Unable to open channel');
+                            default:
+                                if ($client_channel == $channel) {
+                                    $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                                    throw new \RuntimeException('Unexpected response to open request');
+                                }
+                                return $this->get_channel_packet($client_channel, $skip_extended);
+                        }
+                        break;
+                    case NET_SSH2_MSG_CHANNEL_REQUEST:
+                        switch ($type) {
+                            case NET_SSH2_MSG_CHANNEL_SUCCESS:
+                                return true;
+                            case NET_SSH2_MSG_CHANNEL_FAILURE:
+                                return false;
+                            case NET_SSH2_MSG_CHANNEL_DATA:
+                                list($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(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                                throw new \RuntimeException('Unable to fulfill channel request');
+                        }
+                    case NET_SSH2_MSG_CHANNEL_CLOSE:
+                        if ($client_channel == $channel && $type == NET_SSH2_MSG_CHANNEL_CLOSE) {
                             return true;
-                        case NET_SSH2_MSG_CHANNEL_FAILURE:
-                            return false;
-                        default:
-                            user_error('Unable to fulfill channel request');
-                            return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
-                    }
-                case NET_SSH2_MSG_CHANNEL_CLOSE:
-                    return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended);
+                        }
+                        return $this->get_channel_packet($client_channel, $skip_extended);
+                }
             }
 
             // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA
@@ -3032,96 +4217,50 @@ class Net_SSH2
             switch ($type) {
                 case NET_SSH2_MSG_CHANNEL_DATA:
                     /*
-                    if ($channel == NET_SSH2_CHANNEL_EXEC) {
+                    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));
-                    }
-                    */
-                    extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                    $data = $this->_string_shift($response, $length);
-                    if ($client_channel == $channel) {
-                        return $data;
-                    }
-                    if (!isset($this->channel_buffers[$channel])) {
-                        $this->channel_buffers[$channel] = array();
-                    }
-                    $this->channel_buffers[$channel][] = $data;
-                    break;
-                case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA:
-                    /*
-                    if ($client_channel == NET_SSH2_CHANNEL_EXEC) {
-                        $this->_send_channel_packet($client_channel, chr(0));
+                        $this->send_channel_packet($channel, chr(0));
                     }
                     */
-                    // currently, there's only one possible value for $data_type_code: NET_SSH2_EXTENDED_DATA_STDERR
-                    extract(unpack('Ndata_type_code/Nlength', $this->_string_shift($response, 8)));
-                    $data = $this->_string_shift($response, $length);
-                    $this->stdErrorLog.= $data;
-                    if ($skip_extended || $this->quiet_mode) {
+                    list($data) = Strings::unpackSSH2('s', $response);
+
+                    if ($channel == self::CHANNEL_AGENT_FORWARD) {
+                        $agent_response = $this->agent->forwardData($data);
+                        if (!is_bool($agent_response)) {
+                            $this->send_channel_packet($channel, $agent_response);
+                        }
                         break;
                     }
+
                     if ($client_channel == $channel) {
                         return $data;
                     }
-                    if (!isset($this->channel_buffers[$channel])) {
-                        $this->channel_buffers[$channel] = array();
-                    }
-                    $this->channel_buffers[$channel][] = $data;
-                    break;
-                case NET_SSH2_MSG_CHANNEL_REQUEST:
-                    extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                    $value = $this->_string_shift($response, $length);
-                    switch ($value) {
-                        case 'exit-signal':
-                            $this->_string_shift($response, 1);
-                            extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                            $this->errors[] = 'SSH_MSG_CHANNEL_REQUEST (exit-signal): ' . $this->_string_shift($response, $length);
-                            $this->_string_shift($response, 1);
-                            extract(unpack('Nlength', $this->_string_shift($response, 4)));
-                            if ($length) {
-                                $this->errors[count($this->errors)].= "\r\n" . $this->_string_shift($response, $length);
-                            }
-
-                            $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel]));
-                            $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel]));
-
-                            $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_EOF;
-
-                            break;
-                        case 'exit-status':
-                            extract(unpack('Cfalse/Nexit_status', $this->_string_shift($response, 5)));
-                            $this->exit_status = $exit_status;
-
-                            // "The client MAY ignore these messages."
-                            // -- http://tools.ietf.org/html/rfc4254#section-6.10
-
-                            break;
-                        default:
-                            // "Some systems may not implement signals, in which case they SHOULD ignore this message."
-                            //  -- http://tools.ietf.org/html/rfc4254#section-6.9
-                            break;
-                    }
+                    $this->channel_buffers[$channel][] = chr($type) . $data;
                     break;
                 case NET_SSH2_MSG_CHANNEL_CLOSE:
-                    $this->curTimeout = 0;
+                    $this->curTimeout = 5;
+
+                    $this->close_channel_bitmap($channel);
 
-                    if ($this->bitmap & NET_SSH2_MASK_SHELL) {
-                        $this->bitmap&= ~NET_SSH2_MASK_SHELL;
-                    }
                     if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) {
-                        $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel]));
+                        $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel]));
                     }
 
                     $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE;
-                    return true;
+                    $this->channelCount--;
+
+                    if ($client_channel == $channel) {
+                        return true;
+                    }
+                    // fall-through
                 case NET_SSH2_MSG_CHANNEL_EOF:
                     break;
                 default:
-                    user_error('Error reading channel data');
-                    return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                    $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
+                    throw new \RuntimeException("Error reading channel data ($type)");
             }
         }
     }
@@ -3131,60 +4270,148 @@ class Net_SSH2
      *
      * See '6. Binary Packet Protocol' of rfc4253 for more info.
      *
-     * @param String $data
-     * @param optional String $logged
-     * @see Net_SSH2::_get_binary_packet()
-     * @return Boolean
-     * @access private
+     * @param string $data
+     * @param string $logged
+     * @see self::_get_binary_packet()
+     * @return void
      */
-    function _send_binary_packet($data, $logged = null)
+    protected function send_binary_packet($data, $logged = null)
     {
         if (!is_resource($this->fsock) || feof($this->fsock)) {
-            user_error('Connection closed prematurely');
             $this->bitmap = 0;
-            return false;
+            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;
+                }
+                // fall-through
+            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);
+                }
         }
 
-        //if ($this->compress) {
-        //    // the -4 removes the checksum:
-        //    // http://php.net/function.gzcompress#57710
-        //    $data = substr(gzcompress($data), 0, -4);
-        //}
-
         // 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;
+        $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;
-        $padding = crypt_random_string($padding_length);
+        switch (true) {
+            case $this->encrypt && $this->encrypt->usesNonce():
+            case $this->hmac_create instanceof Hash && $this->hmac_create_etm:
+                $padding_length += 4;
+                $packet_length += 4;
+        }
+
+        $padding = Random::string($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 = $this->hmac_create !== false ? $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)) : '';
-        $this->send_seq_no++;
+        $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 \LogicException('$this->encrypt is not a ' . 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->encrypt !== false) {
-            $packet = $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));
+            }
         }
 
-        $packet.= $hmac;
+        $this->send_seq_no++;
+
+        $packet .= $this->encrypt && $this->encrypt->usesNonce() ? $this->encrypt->getTag() : $hmac;
 
-        $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838
-        $result = strlen($packet) == fputs($this->fsock, $packet);
-        $stop = strtok(microtime(), ' ') + strtok('');
+        $start = microtime(true);
+        $sent = @fputs($this->fsock, $packet);
+        $stop = microtime(true);
 
         if (defined('NET_SSH2_LOGGING')) {
-            $current = strtok(microtime(), ' ') + strtok('');
-            $message_number = isset($this->message_numbers[ord($data[0])]) ? $this->message_numbers[ord($data[0])] : 'UNKNOWN (' . ord($data[0]) . ')';
+            $current = microtime(true);
+            $message_number = isset(self::$message_numbers[ord($logged[0])]) ? self::$message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')';
             $message_number = '-> ' . $message_number .
                               ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)';
-            $this->_append_log($message_number, isset($logged) ? $logged : $data);
+            $this->append_log($message_number, $logged);
             $this->last_packet = $current;
         }
 
-        return $result;
+        if (strlen($packet) != $sent) {
+            $this->bitmap = 0;
+            $message = $sent === false ?
+                'Unable to write ' . strlen($packet) . ' bytes' :
+                "Only $sent of " . strlen($packet) . " bytes were sent";
+            throw new \RuntimeException($message);
+        }
     }
 
     /**
@@ -3192,35 +4419,69 @@ class Net_SSH2
      *
      * Makes sure that only the last 1MB worth of packets will be logged
      *
-     * @param String $data
-     * @access private
+     * @param string $message_number
+     * @param string $message
+     */
+    private function append_log($message_number, $message)
+    {
+        $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 int $constant
+     * @param string $message_number
+     * @param string $message
+     * @param array &$message_number_log
+     * @param array &$message_log
+     * @param int &$log_size
+     * @param resource &$realtime_log_file
+     * @param bool &$realtime_log_wrap
+     * @param int &$realtime_log_size
      */
-    function _append_log($message_number, $message)
+    protected function append_log_helper($constant, $message_number, $message, array &$message_number_log, array &$message_log, &$log_size, &$realtime_log_file, &$realtime_log_wrap, &$realtime_log_size)
     {
         // remove the byte identifying the message type from all but the first two messages (ie. the identification strings)
         if (strlen($message_number) > 2) {
-            $this->_string_shift($message);
+            Strings::shift($message);
         }
 
-        switch (NET_SSH2_LOGGING) {
+        switch ($constant) {
             // useful for benchmarks
-            case NET_SSH2_LOG_SIMPLE:
-                $this->message_number_log[] = $message_number;
+            case self::LOG_SIMPLE:
+                $message_number_log[] = $message_number;
+                break;
+            case self::LOG_SIMPLE_REALTIME:
+                echo $message_number;
+                echo PHP_SAPI == 'cli' ? "\r\n" : '<br>';
+                @flush();
+                @ob_flush();
                 break;
             // the most useful log for SSH2
-            case NET_SSH2_LOG_COMPLEX:
-                $this->message_number_log[] = $message_number;
-                $this->log_size+= strlen($message);
-                $this->message_log[] = $message;
-                while ($this->log_size > NET_SSH2_LOG_MAX_SIZE) {
-                    $this->log_size-= strlen(array_shift($this->message_log));
-                    array_shift($this->message_number_log);
+            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 NET_SSH2_LOG_REALTIME:
+            case self::LOG_REALTIME:
                 switch (PHP_SAPI) {
                     case 'cli':
                         $start = $stop = "\r\n";
@@ -3229,37 +4490,37 @@ class Net_SSH2
                         $start = '<pre>';
                         $stop = '</pre>';
                 }
-                echo $start . $this->_format_log(array($message), array($message_number)) . $stop;
+                echo $start . $this->format_log([$message], [$message_number]) . $stop;
                 @flush();
                 @ob_flush();
                 break;
-            // basically the same thing as NET_SSH2_LOG_REALTIME with the caveat that NET_SSH2_LOG_REALTIME_FILE
-            // needs to be defined and that the resultant log file will be capped out at NET_SSH2_LOG_MAX_SIZE.
+            // 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 NET_SSH2_LOG_REALTIME_FILE:
-                if (!isset($this->realtime_log_file)) {
+            case self::LOG_REALTIME_FILE:
+                if (!isset($realtime_log_file)) {
                     // PHP doesn't seem to like using constants in fopen()
                     $filename = NET_SSH2_LOG_REALTIME_FILENAME;
                     $fp = fopen($filename, 'w');
-                    $this->realtime_log_file = $fp;
+                    $realtime_log_file = $fp;
                 }
-                if (!is_resource($this->realtime_log_file)) {
+                if (!is_resource($realtime_log_file)) {
                     break;
                 }
-                $entry = $this->_format_log(array($message), array($message_number));
-                if ($this->realtime_log_wrap) {
+                $entry = $this->format_log([$message], [$message_number]);
+                if ($realtime_log_wrap) {
                     $temp = "<<< START >>>\r\n";
-                    $entry.= $temp;
-                    fseek($this->realtime_log_file, ftell($this->realtime_log_file) - strlen($temp));
+                    $entry .= $temp;
+                    fseek($realtime_log_file, ftell($realtime_log_file) - strlen($temp));
                 }
-                $this->realtime_log_size+= strlen($entry);
-                if ($this->realtime_log_size > NET_SSH2_LOG_MAX_SIZE) {
-                    fseek($this->realtime_log_file, 0);
-                    $this->realtime_log_size = strlen($entry);
-                    $this->realtime_log_wrap = true;
+                $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;
                 }
-                fputs($this->realtime_log_file, $entry);
+                fputs($realtime_log_file, $entry);
         }
     }
 
@@ -3268,19 +4529,18 @@ class Net_SSH2
      *
      * Spans multiple SSH_MSG_CHANNEL_DATAs if appropriate
      *
-     * @param Integer $client_channel
-     * @param String $data
-     * @return Boolean
-     * @access private
+     * @param int $client_channel
+     * @param string $data
+     * @return void
      */
-    function _send_channel_packet($client_channel, $data)
+    protected function send_channel_packet($client_channel, $data)
     {
         while (strlen($data)) {
             if (!$this->window_size_client_to_server[$client_channel]) {
-                $this->bitmap^= NET_SSH2_MASK_WINDOW_ADJUST;
+                $this->bitmap ^= self::MASK_WINDOW_ADJUST;
                 // using an invalid channel will let the buffers be built up for the valid channels
-                $this->_get_channel_packet(-1);
-                $this->bitmap^= NET_SSH2_MASK_WINDOW_ADJUST;
+                $this->get_channel_packet(-1);
+                $this->bitmap ^= self::MASK_WINDOW_ADJUST;
             }
 
             /* The maximum amount of data allowed is determined by the maximum
@@ -3292,92 +4552,95 @@ class Net_SSH2
                 $this->window_size_client_to_server[$client_channel]
             );
 
-            $temp = $this->_string_shift($data, $max_size);
-            $packet = pack('CN2a*',
+            $temp = Strings::shift($data, $max_size);
+            $packet = Strings::packSSH2(
+                'CNs',
                 NET_SSH2_MSG_CHANNEL_DATA,
                 $this->server_channels[$client_channel],
-                strlen($temp),
                 $temp
             );
-            $this->window_size_client_to_server[$client_channel]-= strlen($temp);
-            if (!$this->_send_binary_packet($packet)) {
-                return false;
-            }
+            $this->window_size_client_to_server[$client_channel] -= strlen($temp);
+            $this->send_binary_packet($packet);
         }
-
-        return true;
     }
 
     /**
      * Closes and flushes a channel
      *
-     * Net_SSH2 doesn't properly close most channels.  For exec() channels are normally closed by the server
+     * \phpseclib3\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.
      *
-     * @param Integer $client_channel
-     * @param Boolean $want_reply
-     * @return Boolean
-     * @access private
+     * @param int $client_channel
+     * @param bool $want_reply
+     * @return void
      */
-    function _close_channel($client_channel, $want_reply = false)
+    private function close_channel($client_channel, $want_reply = false)
     {
         // see http://tools.ietf.org/html/rfc4254#section-5.3
 
-        $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel]));
+        $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel]));
 
         if (!$want_reply) {
-            $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel]));
+            $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel]));
         }
 
         $this->channel_status[$client_channel] = NET_SSH2_MSG_CHANNEL_CLOSE;
+        $this->channelCount--;
 
-        $this->curTimeout = 0;
+        $this->curTimeout = 5;
 
-        while (!is_bool($this->_get_channel_packet($client_channel)));
+        while (!is_bool($this->get_channel_packet($client_channel))) {
+        }
 
         if ($want_reply) {
-            $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel]));
+            $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel]));
         }
 
-        if ($this->bitmap & NET_SSH2_MASK_SHELL) {
-            $this->bitmap&= ~NET_SSH2_MASK_SHELL;
-        }
+        $this->close_channel_bitmap($client_channel);
     }
 
     /**
-     * Disconnect
+     * Maintains execution state bitmap in response to channel closure
      *
-     * @param Integer $reason
-     * @return Boolean
-     * @access private
+     * @param int $client_channel The channel number to maintain closure status of
+     * @return void
      */
-    function _disconnect($reason)
+    private function close_channel_bitmap($client_channel)
     {
-        if ($this->bitmap & NET_SSH2_MASK_CONNECTED) {
-            $data = pack('CNNa*Na*', NET_SSH2_MSG_DISCONNECT, $reason, 0, '', 0, '');
-            $this->_send_binary_packet($data);
-            $this->bitmap = 0;
-            fclose($this->fsock);
-            return false;
+        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;
         }
     }
 
     /**
-     * String Shift
-     *
-     * Inspired by array_shift
+     * Disconnect
      *
-     * @param String $string
-     * @param optional Integer $index
-     * @return String
-     * @access private
+     * @param int $reason
+     * @return false
      */
-    function _string_shift(&$string, $index = 1)
+    protected function disconnect_helper($reason)
     {
-        $substr = substr($string, 0, $index);
-        $string = substr($string, $index);
-        return $substr;
+        if ($this->bitmap & self::MASK_CONNECTED) {
+            $data = Strings::packSSH2('CNss', NET_SSH2_MSG_DISCONNECT, $reason, '', '');
+            try {
+                $this->send_binary_packet($data);
+            } catch (\Exception $e) {
+            }
+        }
+
+        $this->bitmap = 0;
+        if (is_resource($this->fsock) && get_resource_type($this->fsock) === 'stream') {
+            fclose($this->fsock);
+        }
+
+        return false;
     }
 
     /**
@@ -3387,14 +4650,13 @@ class Net_SSH2
      * named constants from it, using the value as the name of the constant and the index as the value of the constant.
      * If any of the constants that would be defined already exists, none of the constants will be defined.
      *
-     * @param Array $array
-     * @access private
+     * @param mixed[] ...$args
+     * @access protected
      */
-    function _define_array()
+    protected static function define_array(...$args)
     {
-        $args = func_get_args();
         foreach ($args as $arg) {
-            foreach ($arg as $key=>$value) {
+            foreach ($arg as $key => $value) {
                 if (!defined($value)) {
                     define($value, $key);
                 } else {
@@ -3407,24 +4669,22 @@ class Net_SSH2
     /**
      * Returns a log of the packets that have been sent and received.
      *
-     * Returns a string if NET_SSH2_LOGGING == NET_SSH2_LOG_COMPLEX, an array if NET_SSH2_LOGGING == NET_SSH2_LOG_SIMPLE and false if !defined('NET_SSH2_LOGGING')
+     * 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')
      *
-     * @access public
-     * @return String or Array
+     * @return array|false|string
      */
-    function getLog()
+    public function getLog()
     {
         if (!defined('NET_SSH2_LOGGING')) {
             return false;
         }
 
         switch (NET_SSH2_LOGGING) {
-            case NET_SSH2_LOG_SIMPLE:
+            case self::LOG_SIMPLE:
                 return $this->message_number_log;
-                break;
-            case NET_SSH2_LOG_COMPLEX:
-                return $this->_format_log($this->message_log, $this->message_number_log);
-                break;
+            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 false;
         }
@@ -3433,214 +4693,475 @@ class Net_SSH2
     /**
      * Formats a log for printing
      *
-     * @param Array $message_log
-     * @param Array $message_number_log
-     * @access private
-     * @return String
+     * @param array $message_log
+     * @param array $message_number_log
+     * @return string
      */
-    function _format_log($message_log, $message_number_log)
+    protected function format_log(array $message_log, array $message_number_log)
     {
         $output = '';
         for ($i = 0; $i < count($message_log); $i++) {
-            $output.= $message_number_log[$i] . "\r\n";
+            $output .= $message_number_log[$i] . "\r\n";
             $current_log = $message_log[$i];
             $j = 0;
             do {
                 if (strlen($current_log)) {
-                    $output.= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0  ';
+                    $output .= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0  ';
                 }
-                $fragment = $this->_string_shift($current_log, $this->log_short_width);
-                $hex = substr(preg_replace_callback('#.#s', array($this, '_format_log_helper'), $fragment), strlen($this->log_boundary));
+                $fragment = Strings::shift($current_log, $this->log_short_width);
+                $hex = substr(preg_replace_callback('#.#s', function ($matches) {
+                    return $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";
+                $output .= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n";
                 $j++;
             } while (strlen($current_log));
-            $output.= "\r\n";
+            $output .= "\r\n";
         }
 
         return $output;
     }
 
     /**
-     * Helper function for _format_log
+     * Helper function for agent->on_channel_open()
      *
-     * For use with preg_replace_callback()
+     * Used when channels are created to inform agent
+     * of said channel opening. Must be called after
+     * channel open confirmation received
      *
-     * @param Array $matches
-     * @access private
-     * @return String
      */
-    function _format_log_helper($matches)
+    private function on_channel_open()
     {
-        return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT);
+        if (isset($this->agent)) {
+            $this->agent->registerChannelOpen($this);
+        }
     }
 
     /**
-     * Returns all errors
+     * Returns the first value of the intersection of two arrays or false if
+     * the intersection is empty. The order is defined by the first parameter.
      *
-     * @return String
-     * @access public
+     * @param array $array1
+     * @param array $array2
+     * @return mixed False if intersection is empty, else intersected value.
      */
-    function getErrors()
+    private static function array_intersect_first(array $array1, array $array2)
     {
-        return $this->errors;
+        foreach ($array1 as $value) {
+            if (in_array($value, $array2)) {
+                return $value;
+            }
+        }
+        return false;
     }
 
     /**
-     * Returns the last error
+     * Returns all errors / debug messages on the SSH layer
+     *
+     * If you are looking for messages from the SFTP layer, please see SFTP::getSFTPErrors()
      *
-     * @return String
-     * @access public
+     * @return string[]
      */
-    function getLastError()
+    public function getErrors()
     {
-        return $this->errors[count($this->errors) - 1];
+        return $this->errors;
     }
 
     /**
-     * Return the server identification.
+     * Returns the last error received on the SSH layer
      *
-     * @return String
-     * @access public
+     * If you are looking for messages from the SFTP layer, please see SFTP::getLastSFTPError()
+     *
+     * @return string
      */
-    function getServerIdentification()
+    public function getLastError()
     {
-        $this->_connect();
+        $count = count($this->errors);
 
-        return $this->server_identifier;
+        if ($count > 0) {
+            return $this->errors[$count - 1];
+        }
     }
 
     /**
-     * Return a list of the key exchange algorithms the server supports.
+     * Return the server identification.
      *
-     * @return Array
-     * @access public
+     * @return string|false
      */
-    function getKexAlgorithms()
+    public function getServerIdentification()
     {
-        $this->_connect();
+        $this->connect();
 
-        return $this->kex_algorithms;
+        return $this->server_identifier;
     }
 
     /**
-     * Return a list of the host key (public key) algorithms the server supports.
+     * Returns a list of algorithms the server supports
      *
-     * @return Array
-     * @access public
+     * @return array
      */
-    function getServerHostKeyAlgorithms()
+    public function getServerAlgorithms()
     {
-        $this->_connect();
-
-        return $this->server_host_key_algorithms;
+        $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
+            ]
+        ];
     }
 
     /**
-     * Return a list of the (symmetric key) encryption algorithms the server supports, when receiving stuff from the client.
+     * Returns a list of KEX algorithms that phpseclib supports
      *
-     * @return Array
-     * @access public
+     * @return array
      */
-    function getEncryptionAlgorithmsClient2Server()
+    public static function getSupportedKEXAlgorithms()
     {
-        $this->_connect();
+        $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 $this->encryption_algorithms_client_to_server;
+        return $kex_algorithms;
     }
 
     /**
-     * Return a list of the (symmetric key) encryption algorithms the server supports, when sending stuff to the client.
+     * Returns a list of host key algorithms that phpseclib supports
      *
-     * @return Array
-     * @access public
+     * @return array
      */
-    function getEncryptionAlgorithmsServer2Client()
+    public static function getSupportedHostKeyAlgorithms()
     {
-        $this->_connect();
-
-        return $this->encryption_algorithms_server_to_client;
+        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
+        ];
     }
 
     /**
-     * Return a list of the MAC algorithms the server supports, when receiving stuff from the client.
+     * Returns a list of symmetric key algorithms that phpseclib supports
      *
-     * @return Array
-     * @access public
+     * @return array
      */
-    function getMACAlgorithmsClient2Server()
+    public static function getSupportedEncryptionAlgorithms()
     {
-        $this->_connect();
+        $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',
 
-        return $this->mac_algorithms_client_to_server;
+            //'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 or mcrypt are 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 (GCM)',
+                'OpenSSL',
+                'mcrypt',
+                '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(preg_replace('#[^\d]#', '', $algo));
+                }
+                switch ($algo) {
+                    case 'chacha20-poly1305@openssh.com':
+                    case 'arcfour128':
+                    case 'arcfour256':
+                        if ($engine != 'Eval') {
+                            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;
     }
 
     /**
-     * Return a list of the MAC algorithms the server supports, when sending stuff to the client.
+     * Returns a list of MAC algorithms that phpseclib supports
      *
-     * @return Array
-     * @access public
+     * @return array
      */
-    function getMACAlgorithmsServer2Client()
+    public static function getSupportedMACAlgorithms()
     {
-        $this->_connect();
+        return [
+            'hmac-sha2-256-etm@openssh.com',
+            'hmac-sha2-512-etm@openssh.com',
+            'umac-64-etm@openssh.com',
+            'umac-128-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)
 
-        return $this->mac_algorithms_server_to_client;
+            // from <https://tools.ietf.org/html/draft-miller-secsh-umac-01>:
+            'umac-64@openssh.com',
+            'umac-128@openssh.com',
+
+            '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)
+            //'none'          // OPTIONAL        no MAC; NOT RECOMMENDED
+        ];
     }
 
     /**
-     * Return a list of the compression algorithms the server supports, when receiving stuff from the client.
+     * Returns a list of compression algorithms that phpseclib supports
      *
-     * @return Array
-     * @access public
+     * @return array
      */
-    function getCompressionAlgorithmsClient2Server()
+    public static function getSupportedCompressionAlgorithms()
     {
-        $this->_connect();
-
-        return $this->compression_algorithms_client_to_server;
+        $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 a list of the compression algorithms the server supports, when sending stuff to the client.
+     * Return list of negotiated algorithms
      *
-     * @return Array
-     * @access public
+     * Uses the same format as https://www.php.net/ssh2-methods-negotiated
+     *
+     * @return array
      */
-    function getCompressionAlgorithmsServer2Client()
+    public function getAlgorithmsNegotiated()
     {
-        $this->_connect();
+        $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],
+            ]
+        ];
+    }
 
-        return $this->compression_algorithms_server_to_client;
+    /**
+     * Force multiple channels (even if phpseclib has decided to disable them)
+     */
+    public function forceMultipleChannels()
+    {
+        $this->errorOnMultipleChannels = false;
     }
 
     /**
-     * Return a list of the languages the server supports, when sending stuff to the client.
+     * Allows you to set the terminal
      *
-     * @return Array
-     * @access public
+     * @param string $term
      */
-    function getLanguagesServer2Client()
+    public function setTerminal($term)
     {
-        $this->_connect();
-
-        return $this->languages_server_to_client;
+        $this->term = $term;
     }
 
     /**
-     * Return a list of the languages the server supports, when receiving stuff from the client.
+     * Accepts an associative array with up to four parameters as described at
+     * <https://www.php.net/manual/en/function.ssh2-connect.php>
      *
-     * @return Array
-     * @access public
+     * @param array $methods
      */
-    function getLanguagesClient2Server()
+    public function setPreferredAlgorithms(array $methods)
     {
-        $this->_connect();
+        $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()
+            );
+        }
+
+        $keys = ['client_to_server', 'server_to_client'];
+        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;
 
-        return $this->languages_client_to_server;
+            $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;
     }
 
     /**
@@ -3649,10 +5170,9 @@ class Net_SSH2
      * Quoting from the RFC, "in some jurisdictions, sending a warning message before
      * authentication may be relevant for getting legal protection."
      *
-     * @return String
-     * @access public
+     * @return string
      */
-    function getBannerMessage()
+    public function getBannerMessage()
     {
         return $this->banner_message;
     }
@@ -3663,155 +5183,95 @@ class Net_SSH2
      * 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.
      *
-     * @return Mixed
-     * @access public
+     * @return string|false
+     * @throws \RuntimeException on badly formatted keys
+     * @throws \phpseclib3\Exception\NoSupportedAlgorithmsException when the key isn't in a supported format
      */
-    function getServerPublicHostKey()
+    public function getServerPublicHostKey()
     {
-        if (!($this->bitmap & NET_SSH2_MASK_CONSTRUCTOR)) {
-            if (!$this->_connect()) {
-                return false;
-            }
+        if (!($this->bitmap & self::MASK_CONSTRUCTOR)) {
+            $this->connect();
         }
 
         $signature = $this->signature;
-        $server_public_host_key = $this->server_public_host_key;
-
-        extract(unpack('Nlength', $this->_string_shift($server_public_host_key, 4)));
-        $this->_string_shift($server_public_host_key, $length);
+        $server_public_host_key = base64_encode($this->server_public_host_key);
 
         if ($this->signature_validated) {
             return $this->bitmap ?
-                $this->signature_format . ' ' . base64_encode($this->server_public_host_key) :
+                $this->signature_format . ' ' . $server_public_host_key :
                 false;
         }
 
         $this->signature_validated = true;
 
         switch ($this->signature_format) {
-            case 'ssh-dss':
-                $zero = new Math_BigInteger();
-
-                $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4));
-                $p = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256);
-
-                $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4));
-                $q = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256);
-
-                $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4));
-                $g = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256);
-
-                $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4));
-                $y = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256);
-
-                /* The value for 'dss_signature_blob' is encoded as a string containing
-                   r, followed by s (which are 160-bit integers, without lengths or
-                   padding, unsigned, and in network byte order). */
-                $temp = unpack('Nlength', $this->_string_shift($signature, 4));
-                if ($temp['length'] != 40) {
-                    user_error('Invalid signature');
-                    return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
-                }
-
-                $r = new Math_BigInteger($this->_string_shift($signature, 20), 256);
-                $s = new Math_BigInteger($this->_string_shift($signature, 20), 256);
-
-                switch (true) {
-                    case $r->equals($zero):
-                    case $r->compare($q) >= 0:
-                    case $s->equals($zero):
-                    case $s->compare($q) >= 0:
-                        user_error('Invalid signature');
-                        return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
-                }
-
-                $w = $s->modInverse($q);
-
-                $u1 = $w->multiply(new Math_BigInteger(sha1($this->exchange_hash), 16));
-                list(, $u1) = $u1->divide($q);
-
-                $u2 = $w->multiply($r);
-                list(, $u2) = $u2->divide($q);
-
-                $g = $g->modPow($u1, $p);
-                $y = $y->modPow($u2, $p);
-
-                $v = $g->multiply($y);
-                list(, $v) = $v->divide($p);
-                list(, $v) = $v->divide($q);
-
-                if (!$v->equals($r)) {
-                    user_error('Bad server signature');
-                    return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
+            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');
+                switch ($this->signature_format) {
+                    case 'ssh-ed25519':
+                        $hash = 'sha512';
+                        break;
+                    case 'ecdsa-sha2-nistp256':
+                        $hash = 'sha256';
+                        break;
+                    case 'ecdsa-sha2-nistp384':
+                        $hash = 'sha384';
+                        break;
+                    case 'ecdsa-sha2-nistp521':
+                        $hash = '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':
-                $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4));
-                $e = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256);
-
-                $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4));
-                $rawN = $this->_string_shift($server_public_host_key, $temp['length']);
-                $n = new Math_BigInteger($rawN, -256);
-                $nLength = strlen(ltrim($rawN, "\0"));
-
-                /*
-                $temp = unpack('Nlength', $this->_string_shift($signature, 4));
-                $signature = $this->_string_shift($signature, $temp['length']);
-
-                if (!class_exists('Crypt_RSA')) {
-                    include_once 'Crypt/RSA.php';
-                }
-
-                $rsa = new Crypt_RSA();
-                $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
-                $rsa->loadKey(array('e' => $e, 'n' => $n), CRYPT_RSA_PUBLIC_FORMAT_RAW);
-                if (!$rsa->verify($this->exchange_hash, $signature)) {
-                    user_error('Bad server signature');
-                    return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
-                }
-                */
-
-                $temp = unpack('Nlength', $this->_string_shift($signature, 4));
-                $s = new Math_BigInteger($this->_string_shift($signature, $temp['length']), 256);
-
-                // validate an RSA signature per "8.2 RSASSA-PKCS1-v1_5", "5.2.2 RSAVP1", and "9.1 EMSA-PSS" in the
-                // following URL:
-                // ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1.pdf
-
-                // also, see SSHRSA.c (rsa2_verifysig) in PuTTy's source.
-
-                if ($s->compare(new Math_BigInteger()) < 0 || $s->compare($n->subtract(new Math_BigInteger(1))) > 0) {
-                    user_error('Invalid signature');
-                    return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
-                }
-
-                $s = $s->modPow($e, $n);
-                $s = $s->toBytes();
-
-                $h = pack('N4H*', 0x00302130, 0x0906052B, 0x0E03021A, 0x05000414, sha1($this->exchange_hash));
-                $h = chr(0x01) . str_repeat(chr(0xFF), $nLength - 2 - strlen($h)) . $h;
-
-                if ($s != $h) {
-                    user_error('Bad server signature');
-                    return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
+            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
+                list(, $signature) = Strings::unpackSSH2('ss', $signature);
+
+                $key = RSA::loadFormat('OpenSSH', $server_public_host_key)
+                    ->withPadding(RSA::SIGNATURE_PKCS1);
+                switch ($this->signature_format) {
+                    case 'rsa-sha2-512':
+                        $hash = 'sha512';
+                        break;
+                    case 'rsa-sha2-256':
+                        $hash = 'sha256';
+                        break;
+                    //case 'ssh-rsa':
+                    default:
+                        $hash = 'sha1';
                 }
+                $key = $key->withHash($hash);
                 break;
             default:
-                user_error('Unsupported signature format');
-                return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
+                $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
+                throw new NoSupportedAlgorithmsException('Unsupported signature format');
         }
 
-        return $this->signature_format . ' ' . base64_encode($this->server_public_host_key);
+        if (!$key->verify($this->exchange_hash, $signature)) {
+            return $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE);
+        };
+
+        return $this->signature_format . ' ' . $server_public_host_key;
     }
 
     /**
      * Returns the exit status of an SSH command or false.
      *
-     * @return Integer or false
-     * @access public
+     * @return false|int
      */
-    function getExitStatus()
+    public function getExitStatus()
     {
         if (is_null($this->exit_status)) {
             return false;
@@ -3822,10 +5282,9 @@ class Net_SSH2
     /**
      * Returns the number of columns for the terminal window size.
      *
-     * @return Integer
-     * @access public
+     * @return int
      */
-    function getWindowColumns()
+    public function getWindowColumns()
     {
         return $this->windowColumns;
     }
@@ -3833,10 +5292,9 @@ class Net_SSH2
     /**
      * Returns the number of rows for the terminal window size.
      *
-     * @return Integer
-     * @access public
+     * @return int
      */
-    function getWindowRows()
+    public function getWindowRows()
     {
         return $this->windowRows;
     }
@@ -3844,10 +5302,9 @@ class Net_SSH2
     /**
      * Sets the number of columns for the terminal window size.
      *
-     * @param Integer $value
-     * @access public
+     * @param int $value
      */
-    function setWindowColumns($value)
+    public function setWindowColumns($value)
     {
         $this->windowColumns = $value;
     }
@@ -3855,10 +5312,9 @@ class Net_SSH2
     /**
      * Sets the number of rows for the terminal window size.
      *
-     * @param Integer $value
-     * @access public
+     * @param int $value
      */
-    function setWindowRows($value)
+    public function setWindowRows($value)
     {
         $this->windowRows = $value;
     }
@@ -3866,13 +5322,115 @@ class Net_SSH2
     /**
      * Sets the number of columns and rows for the terminal window size.
      *
-     * @param Integer $columns
-     * @param Integer $rows
-     * @access public
+     * @param int $columns
+     * @param int $rows
      */
-    function setWindowSize($columns = 80, $rows = 24)
+    public function setWindowSize($columns = 80, $rows = 24)
     {
         $this->windowColumns = $columns;
         $this->windowRows = $rows;
     }
+
+    /**
+     * To String Magic Method
+     *
+     * @return string
+     */
+    #[\ReturnTypeWillChange]
+    public function __toString()
+    {
+        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.
+     *
+     * @return string
+     */
+    public function getResourceId()
+    {
+        return '{' . spl_object_hash($this) . '}';
+    }
+
+    /**
+     * Return existing connection
+     *
+     * @param string $id
+     *
+     * @return bool|SSH2 will return false if no such connection
+     */
+    public static function getConnectionByResourceId($id)
+    {
+        if (isset(self::$connections[$id])) {
+            return self::$connections[$id] instanceof \WeakReference ? self::$connections[$id]->get() : self::$connections[$id];
+        }
+        return false;
+    }
+
+    /**
+     * Return all excising connections
+     *
+     * @return array<string, SSH2>
+     */
+    public static function getConnections()
+    {
+        if (!class_exists('WeakReference')) {
+            /** @var array<string, SSH2> */
+            return self::$connections;
+        }
+        $temp = [];
+        foreach (self::$connections as $key => $ref) {
+            $temp[$key] = $ref->get();
+        }
+        return $temp;
+    }
+
+    /*
+     * Update packet types in log history
+     *
+     * @param string $old
+     * @param string $new
+     */
+    private function updateLogHistory($old, $new)
+    {
+        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
+     * @return array|null
+     */
+    public function getAuthMethodsToContinue()
+    {
+        return $this->auth_methods_to_continue;
+    }
+
+    /**
+     * Enables "smart" multi-factor authentication (MFA)
+     */
+    public function enableSmartMFA()
+    {
+        $this->smartMFA = true;
+    }
+
+    /**
+     * Disables "smart" multi-factor authentication (MFA)
+     */
+    public function disableSmartMFA()
+    {
+        $this->smartMFA = false;
+    }
 }