@electerm/ssh2 1.11.2 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,12 +44,14 @@ const { bindingAvailable, NullCipher, NullDecipher } = require('./crypto.js');
44
44
  const {
45
45
  COMPAT_CHECKS,
46
46
  DISCONNECT_REASON,
47
+ eddsaSupported,
47
48
  MESSAGE,
48
49
  SIGNALS,
49
50
  TERMINAL_MODE,
50
51
  } = require('./constants.js');
51
52
  const {
52
- DEFAULT_KEXINIT,
53
+ DEFAULT_KEXINIT_CLIENT,
54
+ DEFAULT_KEXINIT_SERVER,
53
55
  KexInit,
54
56
  kexinit,
55
57
  onKEXPayload,
@@ -138,8 +140,13 @@ class Protocol {
138
140
  let onHandshakeComplete = config.onHandshakeComplete;
139
141
  if (typeof onHandshakeComplete !== 'function')
140
142
  onHandshakeComplete = noop;
143
+ let firstHandshake;
141
144
  this._onHandshakeComplete = (...args) => {
142
145
  this._debug && this._debug('Handshake completed');
146
+ if (firstHandshake === undefined)
147
+ firstHandshake = true;
148
+ else
149
+ firstHandshake = false;
143
150
 
144
151
  // Process packets queued during a rekey where necessary
145
152
  const oldQueue = this._queue;
@@ -165,6 +172,9 @@ class Protocol {
165
172
  this._debug && this._debug('... finished draining outbound queue');
166
173
  }
167
174
 
175
+ if (firstHandshake && this._server && this._kex.remoteExtInfoEnabled)
176
+ sendExtInfo(this);
177
+
168
178
  onHandshakeComplete(...args);
169
179
  };
170
180
  this._queue = undefined;
@@ -205,11 +215,21 @@ class Protocol {
205
215
  }
206
216
 
207
217
  let offer = config.offer;
208
- if (typeof offer !== 'object' || offer === null)
209
- offer = DEFAULT_KEXINIT;
210
- else if (offer.constructor !== KexInit)
218
+ if (typeof offer !== 'object' || offer === null) {
219
+ offer = (this._server ? DEFAULT_KEXINIT_SERVER : DEFAULT_KEXINIT_CLIENT);
220
+ } else if (offer.constructor !== KexInit) {
221
+ if (this._server) {
222
+ offer.kex = offer.kex.concat(['kex-strict-s-v00@openssh.com']);
223
+ } else {
224
+ offer.kex = offer.kex.concat([
225
+ 'ext-info-c',
226
+ 'kex-strict-c-v00@openssh.com',
227
+ ]);
228
+ }
211
229
  offer = new KexInit(offer);
230
+ }
212
231
  this._kex = undefined;
232
+ this._strictMode = undefined;
213
233
  this._kexinit = undefined;
214
234
  this._offer = offer;
215
235
  this._cipher = new NullCipher(0, this._onWrite);
@@ -608,7 +628,7 @@ class Protocol {
608
628
 
609
629
  sendPacket(this, this._packetRW.write.finalize(packet));
610
630
  }
611
- authPK(username, pubKey, cbSign) {
631
+ authPK(username, pubKey, keyAlgo, cbSign) {
612
632
  if (this._server)
613
633
  throw new Error('Client-only method called in server mode');
614
634
 
@@ -627,8 +647,15 @@ class Protocol {
627
647
  }
628
648
  pubKey = pubKey.getPublicSSH();
629
649
 
650
+ if (typeof keyAlgo === 'function') {
651
+ cbSign = keyAlgo;
652
+ keyAlgo = undefined;
653
+ }
654
+ if (!keyAlgo)
655
+ keyAlgo = keyType;
656
+
630
657
  const userLen = Buffer.byteLength(username);
631
- const algoLen = Buffer.byteLength(keyType);
658
+ const algoLen = Buffer.byteLength(keyAlgo);
632
659
  const pubKeyLen = pubKey.length;
633
660
  const sessionID = this._kex.sessionID;
634
661
  const sesLen = sessionID.length;
@@ -662,7 +689,7 @@ class Protocol {
662
689
  packet[p += 9] = (cbSign ? 1 : 0);
663
690
 
664
691
  writeUInt32BE(packet, algoLen, ++p);
665
- packet.utf8Write(keyType, p += 4, algoLen);
692
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
666
693
 
667
694
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
668
695
  packet.set(pubKey, p += 4);
@@ -705,7 +732,7 @@ class Protocol {
705
732
  packet[p += 9] = 1;
706
733
 
707
734
  writeUInt32BE(packet, algoLen, ++p);
708
- packet.utf8Write(keyType, p += 4, algoLen);
735
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
709
736
 
710
737
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
711
738
  packet.set(pubKey, p += 4);
@@ -713,7 +740,7 @@ class Protocol {
713
740
  writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);
714
741
 
715
742
  writeUInt32BE(packet, algoLen, p += 4);
716
- packet.utf8Write(keyType, p += 4, algoLen);
743
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
717
744
 
718
745
  writeUInt32BE(packet, sigLen, p += algoLen);
719
746
  packet.set(signature, p += 4);
@@ -728,7 +755,7 @@ class Protocol {
728
755
  sendPacket(this, this._packetRW.write.finalize(packet));
729
756
  });
730
757
  }
731
- authHostbased(username, pubKey, hostname, userlocal, cbSign) {
758
+ authHostbased(username, pubKey, hostname, userlocal, keyAlgo, cbSign) {
732
759
  // TODO: Make DRY by sharing similar code with authPK()
733
760
  if (this._server)
734
761
  throw new Error('Client-only method called in server mode');
@@ -740,8 +767,15 @@ class Protocol {
740
767
  const keyType = pubKey.type;
741
768
  pubKey = pubKey.getPublicSSH();
742
769
 
770
+ if (typeof keyAlgo === 'function') {
771
+ cbSign = keyAlgo;
772
+ keyAlgo = undefined;
773
+ }
774
+ if (!keyAlgo)
775
+ keyAlgo = keyType;
776
+
743
777
  const userLen = Buffer.byteLength(username);
744
- const algoLen = Buffer.byteLength(keyType);
778
+ const algoLen = Buffer.byteLength(keyAlgo);
745
779
  const pubKeyLen = pubKey.length;
746
780
  const sessionID = this._kex.sessionID;
747
781
  const sesLen = sessionID.length;
@@ -768,7 +802,7 @@ class Protocol {
768
802
  data.utf8Write('hostbased', p += 4, 9);
769
803
 
770
804
  writeUInt32BE(data, algoLen, p += 9);
771
- data.utf8Write(keyType, p += 4, algoLen);
805
+ data.utf8Write(keyAlgo, p += 4, algoLen);
772
806
 
773
807
  writeUInt32BE(data, pubKeyLen, p += algoLen);
774
808
  data.set(pubKey, p += 4);
@@ -795,7 +829,7 @@ class Protocol {
795
829
 
796
830
  writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += reqDataLen);
797
831
  writeUInt32BE(packet, algoLen, p += 4);
798
- packet.utf8Write(keyType, p += 4, algoLen);
832
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
799
833
  writeUInt32BE(packet, sigLen, p += algoLen);
800
834
  packet.set(signature, p += 4);
801
835
 
@@ -2082,4 +2116,29 @@ function modesToBytes(modes) {
2082
2116
  return bytes;
2083
2117
  }
2084
2118
 
2119
+ function sendExtInfo(proto) {
2120
+ let serverSigAlgs =
2121
+ 'ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521'
2122
+ + 'rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss';
2123
+ if (eddsaSupported)
2124
+ serverSigAlgs = `ssh-ed25519,${serverSigAlgs}`;
2125
+ const algsLen = Buffer.byteLength(serverSigAlgs);
2126
+
2127
+ let p = proto._packetRW.write.allocStart;
2128
+ const packet = proto._packetRW.write.alloc(1 + 4 + 4 + 15 + 4 + algsLen);
2129
+
2130
+ packet[p] = MESSAGE.EXT_INFO;
2131
+
2132
+ writeUInt32BE(packet, 1, ++p);
2133
+
2134
+ writeUInt32BE(packet, 15, p += 4);
2135
+ packet.utf8Write('server-sig-algs', p += 4, 15);
2136
+
2137
+ writeUInt32BE(packet, algsLen, p += 15);
2138
+ packet.utf8Write(serverSigAlgs, p += 4, algsLen);
2139
+
2140
+ proto._debug && proto._debug('Outbound: Sending EXT_INFO');
2141
+ sendPacket(proto, proto._packetRW.write.finalize(packet));
2142
+ }
2143
+
2085
2144
  module.exports = Protocol;
@@ -7,7 +7,7 @@ const {
7
7
  Readable: ReadableStream,
8
8
  Writable: WritableStream
9
9
  } = require('stream');
10
- const { inherits, isDate } = require('util');
10
+ const { inherits, types: { isDate } } = require('util');
11
11
 
12
12
  const FastBuffer = Buffer[Symbol.species];
13
13
 
@@ -1588,7 +1588,17 @@ class SFTP extends EventEmitter {
1588
1588
  writeUInt32BE(buf, pathLen, p += 20);
1589
1589
  buf.utf8Write(path, p += 4, pathLen);
1590
1590
 
1591
- this._requests[reqid] = { cb };
1591
+ this._requests[reqid] = {
1592
+ cb: (err, names) => {
1593
+ if (typeof cb !== 'function')
1594
+ return;
1595
+ if (err)
1596
+ return cb(err);
1597
+ if (!names || !names.length)
1598
+ return cb(new Error('Response missing expanded path'));
1599
+ cb(undefined, names[0].filename);
1600
+ }
1601
+ };
1592
1602
 
1593
1603
  const isBuffered = sendOrBuffer(this, buf);
1594
1604
  if (this._debug) {
@@ -1681,6 +1691,146 @@ class SFTP extends EventEmitter {
1681
1691
  this._debug(`SFTP: Outbound: ${status} copy-data`);
1682
1692
  }
1683
1693
  }
1694
+ ext_home_dir(username, cb) {
1695
+ if (this.server)
1696
+ throw new Error('Client-only method called in server mode');
1697
+
1698
+ const ext = this._extensions['home-directory'];
1699
+ if (ext !== '1')
1700
+ throw new Error('Server does not support this extended request');
1701
+
1702
+ if (typeof username !== 'string')
1703
+ throw new TypeError('username is not a string');
1704
+
1705
+ /*
1706
+ uint32 id
1707
+ string "home-directory"
1708
+ string username
1709
+ */
1710
+ let p = 0;
1711
+ const usernameLen = Buffer.byteLength(username);
1712
+ const buf = Buffer.allocUnsafe(
1713
+ 4 + 1
1714
+ + 4
1715
+ + 4 + 14
1716
+ + 4 + usernameLen
1717
+ );
1718
+
1719
+ writeUInt32BE(buf, buf.length - 4, p);
1720
+ p += 4;
1721
+
1722
+ buf[p] = REQUEST.EXTENDED;
1723
+ ++p;
1724
+
1725
+ const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
1726
+ writeUInt32BE(buf, reqid, p);
1727
+ p += 4;
1728
+
1729
+ writeUInt32BE(buf, 14, p);
1730
+ p += 4;
1731
+ buf.utf8Write('home-directory', p, 14);
1732
+ p += 14;
1733
+
1734
+ writeUInt32BE(buf, usernameLen, p);
1735
+ p += 4;
1736
+ buf.utf8Write(username, p, usernameLen);
1737
+ p += usernameLen;
1738
+
1739
+ this._requests[reqid] = {
1740
+ cb: (err, names) => {
1741
+ if (typeof cb !== 'function')
1742
+ return;
1743
+ if (err)
1744
+ return cb(err);
1745
+ if (!names || !names.length)
1746
+ return cb(new Error('Response missing home directory'));
1747
+ cb(undefined, names[0].filename);
1748
+ }
1749
+ };
1750
+
1751
+ const isBuffered = sendOrBuffer(this, buf);
1752
+ if (this._debug) {
1753
+ const status = (isBuffered ? 'Buffered' : 'Sending');
1754
+ this._debug(`SFTP: Outbound: ${status} home-directory`);
1755
+ }
1756
+ }
1757
+ ext_users_groups(uids, gids, cb) {
1758
+ if (this.server)
1759
+ throw new Error('Client-only method called in server mode');
1760
+
1761
+ const ext = this._extensions['users-groups-by-id@openssh.com'];
1762
+ if (ext !== '1')
1763
+ throw new Error('Server does not support this extended request');
1764
+
1765
+ if (!Array.isArray(uids))
1766
+ throw new TypeError('uids is not an array');
1767
+ for (const val of uids) {
1768
+ if (!Number.isInteger(val) || val < 0 || val > (2 ** 32 - 1))
1769
+ throw new Error('uid values must all be 32-bit unsigned integers');
1770
+ }
1771
+ if (!Array.isArray(gids))
1772
+ throw new TypeError('gids is not an array');
1773
+ for (const val of gids) {
1774
+ if (!Number.isInteger(val) || val < 0 || val > (2 ** 32 - 1))
1775
+ throw new Error('gid values must all be 32-bit unsigned integers');
1776
+ }
1777
+
1778
+ /*
1779
+ uint32 id
1780
+ string "users-groups-by-id@openssh.com"
1781
+ string uids
1782
+ uint32 uid1
1783
+ ...
1784
+ string gids
1785
+ uint32 gid1
1786
+ ...
1787
+ */
1788
+ let p = 0;
1789
+ const buf = Buffer.allocUnsafe(
1790
+ 4 + 1
1791
+ + 4
1792
+ + 4 + 30
1793
+ + 4 + (4 * uids.length)
1794
+ + 4 + (4 * gids.length)
1795
+ );
1796
+
1797
+ writeUInt32BE(buf, buf.length - 4, p);
1798
+ p += 4;
1799
+
1800
+ buf[p] = REQUEST.EXTENDED;
1801
+ ++p;
1802
+
1803
+ const reqid = this._writeReqid = (this._writeReqid + 1) & MAX_REQID;
1804
+ writeUInt32BE(buf, reqid, p);
1805
+ p += 4;
1806
+
1807
+ writeUInt32BE(buf, 30, p);
1808
+ p += 4;
1809
+ buf.utf8Write('users-groups-by-id@openssh.com', p, 30);
1810
+ p += 30;
1811
+
1812
+ writeUInt32BE(buf, 4 * uids.length, p);
1813
+ p += 4;
1814
+ for (const val of uids) {
1815
+ writeUInt32BE(buf, val, p);
1816
+ p += 4;
1817
+ }
1818
+
1819
+ writeUInt32BE(buf, 4 * gids.length, p);
1820
+ p += 4;
1821
+ for (const val of gids) {
1822
+ writeUInt32BE(buf, val, p);
1823
+ p += 4;
1824
+ }
1825
+
1826
+ this._requests[reqid] = { extended: 'users-groups-by-id@openssh.com', cb };
1827
+
1828
+ const isBuffered = sendOrBuffer(this, buf);
1829
+ if (this._debug) {
1830
+ const status = (isBuffered ? 'Buffered' : 'Sending');
1831
+ this._debug(`SFTP: Outbound: ${status} users-groups-by-id@openssh.com`);
1832
+ }
1833
+ }
1684
1834
  // ===========================================================================
1685
1835
  // Server-specific ===========================================================
1686
1836
  // ===========================================================================
@@ -2928,6 +3078,44 @@ const CLIENT_HANDLERS = {
2928
3078
  req.cb(undefined, limits);
2929
3079
  return;
2930
3080
  }
3081
+ case 'users-groups-by-id@openssh.com': {
3082
+ /*
3083
+ string usernames
3084
+ string username1
3085
+ ...
3086
+ string groupnames
3087
+ string groupname1
3088
+ ...
3089
+ */
3090
+ const usernameCount = bufferParser.readUInt32BE();
3091
+ if (usernameCount === undefined)
3092
+ break;
3093
+ const usernames = new Array(usernameCount);
3094
+ for (let i = 0; i < usernames.length; ++i)
3095
+ usernames[i] = bufferParser.readString(true);
3096
+
3097
+ const groupnameCount = bufferParser.readUInt32BE();
3098
+ if (groupnameCount === undefined)
3099
+ break;
3100
+ const groupnames = new Array(groupnameCount);
3101
+ for (let i = 0; i < groupnames.length; ++i)
3102
+ groupnames[i] = bufferParser.readString(true);
3103
+ if (groupnames.length > 0
3104
+ && groupnames[groupnames.length - 1] === undefined) {
3105
+ break;
3106
+ }
3107
+
3108
+ if (sftp._debug) {
3109
+ sftp._debug(
3110
+ 'SFTP: Inbound: Received EXTENDED_REPLY '
3111
+ + `(id:${reqID}, ${req.extended})`
3112
+ );
3113
+ }
3114
+ bufferParser.clear();
3115
+ if (typeof req.cb === 'function')
3116
+ req.cb(undefined, usernames, groupnames);
3117
+ return;
3118
+ }
2931
3119
  default:
2932
3120
  // Unknown extended request
2933
3121
  sftp._debug && sftp._debug(
@@ -159,6 +159,7 @@ const COMPAT = {
159
159
  OLD_EXIT: 1 << 1,
160
160
  DYN_RPORT_BUG: 1 << 2,
161
161
  BUG_DHGEX_LARGE: 1 << 3,
162
+ IMPLY_RSA_SHA2_SIGALGS: 1 << 4,
162
163
  };
163
164
 
164
165
  module.exports = {
@@ -170,6 +171,7 @@ module.exports = {
170
171
  DEBUG: 4,
171
172
  SERVICE_REQUEST: 5,
172
173
  SERVICE_ACCEPT: 6,
174
+ EXT_INFO: 7, // RFC 8308
173
175
 
174
176
  // Transport layer protocol -- algorithm negotiation (20-29)
175
177
  KEXINIT: 20,
@@ -327,9 +329,10 @@ module.exports = {
327
329
  COMPAT,
328
330
  COMPAT_CHECKS: [
329
331
  [ 'Cisco-1.25', COMPAT.BAD_DHGEX ],
330
- [ /^Cisco-1\./, COMPAT.BUG_DHGEX_LARGE ],
332
+ [ /^Cisco-1[.]/, COMPAT.BUG_DHGEX_LARGE ],
331
333
  [ /^[0-9.]+$/, COMPAT.OLD_EXIT ], // old SSH.com implementations
332
- [ /^OpenSSH_5\.\d+/, COMPAT.DYN_RPORT_BUG ],
334
+ [ /^OpenSSH_5[.][0-9]+/, COMPAT.DYN_RPORT_BUG ],
335
+ [ /^OpenSSH_7[.]4/, COMPAT.IMPLY_RSA_SHA2_SIGALGS ],
333
336
  ],
334
337
 
335
338
  // KEX proposal-related