@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/lib/agent.js CHANGED
@@ -1,419 +1,1123 @@
1
- var Socket = require('net').Socket;
2
- var EventEmitter = require('events').EventEmitter;
3
- var inherits = require('util').inherits;
4
- var path = require('path');
5
- var fs = require('fs');
6
- var cp = require('child_process');
7
-
8
- var readUInt32BE = require('./buffer-helpers').readUInt32BE;
9
- var writeUInt32BE = require('./buffer-helpers').writeUInt32BE;
10
- var writeUInt32LE = require('./buffer-helpers').writeUInt32LE;
11
-
12
- var REQUEST_IDENTITIES = 11;
13
- var IDENTITIES_ANSWER = 12;
14
- var SIGN_REQUEST = 13;
15
- var SIGN_RESPONSE = 14;
16
- var FAILURE = 5;
17
-
18
- var RE_CYGWIN_SOCK = /^\!<socket >(\d+) s ([A-Z0-9]{8}\-[A-Z0-9]{8}\-[A-Z0-9]{8}\-[A-Z0-9]{8})/;
19
-
20
- // Format of `//./pipe/ANYTHING`, with forward slashes and backward slashes being interchangeable
21
- var WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/;
22
-
23
- module.exports = function(sockPath, key, keyType, data, cb) {
24
- var sock;
25
- var error;
26
- var sig;
27
- var datalen;
28
- var keylen = 0;
29
- var isSigning = Buffer.isBuffer(key);
30
- var type;
31
- var count = 0;
32
- var siglen = 0;
33
- var nkeys = 0;
34
- var keys;
35
- var comlen = 0;
36
- var comment = false;
37
- var accept;
38
- var reject;
39
-
40
- if (typeof key === 'function' && typeof keyType === 'function') {
41
- // agent forwarding
42
- accept = key;
43
- reject = keyType;
44
- } else if (isSigning) {
45
- keylen = key.length;
46
- datalen = data.length;
47
- } else {
48
- cb = key;
49
- key = undefined;
1
+ 'use strict';
2
+
3
+ const { Socket } = require('net');
4
+ const { Duplex } = require('stream');
5
+ const { resolve } = require('path');
6
+ const { readFile } = require('fs');
7
+ const { execFile, spawn } = require('child_process');
8
+
9
+ const { isParsedKey, parseKey } = require('./protocol/keyParser.js');
10
+
11
+ const {
12
+ makeBufferParser,
13
+ readUInt32BE,
14
+ writeUInt32BE,
15
+ writeUInt32LE,
16
+ } = require('./protocol/utils.js');
17
+
18
+ function once(cb) {
19
+ let called = false;
20
+ return (...args) => {
21
+ if (called)
22
+ return;
23
+ called = true;
24
+ cb(...args);
25
+ };
26
+ }
27
+
28
+ function concat(buf1, buf2) {
29
+ const combined = Buffer.allocUnsafe(buf1.length + buf2.length);
30
+ buf1.copy(combined, 0);
31
+ buf2.copy(combined, buf1.length);
32
+ return combined;
33
+ }
34
+
35
+ function noop() {}
36
+
37
+ const EMPTY_BUF = Buffer.alloc(0);
38
+
39
+ const binaryParser = makeBufferParser();
40
+
41
+ class BaseAgent {
42
+ getIdentities(cb) {
43
+ cb(new Error('Missing getIdentities() implementation'));
44
+ }
45
+ sign(pubKey, data, options, cb) {
46
+ if (typeof options === 'function')
47
+ cb = options;
48
+ cb(new Error('Missing sign() implementation'));
50
49
  }
50
+ }
51
51
 
52
- function onconnect() {
53
- var buf;
54
- if (isSigning) {
55
- /*
56
- byte SSH2_AGENTC_SIGN_REQUEST
57
- string key_blob
58
- string data
59
- uint32 flags
60
- */
61
- var p = 9;
62
- buf = Buffer.allocUnsafe(4 + 1 + 4 + keylen + 4 + datalen + 4);
63
- writeUInt32BE(buf, buf.length - 4, 0);
64
- buf[4] = SIGN_REQUEST;
65
- writeUInt32BE(buf, keylen, 5);
66
- key.copy(buf, p);
67
- writeUInt32BE(buf, datalen, p += keylen);
68
- data.copy(buf, p += 4);
69
- writeUInt32BE(buf, 0, p += datalen);
70
- sock.write(buf);
71
- } else {
72
- /*
73
- byte SSH2_AGENTC_REQUEST_IDENTITIES
74
- */
75
- sock.write(Buffer.from([0, 0, 0, 1, REQUEST_IDENTITIES]));
52
+ class OpenSSHAgent extends BaseAgent {
53
+ constructor(socketPath) {
54
+ super();
55
+ this.socketPath = socketPath;
56
+ }
57
+
58
+ getStream(cb) {
59
+ cb = once(cb);
60
+ const sock = new Socket();
61
+ sock.on('connect', () => {
62
+ cb(null, sock);
63
+ });
64
+ sock.on('close', onFail)
65
+ .on('end', onFail)
66
+ .on('error', onFail);
67
+ sock.connect(this.socketPath);
68
+
69
+ function onFail() {
70
+ try {
71
+ sock.destroy();
72
+ } catch {}
73
+
74
+ cb(new Error('Failed to connect to agent'));
76
75
  }
77
76
  }
78
- function ondata(chunk) {
79
- for (var i = 0, len = chunk.length; i < len; ++i) {
80
- if (type === undefined) {
81
- // skip over packet length
82
- if (++count === 5) {
83
- type = chunk[i];
84
- count = 0;
85
- }
86
- } else if (type === SIGN_RESPONSE) {
87
- /*
88
- byte SSH2_AGENT_SIGN_RESPONSE
89
- string signature_blob
90
- */
91
- if (!sig) {
92
- siglen <<= 8;
93
- siglen += chunk[i];
94
- if (++count === 4) {
95
- sig = Buffer.allocUnsafe(siglen);
96
- count = 0;
97
- }
98
- } else {
99
- sig[count] = chunk[i];
100
- if (++count === siglen) {
101
- sock.removeAllListeners('data');
102
- return sock.destroy();
103
- }
104
- }
105
- } else if (type === IDENTITIES_ANSWER) {
106
- /*
107
- byte SSH2_AGENT_IDENTITIES_ANSWER
108
- uint32 num_keys
109
-
110
- Followed by zero or more consecutive keys, encoded as:
111
-
112
- string public key blob
113
- string public key comment
114
- */
115
- if (keys === undefined) {
116
- nkeys <<= 8;
117
- nkeys += chunk[i];
118
- if (++count === 4) {
119
- keys = new Array(nkeys);
120
- count = 0;
121
- if (nkeys === 0) {
122
- sock.removeAllListeners('data');
123
- return sock.destroy();
124
- }
125
- }
126
- } else {
127
- if (!key) {
128
- keylen <<= 8;
129
- keylen += chunk[i];
130
- if (++count === 4) {
131
- key = Buffer.allocUnsafe(keylen);
132
- count = 0;
133
- }
134
- } else if (comment === false) {
135
- key[count] = chunk[i];
136
- if (++count === keylen) {
137
- keys[nkeys - 1] = key;
138
- keylen = 0;
139
- count = 0;
140
- comment = true;
141
- if (--nkeys === 0) {
142
- key = undefined;
143
- sock.removeAllListeners('data');
144
- return sock.destroy();
145
- }
146
- }
147
- } else if (comment === true) {
148
- comlen <<= 8;
149
- comlen += chunk[i];
150
- if (++count === 4) {
151
- count = 0;
152
- if (comlen > 0)
153
- comment = comlen;
154
- else {
155
- key = undefined;
156
- comment = false;
157
- }
158
- comlen = 0;
159
- }
160
- } else {
161
- // skip comments
162
- if (++count === comment) {
163
- comment = false;
164
- count = 0;
165
- key = undefined;
166
- }
167
- }
77
+
78
+ getIdentities(cb) {
79
+ cb = once(cb);
80
+ this.getStream((err, stream) => {
81
+ function onFail(err) {
82
+ if (stream) {
83
+ try {
84
+ stream.destroy();
85
+ } catch {}
168
86
  }
169
- } else if (type === FAILURE) {
170
- if (isSigning)
171
- error = new Error('Agent unable to sign data');
172
- else
173
- error = new Error('Unable to retrieve list of keys from agent');
174
- sock.removeAllListeners('data');
175
- return sock.destroy();
87
+ if (!err)
88
+ err = new Error('Failed to retrieve identities from agent');
89
+ cb(err);
176
90
  }
91
+
92
+ if (err)
93
+ return onFail(err);
94
+
95
+ const protocol = new AgentProtocol(true);
96
+ protocol.on('error', onFail);
97
+ protocol.pipe(stream).pipe(protocol);
98
+
99
+ stream.on('close', onFail)
100
+ .on('end', onFail)
101
+ .on('error', onFail);
102
+
103
+ protocol.getIdentities((err, keys) => {
104
+ if (err)
105
+ return onFail(err);
106
+ try {
107
+ stream.destroy();
108
+ } catch {}
109
+ cb(null, keys);
110
+ });
111
+ });
112
+ }
113
+
114
+ sign(pubKey, data, options, cb) {
115
+ if (typeof options === 'function') {
116
+ cb = options;
117
+ options = undefined;
118
+ } else if (typeof options !== 'object' || options === null) {
119
+ options = undefined;
177
120
  }
121
+
122
+ cb = once(cb);
123
+ this.getStream((err, stream) => {
124
+ function onFail(err) {
125
+ if (stream) {
126
+ try {
127
+ stream.destroy();
128
+ } catch {}
129
+ }
130
+ if (!err)
131
+ err = new Error('Failed to sign data with agent');
132
+ cb(err);
133
+ }
134
+
135
+ if (err)
136
+ return onFail(err);
137
+
138
+ const protocol = new AgentProtocol(true);
139
+ protocol.on('error', onFail);
140
+ protocol.pipe(stream).pipe(protocol);
141
+
142
+ stream.on('close', onFail)
143
+ .on('end', onFail)
144
+ .on('error', onFail);
145
+
146
+ protocol.sign(pubKey, data, options, (err, sig) => {
147
+ if (err)
148
+ return onFail(err);
149
+
150
+ try {
151
+ stream.destroy();
152
+ } catch {}
153
+
154
+ cb(null, sig);
155
+ });
156
+ });
178
157
  }
179
- function onerror(err) {
180
- error = err;
158
+ }
159
+
160
+ const PageantAgent = (() => {
161
+ const RET_ERR_BADARGS = 10;
162
+ const RET_ERR_UNAVAILABLE = 11;
163
+ const RET_ERR_NOMAP = 12;
164
+ const RET_ERR_BINSTDIN = 13;
165
+ const RET_ERR_BINSTDOUT = 14;
166
+ const RET_ERR_BADLEN = 15;
167
+
168
+ const EXEPATH = resolve(__dirname, '..', 'util/pagent.exe');
169
+ const ERROR = {
170
+ [RET_ERR_BADARGS]: new Error('Invalid pagent.exe arguments'),
171
+ [RET_ERR_UNAVAILABLE]: new Error('Pageant is not running'),
172
+ [RET_ERR_NOMAP]: new Error('pagent.exe could not create an mmap'),
173
+ [RET_ERR_BINSTDIN]: new Error('pagent.exe could not set mode for stdin'),
174
+ [RET_ERR_BINSTDOUT]: new Error('pagent.exe could not set mode for stdout'),
175
+ [RET_ERR_BADLEN]:
176
+ new Error('pagent.exe did not get expected input payload'),
177
+ };
178
+
179
+ function destroy(stream) {
180
+ stream.buffer = null;
181
+ if (stream.proc) {
182
+ stream.proc.kill();
183
+ stream.proc = undefined;
184
+ }
181
185
  }
182
- function onclose() {
183
- if (error)
184
- cb(error);
185
- else if ((isSigning && !sig) || (!isSigning && !keys))
186
- cb(new Error('Unexpected disconnection from agent'));
187
- else if (isSigning && sig)
188
- cb(undefined, sig);
189
- else if (!isSigning && keys)
190
- cb(undefined, keys);
186
+
187
+ class PageantSocket extends Duplex {
188
+ constructor() {
189
+ super();
190
+ this.proc = undefined;
191
+ this.buffer = null;
192
+ }
193
+ _read(n) {}
194
+ _write(data, encoding, cb) {
195
+ if (this.buffer === null) {
196
+ this.buffer = data;
197
+ } else {
198
+ const newBuffer = Buffer.allocUnsafe(this.buffer.length + data.length);
199
+ this.buffer.copy(newBuffer, 0);
200
+ data.copy(newBuffer, this.buffer.length);
201
+ this.buffer = newBuffer;
202
+ }
203
+ // Wait for at least all length bytes
204
+ if (this.buffer.length < 4)
205
+ return cb();
206
+
207
+ const len = readUInt32BE(this.buffer, 0);
208
+ // Make sure we have a full message before querying pageant
209
+ if ((this.buffer.length - 4) < len)
210
+ return cb();
211
+
212
+ data = this.buffer.slice(0, 4 + len);
213
+ if (this.buffer.length > (4 + len))
214
+ return cb(new Error('Unexpected multiple agent requests'));
215
+ this.buffer = null;
216
+
217
+ let error;
218
+ const proc = this.proc = spawn(EXEPATH, [ data.length ]);
219
+ proc.stdout.on('data', (data) => {
220
+ this.push(data);
221
+ });
222
+ proc.on('error', (err) => {
223
+ error = err;
224
+ cb(error);
225
+ });
226
+ proc.on('close', (code) => {
227
+ this.proc = undefined;
228
+ if (!error) {
229
+ if (error = ERROR[code])
230
+ return cb(error);
231
+ cb();
232
+ }
233
+ });
234
+ proc.stdin.end(data);
235
+ }
236
+ _final(cb) {
237
+ destroy(this);
238
+ cb();
239
+ }
240
+ _destroy(err, cb) {
241
+ destroy(this);
242
+ cb();
243
+ }
191
244
  }
192
245
 
193
- if (process.platform === 'win32' && !WINDOWS_PIPE_REGEX.test(sockPath)) {
194
- if (sockPath === 'pageant') {
195
- // Pageant (PuTTY authentication agent)
196
- sock = new PageantSock();
197
- } else {
198
- // cygwin ssh-agent instance
199
- var triedCygpath = false;
200
- fs.readFile(sockPath, function readCygsocket(err, data) {
246
+ return class PageantAgent extends OpenSSHAgent {
247
+ getStream(cb) {
248
+ cb(null, new PageantSocket());
249
+ }
250
+ };
251
+ })();
252
+
253
+ const CygwinAgent = (() => {
254
+ const RE_CYGWIN_SOCK = /^!<socket >(\d+) s ([A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8})/;
255
+
256
+ return class CygwinAgent extends OpenSSHAgent {
257
+ getStream(cb) {
258
+ cb = once(cb);
259
+
260
+ // The cygwin ssh-agent connection process looks like this:
261
+ // 1. Read the "socket" as a file to get the underlying TCP port and a
262
+ // special "secret" that must be sent to the TCP server.
263
+ // 2. Connect to the server listening on localhost at the TCP port.
264
+ // 3. Send the "secret" to the server.
265
+ // 4. The server sends back the same "secret".
266
+ // 5. Send three 32-bit integer values of zero. This is ordinarily the
267
+ // pid, uid, and gid of this process, but cygwin will actually
268
+ // send us the correct values as a response.
269
+ // 6. The server sends back the pid, uid, gid.
270
+ // 7. Disconnect.
271
+ // 8. Repeat steps 2-6, except send the received pid, uid, and gid in
272
+ // step 5 instead of zeroes.
273
+ // 9. Connection is ready to be used.
274
+
275
+ let socketPath = this.socketPath;
276
+ let triedCygpath = false;
277
+ readFile(socketPath, function readCygsocket(err, data) {
201
278
  if (err) {
202
279
  if (triedCygpath)
203
280
  return cb(new Error('Invalid cygwin unix socket path'));
204
- // try using `cygpath` to convert a possible *nix-style path to the
281
+
282
+ // Try using `cygpath` to convert a possible *nix-style path to the
205
283
  // real Windows path before giving up ...
206
- cp.exec('cygpath -w "' + sockPath + '"',
207
- function(err, stdout, stderr) {
284
+ execFile('cygpath', ['-w', socketPath], (err, stdout, stderr) => {
208
285
  if (err || stdout.length === 0)
209
286
  return cb(new Error('Invalid cygwin unix socket path'));
287
+
210
288
  triedCygpath = true;
211
- sockPath = stdout.toString().replace(/[\r\n]/g, '');
212
- fs.readFile(sockPath, readCygsocket);
289
+ socketPath = stdout.toString().replace(/[\r\n]/g, '');
290
+ readFile(socketPath, readCygsocket);
213
291
  });
214
292
  return;
215
293
  }
216
294
 
217
- var m;
218
- if (m = RE_CYGWIN_SOCK.exec(data.toString('ascii'))) {
219
- var port;
220
- var secret;
221
- var secretbuf;
222
- var state;
223
- var bc = 0;
224
- var isRetrying = false;
225
- var inbuf = [];
226
- var credsbuf = Buffer.allocUnsafe(12);
227
- var i;
228
- var j;
229
-
230
- // use 0 for pid, uid, and gid to ensure we get an error and also
231
- // a valid uid and gid from cygwin so that we don't have to figure it
232
- // out ourselves
233
- credsbuf.fill(0);
234
-
235
- // parse cygwin unix socket file contents
236
- port = parseInt(m[1], 10);
237
- secret = m[2].replace(/\-/g, '');
238
- secretbuf = Buffer.allocUnsafe(16);
239
- for (i = 0, j = 0; j < 32; ++i,j+=2)
240
- secretbuf[i] = parseInt(secret.substring(j, j + 2), 16);
241
-
242
- // convert to host order (always LE for Windows)
243
- for (i = 0; i < 16; i += 4)
244
- writeUInt32LE(secretbuf, readUInt32BE(secretbuf, i), i);
245
-
246
- function _onconnect() {
247
- bc = 0;
248
- state = 'secret';
249
- sock.write(secretbuf);
295
+ const m = RE_CYGWIN_SOCK.exec(data.toString('ascii'));
296
+ if (!m)
297
+ return cb(new Error('Malformed cygwin unix socket file'));
298
+
299
+ let state;
300
+ let bc = 0;
301
+ let isRetrying = false;
302
+ const inBuf = [];
303
+ let sock;
304
+
305
+ // Use 0 for pid, uid, and gid to ensure we get an error and also
306
+ // a valid uid and gid from cygwin so that we don't have to figure it
307
+ // out ourselves
308
+ let credsBuf = Buffer.alloc(12);
309
+
310
+ // Parse cygwin unix socket file contents
311
+ const port = parseInt(m[1], 10);
312
+ const secret = m[2].replace(/-/g, '');
313
+ const secretBuf = Buffer.allocUnsafe(16);
314
+ for (let i = 0, j = 0; j < 32; ++i, j += 2)
315
+ secretBuf[i] = parseInt(secret.substring(j, j + 2), 16);
316
+
317
+ // Convert to host order (always LE for Windows)
318
+ for (let i = 0; i < 16; i += 4)
319
+ writeUInt32LE(secretBuf, readUInt32BE(secretBuf, i), i);
320
+
321
+ tryConnect();
322
+
323
+ function _onconnect() {
324
+ bc = 0;
325
+ state = 'secret';
326
+ sock.write(secretBuf);
327
+ }
328
+
329
+ function _ondata(data) {
330
+ bc += data.length;
331
+
332
+ if (state === 'secret') {
333
+ // The secret we sent is echoed back to us by cygwin, not sure of
334
+ // the reason for that, but we ignore it nonetheless ...
335
+ if (bc === 16) {
336
+ bc = 0;
337
+ state = 'creds';
338
+ sock.write(credsBuf);
339
+ }
340
+ return;
341
+ }
342
+
343
+ if (state === 'creds') {
344
+ // If this is the first attempt, make sure to gather the valid
345
+ // uid and gid for our next attempt
346
+ if (!isRetrying)
347
+ inBuf.push(data);
348
+
349
+ if (bc === 12) {
350
+ sock.removeListener('connect', _onconnect);
351
+ sock.removeListener('data', _ondata);
352
+ sock.removeListener('error', onFail);
353
+ sock.removeListener('end', onFail);
354
+ sock.removeListener('close', onFail);
355
+
356
+ if (isRetrying)
357
+ return cb(null, sock);
358
+
359
+ isRetrying = true;
360
+ credsBuf = Buffer.concat(inBuf);
361
+ writeUInt32LE(credsBuf, process.pid, 0);
362
+ sock.on('error', () => {});
363
+ sock.destroy();
364
+
365
+ tryConnect();
366
+ }
250
367
  }
251
- function _ondata(data) {
252
- bc += data.length;
253
- if (state === 'secret') {
254
- // the secret we sent is echoed back to us by cygwin, not sure of
255
- // the reason for that, but we ignore it nonetheless ...
256
- if (bc === 16) {
257
- bc = 0;
258
- state = 'creds';
259
- sock.write(credsbuf);
368
+ }
369
+
370
+ function onFail() {
371
+ cb(new Error('Problem negotiating cygwin unix socket security'));
372
+ }
373
+
374
+ function tryConnect() {
375
+ sock = new Socket();
376
+ sock.on('connect', _onconnect);
377
+ sock.on('data', _ondata);
378
+ sock.on('error', onFail);
379
+ sock.on('end', onFail);
380
+ sock.on('close', onFail);
381
+ sock.connect(port);
382
+ }
383
+ });
384
+ }
385
+ };
386
+ })();
387
+
388
+ // Format of `//./pipe/ANYTHING`, with forward slashes and backward slashes
389
+ // being interchangeable
390
+ const WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/;
391
+ function createAgent(path) {
392
+ if (process.platform === 'win32' && !WINDOWS_PIPE_REGEX.test(path)) {
393
+ return (path === 'pageant'
394
+ ? new PageantAgent()
395
+ : new CygwinAgent(path));
396
+ }
397
+ return new OpenSSHAgent(path);
398
+ }
399
+
400
+ const AgentProtocol = (() => {
401
+ // Client->Server messages
402
+ const SSH_AGENTC_REQUEST_IDENTITIES = 11;
403
+ const SSH_AGENTC_SIGN_REQUEST = 13;
404
+ // const SSH_AGENTC_ADD_IDENTITY = 17;
405
+ // const SSH_AGENTC_REMOVE_IDENTITY = 18;
406
+ // const SSH_AGENTC_REMOVE_ALL_IDENTITIES = 19;
407
+ // const SSH_AGENTC_ADD_SMARTCARD_KEY = 20;
408
+ // const SSH_AGENTC_REMOVE_SMARTCARD_KEY = 21;
409
+ // const SSH_AGENTC_LOCK = 22;
410
+ // const SSH_AGENTC_UNLOCK = 23;
411
+ // const SSH_AGENTC_ADD_ID_CONSTRAINED = 25;
412
+ // const SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED = 26;
413
+ // const SSH_AGENTC_EXTENSION = 27;
414
+ // Server->Client messages
415
+ const SSH_AGENT_FAILURE = 5;
416
+ // const SSH_AGENT_SUCCESS = 6;
417
+ const SSH_AGENT_IDENTITIES_ANSWER = 12;
418
+ const SSH_AGENT_SIGN_RESPONSE = 14;
419
+ // const SSH_AGENT_EXTENSION_FAILURE = 28;
420
+
421
+ // const SSH_AGENT_CONSTRAIN_LIFETIME = 1;
422
+ // const SSH_AGENT_CONSTRAIN_CONFIRM = 2;
423
+ // const SSH_AGENT_CONSTRAIN_EXTENSION = 255;
424
+
425
+ const SSH_AGENT_RSA_SHA2_256 = (1 << 1);
426
+ const SSH_AGENT_RSA_SHA2_512 = (1 << 2);
427
+
428
+ const ROLE_CLIENT = 0;
429
+ const ROLE_SERVER = 1;
430
+
431
+ // Ensures that responses get sent back in the same order the requests were
432
+ // received
433
+ function processResponses(protocol) {
434
+ let ret;
435
+ while (protocol[SYM_REQS].length) {
436
+ const nextResponse = protocol[SYM_REQS][0][SYM_RESP];
437
+ if (nextResponse === undefined)
438
+ break;
439
+
440
+ protocol[SYM_REQS].shift();
441
+ ret = protocol.push(nextResponse);
442
+ }
443
+ return ret;
444
+ }
445
+
446
+ const SYM_TYPE = Symbol('Inbound Request Type');
447
+ const SYM_RESP = Symbol('Inbound Request Response');
448
+ const SYM_CTX = Symbol('Inbound Request Context');
449
+ class AgentInboundRequest {
450
+ constructor(type, ctx) {
451
+ this[SYM_TYPE] = type;
452
+ this[SYM_RESP] = undefined;
453
+ this[SYM_CTX] = ctx;
454
+ }
455
+ hasResponded() {
456
+ return (this[SYM_RESP] !== undefined);
457
+ }
458
+ getType() {
459
+ return this[SYM_TYPE];
460
+ }
461
+ getContext() {
462
+ return this[SYM_CTX];
463
+ }
464
+ }
465
+ function respond(protocol, req, data) {
466
+ req[SYM_RESP] = data;
467
+ return processResponses(protocol);
468
+ }
469
+
470
+ function cleanup(protocol) {
471
+ protocol[SYM_BUFFER] = null;
472
+ if (protocol[SYM_MODE] === ROLE_CLIENT) {
473
+ const reqs = protocol[SYM_REQS];
474
+ if (reqs && reqs.length) {
475
+ protocol[SYM_REQS] = [];
476
+ for (const req of reqs)
477
+ req.cb(new Error('No reply from server'));
478
+ }
479
+ }
480
+
481
+ // Node streams hackery to make streams do the "right thing"
482
+ try {
483
+ protocol.end();
484
+ } catch {}
485
+ setImmediate(() => {
486
+ if (!protocol[SYM_ENDED])
487
+ protocol.emit('end');
488
+ if (!protocol[SYM_CLOSED])
489
+ protocol.emit('close');
490
+ });
491
+ }
492
+
493
+ function onClose() {
494
+ this[SYM_CLOSED] = true;
495
+ }
496
+
497
+ function onEnd() {
498
+ this[SYM_ENDED] = true;
499
+ }
500
+
501
+ const SYM_REQS = Symbol('Requests');
502
+ const SYM_MODE = Symbol('Agent Protocol Role');
503
+ const SYM_BUFFER = Symbol('Agent Protocol Buffer');
504
+ const SYM_MSGLEN = Symbol('Agent Protocol Current Message Length');
505
+ const SYM_CLOSED = Symbol('Agent Protocol Closed');
506
+ const SYM_ENDED = Symbol('Agent Protocol Ended');
507
+ // Implementation based on:
508
+ // https://tools.ietf.org/html/draft-miller-ssh-agent-04
509
+ return class AgentProtocol extends Duplex {
510
+ /*
511
+ Notes:
512
+ - `constraint` type consists of:
513
+ byte constraint_type
514
+ byte[] constraint_data
515
+ where `constraint_type` is one of:
516
+ * SSH_AGENT_CONSTRAIN_LIFETIME
517
+ - `constraint_data` consists of:
518
+ uint32 seconds
519
+ * SSH_AGENT_CONSTRAIN_CONFIRM
520
+ - `constraint_data` N/A
521
+ * SSH_AGENT_CONSTRAIN_EXTENSION
522
+ - `constraint_data` consists of:
523
+ string extension name
524
+ byte[] extension-specific details
525
+ */
526
+
527
+ constructor(isClient) {
528
+ super({ autoDestroy: true, emitClose: false });
529
+ this[SYM_MODE] = (isClient ? ROLE_CLIENT : ROLE_SERVER);
530
+ this[SYM_REQS] = [];
531
+ this[SYM_BUFFER] = null;
532
+ this[SYM_MSGLEN] = -1;
533
+ this.once('end', onEnd);
534
+ this.once('close', onClose);
535
+ }
536
+
537
+ _read(n) {}
538
+
539
+ _write(data, encoding, cb) {
540
+ /*
541
+ Messages are of the format:
542
+ uint32 message length
543
+ byte message type
544
+ byte[message length - 1] message contents
545
+ */
546
+ if (this[SYM_BUFFER] === null)
547
+ this[SYM_BUFFER] = data;
548
+ else
549
+ this[SYM_BUFFER] = concat(this[SYM_BUFFER], data);
550
+
551
+ let buffer = this[SYM_BUFFER];
552
+ let bufferLen = buffer.length;
553
+
554
+ let p = 0;
555
+ while (p < bufferLen) {
556
+ // Wait for length + type
557
+ if (bufferLen < 5)
558
+ break;
559
+
560
+ if (this[SYM_MSGLEN] === -1)
561
+ this[SYM_MSGLEN] = readUInt32BE(buffer, p);
562
+
563
+ // Check if we have the entire message
564
+ if (bufferLen < (4 + this[SYM_MSGLEN]))
565
+ break;
566
+
567
+ const msgType = buffer[p += 4];
568
+ ++p;
569
+
570
+ if (this[SYM_MODE] === ROLE_CLIENT) {
571
+ if (this[SYM_REQS].length === 0)
572
+ return cb(new Error('Received unexpected message from server'));
573
+
574
+ const req = this[SYM_REQS].shift();
575
+
576
+ switch (msgType) {
577
+ case SSH_AGENT_FAILURE:
578
+ req.cb(new Error('Agent responded with failure'));
579
+ break;
580
+ case SSH_AGENT_IDENTITIES_ANSWER: {
581
+ if (req.type !== SSH_AGENTC_REQUEST_IDENTITIES)
582
+ return cb(new Error('Agent responded with wrong message type'));
583
+
584
+ /*
585
+ byte SSH_AGENT_IDENTITIES_ANSWER
586
+ uint32 nkeys
587
+
588
+ where `nkeys` is 0 or more of:
589
+ string key blob
590
+ string comment
591
+ */
592
+
593
+ binaryParser.init(buffer, p);
594
+
595
+ const numKeys = binaryParser.readUInt32BE();
596
+
597
+ if (numKeys === undefined) {
598
+ binaryParser.clear();
599
+ return cb(new Error('Malformed agent response'));
260
600
  }
261
- } else if (state === 'creds') {
262
- // if this is the first attempt, make sure to gather the valid
263
- // uid and gid for our next attempt
264
- if (!isRetrying)
265
- inbuf.push(data);
266
-
267
- if (bc === 12) {
268
- sock.removeListener('connect', _onconnect);
269
- sock.removeListener('data', _ondata);
270
- sock.removeListener('close', _onclose);
271
- if (isRetrying) {
272
- addSockListeners();
273
- sock.emit('connect');
274
- } else {
275
- isRetrying = true;
276
- credsbuf = Buffer.concat(inbuf);
277
- writeUInt32LE(credsbuf, process.pid, 0);
278
- sock.destroy();
279
- tryConnect();
601
+
602
+ const keys = [];
603
+ for (let i = 0; i < numKeys; ++i) {
604
+ let pubKey = binaryParser.readString();
605
+ if (pubKey === undefined) {
606
+ binaryParser.clear();
607
+ return cb(new Error('Malformed agent response'));
280
608
  }
609
+
610
+ const comment = binaryParser.readString(true);
611
+ if (comment === undefined) {
612
+ binaryParser.clear();
613
+ return cb(new Error('Malformed agent response'));
614
+ }
615
+
616
+ pubKey = parseKey(pubKey);
617
+ // We continue parsing the packet if we encounter an error
618
+ // in case the error is due to the key being an unsupported
619
+ // type
620
+ if (pubKey instanceof Error)
621
+ continue;
622
+
623
+ pubKey.comment = pubKey.comment || comment;
624
+
625
+ keys.push(pubKey);
281
626
  }
627
+ p = binaryParser.pos();
628
+ binaryParser.clear();
629
+
630
+ req.cb(null, keys);
631
+ break;
282
632
  }
633
+ case SSH_AGENT_SIGN_RESPONSE: {
634
+ if (req.type !== SSH_AGENTC_SIGN_REQUEST)
635
+ return cb(new Error('Agent responded with wrong message type'));
636
+
637
+ /*
638
+ byte SSH_AGENT_SIGN_RESPONSE
639
+ string signature
640
+ */
641
+
642
+ binaryParser.init(buffer, p);
643
+ let signature = binaryParser.readString();
644
+ p = binaryParser.pos();
645
+ binaryParser.clear();
646
+
647
+ if (signature === undefined)
648
+ return cb(new Error('Malformed agent response'));
649
+
650
+ // We strip the algorithm from OpenSSH's output and assume it's
651
+ // using the algorithm we specified. This makes it easier on
652
+ // custom Agent implementations so they don't have to construct
653
+ // the correct binary format for a (OpenSSH-style) signature.
654
+
655
+ // TODO: verify signature type based on key and options used
656
+ // during initial sign request
657
+ binaryParser.init(signature, 0);
658
+ binaryParser.readString(true);
659
+ signature = binaryParser.readString();
660
+ binaryParser.clear();
661
+
662
+ if (signature === undefined)
663
+ return cb(new Error('Malformed OpenSSH signature format'));
664
+
665
+ req.cb(null, signature);
666
+ break;
667
+ }
668
+ default:
669
+ return cb(
670
+ new Error('Agent responded with unsupported message type')
671
+ );
283
672
  }
284
- function _onclose() {
285
- cb(new Error('Problem negotiating cygwin unix socket security'));
286
- }
287
- function tryConnect() {
288
- sock = new Socket();
289
- sock.once('connect', _onconnect);
290
- sock.on('data', _ondata);
291
- sock.once('close', _onclose);
292
- sock.connect(port);
673
+ } else {
674
+ switch (msgType) {
675
+ case SSH_AGENTC_REQUEST_IDENTITIES: {
676
+ const req = new AgentInboundRequest(msgType);
677
+ this[SYM_REQS].push(req);
678
+ /*
679
+ byte SSH_AGENTC_REQUEST_IDENTITIES
680
+ */
681
+ this.emit('identities', req);
682
+ break;
683
+ }
684
+ case SSH_AGENTC_SIGN_REQUEST: {
685
+ /*
686
+ byte SSH_AGENTC_SIGN_REQUEST
687
+ string key_blob
688
+ string data
689
+ uint32 flags
690
+ */
691
+ binaryParser.init(buffer, p);
692
+ let pubKey = binaryParser.readString();
693
+ const data = binaryParser.readString();
694
+ const flagsVal = binaryParser.readUInt32BE();
695
+ p = binaryParser.pos();
696
+ binaryParser.clear();
697
+ if (flagsVal === undefined) {
698
+ const req = new AgentInboundRequest(msgType);
699
+ this[SYM_REQS].push(req);
700
+ return this.failureReply(req);
701
+ }
702
+
703
+ pubKey = parseKey(pubKey);
704
+ if (pubKey instanceof Error) {
705
+ const req = new AgentInboundRequest(msgType);
706
+ this[SYM_REQS].push(req);
707
+ return this.failureReply(req);
708
+ }
709
+
710
+ const flags = {
711
+ hash: undefined,
712
+ };
713
+ let ctx;
714
+ if (pubKey.type === 'ssh-rsa') {
715
+ if (flagsVal & SSH_AGENT_RSA_SHA2_256) {
716
+ ctx = 'rsa-sha2-256';
717
+ flags.hash = 'sha256';
718
+ } else if (flagsVal & SSH_AGENT_RSA_SHA2_512) {
719
+ ctx = 'rsa-sha2-512';
720
+ flags.hash = 'sha512';
721
+ }
722
+ }
723
+ if (ctx === undefined)
724
+ ctx = pubKey.type;
725
+
726
+ const req = new AgentInboundRequest(msgType, ctx);
727
+ this[SYM_REQS].push(req);
728
+
729
+ this.emit('sign', req, pubKey, data, flags);
730
+ break;
731
+ }
732
+ default: {
733
+ const req = new AgentInboundRequest(msgType);
734
+ this[SYM_REQS].push(req);
735
+ this.failureReply(req);
736
+ }
293
737
  }
294
- tryConnect();
295
- } else
296
- cb(new Error('Malformed cygwin unix socket file'));
297
- });
298
- return;
738
+ }
739
+
740
+ // Get ready for next message
741
+ this[SYM_MSGLEN] = -1;
742
+ if (p === bufferLen) {
743
+ // Nothing left to process for now
744
+ this[SYM_BUFFER] = null;
745
+ break;
746
+ } else {
747
+ this[SYM_BUFFER] = buffer = buffer.slice(p);
748
+ bufferLen = buffer.length;
749
+ p = 0;
750
+ }
751
+ }
752
+
753
+ cb();
299
754
  }
300
- } else
301
- sock = new Socket();
302
-
303
- function addSockListeners() {
304
- if (!accept && !reject) {
305
- sock.once('connect', onconnect);
306
- sock.on('data', ondata);
307
- sock.once('error', onerror);
308
- sock.once('close', onclose);
309
- } else {
310
- var chan;
311
- sock.once('connect', function() {
312
- chan = accept();
313
- var isDone = false;
314
- function onDone() {
315
- if (isDone)
316
- return;
317
- sock.destroy();
318
- isDone = true;
755
+
756
+ _destroy(err, cb) {
757
+ cleanup(this);
758
+ cb();
759
+ }
760
+
761
+ _final(cb) {
762
+ cleanup(this);
763
+ cb();
764
+ }
765
+
766
+ // Client->Server messages =================================================
767
+ sign(pubKey, data, options, cb) {
768
+ if (this[SYM_MODE] !== ROLE_CLIENT)
769
+ throw new Error('Client-only method called with server role');
770
+
771
+ if (typeof options === 'function') {
772
+ cb = options;
773
+ options = undefined;
774
+ } else if (typeof options !== 'object' || options === null) {
775
+ options = undefined;
776
+ }
777
+
778
+ let flags = 0;
779
+
780
+ pubKey = parseKey(pubKey);
781
+ if (pubKey instanceof Error)
782
+ throw new Error('Invalid public key argument');
783
+
784
+ if (pubKey.type === 'ssh-rsa' && options) {
785
+ switch (options.hash) {
786
+ case 'sha256':
787
+ flags = SSH_AGENT_RSA_SHA2_256;
788
+ break;
789
+ case 'sha512':
790
+ flags = SSH_AGENT_RSA_SHA2_512;
791
+ break;
319
792
  }
320
- chan.once('end', onDone)
321
- .once('close', onDone)
322
- .on('data', function(data) {
323
- sock.write(data);
324
- });
325
- sock.on('data', function(data) {
326
- chan.write(data);
327
- });
328
- });
329
- sock.once('close', function() {
330
- if (!chan)
331
- reject();
332
- });
793
+ }
794
+ pubKey = pubKey.getPublicSSH();
795
+
796
+ /*
797
+ byte SSH_AGENTC_SIGN_REQUEST
798
+ string key_blob
799
+ string data
800
+ uint32 flags
801
+ */
802
+ const type = SSH_AGENTC_SIGN_REQUEST;
803
+ const keyLen = pubKey.length;
804
+ const dataLen = data.length;
805
+ let p = 0;
806
+ const buf = Buffer.allocUnsafe(4 + 1 + 4 + keyLen + 4 + dataLen + 4);
807
+
808
+ writeUInt32BE(buf, buf.length - 4, p);
809
+
810
+ buf[p += 4] = type;
811
+
812
+ writeUInt32BE(buf, keyLen, ++p);
813
+ pubKey.copy(buf, p += 4);
814
+
815
+ writeUInt32BE(buf, dataLen, p += keyLen);
816
+ data.copy(buf, p += 4);
817
+
818
+ writeUInt32BE(buf, flags, p += dataLen);
819
+
820
+ if (typeof cb !== 'function')
821
+ cb = noop;
822
+
823
+ this[SYM_REQS].push({ type, cb });
824
+
825
+ return this.push(buf);
333
826
  }
334
- }
335
- addSockListeners();
336
- sock.connect(sockPath);
337
- };
827
+ getIdentities(cb) {
828
+ if (this[SYM_MODE] !== ROLE_CLIENT)
829
+ throw new Error('Client-only method called with server role');
338
830
 
831
+ /*
832
+ byte SSH_AGENTC_REQUEST_IDENTITIES
833
+ */
834
+ const type = SSH_AGENTC_REQUEST_IDENTITIES;
339
835
 
340
- // win32 only ------------------------------------------------------------------
341
- if (process.platform === 'win32') {
342
- var RET_ERR_BADARGS = 10;
343
- var RET_ERR_UNAVAILABLE = 11;
344
- var RET_ERR_NOMAP = 12;
345
- var RET_ERR_BINSTDIN = 13;
346
- var RET_ERR_BINSTDOUT = 14;
347
- var RET_ERR_BADLEN = 15;
348
-
349
- var ERROR = {};
350
- var EXEPATH = path.resolve(__dirname, '..', 'util/pagent.exe');
351
- ERROR[RET_ERR_BADARGS] = new Error('Invalid pagent.exe arguments');
352
- ERROR[RET_ERR_UNAVAILABLE] = new Error('Pageant is not running');
353
- ERROR[RET_ERR_NOMAP] = new Error('pagent.exe could not create an mmap');
354
- ERROR[RET_ERR_BINSTDIN] = new Error('pagent.exe could not set mode for stdin');
355
- ERROR[RET_ERR_BINSTDOUT] = new Error('pagent.exe could not set mode for stdout');
356
- ERROR[RET_ERR_BADLEN] = new Error('pagent.exe did not get expected input payload');
357
-
358
- function PageantSock() {
359
- this.proc = undefined;
360
- this.buffer = null;
361
- }
362
- inherits(PageantSock, EventEmitter);
363
-
364
- PageantSock.prototype.write = function(buf) {
365
- if (this.buffer === null)
366
- this.buffer = buf;
367
- else {
368
- this.buffer = Buffer.concat([this.buffer, buf],
369
- this.buffer.length + buf.length);
836
+ let p = 0;
837
+ const buf = Buffer.allocUnsafe(4 + 1);
838
+
839
+ writeUInt32BE(buf, buf.length - 4, p);
840
+
841
+ buf[p += 4] = type;
842
+
843
+ if (typeof cb !== 'function')
844
+ cb = noop;
845
+
846
+ this[SYM_REQS].push({ type, cb });
847
+
848
+ return this.push(buf);
370
849
  }
371
- // Wait for at least all length bytes
372
- if (this.buffer.length < 4)
373
- return;
374
850
 
375
- var len = readUInt32BE(this.buffer, 0);
376
- // Make sure we have a full message before querying pageant
377
- if ((this.buffer.length - 4) < len)
378
- return;
851
+ // Server->Client messages =================================================
852
+ failureReply(req) {
853
+ if (this[SYM_MODE] !== ROLE_SERVER)
854
+ throw new Error('Server-only method called with client role');
379
855
 
380
- buf = this.buffer.slice(0, 4 + len);
381
- if (this.buffer.length > (4 + len))
382
- this.buffer = this.buffer.slice(4 + len);
383
- else
384
- this.buffer = null;
856
+ if (!(req instanceof AgentInboundRequest))
857
+ throw new Error('Wrong request argument');
385
858
 
386
- var self = this;
387
- var proc;
388
- var hadError = false;
389
- proc = this.proc = cp.spawn(EXEPATH, [ buf.length ]);
390
- proc.stdout.on('data', function(data) {
391
- self.emit('data', data);
392
- });
393
- proc.once('error', function(err) {
394
- if (!hadError) {
395
- hadError = true;
396
- self.emit('error', err);
859
+ if (req.hasResponded())
860
+ return true;
861
+
862
+ let p = 0;
863
+ const buf = Buffer.allocUnsafe(4 + 1);
864
+
865
+ writeUInt32BE(buf, buf.length - 4, p);
866
+
867
+ buf[p += 4] = SSH_AGENT_FAILURE;
868
+
869
+ return respond(this, req, buf);
870
+ }
871
+ getIdentitiesReply(req, keys) {
872
+ if (this[SYM_MODE] !== ROLE_SERVER)
873
+ throw new Error('Server-only method called with client role');
874
+
875
+ if (!(req instanceof AgentInboundRequest))
876
+ throw new Error('Wrong request argument');
877
+
878
+ if (req.hasResponded())
879
+ return true;
880
+
881
+ /*
882
+ byte SSH_AGENT_IDENTITIES_ANSWER
883
+ uint32 nkeys
884
+
885
+ where `nkeys` is 0 or more of:
886
+ string key blob
887
+ string comment
888
+ */
889
+
890
+ if (req.getType() !== SSH_AGENTC_REQUEST_IDENTITIES)
891
+ throw new Error('Invalid response to request');
892
+
893
+ if (!Array.isArray(keys))
894
+ throw new Error('Keys argument must be an array');
895
+
896
+ let totalKeysLen = 4; // Include `nkeys` size
897
+
898
+ const newKeys = [];
899
+ for (let i = 0; i < keys.length; ++i) {
900
+ const entry = keys[i];
901
+ if (typeof entry !== 'object' || entry === null)
902
+ throw new Error(`Invalid key entry: ${entry}`);
903
+
904
+ let pubKey;
905
+ let comment;
906
+ if (isParsedKey(entry)) {
907
+ pubKey = entry;
908
+ } else if (isParsedKey(entry.pubKey)) {
909
+ pubKey = entry.pubKey;
910
+ } else {
911
+ if (typeof entry.pubKey !== 'object' || entry.pubKey === null)
912
+ continue;
913
+ ({ pubKey, comment } = entry.pubKey);
914
+ pubKey = parseKey(pubKey);
915
+ if (pubKey instanceof Error)
916
+ continue; // TODO: add debug output
917
+ }
918
+ comment = pubKey.comment || comment;
919
+ pubKey = pubKey.getPublicSSH();
920
+
921
+ totalKeysLen += 4 + pubKey.length;
922
+
923
+ if (comment && typeof comment === 'string')
924
+ comment = Buffer.from(comment);
925
+ else if (!Buffer.isBuffer(comment))
926
+ comment = EMPTY_BUF;
927
+
928
+ totalKeysLen += 4 + comment.length;
929
+
930
+ newKeys.push({ pubKey, comment });
397
931
  }
398
- });
399
- proc.once('close', function(code) {
400
- self.proc = undefined;
401
- if (ERROR[code] && !hadError) {
402
- hadError = true;
403
- self.emit('error', ERROR[code]);
932
+
933
+ let p = 0;
934
+ const buf = Buffer.allocUnsafe(4 + 1 + totalKeysLen);
935
+
936
+ writeUInt32BE(buf, buf.length - 4, p);
937
+
938
+ buf[p += 4] = SSH_AGENT_IDENTITIES_ANSWER;
939
+
940
+ writeUInt32BE(buf, newKeys.length, ++p);
941
+ p += 4;
942
+ for (let i = 0; i < newKeys.length; ++i) {
943
+ const { pubKey, comment } = newKeys[i];
944
+
945
+ writeUInt32BE(buf, pubKey.length, p);
946
+ pubKey.copy(buf, p += 4);
947
+
948
+ writeUInt32BE(buf, comment.length, p += pubKey.length);
949
+ p += 4;
950
+ if (comment.length) {
951
+ comment.copy(buf, p);
952
+ p += comment.length;
953
+ }
404
954
  }
405
- self.emit('close', hadError);
406
- });
407
- proc.stdin.end(buf);
408
- };
409
- PageantSock.prototype.end = PageantSock.prototype.destroy = function() {
410
- this.buffer = null;
411
- if (this.proc) {
412
- this.proc.kill();
413
- this.proc = undefined;
955
+
956
+ return respond(this, req, buf);
957
+ }
958
+ signReply(req, signature) {
959
+ if (this[SYM_MODE] !== ROLE_SERVER)
960
+ throw new Error('Server-only method called with client role');
961
+
962
+ if (!(req instanceof AgentInboundRequest))
963
+ throw new Error('Wrong request argument');
964
+
965
+ if (req.hasResponded())
966
+ return true;
967
+
968
+ /*
969
+ byte SSH_AGENT_SIGN_RESPONSE
970
+ string signature
971
+ */
972
+
973
+ if (req.getType() !== SSH_AGENTC_SIGN_REQUEST)
974
+ throw new Error('Invalid response to request');
975
+
976
+ if (!Buffer.isBuffer(signature))
977
+ throw new Error('Signature argument must be a Buffer');
978
+
979
+ if (signature.length === 0)
980
+ throw new Error('Signature argument must be non-empty');
981
+
982
+ /*
983
+ OpenSSH agent signatures are encoded as:
984
+
985
+ string signature format identifier (as specified by the
986
+ public key/certificate format)
987
+ byte[n] signature blob in format specific encoding.
988
+ - This is actually a `string` for: rsa, dss, ecdsa, and ed25519
989
+ types
990
+ */
991
+
992
+ let p = 0;
993
+ const sigFormat = req.getContext();
994
+ const sigFormatLen = Buffer.byteLength(sigFormat);
995
+ const buf = Buffer.allocUnsafe(
996
+ 4 + 1 + 4 + 4 + sigFormatLen + 4 + signature.length
997
+ );
998
+
999
+ writeUInt32BE(buf, buf.length - 4, p);
1000
+
1001
+ buf[p += 4] = SSH_AGENT_SIGN_RESPONSE;
1002
+
1003
+ writeUInt32BE(buf, 4 + sigFormatLen + 4 + signature.length, ++p);
1004
+ writeUInt32BE(buf, sigFormatLen, p += 4);
1005
+ buf.utf8Write(sigFormat, p += 4, sigFormatLen);
1006
+ writeUInt32BE(buf, signature.length, p += sigFormatLen);
1007
+ signature.copy(buf, p += 4);
1008
+
1009
+ return respond(this, req, buf);
414
1010
  }
415
1011
  };
416
- PageantSock.prototype.connect = function() {
417
- this.emit('connect');
418
- };
1012
+ })();
1013
+
1014
+ const SYM_AGENT = Symbol('Agent');
1015
+ const SYM_AGENT_KEYS = Symbol('Agent Keys');
1016
+ const SYM_AGENT_KEYS_IDX = Symbol('Agent Keys Index');
1017
+ const SYM_AGENT_CBS = Symbol('Agent Init Callbacks');
1018
+ class AgentContext {
1019
+ constructor(agent) {
1020
+ if (typeof agent === 'string')
1021
+ agent = createAgent(agent);
1022
+ else if (!isAgent(agent))
1023
+ throw new Error('Invalid agent argument');
1024
+ this[SYM_AGENT] = agent;
1025
+ this[SYM_AGENT_KEYS] = null;
1026
+ this[SYM_AGENT_KEYS_IDX] = -1;
1027
+ this[SYM_AGENT_CBS] = null;
1028
+ }
1029
+ init(cb) {
1030
+ if (typeof cb !== 'function')
1031
+ cb = noop;
1032
+
1033
+ if (this[SYM_AGENT_KEYS] === null) {
1034
+ if (this[SYM_AGENT_CBS] === null) {
1035
+ this[SYM_AGENT_CBS] = [cb];
1036
+
1037
+ const doCbs = (...args) => {
1038
+ process.nextTick(() => {
1039
+ const cbs = this[SYM_AGENT_CBS];
1040
+ this[SYM_AGENT_CBS] = null;
1041
+ for (const cb of cbs)
1042
+ cb(...args);
1043
+ });
1044
+ };
1045
+
1046
+ this[SYM_AGENT].getIdentities(once((err, keys) => {
1047
+ if (err)
1048
+ return doCbs(err);
1049
+
1050
+ if (!Array.isArray(keys)) {
1051
+ return doCbs(new Error(
1052
+ 'Agent implementation failed to provide keys'
1053
+ ));
1054
+ }
1055
+
1056
+ const newKeys = [];
1057
+ for (let key of keys) {
1058
+ key = parseKey(key);
1059
+ if (key instanceof Error) {
1060
+ // TODO: add debug output
1061
+ continue;
1062
+ }
1063
+ newKeys.push(key);
1064
+ }
1065
+
1066
+ this[SYM_AGENT_KEYS] = newKeys;
1067
+ this[SYM_AGENT_KEYS_IDX] = -1;
1068
+ doCbs();
1069
+ }));
1070
+ } else {
1071
+ this[SYM_AGENT_CBS].push(cb);
1072
+ }
1073
+ } else {
1074
+ process.nextTick(cb);
1075
+ }
1076
+ }
1077
+ nextKey() {
1078
+ if (this[SYM_AGENT_KEYS] === null
1079
+ || ++this[SYM_AGENT_KEYS_IDX] >= this[SYM_AGENT_KEYS].length) {
1080
+ return false;
1081
+ }
1082
+
1083
+ return this[SYM_AGENT_KEYS][this[SYM_AGENT_KEYS_IDX]];
1084
+ }
1085
+ currentKey() {
1086
+ if (this[SYM_AGENT_KEYS] === null
1087
+ || this[SYM_AGENT_KEYS_IDX] >= this[SYM_AGENT_KEYS].length) {
1088
+ return null;
1089
+ }
1090
+
1091
+ return this[SYM_AGENT_KEYS][this[SYM_AGENT_KEYS_IDX]];
1092
+ }
1093
+ pos() {
1094
+ if (this[SYM_AGENT_KEYS] === null
1095
+ || this[SYM_AGENT_KEYS_IDX] >= this[SYM_AGENT_KEYS].length) {
1096
+ return -1;
1097
+ }
1098
+
1099
+ return this[SYM_AGENT_KEYS_IDX];
1100
+ }
1101
+ reset() {
1102
+ this[SYM_AGENT_KEYS_IDX] = -1;
1103
+ }
1104
+
1105
+ sign(...args) {
1106
+ this[SYM_AGENT].sign(...args);
1107
+ }
1108
+ }
1109
+
1110
+ function isAgent(val) {
1111
+ return (val instanceof BaseAgent);
419
1112
  }
1113
+
1114
+ module.exports = {
1115
+ AgentContext,
1116
+ AgentProtocol,
1117
+ BaseAgent,
1118
+ createAgent,
1119
+ CygwinAgent,
1120
+ isAgent,
1121
+ OpenSSHAgent,
1122
+ PageantAgent,
1123
+ };