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,
} 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({

View file

@ -11,6 +11,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'],
@ -69,7 +71,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();

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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();
}

View file

@ -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 = {
@ -375,12 +376,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();

View file

@ -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) {

View file

@ -217,6 +217,36 @@ export const keysToLowercase = <T>(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
*/