]> git.mxchange.org Git - friendica-addons.git/commitdiff
[securemail] Update Composer dependency ahead of release
authorHypolite Petovan <hypolite@mrpetovan.com>
Wed, 20 Mar 2024 02:08:59 +0000 (22:08 -0400)
committerHypolite Petovan <hypolite@mrpetovan.com>
Wed, 20 Mar 2024 03:11:09 +0000 (23:11 -0400)
- Updating phpseclib/phpseclib (3.0.19 => 3.0.37)

23 files changed:
securemail/composer.json
securemail/composer.lock
securemail/vendor/composer/installed.json
securemail/vendor/phpseclib/phpseclib/BACKERS.md
securemail/vendor/phpseclib/phpseclib/README.md
securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/AsymmetricKey.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/Formats/Keys/PKCS8.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/Common/SymmetricKey.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/IEEE.php [new file with mode: 0644]
securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/PrivateKey.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/RSA.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php
securemail/vendor/phpseclib/phpseclib/phpseclib/File/ASN1.php
securemail/vendor/phpseclib/phpseclib/phpseclib/File/X509.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/Engine.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP32.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Math/BigInteger/Engines/PHP64.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Math/BinaryField.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Math/PrimeField/Integer.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Net/SFTP.php
securemail/vendor/phpseclib/phpseclib/phpseclib/Net/SSH2.php

