UNPKG

32.1 kBJavaScriptView Raw
1'use strict';
2
3const { Socket } = require('net');
4const { Duplex } = require('stream');
5const { resolve } = require('path');
6const { readFile } = require('fs');
7const { execFile, spawn } = require('child_process');
8
9const { isParsedKey, parseKey } = require('./protocol/keyParser.js');
10
11const {
12 makeBufferParser,
13 readUInt32BE,
14 writeUInt32BE,
15 writeUInt32LE,
16} = require('./protocol/utils.js');
17
18function once(cb) {
19 let called = false;
20 return (...args) => {
21 if (called)
22 return;
23 called = true;
24 cb(...args);
25 };
26}
27
28function 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
35function noop() {}
36
37const EMPTY_BUF = Buffer.alloc(0);
38
39const binaryParser = makeBufferParser();
40
41class 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'));
49 }
50}
51
52class 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'));
75 }
76 }
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 {}
86 }
87 if (!err)
88 err = new Error('Failed to retrieve identities from agent');
89 cb(err);
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;
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 });
157 }
158}
159
160const 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 }
185 }
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 }
244 }
245
246 return class PageantAgent extends OpenSSHAgent {
247 getStream(cb) {
248 cb(null, new PageantSocket());
249 }
250 };
251})();
252
253const 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) {
278 if (err) {
279 if (triedCygpath)
280 return cb(new Error('Invalid cygwin unix socket path'));
281
282 // Try using `cygpath` to convert a possible *nix-style path to the
283 // real Windows path before giving up ...
284 execFile('cygpath', ['-w', socketPath], (err, stdout, stderr) => {
285 if (err || stdout.length === 0)
286 return cb(new Error('Invalid cygwin unix socket path'));
287
288 triedCygpath = true;
289 socketPath = stdout.toString().replace(/[\r\n]/g, '');
290 readFile(socketPath, readCygsocket);
291 });
292 return;
293 }
294
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 }
367 }
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
390const WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/;
391function 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
400const 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'));
600 }
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'));
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);
626 }
627 p = binaryParser.pos();
628 binaryParser.clear();
629
630 req.cb(null, keys);
631 break;
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 );
672 }
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 }
737 }
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();
754 }
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;
792 }
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);
826 }
827 getIdentities(cb) {
828 if (this[SYM_MODE] !== ROLE_CLIENT)
829 throw new Error('Client-only method called with server role');
830
831 /*
832 byte SSH_AGENTC_REQUEST_IDENTITIES
833 */
834 const type = SSH_AGENTC_REQUEST_IDENTITIES;
835
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);
849 }
850
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');
855
856 if (!(req instanceof AgentInboundRequest))
857 throw new Error('Wrong request argument');
858
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 });
931 }
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 }
954 }
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);
1010 }
1011 };
1012})();
1013
1014const SYM_AGENT = Symbol('Agent');
1015const SYM_AGENT_KEYS = Symbol('Agent Keys');
1016const SYM_AGENT_KEYS_IDX = Symbol('Agent Keys Index');
1017const SYM_AGENT_CBS = Symbol('Agent Init Callbacks');
1018class 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
1110function isAgent(val) {
1111 return (val instanceof BaseAgent);
1112}
1113
1114module.exports = {
1115 AgentContext,
1116 AgentProtocol,
1117 BaseAgent,
1118 createAgent,
1119 CygwinAgent,
1120 isAgent,
1121 OpenSSHAgent,
1122 PageantAgent,
1123};