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) { constructor(argv: string[], cmdConfig: IConfig) {
super(argv, cmdConfig); 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.setInstanceType('worker');
this.setInstanceQueueModeId(); this.setInstanceQueueModeId();
} }

View file

@ -14,7 +14,7 @@ if (inE2ETests) {
process.env.N8N_AI_ENABLED = 'true'; process.env.N8N_AI_ENABLED = 'true';
} else if (inTest) { } else if (inTest) {
process.env.N8N_LOG_LEVEL = 'silent'; 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.N8N_PUBLIC_API_DISABLED = 'true';
process.env.SKIP_STATISTICS_EVENTS = 'true'; process.env.SKIP_STATISTICS_EVENTS = 'true';
} else { } else {

View file

@ -11,6 +11,6 @@ process.env.N8N_USER_FOLDER = testDir;
writeFileSync( writeFileSync(
join(testDir, '.n8n/config'), join(testDir, '.n8n/config'),
JSON.stringify({ encryptionKey: 'testkey', instanceId: '123' }), JSON.stringify({ encryptionKey: 'test_key', instanceId: '123' }),
'utf-8', '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.'); throw new ApplicationError('No data is set so nothing can be returned.');
} }
const decryptedData = this.cipher.decrypt(this.data);
try { try {
const decryptedData = this.cipher.decrypt(this.data);
return jsonParse(decryptedData); return jsonParse(decryptedData);
} catch (e) { } catch (e) {
throw new ApplicationError( throw new ApplicationError(

View file

@ -2,7 +2,7 @@ import path from 'path';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { createHash, randomBytes } from 'crypto'; import { createHash, randomBytes } from 'crypto';
import { Service } from 'typedi'; import { Service } from 'typedi';
import { jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
interface ReadOnlySettings { interface ReadOnlySettings {
encryptionKey: string; encryptionKey: string;
@ -14,6 +14,8 @@ interface WritableSettings {
type Settings = ReadOnlySettings & WritableSettings; type Settings = ReadOnlySettings & WritableSettings;
const inTest = process.env.NODE_ENV === 'test';
@Service() @Service()
export class InstanceSettings { export class InstanceSettings {
private readonly userHome = this.getUserHome(); private readonly userHome = this.getUserHome();
@ -57,26 +59,44 @@ export class InstanceSettings {
return process.env.N8N_USER_FOLDER ?? process.env[homeVarName] ?? process.cwd(); 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 { private loadOrCreate(): Settings {
let settings: Settings; if (existsSync(this.settingsFile)) {
const { settingsFile } = this; const content = readFileSync(this.settingsFile, 'utf8');
if (existsSync(settingsFile)) {
const content = readFileSync(settingsFile, 'utf8'); const settings = jsonParse<Settings>(content, {
settings = jsonParse(content, { errorMessage: `Error parsing n8n-config file "${this.settingsFile}". It does not seem to be valid JSON.`,
errorMessage: `Error parsing n8n-config file "${settingsFile}". It does not seem to be valid JSON.`,
}); });
} else {
// Ensure that the `.n8n` folder exists if (!inTest) console.info(`User settings loaded from: ${this.settingsFile}`);
mkdirSync(this.n8nFolder, { recursive: true });
// If file doesn't exist, create new settings const { encryptionKey, tunnelSubdomain } = settings;
const encryptionKey = process.env.N8N_ENCRYPTION_KEY ?? randomBytes(24).toString('base64');
settings = { encryptionKey }; if (process.env.N8N_ENCRYPTION_KEY && encryptionKey !== process.env.N8N_ENCRYPTION_KEY) {
this.save(settings); throw new ApplicationError(
// console.info(`UserSettings were generated and saved to: ${settingsFile}`); `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; mkdirSync(this.n8nFolder, { recursive: true });
return { encryptionKey, tunnelSubdomain };
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() { private generateInstanceId() {

View file

@ -24,6 +24,12 @@ describe('InstanceSettings', () => {
readSpy.mockReturnValue('{"encryptionKey":"test_key"'); readSpy.mockReturnValue('{"encryptionKey":"test_key"');
expect(() => new InstanceSettings()).toThrowError(); 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', () => { 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; genericCredentialType = this.getNodeParameter('genericAuthType', 0) as string;
if (genericCredentialType === 'httpBasicAuth') { if (genericCredentialType === 'httpBasicAuth') {
try { httpBasicAuth = await this.getCredentials('httpBasicAuth', itemIndex);
httpBasicAuth = await this.getCredentials('httpBasicAuth', itemIndex);
} catch {}
} else if (genericCredentialType === 'httpDigestAuth') { } else if (genericCredentialType === 'httpDigestAuth') {
try { httpDigestAuth = await this.getCredentials('httpDigestAuth', itemIndex);
httpDigestAuth = await this.getCredentials('httpDigestAuth', itemIndex);
} catch {}
} else if (genericCredentialType === 'httpHeaderAuth') { } else if (genericCredentialType === 'httpHeaderAuth') {
try { httpHeaderAuth = await this.getCredentials('httpHeaderAuth', itemIndex);
httpHeaderAuth = await this.getCredentials('httpHeaderAuth', itemIndex);
} catch {}
} else if (genericCredentialType === 'httpQueryAuth') { } else if (genericCredentialType === 'httpQueryAuth') {
try { httpQueryAuth = await this.getCredentials('httpQueryAuth', itemIndex);
httpQueryAuth = await this.getCredentials('httpQueryAuth', itemIndex);
} catch {}
} else if (genericCredentialType === 'httpCustomAuth') { } else if (genericCredentialType === 'httpCustomAuth') {
try { httpCustomAuth = await this.getCredentials('httpCustomAuth', itemIndex);
httpCustomAuth = await this.getCredentials('httpCustomAuth', itemIndex);
} catch {}
} else if (genericCredentialType === 'oAuth1Api') { } else if (genericCredentialType === 'oAuth1Api') {
try { oAuth1Api = await this.getCredentials('oAuth1Api', itemIndex);
oAuth1Api = await this.getCredentials('oAuth1Api', itemIndex);
} catch {}
} else if (genericCredentialType === 'oAuth2Api') { } else if (genericCredentialType === 'oAuth2Api') {
try { oAuth2Api = await this.getCredentials('oAuth2Api', itemIndex);
oAuth2Api = await this.getCredentials('oAuth2Api', itemIndex);
} catch {}
} }
} else if (authentication === 'predefinedCredentialType') { } else if (authentication === 'predefinedCredentialType') {
try { nodeCredentialType = this.getNodeParameter('nodeCredentialType', 0) as string;
nodeCredentialType = this.getNodeParameter('nodeCredentialType', 0) as string;
} catch {}
} }
const requestMethod = this.getNodeParameter('method', itemIndex) as string; const requestMethod = this.getNodeParameter('method', itemIndex) as string;