@electerm/ssh2 1.11.2 → 1.14.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,10 +215,13 @@ 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(['ext-info-c']);
211
223
  offer = new KexInit(offer);
224
+ }
212
225
  this._kex = undefined;
213
226
  this._kexinit = undefined;
214
227
  this._offer = offer;
@@ -608,7 +621,7 @@ class Protocol {
608
621
 
609
622
  sendPacket(this, this._packetRW.write.finalize(packet));
610
623
  }
611
- authPK(username, pubKey, cbSign) {
624
+ authPK(username, pubKey, keyAlgo, cbSign) {
612
625
  if (this._server)
613
626
  throw new Error('Client-only method called in server mode');
614
627
 
@@ -627,8 +640,15 @@ class Protocol {
627
640
  }
628
641
  pubKey = pubKey.getPublicSSH();
629
642
 
643
+ if (typeof keyAlgo === 'function') {
644
+ cbSign = keyAlgo;
645
+ keyAlgo = undefined;
646
+ }
647
+ if (!keyAlgo)
648
+ keyAlgo = keyType;
649
+
630
650
  const userLen = Buffer.byteLength(username);
631
- const algoLen = Buffer.byteLength(keyType);
651
+ const algoLen = Buffer.byteLength(keyAlgo);
632
652
  const pubKeyLen = pubKey.length;
633
653
  const sessionID = this._kex.sessionID;
634
654
  const sesLen = sessionID.length;
@@ -662,7 +682,7 @@ class Protocol {
662
682
  packet[p += 9] = (cbSign ? 1 : 0);
663
683
 
664
684
  writeUInt32BE(packet, algoLen, ++p);
665
- packet.utf8Write(keyType, p += 4, algoLen);
685
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
666
686
 
667
687
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
668
688
  packet.set(pubKey, p += 4);
@@ -705,7 +725,7 @@ class Protocol {
705
725
  packet[p += 9] = 1;
706
726
 
707
727
  writeUInt32BE(packet, algoLen, ++p);
708
- packet.utf8Write(keyType, p += 4, algoLen);
728
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
709
729
 
710
730
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
711
731
  packet.set(pubKey, p += 4);
@@ -713,7 +733,7 @@ class Protocol {
713
733
  writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);
714
734
 
715
735
  writeUInt32BE(packet, algoLen, p += 4);
716
- packet.utf8Write(keyType, p += 4, algoLen);
736
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
717
737
 
718
738
  writeUInt32BE(packet, sigLen, p += algoLen);
719
739
  packet.set(signature, p += 4);
@@ -728,7 +748,7 @@ class Protocol {
728
748
  sendPacket(this, this._packetRW.write.finalize(packet));
729
749
  });
730
750
  }
731
- authHostbased(username, pubKey, hostname, userlocal, cbSign) {
751
+ authHostbased(username, pubKey, hostname, userlocal, keyAlgo, cbSign) {
732
752
  // TODO: Make DRY by sharing similar code with authPK()
733
753
  if (this._server)
734
754
  throw new Error('Client-only method called in server mode');
@@ -740,8 +760,15 @@ class Protocol {
740
760
  const keyType = pubKey.type;
741
761
  pubKey = pubKey.getPublicSSH();
742
762
 
763
+ if (typeof keyAlgo === 'function') {
764
+ cbSign = keyAlgo;
765
+ keyAlgo = undefined;
766
+ }
767
+ if (!keyAlgo)
768
+ keyAlgo = keyType;
769
+
743
770
  const userLen = Buffer.byteLength(username);
744
- const algoLen = Buffer.byteLength(keyType);
771
+ const algoLen = Buffer.byteLength(keyAlgo);
745
772
  const pubKeyLen = pubKey.length;
746
773
  const sessionID = this._kex.sessionID;
747
774
  const sesLen = sessionID.length;
@@ -768,7 +795,7 @@ class Protocol {
768
795
  data.utf8Write('hostbased', p += 4, 9);
769
796
 
770
797
  writeUInt32BE(data, algoLen, p += 9);
771
- data.utf8Write(keyType, p += 4, algoLen);
798
+ data.utf8Write(keyAlgo, p += 4, algoLen);
772
799
 
773
800
  writeUInt32BE(data, pubKeyLen, p += algoLen);
774
801
  data.set(pubKey, p += 4);
@@ -795,7 +822,7 @@ class Protocol {
795
822
 
796
823
  writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += reqDataLen);
797
824
  writeUInt32BE(packet, algoLen, p += 4);
798
- packet.utf8Write(keyType, p += 4, algoLen);
825
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
799
826
  writeUInt32BE(packet, sigLen, p += algoLen);
800
827
  packet.set(signature, p += 4);
801
828
 
@@ -2082,4 +2109,29 @@ function modesToBytes(modes) {
2082
2109
  return bytes;
2083
2110
  }
2084
2111
 
2112
+ function sendExtInfo(proto) {
2113
+ let serverSigAlgs =
2114
+ 'ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521'
2115
+ + 'rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss';
2116
+ if (eddsaSupported)
2117
+ serverSigAlgs = `ssh-ed25519,${serverSigAlgs}`;
2118
+ const algsLen = Buffer.byteLength(serverSigAlgs);
2119
+
2120
+ let p = proto._packetRW.write.allocStart;
2121
+ const packet = proto._packetRW.write.alloc(1 + 4 + 4 + 15 + 4 + algsLen);
2122
+
2123
+ packet[p] = MESSAGE.EXT_INFO;
2124
+
2125
+ writeUInt32BE(packet, 1, ++p);
2126
+
2127
+ writeUInt32BE(packet, 15, p += 4);
2128
+ packet.utf8Write('server-sig-algs', p += 4, 15);
2129
+
2130
+ writeUInt32BE(packet, algsLen, p += 15);
2131
+ packet.utf8Write(serverSigAlgs, p += 4, algsLen);
2132
+
2133
+ proto._debug && proto._debug('Outbound: Sending EXT_INFO');
2134
+ sendPacket(proto, proto._packetRW.write.finalize(packet));
2135
+ }
2136
+
2085
2137
  module.exports = Protocol;
@@ -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
@@ -146,6 +146,48 @@ module.exports = {
146
146
  const handler = self._handlers.SERVICE_ACCEPT;
147
147
  handler && handler(self, name);
148
148
  },
149
+ [MESSAGE.EXT_INFO]: (self, payload) => {
150
+ /*
151
+ byte SSH_MSG_EXT_INFO
152
+ uint32 nr-extensions
153
+ repeat the following 2 fields "nr-extensions" times:
154
+ string extension-name
155
+ string extension-value (binary)
156
+ */
157
+ bufferParser.init(payload, 1);
158
+ const numExts = bufferParser.readUInt32BE();
159
+ let exts;
160
+ if (numExts !== undefined) {
161
+ exts = [];
162
+ for (let i = 0; i < numExts; ++i) {
163
+ const name = bufferParser.readString(true);
164
+ const data = bufferParser.readString();
165
+ if (data !== undefined) {
166
+ switch (name) {
167
+ case 'server-sig-algs': {
168
+ const algs = data.latin1Slice(0, data.length).split(',');
169
+ exts.push({ name, algs });
170
+ continue;
171
+ }
172
+ default:
173
+ continue;
174
+ }
175
+ }
176
+ // Malformed
177
+ exts = undefined;
178
+ break;
179
+ }
180
+ }
181
+ bufferParser.clear();
182
+
183
+ if (exts === undefined)
184
+ return doFatalError(self, 'Inbound: Malformed EXT_INFO packet');
185
+
186
+ self._debug && self._debug('Inbound: Received EXT_INFO');
187
+
188
+ const handler = self._handlers.EXT_INFO;
189
+ handler && handler(self, exts);
190
+ },
149
191
 
150
192
  // User auth protocol -- generic =============================================
151
193
  [MESSAGE.USERAUTH_REQUEST]: (self, payload) => {
@@ -195,7 +237,21 @@ module.exports = {
195
237
  const hasSig = bufferParser.readBool();
196
238
  if (hasSig !== undefined) {
197
239
  const keyAlgo = bufferParser.readString(true);
240
+ let realKeyAlgo = keyAlgo;
198
241
  const key = bufferParser.readString();
242
+
243
+ let hashAlgo;
244
+ switch (keyAlgo) {
245
+ case 'rsa-sha2-256':
246
+ realKeyAlgo = 'ssh-rsa';
247
+ hashAlgo = 'sha256';
248
+ break;
249
+ case 'rsa-sha2-512':
250
+ realKeyAlgo = 'ssh-rsa';
251
+ hashAlgo = 'sha512';
252
+ break;
253
+ }
254
+
199
255
  if (hasSig) {
200
256
  const blobEnd = bufferParser.pos();
201
257
  let signature = bufferParser.readString();
@@ -206,7 +262,7 @@ module.exports = {
206
262
  signature = bufferSlice(signature, 4 + keyAlgo.length + 4);
207
263
  }
208
264
 
209
- signature = sigSSHToASN1(signature, keyAlgo);
265
+ signature = sigSSHToASN1(signature, realKeyAlgo);
210
266
  if (signature) {
211
267
  const sessionID = self._kex.sessionID;
212
268
  const blob = Buffer.allocUnsafe(4 + sessionID.length + blobEnd);
@@ -217,15 +273,16 @@ module.exports = {
217
273
  4 + sessionID.length
218
274
  );
219
275
  methodData = {
220
- keyAlgo,
276
+ keyAlgo: realKeyAlgo,
221
277
  key,
222
278
  signature,
223
279
  blob,
280
+ hashAlgo,
224
281
  };
225
282
  }
226
283
  }
227
284
  } else {
228
- methodData = { keyAlgo, key };
285
+ methodData = { keyAlgo: realKeyAlgo, key, hashAlgo };
229
286
  methodDesc = 'publickey -- check';
230
287
  }
231
288
  }
@@ -241,10 +298,23 @@ module.exports = {
241
298
  string signature
242
299
  */
243
300
  const keyAlgo = bufferParser.readString(true);
301
+ let realKeyAlgo = keyAlgo;
244
302
  const key = bufferParser.readString();
245
303
  const localHostname = bufferParser.readString(true);
246
304
  const localUsername = bufferParser.readString(true);
247
305
 
306
+ let hashAlgo;
307
+ switch (keyAlgo) {
308
+ case 'rsa-sha2-256':
309
+ realKeyAlgo = 'ssh-rsa';
310
+ hashAlgo = 'sha256';
311
+ break;
312
+ case 'rsa-sha2-512':
313
+ realKeyAlgo = 'ssh-rsa';
314
+ hashAlgo = 'sha512';
315
+ break;
316
+ }
317
+
248
318
  const blobEnd = bufferParser.pos();
249
319
  let signature = bufferParser.readString();
250
320
  if (signature !== undefined) {
@@ -254,7 +324,7 @@ module.exports = {
254
324
  signature = bufferSlice(signature, 4 + keyAlgo.length + 4);
255
325
  }
256
326
 
257
- signature = sigSSHToASN1(signature, keyAlgo);
327
+ signature = sigSSHToASN1(signature, realKeyAlgo);
258
328
  if (signature !== undefined) {
259
329
  const sessionID = self._kex.sessionID;
260
330
  const blob = Buffer.allocUnsafe(4 + sessionID.length + blobEnd);
@@ -265,12 +335,13 @@ module.exports = {
265
335
  4 + sessionID.length
266
336
  );
267
337
  methodData = {
268
- keyAlgo,
338
+ keyAlgo: realKeyAlgo,
269
339
  key,
270
340
  signature,
271
341
  blob,
272
342
  localHostname,
273
343
  localUsername,
344
+ hashAlgo
274
345
  };
275
346
  }
276
347
  }