@electerm/ssh2 1.11.1 → 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
 
@@ -616,11 +629,26 @@ class Protocol {
616
629
  if (pubKey instanceof Error)
617
630
  throw new Error('Invalid key');
618
631
 
619
- const keyType = pubKey.type;
632
+ let keyType = pubKey.type;
633
+ if (keyType === 'ssh-rsa') {
634
+ for (const algo of ['rsa-sha2-512', 'rsa-sha2-256']) {
635
+ if (this._remoteHostKeyAlgorithms.includes(algo)) {
636
+ keyType = algo;
637
+ break;
638
+ }
639
+ }
640
+ }
620
641
  pubKey = pubKey.getPublicSSH();
621
642
 
643
+ if (typeof keyAlgo === 'function') {
644
+ cbSign = keyAlgo;
645
+ keyAlgo = undefined;
646
+ }
647
+ if (!keyAlgo)
648
+ keyAlgo = keyType;
649
+
622
650
  const userLen = Buffer.byteLength(username);
623
- const algoLen = Buffer.byteLength(keyType);
651
+ const algoLen = Buffer.byteLength(keyAlgo);
624
652
  const pubKeyLen = pubKey.length;
625
653
  const sessionID = this._kex.sessionID;
626
654
  const sesLen = sessionID.length;
@@ -654,7 +682,7 @@ class Protocol {
654
682
  packet[p += 9] = (cbSign ? 1 : 0);
655
683
 
656
684
  writeUInt32BE(packet, algoLen, ++p);
657
- packet.utf8Write(keyType, p += 4, algoLen);
685
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
658
686
 
659
687
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
660
688
  packet.set(pubKey, p += 4);
@@ -697,7 +725,7 @@ class Protocol {
697
725
  packet[p += 9] = 1;
698
726
 
699
727
  writeUInt32BE(packet, algoLen, ++p);
700
- packet.utf8Write(keyType, p += 4, algoLen);
728
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
701
729
 
702
730
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
703
731
  packet.set(pubKey, p += 4);
@@ -705,7 +733,7 @@ class Protocol {
705
733
  writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += pubKeyLen);
706
734
 
707
735
  writeUInt32BE(packet, algoLen, p += 4);
708
- packet.utf8Write(keyType, p += 4, algoLen);
736
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
709
737
 
710
738
  writeUInt32BE(packet, sigLen, p += algoLen);
711
739
  packet.set(signature, p += 4);
@@ -720,7 +748,7 @@ class Protocol {
720
748
  sendPacket(this, this._packetRW.write.finalize(packet));
721
749
  });
722
750
  }
723
- authHostbased(username, pubKey, hostname, userlocal, cbSign) {
751
+ authHostbased(username, pubKey, hostname, userlocal, keyAlgo, cbSign) {
724
752
  // TODO: Make DRY by sharing similar code with authPK()
725
753
  if (this._server)
726
754
  throw new Error('Client-only method called in server mode');
@@ -732,8 +760,15 @@ class Protocol {
732
760
  const keyType = pubKey.type;
733
761
  pubKey = pubKey.getPublicSSH();
734
762
 
763
+ if (typeof keyAlgo === 'function') {
764
+ cbSign = keyAlgo;
765
+ keyAlgo = undefined;
766
+ }
767
+ if (!keyAlgo)
768
+ keyAlgo = keyType;
769
+
735
770
  const userLen = Buffer.byteLength(username);
736
- const algoLen = Buffer.byteLength(keyType);
771
+ const algoLen = Buffer.byteLength(keyAlgo);
737
772
  const pubKeyLen = pubKey.length;
738
773
  const sessionID = this._kex.sessionID;
739
774
  const sesLen = sessionID.length;
@@ -760,7 +795,7 @@ class Protocol {
760
795
  data.utf8Write('hostbased', p += 4, 9);
761
796
 
762
797
  writeUInt32BE(data, algoLen, p += 9);
763
- data.utf8Write(keyType, p += 4, algoLen);
798
+ data.utf8Write(keyAlgo, p += 4, algoLen);
764
799
 
765
800
  writeUInt32BE(data, pubKeyLen, p += algoLen);
766
801
  data.set(pubKey, p += 4);
@@ -787,7 +822,7 @@ class Protocol {
787
822
 
788
823
  writeUInt32BE(packet, 4 + algoLen + 4 + sigLen, p += reqDataLen);
789
824
  writeUInt32BE(packet, algoLen, p += 4);
790
- packet.utf8Write(keyType, p += 4, algoLen);
825
+ packet.utf8Write(keyAlgo, p += 4, algoLen);
791
826
  writeUInt32BE(packet, sigLen, p += algoLen);
792
827
  packet.set(signature, p += 4);
793
828
 
@@ -2074,4 +2109,29 @@ function modesToBytes(modes) {
2074
2109
  return bytes;
2075
2110
  }
2076
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
+
2077
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
  }