1124 lines
31 KiB
JavaScript
1124 lines
31 KiB
JavaScript
'use strict';
|
|
|
|
const { Socket } = require('net');
|
|
const { Duplex } = require('stream');
|
|
const { resolve } = require('path');
|
|
const { readFile } = require('fs');
|
|
const { execFile, spawn } = require('child_process');
|
|
|
|
const { isParsedKey, parseKey } = require('./protocol/keyParser.js');
|
|
|
|
const {
|
|
makeBufferParser,
|
|
readUInt32BE,
|
|
writeUInt32BE,
|
|
writeUInt32LE,
|
|
} = require('./protocol/utils.js');
|
|
|
|
function once(cb) {
|
|
let called = false;
|
|
return (...args) => {
|
|
if (called)
|
|
return;
|
|
called = true;
|
|
cb(...args);
|
|
};
|
|
}
|
|
|
|
function concat(buf1, buf2) {
|
|
const combined = Buffer.allocUnsafe(buf1.length + buf2.length);
|
|
buf1.copy(combined, 0);
|
|
buf2.copy(combined, buf1.length);
|
|
return combined;
|
|
}
|
|
|
|
function noop() {}
|
|
|
|
const EMPTY_BUF = Buffer.alloc(0);
|
|
|
|
const binaryParser = makeBufferParser();
|
|
|
|
class BaseAgent {
|
|
getIdentities(cb) {
|
|
cb(new Error('Missing getIdentities() implementation'));
|
|
}
|
|
sign(pubKey, data, options, cb) {
|
|
if (typeof options === 'function')
|
|
cb = options;
|
|
cb(new Error('Missing sign() implementation'));
|
|
}
|
|
}
|
|
|
|
class OpenSSHAgent extends BaseAgent {
|
|
constructor(socketPath) {
|
|
super();
|
|
this.socketPath = socketPath;
|
|
}
|
|
|
|
getStream(cb) {
|
|
cb = once(cb);
|
|
const sock = new Socket();
|
|
sock.on('connect', () => {
|
|
cb(null, sock);
|
|
});
|
|
sock.on('close', onFail)
|
|
.on('end', onFail)
|
|
.on('error', onFail);
|
|
sock.connect(this.socketPath);
|
|
|
|
function onFail() {
|
|
try {
|
|
sock.destroy();
|
|
} catch {}
|
|
|
|
cb(new Error('Failed to connect to agent'));
|
|
}
|
|
}
|
|
|
|
getIdentities(cb) {
|
|
cb = once(cb);
|
|
this.getStream((err, stream) => {
|
|
function onFail(err) {
|
|
if (stream) {
|
|
try {
|
|
stream.destroy();
|
|
} catch {}
|
|
}
|
|
if (!err)
|
|
err = new Error('Failed to retrieve identities from agent');
|
|
cb(err);
|
|
}
|
|
|
|
if (err)
|
|
return onFail(err);
|
|
|
|
const protocol = new AgentProtocol(true);
|
|
protocol.on('error', onFail);
|
|
protocol.pipe(stream).pipe(protocol);
|
|
|
|
stream.on('close', onFail)
|
|
.on('end', onFail)
|
|
.on('error', onFail);
|
|
|
|
protocol.getIdentities((err, keys) => {
|
|
if (err)
|
|
return onFail(err);
|
|
try {
|
|
stream.destroy();
|
|
} catch {}
|
|
cb(null, keys);
|
|
});
|
|
});
|
|
}
|
|
|
|
sign(pubKey, data, options, cb) {
|
|
if (typeof options === 'function') {
|
|
cb = options;
|
|
options = undefined;
|
|
} else if (typeof options !== 'object' || options === null) {
|
|
options = undefined;
|
|
}
|
|
|
|
cb = once(cb);
|
|
this.getStream((err, stream) => {
|
|
function onFail(err) {
|
|
if (stream) {
|
|
try {
|
|
stream.destroy();
|
|
} catch {}
|
|
}
|
|
if (!err)
|
|
err = new Error('Failed to sign data with agent');
|
|
cb(err);
|
|
}
|
|
|
|
if (err)
|
|
return onFail(err);
|
|
|
|
const protocol = new AgentProtocol(true);
|
|
protocol.on('error', onFail);
|
|
protocol.pipe(stream).pipe(protocol);
|
|
|
|
stream.on('close', onFail)
|
|
.on('end', onFail)
|
|
.on('error', onFail);
|
|
|
|
protocol.sign(pubKey, data, options, (err, sig) => {
|
|
if (err)
|
|
return onFail(err);
|
|
|
|
try {
|
|
stream.destroy();
|
|
} catch {}
|
|
|
|
cb(null, sig);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
const PageantAgent = (() => {
|
|
const RET_ERR_BADARGS = 10;
|
|
const RET_ERR_UNAVAILABLE = 11;
|
|
const RET_ERR_NOMAP = 12;
|
|
const RET_ERR_BINSTDIN = 13;
|
|
const RET_ERR_BINSTDOUT = 14;
|
|
const RET_ERR_BADLEN = 15;
|
|
|
|
const EXEPATH = resolve(__dirname, '..', 'util/pagent.exe');
|
|
const ERROR = {
|
|
[RET_ERR_BADARGS]: new Error('Invalid pagent.exe arguments'),
|
|
[RET_ERR_UNAVAILABLE]: new Error('Pageant is not running'),
|
|
[RET_ERR_NOMAP]: new Error('pagent.exe could not create an mmap'),
|
|
[RET_ERR_BINSTDIN]: new Error('pagent.exe could not set mode for stdin'),
|
|
[RET_ERR_BINSTDOUT]: new Error('pagent.exe could not set mode for stdout'),
|
|
[RET_ERR_BADLEN]:
|
|
new Error('pagent.exe did not get expected input payload'),
|
|
};
|
|
|
|
function destroy(stream) {
|
|
stream.buffer = null;
|
|
if (stream.proc) {
|
|
stream.proc.kill();
|
|
stream.proc = undefined;
|
|
}
|
|
}
|
|
|
|
class PageantSocket extends Duplex {
|
|
constructor() {
|
|
super();
|
|
this.proc = undefined;
|
|
this.buffer = null;
|
|
}
|
|
_read(n) {}
|
|
_write(data, encoding, cb) {
|
|
if (this.buffer === null) {
|
|
this.buffer = data;
|
|
} else {
|
|
const newBuffer = Buffer.allocUnsafe(this.buffer.length + data.length);
|
|
this.buffer.copy(newBuffer, 0);
|
|
data.copy(newBuffer, this.buffer.length);
|
|
this.buffer = newBuffer;
|
|
}
|
|
// Wait for at least all length bytes
|
|
if (this.buffer.length < 4)
|
|
return cb();
|
|
|
|
const len = readUInt32BE(this.buffer, 0);
|
|
// Make sure we have a full message before querying pageant
|
|
if ((this.buffer.length - 4) < len)
|
|
return cb();
|
|
|
|
data = this.buffer.slice(0, 4 + len);
|
|
if (this.buffer.length > (4 + len))
|
|
return cb(new Error('Unexpected multiple agent requests'));
|
|
this.buffer = null;
|
|
|
|
let error;
|
|
const proc = this.proc = spawn(EXEPATH, [ data.length ]);
|
|
proc.stdout.on('data', (data) => {
|
|
this.push(data);
|
|
});
|
|
proc.on('error', (err) => {
|
|
error = err;
|
|
cb(error);
|
|
});
|
|
proc.on('close', (code) => {
|
|
this.proc = undefined;
|
|
if (!error) {
|
|
if (error = ERROR[code])
|
|
return cb(error);
|
|
cb();
|
|
}
|
|
});
|
|
proc.stdin.end(data);
|
|
}
|
|
_final(cb) {
|
|
destroy(this);
|
|
cb();
|
|
}
|
|
_destroy(err, cb) {
|
|
destroy(this);
|
|
cb();
|
|
}
|
|
}
|
|
|
|
return class PageantAgent extends OpenSSHAgent {
|
|
getStream(cb) {
|
|
cb(null, new PageantSocket());
|
|
}
|
|
};
|
|
})();
|
|
|
|
const CygwinAgent = (() => {
|
|
const RE_CYGWIN_SOCK = /^!<socket >(\d+) s ([A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8}-[A-Z0-9]{8})/;
|
|
|
|
return class CygwinAgent extends OpenSSHAgent {
|
|
getStream(cb) {
|
|
cb = once(cb);
|
|
|
|
// The cygwin ssh-agent connection process looks like this:
|
|
// 1. Read the "socket" as a file to get the underlying TCP port and a
|
|
// special "secret" that must be sent to the TCP server.
|
|
// 2. Connect to the server listening on localhost at the TCP port.
|
|
// 3. Send the "secret" to the server.
|
|
// 4. The server sends back the same "secret".
|
|
// 5. Send three 32-bit integer values of zero. This is ordinarily the
|
|
// pid, uid, and gid of this process, but cygwin will actually
|
|
// send us the correct values as a response.
|
|
// 6. The server sends back the pid, uid, gid.
|
|
// 7. Disconnect.
|
|
// 8. Repeat steps 2-6, except send the received pid, uid, and gid in
|
|
// step 5 instead of zeroes.
|
|
// 9. Connection is ready to be used.
|
|
|
|
let socketPath = this.socketPath;
|
|
let triedCygpath = false;
|
|
readFile(socketPath, function readCygsocket(err, data) {
|
|
if (err) {
|
|
if (triedCygpath)
|
|
return cb(new Error('Invalid cygwin unix socket path'));
|
|
|
|
// Try using `cygpath` to convert a possible *nix-style path to the
|
|
// real Windows path before giving up ...
|
|
execFile('cygpath', ['-w', socketPath], (err, stdout, stderr) => {
|
|
if (err || stdout.length === 0)
|
|
return cb(new Error('Invalid cygwin unix socket path'));
|
|
|
|
triedCygpath = true;
|
|
socketPath = stdout.toString().replace(/[\r\n]/g, '');
|
|
readFile(socketPath, readCygsocket);
|
|
});
|
|
return;
|
|
}
|
|
|
|
const m = RE_CYGWIN_SOCK.exec(data.toString('ascii'));
|
|
if (!m)
|
|
return cb(new Error('Malformed cygwin unix socket file'));
|
|
|
|
let state;
|
|
let bc = 0;
|
|
let isRetrying = false;
|
|
const inBuf = [];
|
|
let sock;
|
|
|
|
// Use 0 for pid, uid, and gid to ensure we get an error and also
|
|
// a valid uid and gid from cygwin so that we don't have to figure it
|
|
// out ourselves
|
|
let credsBuf = Buffer.alloc(12);
|
|
|
|
// Parse cygwin unix socket file contents
|
|
const port = parseInt(m[1], 10);
|
|
const secret = m[2].replace(/-/g, '');
|
|
const secretBuf = Buffer.allocUnsafe(16);
|
|
for (let i = 0, j = 0; j < 32; ++i, j += 2)
|
|
secretBuf[i] = parseInt(secret.substring(j, j + 2), 16);
|
|
|
|
// Convert to host order (always LE for Windows)
|
|
for (let i = 0; i < 16; i += 4)
|
|
writeUInt32LE(secretBuf, readUInt32BE(secretBuf, i), i);
|
|
|
|
tryConnect();
|
|
|
|
function _onconnect() {
|
|
bc = 0;
|
|
state = 'secret';
|
|
sock.write(secretBuf);
|
|
}
|
|
|
|
function _ondata(data) {
|
|
bc += data.length;
|
|
|
|
if (state === 'secret') {
|
|
// The secret we sent is echoed back to us by cygwin, not sure of
|
|
// the reason for that, but we ignore it nonetheless ...
|
|
if (bc === 16) {
|
|
bc = 0;
|
|
state = 'creds';
|
|
sock.write(credsBuf);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (state === 'creds') {
|
|
// If this is the first attempt, make sure to gather the valid
|
|
// uid and gid for our next attempt
|
|
if (!isRetrying)
|
|
inBuf.push(data);
|
|
|
|
if (bc === 12) {
|
|
sock.removeListener('connect', _onconnect);
|
|
sock.removeListener('data', _ondata);
|
|
sock.removeListener('error', onFail);
|
|
sock.removeListener('end', onFail);
|
|
sock.removeListener('close', onFail);
|
|
|
|
if (isRetrying)
|
|
return cb(null, sock);
|
|
|
|
isRetrying = true;
|
|
credsBuf = Buffer.concat(inBuf);
|
|
writeUInt32LE(credsBuf, process.pid, 0);
|
|
sock.on('error', () => {});
|
|
sock.destroy();
|
|
|
|
tryConnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
function onFail() {
|
|
cb(new Error('Problem negotiating cygwin unix socket security'));
|
|
}
|
|
|
|
function tryConnect() {
|
|
sock = new Socket();
|
|
sock.on('connect', _onconnect);
|
|
sock.on('data', _ondata);
|
|
sock.on('error', onFail);
|
|
sock.on('end', onFail);
|
|
sock.on('close', onFail);
|
|
sock.connect(port);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
})();
|
|
|
|
// Format of `//./pipe/ANYTHING`, with forward slashes and backward slashes
|
|
// being interchangeable
|
|
const WINDOWS_PIPE_REGEX = /^[/\\][/\\]\.[/\\]pipe[/\\].+/;
|
|
function createAgent(path) {
|
|
if (process.platform === 'win32' && !WINDOWS_PIPE_REGEX.test(path)) {
|
|
return (path === 'pageant'
|
|
? new PageantAgent()
|
|
: new CygwinAgent(path));
|
|
}
|
|
return new OpenSSHAgent(path);
|
|
}
|
|
|
|
const AgentProtocol = (() => {
|
|
// Client->Server messages
|
|
const SSH_AGENTC_REQUEST_IDENTITIES = 11;
|
|
const SSH_AGENTC_SIGN_REQUEST = 13;
|
|
// const SSH_AGENTC_ADD_IDENTITY = 17;
|
|
// const SSH_AGENTC_REMOVE_IDENTITY = 18;
|
|
// const SSH_AGENTC_REMOVE_ALL_IDENTITIES = 19;
|
|
// const SSH_AGENTC_ADD_SMARTCARD_KEY = 20;
|
|
// const SSH_AGENTC_REMOVE_SMARTCARD_KEY = 21;
|
|
// const SSH_AGENTC_LOCK = 22;
|
|
// const SSH_AGENTC_UNLOCK = 23;
|
|
// const SSH_AGENTC_ADD_ID_CONSTRAINED = 25;
|
|
// const SSH_AGENTC_ADD_SMARTCARD_KEY_CONSTRAINED = 26;
|
|
// const SSH_AGENTC_EXTENSION = 27;
|
|
// Server->Client messages
|
|
const SSH_AGENT_FAILURE = 5;
|
|
// const SSH_AGENT_SUCCESS = 6;
|
|
const SSH_AGENT_IDENTITIES_ANSWER = 12;
|
|
const SSH_AGENT_SIGN_RESPONSE = 14;
|
|
// const SSH_AGENT_EXTENSION_FAILURE = 28;
|
|
|
|
// const SSH_AGENT_CONSTRAIN_LIFETIME = 1;
|
|
// const SSH_AGENT_CONSTRAIN_CONFIRM = 2;
|
|
// const SSH_AGENT_CONSTRAIN_EXTENSION = 255;
|
|
|
|
const SSH_AGENT_RSA_SHA2_256 = (1 << 1);
|
|
const SSH_AGENT_RSA_SHA2_512 = (1 << 2);
|
|
|
|
const ROLE_CLIENT = 0;
|
|
const ROLE_SERVER = 1;
|
|
|
|
// Ensures that responses get sent back in the same order the requests were
|
|
// received
|
|
function processResponses(protocol) {
|
|
let ret;
|
|
while (protocol[SYM_REQS].length) {
|
|
const nextResponse = protocol[SYM_REQS][0][SYM_RESP];
|
|
if (nextResponse === undefined)
|
|
break;
|
|
|
|
protocol[SYM_REQS].shift();
|
|
ret = protocol.push(nextResponse);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
const SYM_TYPE = Symbol('Inbound Request Type');
|
|
const SYM_RESP = Symbol('Inbound Request Response');
|
|
const SYM_CTX = Symbol('Inbound Request Context');
|
|
class AgentInboundRequest {
|
|
constructor(type, ctx) {
|
|
this[SYM_TYPE] = type;
|
|
this[SYM_RESP] = undefined;
|
|
this[SYM_CTX] = ctx;
|
|
}
|
|
hasResponded() {
|
|
return (this[SYM_RESP] !== undefined);
|
|
}
|
|
getType() {
|
|
return this[SYM_TYPE];
|
|
}
|
|
getContext() {
|
|
return this[SYM_CTX];
|
|
}
|
|
}
|
|
function respond(protocol, req, data) {
|
|
req[SYM_RESP] = data;
|
|
return processResponses(protocol);
|
|
}
|
|
|
|
function cleanup(protocol) {
|
|
protocol[SYM_BUFFER] = null;
|
|
if (protocol[SYM_MODE] === ROLE_CLIENT) {
|
|
const reqs = protocol[SYM_REQS];
|
|
if (reqs && reqs.length) {
|
|
protocol[SYM_REQS] = [];
|
|
for (const req of reqs)
|
|
req.cb(new Error('No reply from server'));
|
|
}
|
|
}
|
|
|
|
// Node streams hackery to make streams do the "right thing"
|
|
try {
|
|
protocol.end();
|
|
} catch {}
|
|
setImmediate(() => {
|
|
if (!protocol[SYM_ENDED])
|
|
protocol.emit('end');
|
|
if (!protocol[SYM_CLOSED])
|
|
protocol.emit('close');
|
|
});
|
|
}
|
|
|
|
function onClose() {
|
|
this[SYM_CLOSED] = true;
|
|
}
|
|
|
|
function onEnd() {
|
|
this[SYM_ENDED] = true;
|
|
}
|
|
|
|
const SYM_REQS = Symbol('Requests');
|
|
const SYM_MODE = Symbol('Agent Protocol Role');
|
|
const SYM_BUFFER = Symbol('Agent Protocol Buffer');
|
|
const SYM_MSGLEN = Symbol('Agent Protocol Current Message Length');
|
|
const SYM_CLOSED = Symbol('Agent Protocol Closed');
|
|
const SYM_ENDED = Symbol('Agent Protocol Ended');
|
|
// Implementation based on:
|
|
// https://tools.ietf.org/html/draft-miller-ssh-agent-04
|
|
return class AgentProtocol extends Duplex {
|
|
/*
|
|
Notes:
|
|
- `constraint` type consists of:
|
|
byte constraint_type
|
|
byte[] constraint_data
|
|
where `constraint_type` is one of:
|
|
* SSH_AGENT_CONSTRAIN_LIFETIME
|
|
- `constraint_data` consists of:
|
|
uint32 seconds
|
|
* SSH_AGENT_CONSTRAIN_CONFIRM
|
|
- `constraint_data` N/A
|
|
* SSH_AGENT_CONSTRAIN_EXTENSION
|
|
- `constraint_data` consists of:
|
|
string extension name
|
|
byte[] extension-specific details
|
|
*/
|
|
|
|
constructor(isClient) {
|
|
super({ autoDestroy: true, emitClose: false });
|
|
this[SYM_MODE] = (isClient ? ROLE_CLIENT : ROLE_SERVER);
|
|
this[SYM_REQS] = [];
|
|
this[SYM_BUFFER] = null;
|
|
this[SYM_MSGLEN] = -1;
|
|
this.once('end', onEnd);
|
|
this.once('close', onClose);
|
|
}
|
|
|
|
_read(n) {}
|
|
|
|
_write(data, encoding, cb) {
|
|
/*
|
|
Messages are of the format:
|
|
uint32 message length
|
|
byte message type
|
|
byte[message length - 1] message contents
|
|
*/
|
|
if (this[SYM_BUFFER] === null)
|
|
this[SYM_BUFFER] = data;
|
|
else
|
|
this[SYM_BUFFER] = concat(this[SYM_BUFFER], data);
|
|
|
|
let buffer = this[SYM_BUFFER];
|
|
let bufferLen = buffer.length;
|
|
|
|
let p = 0;
|
|
while (p < bufferLen) {
|
|
// Wait for length + type
|
|
if (bufferLen < 5)
|
|
break;
|
|
|
|
if (this[SYM_MSGLEN] === -1)
|
|
this[SYM_MSGLEN] = readUInt32BE(buffer, p);
|
|
|
|
// Check if we have the entire message
|
|
if (bufferLen < (4 + this[SYM_MSGLEN]))
|
|
break;
|
|
|
|
const msgType = buffer[p += 4];
|
|
++p;
|
|
|
|
if (this[SYM_MODE] === ROLE_CLIENT) {
|
|
if (this[SYM_REQS].length === 0)
|
|
return cb(new Error('Received unexpected message from server'));
|
|
|
|
const req = this[SYM_REQS].shift();
|
|
|
|
switch (msgType) {
|
|
case SSH_AGENT_FAILURE:
|
|
req.cb(new Error('Agent responded with failure'));
|
|
break;
|
|
case SSH_AGENT_IDENTITIES_ANSWER: {
|
|
if (req.type !== SSH_AGENTC_REQUEST_IDENTITIES)
|
|
return cb(new Error('Agent responded with wrong message type'));
|
|
|
|
/*
|
|
byte SSH_AGENT_IDENTITIES_ANSWER
|
|
uint32 nkeys
|
|
|
|
where `nkeys` is 0 or more of:
|
|
string key blob
|
|
string comment
|
|
*/
|
|
|
|
binaryParser.init(buffer, p);
|
|
|
|
const numKeys = binaryParser.readUInt32BE();
|
|
|
|
if (numKeys === undefined) {
|
|
binaryParser.clear();
|
|
return cb(new Error('Malformed agent response'));
|
|
}
|
|
|
|
const keys = [];
|
|
for (let i = 0; i < numKeys; ++i) {
|
|
let pubKey = binaryParser.readString();
|
|
if (pubKey === undefined) {
|
|
binaryParser.clear();
|
|
return cb(new Error('Malformed agent response'));
|
|
}
|
|
|
|
const comment = binaryParser.readString(true);
|
|
if (comment === undefined) {
|
|
binaryParser.clear();
|
|
return cb(new Error('Malformed agent response'));
|
|
}
|
|
|
|
pubKey = parseKey(pubKey);
|
|
// We continue parsing the packet if we encounter an error
|
|
// in case the error is due to the key being an unsupported
|
|
// type
|
|
if (pubKey instanceof Error)
|
|
continue;
|
|
|
|
pubKey.comment = pubKey.comment || comment;
|
|
|
|
keys.push(pubKey);
|
|
}
|
|
p = binaryParser.pos();
|
|
binaryParser.clear();
|
|
|
|
req.cb(null, keys);
|
|
break;
|
|
}
|
|
case SSH_AGENT_SIGN_RESPONSE: {
|
|
if (req.type !== SSH_AGENTC_SIGN_REQUEST)
|
|
return cb(new Error('Agent responded with wrong message type'));
|
|
|
|
/*
|
|
byte SSH_AGENT_SIGN_RESPONSE
|
|
string signature
|
|
*/
|
|
|
|
binaryParser.init(buffer, p);
|
|
let signature = binaryParser.readString();
|
|
p = binaryParser.pos();
|
|
binaryParser.clear();
|
|
|
|
if (signature === undefined)
|
|
return cb(new Error('Malformed agent response'));
|
|
|
|
// We strip the algorithm from OpenSSH's output and assume it's
|
|
// using the algorithm we specified. This makes it easier on
|
|
// custom Agent implementations so they don't have to construct
|
|
// the correct binary format for a (OpenSSH-style) signature.
|
|
|
|
// TODO: verify signature type based on key and options used
|
|
// during initial sign request
|
|
binaryParser.init(signature, 0);
|
|
binaryParser.readString(true);
|
|
signature = binaryParser.readString();
|
|
binaryParser.clear();
|
|
|
|
if (signature === undefined)
|
|
return cb(new Error('Malformed OpenSSH signature format'));
|
|
|
|
req.cb(null, signature);
|
|
break;
|
|
}
|
|
default:
|
|
return cb(
|
|
new Error('Agent responded with unsupported message type')
|
|
);
|
|
}
|
|
} else {
|
|
switch (msgType) {
|
|
case SSH_AGENTC_REQUEST_IDENTITIES: {
|
|
const req = new AgentInboundRequest(msgType);
|
|
this[SYM_REQS].push(req);
|
|
/*
|
|
byte SSH_AGENTC_REQUEST_IDENTITIES
|
|
*/
|
|
this.emit('identities', req);
|
|
break;
|
|
}
|
|
case SSH_AGENTC_SIGN_REQUEST: {
|
|
/*
|
|
byte SSH_AGENTC_SIGN_REQUEST
|
|
string key_blob
|
|
string data
|
|
uint32 flags
|
|
*/
|
|
binaryParser.init(buffer, p);
|
|
let pubKey = binaryParser.readString();
|
|
const data = binaryParser.readString();
|
|
const flagsVal = binaryParser.readUInt32BE();
|
|
p = binaryParser.pos();
|
|
binaryParser.clear();
|
|
if (flagsVal === undefined) {
|
|
const req = new AgentInboundRequest(msgType);
|
|
this[SYM_REQS].push(req);
|
|
return this.failureReply(req);
|
|
}
|
|
|
|
pubKey = parseKey(pubKey);
|
|
if (pubKey instanceof Error) {
|
|
const req = new AgentInboundRequest(msgType);
|
|
this[SYM_REQS].push(req);
|
|
return this.failureReply(req);
|
|
}
|
|
|
|
const flags = {
|
|
hash: undefined,
|
|
};
|
|
let ctx;
|
|
if (pubKey.type === 'ssh-rsa') {
|
|
if (flagsVal & SSH_AGENT_RSA_SHA2_256) {
|
|
ctx = 'rsa-sha2-256';
|
|
flags.hash = 'sha256';
|
|
} else if (flagsVal & SSH_AGENT_RSA_SHA2_512) {
|
|
ctx = 'rsa-sha2-512';
|
|
flags.hash = 'sha512';
|
|
}
|
|
}
|
|
if (ctx === undefined)
|
|
ctx = pubKey.type;
|
|
|
|
const req = new AgentInboundRequest(msgType, ctx);
|
|
this[SYM_REQS].push(req);
|
|
|
|
this.emit('sign', req, pubKey, data, flags);
|
|
break;
|
|
}
|
|
default: {
|
|
const req = new AgentInboundRequest(msgType);
|
|
this[SYM_REQS].push(req);
|
|
this.failureReply(req);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get ready for next message
|
|
this[SYM_MSGLEN] = -1;
|
|
if (p === bufferLen) {
|
|
// Nothing left to process for now
|
|
this[SYM_BUFFER] = null;
|
|
break;
|
|
} else {
|
|
this[SYM_BUFFER] = buffer = buffer.slice(p);
|
|
bufferLen = buffer.length;
|
|
p = 0;
|
|
}
|
|
}
|
|
|
|
cb();
|
|
}
|
|
|
|
_destroy(err, cb) {
|
|
cleanup(this);
|
|
cb();
|
|
}
|
|
|
|
_final(cb) {
|
|
cleanup(this);
|
|
cb();
|
|
}
|
|
|
|
// Client->Server messages =================================================
|
|
sign(pubKey, data, options, cb) {
|
|
if (this[SYM_MODE] !== ROLE_CLIENT)
|
|
throw new Error('Client-only method called with server role');
|
|
|
|
if (typeof options === 'function') {
|
|
cb = options;
|
|
options = undefined;
|
|
} else if (typeof options !== 'object' || options === null) {
|
|
options = undefined;
|
|
}
|
|
|
|
let flags = 0;
|
|
|
|
pubKey = parseKey(pubKey);
|
|
if (pubKey instanceof Error)
|
|
throw new Error('Invalid public key argument');
|
|
|
|
if (pubKey.type === 'ssh-rsa' && options) {
|
|
switch (options.hash) {
|
|
case 'sha256':
|
|
flags = SSH_AGENT_RSA_SHA2_256;
|
|
break;
|
|
case 'sha512':
|
|
flags = SSH_AGENT_RSA_SHA2_512;
|
|
break;
|
|
}
|
|
}
|
|
pubKey = pubKey.getPublicSSH();
|
|
|
|
/*
|
|
byte SSH_AGENTC_SIGN_REQUEST
|
|
string key_blob
|
|
string data
|
|
uint32 flags
|
|
*/
|
|
const type = SSH_AGENTC_SIGN_REQUEST;
|
|
const keyLen = pubKey.length;
|
|
const dataLen = data.length;
|
|
let p = 0;
|
|
const buf = Buffer.allocUnsafe(4 + 1 + 4 + keyLen + 4 + dataLen + 4);
|
|
|
|
writeUInt32BE(buf, buf.length - 4, p);
|
|
|
|
buf[p += 4] = type;
|
|
|
|
writeUInt32BE(buf, keyLen, ++p);
|
|
pubKey.copy(buf, p += 4);
|
|
|
|
writeUInt32BE(buf, dataLen, p += keyLen);
|
|
data.copy(buf, p += 4);
|
|
|
|
writeUInt32BE(buf, flags, p += dataLen);
|
|
|
|
if (typeof cb !== 'function')
|
|
cb = noop;
|
|
|
|
this[SYM_REQS].push({ type, cb });
|
|
|
|
return this.push(buf);
|
|
}
|
|
getIdentities(cb) {
|
|
if (this[SYM_MODE] !== ROLE_CLIENT)
|
|
throw new Error('Client-only method called with server role');
|
|
|
|
/*
|
|
byte SSH_AGENTC_REQUEST_IDENTITIES
|
|
*/
|
|
const type = SSH_AGENTC_REQUEST_IDENTITIES;
|
|
|
|
let p = 0;
|
|
const buf = Buffer.allocUnsafe(4 + 1);
|
|
|
|
writeUInt32BE(buf, buf.length - 4, p);
|
|
|
|
buf[p += 4] = type;
|
|
|
|
if (typeof cb !== 'function')
|
|
cb = noop;
|
|
|
|
this[SYM_REQS].push({ type, cb });
|
|
|
|
return this.push(buf);
|
|
}
|
|
|
|
// Server->Client messages =================================================
|
|
failureReply(req) {
|
|
if (this[SYM_MODE] !== ROLE_SERVER)
|
|
throw new Error('Server-only method called with client role');
|
|
|
|
if (!(req instanceof AgentInboundRequest))
|
|
throw new Error('Wrong request argument');
|
|
|
|
if (req.hasResponded())
|
|
return true;
|
|
|
|
let p = 0;
|
|
const buf = Buffer.allocUnsafe(4 + 1);
|
|
|
|
writeUInt32BE(buf, buf.length - 4, p);
|
|
|
|
buf[p += 4] = SSH_AGENT_FAILURE;
|
|
|
|
return respond(this, req, buf);
|
|
}
|
|
getIdentitiesReply(req, keys) {
|
|
if (this[SYM_MODE] !== ROLE_SERVER)
|
|
throw new Error('Server-only method called with client role');
|
|
|
|
if (!(req instanceof AgentInboundRequest))
|
|
throw new Error('Wrong request argument');
|
|
|
|
if (req.hasResponded())
|
|
return true;
|
|
|
|
/*
|
|
byte SSH_AGENT_IDENTITIES_ANSWER
|
|
uint32 nkeys
|
|
|
|
where `nkeys` is 0 or more of:
|
|
string key blob
|
|
string comment
|
|
*/
|
|
|
|
if (req.getType() !== SSH_AGENTC_REQUEST_IDENTITIES)
|
|
throw new Error('Invalid response to request');
|
|
|
|
if (!Array.isArray(keys))
|
|
throw new Error('Keys argument must be an array');
|
|
|
|
let totalKeysLen = 4; // Include `nkeys` size
|
|
|
|
const newKeys = [];
|
|
for (let i = 0; i < keys.length; ++i) {
|
|
const entry = keys[i];
|
|
if (typeof entry !== 'object' || entry === null)
|
|
throw new Error(`Invalid key entry: ${entry}`);
|
|
|
|
let pubKey;
|
|
let comment;
|
|
if (isParsedKey(entry)) {
|
|
pubKey = entry;
|
|
} else if (isParsedKey(entry.pubKey)) {
|
|
pubKey = entry.pubKey;
|
|
} else {
|
|
if (typeof entry.pubKey !== 'object' || entry.pubKey === null)
|
|
continue;
|
|
({ pubKey, comment } = entry.pubKey);
|
|
pubKey = parseKey(pubKey);
|
|
if (pubKey instanceof Error)
|
|
continue; // TODO: add debug output
|
|
}
|
|
comment = pubKey.comment || comment;
|
|
pubKey = pubKey.getPublicSSH();
|
|
|
|
totalKeysLen += 4 + pubKey.length;
|
|
|
|
if (comment && typeof comment === 'string')
|
|
comment = Buffer.from(comment);
|
|
else if (!Buffer.isBuffer(comment))
|
|
comment = EMPTY_BUF;
|
|
|
|
totalKeysLen += 4 + comment.length;
|
|
|
|
newKeys.push({ pubKey, comment });
|
|
}
|
|
|
|
let p = 0;
|
|
const buf = Buffer.allocUnsafe(4 + 1 + totalKeysLen);
|
|
|
|
writeUInt32BE(buf, buf.length - 4, p);
|
|
|
|
buf[p += 4] = SSH_AGENT_IDENTITIES_ANSWER;
|
|
|
|
writeUInt32BE(buf, newKeys.length, ++p);
|
|
p += 4;
|
|
for (let i = 0; i < newKeys.length; ++i) {
|
|
const { pubKey, comment } = newKeys[i];
|
|
|
|
writeUInt32BE(buf, pubKey.length, p);
|
|
pubKey.copy(buf, p += 4);
|
|
|
|
writeUInt32BE(buf, comment.length, p += pubKey.length);
|
|
p += 4;
|
|
if (comment.length) {
|
|
comment.copy(buf, p);
|
|
p += comment.length;
|
|
}
|
|
}
|
|
|
|
return respond(this, req, buf);
|
|
}
|
|
signReply(req, signature) {
|
|
if (this[SYM_MODE] !== ROLE_SERVER)
|
|
throw new Error('Server-only method called with client role');
|
|
|
|
if (!(req instanceof AgentInboundRequest))
|
|
throw new Error('Wrong request argument');
|
|
|
|
if (req.hasResponded())
|
|
return true;
|
|
|
|
/*
|
|
byte SSH_AGENT_SIGN_RESPONSE
|
|
string signature
|
|
*/
|
|
|
|
if (req.getType() !== SSH_AGENTC_SIGN_REQUEST)
|
|
throw new Error('Invalid response to request');
|
|
|
|
if (!Buffer.isBuffer(signature))
|
|
throw new Error('Signature argument must be a Buffer');
|
|
|
|
if (signature.length === 0)
|
|
throw new Error('Signature argument must be non-empty');
|
|
|
|
/*
|
|
OpenSSH agent signatures are encoded as:
|
|
|
|
string signature format identifier (as specified by the
|
|
public key/certificate format)
|
|
byte[n] signature blob in format specific encoding.
|
|
- This is actually a `string` for: rsa, dss, ecdsa, and ed25519
|
|
types
|
|
*/
|
|
|
|
let p = 0;
|
|
const sigFormat = req.getContext();
|
|
const sigFormatLen = Buffer.byteLength(sigFormat);
|
|
const buf = Buffer.allocUnsafe(
|
|
4 + 1 + 4 + 4 + sigFormatLen + 4 + signature.length
|
|
);
|
|
|
|
writeUInt32BE(buf, buf.length - 4, p);
|
|
|
|
buf[p += 4] = SSH_AGENT_SIGN_RESPONSE;
|
|
|
|
writeUInt32BE(buf, 4 + sigFormatLen + 4 + signature.length, ++p);
|
|
writeUInt32BE(buf, sigFormatLen, p += 4);
|
|
buf.utf8Write(sigFormat, p += 4, sigFormatLen);
|
|
writeUInt32BE(buf, signature.length, p += sigFormatLen);
|
|
signature.copy(buf, p += 4);
|
|
|
|
return respond(this, req, buf);
|
|
}
|
|
};
|
|
})();
|
|
|
|
const SYM_AGENT = Symbol('Agent');
|
|
const SYM_AGENT_KEYS = Symbol('Agent Keys');
|
|
const SYM_AGENT_KEYS_IDX = Symbol('Agent Keys Index');
|
|
const SYM_AGENT_CBS = Symbol('Agent Init Callbacks');
|
|
class AgentContext {
|
|
constructor(agent) {
|
|
if (typeof agent === 'string')
|
|
agent = createAgent(agent);
|
|
else if (!isAgent(agent))
|
|
throw new Error('Invalid agent argument');
|
|
this[SYM_AGENT] = agent;
|
|
this[SYM_AGENT_KEYS] = null;
|
|
this[SYM_AGENT_KEYS_IDX] = -1;
|
|
this[SYM_AGENT_CBS] = null;
|
|
}
|
|
init(cb) {
|
|
if (typeof cb !== 'function')
|
|
cb = noop;
|
|
|
|
if (this[SYM_AGENT_KEYS] === null) {
|
|
if (this[SYM_AGENT_CBS] === null) {
|
|
this[SYM_AGENT_CBS] = [cb];
|
|
|
|
const doCbs = (...args) => {
|
|
process.nextTick(() => {
|
|
const cbs = this[SYM_AGENT_CBS];
|
|
this[SYM_AGENT_CBS] = null;
|
|
for (const cb of cbs)
|
|
cb(...args);
|
|
});
|
|
};
|
|
|
|
this[SYM_AGENT].getIdentities(once((err, keys) => {
|
|
if (err)
|
|
return doCbs(err);
|
|
|
|
if (!Array.isArray(keys)) {
|
|
return doCbs(new Error(
|
|
'Agent implementation failed to provide keys'
|
|
));
|
|
}
|
|
|
|
const newKeys = [];
|
|
for (let key of keys) {
|
|
key = parseKey(key);
|
|
if (key instanceof Error) {
|
|
// TODO: add debug output
|
|
continue;
|
|
}
|
|
newKeys.push(key);
|
|
}
|
|
|
|
this[SYM_AGENT_KEYS] = newKeys;
|
|
this[SYM_AGENT_KEYS_IDX] = -1;
|
|
doCbs();
|
|
}));
|
|
} else {
|
|
this[SYM_AGENT_CBS].push(cb);
|
|
}
|
|
} else {
|
|
process.nextTick(cb);
|
|
}
|
|
}
|
|
nextKey() {
|
|
if (this[SYM_AGENT_KEYS] === null
|
|
|| ++this[SYM_AGENT_KEYS_IDX] >= this[SYM_AGENT_KEYS].length) {
|
|
return false;
|
|
}
|
|
|
|
return this[SYM_AGENT_KEYS][this[SYM_AGENT_KEYS_IDX]];
|
|
}
|
|
currentKey() {
|
|
if (this[SYM_AGENT_KEYS] === null
|
|
|| this[SYM_AGENT_KEYS_IDX] >= this[SYM_AGENT_KEYS].length) {
|
|
return null;
|
|
}
|
|
|
|
return this[SYM_AGENT_KEYS][this[SYM_AGENT_KEYS_IDX]];
|
|
}
|
|
pos() {
|
|
if (this[SYM_AGENT_KEYS] === null
|
|
|| this[SYM_AGENT_KEYS_IDX] >= this[SYM_AGENT_KEYS].length) {
|
|
return -1;
|
|
}
|
|
|
|
return this[SYM_AGENT_KEYS_IDX];
|
|
}
|
|
reset() {
|
|
this[SYM_AGENT_KEYS_IDX] = -1;
|
|
}
|
|
|
|
sign(...args) {
|
|
this[SYM_AGENT].sign(...args);
|
|
}
|
|
}
|
|
|
|
function isAgent(val) {
|
|
return (val instanceof BaseAgent);
|
|
}
|
|
|
|
module.exports = {
|
|
AgentContext,
|
|
AgentProtocol,
|
|
BaseAgent,
|
|
createAgent,
|
|
CygwinAgent,
|
|
isAgent,
|
|
OpenSSHAgent,
|
|
PageantAgent,
|
|
};
|