mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-29 01:39:24 +08:00
fix: Restore legacy PEM headers when reformatting single-line keys (#37088)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Bernhard Wittmann <bernhard.wittmann@n8n.io>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { createPrivateKey, generateKeyPairSync } from 'node:crypto';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatPemBlock } from './format-pem-block';
|
||||
@@ -117,6 +118,47 @@ ${'X'.repeat(64)}
|
||||
expect(formatPemBlock(encrypted)).toBe(encrypted);
|
||||
});
|
||||
|
||||
it('should restore RFC 1421 headers in legacy encrypted RSA PEM keys', () => {
|
||||
// What the SSL Certificates credential stores after a multi-line legacy
|
||||
// encrypted RSA key is pasted into the single-line "Private Key" input:
|
||||
// newlines are collapsed to spaces, so the blank line separating the
|
||||
// Proc-Type/DEK-Info headers from the body becomes two consecutive spaces.
|
||||
const flattened =
|
||||
'-----BEGIN RSA PRIVATE KEY----- ' +
|
||||
'Proc-Type: 4,ENCRYPTED ' +
|
||||
'DEK-Info: AES-256-CBC,A4F349D0CD99508CA625518C9D671B68 ' + // double space = blank line
|
||||
'Pr9ZjzHxUr4HuhWspQ1vQHIgriYbTzbLdbXoWH/n6ABBRTocD3WO5/JFf83jZJzo ' +
|
||||
'yGnbXk6DK1JScbTHPYT6IuBqfDpQGB8FfFCZuANLwYtBZTFqVKdrsEHwZzGb0hSK ' +
|
||||
'-----END RSA PRIVATE KEY-----';
|
||||
|
||||
// OpenSSL 3 needs the blank line after the headers to pick a decoder for the
|
||||
// block, and ssh2 needs the space after each header colon to read the cipher
|
||||
// name; either one missing fails before the passphrase is ever consulted.
|
||||
expect(formatPemBlock(flattened)).toBe(`-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: AES-256-CBC,A4F349D0CD99508CA625518C9D671B68
|
||||
|
||||
Pr9ZjzHxUr4HuhWspQ1vQHIgriYbTzbLdbXoWH/n6ABBRTocD3WO5/JFf83jZJzo
|
||||
yGnbXk6DK1JScbTHPYT6IuBqfDpQGB8FfFCZuANLwYtBZTFqVKdrsEHwZzGb0hSK
|
||||
-----END RSA PRIVATE KEY-----`);
|
||||
});
|
||||
|
||||
it('should keep a flattened legacy encrypted RSA key loadable by OpenSSL', () => {
|
||||
const passphrase = 'passphrase';
|
||||
const { privateKey } = generateKeyPairSync('rsa', {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
|
||||
privateKeyEncoding: { type: 'pkcs1', format: 'pem', cipher: 'aes-256-cbc', passphrase },
|
||||
});
|
||||
const flattened = privateKey.trim().replace(/\n/g, ' ');
|
||||
const formatted = formatPemBlock(flattened);
|
||||
|
||||
expect(() => createPrivateKey({ key: formatted, passphrase })).not.toThrow();
|
||||
// ssh2 is not a dependency here, so pin the header shape its parser relies on:
|
||||
// it slices the DEK-Info value past a fixed ": " offset.
|
||||
expect(formatted).toMatch(/\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,[0-9A-F]+\n\n/);
|
||||
});
|
||||
|
||||
it('should collapse Proc-Type/DEK-Info headers on the fallback path', () => {
|
||||
// Mismatched labels force the fallback formatter, where a body chunk carrying
|
||||
// the encrypted-key headers exercises the Proc-Type/DEK-Info branch.
|
||||
|
||||
@@ -14,7 +14,17 @@ function formatCompactPem(pem: string, isPublic: boolean): string | 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
|
||||
.replace(/:\s+/g, ':')
|
||||
.replace(/\s+/g, '\n')
|
||||
// Restore RFC 1421 shape for a legacy encrypted key's headers. Both halves
|
||||
// are load-bearing: OpenSSL 3 matches no decoder without the blank line
|
||||
// before the body, and ssh2 reads the DEK-Info value at a fixed offset that
|
||||
// assumes a single space after the colon.
|
||||
.replace(
|
||||
/^(?:(?:Proc-Type|DEK-Info):\S+\n)+/,
|
||||
(headers) => `${headers.replace(/^(Proc-Type|DEK-Info):/gm, '$1: ')}\n`,
|
||||
)
|
||||
: (normalizedBody.match(new RegExp(`.{1,${PEM_BODY_LINE_LENGTH}}`, 'g')) ?? []).join('\n');
|
||||
|
||||
return `-----BEGIN ${label}-----\n${formattedBody}\n-----END ${label}-----`;
|
||||
|
||||
Reference in New Issue
Block a user