mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
refactor: Extract formatPemBlock into @n8n/utils (#32998)
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@n8n/utils": "workspace:*",
|
||||
"axios": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { createPrivateKey, createSign, randomUUID, X509Certificate } from 'node:crypto';
|
||||
|
||||
import { formatPrivateKey } from './format-private-key';
|
||||
|
||||
// private_key_jwt (RFC 7521/7523): the client proves its identity with a JWT
|
||||
// signed by its private key instead of a shared secret. The `x5t` header (SHA-1
|
||||
// thumbprint of the certificate) tells the server which public key verifies it.
|
||||
@@ -14,9 +13,7 @@ function base64url(input: Buffer | string): string {
|
||||
}
|
||||
|
||||
function certificateThumbprint(certificate: string): string {
|
||||
// `formatPrivateKey` also normalizes CERTIFICATE PEMs; the name-vs-usage
|
||||
// mismatch is resolved by the shared-helper rename tracked in ENT-114.
|
||||
const fingerprint = new X509Certificate(formatPrivateKey(certificate)).fingerprint;
|
||||
const fingerprint = new X509Certificate(formatPemBlock(certificate)).fingerprint;
|
||||
return Buffer.from(fingerprint.replace(/:/g, ''), 'hex').toString('base64url');
|
||||
}
|
||||
|
||||
@@ -44,7 +41,7 @@ export function buildClientAssertion(options: BuildClientAssertionOptions): stri
|
||||
|
||||
// `createSign('RSA-SHA256')` also signs EC/Ed25519 keys, producing a signature
|
||||
// that contradicts the pinned `alg: RS256` header. Reject non-RSA keys up front.
|
||||
const privateKey = createPrivateKey(formatPrivateKey(options.privateKey));
|
||||
const privateKey = createPrivateKey(formatPemBlock(options.privateKey));
|
||||
if (privateKey.asymmetricKeyType !== 'rsa') {
|
||||
throw new Error('Certificate authentication requires an RSA private key');
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// NOTE: Copied verbatim from `n8n-nodes-base` (utils/utilities.ts). It can't be
|
||||
// imported from there because nodes-base already depends on this package, so a
|
||||
// reverse dependency would be a cycle. De-duplicating both copies into a shared
|
||||
// low-level package is tracked in ENT-114.
|
||||
|
||||
const PEM_BODY_LINE_LENGTH = 64;
|
||||
|
||||
function formatCompactPem(privateKey: string, keyIsPublic: boolean): string | undefined {
|
||||
const trimmed = privateKey.trim();
|
||||
if ((trimmed.match(/-----BEGIN /g) ?? []).length !== 1) return undefined;
|
||||
|
||||
const labelPattern = keyIsPublic ? '[A-Z0-9 ]*PUBLIC KEY' : '[A-Z0-9 ]*PRIVATE KEY|CERTIFICATE';
|
||||
const pemMatch = trimmed.match(
|
||||
new RegExp(`^-----BEGIN (${labelPattern})-----([\\s\\S]*?)-----END \\1-----$`),
|
||||
);
|
||||
|
||||
if (!pemMatch) return undefined;
|
||||
|
||||
const [, label, body] = pemMatch;
|
||||
const normalizedBody = body.replace(/\\n/g, '\n').trim();
|
||||
const formattedBody = /\s/.test(normalizedBody)
|
||||
? normalizedBody.replace(/:\s+/g, ':').replace(/\s+/g, '\n')
|
||||
: (normalizedBody.match(new RegExp(`.{1,${PEM_BODY_LINE_LENGTH}}`, 'g')) ?? []).join('\n');
|
||||
|
||||
return `-----BEGIN ${label}-----\n${formattedBody}\n-----END ${label}-----`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, keyIsPublic = false): string {
|
||||
let regex = /(PRIVATE KEY|CERTIFICATE)/;
|
||||
if (keyIsPublic) {
|
||||
regex = /(PUBLIC KEY)/;
|
||||
}
|
||||
if (!privateKey || /\n/.test(privateKey)) {
|
||||
return privateKey;
|
||||
}
|
||||
const compactPem = formatCompactPem(privateKey, keyIsPublic);
|
||||
if (compactPem !== undefined) {
|
||||
return compactPem;
|
||||
}
|
||||
|
||||
let formattedPrivateKey = '';
|
||||
const parts = privateKey.split('-----').filter((item) => item !== '');
|
||||
parts.forEach((part) => {
|
||||
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').replace(/\s+/g, '\n');
|
||||
} else {
|
||||
formattedPrivateKey += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
|
||||
}
|
||||
}
|
||||
});
|
||||
return formattedPrivateKey;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { createVerify, generateKeyPairSync, X509Certificate } from 'node:crypto';
|
||||
|
||||
import { buildClientAssertion } from '@/client-assertion';
|
||||
import { formatPrivateKey } from '@/format-private-key';
|
||||
|
||||
import * as config from './config';
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('buildClientAssertion', () => {
|
||||
const verified = createVerify('RSA-SHA256')
|
||||
.update(`${headerSeg}.${payloadSeg}`)
|
||||
.verify(
|
||||
new X509Certificate(formatPrivateKey(config.certificate)).publicKey,
|
||||
new X509Certificate(formatPemBlock(config.certificate)).publicKey,
|
||||
Buffer.from(signatureSeg, 'base64url'),
|
||||
);
|
||||
expect(verified).toBe(true);
|
||||
|
||||
+4
-5
@@ -1,6 +1,7 @@
|
||||
import { ProjectsClient } from '@google-cloud/resource-manager';
|
||||
import { VertexAIEmbeddings } from '@langchain/google-vertexai';
|
||||
import { formatPrivateKey } from 'n8n-nodes-base/dist/utils/utilities';
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type {
|
||||
ILoadOptionsFunctions,
|
||||
@@ -10,8 +11,6 @@ import type {
|
||||
SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
|
||||
|
||||
export class EmbeddingsGoogleVertex implements INodeType {
|
||||
methods = {
|
||||
listSearch: {
|
||||
@@ -19,7 +18,7 @@ export class EmbeddingsGoogleVertex implements INodeType {
|
||||
const results: Array<{ name: string; value: string }> = [];
|
||||
|
||||
const credentials = await this.getCredentials('googleApi');
|
||||
const privateKey = formatPrivateKey(credentials.privateKey as string);
|
||||
const privateKey = formatPemBlock(credentials.privateKey as string);
|
||||
const email = (credentials.email as string).trim();
|
||||
|
||||
const client = new ProjectsClient({
|
||||
@@ -129,7 +128,7 @@ export class EmbeddingsGoogleVertex implements INodeType {
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials('googleApi');
|
||||
const privateKey = formatPrivateKey(credentials.privateKey as string);
|
||||
const privateKey = formatPemBlock(credentials.privateKey as string);
|
||||
const email = (credentials.email as string).trim();
|
||||
const region = credentials.region as string;
|
||||
|
||||
|
||||
+8
-8
@@ -1,7 +1,12 @@
|
||||
import { ProjectsClient } from '@google-cloud/resource-manager';
|
||||
import type { GoogleAISafetySetting } from '@langchain/google-common';
|
||||
import { ChatVertexAI, type ChatVertexAIInput } from '@langchain/google-vertexai';
|
||||
import { formatPrivateKey } from 'n8n-nodes-base/dist/utils/utilities';
|
||||
import {
|
||||
makeN8nLlmFailedAttemptHandler,
|
||||
N8nLlmTracing,
|
||||
getConnectionHintNoticeField,
|
||||
} from '@n8n/ai-utilities';
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import {
|
||||
NodeConnectionTypes,
|
||||
type INodeType,
|
||||
@@ -16,11 +21,6 @@ import {
|
||||
|
||||
import { makeErrorFromStatus } from './error-handling';
|
||||
import { getAdditionalOptions } from '../gemini-common/additional-options';
|
||||
import {
|
||||
makeN8nLlmFailedAttemptHandler,
|
||||
N8nLlmTracing,
|
||||
getConnectionHintNoticeField,
|
||||
} from '@n8n/ai-utilities';
|
||||
|
||||
export class LmChatGoogleVertex implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
@@ -106,7 +106,7 @@ export class LmChatGoogleVertex implements INodeType {
|
||||
const results: Array<{ name: string; value: string }> = [];
|
||||
|
||||
const credentials = await this.getCredentials('googleApi');
|
||||
const privateKey = formatPrivateKey(credentials.privateKey as string);
|
||||
const privateKey = formatPemBlock(credentials.privateKey as string);
|
||||
const email = (credentials.email as string).trim();
|
||||
|
||||
const client = new ProjectsClient({
|
||||
@@ -134,7 +134,7 @@ export class LmChatGoogleVertex implements INodeType {
|
||||
|
||||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials('googleApi');
|
||||
const privateKey = formatPrivateKey(credentials.privateKey as string);
|
||||
const privateKey = formatPemBlock(credentials.privateKey as string);
|
||||
const email = (credentials.email as string).trim();
|
||||
const region = credentials.region as string;
|
||||
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import { LmChatGoogleVertex } from '../LmChatGoogleVertex.node';
|
||||
|
||||
vi.mock('@langchain/google-vertexai');
|
||||
vi.mock('@n8n/ai-utilities');
|
||||
vi.mock('n8n-nodes-base/dist/utils/utilities', () => ({
|
||||
formatPrivateKey: vi.fn().mockImplementation((key: string) => key),
|
||||
vi.mock('@n8n/utils', () => ({
|
||||
formatPemBlock: vi.fn().mockImplementation((key: string) => key),
|
||||
}));
|
||||
|
||||
const MockedChatVertexAI = vi.mocked(ChatVertexAI);
|
||||
|
||||
@@ -280,6 +280,7 @@
|
||||
"@n8n/json-schema-to-zod": "workspace:*",
|
||||
"@n8n/typeorm": "0.3.20-16",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/utils": "workspace:*",
|
||||
"@oracle/langchain-oracledb": "0.2.0",
|
||||
"@pinecone-database/pinecone": "^5.0.2",
|
||||
"@qdrant/js-client-rest": "^1.16.2",
|
||||
|
||||
+46
-13
@@ -1,10 +1,12 @@
|
||||
import { formatPrivateKey } from '@/format-private-key';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('formatPrivateKey', () => {
|
||||
import { formatPemBlock } from './format-pem-block';
|
||||
|
||||
describe('formatPemBlock', () => {
|
||||
it('should format compact private PEM blocks with wrapped body lines', () => {
|
||||
const compactKey = `-----BEGIN OPENSSH PRIVATE KEY-----${'A'.repeat(130)}-----END OPENSSH PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
expect(formatPemBlock(compactKey)).toBe(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${'A'.repeat(64)}
|
||||
${'A'.repeat(64)}
|
||||
${'A'.repeat(2)}
|
||||
@@ -14,7 +16,7 @@ ${'A'.repeat(2)}
|
||||
it('should format compact public PEM blocks with wrapped body lines', () => {
|
||||
const compactKey = `-----BEGIN PUBLIC KEY-----${'B'.repeat(66)}-----END PUBLIC KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey, true)).toBe(`-----BEGIN PUBLIC KEY-----
|
||||
expect(formatPemBlock(compactKey, true)).toBe(`-----BEGIN PUBLIC KEY-----
|
||||
${'B'.repeat(64)}
|
||||
${'B'.repeat(2)}
|
||||
-----END PUBLIC KEY-----`);
|
||||
@@ -25,25 +27,34 @@ ${'B'.repeat(2)}
|
||||
ABC
|
||||
-----END OPENSSH PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(multilineKey)).toBe(multilineKey);
|
||||
expect(formatPemBlock(multilineKey)).toBe(multilineKey);
|
||||
});
|
||||
|
||||
it('should return empty string for empty input', () => {
|
||||
expect(formatPrivateKey('')).toBe('');
|
||||
expect(formatPemBlock('')).toBe('');
|
||||
});
|
||||
|
||||
it('should format compact RSA PRIVATE KEY block', () => {
|
||||
const compactKey = `-----BEGIN RSA PRIVATE KEY-----${'C'.repeat(64)}-----END RSA PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN RSA PRIVATE KEY-----
|
||||
expect(formatPemBlock(compactKey)).toBe(`-----BEGIN RSA PRIVATE KEY-----
|
||||
${'C'.repeat(64)}
|
||||
-----END RSA PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should format compact EC PRIVATE KEY block', () => {
|
||||
const compactKey = `-----BEGIN EC PRIVATE KEY-----${'D'.repeat(70)}-----END EC PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPemBlock(compactKey)).toBe(`-----BEGIN EC PRIVATE KEY-----
|
||||
${'D'.repeat(64)}
|
||||
${'D'.repeat(6)}
|
||||
-----END EC PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should format compact CERTIFICATE block (not just private keys)', () => {
|
||||
const compactCert = `-----BEGIN CERTIFICATE-----${'E'.repeat(128)}-----END CERTIFICATE-----`;
|
||||
|
||||
expect(formatPrivateKey(compactCert)).toBe(`-----BEGIN CERTIFICATE-----
|
||||
expect(formatPemBlock(compactCert)).toBe(`-----BEGIN CERTIFICATE-----
|
||||
${'E'.repeat(64)}
|
||||
${'E'.repeat(64)}
|
||||
-----END CERTIFICATE-----`);
|
||||
@@ -52,7 +63,7 @@ ${'E'.repeat(64)}
|
||||
it('should strip surrounding whitespace before formatting compact PEM', () => {
|
||||
const compactKey = ` -----BEGIN OPENSSH PRIVATE KEY-----${'A'.repeat(64)}-----END OPENSSH PRIVATE KEY----- `;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
expect(formatPemBlock(compactKey)).toBe(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${'A'.repeat(64)}
|
||||
-----END OPENSSH PRIVATE KEY-----`);
|
||||
});
|
||||
@@ -60,7 +71,7 @@ ${'A'.repeat(64)}
|
||||
it('should convert escaped \\n sequences in compact body to newlines', () => {
|
||||
const compactKey = `-----BEGIN PRIVATE KEY-----\\n${'F'.repeat(64)}\\n${'F'.repeat(32)}\\n-----END PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN PRIVATE KEY-----
|
||||
expect(formatPemBlock(compactKey)).toBe(`-----BEGIN PRIVATE KEY-----
|
||||
${'F'.repeat(64)}
|
||||
${'F'.repeat(32)}
|
||||
-----END PRIVATE KEY-----`);
|
||||
@@ -69,13 +80,35 @@ ${'F'.repeat(32)}
|
||||
it('should preserve a compact certificate chain unchanged (chain guard)', () => {
|
||||
const chain = `-----BEGIN CERTIFICATE-----${'A'.repeat(10)}-----END CERTIFICATE----------BEGIN CERTIFICATE-----${'B'.repeat(10)}-----END CERTIFICATE-----`;
|
||||
|
||||
expect(formatPrivateKey(chain)).toBe(chain);
|
||||
expect(formatPemBlock(chain)).toBe(chain);
|
||||
});
|
||||
|
||||
it('should preserve multi-line certificate chain unchanged', () => {
|
||||
const chain = `-----BEGIN CERTIFICATE-----
|
||||
AAA
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
BBB
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
expect(formatPemBlock(chain)).toBe(chain);
|
||||
});
|
||||
|
||||
it('should not match when BEGIN/END labels differ', () => {
|
||||
const mismatched = `-----BEGIN RSA PRIVATE KEY-----${'A'.repeat(64)}-----END EC PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(mismatched)).toBe(mismatched);
|
||||
expect(formatPemBlock(mismatched)).toBe(mismatched);
|
||||
});
|
||||
|
||||
it('should keep a multiline encrypted PEM with Proc-Type/DEK-Info unchanged', () => {
|
||||
const encrypted = `-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,1234567890ABCDEF
|
||||
|
||||
${'X'.repeat(64)}
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPemBlock(encrypted)).toBe(encrypted);
|
||||
});
|
||||
|
||||
it('should collapse Proc-Type/DEK-Info headers on the fallback path', () => {
|
||||
@@ -83,6 +116,6 @@ ${'F'.repeat(32)}
|
||||
// the encrypted-key headers exercises the Proc-Type/DEK-Info branch.
|
||||
const encrypted = `-----BEGIN RSA PRIVATE KEY-----Proc-Type: 4,ENCRYPTED ${'A'.repeat(20)}-----END EC PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(encrypted)).toContain('Proc-Type:4,ENCRYPTED');
|
||||
expect(formatPemBlock(encrypted)).toContain('Proc-Type:4,ENCRYPTED');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
const PEM_BODY_LINE_LENGTH = 64;
|
||||
|
||||
function formatCompactPem(pem: string, isPublic: boolean): string | undefined {
|
||||
const trimmed = pem.trim();
|
||||
if ((trimmed.match(/-----BEGIN /g) ?? []).length !== 1) return undefined;
|
||||
|
||||
const labelPattern = isPublic ? '[A-Z0-9 ]*PUBLIC KEY' : '[A-Z0-9 ]*PRIVATE KEY|CERTIFICATE';
|
||||
const pemMatch = trimmed.match(
|
||||
new RegExp(`^-----BEGIN (${labelPattern})-----([\\s\\S]*?)-----END \\1-----$`),
|
||||
);
|
||||
|
||||
if (!pemMatch) return undefined;
|
||||
|
||||
const [, label, body] = pemMatch;
|
||||
const normalizedBody = body.replace(/\\n/g, '\n').trim();
|
||||
const formattedBody = /\s/.test(normalizedBody)
|
||||
? normalizedBody.replace(/:\s+/g, ':').replace(/\s+/g, '\n')
|
||||
: (normalizedBody.match(new RegExp(`.{1,${PEM_BODY_LINE_LENGTH}}`, 'g')) ?? []).join('\n');
|
||||
|
||||
return `-----BEGIN ${label}-----\n${formattedBody}\n-----END ${label}-----`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a single PEM-encoded block (private key, public key, or certificate)
|
||||
* by collapsing whitespace and wrapping the body at 64 chars. Multi-block PEM
|
||||
* chains are returned unchanged.
|
||||
*
|
||||
* @param pem - The PEM-encoded block to format.
|
||||
* @param isPublic - When true, match `PUBLIC KEY` labels instead of the default `PRIVATE KEY` / `CERTIFICATE`.
|
||||
* @returns The formatted PEM block.
|
||||
*/
|
||||
export function formatPemBlock(pem: string, isPublic = false): string {
|
||||
let regex = /(PRIVATE KEY|CERTIFICATE)/;
|
||||
if (isPublic) {
|
||||
regex = /(PUBLIC KEY)/;
|
||||
}
|
||||
if (!pem || /\n/.test(pem)) {
|
||||
return pem;
|
||||
}
|
||||
const compactPem = formatCompactPem(pem, isPublic);
|
||||
if (compactPem !== undefined) {
|
||||
return compactPem;
|
||||
}
|
||||
|
||||
let formattedPem = '';
|
||||
const parts = pem.split('-----').filter((item) => item !== '');
|
||||
parts.forEach((part) => {
|
||||
if (regex.test(part)) {
|
||||
formattedPem += `-----${part}-----`;
|
||||
} else {
|
||||
const passRegex = /Proc-Type|DEK-Info/;
|
||||
if (passRegex.test(part)) {
|
||||
part = part.replace(/:\s+/g, ':');
|
||||
formattedPem += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
|
||||
} else {
|
||||
formattedPem += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
|
||||
}
|
||||
}
|
||||
});
|
||||
return formattedPem;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export * from './files/sanitize-filename';
|
||||
export * from './files/is-windows-file-path';
|
||||
export * from './placeholder';
|
||||
export * from './get-jwt-expiry';
|
||||
export * from './format-pem-block';
|
||||
export * from './scrub-secrets';
|
||||
export type * from './types';
|
||||
export * from './is-record';
|
||||
|
||||
@@ -30,11 +30,7 @@ import { deepCopy, Workflow } from 'n8n-workflow';
|
||||
import { generateKeyPairSync } from 'node:crypto';
|
||||
import { SalesforceJwtApi } from 'n8n-nodes-base/credentials/SalesforceJwtApi.credentials';
|
||||
|
||||
// The credential module resolves to nodes-base source, which uses a package-internal
|
||||
// path alias not mapped by cli's jest config.
|
||||
jest.mock('@utils/utilities', () => ({ formatPrivateKey: (key: string) => key }), {
|
||||
virtual: true,
|
||||
});
|
||||
jest.mock('@n8n/utils', () => ({ formatPemBlock: (key: string) => key }));
|
||||
|
||||
// SalesforceJwtApi.preAuthentication exchanges its signed JWT for a token through the
|
||||
// shared outbound HTTP client (`getTokenRequestClient`), not `this.helpers.httpRequest`.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type {
|
||||
IAuthenticateGeneric,
|
||||
ICredentialDataDecryptedObject,
|
||||
@@ -7,10 +9,6 @@ import type {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
export class GithubAppApi implements ICredentialType {
|
||||
name = 'githubAppApi';
|
||||
|
||||
@@ -66,7 +64,7 @@ export class GithubAppApi implements ICredentialType {
|
||||
): Promise<ICredentialDataDecryptedObject> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const baseUrl = String(credentials.server ?? 'https://api.github.com').replace(/\/$/, '');
|
||||
const privateKey = formatPrivateKey(credentials.privateKey as string);
|
||||
const privateKey = formatPemBlock(credentials.privateKey as string);
|
||||
|
||||
let appJwt: string;
|
||||
try {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import moment from 'moment-timezone';
|
||||
import type {
|
||||
@@ -10,8 +11,6 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
import { getTokenRequestClient, TOKEN_REQUEST_TIMEOUT } from './common/token-request';
|
||||
|
||||
export class SalesforceJwtApi implements ICredentialType {
|
||||
@@ -99,7 +98,7 @@ export class SalesforceJwtApi implements ICredentialType {
|
||||
async preAuthentication(this: IHttpRequestHelper, credentials: ICredentialDataDecryptedObject) {
|
||||
const now = moment().unix();
|
||||
const authUrl = resolveAuthUrl(credentials);
|
||||
const privateKey = formatPrivateKey(credentials.privateKey as string);
|
||||
const privateKey = formatPemBlock(credentials.privateKey as string);
|
||||
const signature = jwt.sign(
|
||||
{
|
||||
iss: credentials.clientId as string,
|
||||
|
||||
@@ -8,8 +8,8 @@ vi.mock('jsonwebtoken', () => ({
|
||||
default: { sign: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@utils/utilities', () => ({
|
||||
formatPrivateKey: (key: string) => key,
|
||||
vi.mock('@n8n/utils', () => ({
|
||||
formatPemBlock: (key: string) => key,
|
||||
}));
|
||||
|
||||
describe('GithubAppApi Credential', () => {
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ import { SalesforceJwtApi } from '../SalesforceJwtApi.credentials';
|
||||
vi.mock('jsonwebtoken', () => ({
|
||||
default: { sign: vi.fn(() => 'signed-jwt') },
|
||||
}));
|
||||
vi.mock('@utils/utilities', () => ({
|
||||
formatPrivateKey: (key: string) => key,
|
||||
vi.mock('@n8n/utils', () => ({
|
||||
formatPemBlock: (key: string) => key,
|
||||
}));
|
||||
|
||||
interface CapturedRequest {
|
||||
|
||||
@@ -14,8 +14,8 @@ import { SalesforceJwtApi, resolveAuthUrl } from '../SalesforceJwtApi.credential
|
||||
vi.mock('jsonwebtoken', () => ({
|
||||
default: { sign: vi.fn() },
|
||||
}));
|
||||
vi.mock('@utils/utilities', () => ({
|
||||
formatPrivateKey: (key: string) => key,
|
||||
vi.mock('@n8n/utils', () => ({
|
||||
formatPemBlock: (key: string) => key,
|
||||
}));
|
||||
|
||||
describe('SalesforceJwtApi Credential', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import type { BinaryToTextEncoding, CipherGCMTypes } from 'crypto';
|
||||
import {
|
||||
constants,
|
||||
@@ -25,8 +26,6 @@ import { deepCopy, BINARY_ENCODING, NodeConnectionTypes, NodeOperationError } fr
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { formatPrivateKey } from '../../../utils/utilities';
|
||||
|
||||
const unsupportedAlgorithms = [
|
||||
'RSA-MD4',
|
||||
'RSA-MDC2',
|
||||
@@ -604,7 +603,7 @@ export class CryptoV2 implements INodeType {
|
||||
'No private key set in credentials. Please add a private key to your Crypto credentials.',
|
||||
);
|
||||
}
|
||||
signPrivateKey = formatPrivateKey(credentials.signPrivateKey);
|
||||
signPrivateKey = formatPemBlock(credentials.signPrivateKey);
|
||||
}
|
||||
|
||||
if (action === 'encrypt' || action === 'decrypt') {
|
||||
@@ -627,7 +626,7 @@ export class CryptoV2 implements INodeType {
|
||||
'No encryption public key set in credentials. Please add an Encryption Public Key to your Crypto credentials.',
|
||||
);
|
||||
}
|
||||
encryptionPublicKey = formatPrivateKey(credentials.encryptionPublicKey, true);
|
||||
encryptionPublicKey = formatPemBlock(credentials.encryptionPublicKey, true);
|
||||
}
|
||||
|
||||
if (mode === 'asymmetric' && action === 'decrypt') {
|
||||
@@ -637,7 +636,7 @@ export class CryptoV2 implements INodeType {
|
||||
'No encryption private key set in credentials. Please add an Encryption Private Key to your Crypto credentials.',
|
||||
);
|
||||
}
|
||||
encryptionPrivateKey = formatPrivateKey(credentials.encryptionPrivateKey);
|
||||
encryptionPrivateKey = formatPemBlock(credentials.encryptionPrivateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { generatePairedItemData } from '@utils/utilities';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { BINARY_ENCODING, NodeApiError, NodeConnectionTypes } from 'n8n-workflow';
|
||||
import type {
|
||||
@@ -20,8 +22,6 @@ import type { Readable } from 'stream';
|
||||
import { pipeline } from 'stream/promises';
|
||||
import { file as tmpFile } from 'tmp-promise';
|
||||
|
||||
import { formatPrivateKey, generatePairedItemData } from '@utils/utilities';
|
||||
|
||||
interface ReturnFtpItem {
|
||||
type: string;
|
||||
name: string;
|
||||
@@ -558,7 +558,7 @@ export class Ftp implements INodeType {
|
||||
port: credentials.port as number,
|
||||
username: credentials.username as string,
|
||||
password: (credentials.password as string) || undefined,
|
||||
privateKey: formatPrivateKey(credentials.privateKey as string),
|
||||
privateKey: formatPemBlock(credentials.privateKey as string),
|
||||
passphrase: credentials.passphrase as string | undefined,
|
||||
});
|
||||
} else {
|
||||
@@ -613,7 +613,7 @@ export class Ftp implements INodeType {
|
||||
port: credentials.port as number,
|
||||
username: credentials.username as string,
|
||||
password: (credentials.password as string) || undefined,
|
||||
privateKey: formatPrivateKey(credentials.privateKey as string),
|
||||
privateKey: formatPemBlock(credentials.privateKey as string),
|
||||
passphrase: credentials.passphrase as string | undefined,
|
||||
readyTimeout: connectionTimeout,
|
||||
algorithms: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { DateTime } from 'luxon';
|
||||
import moment from 'moment-timezone';
|
||||
@@ -12,8 +13,6 @@ import {
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
export const googleServiceAccountScopes = {
|
||||
bigquery: ['https://www.googleapis.com/auth/bigquery'],
|
||||
books: ['https://www.googleapis.com/auth/books'],
|
||||
@@ -87,7 +86,7 @@ export async function getGoogleAccessToken(
|
||||
|
||||
const scopes = googleServiceAccountScopes[service];
|
||||
|
||||
const privateKey = formatPrivateKey(credentials.privateKey as string);
|
||||
const privateKey = formatPemBlock(credentials.privateKey as string);
|
||||
credentials.email = ((credentials.email as string) || '').trim();
|
||||
|
||||
const now = moment().unix();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import FormData from 'form-data';
|
||||
import get from 'lodash/get';
|
||||
import type { Readable } from 'stream';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import set from 'lodash/set';
|
||||
import {
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
type IOAuth2Options,
|
||||
type IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import type { Readable } from 'stream';
|
||||
import type { SecureContextOptions } from 'tls';
|
||||
|
||||
import type { HttpSslAuthCredentials } from './interfaces';
|
||||
import { formatPrivateKey } from '../../utils/utilities';
|
||||
|
||||
export type BodyParameter = {
|
||||
name: string;
|
||||
@@ -284,11 +284,11 @@ export const setAgentOptions = (
|
||||
) => {
|
||||
if (sslCertificates) {
|
||||
const agentOptions: SecureContextOptions = {};
|
||||
if (sslCertificates.ca) agentOptions.ca = formatPrivateKey(sslCertificates.ca);
|
||||
if (sslCertificates.cert) agentOptions.cert = formatPrivateKey(sslCertificates.cert);
|
||||
if (sslCertificates.key) agentOptions.key = formatPrivateKey(sslCertificates.key);
|
||||
if (sslCertificates.ca) agentOptions.ca = formatPemBlock(sslCertificates.ca);
|
||||
if (sslCertificates.cert) agentOptions.cert = formatPemBlock(sslCertificates.cert);
|
||||
if (sslCertificates.key) agentOptions.key = formatPemBlock(sslCertificates.key);
|
||||
if (sslCertificates.passphrase)
|
||||
agentOptions.passphrase = formatPrivateKey(sslCertificates.passphrase);
|
||||
agentOptions.passphrase = formatPemBlock(sslCertificates.passphrase);
|
||||
requestOptions.agentOptions = agentOptions;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import type {
|
||||
IDataObject,
|
||||
@@ -8,7 +9,6 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { formatPrivateKey } from '../../utils/utilities';
|
||||
import { parseJsonParameter } from '../Set/v2/helpers/utils';
|
||||
|
||||
const prettifyOperation = (operation: string) => {
|
||||
@@ -405,7 +405,7 @@ export class Jwt implements INodeType {
|
||||
if (credentials.keyType === 'passphrase') {
|
||||
secretOrPrivateKey = credentials.secret;
|
||||
} else {
|
||||
secretOrPrivateKey = formatPrivateKey(credentials.privateKey);
|
||||
secretOrPrivateKey = formatPemBlock(credentials.privateKey);
|
||||
}
|
||||
|
||||
const algorithm = options.algorithm ?? credentials.algorithm;
|
||||
@@ -442,7 +442,7 @@ export class Jwt implements INodeType {
|
||||
if (credentials.keyType === 'passphrase') {
|
||||
secretOrPublicKey = credentials.secret;
|
||||
} else {
|
||||
secretOrPublicKey = formatPrivateKey(credentials.publicKey, true);
|
||||
secretOrPublicKey = formatPemBlock(credentials.publicKey, true);
|
||||
}
|
||||
|
||||
const { ignoreExpiration, ignoreNotBefore, clockTolerance, complete } = options;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { connect, type IClientOptions, type MqttClient } from 'mqtt';
|
||||
import { OperationalError, randomString } from 'n8n-workflow';
|
||||
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
interface BaseMqttCredential {
|
||||
protocol: 'mqtt' | 'mqtts' | 'ws';
|
||||
host: string;
|
||||
@@ -44,9 +43,9 @@ export const createClient = async (credentials: MqttCredential): Promise<MqttCli
|
||||
}
|
||||
|
||||
if (credentials.ssl) {
|
||||
clientOptions.ca = formatPrivateKey(credentials.ca);
|
||||
clientOptions.cert = formatPrivateKey(credentials.cert);
|
||||
clientOptions.key = formatPrivateKey(credentials.key);
|
||||
clientOptions.ca = formatPemBlock(credentials.ca);
|
||||
clientOptions.cert = formatPemBlock(credentials.cert);
|
||||
clientOptions.key = formatPemBlock(credentials.key);
|
||||
clientOptions.rejectUnauthorized = credentials.rejectUnauthorized;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import { MongoClient, ObjectId } from 'mongodb';
|
||||
@@ -15,7 +16,6 @@ import type {
|
||||
IMongoCredentialsType,
|
||||
IMongoParametricCredentials,
|
||||
} from './mongoDb.types';
|
||||
import { formatPrivateKey } from '../../utils/utilities';
|
||||
|
||||
/**
|
||||
* Standard way of building the MongoDB connection string, unless overridden with a provided string
|
||||
@@ -196,9 +196,9 @@ export async function connectMongoClient(
|
||||
};
|
||||
|
||||
if (credentials.tls) {
|
||||
const ca = credentials.ca ? formatPrivateKey(credentials.ca as string) : undefined;
|
||||
const cert = credentials.cert ? formatPrivateKey(credentials.cert as string) : undefined;
|
||||
const key = credentials.key ? formatPrivateKey(credentials.key as string) : undefined;
|
||||
const ca = credentials.ca ? formatPemBlock(credentials.ca as string) : undefined;
|
||||
const cert = credentials.cert ? formatPemBlock(credentials.cert as string) : undefined;
|
||||
const key = credentials.key ? formatPemBlock(credentials.key as string) : undefined;
|
||||
const passphrase = (credentials.passphrase as string) || undefined;
|
||||
|
||||
const secureContext = createSecureContext({
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { LOCALHOST } from '@utils/constants';
|
||||
import mysql2 from 'mysql2/promise';
|
||||
import type {
|
||||
ICredentialTestFunctions,
|
||||
@@ -7,9 +9,6 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { createServer, type AddressInfo } from 'node:net';
|
||||
|
||||
import { LOCALHOST } from '@utils/constants';
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
import type { Mysql2Pool, MysqlNodeCredentials } from '../helpers/interfaces';
|
||||
|
||||
export async function createPool(
|
||||
@@ -32,12 +31,12 @@ export async function createPool(
|
||||
connectionOptions.ssl = {};
|
||||
|
||||
if (credentials.caCertificate) {
|
||||
connectionOptions.ssl.ca = formatPrivateKey(credentials.caCertificate);
|
||||
connectionOptions.ssl.ca = formatPemBlock(credentials.caCertificate);
|
||||
}
|
||||
|
||||
if (credentials.clientCertificate || credentials.clientPrivateKey) {
|
||||
connectionOptions.ssl.cert = formatPrivateKey(credentials.clientCertificate);
|
||||
connectionOptions.ssl.key = formatPrivateKey(credentials.clientPrivateKey);
|
||||
connectionOptions.ssl.cert = formatPemBlock(credentials.clientCertificate);
|
||||
connectionOptions.ssl.key = formatPemBlock(credentials.clientPrivateKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +64,7 @@ export async function createPool(
|
||||
return mysql2.createPool(connectionOptions);
|
||||
} else {
|
||||
if (credentials.sshAuthenticateWith === 'privateKey' && credentials.privateKey) {
|
||||
credentials.privateKey = formatPrivateKey(credentials.privateKey);
|
||||
credentials.privateKey = formatPemBlock(credentials.privateKey);
|
||||
}
|
||||
const sshClient = await this.helpers.getSSHClient(credentials);
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { ConnectionPoolManager } from '@utils/connection-pool-manager';
|
||||
import { LOCALHOST } from '@utils/constants';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
ICredentialTestFunctions,
|
||||
@@ -8,10 +11,6 @@ import type {
|
||||
import { createServer, type AddressInfo, type Server } from 'node:net';
|
||||
import pgPromise from 'pg-promise';
|
||||
|
||||
import { ConnectionPoolManager } from '@utils/connection-pool-manager';
|
||||
import { LOCALHOST } from '@utils/constants';
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
import type {
|
||||
ConnectionsData,
|
||||
PgpConnectionParameters,
|
||||
@@ -142,7 +141,7 @@ export async function configurePostgres(
|
||||
return { db, pgp };
|
||||
} else {
|
||||
if (credentials.sshAuthenticateWith === 'privateKey' && credentials.privateKey) {
|
||||
credentials.privateKey = formatPrivateKey(credentials.privateKey);
|
||||
credentials.privateKey = formatPemBlock(credentials.privateKey);
|
||||
}
|
||||
const sshClient = await this.helpers.getSSHClient(credentials, abortController);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import * as amqplib from 'amqplib';
|
||||
import type {
|
||||
IDeferredPromise,
|
||||
@@ -10,8 +11,6 @@ import type {
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, sleep } from 'n8n-workflow';
|
||||
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
import type { ExchangeType, Options, RabbitMQCredentials, TriggerOptions } from './types';
|
||||
|
||||
const credentialKeys = ['hostname', 'port', 'username', 'password', 'vhost'] as const;
|
||||
@@ -28,13 +27,12 @@ export async function rabbitmqConnect(
|
||||
if (credentials.ssl) {
|
||||
credentialData.protocol = 'amqps';
|
||||
|
||||
optsData.ca =
|
||||
credentials.ca === '' ? undefined : [Buffer.from(formatPrivateKey(credentials.ca))];
|
||||
optsData.ca = credentials.ca === '' ? undefined : [Buffer.from(formatPemBlock(credentials.ca))];
|
||||
if (credentials.passwordless) {
|
||||
optsData.cert =
|
||||
credentials.cert === '' ? undefined : Buffer.from(formatPrivateKey(credentials.cert));
|
||||
credentials.cert === '' ? undefined : Buffer.from(formatPemBlock(credentials.cert));
|
||||
optsData.key =
|
||||
credentials.key === '' ? undefined : Buffer.from(formatPrivateKey(credentials.key));
|
||||
credentials.key === '' ? undefined : Buffer.from(formatPemBlock(credentials.key));
|
||||
optsData.passphrase = credentials.passphrase === '' ? undefined : credentials.passphrase;
|
||||
optsData.credentials = amqplib.credentials.external();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import set from 'lodash/set';
|
||||
import type {
|
||||
@@ -24,7 +25,7 @@ import type { Readable } from 'stream';
|
||||
|
||||
import { getBinaryResponse } from './utils/binary';
|
||||
import { configuredOutputs } from './utils/outputs';
|
||||
import { formatPrivateKey, generatePairedItemData } from '../../utils/utilities';
|
||||
import { generatePairedItemData } from '../../utils/utilities';
|
||||
|
||||
const respondWithProperty: INodeProperties = {
|
||||
displayName: 'Respond With',
|
||||
@@ -450,7 +451,7 @@ export class RespondToWebhook implements INodeType {
|
||||
if (keyType === 'passphrase') {
|
||||
secretOrPrivateKey = secret;
|
||||
} else {
|
||||
secretOrPrivateKey = formatPrivateKey(privateKey);
|
||||
secretOrPrivateKey = formatPemBlock(privateKey);
|
||||
}
|
||||
const payload = this.getNodeParameter('payload', 0, {}) as IDataObject;
|
||||
const token = jwt.sign(payload, secretOrPrivateKey, { algorithm });
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { createPrivateKey } from 'crypto';
|
||||
import pick from 'lodash/pick';
|
||||
import type snowflake from 'snowflake-sdk';
|
||||
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
const commonConnectionFields = [
|
||||
'account',
|
||||
'database',
|
||||
@@ -36,7 +35,7 @@ export type SnowflakeCredential = Pick<
|
||||
);
|
||||
|
||||
const extractPrivateKey = (credential: { privateKey: string; passphrase?: string }) => {
|
||||
const key = formatPrivateKey(credential.privateKey);
|
||||
const key = formatPemBlock(credential.privateKey);
|
||||
|
||||
if (!credential.passphrase) return key;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import type {
|
||||
ICredentialTestFunctions,
|
||||
@@ -20,8 +21,6 @@ import { NodeSSH } from 'node-ssh';
|
||||
import type { Readable } from 'stream';
|
||||
import { file as tmpFile } from 'tmp-promise';
|
||||
|
||||
import { formatPrivateKey } from '@utils/utilities';
|
||||
|
||||
async function resolveHomeDir(
|
||||
this: IExecuteFunctions,
|
||||
path: string,
|
||||
@@ -301,7 +300,7 @@ export class Ssh implements INodeType {
|
||||
host: credentials.host as string,
|
||||
username: credentials.username as string,
|
||||
port: credentials.port as number,
|
||||
privateKey: formatPrivateKey(credentials.privateKey as string),
|
||||
privateKey: formatPemBlock(credentials.privateKey as string),
|
||||
};
|
||||
|
||||
if (credentials.passphrase) {
|
||||
@@ -354,7 +353,7 @@ export class Ssh implements INodeType {
|
||||
host: credentials.host as string,
|
||||
username: credentials.username as string,
|
||||
port: credentials.port as number,
|
||||
privateKey: formatPrivateKey(credentials.privateKey as string),
|
||||
privateKey: formatPemBlock(credentials.privateKey as string),
|
||||
};
|
||||
|
||||
if (credentials.passphrase) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatPemBlock } from '@n8n/utils';
|
||||
import basicAuth from 'basic-auth';
|
||||
import { rm } from 'fs/promises';
|
||||
import jwt from 'jsonwebtoken';
|
||||
@@ -15,7 +16,6 @@ import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
import { BlockList, isIPv6 } from 'node:net';
|
||||
|
||||
import { WebhookAuthorizationError } from './error';
|
||||
import { formatPrivateKey } from '../../utils/utilities';
|
||||
|
||||
export type WebhookParameters = {
|
||||
httpMethod: string | string[];
|
||||
@@ -342,7 +342,7 @@ export async function validateWebhookAuthentication(
|
||||
if (expectedAuth.keyType === 'passphrase') {
|
||||
secretOrPublicKey = expectedAuth.secret;
|
||||
} else {
|
||||
secretOrPublicKey = formatPrivateKey(expectedAuth.publicKey, true);
|
||||
secretOrPublicKey = formatPemBlock(expectedAuth.publicKey, true);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -924,6 +924,7 @@
|
||||
"@n8n/di": "workspace:*",
|
||||
"@n8n/errors": "workspace:*",
|
||||
"@n8n/imap": "workspace:*",
|
||||
"@n8n/utils": "workspace:*",
|
||||
"@thednp/dommatrix": "^2.0.12",
|
||||
"alasql": "4.4.0",
|
||||
"amqplib": "0.10.6",
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
addExecutionHints,
|
||||
compareItems,
|
||||
flattenKeys,
|
||||
formatPrivateKey,
|
||||
fuzzyCompare,
|
||||
getResolvables,
|
||||
keysToLowercase,
|
||||
@@ -85,128 +84,6 @@ describe('Test wrapData', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test formatPrivateKey', () => {
|
||||
it('should format compact private PEM blocks with wrapped body lines', () => {
|
||||
const compactKey = `-----BEGIN OPENSSH PRIVATE KEY-----${'A'.repeat(
|
||||
130,
|
||||
)}-----END OPENSSH PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${'A'.repeat(64)}
|
||||
${'A'.repeat(64)}
|
||||
${'A'.repeat(2)}
|
||||
-----END OPENSSH PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should format compact public PEM blocks with wrapped body lines', () => {
|
||||
const compactKey = `-----BEGIN PUBLIC KEY-----${'B'.repeat(66)}-----END PUBLIC KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey, true)).toBe(`-----BEGIN PUBLIC KEY-----
|
||||
${'B'.repeat(64)}
|
||||
${'B'.repeat(2)}
|
||||
-----END PUBLIC KEY-----`);
|
||||
});
|
||||
|
||||
it('should keep multiline PEM blocks unchanged', () => {
|
||||
const multilineKey = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
ABC
|
||||
-----END OPENSSH PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(multilineKey)).toBe(multilineKey);
|
||||
});
|
||||
|
||||
it('should return empty string for empty input', () => {
|
||||
expect(formatPrivateKey('')).toBe('');
|
||||
});
|
||||
|
||||
it('should format compact RSA PRIVATE KEY block', () => {
|
||||
const compactKey = `-----BEGIN RSA PRIVATE KEY-----${'C'.repeat(64)}-----END RSA PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN RSA PRIVATE KEY-----
|
||||
${'C'.repeat(64)}
|
||||
-----END RSA PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should format compact EC PRIVATE KEY block', () => {
|
||||
const compactKey = `-----BEGIN EC PRIVATE KEY-----${'D'.repeat(70)}-----END EC PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN EC PRIVATE KEY-----
|
||||
${'D'.repeat(64)}
|
||||
${'D'.repeat(6)}
|
||||
-----END EC PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should format compact CERTIFICATE block', () => {
|
||||
const compactCert = `-----BEGIN CERTIFICATE-----${'E'.repeat(128)}-----END CERTIFICATE-----`;
|
||||
|
||||
expect(formatPrivateKey(compactCert)).toBe(`-----BEGIN CERTIFICATE-----
|
||||
${'E'.repeat(64)}
|
||||
${'E'.repeat(64)}
|
||||
-----END CERTIFICATE-----`);
|
||||
});
|
||||
|
||||
it('should strip surrounding whitespace before formatting compact PEM', () => {
|
||||
const compactKey = ` -----BEGIN OPENSSH PRIVATE KEY-----${'A'.repeat(
|
||||
64,
|
||||
)}-----END OPENSSH PRIVATE KEY----- `;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
${'A'.repeat(64)}
|
||||
-----END OPENSSH PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should convert escaped \\n sequences in compact body to newlines', () => {
|
||||
const compactKey = `-----BEGIN PRIVATE KEY-----\\n${'F'.repeat(64)}\\n${'F'.repeat(
|
||||
32,
|
||||
)}\\n-----END PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(compactKey)).toBe(`-----BEGIN PRIVATE KEY-----
|
||||
${'F'.repeat(64)}
|
||||
${'F'.repeat(32)}
|
||||
-----END PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should preserve compact certificate chain structure (chain guard)', () => {
|
||||
const chain = `-----BEGIN CERTIFICATE-----${'A'.repeat(
|
||||
10,
|
||||
)}-----END CERTIFICATE----------BEGIN CERTIFICATE-----${'B'.repeat(
|
||||
10,
|
||||
)}-----END CERTIFICATE-----`;
|
||||
|
||||
expect(formatPrivateKey(chain)).toBe(chain);
|
||||
});
|
||||
|
||||
it('should preserve multi-line certificate chain unchanged', () => {
|
||||
const chain = `-----BEGIN CERTIFICATE-----
|
||||
AAA
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
BBB
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
expect(formatPrivateKey(chain)).toBe(chain);
|
||||
});
|
||||
|
||||
it('should not match when BEGIN/END labels differ', () => {
|
||||
const mismatched = `-----BEGIN RSA PRIVATE KEY-----${'A'.repeat(
|
||||
64,
|
||||
)}-----END EC PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(mismatched)).toBe(mismatched);
|
||||
});
|
||||
|
||||
it('should keep a multiline encrypted PEM with Proc-Type/DEK-Info unchanged', () => {
|
||||
const encrypted = `-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,1234567890ABCDEF
|
||||
|
||||
${'X'.repeat(64)}
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
|
||||
expect(formatPrivateKey(encrypted)).toBe(encrypted);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test keysToLowercase', () => {
|
||||
it('should convert keys to lowercase', () => {
|
||||
const headers = {
|
||||
|
||||
@@ -281,64 +281,6 @@ export const keysToLowercase = <T>(headers: T) => {
|
||||
}, {} as IDataObject);
|
||||
};
|
||||
|
||||
const PEM_BODY_LINE_LENGTH = 64;
|
||||
|
||||
function formatCompactPem(privateKey: string, keyIsPublic: boolean): string | undefined {
|
||||
const trimmed = privateKey.trim();
|
||||
if ((trimmed.match(/-----BEGIN /g) ?? []).length !== 1) return undefined;
|
||||
|
||||
const labelPattern = keyIsPublic ? '[A-Z0-9 ]*PUBLIC KEY' : '[A-Z0-9 ]*PRIVATE KEY|CERTIFICATE';
|
||||
const pemMatch = trimmed.match(
|
||||
new RegExp(`^-----BEGIN (${labelPattern})-----([\\s\\S]*?)-----END \\1-----$`),
|
||||
);
|
||||
|
||||
if (!pemMatch) return undefined;
|
||||
|
||||
const [, label, body] = pemMatch;
|
||||
const normalizedBody = body.replace(/\\n/g, '\n').trim();
|
||||
const formattedBody = /\s/.test(normalizedBody)
|
||||
? normalizedBody.replace(/:\s+/g, ':').replace(/\s+/g, '\n')
|
||||
: (normalizedBody.match(new RegExp(`.{1,${PEM_BODY_LINE_LENGTH}}`, 'g')) ?? []).join('\n');
|
||||
|
||||
return `-----BEGIN ${label}-----\n${formattedBody}\n-----END ${label}-----`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, keyIsPublic = false): string {
|
||||
let regex = /(PRIVATE KEY|CERTIFICATE)/;
|
||||
if (keyIsPublic) {
|
||||
regex = /(PUBLIC KEY)/;
|
||||
}
|
||||
if (!privateKey || /\n/.test(privateKey)) {
|
||||
return privateKey;
|
||||
}
|
||||
const compactPem = formatCompactPem(privateKey, keyIsPublic);
|
||||
if (compactPem !== undefined) {
|
||||
return compactPem;
|
||||
}
|
||||
|
||||
let formattedPrivateKey = '';
|
||||
const parts = privateKey.split('-----').filter((item) => item !== '');
|
||||
parts.forEach((part) => {
|
||||
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').replace(/\s+/g, '\n');
|
||||
} else {
|
||||
formattedPrivateKey += part.replace(/\\n/g, '\n').replace(/\s+/g, '\n');
|
||||
}
|
||||
}
|
||||
});
|
||||
return formattedPrivateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @TECH_DEBT Explore replacing with handlebars
|
||||
*/
|
||||
|
||||
Generated
+9
@@ -1549,6 +1549,9 @@ importers:
|
||||
|
||||
packages/@n8n/client-oauth2:
|
||||
dependencies:
|
||||
'@n8n/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
axios:
|
||||
specifier: 1.18.0
|
||||
version: 1.18.0(debug@4.4.3)
|
||||
@@ -2909,6 +2912,9 @@ importers:
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../typescript-config
|
||||
'@n8n/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../utils
|
||||
'@oracle/langchain-oracledb':
|
||||
specifier: 0.2.0
|
||||
version: 0.2.0(@langchain/core@1.1.41(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.34.0(ws@8.21.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(ws@8.21.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@langchain/textsplitters@1.0.1(@langchain/core@1.1.41(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.217.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.0))(openai@6.34.0(ws@8.21.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(ws@8.21.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))))(oracledb@6.10.0)
|
||||
@@ -5443,6 +5449,9 @@ importers:
|
||||
'@n8n/imap':
|
||||
specifier: workspace:*
|
||||
version: link:../@n8n/imap
|
||||
'@n8n/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../@n8n/utils
|
||||
'@thednp/dommatrix':
|
||||
specifier: ^2.0.12
|
||||
version: 2.0.12
|
||||
|
||||
Reference in New Issue
Block a user