fix(core): Repair data-encryption keys stored as the raw instance key (backport to release-candidate/2.39.x) (#38911)

Co-authored-by: Andreas Fitzek <andreas.fitzek@n8n.io>
This commit is contained in:
n8n-assistant[bot] 2026-09-17 08:24:43 +00:00 committed by GitHub
parent 829b4ac490
commit 954f6d8c8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 58 additions and 3 deletions

View File

@ -694,13 +694,14 @@ describe('KeyManagerService', () => {
const makeRepairService = (encryptionKey = randomBytes(24).toString('base64')) => {
const repo = mock<DeploymentKeyRepository>();
const cipher = realCipher(encryptionKey);
const logger = mock<Logger>();
const service = new KeyManagerService(
repo,
cipher,
mock<InstanceSettings>({ encryptionKey }),
mock<Logger>(),
logger,
);
return { service, repo, cipher };
return { service, repo, cipher, logger };
};
// A real, freshly generated DEK — never a hand-built constant.
@ -721,6 +722,18 @@ describe('KeyManagerService', () => {
expect(cipher.decryptDEKWithInstanceKey(wrapped)).toBe(rawKey);
});
it('recovers a raw instance key stored verbatim and re-wraps it as GCM', async () => {
const encryptionKey = randomBytes(24).toString('base64');
const { service, repo, cipher } = makeRepairService(encryptionKey);
repo.findDataEncryptionKeys.mockResolvedValue([makeKey({ id: 'k', value: encryptionKey })]);
await service.repairLegacyDataEncryptionKeys();
const [id, , wrapped] = repo.rewrapLegacyDataEncryptionValue.mock.calls[0];
expect(id).toBe('k');
expect(cipher.decryptDEKWithInstanceKey(wrapped)).toBe(encryptionKey);
});
it.each<[string, (cipher: Cipher, other: Cipher) => string]>([
['already GCM-wrapped', (cipher) => cipher.encryptDEKWithInstanceKey(rawKey)],
[
@ -740,6 +753,32 @@ describe('KeyManagerService', () => {
expect(repo.rewrapLegacyDataEncryptionValue).not.toHaveBeenCalled();
});
it('warns about an unrecognized value it cannot re-wrap', async () => {
const { service, repo, logger } = makeRepairService();
repo.findDataEncryptionKeys.mockResolvedValue([
makeKey({ id: 'k', value: randomBytes(48).toString('base64') }),
]);
await service.repairLegacyDataEncryptionKeys();
expect(repo.rewrapLegacyDataEncryptionValue).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('unrecognized format'), {
error: expect.any(Error),
});
});
it('does not warn about an already GCM-wrapped value', async () => {
const { service, repo, cipher, logger } = makeRepairService();
repo.findDataEncryptionKeys.mockResolvedValue([
makeKey({ id: 'k', value: cipher.encryptDEKWithInstanceKey(rawKey) }),
]);
await service.repairLegacyDataEncryptionKeys();
expect(repo.rewrapLegacyDataEncryptionValue).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
it('is a no-op when there are no data-encryption keys', async () => {
const { service, repo } = makeRepairService();
repo.findDataEncryptionKeys.mockResolvedValue([]);

View File

@ -134,6 +134,12 @@ export class KeyManagerService implements IEncryptionKeyProvider {
*/
private recoverLegacyDek(keyInfo: DeploymentKey): string | null {
const { value } = keyInfo;
// Older seed: the DEK stored as the instance key verbatim, unwrapped. Checked
// first so a value equal to the instance key is recovered whatever its shape.
if (value === this.instanceSettings.encryptionKey) {
return value;
}
// 2.18.x: raw key material, used directly. Re-wrap as-is, no decrypt needed.
if (RAW_DEK_PATTERN.test(value)) {
return value;
@ -159,7 +165,17 @@ export class KeyManagerService implements IEncryptionKeyProvider {
return recovered;
}
// Already GCM-wrapped or an unknown format: leave untouched.
// A value that already unwraps is current-format and needs no repair. Warn
// about anything else. A silent skip leaves every no-prefix read failing
// with a message that blames the instance key.
try {
this.cipher.decryptDEKWithInstanceKey(value);
} catch (error) {
this.logger.warn(
`DEK ${keyInfo.id} is in an unrecognized format. n8n cannot re-wrap it with this instance key, so reads of data without a key-id prefix will fail.`,
{ error },
);
}
return null;
}