feat: allow passphrase encrypted ssh keys from client #381
This commit is contained in:
parent
b4b74297ea
commit
056e87b40d
2 changed files with 99 additions and 14 deletions
89
app/ssh.js
89
app/ssh.js
|
@ -25,14 +25,19 @@ class SSHConnection extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the format of an RSA private key
|
* Validates the format of an RSA private key, supporting both standard and encrypted keys
|
||||||
* @param {string} key - The private key string to validate
|
* @param {string} key - The private key string to validate
|
||||||
* @returns {boolean} - Whether the key appears to be valid
|
* @returns {boolean} - Whether the key appears to be valid
|
||||||
*/
|
*/
|
||||||
validatePrivateKey(key) {
|
validatePrivateKey(key) {
|
||||||
const keyPattern =
|
// Pattern for standard RSA private key
|
||||||
/^-----BEGIN (?:RSA )?PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END (?:RSA )?PRIVATE KEY-----\r?\n?$/
|
const standardKeyPattern = /^-----BEGIN (?:RSA )?PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END (?:RSA )?PRIVATE KEY-----\r?\n?$/
|
||||||
return keyPattern.test(key)
|
|
||||||
|
// Pattern for encrypted RSA private key
|
||||||
|
const encryptedKeyPattern = /^-----BEGIN RSA PRIVATE KEY-----\r?\n(?:Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: ([^\r\n]+)\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)\r?\n-----END RSA PRIVATE KEY-----\r?\n?$/
|
||||||
|
|
||||||
|
// Test for either standard or encrypted key format
|
||||||
|
return standardKeyPattern.test(key) || encryptedKeyPattern.test(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,7 +81,7 @@ class SSHConnection extends EventEmitter {
|
||||||
resolve(this.conn)
|
resolve(this.conn)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.conn.on("error", (err) => {
|
this.conn.on("error", err => {
|
||||||
debug(`connect: error: ${err.message}`)
|
debug(`connect: error: ${err.message}`)
|
||||||
|
|
||||||
// Check if this is an authentication error and we haven't exceeded max attempts
|
// Check if this is an authentication error and we haven't exceeded max attempts
|
||||||
|
@ -109,7 +114,7 @@ class SSHConnection extends EventEmitter {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for password response one time
|
// Listen for password response one time
|
||||||
this.once("password-response", (password) => {
|
this.once("password-response", password => {
|
||||||
this.creds.password = password
|
this.creds.password = password
|
||||||
const newConfig = this.getSSHConfig(this.creds, false)
|
const newConfig = this.getSSHConfig(this.creds, false)
|
||||||
this.setupConnectionHandlers(resolve, reject)
|
this.setupConnectionHandlers(resolve, reject)
|
||||||
|
@ -140,9 +145,69 @@ class SSHConnection extends EventEmitter {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles keyboard-interactive authentication prompts.
|
||||||
|
* @param {string} name - The name of the authentication request.
|
||||||
|
* @param {string} instructions - The instructions for the keyboard-interactive prompt.
|
||||||
|
* @param {string} lang - The language of the prompt.
|
||||||
|
* @param {Array<Object>} prompts - The list of prompts provided by the server.
|
||||||
|
* @param {Function} finish - The callback to complete the keyboard-interactive authentication.
|
||||||
|
*/
|
||||||
|
|
||||||
|
handleKeyboardInteractive(name, instructions, lang, prompts, finish) {
|
||||||
|
debug("handleKeyboardInteractive: Keyboard-interactive auth %O", prompts)
|
||||||
|
|
||||||
|
// Check if we should always send prompts to the client
|
||||||
|
if (this.config.ssh.alwaysSendKeyboardInteractivePrompts) {
|
||||||
|
this.sendPromptsToClient(name, instructions, prompts, finish)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = []
|
||||||
|
let shouldSendToClient = false
|
||||||
|
|
||||||
|
for (let i = 0; i < prompts.length; i += 1) {
|
||||||
|
if (
|
||||||
|
prompts[i].prompt.toLowerCase().includes("password") &&
|
||||||
|
this.creds.password
|
||||||
|
) {
|
||||||
|
responses.push(this.creds.password)
|
||||||
|
} else {
|
||||||
|
shouldSendToClient = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSendToClient) {
|
||||||
|
this.sendPromptsToClient(name, instructions, prompts, finish)
|
||||||
|
} else {
|
||||||
|
finish(responses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends prompts to the client for keyboard-interactive authentication.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the authentication method.
|
||||||
|
* @param {string} instructions - The instructions for the authentication.
|
||||||
|
* @param {Array<{ prompt: string, echo: boolean }>} prompts - The prompts to be sent to the client.
|
||||||
|
* @param {Function} finish - The callback function to be called when the client responds.
|
||||||
|
*/
|
||||||
|
sendPromptsToClient(name, instructions, prompts, finish) {
|
||||||
|
this.emit("keyboard-interactive", {
|
||||||
|
name: name,
|
||||||
|
instructions: instructions,
|
||||||
|
prompts: prompts.map(p => ({ prompt: p.prompt, echo: p.echo }))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.once("keyboard-interactive-response", responses => {
|
||||||
|
finish(responses)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the SSH configuration object based on credentials.
|
* Generates the SSH configuration object based on credentials.
|
||||||
* @param {Object} creds - The credentials object containing host, port, username, and optional password.
|
* @param {Object} creds - The credentials object containing host, port, username, and optional password/privateKey/passphrase.
|
||||||
* @param {boolean} useKey - Whether to attempt key authentication
|
* @param {boolean} useKey - Whether to attempt key authentication
|
||||||
* @returns {Object} - The SSH configuration object.
|
* @returns {Object} - The SSH configuration object.
|
||||||
*/
|
*/
|
||||||
|
@ -163,10 +228,18 @@ class SSHConnection extends EventEmitter {
|
||||||
if (useKey && (creds.privateKey || this.config.user.privateKey)) {
|
if (useKey && (creds.privateKey || this.config.user.privateKey)) {
|
||||||
debug("Using private key authentication")
|
debug("Using private key authentication")
|
||||||
const privateKey = creds.privateKey || this.config.user.privateKey
|
const privateKey = creds.privateKey || this.config.user.privateKey
|
||||||
|
|
||||||
if (!this.validatePrivateKey(privateKey)) {
|
if (!this.validatePrivateKey(privateKey)) {
|
||||||
throw new SSHConnectionError("Invalid private key format")
|
throw new SSHConnectionError("Invalid private key format")
|
||||||
}
|
}
|
||||||
|
|
||||||
config.privateKey = privateKey
|
config.privateKey = privateKey
|
||||||
|
|
||||||
|
// Add passphrase if provided
|
||||||
|
if (creds.passphrase) {
|
||||||
|
debug("Passphrase provided for private key")
|
||||||
|
config.passphrase = creds.passphrase
|
||||||
|
}
|
||||||
} else if (creds.password) {
|
} else if (creds.password) {
|
||||||
debug("Using password authentication")
|
debug("Using password authentication")
|
||||||
config.password = creds.password
|
config.password = creds.password
|
||||||
|
@ -234,7 +307,7 @@ class SSHConnection extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (envVars) {
|
if (envVars) {
|
||||||
Object.keys(envVars).forEach((key) => {
|
Object.keys(envVars).forEach(key => {
|
||||||
env[key] = envVars[key]
|
env[key] = envVars[key]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
24
app/utils.js
24
app/utils.js
|
@ -87,7 +87,7 @@ function getValidatedPort(portInput) {
|
||||||
* - port (number)
|
* - port (number)
|
||||||
* AND either:
|
* AND either:
|
||||||
* - password (string) OR
|
* - password (string) OR
|
||||||
* - privateKey/privateKey (string)
|
* - privateKey (string) with optional passphrase (string)
|
||||||
*
|
*
|
||||||
* @param {Object} creds - The credentials object.
|
* @param {Object} creds - The credentials object.
|
||||||
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
|
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
|
||||||
|
@ -104,12 +104,15 @@ function isValidCredentials(creds) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must have either password or privateKey/privateKey
|
// Must have either password or privateKey
|
||||||
const hasPassword = typeof creds.password === "string"
|
const hasPassword = typeof creds.password === "string"
|
||||||
const hasPrivateKey =
|
const hasPrivateKey = typeof creds.privateKey === "string"
|
||||||
typeof creds.privateKey === "string" || typeof creds.privateKey === "string"
|
|
||||||
|
|
||||||
return hasPassword || hasPrivateKey
|
// Passphrase is optional but must be string if provided
|
||||||
|
const hasValidPassphrase =
|
||||||
|
!creds.passphrase || typeof creds.passphrase === "string"
|
||||||
|
|
||||||
|
return (hasPassword || hasPrivateKey) && hasValidPassphrase
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -185,7 +188,16 @@ function modifyHtml(html, config) {
|
||||||
* @returns {Object} The masked object
|
* @returns {Object} The masked object
|
||||||
*/
|
*/
|
||||||
function maskSensitiveData(obj, options) {
|
function maskSensitiveData(obj, options) {
|
||||||
const defaultOptions = {}
|
const defaultOptions = {
|
||||||
|
properties: [
|
||||||
|
"password",
|
||||||
|
"privateKey",
|
||||||
|
"passphrase",
|
||||||
|
"key",
|
||||||
|
"secret",
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
debug("maskSensitiveData")
|
debug("maskSensitiveData")
|
||||||
|
|
||||||
const maskingOptions = Object.assign({}, defaultOptions, options || {})
|
const maskingOptions = Object.assign({}, defaultOptions, options || {})
|
||||||
|
|
Loading…
Reference in a new issue