diff --git a/packages/nodes-base/nodes/Ftp/Ftp.node.ts b/packages/nodes-base/nodes/Ftp/Ftp.node.ts index 84bbf0d4ff7fd..5d5520be08eea 100644 --- a/packages/nodes-base/nodes/Ftp/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp/Ftp.node.ts @@ -11,6 +11,7 @@ import type { JsonObject, } from 'n8n-workflow'; import { BINARY_ENCODING, NodeApiError } from 'n8n-workflow'; +import { formatPrivateKey } from '@utils/utilities'; import { createWriteStream } from 'fs'; import { basename, dirname } from 'path'; import type { Readable } from 'stream'; @@ -463,14 +464,22 @@ export class Ftp implements INodeType { const credentials = credential.data as ICredentialDataDecryptedObject; try { const sftp = new sftpClient(); - await sftp.connect({ - host: credentials.host as string, - port: credentials.port as number, - username: credentials.username as string, - password: credentials.password as string, - privateKey: credentials.privateKey as string | undefined, - passphrase: credentials.passphrase as string | undefined, - }); + if (credentials.privateKey) { + await sftp.connect({ + host: credentials.host as string, + port: credentials.port as number, + username: credentials.username as string, + privateKey: formatPrivateKey(credentials.privateKey as string), + passphrase: credentials.passphrase as string | undefined, + }); + } else { + await sftp.connect({ + host: credentials.host as string, + port: credentials.port as number, + username: credentials.username as string, + password: credentials.password as string, + }); + } } catch (error) { return { status: 'Error', @@ -506,14 +515,22 @@ export class Ftp implements INodeType { if (protocol === 'sftp') { sftp = new sftpClient(); - await sftp.connect({ - host: credentials.host as string, - port: credentials.port as number, - username: credentials.username as string, - password: credentials.password as string, - privateKey: credentials.privateKey as string | undefined, - passphrase: credentials.passphrase as string | undefined, - }); + if (credentials.privateKey) { + await sftp.connect({ + host: credentials.host as string, + port: credentials.port as number, + username: credentials.username as string, + privateKey: formatPrivateKey(credentials.privateKey as string), + passphrase: credentials.passphrase as string | undefined, + }); + } else { + await sftp.connect({ + host: credentials.host as string, + port: credentials.port as number, + username: credentials.username as string, + password: credentials.password as string, + }); + } } else { ftp = new ftpClient(); await ftp.connect({ diff --git a/packages/nodes-base/nodes/Google/GenericFunctions.ts b/packages/nodes-base/nodes/Google/GenericFunctions.ts index 4ee5f0dc4c479..3fece1e1db94c 100644 --- a/packages/nodes-base/nodes/Google/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/GenericFunctions.ts @@ -5,6 +5,8 @@ import type { OptionsWithUri } from 'request'; import moment from 'moment-timezone'; import * as jwt from 'jsonwebtoken'; +import { formatPrivateKey } from '@utils/utilities'; + const googleServiceAccountScopes = { bigquery: ['https://www.googleapis.com/auth/bigquery'], books: ['https://www.googleapis.com/auth/books'], @@ -63,7 +65,7 @@ export async function getGoogleAccessToken( const scopes = googleServiceAccountScopes[service]; - const privateKey = (credentials.privateKey as string).replace(/\\n/g, '\n').trim(); + const privateKey = formatPrivateKey(credentials.privateKey as string); credentials.email = ((credentials.email as string) || '').trim(); const now = moment().unix(); diff --git a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts index 420fcc64e05d9..71c4cbd7d2e7e 100644 --- a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts +++ b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts @@ -10,6 +10,7 @@ import type { } from 'n8n-workflow'; import * as mqtt from 'mqtt'; +import { formatPrivateKey } from '@utils/utilities'; export class Mqtt implements INodeType { description: INodeTypeDescription = { @@ -118,9 +119,9 @@ export class Mqtt implements INodeType { (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; const clean = credentials.clean as boolean; const ssl = credentials.ssl as boolean; - const ca = credentials.ca as string; - const cert = credentials.cert as string; - const key = credentials.key as string; + const ca = formatPrivateKey(credentials.ca as string); + const cert = formatPrivateKey(credentials.cert as string); + const key = formatPrivateKey(credentials.key as string); const rejectUnauthorized = credentials.rejectUnauthorized as boolean; let client: mqtt.MqttClient; diff --git a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts index 26880e3dfb3de..f6d4ed1e21275 100644 --- a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts +++ b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts @@ -8,6 +8,7 @@ import type { import { NodeOperationError } from 'n8n-workflow'; import * as mqtt from 'mqtt'; +import { formatPrivateKey } from '@utils/utilities'; export class MqttTrigger implements INodeType { description: INodeTypeDescription = { @@ -101,9 +102,9 @@ export class MqttTrigger implements INodeType { (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; const clean = credentials.clean as boolean; const ssl = credentials.ssl as boolean; - const ca = credentials.ca as string; - const cert = credentials.cert as string; - const key = credentials.key as string; + const ca = formatPrivateKey(credentials.ca as string); + const cert = formatPrivateKey(credentials.cert as string); + const key = formatPrivateKey(credentials.key as string); const rejectUnauthorized = credentials.rejectUnauthorized as boolean; let client: mqtt.MqttClient; diff --git a/packages/nodes-base/nodes/MySql/v2/transport/index.ts b/packages/nodes-base/nodes/MySql/v2/transport/index.ts index af40aa5c82375..aaef1c2172939 100644 --- a/packages/nodes-base/nodes/MySql/v2/transport/index.ts +++ b/packages/nodes-base/nodes/MySql/v2/transport/index.ts @@ -1,10 +1,10 @@ import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow'; +import { formatPrivateKey } from '@utils/utilities'; import mysql2 from 'mysql2/promise'; import type { Client, ConnectConfig } from 'ssh2'; -import { rm, writeFile } from 'fs/promises'; +import { rm } from 'fs/promises'; -import { file } from 'tmp-promise'; import type { Mysql2Pool } from '../helpers/interfaces'; async function createSshConnectConfig(credentials: IDataObject) { @@ -16,14 +16,11 @@ async function createSshConnectConfig(credentials: IDataObject) { password: credentials.sshPassword as string, } as ConnectConfig; } else { - const { path } = await file({ prefix: 'n8n-ssh-' }); - await writeFile(path, credentials.privateKey as string); - const options: ConnectConfig = { - host: credentials.host as string, - username: credentials.username as string, - port: credentials.port as number, - privateKey: path, + host: credentials.sshHost as string, + username: credentials.sshUser as string, + port: credentials.sshPort as number, + privateKey: formatPrivateKey(credentials.privateKey as string), }; if (credentials.passphrase) { @@ -63,12 +60,12 @@ export async function createPool( baseCredentials.ssl = {}; if (caCertificate) { - baseCredentials.ssl.ca = caCertificate; + baseCredentials.ssl.ca = formatPrivateKey(caCertificate as string); } if (clientCertificate || clientPrivateKey) { - baseCredentials.ssl.cert = clientCertificate; - baseCredentials.ssl.key = clientPrivateKey; + baseCredentials.ssl.cert = formatPrivateKey(clientCertificate as string); + baseCredentials.ssl.key = formatPrivateKey(clientPrivateKey as string); } } diff --git a/packages/nodes-base/nodes/Postgres/v2/transport/index.ts b/packages/nodes-base/nodes/Postgres/v2/transport/index.ts index 4fe375ceff233..06083dec3a2d3 100644 --- a/packages/nodes-base/nodes/Postgres/v2/transport/index.ts +++ b/packages/nodes-base/nodes/Postgres/v2/transport/index.ts @@ -1,4 +1,5 @@ import type { IDataObject } from 'n8n-workflow'; +import { formatPrivateKey } from '@utils/utilities'; import { Client } from 'ssh2'; import type { ConnectConfig } from 'ssh2'; @@ -8,8 +9,7 @@ import { createServer } from 'net'; import pgPromise from 'pg-promise'; -import { rm, writeFile } from 'fs/promises'; -import { file } from 'tmp-promise'; +import { rm } from 'fs/promises'; import type { PgpDatabase } from '../helpers/interfaces'; @@ -22,14 +22,11 @@ async function createSshConnectConfig(credentials: IDataObject) { password: credentials.sshPassword as string, } as ConnectConfig; } else { - const { path } = await file({ prefix: 'n8n-ssh-' }); - await writeFile(path, credentials.privateKey as string); - const options: ConnectConfig = { - host: credentials.host as string, - username: credentials.username as string, - port: credentials.port as number, - privateKey: path, + host: credentials.sshHost as string, + username: credentials.sshUser as string, + port: credentials.sshPort as number, + privateKey: formatPrivateKey(credentials.privateKey as string), }; if (credentials.passphrase) { diff --git a/packages/nodes-base/nodes/RabbitMQ/GenericFunctions.ts b/packages/nodes-base/nodes/RabbitMQ/GenericFunctions.ts index 8f925ce0940ce..f268c01bc2608 100644 --- a/packages/nodes-base/nodes/RabbitMQ/GenericFunctions.ts +++ b/packages/nodes-base/nodes/RabbitMQ/GenericFunctions.ts @@ -1,5 +1,6 @@ import type { IDataObject, IExecuteFunctions, ITriggerFunctions } from 'n8n-workflow'; import { sleep } from 'n8n-workflow'; +import { formatPrivateKey } from '@utils/utilities'; import * as amqplib from 'amqplib'; @@ -20,10 +21,17 @@ export async function rabbitmqConnect( if (credentials.ssl === true) { credentialData.protocol = 'amqps'; - optsData.ca = credentials.ca === '' ? undefined : [Buffer.from(credentials.ca as string)]; + optsData.ca = + credentials.ca === '' ? undefined : [Buffer.from(formatPrivateKey(credentials.ca as string))]; if (credentials.passwordless === true) { - optsData.cert = credentials.cert === '' ? undefined : Buffer.from(credentials.cert as string); - optsData.key = credentials.key === '' ? undefined : Buffer.from(credentials.key as string); + optsData.cert = + credentials.cert === '' + ? undefined + : Buffer.from(formatPrivateKey(credentials.cert as string)); + optsData.key = + credentials.key === '' + ? undefined + : Buffer.from(formatPrivateKey(credentials.key as string)); optsData.passphrase = credentials.passphrase === '' ? undefined : credentials.passphrase; optsData.credentials = amqplib.credentials.external(); } diff --git a/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts b/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts index a0bdd74d0597b..1c093f8afe95f 100644 --- a/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts +++ b/packages/nodes-base/nodes/RabbitMQ/RabbitMQ.node.ts @@ -14,6 +14,7 @@ import type { import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import { rabbitmqConnectExchange, rabbitmqConnectQueue } from './GenericFunctions'; +import { formatPrivateKey } from '@utils/utilities'; export class RabbitMQ implements INodeType { description: INodeTypeDescription = { @@ -376,12 +377,18 @@ export class RabbitMQ implements INodeType { credentialData.protocol = 'amqps'; optsData.ca = - credentials.ca === '' ? undefined : [Buffer.from(credentials.ca as string)]; + credentials.ca === '' + ? undefined + : [Buffer.from(formatPrivateKey(credentials.ca as string))]; if (credentials.passwordless === true) { optsData.cert = - credentials.cert === '' ? undefined : Buffer.from(credentials.cert as string); + credentials.cert === '' + ? undefined + : Buffer.from(formatPrivateKey(credentials.cert as string)); optsData.key = - credentials.key === '' ? undefined : Buffer.from(credentials.key as string); + credentials.key === '' + ? undefined + : Buffer.from(formatPrivateKey(credentials.key as string)); optsData.passphrase = credentials.passphrase === '' ? undefined : credentials.passphrase; optsData.credentials = amqplib.credentials.external(); diff --git a/packages/nodes-base/nodes/Ssh/Ssh.node.ts b/packages/nodes-base/nodes/Ssh/Ssh.node.ts index 7f8ac5aedf9c8..b6113498954df 100644 --- a/packages/nodes-base/nodes/Ssh/Ssh.node.ts +++ b/packages/nodes-base/nodes/Ssh/Ssh.node.ts @@ -10,6 +10,8 @@ import type { } from 'n8n-workflow'; import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow'; +import { formatPrivateKey } from '@utils/utilities'; + import { rm, writeFile } from 'fs/promises'; import { file as tmpFile } from 'tmp-promise'; @@ -47,14 +49,6 @@ async function resolveHomeDir( return path; } -function sanitizePrivateKey(privateKey: string) { - const [openSshKey, bodySshKey, endSshKey] = privateKey - .split('-----') - .filter((item) => item !== ''); - - return `-----${openSshKey}-----\n${bodySshKey.replace(/ /g, '\n')}\n-----${endSshKey}-----`; -} - export class Ssh implements INodeType { description: INodeTypeDescription = { displayName: 'SSH', @@ -304,15 +298,11 @@ export class Ssh implements INodeType { password: credentials.password as string, }); } else { - const { path } = await tmpFile({ prefix: 'n8n-ssh-' }); - temporaryFiles.push(path); - await writeFile(path, sanitizePrivateKey(credentials.privateKey as string)); - const options: Config = { host: credentials.host as string, username: credentials.username as string, port: credentials.port as number, - privateKey: path, + privateKey: formatPrivateKey(credentials.privateKey as string), }; if (credentials.passphrase) { @@ -364,16 +354,11 @@ export class Ssh implements INodeType { }); } else if (authentication === 'privateKey') { const credentials = await this.getCredentials('sshPrivateKey'); - - const { path } = await tmpFile({ prefix: 'n8n-ssh-' }); - temporaryFiles.push(path); - await writeFile(path, sanitizePrivateKey(credentials.privateKey as string)); - const options: Config = { host: credentials.host as string, username: credentials.username as string, port: credentials.port as number, - privateKey: path, + privateKey: formatPrivateKey(credentials.privateKey as string), }; if (credentials.passphrase) { diff --git a/packages/nodes-base/utils/utilities.ts b/packages/nodes-base/utils/utilities.ts index 6de1b90978889..d94a1b6dc5164 100644 --- a/packages/nodes-base/utils/utilities.ts +++ b/packages/nodes-base/utils/utilities.ts @@ -217,6 +217,36 @@ export const keysToLowercase = (headers: T) => { }, {} as IDataObject); }; +/** + * Formats a private key by removing unnecessary whitespace and adding line breaks. + * @param privateKey - The private key to format. + * @returns The formatted private key. + */ +export function formatPrivateKey(privateKey: string): string { + if (/\n/.test(privateKey)) { + return privateKey; + } + let formattedPrivateKey = ''; + const parts = privateKey.split('-----').filter((item) => item !== ''); + parts.forEach((part) => { + const regex = /(PRIVATE KEY|CERTIFICATE)/; + if (regex.test(part)) { + formattedPrivateKey += `-----${part}-----`; + } else { + const passRegex = /Proc-Type|DEK-Info/; + if (passRegex.test(part)) { + part = part.replace(/:\s+/g, ':'); + formattedPrivateKey += part.replace(/\\n/g, '\n'); + formattedPrivateKey += part.replace(/\s+/g, '\n'); + } else { + formattedPrivateKey += part.replace(/\\n/g, '\n'); + formattedPrivateKey += part.replace(/\s+/g, '\n'); + } + } + }); + return formattedPrivateKey; +} + /** * @TECH_DEBT Explore replacing with handlebars */