fix(core): Prevent issues with missing or mismatching encryption key (#8332)

This commit is contained in:
Iván Ovejero 2024-01-16 18:25:53 +01:00 committed by GitHub
parent 7bb2d1799e
commit d4c93b1607
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 62 additions and 45 deletions

View file

@ -259,6 +259,13 @@ export class Worker extends BaseCommand {
constructor(argv: string[], cmdConfig: IConfig) {
super(argv, cmdConfig);
if (!process.env.N8N_ENCRYPTION_KEY) {
throw new ApplicationError(
'Missing encryption key. Worker started without the required N8N_ENCRYPTION_KEY env var. More information: https://docs.n8n.io/hosting/environment-variables/configuration-methods/#encryption-key',
);
}
this.setInstanceType('worker');
this.setInstanceQueueModeId();
}

View file

@ -14,7 +14,7 @@ if (inE2ETests) {
process.env.N8N_AI_ENABLED = 'true';
} else if (inTest) {
process.env.N8N_LOG_LEVEL = 'silent';
process.env.N8N_ENCRYPTION_KEY = 'test-encryption-key';
process.env.N8N_ENCRYPTION_KEY = 'test_key';
process.env.N8N_PUBLIC_API_DISABLED = 'true';
process.env.SKIP_STATISTICS_EVENTS = 'true';
} else {

View file

@ -11,6 +11,6 @@ process.env.N8N_USER_FOLDER = testDir;
writeFileSync(
join(testDir, '.n8n/config'),
JSON.stringify({ encryptionKey: 'testkey', instanceId: '123' }),
JSON.stringify({ encryptionKey: 'test_key', instanceId: '123' }),
'utf-8',
);

View file

@ -41,9 +41,9 @@ export class Credentials extends ICredentials {
throw new ApplicationError('No data is set so nothing can be returned.');
}
const decryptedData = this.cipher.decrypt(this.data);
try {
const decryptedData = this.cipher.decrypt(this.data);
return jsonParse(decryptedData);
} catch (e) {
throw new ApplicationError(

View file

@ -2,7 +2,7 @@ import path from 'path';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { createHash, randomBytes } from 'crypto';
import { Service } from 'typedi';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
interface ReadOnlySettings {
encryptionKey: string;
@ -14,6 +14,8 @@ interface WritableSettings {
type Settings = ReadOnlySettings & WritableSettings;
const inTest = process.env.NODE_ENV === 'test';
@Service()
export class InstanceSettings {
private readonly userHome = this.getUserHome();
@ -57,26 +59,44 @@ export class InstanceSettings {
return process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd();
}
/**
* Load instance settings from the settings file. If missing, create a new
* settings file with an auto-generated encryption key.
*/
private loadOrCreate(): Settings {
let settings: Settings;
const { settingsFile } = this;
if (existsSync(settingsFile)) {
const content = readFileSync(settingsFile, 'utf8');
settings = jsonParse(content, {
errorMessage: `Error parsing n8n-config file "${settingsFile}". It does not seem to be valid JSON.`,
if (existsSync(this.settingsFile)) {
const content = readFileSync(this.settingsFile, 'utf8');
const settings = jsonParse<Settings>(content, {
errorMessage: `Error parsing n8n-config file "${this.settingsFile}". It does not seem to be valid JSON.`,
});
} else {
// Ensure that the `.n8n` folder exists
mkdirSync(this.n8nFolder, { recursive: true });
// If file doesn't exist, create new settings
const encryptionKey = process.env.N8N_ENCRYPTION_KEY ?? randomBytes(24).toString('base64');
settings = { encryptionKey };
this.save(settings);
// console.info(`UserSettings were generated and saved to: ${settingsFile}`);
if (!inTest) console.info(`User settings loaded from: ${this.settingsFile}`);
const { encryptionKey, tunnelSubdomain } = settings;
if (process.env.N8N_ENCRYPTION_KEY && encryptionKey !== process.env.N8N_ENCRYPTION_KEY) {
throw new ApplicationError(
`Mismatching encryption keys. The encryption key in the settings file ${this.settingsFile} does not match the N8N_ENCRYPTION_KEY env var. Please make sure both keys match. More information: https://docs.n8n.io/hosting/environment-variables/configuration-methods/#encryption-key`,
);
}
return { encryptionKey, tunnelSubdomain };
}
const { encryptionKey, tunnelSubdomain } = settings;
return { encryptionKey, tunnelSubdomain };
mkdirSync(this.n8nFolder, { recursive: true });
const encryptionKey = process.env.N8N_ENCRYPTION_KEY ?? randomBytes(24).toString('base64');
const settings: Settings = { encryptionKey };
this.save(settings);
if (!inTest && !process.env.N8N_ENCRYPTION_KEY) {
console.info(`No encryption key found - Auto-generated and saved to: ${this.settingsFile}`);
}
return settings;
}
private generateInstanceId() {

View file

@ -24,6 +24,12 @@ describe('InstanceSettings', () => {
readSpy.mockReturnValue('{"encryptionKey":"test_key"');
expect(() => new InstanceSettings()).toThrowError();
});
it('should throw if the env and file keys do not match', () => {
readSpy.mockReturnValue(JSON.stringify({ encryptionKey: 'key_1' }));
process.env.N8N_ENCRYPTION_KEY = 'key_2';
expect(() => new InstanceSettings()).toThrowError();
});
});
describe('If the settings file does not exist', () => {

View file

@ -1259,38 +1259,22 @@ export class HttpRequestV3 implements INodeType {
genericCredentialType = this.getNodeParameter('genericAuthType', 0) as string;
if (genericCredentialType === 'httpBasicAuth') {
try {
httpBasicAuth = await this.getCredentials('httpBasicAuth', itemIndex);
} catch {}
httpBasicAuth = await this.getCredentials('httpBasicAuth', itemIndex);
} else if (genericCredentialType === 'httpDigestAuth') {
try {
httpDigestAuth = await this.getCredentials('httpDigestAuth', itemIndex);
} catch {}
httpDigestAuth = await this.getCredentials('httpDigestAuth', itemIndex);
} else if (genericCredentialType === 'httpHeaderAuth') {
try {
httpHeaderAuth = await this.getCredentials('httpHeaderAuth', itemIndex);
} catch {}
httpHeaderAuth = await this.getCredentials('httpHeaderAuth', itemIndex);
} else if (genericCredentialType === 'httpQueryAuth') {
try {
httpQueryAuth = await this.getCredentials('httpQueryAuth', itemIndex);
} catch {}
httpQueryAuth = await this.getCredentials('httpQueryAuth', itemIndex);
} else if (genericCredentialType === 'httpCustomAuth') {
try {
httpCustomAuth = await this.getCredentials('httpCustomAuth', itemIndex);
} catch {}
httpCustomAuth = await this.getCredentials('httpCustomAuth', itemIndex);
} else if (genericCredentialType === 'oAuth1Api') {
try {
oAuth1Api = await this.getCredentials('oAuth1Api', itemIndex);
} catch {}
oAuth1Api = await this.getCredentials('oAuth1Api', itemIndex);
} else if (genericCredentialType === 'oAuth2Api') {
try {
oAuth2Api = await this.getCredentials('oAuth2Api', itemIndex);
} catch {}
oAuth2Api = await this.getCredentials('oAuth2Api', itemIndex);
}
} else if (authentication === 'predefinedCredentialType') {
try {
nodeCredentialType = this.getNodeParameter('nodeCredentialType', 0) as string;
} catch {}
nodeCredentialType = this.getNodeParameter('nodeCredentialType', 0) as string;
}
const requestMethod = this.getNodeParameter('method', itemIndex) as string;