From 2f4083ff4f02606d2a5cb82e6f41141dadc1fddc Mon Sep 17 00:00:00 2001 From: Bill Church Date: Mon, 2 Dec 2024 02:19:16 +0000 Subject: [PATCH] feat: support uploading of ssh-rsa private key from client for authentication #381 --- app/socket.js | 36 ++++++++++++++++++++---------------- app/ssh.js | 29 ++++++++--------------------- app/utils.js | 12 ++++-------- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/app/socket.js b/app/socket.js index 5448ba9..0258707 100644 --- a/app/socket.js +++ b/app/socket.js @@ -25,6 +25,8 @@ class WebSSH2Socket extends EventEmitter { authenticated: false, username: null, password: null, + privatekey: null, + keyPassword: null, host: null, port: null, term: null, @@ -110,9 +112,16 @@ class WebSSH2Socket extends EventEmitter { debug(`handleAuthenticate: ${this.socket.id}, %O`, maskSensitiveData(creds)) if (isValidCredentials(creds)) { + // Set term if provided, otherwise use config default this.sessionState.term = validateSshTerm(creds.term) ? creds.term : this.config.ssh.term + + // Map the client's privateKey field to our internal privatekey field if present + if (creds.privateKey) { + creds.privatekey = creds.privateKey + } + this.initializeConnection(creds) } else { debug(`handleAuthenticate: ${this.socket.id}, CREDENTIALS INVALID`) @@ -129,9 +138,8 @@ class WebSSH2Socket extends EventEmitter { maskSensitiveData(creds) ) - // Add private key from config if available + // Add private key from config if available and not provided in creds if (this.config.user.privatekey && !creds.privatekey) { - // eslint-disable-next-line no-param-reassign creds.privatekey = this.config.user.privatekey } @@ -143,6 +151,7 @@ class WebSSH2Socket extends EventEmitter { username: creds.username, password: creds.password, privatekey: creds.privatekey, + keyPassword: creds.keyPassword, host: creds.host, port: creds.port }) @@ -170,21 +179,16 @@ class WebSSH2Socket extends EventEmitter { debug( `initializeConnection: SSH CONNECTION ERROR: ${this.socket.id}, Host: ${creds.host}, Error: ${err.message}` ) - handleError(new SSHConnectionError(`${err.message}`)) - this.socket.emit("ssherror", `${err.message}`) + const errorMessage = + err instanceof SSHConnectionError + ? err.message + : "SSH connection failed" + this.socket.emit("authentication", { + action: "auth_result", + success: false, + message: errorMessage + }) }) - - // Set up password prompt handler - this.ssh.on("password-prompt", (data) => { - this.socket.emit("authentication", { - action: "password_prompt", - message: `Key authentication failed. Please enter password for ${data.username}@${data.host}` - }) - }) - - this.socket.on("password_response", (password) => { - this.ssh.emit("password-response", password) - }) } /** diff --git a/app/ssh.js b/app/ssh.js index 5fd71f1..9bd4e50 100644 --- a/app/ssh.js +++ b/app/ssh.js @@ -30,24 +30,19 @@ class SSHConnection extends EventEmitter { * @returns {boolean} - Whether the key appears to be valid */ validatePrivateKey(key) { - const keyStart = "-----BEGIN RSA PRIVATE KEY-----" - const keyEnd = "-----END RSA PRIVATE KEY-----" - return ( - typeof key === "string" && - key.includes(keyStart) && - key.includes(keyEnd) && - key.trim().length > keyStart.length + keyEnd.length - ) + 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) } /** - * Connects to the SSH server using the provided credentials. - * @param {Object} creds - The credentials object containing host, port, username, and optional password. - * @returns {Promise} - A promise that resolves with the SSH connection instance. + * Attempts to connect using the provided credentials + * @param {Object} creds - The credentials object + * @returns {Promise} - A promise that resolves with the SSH connection */ connect(creds) { - this.creds = creds debug("connect: %O", maskSensitiveData(creds)) + this.creds = creds return new Promise((resolve, reject) => { if (this.conn) { this.conn.end() @@ -58,6 +53,7 @@ class SSHConnection extends EventEmitter { // First try with key authentication if available const sshConfig = this.getSSHConfig(creds, true) + debug("Initial connection config: %O", maskSensitiveData(sshConfig)) this.setupConnectionHandlers(resolve, reject) @@ -80,14 +76,6 @@ class SSHConnection extends EventEmitter { resolve(this.conn) }) - this.conn.on("end", () => { - debug("connect: end") - }) - - this.conn.on("close", () => { - debug("connect: close") - }) - this.conn.on("error", (err) => { debug(`connect: error: ${err.message}`) @@ -114,7 +102,6 @@ class SSHConnection extends EventEmitter { this.setupConnectionHandlers(resolve, reject) this.conn.connect(passwordConfig) } else { - // No password available, emit event to request password debug("No password available, requesting password from client") this.emit("password-prompt", { host: this.creds.host, diff --git a/app/utils.js b/app/utils.js index 5ab7ca3..9470cdc 100644 --- a/app/utils.js +++ b/app/utils.js @@ -87,14 +87,9 @@ function getValidatedPort(portInput) { * - port (number) * AND either: * - password (string) OR - * - privatekey (string) + * - privatekey/privateKey (string) * * @param {Object} creds - The credentials object. - * @param {string} creds.username - The username. - * @param {string} [creds.password] - The password. - * @param {string} [creds.privatekey] - The private key. - * @param {string} creds.host - The host. - * @param {number} creds.port - The port. * @returns {boolean} - Returns true if the credentials are valid, otherwise false. */ function isValidCredentials(creds) { @@ -109,9 +104,10 @@ function isValidCredentials(creds) { return false } - // Must have either password or privatekey + // Must have either password or privatekey/privateKey const hasPassword = typeof creds.password === "string" - const hasPrivateKey = typeof creds.privatekey === "string" + const hasPrivateKey = + typeof creds.privatekey === "string" || typeof creds.privateKey === "string" return hasPassword || hasPrivateKey }