@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
package/lib/agent.js
CHANGED
|
@@ -1,419 +1,1123 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
cb
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
281
|
+
|
|
282
|
+
// Try using `cygpath` to convert a possible *nix-style path to the
|
|
205
283
|
// real Windows path before giving up ...
|
|
206
|
-
|
|
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
|
-
|
|
212
|
-
|
|
289
|
+
socketPath = stdout.toString().replace(/[\r\n]/g, '');
|
|
290
|
+
readFile(socketPath, readCygsocket);
|
|
213
291
|
});
|
|
214
292
|
return;
|
|
215
293
|
}
|
|
216
294
|
|
|
217
|
-
|
|
218
|
-
if (m
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
+
};
|