@electerm/ssh2 0.8.11 → 1.5.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.
- package/README.md +8 -1
- package/install.js +20 -0
- package/lib/Channel.js +236 -450
- package/lib/agent.js +1080 -376
- package/lib/client.js +1698 -1258
- package/lib/http-agents.js +72 -51
- package/lib/index.js +43 -0
- package/lib/protocol/Protocol.js +2077 -0
- package/lib/protocol/SFTP.js +3778 -0
- package/lib/protocol/constants.js +342 -0
- package/lib/protocol/crypto/binding.gyp +14 -0
- package/lib/protocol/crypto/poly1305.js +43 -0
- package/lib/protocol/crypto/src/binding.cc +2003 -0
- package/lib/protocol/crypto.js +1602 -0
- package/lib/protocol/handlers.js +16 -0
- package/lib/protocol/handlers.misc.js +1214 -0
- package/lib/protocol/kex.js +1831 -0
- package/lib/protocol/keyParser.js +1481 -0
- package/lib/protocol/node-fs-compat.js +115 -0
- package/lib/protocol/utils.js +356 -0
- package/lib/protocol/zlib.js +255 -0
- package/lib/server.js +1226 -1019
- package/lib/utils.js +336 -0
- package/package.json +42 -9
- package/lib/SFTPWrapper.js +0 -145
- package/lib/buffer-helpers.js +0 -22
- package/lib/keepalivemgr.js +0 -80
|
@@ -0,0 +1,1831 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createDiffieHellman,
|
|
5
|
+
createDiffieHellmanGroup,
|
|
6
|
+
createECDH,
|
|
7
|
+
createHash,
|
|
8
|
+
createPublicKey,
|
|
9
|
+
diffieHellman,
|
|
10
|
+
generateKeyPairSync,
|
|
11
|
+
randomFillSync,
|
|
12
|
+
} = require('crypto');
|
|
13
|
+
|
|
14
|
+
const { Ber } = require('asn1');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
COMPAT,
|
|
18
|
+
curve25519Supported,
|
|
19
|
+
DEFAULT_KEX,
|
|
20
|
+
DEFAULT_SERVER_HOST_KEY,
|
|
21
|
+
DEFAULT_CIPHER,
|
|
22
|
+
DEFAULT_MAC,
|
|
23
|
+
DEFAULT_COMPRESSION,
|
|
24
|
+
DISCONNECT_REASON,
|
|
25
|
+
MESSAGE,
|
|
26
|
+
} = require('./constants.js');
|
|
27
|
+
const {
|
|
28
|
+
CIPHER_INFO,
|
|
29
|
+
createCipher,
|
|
30
|
+
createDecipher,
|
|
31
|
+
MAC_INFO,
|
|
32
|
+
} = require('./crypto.js');
|
|
33
|
+
const { parseDERKey } = require('./keyParser.js');
|
|
34
|
+
const {
|
|
35
|
+
bufferFill,
|
|
36
|
+
bufferParser,
|
|
37
|
+
convertSignature,
|
|
38
|
+
doFatalError,
|
|
39
|
+
FastBuffer,
|
|
40
|
+
sigSSHToASN1,
|
|
41
|
+
writeUInt32BE,
|
|
42
|
+
} = require('./utils.js');
|
|
43
|
+
const {
|
|
44
|
+
PacketReader,
|
|
45
|
+
PacketWriter,
|
|
46
|
+
ZlibPacketReader,
|
|
47
|
+
ZlibPacketWriter,
|
|
48
|
+
} = require('./zlib.js');
|
|
49
|
+
|
|
50
|
+
let MESSAGE_HANDLERS;
|
|
51
|
+
|
|
52
|
+
const GEX_MIN_BITS = 2048; // RFC 8270
|
|
53
|
+
const GEX_MAX_BITS = 8192; // RFC 8270
|
|
54
|
+
|
|
55
|
+
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
56
|
+
|
|
57
|
+
// Client/Server
|
|
58
|
+
function kexinit(self) {
|
|
59
|
+
/*
|
|
60
|
+
byte SSH_MSG_KEXINIT
|
|
61
|
+
byte[16] cookie (random bytes)
|
|
62
|
+
name-list kex_algorithms
|
|
63
|
+
name-list server_host_key_algorithms
|
|
64
|
+
name-list encryption_algorithms_client_to_server
|
|
65
|
+
name-list encryption_algorithms_server_to_client
|
|
66
|
+
name-list mac_algorithms_client_to_server
|
|
67
|
+
name-list mac_algorithms_server_to_client
|
|
68
|
+
name-list compression_algorithms_client_to_server
|
|
69
|
+
name-list compression_algorithms_server_to_client
|
|
70
|
+
name-list languages_client_to_server
|
|
71
|
+
name-list languages_server_to_client
|
|
72
|
+
boolean first_kex_packet_follows
|
|
73
|
+
uint32 0 (reserved for future extension)
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
let payload;
|
|
77
|
+
if (self._compatFlags & COMPAT.BAD_DHGEX) {
|
|
78
|
+
const entry = self._offer.lists.kex;
|
|
79
|
+
let kex = entry.array;
|
|
80
|
+
let found = false;
|
|
81
|
+
for (let i = 0; i < kex.length; ++i) {
|
|
82
|
+
if (kex[i].includes('group-exchange')) {
|
|
83
|
+
if (!found) {
|
|
84
|
+
found = true;
|
|
85
|
+
// Copy array lazily
|
|
86
|
+
kex = kex.slice();
|
|
87
|
+
}
|
|
88
|
+
kex.splice(i--, 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (found) {
|
|
92
|
+
let len = 1 + 16 + self._offer.totalSize + 1 + 4;
|
|
93
|
+
const newKexBuf = Buffer.from(kex.join(','));
|
|
94
|
+
len -= (entry.buffer.length - newKexBuf.length);
|
|
95
|
+
|
|
96
|
+
const all = self._offer.lists.all;
|
|
97
|
+
const rest = new Uint8Array(
|
|
98
|
+
all.buffer,
|
|
99
|
+
all.byteOffset + 4 + entry.buffer.length,
|
|
100
|
+
all.length - (4 + entry.buffer.length)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
payload = Buffer.allocUnsafe(len);
|
|
104
|
+
writeUInt32BE(payload, newKexBuf.length, 17);
|
|
105
|
+
payload.set(newKexBuf, 17 + 4);
|
|
106
|
+
payload.set(rest, 17 + 4 + newKexBuf.length);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (payload === undefined) {
|
|
111
|
+
payload = Buffer.allocUnsafe(1 + 16 + self._offer.totalSize + 1 + 4);
|
|
112
|
+
self._offer.copyAllTo(payload, 17);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
self._debug && self._debug('Outbound: Sending KEXINIT');
|
|
116
|
+
|
|
117
|
+
payload[0] = MESSAGE.KEXINIT;
|
|
118
|
+
randomFillSync(payload, 1, 16);
|
|
119
|
+
|
|
120
|
+
// Zero-fill first_kex_packet_follows and reserved bytes
|
|
121
|
+
bufferFill(payload, 0, payload.length - 5);
|
|
122
|
+
|
|
123
|
+
self._kexinit = payload;
|
|
124
|
+
|
|
125
|
+
// Needed to correct the starting position in allocated "packets" when packets
|
|
126
|
+
// will be buffered due to active key exchange
|
|
127
|
+
self._packetRW.write.allocStart = 0;
|
|
128
|
+
|
|
129
|
+
// TODO: only create single buffer and set _kexinit as slice of packet instead
|
|
130
|
+
{
|
|
131
|
+
const p = self._packetRW.write.allocStartKEX;
|
|
132
|
+
const packet = self._packetRW.write.alloc(payload.length, true);
|
|
133
|
+
packet.set(payload, p);
|
|
134
|
+
self._cipher.encrypt(self._packetRW.write.finalize(packet, true));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function handleKexInit(self, payload) {
|
|
139
|
+
/*
|
|
140
|
+
byte SSH_MSG_KEXINIT
|
|
141
|
+
byte[16] cookie (random bytes)
|
|
142
|
+
name-list kex_algorithms
|
|
143
|
+
name-list server_host_key_algorithms
|
|
144
|
+
name-list encryption_algorithms_client_to_server
|
|
145
|
+
name-list encryption_algorithms_server_to_client
|
|
146
|
+
name-list mac_algorithms_client_to_server
|
|
147
|
+
name-list mac_algorithms_server_to_client
|
|
148
|
+
name-list compression_algorithms_client_to_server
|
|
149
|
+
name-list compression_algorithms_server_to_client
|
|
150
|
+
name-list languages_client_to_server
|
|
151
|
+
name-list languages_server_to_client
|
|
152
|
+
boolean first_kex_packet_follows
|
|
153
|
+
uint32 0 (reserved for future extension)
|
|
154
|
+
*/
|
|
155
|
+
const init = {
|
|
156
|
+
kex: undefined,
|
|
157
|
+
serverHostKey: undefined,
|
|
158
|
+
cs: {
|
|
159
|
+
cipher: undefined,
|
|
160
|
+
mac: undefined,
|
|
161
|
+
compress: undefined,
|
|
162
|
+
lang: undefined,
|
|
163
|
+
},
|
|
164
|
+
sc: {
|
|
165
|
+
cipher: undefined,
|
|
166
|
+
mac: undefined,
|
|
167
|
+
compress: undefined,
|
|
168
|
+
lang: undefined,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
bufferParser.init(payload, 17);
|
|
173
|
+
|
|
174
|
+
if ((init.kex = bufferParser.readList()) === undefined
|
|
175
|
+
|| (init.serverHostKey = bufferParser.readList()) === undefined
|
|
176
|
+
|| (init.cs.cipher = bufferParser.readList()) === undefined
|
|
177
|
+
|| (init.sc.cipher = bufferParser.readList()) === undefined
|
|
178
|
+
|| (init.cs.mac = bufferParser.readList()) === undefined
|
|
179
|
+
|| (init.sc.mac = bufferParser.readList()) === undefined
|
|
180
|
+
|| (init.cs.compress = bufferParser.readList()) === undefined
|
|
181
|
+
|| (init.sc.compress = bufferParser.readList()) === undefined
|
|
182
|
+
|| (init.cs.lang = bufferParser.readList()) === undefined
|
|
183
|
+
|| (init.sc.lang = bufferParser.readList()) === undefined) {
|
|
184
|
+
bufferParser.clear();
|
|
185
|
+
return doFatalError(
|
|
186
|
+
self,
|
|
187
|
+
'Received malformed KEXINIT',
|
|
188
|
+
'handshake',
|
|
189
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const pos = bufferParser.pos();
|
|
194
|
+
const firstFollows = (pos < payload.length && payload[pos] === 1);
|
|
195
|
+
bufferParser.clear();
|
|
196
|
+
|
|
197
|
+
const local = self._offer;
|
|
198
|
+
const remote = init;
|
|
199
|
+
|
|
200
|
+
let localKex = local.lists.kex.array;
|
|
201
|
+
if (self._compatFlags & COMPAT.BAD_DHGEX) {
|
|
202
|
+
let found = false;
|
|
203
|
+
for (let i = 0; i < localKex.length; ++i) {
|
|
204
|
+
if (localKex[i].indexOf('group-exchange') !== -1) {
|
|
205
|
+
if (!found) {
|
|
206
|
+
found = true;
|
|
207
|
+
// Copy array lazily
|
|
208
|
+
localKex = localKex.slice();
|
|
209
|
+
}
|
|
210
|
+
localKex.splice(i--, 1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let clientList;
|
|
216
|
+
let serverList;
|
|
217
|
+
let i;
|
|
218
|
+
const debug = self._debug;
|
|
219
|
+
|
|
220
|
+
debug && debug('Inbound: Handshake in progress');
|
|
221
|
+
|
|
222
|
+
// Key exchange method =======================================================
|
|
223
|
+
debug && debug(`Handshake: (local) KEX method: ${localKex}`);
|
|
224
|
+
debug && debug(`Handshake: (remote) KEX method: ${remote.kex}`);
|
|
225
|
+
if (self._server) {
|
|
226
|
+
serverList = localKex;
|
|
227
|
+
clientList = remote.kex;
|
|
228
|
+
} else {
|
|
229
|
+
serverList = remote.kex;
|
|
230
|
+
clientList = localKex;
|
|
231
|
+
}
|
|
232
|
+
// Check for agreeable key exchange algorithm
|
|
233
|
+
for (i = 0;
|
|
234
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
235
|
+
++i);
|
|
236
|
+
if (i === clientList.length) {
|
|
237
|
+
// No suitable match found!
|
|
238
|
+
debug && debug('Handshake: No matching key exchange algorithm');
|
|
239
|
+
return doFatalError(
|
|
240
|
+
self,
|
|
241
|
+
'Handshake failed: no matching key exchange algorithm',
|
|
242
|
+
'handshake',
|
|
243
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
init.kex = clientList[i];
|
|
247
|
+
debug && debug(`Handshake: KEX algorithm: ${clientList[i]}`);
|
|
248
|
+
if (firstFollows && (!remote.kex.length || clientList[i] !== remote.kex[0])) {
|
|
249
|
+
// Ignore next inbound packet, it was a wrong first guess at KEX algorithm
|
|
250
|
+
self._skipNextInboundPacket = true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
// Server host key format ====================================================
|
|
255
|
+
const localSrvHostKey = local.lists.serverHostKey.array;
|
|
256
|
+
debug && debug(`Handshake: (local) Host key format: ${localSrvHostKey}`);
|
|
257
|
+
debug && debug(
|
|
258
|
+
`Handshake: (remote) Host key format: ${remote.serverHostKey}`
|
|
259
|
+
);
|
|
260
|
+
if (self._server) {
|
|
261
|
+
serverList = localSrvHostKey;
|
|
262
|
+
clientList = remote.serverHostKey;
|
|
263
|
+
} else {
|
|
264
|
+
serverList = remote.serverHostKey;
|
|
265
|
+
clientList = localSrvHostKey;
|
|
266
|
+
}
|
|
267
|
+
// Check for agreeable server host key format
|
|
268
|
+
for (i = 0;
|
|
269
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
270
|
+
++i);
|
|
271
|
+
if (i === clientList.length) {
|
|
272
|
+
// No suitable match found!
|
|
273
|
+
debug && debug('Handshake: No matching host key format');
|
|
274
|
+
return doFatalError(
|
|
275
|
+
self,
|
|
276
|
+
'Handshake failed: no matching host key format',
|
|
277
|
+
'handshake',
|
|
278
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
init.serverHostKey = clientList[i];
|
|
282
|
+
debug && debug(`Handshake: Host key format: ${clientList[i]}`);
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
// Client->Server cipher =====================================================
|
|
286
|
+
const localCSCipher = local.lists.cs.cipher.array;
|
|
287
|
+
debug && debug(`Handshake: (local) C->S cipher: ${localCSCipher}`);
|
|
288
|
+
debug && debug(`Handshake: (remote) C->S cipher: ${remote.cs.cipher}`);
|
|
289
|
+
if (self._server) {
|
|
290
|
+
serverList = localCSCipher;
|
|
291
|
+
clientList = remote.cs.cipher;
|
|
292
|
+
} else {
|
|
293
|
+
serverList = remote.cs.cipher;
|
|
294
|
+
clientList = localCSCipher;
|
|
295
|
+
}
|
|
296
|
+
// Check for agreeable client->server cipher
|
|
297
|
+
for (i = 0;
|
|
298
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
299
|
+
++i);
|
|
300
|
+
if (i === clientList.length) {
|
|
301
|
+
// No suitable match found!
|
|
302
|
+
debug && debug('Handshake: No matching C->S cipher');
|
|
303
|
+
return doFatalError(
|
|
304
|
+
self,
|
|
305
|
+
'Handshake failed: no matching C->S cipher',
|
|
306
|
+
'handshake',
|
|
307
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
init.cs.cipher = clientList[i];
|
|
311
|
+
debug && debug(`Handshake: C->S Cipher: ${clientList[i]}`);
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
// Server->Client cipher =====================================================
|
|
315
|
+
const localSCCipher = local.lists.sc.cipher.array;
|
|
316
|
+
debug && debug(`Handshake: (local) S->C cipher: ${localSCCipher}`);
|
|
317
|
+
debug && debug(`Handshake: (remote) S->C cipher: ${remote.sc.cipher}`);
|
|
318
|
+
if (self._server) {
|
|
319
|
+
serverList = localSCCipher;
|
|
320
|
+
clientList = remote.sc.cipher;
|
|
321
|
+
} else {
|
|
322
|
+
serverList = remote.sc.cipher;
|
|
323
|
+
clientList = localSCCipher;
|
|
324
|
+
}
|
|
325
|
+
// Check for agreeable server->client cipher
|
|
326
|
+
for (i = 0;
|
|
327
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
328
|
+
++i);
|
|
329
|
+
if (i === clientList.length) {
|
|
330
|
+
// No suitable match found!
|
|
331
|
+
debug && debug('Handshake: No matching S->C cipher');
|
|
332
|
+
return doFatalError(
|
|
333
|
+
self,
|
|
334
|
+
'Handshake failed: no matching S->C cipher',
|
|
335
|
+
'handshake',
|
|
336
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
init.sc.cipher = clientList[i];
|
|
340
|
+
debug && debug(`Handshake: S->C cipher: ${clientList[i]}`);
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
// Client->Server MAC ========================================================
|
|
344
|
+
const localCSMAC = local.lists.cs.mac.array;
|
|
345
|
+
debug && debug(`Handshake: (local) C->S MAC: ${localCSMAC}`);
|
|
346
|
+
debug && debug(`Handshake: (remote) C->S MAC: ${remote.cs.mac}`);
|
|
347
|
+
if (CIPHER_INFO[init.cs.cipher].authLen > 0) {
|
|
348
|
+
init.cs.mac = '';
|
|
349
|
+
debug && debug('Handshake: C->S MAC: <implicit>');
|
|
350
|
+
} else {
|
|
351
|
+
if (self._server) {
|
|
352
|
+
serverList = localCSMAC;
|
|
353
|
+
clientList = remote.cs.mac;
|
|
354
|
+
} else {
|
|
355
|
+
serverList = remote.cs.mac;
|
|
356
|
+
clientList = localCSMAC;
|
|
357
|
+
}
|
|
358
|
+
// Check for agreeable client->server hmac algorithm
|
|
359
|
+
for (i = 0;
|
|
360
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
361
|
+
++i);
|
|
362
|
+
if (i === clientList.length) {
|
|
363
|
+
// No suitable match found!
|
|
364
|
+
debug && debug('Handshake: No matching C->S MAC');
|
|
365
|
+
return doFatalError(
|
|
366
|
+
self,
|
|
367
|
+
'Handshake failed: no matching C->S MAC',
|
|
368
|
+
'handshake',
|
|
369
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
init.cs.mac = clientList[i];
|
|
373
|
+
debug && debug(`Handshake: C->S MAC: ${clientList[i]}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
// Server->Client MAC ========================================================
|
|
378
|
+
const localSCMAC = local.lists.sc.mac.array;
|
|
379
|
+
debug && debug(`Handshake: (local) S->C MAC: ${localSCMAC}`);
|
|
380
|
+
debug && debug(`Handshake: (remote) S->C MAC: ${remote.sc.mac}`);
|
|
381
|
+
if (CIPHER_INFO[init.sc.cipher].authLen > 0) {
|
|
382
|
+
init.sc.mac = '';
|
|
383
|
+
debug && debug('Handshake: S->C MAC: <implicit>');
|
|
384
|
+
} else {
|
|
385
|
+
if (self._server) {
|
|
386
|
+
serverList = localSCMAC;
|
|
387
|
+
clientList = remote.sc.mac;
|
|
388
|
+
} else {
|
|
389
|
+
serverList = remote.sc.mac;
|
|
390
|
+
clientList = localSCMAC;
|
|
391
|
+
}
|
|
392
|
+
// Check for agreeable server->client hmac algorithm
|
|
393
|
+
for (i = 0;
|
|
394
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
395
|
+
++i);
|
|
396
|
+
if (i === clientList.length) {
|
|
397
|
+
// No suitable match found!
|
|
398
|
+
debug && debug('Handshake: No matching S->C MAC');
|
|
399
|
+
return doFatalError(
|
|
400
|
+
self,
|
|
401
|
+
'Handshake failed: no matching S->C MAC',
|
|
402
|
+
'handshake',
|
|
403
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
init.sc.mac = clientList[i];
|
|
407
|
+
debug && debug(`Handshake: S->C MAC: ${clientList[i]}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// Client->Server compression ================================================
|
|
412
|
+
const localCSCompress = local.lists.cs.compress.array;
|
|
413
|
+
debug && debug(`Handshake: (local) C->S compression: ${localCSCompress}`);
|
|
414
|
+
debug && debug(`Handshake: (remote) C->S compression: ${remote.cs.compress}`);
|
|
415
|
+
if (self._server) {
|
|
416
|
+
serverList = localCSCompress;
|
|
417
|
+
clientList = remote.cs.compress;
|
|
418
|
+
} else {
|
|
419
|
+
serverList = remote.cs.compress;
|
|
420
|
+
clientList = localCSCompress;
|
|
421
|
+
}
|
|
422
|
+
// Check for agreeable client->server compression algorithm
|
|
423
|
+
for (i = 0;
|
|
424
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
425
|
+
++i);
|
|
426
|
+
if (i === clientList.length) {
|
|
427
|
+
// No suitable match found!
|
|
428
|
+
debug && debug('Handshake: No matching C->S compression');
|
|
429
|
+
return doFatalError(
|
|
430
|
+
self,
|
|
431
|
+
'Handshake failed: no matching C->S compression',
|
|
432
|
+
'handshake',
|
|
433
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
init.cs.compress = clientList[i];
|
|
437
|
+
debug && debug(`Handshake: C->S compression: ${clientList[i]}`);
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
// Server->Client compression ================================================
|
|
441
|
+
const localSCCompress = local.lists.sc.compress.array;
|
|
442
|
+
debug && debug(`Handshake: (local) S->C compression: ${localSCCompress}`);
|
|
443
|
+
debug && debug(`Handshake: (remote) S->C compression: ${remote.sc.compress}`);
|
|
444
|
+
if (self._server) {
|
|
445
|
+
serverList = localSCCompress;
|
|
446
|
+
clientList = remote.sc.compress;
|
|
447
|
+
} else {
|
|
448
|
+
serverList = remote.sc.compress;
|
|
449
|
+
clientList = localSCCompress;
|
|
450
|
+
}
|
|
451
|
+
// Check for agreeable server->client compression algorithm
|
|
452
|
+
for (i = 0;
|
|
453
|
+
i < clientList.length && serverList.indexOf(clientList[i]) === -1;
|
|
454
|
+
++i);
|
|
455
|
+
if (i === clientList.length) {
|
|
456
|
+
// No suitable match found!
|
|
457
|
+
debug && debug('Handshake: No matching S->C compression');
|
|
458
|
+
return doFatalError(
|
|
459
|
+
self,
|
|
460
|
+
'Handshake failed: no matching S->C compression',
|
|
461
|
+
'handshake',
|
|
462
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
init.sc.compress = clientList[i];
|
|
466
|
+
debug && debug(`Handshake: S->C compression: ${clientList[i]}`);
|
|
467
|
+
|
|
468
|
+
init.cs.lang = '';
|
|
469
|
+
init.sc.lang = '';
|
|
470
|
+
|
|
471
|
+
// XXX: hack -- find a better way to do this
|
|
472
|
+
if (self._kex) {
|
|
473
|
+
if (!self._kexinit) {
|
|
474
|
+
// We received a rekey request, but we haven't sent a KEXINIT in response
|
|
475
|
+
// yet
|
|
476
|
+
kexinit(self);
|
|
477
|
+
}
|
|
478
|
+
self._decipher._onPayload = onKEXPayload.bind(self, { firstPacket: false });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
self._kex = createKeyExchange(init, self, payload);
|
|
482
|
+
self._kex.start();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const createKeyExchange = (() => {
|
|
486
|
+
function convertToMpint(buf) {
|
|
487
|
+
let idx = 0;
|
|
488
|
+
let length = buf.length;
|
|
489
|
+
while (buf[idx] === 0x00) {
|
|
490
|
+
++idx;
|
|
491
|
+
--length;
|
|
492
|
+
}
|
|
493
|
+
let newBuf;
|
|
494
|
+
if (buf[idx] & 0x80) {
|
|
495
|
+
newBuf = Buffer.allocUnsafe(1 + length);
|
|
496
|
+
newBuf[0] = 0;
|
|
497
|
+
buf.copy(newBuf, 1, idx);
|
|
498
|
+
buf = newBuf;
|
|
499
|
+
} else if (length !== buf.length) {
|
|
500
|
+
newBuf = Buffer.allocUnsafe(length);
|
|
501
|
+
buf.copy(newBuf, 0, idx);
|
|
502
|
+
buf = newBuf;
|
|
503
|
+
}
|
|
504
|
+
return buf;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
class KeyExchange {
|
|
508
|
+
constructor(negotiated, protocol, remoteKexinit) {
|
|
509
|
+
this._protocol = protocol;
|
|
510
|
+
|
|
511
|
+
this.sessionID = (protocol._kex ? protocol._kex.sessionID : undefined);
|
|
512
|
+
this.negotiated = negotiated;
|
|
513
|
+
this._step = 1;
|
|
514
|
+
this._public = null;
|
|
515
|
+
this._dh = null;
|
|
516
|
+
this._sentNEWKEYS = false;
|
|
517
|
+
this._receivedNEWKEYS = false;
|
|
518
|
+
this._finished = false;
|
|
519
|
+
this._hostVerified = false;
|
|
520
|
+
|
|
521
|
+
// Data needed for initializing cipher/decipher/etc.
|
|
522
|
+
this._kexinit = protocol._kexinit;
|
|
523
|
+
this._remoteKexinit = remoteKexinit;
|
|
524
|
+
this._identRaw = protocol._identRaw;
|
|
525
|
+
this._remoteIdentRaw = protocol._remoteIdentRaw;
|
|
526
|
+
this._hostKey = undefined;
|
|
527
|
+
this._dhData = undefined;
|
|
528
|
+
this._sig = undefined;
|
|
529
|
+
}
|
|
530
|
+
finish() {
|
|
531
|
+
if (this._finished)
|
|
532
|
+
return false;
|
|
533
|
+
this._finished = true;
|
|
534
|
+
|
|
535
|
+
const isServer = this._protocol._server;
|
|
536
|
+
const negotiated = this.negotiated;
|
|
537
|
+
|
|
538
|
+
const pubKey = this.convertPublicKey(this._dhData);
|
|
539
|
+
let secret = this.computeSecret(this._dhData);
|
|
540
|
+
if (secret instanceof Error) {
|
|
541
|
+
secret.message =
|
|
542
|
+
`Error while computing DH secret (${this.type}): ${secret.message}`;
|
|
543
|
+
secret.level = 'handshake';
|
|
544
|
+
return doFatalError(
|
|
545
|
+
this._protocol,
|
|
546
|
+
secret,
|
|
547
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const hash = createHash(this.hashName);
|
|
552
|
+
// V_C
|
|
553
|
+
hashString(hash, (isServer ? this._remoteIdentRaw : this._identRaw));
|
|
554
|
+
// "V_S"
|
|
555
|
+
hashString(hash, (isServer ? this._identRaw : this._remoteIdentRaw));
|
|
556
|
+
// "I_C"
|
|
557
|
+
hashString(hash, (isServer ? this._remoteKexinit : this._kexinit));
|
|
558
|
+
// "I_S"
|
|
559
|
+
hashString(hash, (isServer ? this._kexinit : this._remoteKexinit));
|
|
560
|
+
// "K_S"
|
|
561
|
+
const serverPublicHostKey = (isServer
|
|
562
|
+
? this._hostKey.getPublicSSH()
|
|
563
|
+
: this._hostKey);
|
|
564
|
+
hashString(hash, serverPublicHostKey);
|
|
565
|
+
|
|
566
|
+
if (this.type === 'groupex') {
|
|
567
|
+
// Group exchange-specific
|
|
568
|
+
const params = this.getDHParams();
|
|
569
|
+
const num = Buffer.allocUnsafe(4);
|
|
570
|
+
// min (uint32)
|
|
571
|
+
writeUInt32BE(num, this._minBits, 0);
|
|
572
|
+
hash.update(num);
|
|
573
|
+
// preferred (uint32)
|
|
574
|
+
writeUInt32BE(num, this._prefBits, 0);
|
|
575
|
+
hash.update(num);
|
|
576
|
+
// max (uint32)
|
|
577
|
+
writeUInt32BE(num, this._maxBits, 0);
|
|
578
|
+
hash.update(num);
|
|
579
|
+
// prime
|
|
580
|
+
hashString(hash, params.prime);
|
|
581
|
+
// generator
|
|
582
|
+
hashString(hash, params.generator);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// method-specific data sent by client
|
|
586
|
+
hashString(hash, (isServer ? pubKey : this.getPublicKey()));
|
|
587
|
+
// method-specific data sent by server
|
|
588
|
+
const serverPublicKey = (isServer ? this.getPublicKey() : pubKey);
|
|
589
|
+
hashString(hash, serverPublicKey);
|
|
590
|
+
// shared secret ("K")
|
|
591
|
+
hashString(hash, secret);
|
|
592
|
+
|
|
593
|
+
// "H"
|
|
594
|
+
const exchangeHash = hash.digest();
|
|
595
|
+
|
|
596
|
+
if (!isServer) {
|
|
597
|
+
bufferParser.init(this._sig, 0);
|
|
598
|
+
const sigType = bufferParser.readString(true);
|
|
599
|
+
|
|
600
|
+
if (!sigType) {
|
|
601
|
+
return doFatalError(
|
|
602
|
+
this._protocol,
|
|
603
|
+
'Malformed packet while reading signature',
|
|
604
|
+
'handshake',
|
|
605
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (sigType !== negotiated.serverHostKey) {
|
|
610
|
+
return doFatalError(
|
|
611
|
+
this._protocol,
|
|
612
|
+
`Wrong signature type: ${sigType}, `
|
|
613
|
+
+ `expected: ${negotiated.serverHostKey}`,
|
|
614
|
+
'handshake',
|
|
615
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// "s"
|
|
620
|
+
let sigValue = bufferParser.readString();
|
|
621
|
+
|
|
622
|
+
bufferParser.clear();
|
|
623
|
+
|
|
624
|
+
if (sigValue === undefined) {
|
|
625
|
+
return doFatalError(
|
|
626
|
+
this._protocol,
|
|
627
|
+
'Malformed packet while reading signature',
|
|
628
|
+
'handshake',
|
|
629
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!(sigValue = sigSSHToASN1(sigValue, sigType))) {
|
|
634
|
+
return doFatalError(
|
|
635
|
+
this._protocol,
|
|
636
|
+
'Malformed signature',
|
|
637
|
+
'handshake',
|
|
638
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let parsedHostKey;
|
|
643
|
+
{
|
|
644
|
+
bufferParser.init(this._hostKey, 0);
|
|
645
|
+
const name = bufferParser.readString(true);
|
|
646
|
+
const hostKey = this._hostKey.slice(bufferParser.pos());
|
|
647
|
+
bufferParser.clear();
|
|
648
|
+
parsedHostKey = parseDERKey(hostKey, name);
|
|
649
|
+
if (parsedHostKey instanceof Error) {
|
|
650
|
+
parsedHostKey.level = 'handshake';
|
|
651
|
+
return doFatalError(
|
|
652
|
+
this._protocol,
|
|
653
|
+
parsedHostKey,
|
|
654
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
let hashAlgo;
|
|
660
|
+
// Check if we need to override the default hash algorithm
|
|
661
|
+
switch (this.negotiated.serverHostKey) {
|
|
662
|
+
case 'rsa-sha2-256': hashAlgo = 'sha256'; break;
|
|
663
|
+
case 'rsa-sha2-512': hashAlgo = 'sha512'; break;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
this._protocol._debug
|
|
667
|
+
&& this._protocol._debug('Verifying signature ...');
|
|
668
|
+
|
|
669
|
+
const verified = parsedHostKey.verify(exchangeHash, sigValue, hashAlgo);
|
|
670
|
+
if (verified !== true) {
|
|
671
|
+
if (verified instanceof Error) {
|
|
672
|
+
this._protocol._debug && this._protocol._debug(
|
|
673
|
+
`Signature verification failed: ${verified.stack}`
|
|
674
|
+
);
|
|
675
|
+
} else {
|
|
676
|
+
this._protocol._debug && this._protocol._debug(
|
|
677
|
+
'Signature verification failed'
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return doFatalError(
|
|
681
|
+
this._protocol,
|
|
682
|
+
'Handshake failed: signature verification failed',
|
|
683
|
+
'handshake',
|
|
684
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
this._protocol._debug && this._protocol._debug('Verified signature');
|
|
688
|
+
} else {
|
|
689
|
+
// Server
|
|
690
|
+
|
|
691
|
+
let hashAlgo;
|
|
692
|
+
// Check if we need to override the default hash algorithm
|
|
693
|
+
switch (this.negotiated.serverHostKey) {
|
|
694
|
+
case 'rsa-sha2-256': hashAlgo = 'sha256'; break;
|
|
695
|
+
case 'rsa-sha2-512': hashAlgo = 'sha512'; break;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
this._protocol._debug && this._protocol._debug(
|
|
699
|
+
'Generating signature ...'
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
let signature = this._hostKey.sign(exchangeHash, hashAlgo);
|
|
703
|
+
if (signature instanceof Error) {
|
|
704
|
+
return doFatalError(
|
|
705
|
+
this._protocol,
|
|
706
|
+
'Handshake failed: signature generation failed for '
|
|
707
|
+
+ `${this._hostKey.type} host key: ${signature.message}`,
|
|
708
|
+
'handshake',
|
|
709
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
signature = convertSignature(signature, this._hostKey.type);
|
|
714
|
+
if (signature === false) {
|
|
715
|
+
return doFatalError(
|
|
716
|
+
this._protocol,
|
|
717
|
+
'Handshake failed: signature conversion failed for '
|
|
718
|
+
+ `${this._hostKey.type} host key`,
|
|
719
|
+
'handshake',
|
|
720
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Send KEX reply
|
|
725
|
+
/*
|
|
726
|
+
byte SSH_MSG_KEXDH_REPLY
|
|
727
|
+
/ SSH_MSG_KEX_DH_GEX_REPLY
|
|
728
|
+
/ SSH_MSG_KEX_ECDH_REPLY
|
|
729
|
+
string server public host key and certificates (K_S)
|
|
730
|
+
string <method-specific data>
|
|
731
|
+
string signature of H
|
|
732
|
+
*/
|
|
733
|
+
const sigType = this.negotiated.serverHostKey;
|
|
734
|
+
const sigTypeLen = Buffer.byteLength(sigType);
|
|
735
|
+
const sigLen = 4 + sigTypeLen + 4 + signature.length;
|
|
736
|
+
let p = this._protocol._packetRW.write.allocStartKEX;
|
|
737
|
+
const packet = this._protocol._packetRW.write.alloc(
|
|
738
|
+
1
|
|
739
|
+
+ 4 + serverPublicHostKey.length
|
|
740
|
+
+ 4 + serverPublicKey.length
|
|
741
|
+
+ 4 + sigLen,
|
|
742
|
+
true
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
packet[p] = MESSAGE.KEXDH_REPLY;
|
|
746
|
+
|
|
747
|
+
writeUInt32BE(packet, serverPublicHostKey.length, ++p);
|
|
748
|
+
packet.set(serverPublicHostKey, p += 4);
|
|
749
|
+
|
|
750
|
+
writeUInt32BE(packet,
|
|
751
|
+
serverPublicKey.length,
|
|
752
|
+
p += serverPublicHostKey.length);
|
|
753
|
+
packet.set(serverPublicKey, p += 4);
|
|
754
|
+
|
|
755
|
+
writeUInt32BE(packet, sigLen, p += serverPublicKey.length);
|
|
756
|
+
|
|
757
|
+
writeUInt32BE(packet, sigTypeLen, p += 4);
|
|
758
|
+
packet.utf8Write(sigType, p += 4, sigTypeLen);
|
|
759
|
+
|
|
760
|
+
writeUInt32BE(packet, signature.length, p += sigTypeLen);
|
|
761
|
+
packet.set(signature, p += 4);
|
|
762
|
+
|
|
763
|
+
if (this._protocol._debug) {
|
|
764
|
+
let type;
|
|
765
|
+
switch (this.type) {
|
|
766
|
+
case 'group':
|
|
767
|
+
type = 'KEXDH_REPLY';
|
|
768
|
+
break;
|
|
769
|
+
case 'groupex':
|
|
770
|
+
type = 'KEXDH_GEX_REPLY';
|
|
771
|
+
break;
|
|
772
|
+
default:
|
|
773
|
+
type = 'KEXECDH_REPLY';
|
|
774
|
+
}
|
|
775
|
+
this._protocol._debug(`Outbound: Sending ${type}`);
|
|
776
|
+
}
|
|
777
|
+
this._protocol._cipher.encrypt(
|
|
778
|
+
this._protocol._packetRW.write.finalize(packet, true)
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
trySendNEWKEYS(this);
|
|
782
|
+
|
|
783
|
+
const completeHandshake = () => {
|
|
784
|
+
if (!this.sessionID)
|
|
785
|
+
this.sessionID = exchangeHash;
|
|
786
|
+
|
|
787
|
+
{
|
|
788
|
+
const newSecret = Buffer.allocUnsafe(4 + secret.length);
|
|
789
|
+
writeUInt32BE(newSecret, secret.length, 0);
|
|
790
|
+
newSecret.set(secret, 4);
|
|
791
|
+
secret = newSecret;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Initialize new ciphers, deciphers, etc.
|
|
795
|
+
|
|
796
|
+
const csCipherInfo = CIPHER_INFO[negotiated.cs.cipher];
|
|
797
|
+
const scCipherInfo = CIPHER_INFO[negotiated.sc.cipher];
|
|
798
|
+
|
|
799
|
+
const csIV = generateKEXVal(csCipherInfo.ivLen,
|
|
800
|
+
this.hashName,
|
|
801
|
+
secret,
|
|
802
|
+
exchangeHash,
|
|
803
|
+
this.sessionID,
|
|
804
|
+
'A');
|
|
805
|
+
const scIV = generateKEXVal(scCipherInfo.ivLen,
|
|
806
|
+
this.hashName,
|
|
807
|
+
secret,
|
|
808
|
+
exchangeHash,
|
|
809
|
+
this.sessionID,
|
|
810
|
+
'B');
|
|
811
|
+
const csKey = generateKEXVal(csCipherInfo.keyLen,
|
|
812
|
+
this.hashName,
|
|
813
|
+
secret,
|
|
814
|
+
exchangeHash,
|
|
815
|
+
this.sessionID,
|
|
816
|
+
'C');
|
|
817
|
+
const scKey = generateKEXVal(scCipherInfo.keyLen,
|
|
818
|
+
this.hashName,
|
|
819
|
+
secret,
|
|
820
|
+
exchangeHash,
|
|
821
|
+
this.sessionID,
|
|
822
|
+
'D');
|
|
823
|
+
let csMacInfo;
|
|
824
|
+
let csMacKey;
|
|
825
|
+
if (!csCipherInfo.authLen) {
|
|
826
|
+
csMacInfo = MAC_INFO[negotiated.cs.mac];
|
|
827
|
+
csMacKey = generateKEXVal(csMacInfo.len,
|
|
828
|
+
this.hashName,
|
|
829
|
+
secret,
|
|
830
|
+
exchangeHash,
|
|
831
|
+
this.sessionID,
|
|
832
|
+
'E');
|
|
833
|
+
}
|
|
834
|
+
let scMacInfo;
|
|
835
|
+
let scMacKey;
|
|
836
|
+
if (!scCipherInfo.authLen) {
|
|
837
|
+
scMacInfo = MAC_INFO[negotiated.sc.mac];
|
|
838
|
+
scMacKey = generateKEXVal(scMacInfo.len,
|
|
839
|
+
this.hashName,
|
|
840
|
+
secret,
|
|
841
|
+
exchangeHash,
|
|
842
|
+
this.sessionID,
|
|
843
|
+
'F');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const config = {
|
|
847
|
+
inbound: {
|
|
848
|
+
onPayload: this._protocol._onPayload,
|
|
849
|
+
seqno: this._protocol._decipher.inSeqno,
|
|
850
|
+
decipherInfo: (!isServer ? scCipherInfo : csCipherInfo),
|
|
851
|
+
decipherIV: (!isServer ? scIV : csIV),
|
|
852
|
+
decipherKey: (!isServer ? scKey : csKey),
|
|
853
|
+
macInfo: (!isServer ? scMacInfo : csMacInfo),
|
|
854
|
+
macKey: (!isServer ? scMacKey : csMacKey),
|
|
855
|
+
},
|
|
856
|
+
outbound: {
|
|
857
|
+
onWrite: this._protocol._onWrite,
|
|
858
|
+
seqno: this._protocol._cipher.outSeqno,
|
|
859
|
+
cipherInfo: (isServer ? scCipherInfo : csCipherInfo),
|
|
860
|
+
cipherIV: (isServer ? scIV : csIV),
|
|
861
|
+
cipherKey: (isServer ? scKey : csKey),
|
|
862
|
+
macInfo: (isServer ? scMacInfo : csMacInfo),
|
|
863
|
+
macKey: (isServer ? scMacKey : csMacKey),
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
this._protocol._cipher && this._protocol._cipher.free();
|
|
867
|
+
this._protocol._decipher && this._protocol._decipher.free();
|
|
868
|
+
this._protocol._cipher = createCipher(config);
|
|
869
|
+
this._protocol._decipher = createDecipher(config);
|
|
870
|
+
|
|
871
|
+
const rw = {
|
|
872
|
+
read: undefined,
|
|
873
|
+
write: undefined,
|
|
874
|
+
};
|
|
875
|
+
switch (negotiated.cs.compress) {
|
|
876
|
+
case 'zlib': // starts immediately
|
|
877
|
+
if (isServer)
|
|
878
|
+
rw.read = new ZlibPacketReader();
|
|
879
|
+
else
|
|
880
|
+
rw.write = new ZlibPacketWriter(this._protocol);
|
|
881
|
+
break;
|
|
882
|
+
case 'zlib@openssh.com':
|
|
883
|
+
// Starts after successful user authentication
|
|
884
|
+
|
|
885
|
+
if (this._protocol._authenticated) {
|
|
886
|
+
// If a rekey happens and this compression method is selected and
|
|
887
|
+
// we already authenticated successfully, we need to start
|
|
888
|
+
// immediately instead
|
|
889
|
+
if (isServer)
|
|
890
|
+
rw.read = new ZlibPacketReader();
|
|
891
|
+
else
|
|
892
|
+
rw.write = new ZlibPacketWriter(this._protocol);
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
// FALLTHROUGH
|
|
896
|
+
default:
|
|
897
|
+
// none -- never any compression/decompression
|
|
898
|
+
|
|
899
|
+
if (isServer)
|
|
900
|
+
rw.read = new PacketReader();
|
|
901
|
+
else
|
|
902
|
+
rw.write = new PacketWriter(this._protocol);
|
|
903
|
+
}
|
|
904
|
+
switch (negotiated.sc.compress) {
|
|
905
|
+
case 'zlib': // starts immediately
|
|
906
|
+
if (isServer)
|
|
907
|
+
rw.write = new ZlibPacketWriter(this._protocol);
|
|
908
|
+
else
|
|
909
|
+
rw.read = new ZlibPacketReader();
|
|
910
|
+
break;
|
|
911
|
+
case 'zlib@openssh.com':
|
|
912
|
+
// Starts after successful user authentication
|
|
913
|
+
|
|
914
|
+
if (this._protocol._authenticated) {
|
|
915
|
+
// If a rekey happens and this compression method is selected and
|
|
916
|
+
// we already authenticated successfully, we need to start
|
|
917
|
+
// immediately instead
|
|
918
|
+
if (isServer)
|
|
919
|
+
rw.write = new ZlibPacketWriter(this._protocol);
|
|
920
|
+
else
|
|
921
|
+
rw.read = new ZlibPacketReader();
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
// FALLTHROUGH
|
|
925
|
+
default:
|
|
926
|
+
// none -- never any compression/decompression
|
|
927
|
+
|
|
928
|
+
if (isServer)
|
|
929
|
+
rw.write = new PacketWriter(this._protocol);
|
|
930
|
+
else
|
|
931
|
+
rw.read = new PacketReader();
|
|
932
|
+
}
|
|
933
|
+
this._protocol._packetRW.read.cleanup();
|
|
934
|
+
this._protocol._packetRW.write.cleanup();
|
|
935
|
+
this._protocol._packetRW = rw;
|
|
936
|
+
|
|
937
|
+
// Cleanup/reset various state
|
|
938
|
+
this._public = null;
|
|
939
|
+
this._dh = null;
|
|
940
|
+
this._kexinit = this._protocol._kexinit = undefined;
|
|
941
|
+
this._remoteKexinit = undefined;
|
|
942
|
+
this._identRaw = undefined;
|
|
943
|
+
this._remoteIdentRaw = undefined;
|
|
944
|
+
this._hostKey = undefined;
|
|
945
|
+
this._dhData = undefined;
|
|
946
|
+
this._sig = undefined;
|
|
947
|
+
|
|
948
|
+
this._protocol._onHandshakeComplete(negotiated);
|
|
949
|
+
|
|
950
|
+
return false;
|
|
951
|
+
};
|
|
952
|
+
if (!isServer)
|
|
953
|
+
return completeHandshake();
|
|
954
|
+
this.finish = completeHandshake;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
start() {
|
|
958
|
+
if (!this._protocol._server) {
|
|
959
|
+
if (this._protocol._debug) {
|
|
960
|
+
let type;
|
|
961
|
+
switch (this.type) {
|
|
962
|
+
case 'group':
|
|
963
|
+
type = 'KEXDH_INIT';
|
|
964
|
+
break;
|
|
965
|
+
default:
|
|
966
|
+
type = 'KEXECDH_INIT';
|
|
967
|
+
}
|
|
968
|
+
this._protocol._debug(`Outbound: Sending ${type}`);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const pubKey = this.getPublicKey();
|
|
972
|
+
|
|
973
|
+
let p = this._protocol._packetRW.write.allocStartKEX;
|
|
974
|
+
const packet = this._protocol._packetRW.write.alloc(
|
|
975
|
+
1 + 4 + pubKey.length,
|
|
976
|
+
true
|
|
977
|
+
);
|
|
978
|
+
packet[p] = MESSAGE.KEXDH_INIT;
|
|
979
|
+
writeUInt32BE(packet, pubKey.length, ++p);
|
|
980
|
+
packet.set(pubKey, p += 4);
|
|
981
|
+
this._protocol._cipher.encrypt(
|
|
982
|
+
this._protocol._packetRW.write.finalize(packet, true)
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
getPublicKey() {
|
|
987
|
+
this.generateKeys();
|
|
988
|
+
|
|
989
|
+
const key = this._public;
|
|
990
|
+
|
|
991
|
+
if (key)
|
|
992
|
+
return this.convertPublicKey(key);
|
|
993
|
+
}
|
|
994
|
+
convertPublicKey(key) {
|
|
995
|
+
let newKey;
|
|
996
|
+
let idx = 0;
|
|
997
|
+
let len = key.length;
|
|
998
|
+
while (key[idx] === 0x00) {
|
|
999
|
+
++idx;
|
|
1000
|
+
--len;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (key[idx] & 0x80) {
|
|
1004
|
+
newKey = Buffer.allocUnsafe(1 + len);
|
|
1005
|
+
newKey[0] = 0;
|
|
1006
|
+
key.copy(newKey, 1, idx);
|
|
1007
|
+
return newKey;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (len !== key.length) {
|
|
1011
|
+
newKey = Buffer.allocUnsafe(len);
|
|
1012
|
+
key.copy(newKey, 0, idx);
|
|
1013
|
+
key = newKey;
|
|
1014
|
+
}
|
|
1015
|
+
return key;
|
|
1016
|
+
}
|
|
1017
|
+
computeSecret(otherPublicKey) {
|
|
1018
|
+
this.generateKeys();
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
return convertToMpint(this._dh.computeSecret(otherPublicKey));
|
|
1022
|
+
} catch (ex) {
|
|
1023
|
+
return ex;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
parse(payload) {
|
|
1027
|
+
const type = payload[0];
|
|
1028
|
+
switch (this._step) {
|
|
1029
|
+
case 1:
|
|
1030
|
+
if (this._protocol._server) {
|
|
1031
|
+
// Server
|
|
1032
|
+
if (type !== MESSAGE.KEXDH_INIT) {
|
|
1033
|
+
return doFatalError(
|
|
1034
|
+
this._protocol,
|
|
1035
|
+
`Received packet ${type} instead of ${MESSAGE.KEXDH_INIT}`,
|
|
1036
|
+
'handshake',
|
|
1037
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
this._protocol._debug && this._protocol._debug(
|
|
1041
|
+
'Received DH Init'
|
|
1042
|
+
);
|
|
1043
|
+
/*
|
|
1044
|
+
byte SSH_MSG_KEXDH_INIT
|
|
1045
|
+
/ SSH_MSG_KEX_ECDH_INIT
|
|
1046
|
+
string <method-specific data>
|
|
1047
|
+
*/
|
|
1048
|
+
bufferParser.init(payload, 1);
|
|
1049
|
+
const dhData = bufferParser.readString();
|
|
1050
|
+
bufferParser.clear();
|
|
1051
|
+
if (dhData === undefined) {
|
|
1052
|
+
return doFatalError(
|
|
1053
|
+
this._protocol,
|
|
1054
|
+
'Received malformed KEX*_INIT',
|
|
1055
|
+
'handshake',
|
|
1056
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Client public key
|
|
1061
|
+
this._dhData = dhData;
|
|
1062
|
+
|
|
1063
|
+
let hostKey =
|
|
1064
|
+
this._protocol._hostKeys[this.negotiated.serverHostKey];
|
|
1065
|
+
if (Array.isArray(hostKey))
|
|
1066
|
+
hostKey = hostKey[0];
|
|
1067
|
+
this._hostKey = hostKey;
|
|
1068
|
+
|
|
1069
|
+
this.finish();
|
|
1070
|
+
} else {
|
|
1071
|
+
// Client
|
|
1072
|
+
if (type !== MESSAGE.KEXDH_REPLY) {
|
|
1073
|
+
return doFatalError(
|
|
1074
|
+
this._protocol,
|
|
1075
|
+
`Received packet ${type} instead of ${MESSAGE.KEXDH_REPLY}`,
|
|
1076
|
+
'handshake',
|
|
1077
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
this._protocol._debug && this._protocol._debug(
|
|
1081
|
+
'Received DH Reply'
|
|
1082
|
+
);
|
|
1083
|
+
/*
|
|
1084
|
+
byte SSH_MSG_KEXDH_REPLY
|
|
1085
|
+
/ SSH_MSG_KEX_DH_GEX_REPLY
|
|
1086
|
+
/ SSH_MSG_KEX_ECDH_REPLY
|
|
1087
|
+
string server public host key and certificates (K_S)
|
|
1088
|
+
string <method-specific data>
|
|
1089
|
+
string signature of H
|
|
1090
|
+
*/
|
|
1091
|
+
bufferParser.init(payload, 1);
|
|
1092
|
+
let hostPubKey;
|
|
1093
|
+
let dhData;
|
|
1094
|
+
let sig;
|
|
1095
|
+
if ((hostPubKey = bufferParser.readString()) === undefined
|
|
1096
|
+
|| (dhData = bufferParser.readString()) === undefined
|
|
1097
|
+
|| (sig = bufferParser.readString()) === undefined) {
|
|
1098
|
+
bufferParser.clear();
|
|
1099
|
+
return doFatalError(
|
|
1100
|
+
this._protocol,
|
|
1101
|
+
'Received malformed KEX*_REPLY',
|
|
1102
|
+
'handshake',
|
|
1103
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
bufferParser.clear();
|
|
1107
|
+
|
|
1108
|
+
// Check that the host public key type matches what was negotiated
|
|
1109
|
+
// during KEXINIT swap
|
|
1110
|
+
bufferParser.init(hostPubKey, 0);
|
|
1111
|
+
const hostPubKeyType = bufferParser.readString(true);
|
|
1112
|
+
bufferParser.clear();
|
|
1113
|
+
if (hostPubKeyType === undefined) {
|
|
1114
|
+
return doFatalError(
|
|
1115
|
+
this._protocol,
|
|
1116
|
+
'Received malformed host public key',
|
|
1117
|
+
'handshake',
|
|
1118
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (hostPubKeyType !== this.negotiated.serverHostKey) {
|
|
1122
|
+
// Check if we need to make an exception
|
|
1123
|
+
switch (this.negotiated.serverHostKey) {
|
|
1124
|
+
case 'rsa-sha2-256':
|
|
1125
|
+
case 'rsa-sha2-512':
|
|
1126
|
+
if (hostPubKeyType === 'ssh-rsa')
|
|
1127
|
+
break;
|
|
1128
|
+
// FALLTHROUGH
|
|
1129
|
+
default:
|
|
1130
|
+
return doFatalError(
|
|
1131
|
+
this._protocol,
|
|
1132
|
+
'Host key does not match negotiated type',
|
|
1133
|
+
'handshake',
|
|
1134
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
this._hostKey = hostPubKey;
|
|
1140
|
+
this._dhData = dhData;
|
|
1141
|
+
this._sig = sig;
|
|
1142
|
+
|
|
1143
|
+
let checked = false;
|
|
1144
|
+
let ret;
|
|
1145
|
+
if (this._protocol._hostVerifier === undefined) {
|
|
1146
|
+
ret = true;
|
|
1147
|
+
this._protocol._debug && this._protocol._debug(
|
|
1148
|
+
'Host accepted by default (no verification)'
|
|
1149
|
+
);
|
|
1150
|
+
} else {
|
|
1151
|
+
ret = this._protocol._hostVerifier(hostPubKey, (permitted) => {
|
|
1152
|
+
if (checked)
|
|
1153
|
+
return;
|
|
1154
|
+
checked = true;
|
|
1155
|
+
if (permitted === false) {
|
|
1156
|
+
this._protocol._debug && this._protocol._debug(
|
|
1157
|
+
'Host denied (verification failed)'
|
|
1158
|
+
);
|
|
1159
|
+
return doFatalError(
|
|
1160
|
+
this._protocol,
|
|
1161
|
+
'Host denied (verification failed)',
|
|
1162
|
+
'handshake',
|
|
1163
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
this._protocol._debug && this._protocol._debug(
|
|
1167
|
+
'Host accepted (verified)'
|
|
1168
|
+
);
|
|
1169
|
+
this._hostVerified = true;
|
|
1170
|
+
if (this._receivedNEWKEYS)
|
|
1171
|
+
this.finish();
|
|
1172
|
+
else
|
|
1173
|
+
trySendNEWKEYS(this);
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (ret === undefined) {
|
|
1177
|
+
// Async host verification
|
|
1178
|
+
++this._step;
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
checked = true;
|
|
1182
|
+
if (ret === false) {
|
|
1183
|
+
this._protocol._debug && this._protocol._debug(
|
|
1184
|
+
'Host denied (verification failed)'
|
|
1185
|
+
);
|
|
1186
|
+
return doFatalError(
|
|
1187
|
+
this._protocol,
|
|
1188
|
+
'Host denied (verification failed)',
|
|
1189
|
+
'handshake',
|
|
1190
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
this._protocol._debug && this._protocol._debug(
|
|
1194
|
+
'Host accepted (verified)'
|
|
1195
|
+
);
|
|
1196
|
+
this._hostVerified = true;
|
|
1197
|
+
trySendNEWKEYS(this);
|
|
1198
|
+
}
|
|
1199
|
+
++this._step;
|
|
1200
|
+
break;
|
|
1201
|
+
case 2:
|
|
1202
|
+
if (type !== MESSAGE.NEWKEYS) {
|
|
1203
|
+
return doFatalError(
|
|
1204
|
+
this._protocol,
|
|
1205
|
+
`Received packet ${type} instead of ${MESSAGE.NEWKEYS}`,
|
|
1206
|
+
'handshake',
|
|
1207
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
this._protocol._debug && this._protocol._debug(
|
|
1211
|
+
'Inbound: NEWKEYS'
|
|
1212
|
+
);
|
|
1213
|
+
this._receivedNEWKEYS = true;
|
|
1214
|
+
++this._step;
|
|
1215
|
+
if (this._protocol._server || this._hostVerified)
|
|
1216
|
+
return this.finish();
|
|
1217
|
+
|
|
1218
|
+
// Signal to current decipher that we need to change to a new decipher
|
|
1219
|
+
// for the next packet
|
|
1220
|
+
return false;
|
|
1221
|
+
default:
|
|
1222
|
+
return doFatalError(
|
|
1223
|
+
this._protocol,
|
|
1224
|
+
`Received unexpected packet ${type} after NEWKEYS`,
|
|
1225
|
+
'handshake',
|
|
1226
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
class Curve25519Exchange extends KeyExchange {
|
|
1233
|
+
constructor(hashName, ...args) {
|
|
1234
|
+
super(...args);
|
|
1235
|
+
|
|
1236
|
+
this.type = '25519';
|
|
1237
|
+
this.hashName = hashName;
|
|
1238
|
+
this._keys = null;
|
|
1239
|
+
}
|
|
1240
|
+
generateKeys() {
|
|
1241
|
+
if (!this._keys)
|
|
1242
|
+
this._keys = generateKeyPairSync('x25519');
|
|
1243
|
+
}
|
|
1244
|
+
getPublicKey() {
|
|
1245
|
+
this.generateKeys();
|
|
1246
|
+
|
|
1247
|
+
const key = this._keys.publicKey.export({ type: 'spki', format: 'der' });
|
|
1248
|
+
return key.slice(-32); // HACK: avoids parsing DER/BER header
|
|
1249
|
+
}
|
|
1250
|
+
convertPublicKey(key) {
|
|
1251
|
+
let newKey;
|
|
1252
|
+
let idx = 0;
|
|
1253
|
+
let len = key.length;
|
|
1254
|
+
while (key[idx] === 0x00) {
|
|
1255
|
+
++idx;
|
|
1256
|
+
--len;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (key.length === 32)
|
|
1260
|
+
return key;
|
|
1261
|
+
|
|
1262
|
+
if (len !== key.length) {
|
|
1263
|
+
newKey = Buffer.allocUnsafe(len);
|
|
1264
|
+
key.copy(newKey, 0, idx);
|
|
1265
|
+
key = newKey;
|
|
1266
|
+
}
|
|
1267
|
+
return key;
|
|
1268
|
+
}
|
|
1269
|
+
computeSecret(otherPublicKey) {
|
|
1270
|
+
this.generateKeys();
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
const asnWriter = new Ber.Writer();
|
|
1274
|
+
asnWriter.startSequence();
|
|
1275
|
+
// algorithm
|
|
1276
|
+
asnWriter.startSequence();
|
|
1277
|
+
asnWriter.writeOID('1.3.101.110'); // id-X25519
|
|
1278
|
+
asnWriter.endSequence();
|
|
1279
|
+
|
|
1280
|
+
// PublicKey
|
|
1281
|
+
asnWriter.startSequence(Ber.BitString);
|
|
1282
|
+
asnWriter.writeByte(0x00);
|
|
1283
|
+
// XXX: hack to write a raw buffer without a tag -- yuck
|
|
1284
|
+
asnWriter._ensure(otherPublicKey.length);
|
|
1285
|
+
otherPublicKey.copy(asnWriter._buf,
|
|
1286
|
+
asnWriter._offset,
|
|
1287
|
+
0,
|
|
1288
|
+
otherPublicKey.length);
|
|
1289
|
+
asnWriter._offset += otherPublicKey.length;
|
|
1290
|
+
asnWriter.endSequence();
|
|
1291
|
+
asnWriter.endSequence();
|
|
1292
|
+
|
|
1293
|
+
return convertToMpint(diffieHellman({
|
|
1294
|
+
privateKey: this._keys.privateKey,
|
|
1295
|
+
publicKey: createPublicKey({
|
|
1296
|
+
key: asnWriter.buffer,
|
|
1297
|
+
type: 'spki',
|
|
1298
|
+
format: 'der',
|
|
1299
|
+
}),
|
|
1300
|
+
}));
|
|
1301
|
+
} catch (ex) {
|
|
1302
|
+
return ex;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
class ECDHExchange extends KeyExchange {
|
|
1308
|
+
constructor(curveName, hashName, ...args) {
|
|
1309
|
+
super(...args);
|
|
1310
|
+
|
|
1311
|
+
this.type = 'ecdh';
|
|
1312
|
+
this.curveName = curveName;
|
|
1313
|
+
this.hashName = hashName;
|
|
1314
|
+
}
|
|
1315
|
+
generateKeys() {
|
|
1316
|
+
if (!this._dh) {
|
|
1317
|
+
this._dh = createECDH(this.curveName);
|
|
1318
|
+
this._public = this._dh.generateKeys();
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
class DHGroupExchange extends KeyExchange {
|
|
1324
|
+
constructor(hashName, ...args) {
|
|
1325
|
+
super(...args);
|
|
1326
|
+
|
|
1327
|
+
this.type = 'groupex';
|
|
1328
|
+
this.hashName = hashName;
|
|
1329
|
+
this._prime = null;
|
|
1330
|
+
this._generator = null;
|
|
1331
|
+
this._minBits = GEX_MIN_BITS;
|
|
1332
|
+
this._prefBits = dhEstimate(this.negotiated);
|
|
1333
|
+
if (this._protocol._compatFlags & COMPAT.BUG_DHGEX_LARGE)
|
|
1334
|
+
this._prefBits = Math.min(this._prefBits, 4096);
|
|
1335
|
+
this._maxBits = GEX_MAX_BITS;
|
|
1336
|
+
}
|
|
1337
|
+
start() {
|
|
1338
|
+
if (this._protocol._server)
|
|
1339
|
+
return;
|
|
1340
|
+
this._protocol._debug && this._protocol._debug(
|
|
1341
|
+
'Outbound: Sending KEXDH_GEX_REQUEST'
|
|
1342
|
+
);
|
|
1343
|
+
let p = this._protocol._packetRW.write.allocStartKEX;
|
|
1344
|
+
const packet = this._protocol._packetRW.write.alloc(
|
|
1345
|
+
1 + 4 + 4 + 4,
|
|
1346
|
+
true
|
|
1347
|
+
);
|
|
1348
|
+
packet[p] = MESSAGE.KEXDH_GEX_REQUEST;
|
|
1349
|
+
writeUInt32BE(packet, this._minBits, ++p);
|
|
1350
|
+
writeUInt32BE(packet, this._prefBits, p += 4);
|
|
1351
|
+
writeUInt32BE(packet, this._maxBits, p += 4);
|
|
1352
|
+
this._protocol._cipher.encrypt(
|
|
1353
|
+
this._protocol._packetRW.write.finalize(packet, true)
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
generateKeys() {
|
|
1357
|
+
if (!this._dh && this._prime && this._generator) {
|
|
1358
|
+
this._dh = createDiffieHellman(this._prime, this._generator);
|
|
1359
|
+
this._public = this._dh.generateKeys();
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
setDHParams(prime, generator) {
|
|
1363
|
+
if (!Buffer.isBuffer(prime))
|
|
1364
|
+
throw new Error('Invalid prime value');
|
|
1365
|
+
if (!Buffer.isBuffer(generator))
|
|
1366
|
+
throw new Error('Invalid generator value');
|
|
1367
|
+
this._prime = prime;
|
|
1368
|
+
this._generator = generator;
|
|
1369
|
+
}
|
|
1370
|
+
getDHParams() {
|
|
1371
|
+
if (this._dh) {
|
|
1372
|
+
return {
|
|
1373
|
+
prime: convertToMpint(this._dh.getPrime()),
|
|
1374
|
+
generator: convertToMpint(this._dh.getGenerator()),
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
parse(payload) {
|
|
1379
|
+
const type = payload[0];
|
|
1380
|
+
switch (this._step) {
|
|
1381
|
+
case 1:
|
|
1382
|
+
if (this._protocol._server) {
|
|
1383
|
+
if (type !== MESSAGE.KEXDH_GEX_REQUEST) {
|
|
1384
|
+
return doFatalError(
|
|
1385
|
+
this._protocol,
|
|
1386
|
+
`Received packet ${type} instead of `
|
|
1387
|
+
+ MESSAGE.KEXDH_GEX_REQUEST,
|
|
1388
|
+
'handshake',
|
|
1389
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
// TODO: allow user implementation to provide safe prime and
|
|
1393
|
+
// generator on demand to support group exchange on server side
|
|
1394
|
+
return doFatalError(
|
|
1395
|
+
this._protocol,
|
|
1396
|
+
'Group exchange not implemented for server',
|
|
1397
|
+
'handshake',
|
|
1398
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (type !== MESSAGE.KEXDH_GEX_GROUP) {
|
|
1403
|
+
return doFatalError(
|
|
1404
|
+
this._protocol,
|
|
1405
|
+
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_GROUP}`,
|
|
1406
|
+
'handshake',
|
|
1407
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
this._protocol._debug && this._protocol._debug(
|
|
1412
|
+
'Received DH GEX Group'
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1415
|
+
/*
|
|
1416
|
+
byte SSH_MSG_KEX_DH_GEX_GROUP
|
|
1417
|
+
mpint p, safe prime
|
|
1418
|
+
mpint g, generator for subgroup in GF(p)
|
|
1419
|
+
*/
|
|
1420
|
+
bufferParser.init(payload, 1);
|
|
1421
|
+
let prime;
|
|
1422
|
+
let gen;
|
|
1423
|
+
if ((prime = bufferParser.readString()) === undefined
|
|
1424
|
+
|| (gen = bufferParser.readString()) === undefined) {
|
|
1425
|
+
bufferParser.clear();
|
|
1426
|
+
return doFatalError(
|
|
1427
|
+
this._protocol,
|
|
1428
|
+
'Received malformed KEXDH_GEX_GROUP',
|
|
1429
|
+
'handshake',
|
|
1430
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
bufferParser.clear();
|
|
1434
|
+
|
|
1435
|
+
// TODO: validate prime
|
|
1436
|
+
this.setDHParams(prime, gen);
|
|
1437
|
+
this.generateKeys();
|
|
1438
|
+
const pubkey = this.getPublicKey();
|
|
1439
|
+
|
|
1440
|
+
this._protocol._debug && this._protocol._debug(
|
|
1441
|
+
'Outbound: Sending KEXDH_GEX_INIT'
|
|
1442
|
+
);
|
|
1443
|
+
|
|
1444
|
+
let p = this._protocol._packetRW.write.allocStartKEX;
|
|
1445
|
+
const packet =
|
|
1446
|
+
this._protocol._packetRW.write.alloc(1 + 4 + pubkey.length, true);
|
|
1447
|
+
packet[p] = MESSAGE.KEXDH_GEX_INIT;
|
|
1448
|
+
writeUInt32BE(packet, pubkey.length, ++p);
|
|
1449
|
+
packet.set(pubkey, p += 4);
|
|
1450
|
+
this._protocol._cipher.encrypt(
|
|
1451
|
+
this._protocol._packetRW.write.finalize(packet, true)
|
|
1452
|
+
);
|
|
1453
|
+
|
|
1454
|
+
++this._step;
|
|
1455
|
+
break;
|
|
1456
|
+
case 2:
|
|
1457
|
+
if (this._protocol._server) {
|
|
1458
|
+
if (type !== MESSAGE.KEXDH_GEX_INIT) {
|
|
1459
|
+
return doFatalError(
|
|
1460
|
+
this._protocol,
|
|
1461
|
+
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_INIT}`,
|
|
1462
|
+
'handshake',
|
|
1463
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
this._protocol._debug && this._protocol._debug(
|
|
1467
|
+
'Received DH GEX Init'
|
|
1468
|
+
);
|
|
1469
|
+
return doFatalError(
|
|
1470
|
+
this._protocol,
|
|
1471
|
+
'Group exchange not implemented for server',
|
|
1472
|
+
'handshake',
|
|
1473
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1474
|
+
);
|
|
1475
|
+
} else if (type !== MESSAGE.KEXDH_GEX_REPLY) {
|
|
1476
|
+
return doFatalError(
|
|
1477
|
+
this._protocol,
|
|
1478
|
+
`Received packet ${type} instead of ${MESSAGE.KEXDH_GEX_REPLY}`,
|
|
1479
|
+
'handshake',
|
|
1480
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
this._protocol._debug && this._protocol._debug(
|
|
1484
|
+
'Received DH GEX Reply'
|
|
1485
|
+
);
|
|
1486
|
+
this._step = 1;
|
|
1487
|
+
payload[0] = MESSAGE.KEXDH_REPLY;
|
|
1488
|
+
this.parse = KeyExchange.prototype.parse;
|
|
1489
|
+
this.parse(payload);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
class DHExchange extends KeyExchange {
|
|
1495
|
+
constructor(groupName, hashName, ...args) {
|
|
1496
|
+
super(...args);
|
|
1497
|
+
|
|
1498
|
+
this.type = 'group';
|
|
1499
|
+
this.groupName = groupName;
|
|
1500
|
+
this.hashName = hashName;
|
|
1501
|
+
}
|
|
1502
|
+
start() {
|
|
1503
|
+
if (!this._protocol._server) {
|
|
1504
|
+
this._protocol._debug && this._protocol._debug(
|
|
1505
|
+
'Outbound: Sending KEXDH_INIT'
|
|
1506
|
+
);
|
|
1507
|
+
const pubKey = this.getPublicKey();
|
|
1508
|
+
let p = this._protocol._packetRW.write.allocStartKEX;
|
|
1509
|
+
const packet =
|
|
1510
|
+
this._protocol._packetRW.write.alloc(1 + 4 + pubKey.length, true);
|
|
1511
|
+
packet[p] = MESSAGE.KEXDH_INIT;
|
|
1512
|
+
writeUInt32BE(packet, pubKey.length, ++p);
|
|
1513
|
+
packet.set(pubKey, p += 4);
|
|
1514
|
+
this._protocol._cipher.encrypt(
|
|
1515
|
+
this._protocol._packetRW.write.finalize(packet, true)
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
generateKeys() {
|
|
1520
|
+
if (!this._dh) {
|
|
1521
|
+
this._dh = createDiffieHellmanGroup(this.groupName);
|
|
1522
|
+
this._public = this._dh.generateKeys();
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
getDHParams() {
|
|
1526
|
+
if (this._dh) {
|
|
1527
|
+
return {
|
|
1528
|
+
prime: convertToMpint(this._dh.getPrime()),
|
|
1529
|
+
generator: convertToMpint(this._dh.getGenerator()),
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
return (negotiated, ...args) => {
|
|
1536
|
+
if (typeof negotiated !== 'object' || negotiated === null)
|
|
1537
|
+
throw new Error('Invalid negotiated argument');
|
|
1538
|
+
const kexType = negotiated.kex;
|
|
1539
|
+
if (typeof kexType === 'string') {
|
|
1540
|
+
args = [negotiated, ...args];
|
|
1541
|
+
switch (kexType) {
|
|
1542
|
+
case 'curve25519-sha256':
|
|
1543
|
+
case 'curve25519-sha256@libssh.org':
|
|
1544
|
+
if (!curve25519Supported)
|
|
1545
|
+
break;
|
|
1546
|
+
return new Curve25519Exchange('sha256', ...args);
|
|
1547
|
+
|
|
1548
|
+
case 'ecdh-sha2-nistp256':
|
|
1549
|
+
return new ECDHExchange('prime256v1', 'sha256', ...args);
|
|
1550
|
+
case 'ecdh-sha2-nistp384':
|
|
1551
|
+
return new ECDHExchange('secp384r1', 'sha384', ...args);
|
|
1552
|
+
case 'ecdh-sha2-nistp521':
|
|
1553
|
+
return new ECDHExchange('secp521r1', 'sha512', ...args);
|
|
1554
|
+
|
|
1555
|
+
case 'diffie-hellman-group1-sha1':
|
|
1556
|
+
return new DHExchange('modp2', 'sha1', ...args);
|
|
1557
|
+
case 'diffie-hellman-group14-sha1':
|
|
1558
|
+
return new DHExchange('modp14', 'sha1', ...args);
|
|
1559
|
+
case 'diffie-hellman-group14-sha256':
|
|
1560
|
+
return new DHExchange('modp14', 'sha256', ...args);
|
|
1561
|
+
case 'diffie-hellman-group15-sha512':
|
|
1562
|
+
return new DHExchange('modp15', 'sha512', ...args);
|
|
1563
|
+
case 'diffie-hellman-group16-sha512':
|
|
1564
|
+
return new DHExchange('modp16', 'sha512', ...args);
|
|
1565
|
+
case 'diffie-hellman-group17-sha512':
|
|
1566
|
+
return new DHExchange('modp17', 'sha512', ...args);
|
|
1567
|
+
case 'diffie-hellman-group18-sha512':
|
|
1568
|
+
return new DHExchange('modp18', 'sha512', ...args);
|
|
1569
|
+
|
|
1570
|
+
case 'diffie-hellman-group-exchange-sha1':
|
|
1571
|
+
return new DHGroupExchange('sha1', ...args);
|
|
1572
|
+
case 'diffie-hellman-group-exchange-sha256':
|
|
1573
|
+
return new DHGroupExchange('sha256', ...args);
|
|
1574
|
+
}
|
|
1575
|
+
throw new Error(`Unsupported key exchange algorithm: ${kexType}`);
|
|
1576
|
+
}
|
|
1577
|
+
throw new Error(`Invalid key exchange type: ${kexType}`);
|
|
1578
|
+
};
|
|
1579
|
+
})();
|
|
1580
|
+
|
|
1581
|
+
const KexInit = (() => {
|
|
1582
|
+
const KEX_PROPERTY_NAMES = [
|
|
1583
|
+
'kex',
|
|
1584
|
+
'serverHostKey',
|
|
1585
|
+
['cs', 'cipher' ],
|
|
1586
|
+
['sc', 'cipher' ],
|
|
1587
|
+
['cs', 'mac' ],
|
|
1588
|
+
['sc', 'mac' ],
|
|
1589
|
+
['cs', 'compress' ],
|
|
1590
|
+
['sc', 'compress' ],
|
|
1591
|
+
['cs', 'lang' ],
|
|
1592
|
+
['sc', 'lang' ],
|
|
1593
|
+
];
|
|
1594
|
+
return class KexInit {
|
|
1595
|
+
constructor(obj) {
|
|
1596
|
+
if (typeof obj !== 'object' || obj === null)
|
|
1597
|
+
throw new TypeError('Argument must be an object');
|
|
1598
|
+
|
|
1599
|
+
const lists = {
|
|
1600
|
+
kex: undefined,
|
|
1601
|
+
serverHostKey: undefined,
|
|
1602
|
+
cs: {
|
|
1603
|
+
cipher: undefined,
|
|
1604
|
+
mac: undefined,
|
|
1605
|
+
compress: undefined,
|
|
1606
|
+
lang: undefined,
|
|
1607
|
+
},
|
|
1608
|
+
sc: {
|
|
1609
|
+
cipher: undefined,
|
|
1610
|
+
mac: undefined,
|
|
1611
|
+
compress: undefined,
|
|
1612
|
+
lang: undefined,
|
|
1613
|
+
},
|
|
1614
|
+
|
|
1615
|
+
all: undefined,
|
|
1616
|
+
};
|
|
1617
|
+
let totalSize = 0;
|
|
1618
|
+
for (const prop of KEX_PROPERTY_NAMES) {
|
|
1619
|
+
let base;
|
|
1620
|
+
let val;
|
|
1621
|
+
let desc;
|
|
1622
|
+
let key;
|
|
1623
|
+
if (typeof prop === 'string') {
|
|
1624
|
+
base = lists;
|
|
1625
|
+
val = obj[prop];
|
|
1626
|
+
desc = key = prop;
|
|
1627
|
+
} else {
|
|
1628
|
+
const parent = prop[0];
|
|
1629
|
+
base = lists[parent];
|
|
1630
|
+
key = prop[1];
|
|
1631
|
+
val = obj[parent][key];
|
|
1632
|
+
desc = `${parent}.${key}`;
|
|
1633
|
+
}
|
|
1634
|
+
const entry = { array: undefined, buffer: undefined };
|
|
1635
|
+
if (Buffer.isBuffer(val)) {
|
|
1636
|
+
entry.array = ('' + val).split(',');
|
|
1637
|
+
entry.buffer = val;
|
|
1638
|
+
totalSize += 4 + val.length;
|
|
1639
|
+
} else {
|
|
1640
|
+
if (typeof val === 'string')
|
|
1641
|
+
val = val.split(',');
|
|
1642
|
+
if (Array.isArray(val)) {
|
|
1643
|
+
entry.array = val;
|
|
1644
|
+
entry.buffer = Buffer.from(val.join(','));
|
|
1645
|
+
} else {
|
|
1646
|
+
throw new TypeError(`Invalid \`${desc}\` type: ${typeof val}`);
|
|
1647
|
+
}
|
|
1648
|
+
totalSize += 4 + entry.buffer.length;
|
|
1649
|
+
}
|
|
1650
|
+
base[key] = entry;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const all = Buffer.allocUnsafe(totalSize);
|
|
1654
|
+
lists.all = all;
|
|
1655
|
+
|
|
1656
|
+
let allPos = 0;
|
|
1657
|
+
for (const prop of KEX_PROPERTY_NAMES) {
|
|
1658
|
+
let data;
|
|
1659
|
+
if (typeof prop === 'string')
|
|
1660
|
+
data = lists[prop].buffer;
|
|
1661
|
+
else
|
|
1662
|
+
data = lists[prop[0]][prop[1]].buffer;
|
|
1663
|
+
allPos = writeUInt32BE(all, data.length, allPos);
|
|
1664
|
+
all.set(data, allPos);
|
|
1665
|
+
allPos += data.length;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
this.totalSize = totalSize;
|
|
1669
|
+
this.lists = lists;
|
|
1670
|
+
}
|
|
1671
|
+
copyAllTo(buf, offset) {
|
|
1672
|
+
const src = this.lists.all;
|
|
1673
|
+
if (typeof offset !== 'number')
|
|
1674
|
+
throw new TypeError(`Invalid offset value: ${typeof offset}`);
|
|
1675
|
+
if (buf.length - offset < src.length)
|
|
1676
|
+
throw new Error('Insufficient space to copy list');
|
|
1677
|
+
buf.set(src, offset);
|
|
1678
|
+
return src.length;
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
})();
|
|
1682
|
+
|
|
1683
|
+
const hashString = (() => {
|
|
1684
|
+
const LEN = Buffer.allocUnsafe(4);
|
|
1685
|
+
return (hash, buf) => {
|
|
1686
|
+
writeUInt32BE(LEN, buf.length, 0);
|
|
1687
|
+
hash.update(LEN);
|
|
1688
|
+
hash.update(buf);
|
|
1689
|
+
};
|
|
1690
|
+
})();
|
|
1691
|
+
|
|
1692
|
+
function generateKEXVal(len, hashName, secret, exchangeHash, sessionID, char) {
|
|
1693
|
+
let ret;
|
|
1694
|
+
if (len) {
|
|
1695
|
+
let digest = createHash(hashName)
|
|
1696
|
+
.update(secret)
|
|
1697
|
+
.update(exchangeHash)
|
|
1698
|
+
.update(char)
|
|
1699
|
+
.update(sessionID)
|
|
1700
|
+
.digest();
|
|
1701
|
+
while (digest.length < len) {
|
|
1702
|
+
const chunk = createHash(hashName)
|
|
1703
|
+
.update(secret)
|
|
1704
|
+
.update(exchangeHash)
|
|
1705
|
+
.update(digest)
|
|
1706
|
+
.digest();
|
|
1707
|
+
const extended = Buffer.allocUnsafe(digest.length + chunk.length);
|
|
1708
|
+
extended.set(digest, 0);
|
|
1709
|
+
extended.set(chunk, digest.length);
|
|
1710
|
+
digest = extended;
|
|
1711
|
+
}
|
|
1712
|
+
if (digest.length === len)
|
|
1713
|
+
ret = digest;
|
|
1714
|
+
else
|
|
1715
|
+
ret = new FastBuffer(digest.buffer, digest.byteOffset, len);
|
|
1716
|
+
} else {
|
|
1717
|
+
ret = EMPTY_BUFFER;
|
|
1718
|
+
}
|
|
1719
|
+
return ret;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function onKEXPayload(state, payload) {
|
|
1723
|
+
// XXX: move this to the Decipher implementations?
|
|
1724
|
+
if (payload.length === 0) {
|
|
1725
|
+
this._debug && this._debug('Inbound: Skipping empty packet payload');
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (this._skipNextInboundPacket) {
|
|
1730
|
+
this._skipNextInboundPacket = false;
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
payload = this._packetRW.read.read(payload);
|
|
1735
|
+
|
|
1736
|
+
const type = payload[0];
|
|
1737
|
+
switch (type) {
|
|
1738
|
+
case MESSAGE.DISCONNECT:
|
|
1739
|
+
case MESSAGE.IGNORE:
|
|
1740
|
+
case MESSAGE.UNIMPLEMENTED:
|
|
1741
|
+
case MESSAGE.DEBUG:
|
|
1742
|
+
if (!MESSAGE_HANDLERS)
|
|
1743
|
+
MESSAGE_HANDLERS = require('./handlers.js');
|
|
1744
|
+
return MESSAGE_HANDLERS[type](this, payload);
|
|
1745
|
+
case MESSAGE.KEXINIT:
|
|
1746
|
+
if (!state.firstPacket) {
|
|
1747
|
+
return doFatalError(
|
|
1748
|
+
this,
|
|
1749
|
+
'Received extra KEXINIT during handshake',
|
|
1750
|
+
'handshake',
|
|
1751
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
state.firstPacket = false;
|
|
1755
|
+
return handleKexInit(this, payload);
|
|
1756
|
+
default:
|
|
1757
|
+
if (type < 20 || type > 49) {
|
|
1758
|
+
return doFatalError(
|
|
1759
|
+
this,
|
|
1760
|
+
`Received unexpected packet type ${type}`,
|
|
1761
|
+
'handshake',
|
|
1762
|
+
DISCONNECT_REASON.KEY_EXCHANGE_FAILED
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
return this._kex.parse(payload);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function dhEstimate(neg) {
|
|
1771
|
+
const csCipher = CIPHER_INFO[neg.cs.cipher];
|
|
1772
|
+
const scCipher = CIPHER_INFO[neg.sc.cipher];
|
|
1773
|
+
// XXX: if OpenSSH's `umac-*` MACs are ever supported, their key lengths will
|
|
1774
|
+
// also need to be considered when calculating `bits`
|
|
1775
|
+
const bits = Math.max(
|
|
1776
|
+
0,
|
|
1777
|
+
(csCipher.sslName === 'des-ede3-cbc' ? 14 : csCipher.keyLen),
|
|
1778
|
+
csCipher.blockLen,
|
|
1779
|
+
csCipher.ivLen,
|
|
1780
|
+
(scCipher.sslName === 'des-ede3-cbc' ? 14 : scCipher.keyLen),
|
|
1781
|
+
scCipher.blockLen,
|
|
1782
|
+
scCipher.ivLen
|
|
1783
|
+
) * 8;
|
|
1784
|
+
if (bits <= 112)
|
|
1785
|
+
return 2048;
|
|
1786
|
+
if (bits <= 128)
|
|
1787
|
+
return 3072;
|
|
1788
|
+
if (bits <= 192)
|
|
1789
|
+
return 7680;
|
|
1790
|
+
return 8192;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function trySendNEWKEYS(kex) {
|
|
1794
|
+
if (!kex._sentNEWKEYS) {
|
|
1795
|
+
kex._protocol._debug && kex._protocol._debug(
|
|
1796
|
+
'Outbound: Sending NEWKEYS'
|
|
1797
|
+
);
|
|
1798
|
+
const p = kex._protocol._packetRW.write.allocStartKEX;
|
|
1799
|
+
const packet = kex._protocol._packetRW.write.alloc(1, true);
|
|
1800
|
+
packet[p] = MESSAGE.NEWKEYS;
|
|
1801
|
+
kex._protocol._cipher.encrypt(
|
|
1802
|
+
kex._protocol._packetRW.write.finalize(packet, true)
|
|
1803
|
+
);
|
|
1804
|
+
kex._sentNEWKEYS = true;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
module.exports = {
|
|
1809
|
+
KexInit,
|
|
1810
|
+
kexinit,
|
|
1811
|
+
onKEXPayload,
|
|
1812
|
+
DEFAULT_KEXINIT: new KexInit({
|
|
1813
|
+
kex: DEFAULT_KEX,
|
|
1814
|
+
serverHostKey: DEFAULT_SERVER_HOST_KEY,
|
|
1815
|
+
cs: {
|
|
1816
|
+
cipher: DEFAULT_CIPHER,
|
|
1817
|
+
mac: DEFAULT_MAC,
|
|
1818
|
+
compress: DEFAULT_COMPRESSION,
|
|
1819
|
+
lang: [],
|
|
1820
|
+
},
|
|
1821
|
+
sc: {
|
|
1822
|
+
cipher: DEFAULT_CIPHER,
|
|
1823
|
+
mac: DEFAULT_MAC,
|
|
1824
|
+
compress: DEFAULT_COMPRESSION,
|
|
1825
|
+
lang: [],
|
|
1826
|
+
},
|
|
1827
|
+
}),
|
|
1828
|
+
HANDLERS: {
|
|
1829
|
+
[MESSAGE.KEXINIT]: handleKexInit,
|
|
1830
|
+
},
|
|
1831
|
+
};
|