index 4c7cb54b00dcf90bf2c340da7484ac5ff9fedd08..9383d645af59d1131768d0121db7b057360cfeb1 100644 (file)
@@ -1,26 +1,26 @@
 {
-  "name": "friendica-addons/securemail",
-  "description": "Send notification mail encrypted with user-defined public GPG key.",
-  "type": "friendica-addon",
-  "authors": [
-    {
-      "name": "Fabio Comuni",
-      "homepage": "https://kirgroup.com/profile/fabrixxm",
-      "role": "Developer"
+    "name": "friendica-addons/securemail",
+    "description": "Send notification mail encrypted with user-defined public GPG key.",
+    "type": "friendica-addon",
+    "authors": [
+        {
+            "name": "Fabio Comuni",
+            "homepage": "https://kirgroup.com/profile/fabrixxm",
+            "role": "Developer"
+        }
+    ],
+    "require": {
+        "singpolyma/openpgp-php": "0.6.0"
+    },
+    "replace": {
+        "paragonie/random_compat": "9.99.99"
+    },
+    "license": "AGPL-3.0+",
+    "minimum-stability": "stable",
+    "config": {
+        "autoloader-suffix": "SecuremailAddon",
+        "platform": {
+            "php": "7.3"
+        }
     }
-  ],
-  "require": {
-    "singpolyma/openpgp-php": "0.6.0"
-  },
-  "replace": {
-    "paragonie/random_compat": "9.99.99"
-  },
-  "license": "AGPL-3.0+",
-  "minimum-stability": "stable",
-  "config": {
-    "autoloader-suffix": "SecuremailAddon",
-    "platform": {
-      "php": "7.3"
-    }
-  }
 }
index 7934eebcfd7b824eea3ef46a77a494530fe60f58..8960cfcb56df8d496dfa997e91895b7dfb8764fe 100644 (file)
         },
         {
             "name": "phpseclib/phpseclib",
-            "version": "3.0.19",
+            "version": "3.0.37",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpseclib/phpseclib.git",
-                "reference": "cc181005cf548bfd8a4896383bb825d859259f95"
+                "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cc181005cf548bfd8a4896383bb825d859259f95",
-                "reference": "cc181005cf548bfd8a4896383bb825d859259f95",
+                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
+                "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-03-05T17:13:09+00:00"
+            "time": "2024-03-03T02:14:58+00:00"
         },
         {
             "name": "singpolyma/openpgp-php",
index 25c1aae4f0e165f4752565dab7412153a00c27be..680a3ffe17002386b43486bdd77f25fceb8e1651 100644 (file)
     },
     {
         "name": "phpseclib/phpseclib",
-        "version": "3.0.19",
-        "version_normalized": "3.0.19.0",
+        "version": "3.0.37",
+        "version_normalized": "3.0.37.0",
         "source": {
             "type": "git",
             "url": "https://github.com/phpseclib/phpseclib.git",
-            "reference": "cc181005cf548bfd8a4896383bb825d859259f95"
+            "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cc181005cf548bfd8a4896383bb825d859259f95",
-            "reference": "cc181005cf548bfd8a4896383bb825d859259f95",
+            "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
+            "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8",
             "shasum": ""
         },
         "require": {
@@ -93,7 +93,7 @@
             "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
             "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
         },
-        "time": "2023-03-05T17:13:09+00:00",
+        "time": "2024-03-03T02:14:58+00:00",
         "type": "library",
         "installation-source": "dist",
         "autoload": {
index 5e5c6d99bb71fffdd09a045832155d80b650afb8..efca482ada32a00413b97ff0d7c88e979b0bf882 100644 (file)
@@ -12,4 +12,6 @@ phpseclib ongoing development is made possible by [Tidelift](https://tidelift.co
 - [Charles Severance](https://github.com/csev)
 - [Rachel Fish](https://github.com/itsrachelfish)
 - Tharyrok
-- [cjhaas](https://github.com/cjhaas)
\ No newline at end of file
+- [cjhaas](https://github.com/cjhaas)
+- [istiak-tridip](https://github.com/istiak-tridip)
+- [Anna Filina](https://github.com/afilina)
\ No newline at end of file
index 64c06ba28b359496f9dac0cb776613ffcf7517a9..37cbcb9d5609f1c44b22667fe3b53ae64510f019 100644 (file)
@@ -51,8 +51,7 @@ SSH-2, SFTP, X.509, an arbitrary-precision integer arithmetic library, Ed25519 /
 * PHP4 compatible
 * Composer compatible (PSR-0 autoloading)
 * Install using Composer: `composer require phpseclib/phpseclib:~1.0`
-* Install using PEAR: See [phpseclib PEAR Channel Documentation](http://phpseclib.sourceforge.net/pear.htm)
-* [Download 1.0.20 as ZIP](http://sourceforge.net/projects/phpseclib/files/phpseclib1.0.20.zip/download)
+* [Download 1.0.23 as ZIP](http://sourceforge.net/projects/phpseclib/files/phpseclib1.0.23.zip/download)
 
 ## Security contact information
 
index 407f0369935dbf18de7b37491f3d13384a76a3ee..09eb5b1b37df3da45dd061b2baaab819a4b66a93 100644 (file)
@@ -94,7 +94,7 @@ abstract class AsymmetricKey
 
     /**
      * @param string $type
-     * @return string
+     * @return array|string
      */
     abstract public function toString($type, array $options = []);
 
@@ -130,7 +130,7 @@ abstract class AsymmetricKey
      *
      * @param string $key
      * @param string $password optional
-     * @return AsymmetricKey
+     * @return \phpseclib3\Crypt\Common\PublicKey|\phpseclib3\Crypt\Common\PrivateKey
      */
     public static function load($key, $password = false)
     {
@@ -382,7 +382,7 @@ abstract class AsymmetricKey
             $shortname = $meta->getShortName();
             self::$plugins[static::ALGORITHM]['Keys'][strtolower($shortname)] = $fullname;
             if ($meta->hasConstant('IS_INVISIBLE')) {
-                self::$invisiblePlugins[static::ALGORITHM] = strtolower($name);
+                self::$invisiblePlugins[static::ALGORITHM][] = strtolower($shortname);
             }
         }
     }
index 4638a53931b3e656ce45ac0656ccf13a50c5415d..7aa554808ffb6927f30938fbf01ae041dd5ffe74 100644 (file)
@@ -141,6 +141,7 @@ abstract class PKCS8 extends PKCS
                 break;
             case 'RC2':
                 $cipher = new RC2('cbc');
+                $cipher->setKeyLength(64);
                 break;
             case '3-KeyTripleDES':
                 $cipher = new TripleDES('cbc');
@@ -218,7 +219,7 @@ abstract class PKCS8 extends PKCS
     {
         switch ($algo) {
             case 'desCBC':
-                $cipher = new TripleDES('cbc');
+                $cipher = new DES('cbc');
                 break;
             case 'des-EDE3-CBC':
                 $cipher = new TripleDES('cbc');
index 2a376f113c8aaed62d243ef507de86ab3976b3a7..175508d413374b995c16c099f16fef496f8f8304 100644 (file)
@@ -668,6 +668,8 @@ abstract class SymmetricKey
             switch (true) {
                 // PHP_OS & "\xDF\xDF\xDF" == strtoupper(substr(PHP_OS, 0, 3)), but a lot faster
                 case (PHP_OS & "\xDF\xDF\xDF") === 'WIN':
+                case !function_exists('php_uname'):
+                case !is_string(php_uname('m')):
                 case (php_uname('m') & "\xDF\xDF\xDF") != 'ARM':
                 case defined('PHP_INT_SIZE') && PHP_INT_SIZE == 8:
                     self::$use_reg_intval = true;
@@ -917,7 +919,7 @@ abstract class SymmetricKey
      * @see Crypt/Hash.php
      * @param string $password
      * @param string $method
-     * @param string[] ...$func_args
+     * @param int|string ...$func_args
      * @throws \LengthException if pbkdf1 is being used and the derived key length exceeds the hash length
      * @throws \RuntimeException if bcrypt is being used and a salt isn't provided
      * @return bool
diff --git a/securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/IEEE.php b/securemail/vendor/phpseclib/phpseclib/phpseclib/Crypt/EC/Formats/Signature/IEEE.php
new file mode 100644 (file)
index 0000000..69139da
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * IEEE P1363 Signature Handler
+ *
+ * PHP version 5
+ *
+ * Handles signatures in the format described in
+ * https://standards.ieee.org/ieee/1363/2049/ and
+ * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#ecdsa
+ *
+ * @author    Jim Wigginton <terrafrost@php.net>
+ * @copyright 2016 Jim Wigginton
+ * @license   http://www.opensource.org/licenses/mit-license.html  MIT License
+ * @link      http://phpseclib.sourceforge.net
+ */
+
+namespace phpseclib3\Crypt\EC\Formats\Signature;
+
+use phpseclib3\Math\BigInteger;
+
+/**
+ * ASN1 Signature Handler
+ *
+ * @author  Jim Wigginton <terrafrost@php.net>
+ */
+abstract class IEEE
+{
+    /**
+     * Loads a signature
+     *
+     * @param string $sig
+     * @return array
+     */
+    public static function load($sig)
+    {
+        if (!is_string($sig)) {
+            return false;
+        }
+
+        $len = strlen($sig);
+        if ($len & 1) {
+            return false;
+        }
+
+        $r = new BigInteger(substr($sig, 0, $len >> 1), 256);
+        $s = new BigInteger(substr($sig, $len >> 1), 256);
+
+        return compact('r', 's');
+    }
+
+    /**
+     * Returns a signature in the appropriate format
+     *
+     * @param \phpseclib3\Math\BigInteger $r
+     * @param \phpseclib3\Math\BigInteger $s
+     * @return string
+     */
+    public static function save(BigInteger $r, BigInteger $s)
+    {
+        $r = $r->toBytes();
+        $s = $s->toBytes();
+        $len = max(strlen($r), strlen($s));
+        return str_pad($r, $len, "\0", STR_PAD_LEFT) . str_pad($s, $len, "\0", STR_PAD_LEFT);
+    }
+}
index 462ea1a3363cc8b906d0df37e86d649d412c6d99..598869614bda985657eafc0f4c5804ab260e206d 100644 (file)
@@ -150,7 +150,7 @@ final class PrivateKey extends EC implements Common\PrivateKey
             // we use specified curves to avoid issues with OpenSSL possibly not supporting a given named curve;
             // doing this may mean some curve-specific optimizations can't be used but idk if OpenSSL even
             // has curve-specific optimizations
-            $result = openssl_sign($message, $signature, $this->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash());
+            $result = openssl_sign($message, $signature, $this->withPassword()->toString('PKCS8', ['namedCurve' => false]), $this->hash->getHash());
 
             if ($result) {
                 if ($shortFormat == 'ASN1') {
index 7b935cc2dc4d2be7bf40a919fa44b5fc70f96efe..19dcfea3ffb266543a26eb6ac89a457c058952e1 100644 (file)
@@ -332,6 +332,7 @@ abstract class RSA extends AsymmetricKey
                 openssl_pkey_export($rsa, $privatekeystr, null, $config);
 
                 // clear the buffer of error strings stemming from a minimalistic openssl.cnf
+                // https://github.com/php/php-src/issues/11054 talks about other errors this'll pick up
                 while (openssl_error_string() !== false) {
                 }
 
@@ -841,15 +842,15 @@ abstract class RSA extends AsymmetricKey
             self::ENCRYPTION_PKCS1,
             self::ENCRYPTION_NONE
         ];
-        $numSelected = 0;
+        $encryptedCount = 0;
         $selected = 0;
         foreach ($masks as $mask) {
             if ($padding & $mask) {
                 $selected = $mask;
-                $numSelected++;
+                $encryptedCount++;
             }
         }
-        if ($numSelected > 1) {
+        if ($encryptedCount > 1) {
             throw new InconsistentSetupException('Multiple encryption padding modes have been selected; at most only one should be selected');
         }
         $encryptionPadding = $selected;
@@ -859,22 +860,26 @@ abstract class RSA extends AsymmetricKey
             self::SIGNATURE_RELAXED_PKCS1,
             self::SIGNATURE_PKCS1
         ];
-        $numSelected = 0;
+        $signatureCount = 0;
         $selected = 0;
         foreach ($masks as $mask) {
             if ($padding & $mask) {
                 $selected = $mask;
-                $numSelected++;
+                $signatureCount++;
             }
         }
-        if ($numSelected > 1) {
+        if ($signatureCount > 1) {
             throw new InconsistentSetupException('Multiple signature padding modes have been selected; at most only one should be selected');
         }
         $signaturePadding = $selected;
 
         $new = clone $this;
-        $new->encryptionPadding = $encryptionPadding;
-        $new->signaturePadding = $signaturePadding;
+        if ($encryptedCount) {
+            $new->encryptionPadding = $encryptionPadding;
+        }
+        if ($signatureCount) {
+            $new->signaturePadding = $signaturePadding;
+        }
         return $new;
     }
 
index cd8b762721a3ba3c57b803dc46ed525fe53a0abc..ff31f9c88ad04ad7d692387d12ae4a03f5c92a49 100644 (file)
@@ -833,7 +833,6 @@ class Rijndael extends BlockCipher
 
         // Generating encrypt code:
         $init_encrypt .= '
-            static $tables;
             if (empty($tables)) {
                 $tables = &$this->getTables();
             }
@@ -890,7 +889,6 @@ class Rijndael extends BlockCipher
 
         // Generating decrypt code:
         $init_decrypt .= '
-            static $invtables;
             if (empty($invtables)) {
                 $invtables = &$this->getInvTables();
             }
@@ -947,7 +945,7 @@ class Rijndael extends BlockCipher
 
         $this->inline_crypt = $this->createInlineCryptFunction(
             [
-               'init_crypt'    => '',
+               'init_crypt'    => 'static $tables; static $invtables;',
                'init_encrypt'  => $init_encrypt,
                'init_decrypt'  => $init_decrypt,
                'encrypt_block' => $encrypt_block,
index e21589c51097e05c092cd43acd8173d6c8a3ab17..c4b06a5609bbe1608e2c88fd8c95d77e312d5686 100644 (file)
@@ -21,7 +21,6 @@
 
 namespace phpseclib3\File;
 
-use DateTime;
 use phpseclib3\Common\Functions\Strings;
 use phpseclib3\File\ASN1\Element;
 use phpseclib3\Math\BigInteger;
@@ -205,7 +204,7 @@ abstract class ASN1
             return null;
         }
 
-        return [self::decode_ber($encoded)];
+        return [$decoded];
     }
 
     /**
@@ -1149,6 +1148,11 @@ abstract class ASN1
         $oid = [];
         $pos = 0;
         $len = strlen($content);
+        // see https://github.com/openjdk/jdk/blob/2deb318c9f047ec5a4b160d66a4b52f93688ec42/src/java.base/share/classes/sun/security/util/ObjectIdentifier.java#L55
+        if ($len > 4096) {
+            //throw new \RuntimeException("Object identifier size is limited to 4096 bytes ($len bytes present)");
+            return false;
+        }
 
         if (ord($content[$len - 1]) & 0x80) {
             return false;
@@ -1403,7 +1407,7 @@ abstract class ASN1
                         return false;
                     }
                     break;
-                case ($c & 0x80000000) != 0:
+                case ($c & (PHP_INT_SIZE == 8 ? 0x80000000 : (1 << 31))) != 0:
                     return false;
                 case $c >= 0x04000000:
                     $v .= chr(0x80 | ($c & 0x3F));
index 7aa278f2a559b5b78c7e169e28572c23611d9b52..5e2f073f4a48ab8f6840eb8a801ceacb41c0b5a2 100644 (file)
@@ -164,7 +164,7 @@ class X509
      *
      * @var array
      */
-    private $CAs;
+    private $CAs = [];
 
     /**
      * The currently loaded certificate
@@ -315,6 +315,10 @@ class X509
                 'id-at-uniqueIdentifier' => '2.5.4.45',
                 'id-at-role' => '2.5.4.72',
                 'id-at-postalAddress' => '2.5.4.16',
+                'jurisdictionOfIncorporationCountryName' => '1.3.6.1.4.1.311.60.2.1.3',
+                'jurisdictionOfIncorporationStateOrProvinceName' => '1.3.6.1.4.1.311.60.2.1.2',
+                'jurisdictionLocalityName' => '1.3.6.1.4.1.311.60.2.1.1',
+                'id-at-businessCategory' => '2.5.4.15',
 
                 //'id-domainComponent' => '0.9.2342.19200300.100.1.25',
                 //'pkcs-9' => '1.2.840.113549.1.9',
@@ -1038,7 +1042,8 @@ class X509
         if ($names = $this->getExtension('id-ce-subjectAltName')) {
             foreach ($names as $name) {
                 foreach ($name as $key => $value) {
-                    $value = str_replace(['.', '*'], ['\.', '[^.]*'], $value);
+                    $value = preg_quote($value);
+                    $value = str_replace('\*', '[^.]*', $value);
                     switch ($key) {
                         case 'dNSName':
                             /* From RFC2818 "HTTP over TLS":
@@ -1538,6 +1543,20 @@ class X509
     private function translateDNProp($propName)
     {
         switch (strtolower($propName)) {
+            case 'jurisdictionofincorporationcountryname':
+            case 'jurisdictioncountryname':
+            case 'jurisdictionc':
+                return 'jurisdictionOfIncorporationCountryName';
+            case 'jurisdictionofincorporationstateorprovincename':
+            case 'jurisdictionstateorprovincename':
+            case 'jurisdictionst':
+                return 'jurisdictionOfIncorporationStateOrProvinceName';
+            case 'jurisdictionlocalityname':
+            case 'jurisdictionl':
+                return 'jurisdictionLocalityName';
+            case 'id-at-businesscategory':
+            case 'businesscategory':
+                return 'id-at-businessCategory';
             case 'id-at-countryname':
             case 'countryname':
             case 'c':
@@ -2030,9 +2049,6 @@ class X509
         if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) {
             return false;
         }
-        if (empty($this->CAs)) {
-            return $chain;
-        }
         while (true) {
             $currentCert = $chain[count($chain) - 1];
             for ($i = 0; $i < count($this->CAs); $i++) {
index 70491abd63488ea5eb22a3759fea2a0022caadb0..67d4788f940cb3add9f9fbbe9139fa9b3e615f5f 100644 (file)
@@ -100,10 +100,6 @@ class BigInteger implements \JsonSerializable
         /** @var class-string<Engine> $fqmain */
         self::$mainEngine = $fqmain;
 
-        if (!in_array('Default', $modexps)) {
-            $modexps[] = 'DefaultEngine';
-        }
-
         $found = false;
         foreach ($modexps as $modexp) {
             try {
@@ -140,18 +136,23 @@ class BigInteger implements \JsonSerializable
     {
         if (!isset(self::$mainEngine)) {
             $engines = [
-                ['GMP'],
+                ['GMP', ['DefaultEngine']],
                 ['PHP64', ['OpenSSL']],
                 ['BCMath', ['OpenSSL']],
-                ['PHP32', ['OpenSSL']]
+                ['PHP32', ['OpenSSL']],
+                ['PHP64', ['DefaultEngine']],
+                ['PHP32', ['DefaultEngine']]
             ];
+
             foreach ($engines as $engine) {
                 try {
-                    self::setEngine($engine[0], isset($engine[1]) ? $engine[1] : []);
-                    break;
+                    self::setEngine($engine[0], $engine[1]);
+                    return;
                 } catch (\Exception $e) {
                 }
             }
+
+            throw new \UnexpectedValueException('No valid BigInteger found. This is only possible when JIT is enabled on Windows and neither the GMP or BCMath extensions are available so either disable JIT or install GMP / BCMath');
         }
     }
 
index 2b00bc37c83fe371499fd7f6823aa5dd173e7600..474abe105b573141e1c66fa68ac0201401fe5acf 100644 (file)
@@ -619,7 +619,7 @@ abstract class Engine implements \JsonSerializable
      */
     public function getLengthInBytes()
     {
-        return strlen($this->toBytes());
+        return (int) ceil($this->getLength() / 8);
     }
 
     /**
@@ -644,6 +644,11 @@ abstract class Engine implements \JsonSerializable
             return $this->normalize($temp->powModInner($e, $n));
         }
 
+        if ($this->compare($n) > 0) {
+            list(, $temp) = $this->divide($n);
+            return $temp->powModInner($e, $n);
+        }
+
         return $this->powModInner($e, $n);
     }
 
@@ -781,6 +786,11 @@ abstract class Engine implements \JsonSerializable
             $min = $temp;
         }
 
+        $length = $max->getLength();
+        if ($length > 8196) {
+            throw new \RuntimeException("Generation of random prime numbers larger than 8196 has been disabled ($length)");
+        }
+
         $x = static::randomRange($min, $max);
 
         return static::randomRangePrimeInner($x, $min, $max);
@@ -985,6 +995,15 @@ abstract class Engine implements \JsonSerializable
      */
     public function isPrime($t = false)
     {
+        // OpenSSL limits RSA keys to 16384 bits. The length of an RSA key is equal to the length of the modulo, which is
+        // produced by multiplying the primes p and q by one another. The largest number two 8196 bit primes can produce is
+        // a 16384 bit number so, basically, 8196 bit primes are the largest OpenSSL will generate and if that's the largest
+        // that it'll generate it also stands to reason that that's the largest you'll be able to test primality on
+        $length = $this->getLength();
+        if ($length > 8196) {
+            throw new \RuntimeException("Primality testing is not supported for numbers larger than 8196 bits ($length)");
+        }
+
         if (!$t) {
             $t = $this->setupIsPrime();
         }
index ab9bdc99b05b486732124baff02ee59a7e8a9fae..2d895952255e6e0a82d655c848bb4bf519342ca3 100644 (file)
@@ -1326,4 +1326,32 @@ abstract class PHP extends Engine
 
         return array_reverse($vals);
     }
+
+    /**
+     * @return bool
+     */
+    protected static function testJITOnWindows()
+    {
+        // see https://github.com/php/php-src/issues/11917
+        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' && function_exists('opcache_get_status') && PHP_VERSION_ID < 80213 && !defined('PHPSECLIB_ALLOW_JIT')) {
+            $status = opcache_get_status();
+            if ($status && isset($status['jit']) && $status['jit']['enabled'] && $status['jit']['on']) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the size of a BigInteger in bits
+     *
+     * @return int
+     */
+    public function getLength()
+    {
+        $max = count($this->value) - 1;
+        return $max != -1 ?
+            $max * static::BASE + intval(ceil(log($this->value[$max] + 1, 2))) :
+            0;
+    }
 }
index 964cd170def7a4fe79eeec9e660585c4401b7d9c..3a775e7dba8df2ca0ca2c695917faf31140d9ed0 100644 (file)
@@ -80,10 +80,10 @@ class PHP32 extends PHP
             }
             $step = count($vals) & 3;
             if ($step) {
-                $digit = floor($digit / pow(2, 2 * $step));
+                $digit = (int) floor($digit / pow(2, 2 * $step));
             }
             if ($step != 3) {
-                $digit &= static::MAX_DIGIT;
+                $digit = (int) fmod($digit, static::BASE_FULL);
                 $i++;
             }
             $vals[] = $digit;
@@ -102,7 +102,7 @@ class PHP32 extends PHP
      */
     public static function isValidEngine()
     {
-        return PHP_INT_SIZE >= 4;
+        return PHP_INT_SIZE >= 4 && !self::testJITOnWindows();
     }
 
     /**
index ca11c08d466735004ac189516e1b641fef383a36..70a2e173bcea9092d15fb3dcf88a0f1fd0de9984 100644 (file)
@@ -103,7 +103,7 @@ class PHP64 extends PHP
      */
     public static function isValidEngine()
     {
-        return PHP_INT_SIZE >= 8;
+        return PHP_INT_SIZE >= 8 && !self::testJITOnWindows();
     }
 
     /**
index 3e21a67ad68e398c6e8b9a76345d42d8c90dc217..5da8c937e239b5d10591ebb2ee1e92cb39fd8313 100644 (file)
@@ -48,6 +48,15 @@ class BinaryField extends FiniteField
     public function __construct(...$indices)
     {
         $m = array_shift($indices);
+        if ($m > 571) {
+            /* sect571r1 and sect571k1 are the largest binary curves that https://www.secg.org/sec2-v2.pdf defines
+               altho theoretically there may be legit reasons to use binary finite fields with larger degrees
+               imposing a limit on the maximum size is both reasonable and precedented. in particular,
+               http://tools.ietf.org/html/rfc4253#section-6.1 (The Secure Shell (SSH) Transport Layer Protocol) says
+               "implementations SHOULD check that the packet length is reasonable in order for the implementation to
+                avoid denial of service and/or buffer overflow attacks" */
+            throw new \OutOfBoundsException('Degrees larger than 571 are not supported');
+        }
         $val = str_repeat('0', $m) . '1';
         foreach ($indices as $index) {
             $val[$index] = '1';
index 748f9a49f0e43a0ae96cff67b23f2f343554bbf6..1bd7aaf0f27bf05ab4cdcf0b06d42da9c3b7ce43 100644 (file)
@@ -263,7 +263,7 @@ class Integer extends Base
         $r = $this->value->powMod($temp, static::$modulo[$this->instanceID]);
 
         while (!$t->equals($one)) {
-            for ($i == clone $one; $i->compare($m) < 0; $i = $i->add($one)) {
+            for ($i = clone $one; $i->compare($m) < 0; $i = $i->add($one)) {
                 if ($t->powMod($two->pow($i), static::$modulo[$this->instanceID])->equals($one)) {
                     break;
                 }
@@ -312,8 +312,11 @@ class Integer extends Base
      */
     public function toBytes()
     {
-        $length = static::$modulo[$this->instanceID]->getLengthInBytes();
-        return str_pad($this->value->toBytes(), $length, "\0", STR_PAD_LEFT);
+        if (isset(static::$modulo[$this->instanceID])) {
+            $length = static::$modulo[$this->instanceID]->getLengthInBytes();
+            return str_pad($this->value->toBytes(), $length, "\0", STR_PAD_LEFT);
+        }
+        return $this->value->toBytes();
     }
 
     /**
index 1c675a6e05120e88d30e4ecaa7a7f9de2eb15495..144ef7950b7102cac3e233c1c286625b5d18a1ce 100644 (file)
@@ -93,7 +93,7 @@ class SFTP extends SSH2
      * @var array
      * @access private
      */
-    private $packet_types = [];
+    private static $packet_types = [];
 
     /**
      * Status Codes
@@ -102,19 +102,19 @@ class SFTP extends SSH2
      * @var array
      * @access private
      */
-    private $status_codes = [];
+    private static $status_codes = [];
 
     /** @var array<int, string> */
-    private $attributes;
+    private static $attributes;
 
     /** @var array<int, string> */
-    private $open_flags;
+    private static $open_flags;
 
     /** @var array<int, string> */
-    private $open_flags5;
+    private static $open_flags5;
 
     /** @var array<int, string> */
-    private $file_types;
+    private static $file_types;
 
     /**
      * The Request ID
@@ -350,7 +350,9 @@ class SFTP extends SSH2
      *
      * Connects to an SFTP server
      *
-     * @param string $host
+     * $host can either be a string, representing the host, or a stream resource.
+     *
+     * @param mixed $host
      * @param int $port
      * @param int $timeout
      */
@@ -360,154 +362,156 @@ class SFTP extends SSH2
 
         $this->max_sftp_packet = 1 << 15;
 
-        $this->packet_types = [
-            1  => 'NET_SFTP_INIT',
-            2  => 'NET_SFTP_VERSION',
-            3  => 'NET_SFTP_OPEN',
-            4  => 'NET_SFTP_CLOSE',
-            5  => 'NET_SFTP_READ',
-            6  => 'NET_SFTP_WRITE',
-            7  => 'NET_SFTP_LSTAT',
-            9  => 'NET_SFTP_SETSTAT',
-            10 => 'NET_SFTP_FSETSTAT',
-            11 => 'NET_SFTP_OPENDIR',
-            12 => 'NET_SFTP_READDIR',
-            13 => 'NET_SFTP_REMOVE',
-            14 => 'NET_SFTP_MKDIR',
-            15 => 'NET_SFTP_RMDIR',
-            16 => 'NET_SFTP_REALPATH',
-            17 => 'NET_SFTP_STAT',
-            18 => 'NET_SFTP_RENAME',
-            19 => 'NET_SFTP_READLINK',
-            20 => 'NET_SFTP_SYMLINK',
-            21 => 'NET_SFTP_LINK',
-
-            101 => 'NET_SFTP_STATUS',
-            102 => 'NET_SFTP_HANDLE',
-            103 => 'NET_SFTP_DATA',
-            104 => 'NET_SFTP_NAME',
-            105 => 'NET_SFTP_ATTRS',
-
-            200 => 'NET_SFTP_EXTENDED'
-        ];
-        $this->status_codes = [
-            0 => 'NET_SFTP_STATUS_OK',
-            1 => 'NET_SFTP_STATUS_EOF',
-            2 => 'NET_SFTP_STATUS_NO_SUCH_FILE',
-            3 => 'NET_SFTP_STATUS_PERMISSION_DENIED',
-            4 => 'NET_SFTP_STATUS_FAILURE',
-            5 => 'NET_SFTP_STATUS_BAD_MESSAGE',
-            6 => 'NET_SFTP_STATUS_NO_CONNECTION',
-            7 => 'NET_SFTP_STATUS_CONNECTION_LOST',
-            8 => 'NET_SFTP_STATUS_OP_UNSUPPORTED',
-            9 => 'NET_SFTP_STATUS_INVALID_HANDLE',
-            10 => 'NET_SFTP_STATUS_NO_SUCH_PATH',
-            11 => 'NET_SFTP_STATUS_FILE_ALREADY_EXISTS',
-            12 => 'NET_SFTP_STATUS_WRITE_PROTECT',
-            13 => 'NET_SFTP_STATUS_NO_MEDIA',
-            14 => 'NET_SFTP_STATUS_NO_SPACE_ON_FILESYSTEM',
-            15 => 'NET_SFTP_STATUS_QUOTA_EXCEEDED',
-            16 => 'NET_SFTP_STATUS_UNKNOWN_PRINCIPAL',
-            17 => 'NET_SFTP_STATUS_LOCK_CONFLICT',
-            18 => 'NET_SFTP_STATUS_DIR_NOT_EMPTY',
-            19 => 'NET_SFTP_STATUS_NOT_A_DIRECTORY',
-            20 => 'NET_SFTP_STATUS_INVALID_FILENAME',
-            21 => 'NET_SFTP_STATUS_LINK_LOOP',
-            22 => 'NET_SFTP_STATUS_CANNOT_DELETE',
-            23 => 'NET_SFTP_STATUS_INVALID_PARAMETER',
-            24 => 'NET_SFTP_STATUS_FILE_IS_A_DIRECTORY',
-            25 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_CONFLICT',
-            26 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_REFUSED',
-            27 => 'NET_SFTP_STATUS_DELETE_PENDING',
-            28 => 'NET_SFTP_STATUS_FILE_CORRUPT',
-            29 => 'NET_SFTP_STATUS_OWNER_INVALID',
-            30 => 'NET_SFTP_STATUS_GROUP_INVALID',
-            31 => 'NET_SFTP_STATUS_NO_MATCHING_BYTE_RANGE_LOCK'
-        ];
-        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1
-        // the order, in this case, matters quite a lot - see \phpseclib3\Net\SFTP::_parseAttributes() to understand why
-        $this->attributes = [
-            0x00000001 => 'NET_SFTP_ATTR_SIZE',
-            0x00000002 => 'NET_SFTP_ATTR_UIDGID',          // defined in SFTPv3, removed in SFTPv4+
-            0x00000080 => 'NET_SFTP_ATTR_OWNERGROUP',      // defined in SFTPv4+
-            0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS',
-            0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME',
-            0x00000010 => 'NET_SFTP_ATTR_CREATETIME',      // SFTPv4+
-            0x00000020 => 'NET_SFTP_ATTR_MODIFYTIME',
-            0x00000040 => 'NET_SFTP_ATTR_ACL',
-            0x00000100 => 'NET_SFTP_ATTR_SUBSECOND_TIMES',
-            0x00000200 => 'NET_SFTP_ATTR_BITS',            // SFTPv5+
-            0x00000400 => 'NET_SFTP_ATTR_ALLOCATION_SIZE', // SFTPv6+
-            0x00000800 => 'NET_SFTP_ATTR_TEXT_HINT',
-            0x00001000 => 'NET_SFTP_ATTR_MIME_TYPE',
-            0x00002000 => 'NET_SFTP_ATTR_LINK_COUNT',
-            0x00004000 => 'NET_SFTP_ATTR_UNTRANSLATED_NAME',
-            0x00008000 => 'NET_SFTP_ATTR_CTIME',
-            // 0x80000000 will yield a floating point on 32-bit systems and converting floating points to integers
-            // yields inconsistent behavior depending on how php is compiled.  so we left shift -1 (which, in
-            // two's compliment, consists of all 1 bits) by 31.  on 64-bit systems this'll yield 0xFFFFFFFF80000000.
-            // that's not a problem, however, and 'anded' and a 32-bit number, as all the leading 1 bits are ignored.
-            (PHP_INT_SIZE == 4 ? -1 : 0xFFFFFFFF) => 'NET_SFTP_ATTR_EXTENDED'
-        ];
-        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.3
-        // the flag definitions change somewhat in SFTPv5+.  if SFTPv5+ support is added to this library, maybe name
-        // the array for that $this->open5_flags and similarly alter the constant names.
-        $this->open_flags = [
-            0x00000001 => 'NET_SFTP_OPEN_READ',
-            0x00000002 => 'NET_SFTP_OPEN_WRITE',
-            0x00000004 => 'NET_SFTP_OPEN_APPEND',
-            0x00000008 => 'NET_SFTP_OPEN_CREATE',
-            0x00000010 => 'NET_SFTP_OPEN_TRUNCATE',
-            0x00000020 => 'NET_SFTP_OPEN_EXCL',
-            0x00000040 => 'NET_SFTP_OPEN_TEXT' // defined in SFTPv4
-        ];
-        // SFTPv5+ changed the flags up:
-        // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-8.1.1.3
-        $this->open_flags5 = [
-            // when SSH_FXF_ACCESS_DISPOSITION is a 3 bit field that controls how the file is opened
-            0x00000000 => 'NET_SFTP_OPEN_CREATE_NEW',
-            0x00000001 => 'NET_SFTP_OPEN_CREATE_TRUNCATE',
-            0x00000002 => 'NET_SFTP_OPEN_OPEN_EXISTING',
-            0x00000003 => 'NET_SFTP_OPEN_OPEN_OR_CREATE',
-            0x00000004 => 'NET_SFTP_OPEN_TRUNCATE_EXISTING',
-            // the rest of the flags are not supported
-            0x00000008 => 'NET_SFTP_OPEN_APPEND_DATA', // "the offset field of SS_FXP_WRITE requests is ignored"
-            0x00000010 => 'NET_SFTP_OPEN_APPEND_DATA_ATOMIC',
-            0x00000020 => 'NET_SFTP_OPEN_TEXT_MODE',
-            0x00000040 => 'NET_SFTP_OPEN_BLOCK_READ',
-            0x00000080 => 'NET_SFTP_OPEN_BLOCK_WRITE',
-            0x00000100 => 'NET_SFTP_OPEN_BLOCK_DELETE',
-            0x00000200 => 'NET_SFTP_OPEN_BLOCK_ADVISORY',
-            0x00000400 => 'NET_SFTP_OPEN_NOFOLLOW',
-            0x00000800 => 'NET_SFTP_OPEN_DELETE_ON_CLOSE',
-            0x00001000 => 'NET_SFTP_OPEN_ACCESS_AUDIT_ALARM_INFO',
-            0x00002000 => 'NET_SFTP_OPEN_ACCESS_BACKUP',
-            0x00004000 => 'NET_SFTP_OPEN_BACKUP_STREAM',
-            0x00008000 => 'NET_SFTP_OPEN_OVERRIDE_OWNER',
-        ];
-        // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2
-        // see \phpseclib3\Net\SFTP::_parseLongname() for an explanation
-        $this->file_types = [
-            1 => 'NET_SFTP_TYPE_REGULAR',
-            2 => 'NET_SFTP_TYPE_DIRECTORY',
-            3 => 'NET_SFTP_TYPE_SYMLINK',
-            4 => 'NET_SFTP_TYPE_SPECIAL',
-            5 => 'NET_SFTP_TYPE_UNKNOWN',
-            // the following types were first defined for use in SFTPv5+
-            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2
-            6 => 'NET_SFTP_TYPE_SOCKET',
-            7 => 'NET_SFTP_TYPE_CHAR_DEVICE',
-            8 => 'NET_SFTP_TYPE_BLOCK_DEVICE',
-            9 => 'NET_SFTP_TYPE_FIFO'
-        ];
-        $this->define_array(
-            $this->packet_types,
-            $this->status_codes,
-            $this->attributes,
-            $this->open_flags,
-            $this->open_flags5,
-            $this->file_types
-        );
+        if (empty(self::$packet_types)) {
+            self::$packet_types = [
+                1  => 'NET_SFTP_INIT',
+                2  => 'NET_SFTP_VERSION',
+                3  => 'NET_SFTP_OPEN',
+                4  => 'NET_SFTP_CLOSE',
+                5  => 'NET_SFTP_READ',
+                6  => 'NET_SFTP_WRITE',
+                7  => 'NET_SFTP_LSTAT',
+                9  => 'NET_SFTP_SETSTAT',
+                10 => 'NET_SFTP_FSETSTAT',
+                11 => 'NET_SFTP_OPENDIR',
+                12 => 'NET_SFTP_READDIR',
+                13 => 'NET_SFTP_REMOVE',
+                14 => 'NET_SFTP_MKDIR',
+                15 => 'NET_SFTP_RMDIR',
+                16 => 'NET_SFTP_REALPATH',
+                17 => 'NET_SFTP_STAT',
+                18 => 'NET_SFTP_RENAME',
+                19 => 'NET_SFTP_READLINK',
+                20 => 'NET_SFTP_SYMLINK',
+                21 => 'NET_SFTP_LINK',
+
+                101 => 'NET_SFTP_STATUS',
+                102 => 'NET_SFTP_HANDLE',
+                103 => 'NET_SFTP_DATA',
+                104 => 'NET_SFTP_NAME',
+                105 => 'NET_SFTP_ATTRS',
+
+                200 => 'NET_SFTP_EXTENDED'
+            ];
+            self::$status_codes = [
+                0 => 'NET_SFTP_STATUS_OK',
+                1 => 'NET_SFTP_STATUS_EOF',
+                2 => 'NET_SFTP_STATUS_NO_SUCH_FILE',
+                3 => 'NET_SFTP_STATUS_PERMISSION_DENIED',
+                4 => 'NET_SFTP_STATUS_FAILURE',
+                5 => 'NET_SFTP_STATUS_BAD_MESSAGE',
+                6 => 'NET_SFTP_STATUS_NO_CONNECTION',
+                7 => 'NET_SFTP_STATUS_CONNECTION_LOST',
+                8 => 'NET_SFTP_STATUS_OP_UNSUPPORTED',
+                9 => 'NET_SFTP_STATUS_INVALID_HANDLE',
+                10 => 'NET_SFTP_STATUS_NO_SUCH_PATH',
+                11 => 'NET_SFTP_STATUS_FILE_ALREADY_EXISTS',
+                12 => 'NET_SFTP_STATUS_WRITE_PROTECT',
+                13 => 'NET_SFTP_STATUS_NO_MEDIA',
+                14 => 'NET_SFTP_STATUS_NO_SPACE_ON_FILESYSTEM',
+                15 => 'NET_SFTP_STATUS_QUOTA_EXCEEDED',
+                16 => 'NET_SFTP_STATUS_UNKNOWN_PRINCIPAL',
+                17 => 'NET_SFTP_STATUS_LOCK_CONFLICT',
+                18 => 'NET_SFTP_STATUS_DIR_NOT_EMPTY',
+                19 => 'NET_SFTP_STATUS_NOT_A_DIRECTORY',
+                20 => 'NET_SFTP_STATUS_INVALID_FILENAME',
+                21 => 'NET_SFTP_STATUS_LINK_LOOP',
+                22 => 'NET_SFTP_STATUS_CANNOT_DELETE',
+                23 => 'NET_SFTP_STATUS_INVALID_PARAMETER',
+                24 => 'NET_SFTP_STATUS_FILE_IS_A_DIRECTORY',
+                25 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_CONFLICT',
+                26 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_REFUSED',
+                27 => 'NET_SFTP_STATUS_DELETE_PENDING',
+                28 => 'NET_SFTP_STATUS_FILE_CORRUPT',
+                29 => 'NET_SFTP_STATUS_OWNER_INVALID',
+                30 => 'NET_SFTP_STATUS_GROUP_INVALID',
+                31 => 'NET_SFTP_STATUS_NO_MATCHING_BYTE_RANGE_LOCK'
+            ];
+            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1
+            // the order, in this case, matters quite a lot - see \phpseclib3\Net\SFTP::_parseAttributes() to understand why
+            self::$attributes = [
+                0x00000001 => 'NET_SFTP_ATTR_SIZE',
+                0x00000002 => 'NET_SFTP_ATTR_UIDGID',          // defined in SFTPv3, removed in SFTPv4+
+                0x00000080 => 'NET_SFTP_ATTR_OWNERGROUP',      // defined in SFTPv4+
+                0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS',
+                0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME',
+                0x00000010 => 'NET_SFTP_ATTR_CREATETIME',      // SFTPv4+
+                0x00000020 => 'NET_SFTP_ATTR_MODIFYTIME',
+                0x00000040 => 'NET_SFTP_ATTR_ACL',
+                0x00000100 => 'NET_SFTP_ATTR_SUBSECOND_TIMES',
+                0x00000200 => 'NET_SFTP_ATTR_BITS',            // SFTPv5+
+                0x00000400 => 'NET_SFTP_ATTR_ALLOCATION_SIZE', // SFTPv6+
+                0x00000800 => 'NET_SFTP_ATTR_TEXT_HINT',
+                0x00001000 => 'NET_SFTP_ATTR_MIME_TYPE',
+                0x00002000 => 'NET_SFTP_ATTR_LINK_COUNT',
+                0x00004000 => 'NET_SFTP_ATTR_UNTRANSLATED_NAME',
+                0x00008000 => 'NET_SFTP_ATTR_CTIME',
+                // 0x80000000 will yield a floating point on 32-bit systems and converting floating points to integers
+                // yields inconsistent behavior depending on how php is compiled.  so we left shift -1 (which, in
+                // two's compliment, consists of all 1 bits) by 31.  on 64-bit systems this'll yield 0xFFFFFFFF80000000.
+                // that's not a problem, however, and 'anded' and a 32-bit number, as all the leading 1 bits are ignored.
+                (PHP_INT_SIZE == 4 ? (-1 << 31) : 0x80000000) => 'NET_SFTP_ATTR_EXTENDED'
+            ];
+            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.3
+            // the flag definitions change somewhat in SFTPv5+.  if SFTPv5+ support is added to this library, maybe name
+            // the array for that $this->open5_flags and similarly alter the constant names.
+            self::$open_flags = [
+                0x00000001 => 'NET_SFTP_OPEN_READ',
+                0x00000002 => 'NET_SFTP_OPEN_WRITE',
+                0x00000004 => 'NET_SFTP_OPEN_APPEND',
+                0x00000008 => 'NET_SFTP_OPEN_CREATE',
+                0x00000010 => 'NET_SFTP_OPEN_TRUNCATE',
+                0x00000020 => 'NET_SFTP_OPEN_EXCL',
+                0x00000040 => 'NET_SFTP_OPEN_TEXT' // defined in SFTPv4
+            ];
+            // SFTPv5+ changed the flags up:
+            // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-8.1.1.3
+            self::$open_flags5 = [
+                // when SSH_FXF_ACCESS_DISPOSITION is a 3 bit field that controls how the file is opened
+                0x00000000 => 'NET_SFTP_OPEN_CREATE_NEW',
+                0x00000001 => 'NET_SFTP_OPEN_CREATE_TRUNCATE',
+                0x00000002 => 'NET_SFTP_OPEN_OPEN_EXISTING',
+                0x00000003 => 'NET_SFTP_OPEN_OPEN_OR_CREATE',
+                0x00000004 => 'NET_SFTP_OPEN_TRUNCATE_EXISTING',
+                // the rest of the flags are not supported
+                0x00000008 => 'NET_SFTP_OPEN_APPEND_DATA', // "the offset field of SS_FXP_WRITE requests is ignored"
+                0x00000010 => 'NET_SFTP_OPEN_APPEND_DATA_ATOMIC',
+                0x00000020 => 'NET_SFTP_OPEN_TEXT_MODE',
+                0x00000040 => 'NET_SFTP_OPEN_BLOCK_READ',
+                0x00000080 => 'NET_SFTP_OPEN_BLOCK_WRITE',
+                0x00000100 => 'NET_SFTP_OPEN_BLOCK_DELETE',
+                0x00000200 => 'NET_SFTP_OPEN_BLOCK_ADVISORY',
+                0x00000400 => 'NET_SFTP_OPEN_NOFOLLOW',
+                0x00000800 => 'NET_SFTP_OPEN_DELETE_ON_CLOSE',
+                0x00001000 => 'NET_SFTP_OPEN_ACCESS_AUDIT_ALARM_INFO',
+                0x00002000 => 'NET_SFTP_OPEN_ACCESS_BACKUP',
+                0x00004000 => 'NET_SFTP_OPEN_BACKUP_STREAM',
+                0x00008000 => 'NET_SFTP_OPEN_OVERRIDE_OWNER',
+            ];
+            // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2
+            // see \phpseclib3\Net\SFTP::_parseLongname() for an explanation
+            self::$file_types = [
+                1 => 'NET_SFTP_TYPE_REGULAR',
+                2 => 'NET_SFTP_TYPE_DIRECTORY',
+                3 => 'NET_SFTP_TYPE_SYMLINK',
+                4 => 'NET_SFTP_TYPE_SPECIAL',
+                5 => 'NET_SFTP_TYPE_UNKNOWN',
+                // the following types were first defined for use in SFTPv5+
+                // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2
+                6 => 'NET_SFTP_TYPE_SOCKET',
+                7 => 'NET_SFTP_TYPE_CHAR_DEVICE',
+                8 => 'NET_SFTP_TYPE_BLOCK_DEVICE',
+                9 => 'NET_SFTP_TYPE_FIFO'
+            ];
+            self::define_array(
+                self::$packet_types,
+                self::$status_codes,
+                self::$attributes,
+                self::$open_flags,
+                self::$open_flags5,
+                self::$file_types
+            );
+        }
 
         if (!defined('NET_SFTP_QUEUE_SIZE')) {
             define('NET_SFTP_QUEUE_SIZE', 32);
@@ -543,22 +547,7 @@ class SFTP extends SSH2
      */
     private function partial_init_sftp_connection()
     {
-        $this->window_size_server_to_client[self::CHANNEL] = $this->window_size;
-
-        $packet = Strings::packSSH2(
-            'CsN3',
-            NET_SSH2_MSG_CHANNEL_OPEN,
-            'session',
-            self::CHANNEL,
-            $this->window_size,
-            0x4000
-        );
-
-        $this->send_binary_packet($packet);
-
-        $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_OPEN;
-
-        $response = $this->get_channel_packet(self::CHANNEL, true);
+        $response = $this->open_channel(self::CHANNEL, true);
         if ($response === true && $this->isTimeout()) {
             return false;
         }
@@ -815,7 +804,7 @@ class SFTP extends SSH2
             list($status) = Strings::unpackSSH2('N', $response);
         }
 
-        $error = $this->status_codes[$status];
+        $error = self::$status_codes[$status];
 
         if ($this->version > 2) {
             list($message) = Strings::unpackSSH2('s', $response);
@@ -2138,10 +2127,10 @@ class SFTP extends SSH2
 
         if ($start >= 0) {
             $offset = $start;
-        } elseif ($mode & self::RESUME) {
+        } elseif ($mode & (self::RESUME | self::RESUME_START)) {
             // if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called
-            $size = $this->stat($remote_file)['size'];
-            $offset = $size !== false ? $size : 0;
+            $stat = $this->stat($remote_file);
+            $offset = $stat !== false && $stat['size'] ? $stat['size'] : 0;
         } else {
             $offset = 0;
             if ($this->version >= 5) {
@@ -2210,6 +2199,9 @@ class SFTP extends SSH2
             if ($local_start >= 0) {
                 fseek($fp, $local_start);
                 $size -= $local_start;
+            } elseif ($mode & self::RESUME) {
+                fseek($fp, $offset);
+                $size -= $offset;
             }
         } elseif ($dataCallback) {
             $size = 0;
@@ -2497,14 +2489,6 @@ class SFTP extends SSH2
             }
         }
 
-        if ($length > 0 && $length <= $offset - $start) {
-            if ($local_file === false) {
-                $content = substr($content, 0, $length);
-            } else {
-                ftruncate($fp, $length + $res_offset);
-            }
-        }
-
         if ($fclose_check) {
             fclose($fp);
 
@@ -2840,15 +2824,37 @@ class SFTP extends SSH2
         return $this->get_stat_cache_prop($path, 'gid');
     }
 
+    /**
+     * Recursively go through rawlist() output to get the total filesize
+     *
+     * @return int
+     */
+    private static function recursiveFilesize(array $files)
+    {
+        $size = 0;
+        foreach ($files as $name => $file) {
+            if ($name == '.' || $name == '..') {
+                continue;
+            }
+            $size += is_array($file) ?
+                self::recursiveFilesize($file) :
+                $file->size;
+        }
+        return $size;
+    }
+
     /**
      * Gets file size
      *
      * @param string $path
+     * @param bool $recursive
      * @return mixed
      */
-    public function filesize($path)
+    public function filesize($path, $recursive = false)
     {
-        return $this->get_stat_cache_prop($path, 'size');
+        return !$recursive || $this->filetype($path) != 'dir' ?
+            $this->get_stat_cache_prop($path, 'size') :
+            self::recursiveFilesize($this->rawlist($path, true));
     }
 
     /**
@@ -3041,7 +3047,7 @@ class SFTP extends SSH2
             list($flags) = Strings::unpackSSH2('N', $response);
         }
 
-        foreach ($this->attributes as $key => $value) {
+        foreach (self::$attributes as $key => $value) {
             switch ($flags & $key) {
                 case NET_SFTP_ATTR_UIDGID:
                     if ($this->version > 3) {
@@ -3272,7 +3278,7 @@ class SFTP extends SSH2
         $stop = microtime(true);
 
         if (defined('NET_SFTP_LOGGING')) {
-            $packet_type = '-> ' . $this->packet_types[$type] .
+            $packet_type = '-> ' . self::$packet_types[$type] .
                            ' (' . round($stop - $start, 4) . 's)';
             $this->append_log($packet_type, $data);
         }
@@ -3289,6 +3295,7 @@ class SFTP extends SSH2
         $this->use_request_id = false;
         $this->pwd = false;
         $this->requestBuffer = [];
+        $this->partial_init = false;
     }
 
     /**
@@ -3376,7 +3383,7 @@ class SFTP extends SSH2
         $packet = Strings::shift($this->packet_buffer, $length);
 
         if (defined('NET_SFTP_LOGGING')) {
-            $packet_type = '<- ' . $this->packet_types[$this->packet_type] .
+            $packet_type = '<- ' . self::$packet_types[$this->packet_type] .
                            ' (' . round($stop - $start, 4) . 's)';
             $this->append_log($packet_type, $packet);
         }
@@ -3420,7 +3427,7 @@ class SFTP extends SSH2
      *
      * Returns a string if NET_SFTP_LOGGING == self::LOG_COMPLEX, an array if NET_SFTP_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SFTP_LOGGING')
      *
-     * @return array|string
+     * @return array|string|false
      */
     public function getSFTPLog()
     {
@@ -3439,7 +3446,7 @@ class SFTP extends SSH2
     }
 
     /**
-     * Returns all errors
+     * Returns all errors on the SFTP layer
      *
      * @return array
      */
@@ -3449,7 +3456,7 @@ class SFTP extends SSH2
     }
 
     /**
-     * Returns the last error
+     * Returns the last error on the SFTP layer
      *
      * @return string
      */
index 9a0133fdc1bfbf5c41960988b26d1747128f49ae..eee2e108d950894700398ad5d06161caa1de5654 100644 (file)
@@ -553,7 +553,7 @@ class SSH2
      * @var array
      * @access private
      */
-    private $message_numbers = [];
+    private static $message_numbers = [];
 
     /**
      * Disconnection Message 'reason codes' defined in RFC4253
@@ -562,7 +562,7 @@ class SSH2
      * @var array
      * @access private
      */
-    private $disconnect_reasons = [];
+    private static $disconnect_reasons = [];
 
     /**
      * SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254
@@ -571,7 +571,7 @@ class SSH2
      * @var array
      * @access private
      */
-    private $channel_open_failure_reasons = [];
+    private static $channel_open_failure_reasons = [];
 
     /**
      * Terminal Modes
@@ -581,7 +581,7 @@ class SSH2
      * @var array
      * @access private
      */
-    private $terminal_modes = [];
+    private static $terminal_modes = [];
 
     /**
      * SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes
@@ -591,7 +591,7 @@ class SSH2
      * @var array
      * @access private
      */
-    private $channel_extended_data_type_codes = [];
+    private static $channel_extended_data_type_codes = [];
 
     /**
      * Send Sequence Number
@@ -646,6 +646,14 @@ class SSH2
      */
     protected $channel_status = [];
 
+    /**
+     * The identifier of the interactive channel which was opened most recently
+     *
+     * @see self::getInteractiveChannelId()
+     * @var int
+     */
+    private $channel_id_last_interactive = 0;
+
     /**
      * Packet Size
      *
@@ -837,20 +845,6 @@ class SSH2
      */
     private $request_pty = false;
 
-    /**
-     * Flag set while exec() is running when using enablePTY()
-     *
-     * @var bool
-     */
-    private $in_request_pty_exec = false;
-
-    /**
-     * Flag set after startSubsystem() is called
-     *
-     * @var bool
-     */
-    private $in_subsystem;
-
     /**
      * Contents of stdError
      *
@@ -1093,10 +1087,37 @@ class SSH2
      */
     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
@@ -1105,84 +1126,87 @@ class SSH2
      */
     public function __construct($host, $port = 22, $timeout = 10)
     {
-        $this->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',
-            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 = [
-            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 = [
-            1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED'
-        ];
-        $this->terminal_modes = [
-            0 => 'NET_SSH2_TTY_OP_END'
-        ];
-        $this->channel_extended_data_type_codes = [
-            1 => 'NET_SSH2_EXTENDED_DATA_STDERR'
-        ];
+        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'
+            ];
 
-        $this->define_array(
-            $this->message_numbers,
-            $this->disconnect_reasons,
-            $this->channel_open_failure_reasons,
-            $this->terminal_modes,
-            $this->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']
-        );
+            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']
+            );
+        }
 
         /**
          * Typehint is required due to a bug in Psalm: https://github.com/vimeo/psalm/issues/7508
@@ -1192,6 +1216,8 @@ class SSH2
             ? \WeakReference::create($this)
             : $this;
 
+        $this->timeout = $timeout;
+
         if (is_resource($host)) {
             $this->fsock = $host;
             return;
@@ -1200,7 +1226,6 @@ class SSH2
         if (Strings::is_stringable($host)) {
             $this->host = $host;
             $this->port = $port;
-            $this->timeout = $timeout;
         }
     }
 
@@ -1269,6 +1294,32 @@ class SSH2
         $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
      *
@@ -1333,7 +1384,7 @@ class SSH2
                     $start = microtime(true);
                     $sec = (int) floor($this->curTimeout);
                     $usec = (int) (1000000 * ($this->curTimeout - $sec));
-                    if (@stream_select($read, $write, $except, $sec, $usec) === false) {
+                    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;
@@ -1388,6 +1439,18 @@ class SSH2
             throw new UnableToConnectException("Cannot connect to SSH $matches[3] servers");
         }
 
+        // Ubuntu's OpenSSH from 5.8 to 6.9 didn't work with multiple channels. see
+        // https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/1334916 for more info.
+        // https://lists.ubuntu.com/archives/oneiric-changes/2011-July/005772.html discusses
+        // when consolekit was incorporated.
+        // https://marc.info/?l=openssh-unix-dev&m=163409903417589&w=2 discusses some of the
+        // issues with how Ubuntu incorporated consolekit
+        $pattern = '#^SSH-2\.0-OpenSSH_([\d.]+)[^ ]* Ubuntu-.*$#';
+        $match = preg_match($pattern, $this->server_identifier, $matches);
+        $match = $match && version_compare('5.8', $matches[1], '<=');
+        $match = $match && version_compare('6.9', $matches[1], '>=');
+        $this->errorOnMultipleChannels = $match;
+
         if (!$this->send_id_string_first) {
             fputs($this->fsock, $this->identifier . "\r\n");
         }
@@ -1486,6 +1549,8 @@ class SSH2
             $preferred['client_to_server']['comp'] :
             SSH2::getSupportedCompressionAlgorithms();
 
+        $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c', 'kex-strict-c-v00@openssh.com']);
+
         // some SSH servers have buggy implementations of some of the above algorithms
         switch (true) {
             case $this->server_identifier == 'SSH-2.0-SSHD':
@@ -1502,6 +1567,20 @@ class SSH2
                         ['hmac-sha1-96', 'hmac-md5-96']
                     ));
                 }
+                break;
+            case substr($this->server_identifier, 0, 24) == 'SSH-2.0-TurboFTP_SERVER_':
+                if (!isset($preferred['server_to_client']['crypt'])) {
+                    $s2c_encryption_algorithms = array_values(array_diff(
+                        $s2c_encryption_algorithms,
+                        ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com']
+                    ));
+                }
+                if (!isset($preferred['client_to_server']['crypt'])) {
+                    $c2s_encryption_algorithms = array_values(array_diff(
+                        $c2s_encryption_algorithms,
+                        ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com']
+                    ));
+                }
         }
 
         $client_cookie = Random::string(16);
@@ -1526,6 +1605,7 @@ class SSH2
         if ($kexinit_payload_server === false) {
             $this->send_binary_packet($kexinit_payload_client);
 
+            $this->extra_packets = 0;
             $kexinit_payload_server = $this->get_binary_packet();
 
             if (
@@ -1557,6 +1637,11 @@ class SSH2
             $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');
+            }
+        }
 
         $this->supported_private_key_algorithms = $this->server_host_key_algorithms;
 
@@ -1815,6 +1900,10 @@ class SSH2
             throw new \UnexpectedValueException('Expected SSH_MSG_NEWKEYS');
         }
 
+        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);
@@ -2121,13 +2210,15 @@ class SSH2
      * 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 string|AsymmetricKey|array[]|Agent|null ...$args
+     * @param string|PrivateKey|array[]|Agent|null ...$args
      * @return bool
      * @see self::_login()
      */
     public function login($username, ...$args)
     {
-        $this->auth[] = func_get_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
@@ -2146,7 +2237,7 @@ class SSH2
      * Login Helper
      *
      * @param string $username
-     * @param string ...$args
+     * @param string|PrivateKey|array[]|Agent|null ...$args
      * @return bool
      * @see self::_login_helper()
      */
@@ -2266,10 +2357,26 @@ class SSH2
                     return $this->login_helper($username, $password);
                 }
                 $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
-                throw new ConnectionClosedException('Connection closed by server');
+                throw $e;
+            }
+
+            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);
             }
 
-            list($type, $service) = Strings::unpackSSH2('Cs', $response);
+            list($service) = Strings::unpackSSH2('s', $response);
+
             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');
@@ -2545,7 +2652,7 @@ class SSH2
             $privatekey = $privatekey->withPadding(RSA::SIGNATURE_PKCS1);
             $algos = ['rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa'];
             if (isset($this->preferred['hostkey'])) {
-                $algos = array_intersect($this->preferred['hostkey'], $algos);
+                $algos = array_intersect($algos, $this->preferred['hostkey']);
             }
             $algo = self::array_intersect_first($algos, $this->supported_private_key_algorithms);
             switch ($algo) {
@@ -2729,32 +2836,11 @@ class SSH2
             return false;
         }
 
-        if ($this->in_request_pty_exec) {
-            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.');
-        }
+        //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.');
+        //}
 
-        // 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[self::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 = Strings::packSSH2(
-            'CsN3',
-            NET_SSH2_MSG_CHANNEL_OPEN,
-            'session',
-            self::CHANNEL_EXEC,
-            $this->window_size_server_to_client[self::CHANNEL_EXEC],
-            $packet_size
-        );
-        $this->send_binary_packet($packet);
-
-        $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_OPEN;
-
-        $this->get_channel_packet(self::CHANNEL_EXEC);
+        $this->open_channel(self::CHANNEL_EXEC);
 
         if ($this->request_pty === true) {
             $terminal_modes = pack('C', NET_SSH2_TTY_OP_END);
@@ -2779,8 +2865,6 @@ class SSH2
                 $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
@@ -2810,7 +2894,8 @@ class SSH2
 
         $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA;
 
-        if ($this->in_request_pty_exec) {
+        if ($this->request_pty === true) {
+            $this->channel_id_last_interactive = self::CHANNEL_EXEC;
             return true;
         }
 
@@ -2836,37 +2921,80 @@ class SSH2
     }
 
     /**
-     * Creates an interactive shell
+     * How many channels are currently open?
      *
-     * @see self::read()
-     * @see self::write()
+     * @return int
+     */
+    public function getOpenChannelCount()
+    {
+        return $this->channelCount;
+    }
+
+    /**
+     * Opens a channel
+     *
+     * @param string $channel
+     * @param bool $skip_extended
      * @return bool
-     * @throws \UnexpectedValueException on receipt of unexpected packets
-     * @throws \RuntimeException on other errors
      */
-    private function initShell()
+    protected function open_channel($channel, $skip_extended = false)
     {
-        if ($this->in_request_pty_exec === true) {
-            return true;
+        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->window_size_server_to_client[self::CHANNEL_SHELL] = $this->window_size;
+        $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");
+        }
+
+        // RFC4254 defines the (client) window size as "bytes the other party can send before it must wait for the window to
+        // be adjusted".  0x7FFFFFFF is, at 2GB, the max size.  technically, it should probably be decremented, but,
+        // honestly, if you're transferring more than 2GB, you probably shouldn't be using phpseclib, anyway.
+        // see http://tools.ietf.org/html/rfc4254#section-5.2 for more info
+        $this->window_size_server_to_client[$channel] = $this->window_size;
+        // 0x8000 is the maximum max packet size, per http://tools.ietf.org/html/rfc4253#section-6.1, although since PuTTy
+        // uses 0x4000, that's what will be used here, as well.
         $packet_size = 0x4000;
 
         $packet = Strings::packSSH2(
             'CsN3',
             NET_SSH2_MSG_CHANNEL_OPEN,
             'session',
-            self::CHANNEL_SHELL,
-            $this->window_size_server_to_client[self::CHANNEL_SHELL],
+            $channel,
+            $this->window_size_server_to_client[$channel],
             $packet_size
         );
 
         $this->send_binary_packet($packet);
 
-        $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_OPEN;
+        $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_OPEN;
+
+        return $this->get_channel_packet($channel, $skip_extended);
+    }
+
+    /**
+     * Creates an interactive shell
+     *
+     * Returns bool(true) if the shell was opened.
+     * Returns bool(false) if the shell was already open.
+     *
+     * @see self::isShellOpen()
+     * @see self::read()
+     * @see self::write()
+     * @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->get_channel_packet(self::CHANNEL_SHELL);
+        $this->open_channel(self::CHANNEL_SHELL);
 
         $terminal_modes = pack('C', NET_SSH2_TTY_OP_END);
         $packet = Strings::packSSH2(
@@ -2907,14 +3035,18 @@ class SSH2
 
         $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA;
 
+        $this->channel_id_last_interactive = self::CHANNEL_SHELL;
+
         $this->bitmap |= self::MASK_SHELL;
 
         return true;
     }
 
     /**
-     * Return the channel to be used with read() / write()
-     *
+     * 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
@@ -2922,15 +3054,26 @@ class SSH2
     private function get_interactive_channel()
     {
         switch (true) {
-            case $this->in_subsystem:
+            case $this->is_channel_status_data(self::CHANNEL_SUBSYSTEM):
                 return self::CHANNEL_SUBSYSTEM;
-            case $this->in_request_pty_exec:
+            case $this->is_channel_status_data(self::CHANNEL_EXEC):
                 return self::CHANNEL_EXEC;
             default:
                 return self::CHANNEL_SHELL;
         }
     }
 
+    /**
+     * Indicates the DATA status on the given channel
+     *
+     * @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;
+    }
+
     /**
      * Return an available open channel
      *
@@ -2987,26 +3130,41 @@ class SSH2
      * Returns when there's a match for $expect, which can take the form of a string literal or,
      * if $mode == self::READ_REGEX, a regular expression.
      *
+     * If not specifying a channel, an open interactive channel will be selected, or, if there are
+     * no open channels, an interactive shell will be created. If there are multiple open
+     * interactive channels, a legacy behavior will apply in which channel selection prioritizes
+     * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive
+     * channels, callers are discouraged from relying on this legacy behavior and should specify
+     * the intended channel.
+     *
      * @see self::write()
      * @param string $expect
-     * @param int $mode
+     * @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)
+    public function read($expect = '', $mode = self::READ_SIMPLE, $channel = null)
     {
-        $this->curTimeout = $this->timeout;
-        $this->is_timeout = false;
-
         if (!$this->isAuthenticated()) {
             throw new InsufficientSetupException('Operation disallowed prior to login()');
         }
 
-        if (!($this->bitmap & self::MASK_SHELL) && !$this->initShell()) {
-            throw new \RuntimeException('Unable to initiate an interactive shell session');
+        $this->curTimeout = $this->timeout;
+        $this->is_timeout = false;
+
+        if ($channel === null) {
+            $channel = $this->get_interactive_channel();
         }
 
-        $channel = $this->get_interactive_channel();
+        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');
+            }
+        }
 
         if ($mode == self::READ_NEXT) {
             return $this->get_channel_packet($channel);
@@ -3024,7 +3182,6 @@ class SSH2
             }
             $response = $this->get_channel_packet($channel);
             if ($response === true) {
-                $this->in_request_pty_exec = false;
                 return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer));
             }
 
@@ -3035,22 +3192,39 @@ class SSH2
     /**
      * Inputs a command into an interactive shell.
      *
+     * If not specifying a channel, an open interactive channel will be selected, or, if there are
+     * no open channels, an interactive shell will be created. If there are multiple open
+     * interactive channels, a legacy behavior will apply in which channel selection prioritizes
+     * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive
+     * channels, callers are discouraged from relying on this legacy behavior and should specify
+     * the intended channel.
+     *
      * @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)
+    public function write($cmd, $channel = null)
     {
         if (!$this->isAuthenticated()) {
             throw new InsufficientSetupException('Operation disallowed prior to login()');
         }
 
-        if (!($this->bitmap & self::MASK_SHELL) && !$this->initShell()) {
-            throw new \RuntimeException('Unable to initiate an interactive shell session');
+        if ($channel === null) {
+            $channel = $this->get_interactive_channel();
+        }
+
+        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($this->get_interactive_channel(), $cmd);
+        $this->send_channel_packet($channel, $cmd);
     }
 
     /**
@@ -3068,22 +3242,7 @@ class SSH2
      */
     public function startSubsystem($subsystem)
     {
-        $this->window_size_server_to_client[self::CHANNEL_SUBSYSTEM] = $this->window_size;
-
-        $packet = Strings::packSSH2(
-            'CsN3',
-            NET_SSH2_MSG_CHANNEL_OPEN,
-            'session',
-            self::CHANNEL_SUBSYSTEM,
-            $this->window_size,
-            0x4000
-        );
-
-        $this->send_binary_packet($packet);
-
-        $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_OPEN;
-
-        $this->get_channel_packet(self::CHANNEL_SUBSYSTEM);
+        $this->open_channel(self::CHANNEL_SUBSYSTEM);
 
         $packet = Strings::packSSH2(
             'CNsCs',
@@ -3103,8 +3262,7 @@ class SSH2
 
         $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA;
 
-        $this->bitmap |= self::MASK_SHELL;
-        $this->in_subsystem = true;
+        $this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM;
 
         return true;
     }
@@ -3117,8 +3275,9 @@ class SSH2
      */
     public function stopSubsystem()
     {
-        $this->in_subsystem = false;
-        $this->close_channel(self::CHANNEL_SUBSYSTEM);
+        if ($this->isInteractiveChannelOpen(self::CHANNEL_SUBSYSTEM)) {
+            $this->close_channel(self::CHANNEL_SUBSYSTEM);
+        }
         return true;
     }
 
@@ -3127,10 +3286,23 @@ class SSH2
      *
      * 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()
+    public function reset($channel = null)
     {
-        $this->close_channel($this->get_interactive_channel());
+        if ($channel === null) {
+            $channel = $this->get_interactive_channel();
+        }
+        if ($this->isInteractiveChannelOpen($channel)) {
+            $this->close_channel($channel);
+        }
     }
 
     /**
@@ -3172,11 +3344,38 @@ class SSH2
     /**
      * 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()
+    public function isConnected($level = 0)
     {
-        return (bool) ($this->bitmap & self::MASK_CONNECTED);
+        if (!is_int($level) || $level < 0 || $level > 2) {
+            throw new \InvalidArgumentException('$level must be 0, 1 or 2');
+        }
+
+        if ($level == 0) {
+            return ($this->bitmap & self::MASK_CONNECTED) && is_resource($this->fsock) && !feof($this->fsock);
+        }
+        try {
+            if ($level == 1) {
+                $this->send_binary_packet(pack('CN', 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;
+        }
     }
 
     /**
@@ -3189,6 +3388,49 @@ class SSH2
         return (bool) ($this->bitmap & self::MASK_LOGIN);
     }
 
+    /**
+     * Is the interactive shell active?
+     *
+     * @return bool
+     */
+    public function isShellOpen()
+    {
+        return $this->isInteractiveChannelOpen(self::CHANNEL_SHELL);
+    }
+
+    /**
+     * Is the exec pty active?
+     *
+     * @return bool
+     */
+    public function isPTYOpen()
+    {
+        return $this->isInteractiveChannelOpen(self::CHANNEL_EXEC);
+    }
+
+    /**
+     * Is the given interactive channel active?
+     *
+     * @param int $channel Channel id returned by self::getInteractiveChannelId()
+     * @return bool
+     */
+    public function isInteractiveChannelOpen($channel)
+    {
+        return $this->isAuthenticated() && $this->is_channel_status_data($channel);
+    }
+
+    /**
+     * Returns a channel identifier, presently of the last interactive channel opened, regardless of current status.
+     * Returns 0 if no interactive channel has been opened.
+     *
+     * @see self::isInteractiveChannelOpen()
+     * @return int
+     */
+    public function getInteractiveChannelId()
+    {
+        return $this->channel_id_last_interactive;
+    }
+
     /**
      * Pings a server connection, or tries to reconnect if the connection has gone down
      *
@@ -3205,23 +3447,8 @@ class SSH2
             return false;
         }
 
-        $this->window_size_server_to_client[self::CHANNEL_KEEP_ALIVE] = $this->window_size;
-        $packet_size = 0x4000;
-        $packet = Strings::packSSH2(
-            'CsN3',
-            NET_SSH2_MSG_CHANNEL_OPEN,
-            'session',
-            self::CHANNEL_KEEP_ALIVE,
-            $this->window_size_server_to_client[self::CHANNEL_KEEP_ALIVE],
-            $packet_size
-        );
-
         try {
-            $this->send_binary_packet($packet);
-
-            $this->channel_status[self::CHANNEL_KEEP_ALIVE] = NET_SSH2_MSG_CHANNEL_OPEN;
-
-            $response = $this->get_channel_packet(self::CHANNEL_KEEP_ALIVE);
+            $this->open_channel(self::CHANNEL_KEEP_ALIVE);
         } catch (\RuntimeException $e) {
             return $this->reconnect();
         }
@@ -3261,6 +3488,8 @@ class SSH2
         $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;
     }
 
     /**
@@ -3283,9 +3512,9 @@ class SSH2
 
             if (!$this->curTimeout) {
                 if ($this->keepAlive <= 0) {
-                    @stream_select($read, $write, $except, null);
+                    static::stream_select($read, $write, $except, null);
                 } else {
-                    if (!@stream_select($read, $write, $except, $this->keepAlive)) {
+                    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);
                     }
@@ -3299,7 +3528,7 @@ class SSH2
                 $start = microtime(true);
 
                 if ($this->keepAlive > 0 && $this->keepAlive < $this->curTimeout) {
-                    if (!@stream_select($read, $write, $except, $this->keepAlive)) {
+                    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;
@@ -3313,7 +3542,7 @@ class SSH2
                 $usec = (int) (1000000 * ($this->curTimeout - $sec));
 
                 // this can return a "stream_select(): unable to select [4]: Interrupted system call" error
-                if (!@stream_select($read, $write, $except, $sec, $usec)) {
+                if (!static::stream_select($read, $write, $except, $sec, $usec)) {
                     $this->is_timeout = true;
                     return true;
                 }
@@ -3332,6 +3561,11 @@ class SSH2
         }
 
         $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)) {
@@ -3504,7 +3738,7 @@ class SSH2
 
         if (defined('NET_SSH2_LOGGING')) {
             $current = microtime(true);
-            $message_number = isset($this->message_numbers[ord($payload[0])]) ? $this->message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')';
+            $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);
@@ -3586,13 +3820,15 @@ class SSH2
             case NET_SSH2_MSG_DISCONNECT:
                 Strings::shift($payload, 1);
                 list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload);
-                $this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n$message";
+                $this->errors[] = 'SSH_MSG_DISCONNECT: ' . self::$disconnect_reasons[$reason_code] . "\r\n$message";
                 $this->bitmap = 0;
                 return false;
             case NET_SSH2_MSG_IGNORE:
+                $this->extra_packets++;
                 $payload = $this->get_binary_packet($skip_channel_filter);
                 break;
             case NET_SSH2_MSG_DEBUG:
+                $this->extra_packets++;
                 Strings::shift($payload, 2); // second byte is "always_display"
                 list($message) = Strings::unpackSSH2('s', $payload);
                 $this->errors[] = "SSH_MSG_DEBUG: $message";
@@ -3601,6 +3837,7 @@ class SSH2
             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)) {
                         $this->bitmap = 0;
@@ -3773,9 +4010,8 @@ class SSH2
      */
     public function disablePTY()
     {
-        if ($this->in_request_pty_exec) {
+        if ($this->isPTYOpen()) {
             $this->close_channel(self::CHANNEL_EXEC);
-            $this->in_request_pty_exec = false;
         }
         $this->request_pty = false;
     }
@@ -3801,6 +4037,7 @@ class SSH2
      * - 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:
      *
@@ -3968,7 +4205,10 @@ class SSH2
                                 throw new \RuntimeException('Unable to fulfill channel request');
                         }
                     case NET_SSH2_MSG_CHANNEL_CLOSE:
-                        return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->get_channel_packet($client_channel, $skip_extended);
+                        if ($client_channel == $channel && $type == NET_SSH2_MSG_CHANNEL_CLOSE) {
+                            return true;
+                        }
+                        return $this->get_channel_packet($client_channel, $skip_extended);
                 }
             }
 
@@ -4003,14 +4243,15 @@ class SSH2
                 case NET_SSH2_MSG_CHANNEL_CLOSE:
                     $this->curTimeout = 5;
 
-                    if ($this->bitmap & self::MASK_SHELL) {
-                        $this->bitmap &= ~self::MASK_SHELL;
-                    }
+                    $this->close_channel_bitmap($channel);
+
                     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->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE;
+                    $this->channelCount--;
+
                     if ($client_channel == $channel) {
                         return true;
                     }
@@ -4157,7 +4398,7 @@ class SSH2
 
         if (defined('NET_SSH2_LOGGING')) {
             $current = microtime(true);
-            $message_number = isset($this->message_numbers[ord($logged[0])]) ? $this->message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')';
+            $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, $logged);
@@ -4166,7 +4407,10 @@ class SSH2
 
         if (strlen($packet) != $sent) {
             $this->bitmap = 0;
-            throw new \RuntimeException("Only $sent of " . strlen($packet) . " bytes were sent");
+            $message = $sent === false ?
+                'Unable to write ' . strlen($packet) . ' bytes' :
+                "Only $sent of " . strlen($packet) . " bytes were sent";
+            throw new \RuntimeException($message);
         }
     }
 
@@ -4342,22 +4586,36 @@ class SSH2
         }
 
         $this->channel_status[$client_channel] = NET_SSH2_MSG_CHANNEL_CLOSE;
+        $this->channelCount--;
 
         $this->curTimeout = 5;
 
         while (!is_bool($this->get_channel_packet($client_channel))) {
         }
 
-        if ($this->is_timeout) {
-            $this->disconnect();
-        }
-
         if ($want_reply) {
             $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel]));
         }
 
-        if ($this->bitmap & self::MASK_SHELL) {
-            $this->bitmap &= ~self::MASK_SHELL;
+        $this->close_channel_bitmap($client_channel);
+    }
+
+    /**
+     * Maintains execution state bitmap in response to channel closure
+     *
+     * @param int $client_channel The channel number to maintain closure status of
+     * @return void
+     */
+    private function close_channel_bitmap($client_channel)
+    {
+        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;
         }
     }
 
@@ -4395,7 +4653,7 @@ class SSH2
      * @param mixed[] ...$args
      * @access protected
      */
-    protected function define_array(...$args)
+    protected static function define_array(...$args)
     {
         foreach ($args as $arg) {
             foreach ($arg as $key => $value) {
@@ -4501,7 +4759,9 @@ class SSH2
     }
 
     /**
-     * Returns all errors
+     * 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[]
      */
@@ -4511,7 +4771,9 @@ class SSH2
     }
 
     /**
-     * Returns the last error
+     * Returns the last error received on the SSH layer
+     *
+     * If you are looking for messages from the SFTP layer, please see SFTP::getLastSFTPError()
      *
      * @return string
      */
@@ -4800,6 +5062,14 @@ class SSH2
         ];
     }
 
+    /**
+     * Force multiple channels (even if phpseclib has decided to disable them)
+     */
+    public function forceMultipleChannels()
+    {
+        $this->errorOnMultipleChannels = false;
+    }
+
     /**
      * Allows you to set the terminal
      *