Files
bare-operating-system/packages/bare-os-openssh/vendor/bare-ssh2/lib/client.js
T

1964 lines
58 KiB
JavaScript

// TODO:
// * add `.connected` or similar property to allow immediate connection
// status checking
// * add/improve debug output during user authentication phase
'use strict'
require('./buffer-polyfill.js')
const { createHash, getHashes, randomFillSync } = require('crypto')
const { Socket } = require('net')
const { lookup: dnsLookup } = require('dns')
const EventEmitter = require('events')
const HASHES = getHashes()
const {
COMPAT,
CHANNEL_EXTENDED_DATATYPE: { STDERR },
CHANNEL_OPEN_FAILURE,
DEFAULT_CIPHER,
DEFAULT_COMPRESSION,
DEFAULT_KEX,
DEFAULT_MAC,
DEFAULT_SERVER_HOST_KEY,
DISCONNECT_REASON,
DISCONNECT_REASON_BY_VALUE,
SUPPORTED_CIPHER,
SUPPORTED_COMPRESSION,
SUPPORTED_KEX,
SUPPORTED_MAC,
SUPPORTED_SERVER_HOST_KEY
} = require('./protocol/constants.js')
const { init: cryptoInit } = require('./protocol/crypto.js')
const Protocol = require('./protocol/Protocol.js')
const { parseKey } = require('./protocol/keyParser.js')
const { SFTP } = require('./protocol/SFTP.js')
const {
bufferCopy,
makeBufferParser,
makeError,
readUInt32BE,
sigSSHToASN1,
writeUInt32BE
} = require('./protocol/utils.js')
const { AgentContext, createAgent, isAgent } = require('./agent.js')
const { Channel, MAX_WINDOW, PACKET_SIZE, windowAdjust, WINDOW_THRESHOLD } = require('./Channel.js')
const {
ChannelManager,
generateAlgorithmList,
isWritable,
onChannelOpenFailure,
onCHANNEL_CLOSE
} = require('./utils.js')
const bufferParser = makeBufferParser()
const sigParser = makeBufferParser()
const RE_OPENSSH = /^OpenSSH_(?:(?![0-4])\d)|(?:\d{2,})/
const noop = (err) => {}
class Client extends EventEmitter {
constructor() {
super()
this.config = {
host: undefined,
port: undefined,
localAddress: undefined,
localPort: undefined,
forceIPv4: undefined,
forceIPv6: undefined,
keepaliveCountMax: undefined,
keepaliveInterval: undefined,
readyTimeout: undefined,
ident: undefined,
username: undefined,
password: undefined,
privateKey: undefined,
tryKeyboard: undefined,
agent: undefined,
allowAgentFwd: undefined,
authHandler: undefined,
hostHashAlgo: undefined,
hostHashCb: undefined,
strictVendor: undefined,
debug: undefined
}
this._agent = undefined
this._readyTimeout = undefined
this._chanMgr = undefined
this._callbacks = undefined
this._forwarding = undefined
this._forwardingUnix = undefined
this._acceptX11 = undefined
this._agentFwdEnabled = undefined
this._remoteVer = undefined
this._protocol = undefined
this._sock = undefined
this._resetKA = undefined
}
connect(cfg) {
if (this._sock && isWritable(this._sock)) {
this.once('close', () => {
this.connect(cfg)
})
this.end()
return this
}
this.config.host = cfg.hostname || cfg.host || 'localhost'
this.config.port = cfg.port || 22
this.config.localAddress = typeof cfg.localAddress === 'string' ? cfg.localAddress : undefined
this.config.localPort =
typeof cfg.localPort === 'string' || typeof cfg.localPort === 'number'
? cfg.localPort
: undefined
this.config.forceIPv4 = cfg.forceIPv4 || false
this.config.forceIPv6 = cfg.forceIPv6 || false
this.config.keepaliveCountMax =
typeof cfg.keepaliveCountMax === 'number' && cfg.keepaliveCountMax >= 0
? cfg.keepaliveCountMax
: 3
this.config.keepaliveInterval =
typeof cfg.keepaliveInterval === 'number' && cfg.keepaliveInterval > 0
? cfg.keepaliveInterval
: 0
this.config.readyTimeout =
typeof cfg.readyTimeout === 'number' && cfg.readyTimeout >= 0 ? cfg.readyTimeout : 20000
this.config.ident =
typeof cfg.ident === 'string' || Buffer.isBuffer(cfg.ident) ? cfg.ident : undefined
const algorithms = {
kex: undefined,
serverHostKey: undefined,
cs: {
cipher: undefined,
mac: undefined,
compress: undefined,
lang: []
},
sc: undefined
}
let allOfferDefaults = true
if (typeof cfg.algorithms === 'object' && cfg.algorithms !== null) {
algorithms.kex = generateAlgorithmList(cfg.algorithms.kex, DEFAULT_KEX, SUPPORTED_KEX)
if (algorithms.kex !== DEFAULT_KEX) allOfferDefaults = false
algorithms.serverHostKey = generateAlgorithmList(
cfg.algorithms.serverHostKey,
DEFAULT_SERVER_HOST_KEY,
SUPPORTED_SERVER_HOST_KEY
)
if (algorithms.serverHostKey !== DEFAULT_SERVER_HOST_KEY) allOfferDefaults = false
algorithms.cs.cipher = generateAlgorithmList(
cfg.algorithms.cipher,
DEFAULT_CIPHER,
SUPPORTED_CIPHER
)
if (algorithms.cs.cipher !== DEFAULT_CIPHER) allOfferDefaults = false
algorithms.cs.mac = generateAlgorithmList(cfg.algorithms.hmac, DEFAULT_MAC, SUPPORTED_MAC)
if (algorithms.cs.mac !== DEFAULT_MAC) allOfferDefaults = false
algorithms.cs.compress = generateAlgorithmList(
cfg.algorithms.compress,
DEFAULT_COMPRESSION,
SUPPORTED_COMPRESSION
)
if (algorithms.cs.compress !== DEFAULT_COMPRESSION) allOfferDefaults = false
if (!allOfferDefaults) algorithms.sc = algorithms.cs
}
if (typeof cfg.username === 'string') this.config.username = cfg.username
else if (typeof cfg.user === 'string') this.config.username = cfg.user
else throw new Error('Invalid username')
this.config.password = typeof cfg.password === 'string' ? cfg.password : undefined
this.config.privateKey =
typeof cfg.privateKey === 'string' || Buffer.isBuffer(cfg.privateKey)
? cfg.privateKey
: undefined
this.config.localHostname =
typeof cfg.localHostname === 'string' ? cfg.localHostname : undefined
this.config.localUsername =
typeof cfg.localUsername === 'string' ? cfg.localUsername : undefined
this.config.tryKeyboard = cfg.tryKeyboard === true
if (typeof cfg.agent === 'string' && cfg.agent.length)
this.config.agent = createAgent(cfg.agent)
else if (isAgent(cfg.agent)) this.config.agent = cfg.agent
else this.config.agent = undefined
this.config.allowAgentFwd = cfg.agentForward === true && this.config.agent !== undefined
let authHandler = (this.config.authHandler =
typeof cfg.authHandler === 'function' || Array.isArray(cfg.authHandler)
? cfg.authHandler
: undefined)
this.config.strictVendor = typeof cfg.strictVendor === 'boolean' ? cfg.strictVendor : true
const debug = (this.config.debug = typeof cfg.debug === 'function' ? cfg.debug : undefined)
if (cfg.agentForward === true && !this.config.allowAgentFwd) {
throw new Error('You must set a valid agent path to allow agent forwarding')
}
let callbacks = (this._callbacks = [])
this._chanMgr = new ChannelManager(this)
this._forwarding = {}
this._forwardingUnix = {}
this._acceptX11 = 0
this._agentFwdEnabled = false
this._agent = this.config.agent ? this.config.agent : undefined
this._remoteVer = undefined
let privateKey
if (this.config.privateKey) {
privateKey = parseKey(this.config.privateKey, cfg.passphrase)
if (privateKey instanceof Error)
throw new Error(`Cannot parse privateKey: ${privateKey.message}`)
if (Array.isArray(privateKey)) {
// OpenSSH's newer format only stores 1 key for now
privateKey = privateKey[0]
}
if (privateKey.getPrivatePEM() === null) {
throw new Error('privateKey value does not contain a (valid) private key')
}
}
let hostVerifier
if (typeof cfg.hostVerifier === 'function') {
const hashCb = cfg.hostVerifier
let hashAlgo
if (HASHES.indexOf(cfg.hostHash) !== -1) {
// Default to old behavior of hashing on user's behalf
hashAlgo = cfg.hostHash
}
hostVerifier = (key, verify) => {
if (hashAlgo) key = createHash(hashAlgo).update(key).digest('hex')
const ret = hashCb(key, verify)
if (ret !== undefined) verify(ret)
}
}
const sock = (this._sock = cfg.sock || new Socket())
let ready = false
let sawHeader = false
if (this._protocol) this._protocol.cleanup()
const DEBUG_HANDLER = !debug
? undefined
: (p, display, msg) => {
debug(`Debug output from server: ${JSON.stringify(msg)}`)
}
let serverSigAlgs
const proto = (this._protocol = new Protocol({
ident: this.config.ident,
offer: allOfferDefaults ? undefined : algorithms,
onWrite: (data) => {
if (isWritable(sock)) sock.write(data)
},
onError: (err) => {
if (err.level === 'handshake') clearTimeout(this._readyTimeout)
if (!proto._destruct) sock.removeAllListeners('data')
this.emit('error', err)
try {
sock.end()
} catch {}
},
onHeader: (header) => {
sawHeader = true
this._remoteVer = header.versions.software
if (header.greeting) this.emit('greeting', header.greeting)
},
onHandshakeComplete: (negotiated) => {
this.emit('handshake', negotiated)
if (!ready) {
ready = true
proto.service('ssh-userauth')
}
},
debug,
hostVerifier,
messageHandlers: {
DEBUG: DEBUG_HANDLER,
DISCONNECT: (p, reason, desc) => {
if (reason !== DISCONNECT_REASON.BY_APPLICATION) {
if (!desc) {
desc = DISCONNECT_REASON_BY_VALUE[reason]
if (desc === undefined) desc = `Unexpected disconnection reason: ${reason}`
}
const err = new Error(desc)
err.code = reason
this.emit('error', err)
}
sock.end()
},
SERVICE_ACCEPT: (p, name) => {
if (name === 'ssh-userauth') tryNextAuth()
},
EXT_INFO: (p, exts) => {
if (serverSigAlgs === undefined) {
for (const ext of exts) {
if (ext.name === 'server-sig-algs') {
serverSigAlgs = ext.algs
return
}
}
serverSigAlgs = null
}
},
USERAUTH_BANNER: (p, msg) => {
this.emit('banner', msg)
},
USERAUTH_SUCCESS: (p) => {
// Start keepalive mechanism
resetKA()
clearTimeout(this._readyTimeout)
this.emit('ready')
},
USERAUTH_FAILURE: (p, authMethods, partialSuccess) => {
// For key-based authentication, check if we should retry the current
// key with a different algorithm first
if (curAuth.keyAlgos) {
const oldKeyAlgo = curAuth.keyAlgos[0][0]
if (debug) debug(`Client: ${curAuth.type} (${oldKeyAlgo}) auth failed`)
curAuth.keyAlgos.shift()
if (curAuth.keyAlgos.length) {
const [keyAlgo, hashAlgo] = curAuth.keyAlgos[0]
switch (curAuth.type) {
case 'agent':
proto.authPK(curAuth.username, curAuth.agentCtx.currentKey(), keyAlgo)
return
case 'publickey':
proto.authPK(curAuth.username, curAuth.key, keyAlgo)
return
case 'hostbased':
proto.authHostbased(
curAuth.username,
curAuth.key,
curAuth.localHostname,
curAuth.localUsername,
keyAlgo,
(buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo)
if (signature instanceof Error) {
signature.message = `Error while signing with key: ${signature.message}`
signature.level = 'client-authentication'
this.emit('error', signature)
return tryNextAuth()
}
cb(signature)
}
)
return
}
} else {
curAuth.keyAlgos = undefined
}
}
if (curAuth.type === 'agent') {
const pos = curAuth.agentCtx.pos()
debug && debug(`Client: Agent key #${pos + 1} failed`)
return tryNextAgentKey()
}
debug && debug(`Client: ${curAuth.type} auth failed`)
curPartial = partialSuccess
curAuthsLeft = authMethods
tryNextAuth()
},
USERAUTH_PASSWD_CHANGEREQ: (p, prompt) => {
if (curAuth.type === 'password') {
// TODO: support a `changePrompt()` on `curAuth` that defaults to
// emitting 'change password' as before
this.emit('change password', prompt, (newPassword) => {
proto.authPassword(this.config.username, this.config.password, newPassword)
})
}
},
USERAUTH_PK_OK: (p) => {
let keyAlgo
let hashAlgo
if (curAuth.keyAlgos) [keyAlgo, hashAlgo] = curAuth.keyAlgos[0]
if (curAuth.type === 'agent') {
const key = curAuth.agentCtx.currentKey()
proto.authPK(curAuth.username, key, keyAlgo, (buf, cb) => {
const opts = { hash: hashAlgo }
curAuth.agentCtx.sign(key, buf, opts, (err, signed) => {
if (err) {
err.level = 'agent'
this.emit('error', err)
} else {
return cb(signed)
}
tryNextAgentKey()
})
})
} else if (curAuth.type === 'publickey') {
proto.authPK(curAuth.username, curAuth.key, keyAlgo, (buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo)
if (signature instanceof Error) {
signature.message = `Error signing data with key: ${signature.message}`
signature.level = 'client-authentication'
this.emit('error', signature)
return tryNextAuth()
}
cb(signature)
})
}
},
USERAUTH_INFO_REQUEST: (p, name, instructions, prompts) => {
if (curAuth.type === 'keyboard-interactive') {
const nprompts = Array.isArray(prompts) ? prompts.length : 0
if (nprompts === 0) {
debug && debug('Client: Sending automatic USERAUTH_INFO_RESPONSE')
proto.authInfoRes()
return
}
// We sent a keyboard-interactive user authentication request and
// now the server is sending us the prompts we need to present to
// the user
curAuth.prompt(name, instructions, '', prompts, (answers) => {
proto.authInfoRes(answers)
})
}
},
REQUEST_SUCCESS: (p, data) => {
if (callbacks.length) callbacks.shift()(false, data)
},
REQUEST_FAILURE: (p) => {
if (callbacks.length) callbacks.shift()(true)
},
GLOBAL_REQUEST: (p, name, wantReply, data) => {
switch (name) {
case '[email protected]':
// Automatically verify keys before passing to end user
hostKeysProve(this, data, (err, keys) => {
if (err) return
this.emit('hostkeys', keys)
})
if (wantReply) proto.requestSuccess()
break
default:
// Auto-reject all other global requests, this can be especially
// useful if the server is sending us dummy keepalive global
// requests
if (wantReply) proto.requestFailure()
}
},
CHANNEL_OPEN: (p, info) => {
// Handle incoming requests from server, typically a forwarded TCP or
// X11 connection
onCHANNEL_OPEN(this, info)
},
CHANNEL_OPEN_CONFIRMATION: (p, info) => {
const channel = this._chanMgr.get(info.recipient)
if (typeof channel !== 'function') return
const isSFTP = channel.type === 'sftp'
const type = isSFTP ? 'session' : channel.type
const chanInfo = {
type,
incoming: {
id: info.recipient,
window: MAX_WINDOW,
packetSize: PACKET_SIZE,
state: 'open'
},
outgoing: {
id: info.sender,
window: info.window,
packetSize: info.packetSize,
state: 'open'
}
}
const instance = isSFTP
? new SFTP(this, chanInfo, { debug })
: new Channel(this, chanInfo)
this._chanMgr.update(info.recipient, instance)
channel(undefined, instance)
},
CHANNEL_OPEN_FAILURE: (p, recipient, reason, description) => {
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'function') return
const info = { reason, description }
onChannelOpenFailure(this, recipient, info, channel)
},
CHANNEL_DATA: (p, recipient, data) => {
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'object' || channel === null) return
// The remote party should not be sending us data if there is no
// window space available ...
// TODO: raise error on data with not enough window?
if (channel.incoming.window === 0) return
channel.incoming.window -= data.length
if (channel.push(data) === false) {
channel._waitChanDrain = true
return
}
if (channel.incoming.window <= WINDOW_THRESHOLD) windowAdjust(channel)
},
CHANNEL_EXTENDED_DATA: (p, recipient, data, type) => {
if (type !== STDERR) return
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'object' || channel === null) return
// The remote party should not be sending us data if there is no
// window space available ...
// TODO: raise error on data with not enough window?
if (channel.incoming.window === 0) return
channel.incoming.window -= data.length
if (!channel.stderr.push(data)) {
channel._waitChanDrain = true
return
}
if (channel.incoming.window <= WINDOW_THRESHOLD) windowAdjust(channel)
},
CHANNEL_WINDOW_ADJUST: (p, recipient, amount) => {
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'object' || channel === null) return
// The other side is allowing us to send `amount` more bytes of data
channel.outgoing.window += amount
if (channel._waitWindow) {
channel._waitWindow = false
if (channel._chunk) {
channel._write(channel._chunk, null, channel._chunkcb)
} else if (channel._chunkcb) {
channel._chunkcb()
} else if (channel._chunkErr) {
channel.stderr._write(channel._chunkErr, null, channel._chunkcbErr)
} else if (channel._chunkcbErr) {
channel._chunkcbErr()
}
}
},
CHANNEL_SUCCESS: (p, recipient) => {
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'object' || channel === null) return
this._resetKA()
if (channel._callbacks.length) channel._callbacks.shift()(false)
},
CHANNEL_FAILURE: (p, recipient) => {
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'object' || channel === null) return
this._resetKA()
if (channel._callbacks.length) channel._callbacks.shift()(true)
},
CHANNEL_REQUEST: (p, recipient, type, wantReply, data) => {
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'object' || channel === null) return
const exit = channel._exit
if (exit.code !== undefined) return
switch (type) {
case 'exit-status':
channel.emit('exit', (exit.code = data))
return
case 'exit-signal':
channel.emit(
'exit',
(exit.code = null),
(exit.signal = `SIG${data.signal}`),
(exit.dump = data.coreDumped),
(exit.desc = data.errorMessage)
)
return
}
// Keepalive request? OpenSSH will send one as a channel request if
// there is a channel open
if (wantReply) p.channelFailure(channel.outgoing.id)
},
CHANNEL_EOF: (p, recipient) => {
const channel = this._chanMgr.get(recipient)
if (typeof channel !== 'object' || channel === null) return
if (channel.incoming.state !== 'open') return
channel.incoming.state = 'eof'
if (channel.readable) channel.push(null)
if (channel.stderr.readable) channel.stderr.push(null)
},
CHANNEL_CLOSE: (p, recipient) => {
onCHANNEL_CLOSE(this, recipient, this._chanMgr.get(recipient))
}
}
}))
sock.pause()
// TODO: check keepalive implementation
// Keepalive-related
const kainterval = this.config.keepaliveInterval
const kacountmax = this.config.keepaliveCountMax
let kacount = 0
let katimer
const sendKA = () => {
if (++kacount > kacountmax) {
clearInterval(katimer)
if (sock.readable) {
const err = new Error('Keepalive timeout')
err.level = 'client-timeout'
this.emit('error', err)
sock.destroy()
}
return
}
if (isWritable(sock)) {
// Append dummy callback to keep correct callback order
callbacks.push(resetKA)
proto.ping()
} else {
clearInterval(katimer)
}
}
function resetKA() {
if (kainterval > 0) {
kacount = 0
clearInterval(katimer)
if (isWritable(sock)) katimer = setInterval(sendKA, kainterval)
}
}
this._resetKA = resetKA
const onDone = (() => {
let called = false
return () => {
if (called) return
called = true
if (wasConnected && !sawHeader) {
const err = makeError('Connection lost before handshake', 'protocol', true)
this.emit('error', err)
}
}
})()
const onConnect = (() => {
let called = false
return () => {
if (called) return
called = true
wasConnected = true
debug && debug('Socket connected')
this.emit('connect')
cryptoInit
.then(() => {
proto.start()
sock.on('data', (data) => {
try {
proto.parse(data, 0, data.length)
} catch (ex) {
this.emit('error', ex)
try {
if (isWritable(sock)) sock.end()
} catch {}
}
})
// Drain stderr if we are connection hopping using an exec stream
if (sock.stderr && typeof sock.stderr.resume === 'function') sock.stderr.resume()
sock.resume()
})
.catch((err) => {
this.emit('error', err)
try {
if (isWritable(sock)) sock.end()
} catch {}
})
}
})()
let wasConnected = false
sock
.on('connect', onConnect)
.on('timeout', () => {
this.emit('timeout')
})
.on('error', (err) => {
debug && debug(`Socket error: ${err.message}`)
clearTimeout(this._readyTimeout)
err.level = 'client-socket'
this.emit('error', err)
})
.on('end', () => {
debug && debug('Socket ended')
onDone()
proto.cleanup()
clearTimeout(this._readyTimeout)
clearInterval(katimer)
this.emit('end')
})
.on('close', () => {
debug && debug('Socket closed')
onDone()
proto.cleanup()
clearTimeout(this._readyTimeout)
clearInterval(katimer)
this.emit('close')
// Notify outstanding channel requests of disconnection ...
const callbacks_ = callbacks
callbacks = this._callbacks = []
const err = new Error('No response from server')
for (let i = 0; i < callbacks_.length; ++i) callbacks_[i](err)
// Simulate error for any channels waiting to be opened
this._chanMgr.cleanup(err)
})
// Begin authentication handling ===========================================
let curAuth
let curPartial = null
let curAuthsLeft = null
const authsAllowed = ['none']
if (this.config.password !== undefined) authsAllowed.push('password')
if (privateKey !== undefined) authsAllowed.push('publickey')
if (this._agent !== undefined) authsAllowed.push('agent')
if (this.config.tryKeyboard) authsAllowed.push('keyboard-interactive')
if (
privateKey !== undefined &&
this.config.localHostname !== undefined &&
this.config.localUsername !== undefined
) {
authsAllowed.push('hostbased')
}
if (Array.isArray(authHandler)) authHandler = makeSimpleAuthHandler(authHandler)
else if (typeof authHandler !== 'function') authHandler = makeSimpleAuthHandler(authsAllowed)
let hasSentAuth = false
const doNextAuth = (nextAuth) => {
if (hasSentAuth) return
hasSentAuth = true
if (nextAuth === false) {
const err = new Error('All configured authentication methods failed')
err.level = 'client-authentication'
this.emit('error', err)
this.end()
return
}
if (typeof nextAuth === 'string') {
// Remain backwards compatible with original `authHandler()` usage,
// which only supported passing names of next method to try using data
// from the `connect()` config object
const type = nextAuth
if (authsAllowed.indexOf(type) === -1)
return skipAuth(`Authentication method not allowed: ${type}`)
const username = this.config.username
switch (type) {
case 'password':
nextAuth = { type, username, password: this.config.password }
break
case 'publickey':
nextAuth = { type, username, key: privateKey }
break
case 'hostbased':
nextAuth = {
type,
username,
key: privateKey,
localHostname: this.config.localHostname,
localUsername: this.config.localUsername
}
break
case 'agent':
nextAuth = {
type,
username,
agentCtx: new AgentContext(this._agent)
}
break
case 'keyboard-interactive':
nextAuth = {
type,
username,
prompt: (...args) => this.emit('keyboard-interactive', ...args)
}
break
case 'none':
nextAuth = { type, username }
break
default:
return skipAuth(`Skipping unsupported authentication method: ${nextAuth}`)
}
} else if (typeof nextAuth !== 'object' || nextAuth === null) {
return skipAuth(`Skipping invalid authentication attempt: ${nextAuth}`)
} else {
const username = nextAuth.username
if (typeof username !== 'string') {
return skipAuth(`Skipping invalid authentication attempt: ${nextAuth}`)
}
const type = nextAuth.type
switch (type) {
case 'password': {
const { password } = nextAuth
if (typeof password !== 'string' && !Buffer.isBuffer(password))
return skipAuth('Skipping invalid password auth attempt')
nextAuth = { type, username, password }
break
}
case 'publickey': {
const key = parseKey(nextAuth.key, nextAuth.passphrase)
if (key instanceof Error) return skipAuth('Skipping invalid key auth attempt')
if (!key.isPrivateKey()) return skipAuth('Skipping non-private key')
nextAuth = { type, username, key }
break
}
case 'hostbased': {
const { localHostname, localUsername } = nextAuth
const key = parseKey(nextAuth.key, nextAuth.passphrase)
if (
key instanceof Error ||
typeof localHostname !== 'string' ||
typeof localUsername !== 'string'
) {
return skipAuth('Skipping invalid hostbased auth attempt')
}
if (!key.isPrivateKey()) return skipAuth('Skipping non-private key')
nextAuth = { type, username, key, localHostname, localUsername }
break
}
case 'agent': {
let agent = nextAuth.agent
if (typeof agent === 'string' && agent.length) {
agent = createAgent(agent)
} else if (!isAgent(agent)) {
return skipAuth(`Skipping invalid agent: ${nextAuth.agent}`)
}
nextAuth = { type, username, agentCtx: new AgentContext(agent) }
break
}
case 'keyboard-interactive': {
const { prompt } = nextAuth
if (typeof prompt !== 'function') {
return skipAuth('Skipping invalid keyboard-interactive auth attempt')
}
nextAuth = { type, username, prompt }
break
}
case 'none':
nextAuth = { type, username }
break
default:
return skipAuth(`Skipping unsupported authentication method: ${nextAuth}`)
}
}
curAuth = nextAuth
// Begin authentication method's process
try {
const username = curAuth.username
switch (curAuth.type) {
case 'password':
proto.authPassword(username, curAuth.password)
break
case 'publickey': {
let keyAlgo
curAuth.keyAlgos = getKeyAlgos(this, curAuth.key, serverSigAlgs)
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
keyAlgo = curAuth.keyAlgos[0][0]
} else {
return skipAuth('Skipping key authentication (no mutual hash algorithm)')
}
}
proto.authPK(username, curAuth.key, keyAlgo)
break
}
case 'hostbased': {
let keyAlgo
let hashAlgo
curAuth.keyAlgos = getKeyAlgos(this, curAuth.key, serverSigAlgs)
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
;[keyAlgo, hashAlgo] = curAuth.keyAlgos[0]
} else {
return skipAuth('Skipping hostbased authentication (no mutual hash algorithm)')
}
}
proto.authHostbased(
username,
curAuth.key,
curAuth.localHostname,
curAuth.localUsername,
keyAlgo,
(buf, cb) => {
const signature = curAuth.key.sign(buf, hashAlgo)
if (signature instanceof Error) {
signature.message = `Error while signing with key: ${signature.message}`
signature.level = 'client-authentication'
this.emit('error', signature)
return tryNextAuth()
}
cb(signature)
}
)
break
}
case 'agent':
curAuth.agentCtx.init((err) => {
if (err) {
err.level = 'agent'
this.emit('error', err)
return tryNextAuth()
}
tryNextAgentKey()
})
break
case 'keyboard-interactive':
proto.authKeyboard(username)
break
case 'none':
proto.authNone(username)
break
}
} finally {
hasSentAuth = false
}
}
function skipAuth(msg) {
debug && debug(msg)
process.nextTick(tryNextAuth)
}
function tryNextAuth() {
hasSentAuth = false
const auth = authHandler(curAuthsLeft, curPartial, doNextAuth)
if (hasSentAuth || auth === undefined) return
doNextAuth(auth)
}
const tryNextAgentKey = () => {
if (curAuth.type === 'agent') {
const key = curAuth.agentCtx.nextKey()
if (key === false) {
debug && debug('Agent: No more keys left to try')
debug && debug('Client: agent auth failed')
tryNextAuth()
} else {
const pos = curAuth.agentCtx.pos()
let keyAlgo
curAuth.keyAlgos = getKeyAlgos(this, key, serverSigAlgs)
if (curAuth.keyAlgos) {
if (curAuth.keyAlgos.length) {
keyAlgo = curAuth.keyAlgos[0][0]
} else {
debug && debug(`Agent: Skipping key #${pos + 1} (no mutual hash algorithm)`)
tryNextAgentKey()
return
}
}
debug && debug(`Agent: Trying key #${pos + 1}`)
proto.authPK(curAuth.username, key, keyAlgo)
}
}
}
const startTimeout = () => {
if (this.config.readyTimeout > 0) {
this._readyTimeout = setTimeout(() => {
const err = new Error('Timed out while waiting for handshake')
err.level = 'client-timeout'
this.emit('error', err)
sock.destroy()
}, this.config.readyTimeout)
}
}
if (!cfg.sock) {
let host = this.config.host
const forceIPv4 = this.config.forceIPv4
const forceIPv6 = this.config.forceIPv6
debug && debug(`Client: Trying ${host} on port ${this.config.port} ...`)
const doConnect = () => {
startTimeout()
sock.connect({
host,
port: this.config.port,
localAddress: this.config.localAddress,
localPort: this.config.localPort
})
sock.setMaxListeners(0)
sock.setTimeout(typeof cfg.timeout === 'number' ? cfg.timeout : 0)
}
if ((!forceIPv4 && !forceIPv6) || (forceIPv4 && forceIPv6)) {
doConnect()
} else {
dnsLookup(host, forceIPv4 ? 4 : 6, (err, address, family) => {
if (err) {
const type = forceIPv4 ? 'IPv4' : 'IPv6'
const error = new Error(`Error while looking up ${type} address for '${host}': ${err}`)
clearTimeout(this._readyTimeout)
error.level = 'client-dns'
this.emit('error', error)
this.emit('close')
return
}
host = address
doConnect()
})
}
} else {
// Custom socket passed in
startTimeout()
if (typeof sock.connecting === 'boolean') {
// net.Socket
if (!sock.connecting) {
// Already connected
onConnect()
}
} else {
// Assume socket/stream is already "connected"
onConnect()
}
}
return this
}
end() {
if (this._sock && isWritable(this._sock)) {
this._protocol.disconnect(DISCONNECT_REASON.BY_APPLICATION)
this._sock.end()
}
return this
}
destroy() {
this._sock && isWritable(this._sock) && this._sock.destroy()
return this
}
exec(cmd, opts, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
if (typeof opts === 'function') {
cb = opts
opts = {}
}
const extraOpts = { allowHalfOpen: opts.allowHalfOpen !== false }
openChannel(this, 'session', extraOpts, (err, chan) => {
if (err) {
cb(err)
return
}
const todo = []
function reqCb(err) {
if (err) {
chan.close()
cb(err)
return
}
if (todo.length) todo.shift()()
}
if (
this.config.allowAgentFwd === true ||
(opts && opts.agentForward === true && this._agent !== undefined)
) {
todo.push(() => reqAgentFwd(chan, reqCb))
}
if (typeof opts === 'object' && opts !== null) {
if (typeof opts.env === 'object' && opts.env !== null) reqEnv(chan, opts.env)
if ((typeof opts.pty === 'object' && opts.pty !== null) || opts.pty === true) {
todo.push(() => reqPty(chan, opts.pty, reqCb))
}
if (
(typeof opts.x11 === 'object' && opts.x11 !== null) ||
opts.x11 === 'number' ||
opts.x11 === true
) {
todo.push(() => reqX11(chan, opts.x11, reqCb))
}
}
todo.push(() => reqExec(chan, cmd, opts, cb))
todo.shift()()
})
return this
}
shell(wndopts, opts, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
if (typeof wndopts === 'function') {
cb = wndopts
wndopts = opts = undefined
} else if (typeof opts === 'function') {
cb = opts
opts = undefined
}
if (wndopts && (wndopts.x11 !== undefined || wndopts.env !== undefined)) {
opts = wndopts
wndopts = undefined
}
openChannel(this, 'session', (err, chan) => {
if (err) {
cb(err)
return
}
const todo = []
function reqCb(err) {
if (err) {
chan.close()
cb(err)
return
}
if (todo.length) todo.shift()()
}
if (
this.config.allowAgentFwd === true ||
(opts && opts.agentForward === true && this._agent !== undefined)
) {
todo.push(() => reqAgentFwd(chan, reqCb))
}
if (wndopts !== false) todo.push(() => reqPty(chan, wndopts, reqCb))
if (typeof opts === 'object' && opts !== null) {
if (typeof opts.env === 'object' && opts.env !== null) reqEnv(chan, opts.env)
if (
(typeof opts.x11 === 'object' && opts.x11 !== null) ||
opts.x11 === 'number' ||
opts.x11 === true
) {
todo.push(() => reqX11(chan, opts.x11, reqCb))
}
}
todo.push(() => reqShell(chan, cb))
todo.shift()()
})
return this
}
subsys(name, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
openChannel(this, 'session', (err, chan) => {
if (err) {
cb(err)
return
}
reqSubsystem(chan, name, (err, stream) => {
if (err) {
cb(err)
return
}
cb(undefined, stream)
})
})
return this
}
forwardIn(bindAddr, bindPort, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
// Send a request for the server to start forwarding TCP connections to us
// on a particular address and port
const wantReply = typeof cb === 'function'
if (wantReply) {
this._callbacks.push((had_err, data) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error(`Unable to bind to ${bindAddr}:${bindPort}`))
return
}
let realPort = bindPort
if (bindPort === 0 && data && data.length >= 4) {
realPort = readUInt32BE(data, 0)
if (!(this._protocol._compatFlags & COMPAT.DYN_RPORT_BUG)) bindPort = realPort
}
this._forwarding[`${bindAddr}:${bindPort}`] = realPort
cb(undefined, realPort)
})
}
this._protocol.tcpipForward(bindAddr, bindPort, wantReply)
return this
}
unforwardIn(bindAddr, bindPort, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
// Send a request to stop forwarding us new connections for a particular
// address and port
const wantReply = typeof cb === 'function'
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(
had_err !== true ? had_err : new Error(`Unable to unbind from ${bindAddr}:${bindPort}`)
)
return
}
delete this._forwarding[`${bindAddr}:${bindPort}`]
cb()
})
}
this._protocol.cancelTcpipForward(bindAddr, bindPort, wantReply)
return this
}
forwardOut(srcIP, srcPort, dstIP, dstPort, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
// Send a request to forward a TCP connection to the server
const cfg = {
srcIP: srcIP,
srcPort: srcPort,
dstIP: dstIP,
dstPort: dstPort
}
if (typeof cb !== 'function') cb = noop
openChannel(this, 'direct-tcpip', cfg, cb)
return this
}
openssh_noMoreSessions(cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
const wantReply = typeof cb === 'function'
if (
!this.config.strictVendor ||
(this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))
) {
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error('Unable to disable future sessions'))
return
}
cb()
})
}
this._protocol.openssh_noMoreSessions(wantReply)
return this
}
if (!wantReply) return this
process.nextTick(
cb,
new Error('strictVendor enabled and server is not OpenSSH or compatible version')
)
return this
}
openssh_forwardInStreamLocal(socketPath, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
const wantReply = typeof cb === 'function'
if (
!this.config.strictVendor ||
(this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))
) {
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error(`Unable to bind to ${socketPath}`))
return
}
this._forwardingUnix[socketPath] = true
cb()
})
}
this._protocol.openssh_streamLocalForward(socketPath, wantReply)
return this
}
if (!wantReply) return this
process.nextTick(
cb,
new Error('strictVendor enabled and server is not OpenSSH or compatible version')
)
return this
}
openssh_unforwardInStreamLocal(socketPath, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
const wantReply = typeof cb === 'function'
if (
!this.config.strictVendor ||
(this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))
) {
if (wantReply) {
this._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error(`Unable to unbind from ${socketPath}`))
return
}
delete this._forwardingUnix[socketPath]
cb()
})
}
this._protocol.openssh_cancelStreamLocalForward(socketPath, wantReply)
return this
}
if (!wantReply) return this
process.nextTick(
cb,
new Error('strictVendor enabled and server is not OpenSSH or compatible version')
)
return this
}
openssh_forwardOutStreamLocal(socketPath, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
if (typeof cb !== 'function') cb = noop
if (
!this.config.strictVendor ||
(this.config.strictVendor && RE_OPENSSH.test(this._remoteVer))
) {
openChannel(this, '[email protected]', { socketPath }, cb)
return this
}
process.nextTick(
cb,
new Error('strictVendor enabled and server is not OpenSSH or compatible version')
)
return this
}
sftp(env, cb) {
if (!this._sock || !isWritable(this._sock)) throw new Error('Not connected')
if (typeof env === 'function') {
cb = env
env = undefined
}
openChannel(this, 'sftp', (err, sftp) => {
if (err) {
cb(err)
return
}
const reqSubsystemCb = (err, sftp_) => {
if (err) {
cb(err)
return
}
function removeListeners() {
sftp.removeListener('ready', onReady)
sftp.removeListener('error', onError)
sftp.removeListener('exit', onExit)
sftp.removeListener('close', onExit)
}
function onReady() {
// TODO: do not remove exit/close in case remote end closes the
// channel abruptly and we need to notify outstanding callbacks
removeListeners()
cb(undefined, sftp)
}
function onError(err) {
removeListeners()
cb(err)
}
function onExit(code, signal) {
removeListeners()
let msg
if (typeof code === 'number')
msg = `Received exit code ${code} while establishing SFTP session`
else if (signal !== undefined)
msg = `Received signal ${signal} while establishing SFTP session`
else msg = 'Received unexpected SFTP session termination'
const err = new Error(msg)
err.code = code
err.signal = signal
cb(err)
}
sftp.on('ready', onReady).on('error', onError).on('exit', onExit).on('close', onExit)
sftp._init()
}
if (typeof env === 'object' && env !== null) {
reqEnv(sftp, env, (err) => {
if (err) {
cb(err)
return
}
reqSubsystem(sftp, 'sftp', reqSubsystemCb)
})
} else {
reqSubsystem(sftp, 'sftp', reqSubsystemCb)
}
})
return this
}
setNoDelay(noDelay) {
if (this._sock && typeof this._sock.setNoDelay === 'function') this._sock.setNoDelay(noDelay)
return this
}
}
function openChannel(self, type, opts, cb) {
// Ask the server to open a channel for some purpose
// (e.g. session (sftp, exec, shell), or forwarding a TCP connection
const initWindow = MAX_WINDOW
const maxPacket = PACKET_SIZE
if (typeof opts === 'function') {
cb = opts
opts = {}
}
const wrapper = (err, stream) => {
cb(err, stream)
}
wrapper.type = type
const localChan = self._chanMgr.add(wrapper)
if (localChan === -1) {
cb(new Error('No free channels available'))
return
}
switch (type) {
case 'session':
case 'sftp':
self._protocol.session(localChan, initWindow, maxPacket)
break
case 'direct-tcpip':
self._protocol.directTcpip(localChan, initWindow, maxPacket, opts)
break
case '[email protected]':
self._protocol.openssh_directStreamLocal(localChan, initWindow, maxPacket, opts)
break
default:
throw new Error(`Unsupported channel type: ${type}`)
}
}
function reqX11(chan, screen, cb) {
// Asks server to start sending us X11 connections
const cfg = {
single: false,
protocol: 'MIT-MAGIC-COOKIE-1',
cookie: undefined,
screen: 0
}
if (typeof screen === 'function') {
cb = screen
} else if (typeof screen === 'object' && screen !== null) {
if (typeof screen.single === 'boolean') cfg.single = screen.single
if (typeof screen.screen === 'number') cfg.screen = screen.screen
if (typeof screen.protocol === 'string') cfg.protocol = screen.protocol
if (typeof screen.cookie === 'string') cfg.cookie = screen.cookie
else if (Buffer.isBuffer(screen.cookie))
cfg.cookie = screen.cookie.hexSlice(0, screen.cookie.length)
}
if (cfg.cookie === undefined) cfg.cookie = randomCookie()
const wantReply = typeof cb === 'function'
if (chan.outgoing.state !== 'open') {
if (wantReply) cb(new Error('Channel is not open'))
return
}
if (wantReply) {
chan._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error('Unable to request X11'))
return
}
chan._hasX11 = true
++chan._client._acceptX11
chan.once('close', () => {
if (chan._client._acceptX11) --chan._client._acceptX11
})
cb()
})
}
chan._client._protocol.x11Forward(chan.outgoing.id, cfg, wantReply)
}
function reqPty(chan, opts, cb) {
let rows = 24
let cols = 80
let width = 640
let height = 480
let term = 'vt100'
let modes = null
if (typeof opts === 'function') {
cb = opts
} else if (typeof opts === 'object' && opts !== null) {
if (typeof opts.rows === 'number') rows = opts.rows
if (typeof opts.cols === 'number') cols = opts.cols
if (typeof opts.width === 'number') width = opts.width
if (typeof opts.height === 'number') height = opts.height
if (typeof opts.term === 'string') term = opts.term
if (typeof opts.modes === 'object') modes = opts.modes
}
const wantReply = typeof cb === 'function'
if (chan.outgoing.state !== 'open') {
if (wantReply) cb(new Error('Channel is not open'))
return
}
if (wantReply) {
chan._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error('Unable to request a pseudo-terminal'))
return
}
cb()
})
}
chan._client._protocol.pty(chan.outgoing.id, rows, cols, height, width, term, modes, wantReply)
}
function reqAgentFwd(chan, cb) {
const wantReply = typeof cb === 'function'
if (chan.outgoing.state !== 'open') {
wantReply && cb(new Error('Channel is not open'))
return
}
if (chan._client._agentFwdEnabled) {
wantReply && cb(false)
return
}
chan._client._agentFwdEnabled = true
chan._callbacks.push((had_err) => {
if (had_err) {
chan._client._agentFwdEnabled = false
if (wantReply) {
cb(had_err !== true ? had_err : new Error('Unable to request agent forwarding'))
}
return
}
if (wantReply) cb()
})
chan._client._protocol.openssh_agentForward(chan.outgoing.id, true)
}
function reqShell(chan, cb) {
if (chan.outgoing.state !== 'open') {
cb(new Error('Channel is not open'))
return
}
chan._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error('Unable to open shell'))
return
}
chan.subtype = 'shell'
cb(undefined, chan)
})
chan._client._protocol.shell(chan.outgoing.id, true)
}
function reqExec(chan, cmd, opts, cb) {
if (chan.outgoing.state !== 'open') {
cb(new Error('Channel is not open'))
return
}
chan._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error('Unable to exec'))
return
}
chan.subtype = 'exec'
chan.allowHalfOpen = opts.allowHalfOpen !== false
cb(undefined, chan)
})
chan._client._protocol.exec(chan.outgoing.id, cmd, true)
}
function reqEnv(chan, env, cb) {
const wantReply = typeof cb === 'function'
if (chan.outgoing.state !== 'open') {
if (wantReply) cb(new Error('Channel is not open'))
return
}
if (wantReply) {
chan._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error('Unable to set environment'))
return
}
cb()
})
}
const keys = Object.keys(env || {})
for (let i = 0; i < keys.length; ++i) {
const key = keys[i]
const val = env[key]
chan._client._protocol.env(chan.outgoing.id, key, val, wantReply)
}
}
function reqSubsystem(chan, name, cb) {
if (chan.outgoing.state !== 'open') {
cb(new Error('Channel is not open'))
return
}
chan._callbacks.push((had_err) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error(`Unable to start subsystem: ${name}`))
return
}
chan.subtype = 'subsystem'
cb(undefined, chan)
})
chan._client._protocol.subsystem(chan.outgoing.id, name, true)
}
// TODO: inline implementation into single call site
function onCHANNEL_OPEN(self, info) {
// The server is trying to open a channel with us, this is usually when
// we asked the server to forward us connections on some port and now they
// are asking us to accept/deny an incoming connection on their side
let localChan = -1
let reason
const accept = () => {
const chanInfo = {
type: info.type,
incoming: {
id: localChan,
window: MAX_WINDOW,
packetSize: PACKET_SIZE,
state: 'open'
},
outgoing: {
id: info.sender,
window: info.window,
packetSize: info.packetSize,
state: 'open'
}
}
const stream = new Channel(self, chanInfo)
self._chanMgr.update(localChan, stream)
self._protocol.channelOpenConfirm(info.sender, localChan, MAX_WINDOW, PACKET_SIZE)
return stream
}
const reject = () => {
if (reason === undefined) {
if (localChan === -1) reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE
else reason = CHANNEL_OPEN_FAILURE.CONNECT_FAILED
}
if (localChan !== -1) self._chanMgr.remove(localChan)
self._protocol.channelOpenFail(info.sender, reason, '')
}
const reserveChannel = () => {
localChan = self._chanMgr.add()
if (localChan === -1) {
reason = CHANNEL_OPEN_FAILURE.RESOURCE_SHORTAGE
if (self.config.debug) {
self.config.debug(
'Client: Automatic rejection of incoming channel open: ' + 'no channels available'
)
}
}
return localChan !== -1
}
const data = info.data
switch (info.type) {
case 'forwarded-tcpip': {
const val = self._forwarding[`${data.destIP}:${data.destPort}`]
if (val !== undefined && reserveChannel()) {
if (data.destPort === 0) data.destPort = val
self.emit('tcp connection', data, accept, reject)
return
}
break
}
case '[email protected]':
if (self._forwardingUnix[data.socketPath] !== undefined && reserveChannel()) {
self.emit('unix connection', data, accept, reject)
return
}
break
case '[email protected]':
if (
self._agentFwdEnabled &&
typeof self._agent.getStream === 'function' &&
reserveChannel()
) {
self._agent.getStream((err, stream) => {
if (err) return reject()
const upstream = accept()
upstream.pipe(stream).pipe(upstream)
})
return
}
break
case 'x11':
if (self._acceptX11 !== 0 && reserveChannel()) {
self.emit('x11', data, accept, reject)
return
}
break
default:
// Automatically reject any unsupported channel open requests
reason = CHANNEL_OPEN_FAILURE.UNKNOWN_CHANNEL_TYPE
if (self.config.debug) {
self.config.debug(
'Client: Automatic rejection of unsupported incoming channel open ' + `type: ${info.type}`
)
}
}
if (reason === undefined) {
reason = CHANNEL_OPEN_FAILURE.ADMINISTRATIVELY_PROHIBITED
if (self.config.debug) {
self.config.debug(
'Client: Automatic rejection of unexpected incoming channel open for: ' + info.type
)
}
}
reject()
}
const randomCookie = (() => {
const buffer = Buffer.allocUnsafe(16)
return () => {
randomFillSync(buffer, 0, 16)
return buffer.hexSlice(0, 16)
}
})()
function makeSimpleAuthHandler(authList) {
if (!Array.isArray(authList)) throw new Error('authList must be an array')
let a = 0
return (authsLeft, partialSuccess, cb) => {
if (a === authList.length) return false
return authList[a++]
}
}
function hostKeysProve(client, keys_, cb) {
if (!client._sock || !isWritable(client._sock)) return
if (typeof cb !== 'function') cb = noop
if (!Array.isArray(keys_)) throw new TypeError('Invalid keys argument type')
const keys = []
for (const key of keys_) {
const parsed = parseKey(key)
if (parsed instanceof Error) throw parsed
keys.push(parsed)
}
if (
!client.config.strictVendor ||
(client.config.strictVendor && RE_OPENSSH.test(client._remoteVer))
) {
client._callbacks.push((had_err, data) => {
if (had_err) {
cb(had_err !== true ? had_err : new Error('Server failed to prove supplied keys'))
return
}
// TODO: move all of this parsing/verifying logic out of the client?
const ret = []
let keyIdx = 0
bufferParser.init(data, 0)
while (bufferParser.avail()) {
if (keyIdx === keys.length) break
const key = keys[keyIdx++]
const keyPublic = key.getPublicSSH()
const sigEntry = bufferParser.readString()
sigParser.init(sigEntry, 0)
const type = sigParser.readString(true)
let value = sigParser.readString()
let algo
if (type !== key.type) {
if (key.type === 'ssh-rsa') {
switch (type) {
case 'rsa-sha2-256':
algo = 'sha256'
break
case 'rsa-sha2-512':
algo = 'sha512'
break
default:
continue
}
} else {
continue
}
}
const sessionID = client._protocol._kex.sessionID
const verifyData = Buffer.allocUnsafe(4 + 29 + 4 + sessionID.length + 4 + keyPublic.length)
let p = 0
writeUInt32BE(verifyData, 29, p)
verifyData.utf8Write('[email protected]', (p += 4), 29)
writeUInt32BE(verifyData, sessionID.length, (p += 29))
bufferCopy(sessionID, verifyData, 0, sessionID.length, (p += 4))
writeUInt32BE(verifyData, keyPublic.length, (p += sessionID.length))
bufferCopy(keyPublic, verifyData, 0, keyPublic.length, (p += 4))
if (!(value = sigSSHToASN1(value, type))) continue
if (key.verify(verifyData, value, algo) === true) ret.push(key)
}
sigParser.clear()
bufferParser.clear()
cb(null, ret)
})
client._protocol.openssh_hostKeysProve(keys)
return
}
process.nextTick(
cb,
new Error('strictVendor enabled and server is not OpenSSH or compatible version')
)
}
function getKeyAlgos(client, key, serverSigAlgs) {
switch (key.type) {
case 'ssh-rsa':
if (client._protocol._compatFlags & COMPAT.IMPLY_RSA_SHA2_SIGALGS) {
if (!Array.isArray(serverSigAlgs)) serverSigAlgs = ['rsa-sha2-256', 'rsa-sha2-512']
else serverSigAlgs = ['rsa-sha2-256', 'rsa-sha2-512', ...serverSigAlgs]
}
if (Array.isArray(serverSigAlgs)) {
if (serverSigAlgs.indexOf('rsa-sha2-256') !== -1) return [['rsa-sha2-256', 'sha256']]
if (serverSigAlgs.indexOf('rsa-sha2-512') !== -1) return [['rsa-sha2-512', 'sha512']]
if (serverSigAlgs.indexOf('ssh-rsa') === -1) return []
}
return [['ssh-rsa', 'sha1']]
}
}
module.exports = Client