mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
fix(core): Prevent issues with missing or mismatching encryption key (#8332)
This commit is contained in:
parent
7bb2d1799e
commit
d4c93b1607
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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',
|
||||||
);
|
);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue