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
|
||||
* @returns {boolean} - Whether the key appears to be valid
|
||||
*/
|
||||
validatePrivateKey(key) {
|
||||
const keyPattern =
|
||||
/^-----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 standard RSA private key
|
||||
const standardKeyPattern = /^-----BEGIN (?:RSA )?PRIVATE KEY-----\r?\n([A-Za-z0-9+/=\r\n]+)\r?\n-----END (?:RSA )?PRIVATE KEY-----\r?\n?$/
|
||||
|
||||
// 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)
|
||||
})
|
||||
|
||||
this.conn.on("error", (err) => {
|
||||
this.conn.on("error", err => {
|
||||
debug(`connect: error: ${err.message}`)
|
||||
|
||||
// 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
|
||||
this.once("password-response", (password) => {
|
||||
this.once("password-response", password => {
|
||||
this.creds.password = password
|
||||
const newConfig = this.getSSHConfig(this.creds, false)
|
||||
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.
|
||||
* @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
|
||||
* @returns {Object} - The SSH configuration object.
|
||||
*/
|
||||
|
@ -163,10 +228,18 @@ class SSHConnection extends EventEmitter {
|
|||
if (useKey && (creds.privateKey || this.config.user.privateKey)) {
|
||||
debug("Using private key authentication")
|
||||
const privateKey = creds.privateKey || this.config.user.privateKey
|
||||
|
||||
if (!this.validatePrivateKey(privateKey)) {
|
||||
throw new SSHConnectionError("Invalid private key format")
|
||||
}
|
||||
|
||||
config.privateKey = privateKey
|
||||
|
||||
// Add passphrase if provided
|
||||
if (creds.passphrase) {
|
||||
debug("Passphrase provided for private key")
|
||||
config.passphrase = creds.passphrase
|
||||
}
|
||||
} else if (creds.password) {
|
||||
debug("Using password authentication")
|
||||
config.password = creds.password
|
||||
|
@ -234,7 +307,7 @@ class SSHConnection extends EventEmitter {
|
|||
}
|
||||
|
||||
if (envVars) {
|
||||
Object.keys(envVars).forEach((key) => {
|
||||
Object.keys(envVars).forEach(key => {
|
||||
env[key] = envVars[key]
|
||||
})
|
||||
}
|
||||
|
|
24
app/utils.js
24
app/utils.js
|
@ -87,7 +87,7 @@ function getValidatedPort(portInput) {
|
|||
* - port (number)
|
||||
* AND either:
|
||||
* - password (string) OR
|
||||
* - privateKey/privateKey (string)
|
||||
* - privateKey (string) with optional passphrase (string)
|
||||
*
|
||||
* @param {Object} creds - The credentials object.
|
||||
* @returns {boolean} - Returns true if the credentials are valid, otherwise false.
|
||||
|
@ -104,12 +104,15 @@ function isValidCredentials(creds) {
|
|||
return false
|
||||
}
|
||||
|
||||
// Must have either password or privateKey/privateKey
|
||||
// Must have either password or privateKey
|
||||
const hasPassword = typeof creds.password === "string"
|
||||
const hasPrivateKey =
|
||||
typeof creds.privateKey === "string" || typeof creds.privateKey === "string"
|
||||
const hasPrivateKey = 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
|
||||
*/
|
||||
function maskSensitiveData(obj, options) {
|
||||
const defaultOptions = {}
|
||||
const defaultOptions = {
|
||||
properties: [
|
||||
"password",
|
||||
"privateKey",
|
||||
"passphrase",
|
||||
"key",
|
||||
"secret",
|
||||
"token"
|
||||
]
|
||||
}
|
||||
debug("maskSensitiveData")
|
||||
|
||||
const maskingOptions = Object.assign({}, defaultOptions, options || {})
|
||||
|
|
Loading…
Reference in a new issue