fix: Fix issue with key based credentials not being read correctly (#6824)

This commit is contained in:
Jon 2023-08-09 12:30:53 +01:00 committed by GitHub
parent 6553d92c7c
commit db21a8db75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 114 additions and 69 deletions

View file

@ -11,6 +11,7 @@ import type {
JsonObject, JsonObject,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { BINARY_ENCODING, NodeApiError } from 'n8n-workflow'; import { BINARY_ENCODING, NodeApiError } from 'n8n-workflow';
import { formatPrivateKey } from '@utils/utilities';
import { createWriteStream } from 'fs'; import { createWriteStream } from 'fs';
import { basename, dirname } from 'path'; import { basename, dirname } from 'path';
import type { Readable } from 'stream'; import type { Readable } from 'stream';
@ -463,14 +464,22 @@ export class Ftp implements INodeType {
const credentials = credential.data as ICredentialDataDecryptedObject; const credentials = credential.data as ICredentialDataDecryptedObject;
try { try {
const sftp = new sftpClient(); const sftp = new sftpClient();
await sftp.connect({ if (credentials.privateKey) {
host: credentials.host as string, await sftp.connect({
port: credentials.port as number, host: credentials.host as string,
username: credentials.username as string, port: credentials.port as number,
password: credentials.password as string, username: credentials.username as string,
privateKey: credentials.privateKey as string | undefined, privateKey: formatPrivateKey(credentials.privateKey as string),
passphrase: credentials.passphrase as string | undefined, 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) { } catch (error) {
return { return {
status: 'Error', status: 'Error',
@ -506,14 +515,22 @@ export class Ftp implements INodeType {
if (protocol === 'sftp') { if (protocol === 'sftp') {
sftp = new sftpClient(); sftp = new sftpClient();
await sftp.connect({ if (credentials.privateKey) {
host: credentials.host as string, await sftp.connect({
port: credentials.port as number, host: credentials.host as string,
username: credentials.username as string, port: credentials.port as number,
password: credentials.password as string, username: credentials.username as string,
privateKey: credentials.privateKey as string | undefined, privateKey: formatPrivateKey(credentials.privateKey as string),
passphrase: credentials.passphrase as string | undefined, 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 { } else {
ftp = new ftpClient(); ftp = new ftpClient();
await ftp.connect({ await ftp.connect({

View file

@ -11,6 +11,8 @@ import type { OptionsWithUri } from 'request';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
import { formatPrivateKey } from '@utils/utilities';
const googleServiceAccountScopes = { const googleServiceAccountScopes = {
bigquery: ['https://www.googleapis.com/auth/bigquery'], bigquery: ['https://www.googleapis.com/auth/bigquery'],
books: ['https://www.googleapis.com/auth/books'], books: ['https://www.googleapis.com/auth/books'],
@ -69,7 +71,7 @@ export async function getGoogleAccessToken(
const scopes = googleServiceAccountScopes[service]; 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(); credentials.email = ((credentials.email as string) || '').trim();
const now = moment().unix(); const now = moment().unix();

View file

@ -10,6 +10,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as mqtt from 'mqtt'; import * as mqtt from 'mqtt';
import { formatPrivateKey } from '@utils/utilities';
export class Mqtt implements INodeType { export class Mqtt implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -118,9 +119,9 @@ export class Mqtt implements INodeType {
(credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`;
const clean = credentials.clean as boolean; const clean = credentials.clean as boolean;
const ssl = credentials.ssl as boolean; const ssl = credentials.ssl as boolean;
const ca = credentials.ca as string; const ca = formatPrivateKey(credentials.ca as string);
const cert = credentials.cert as string; const cert = formatPrivateKey(credentials.cert as string);
const key = credentials.key as string; const key = formatPrivateKey(credentials.key as string);
const rejectUnauthorized = credentials.rejectUnauthorized as boolean; const rejectUnauthorized = credentials.rejectUnauthorized as boolean;
let client: mqtt.MqttClient; let client: mqtt.MqttClient;

View file

@ -8,6 +8,7 @@ import type {
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
import * as mqtt from 'mqtt'; import * as mqtt from 'mqtt';
import { formatPrivateKey } from '@utils/utilities';
export class MqttTrigger implements INodeType { export class MqttTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -101,9 +102,9 @@ export class MqttTrigger implements INodeType {
(credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`; (credentials.clientId as string) || `mqttjs_${Math.random().toString(16).substr(2, 8)}`;
const clean = credentials.clean as boolean; const clean = credentials.clean as boolean;
const ssl = credentials.ssl as boolean; const ssl = credentials.ssl as boolean;
const ca = credentials.ca as string; const ca = formatPrivateKey(credentials.ca as string);
const cert = credentials.cert as string; const cert = formatPrivateKey(credentials.cert as string);
const key = credentials.key as string; const key = formatPrivateKey(credentials.key as string);
const rejectUnauthorized = credentials.rejectUnauthorized as boolean; const rejectUnauthorized = credentials.rejectUnauthorized as boolean;
let client: mqtt.MqttClient; let client: mqtt.MqttClient;

View file

@ -1,10 +1,10 @@
import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow'; import type { ICredentialDataDecryptedObject, IDataObject } from 'n8n-workflow';
import { formatPrivateKey } from '@utils/utilities';
import mysql2 from 'mysql2/promise'; import mysql2 from 'mysql2/promise';
import type { Client, ConnectConfig } from 'ssh2'; 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'; import type { Mysql2Pool } from '../helpers/interfaces';
async function createSshConnectConfig(credentials: IDataObject) { async function createSshConnectConfig(credentials: IDataObject) {
@ -16,14 +16,11 @@ async function createSshConnectConfig(credentials: IDataObject) {
password: credentials.sshPassword as string, password: credentials.sshPassword as string,
} as ConnectConfig; } as ConnectConfig;
} else { } else {
const { path } = await file({ prefix: 'n8n-ssh-' });
await writeFile(path, credentials.privateKey as string);
const options: ConnectConfig = { const options: ConnectConfig = {
host: credentials.host as string, host: credentials.sshHost as string,
username: credentials.username as string, username: credentials.sshUser as string,
port: credentials.port as number, port: credentials.sshPort as number,
privateKey: path, privateKey: formatPrivateKey(credentials.privateKey as string),
}; };
if (credentials.passphrase) { if (credentials.passphrase) {
@ -63,12 +60,12 @@ export async function createPool(
baseCredentials.ssl = {}; baseCredentials.ssl = {};
if (caCertificate) { if (caCertificate) {
baseCredentials.ssl.ca = caCertificate; baseCredentials.ssl.ca = formatPrivateKey(caCertificate as string);
} }
if (clientCertificate || clientPrivateKey) { if (clientCertificate || clientPrivateKey) {
baseCredentials.ssl.cert = clientCertificate; baseCredentials.ssl.cert = formatPrivateKey(clientCertificate as string);
baseCredentials.ssl.key = clientPrivateKey; baseCredentials.ssl.key = formatPrivateKey(clientPrivateKey as string);
} }
} }

View file

@ -1,4 +1,5 @@
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { formatPrivateKey } from '@utils/utilities';
import { Client } from 'ssh2'; import { Client } from 'ssh2';
import type { ConnectConfig } from 'ssh2'; import type { ConnectConfig } from 'ssh2';
@ -8,8 +9,7 @@ import { createServer } from 'net';
import pgPromise from 'pg-promise'; import pgPromise from 'pg-promise';
import { rm, writeFile } from 'fs/promises'; import { rm } from 'fs/promises';
import { file } from 'tmp-promise';
import type { PgpDatabase } from '../helpers/interfaces'; import type { PgpDatabase } from '../helpers/interfaces';
@ -22,14 +22,11 @@ async function createSshConnectConfig(credentials: IDataObject) {
password: credentials.sshPassword as string, password: credentials.sshPassword as string,
} as ConnectConfig; } as ConnectConfig;
} else { } else {
const { path } = await file({ prefix: 'n8n-ssh-' });
await writeFile(path, credentials.privateKey as string);
const options: ConnectConfig = { const options: ConnectConfig = {
host: credentials.host as string, host: credentials.sshHost as string,
username: credentials.username as string, username: credentials.sshUser as string,
port: credentials.port as number, port: credentials.sshPort as number,
privateKey: path, privateKey: formatPrivateKey(credentials.privateKey as string),
}; };
if (credentials.passphrase) { if (credentials.passphrase) {

View file

@ -1,5 +1,6 @@
import type { IDataObject, IExecuteFunctions, ITriggerFunctions } from 'n8n-workflow'; import type { IDataObject, IExecuteFunctions, ITriggerFunctions } from 'n8n-workflow';
import { sleep } from 'n8n-workflow'; import { sleep } from 'n8n-workflow';
import { formatPrivateKey } from '@utils/utilities';
import * as amqplib from 'amqplib'; import * as amqplib from 'amqplib';
@ -20,10 +21,17 @@ export async function rabbitmqConnect(
if (credentials.ssl === true) { if (credentials.ssl === true) {
credentialData.protocol = 'amqps'; 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) { if (credentials.passwordless === true) {
optsData.cert = credentials.cert === '' ? undefined : Buffer.from(credentials.cert as string); optsData.cert =
optsData.key = credentials.key === '' ? undefined : Buffer.from(credentials.key as string); 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.passphrase = credentials.passphrase === '' ? undefined : credentials.passphrase;
optsData.credentials = amqplib.credentials.external(); optsData.credentials = amqplib.credentials.external();
} }

View file

@ -14,6 +14,7 @@ import type {
import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import { rabbitmqConnectExchange, rabbitmqConnectQueue } from './GenericFunctions'; import { rabbitmqConnectExchange, rabbitmqConnectQueue } from './GenericFunctions';
import { formatPrivateKey } from '@utils/utilities';
export class RabbitMQ implements INodeType { export class RabbitMQ implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -375,12 +376,18 @@ export class RabbitMQ implements INodeType {
credentialData.protocol = 'amqps'; credentialData.protocol = 'amqps';
optsData.ca = 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) { if (credentials.passwordless === true) {
optsData.cert = optsData.cert =
credentials.cert === '' ? undefined : Buffer.from(credentials.cert as string); credentials.cert === ''
? undefined
: Buffer.from(formatPrivateKey(credentials.cert as string));
optsData.key = optsData.key =
credentials.key === '' ? undefined : Buffer.from(credentials.key as string); credentials.key === ''
? undefined
: Buffer.from(formatPrivateKey(credentials.key as string));
optsData.passphrase = optsData.passphrase =
credentials.passphrase === '' ? undefined : credentials.passphrase; credentials.passphrase === '' ? undefined : credentials.passphrase;
optsData.credentials = amqplib.credentials.external(); optsData.credentials = amqplib.credentials.external();

View file

@ -10,6 +10,8 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow'; import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow';
import { formatPrivateKey } from '@utils/utilities';
import { rm, writeFile } from 'fs/promises'; import { rm, writeFile } from 'fs/promises';
import { file as tmpFile } from 'tmp-promise'; import { file as tmpFile } from 'tmp-promise';
@ -47,14 +49,6 @@ async function resolveHomeDir(
return path; 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 { export class Ssh implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'SSH', displayName: 'SSH',
@ -304,15 +298,11 @@ export class Ssh implements INodeType {
password: credentials.password as string, password: credentials.password as string,
}); });
} else { } else {
const { path } = await tmpFile({ prefix: 'n8n-ssh-' });
temporaryFiles.push(path);
await writeFile(path, sanitizePrivateKey(credentials.privateKey as string));
const options: Config = { const options: Config = {
host: credentials.host as string, host: credentials.host as string,
username: credentials.username as string, username: credentials.username as string,
port: credentials.port as number, port: credentials.port as number,
privateKey: path, privateKey: formatPrivateKey(credentials.privateKey as string),
}; };
if (credentials.passphrase) { if (credentials.passphrase) {
@ -364,16 +354,11 @@ export class Ssh implements INodeType {
}); });
} else if (authentication === 'privateKey') { } else if (authentication === 'privateKey') {
const credentials = await this.getCredentials('sshPrivateKey'); 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 = { const options: Config = {
host: credentials.host as string, host: credentials.host as string,
username: credentials.username as string, username: credentials.username as string,
port: credentials.port as number, port: credentials.port as number,
privateKey: path, privateKey: formatPrivateKey(credentials.privateKey as string),
}; };
if (credentials.passphrase) { if (credentials.passphrase) {

View file

@ -217,6 +217,36 @@ export const keysToLowercase = <T>(headers: T) => {
}, {} as IDataObject); }, {} 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 * @TECH_DEBT Explore replacing with handlebars
*/ */