diff --git a/packages/cli/src/encryption/__tests__/key-manager.service.test.ts b/packages/cli/src/encryption/__tests__/key-manager.service.test.ts index 0693a6cb3f5..04831949777 100644 --- a/packages/cli/src/encryption/__tests__/key-manager.service.test.ts +++ b/packages/cli/src/encryption/__tests__/key-manager.service.test.ts @@ -694,13 +694,14 @@ describe('KeyManagerService', () => { const makeRepairService = (encryptionKey = randomBytes(24).toString('base64')) => { const repo = mock(); const cipher = realCipher(encryptionKey); + const logger = mock(); const service = new KeyManagerService( repo, cipher, mock({ encryptionKey }), - mock(), + 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([]); diff --git a/packages/cli/src/encryption/key-manager.service.ts b/packages/cli/src/encryption/key-manager.service.ts index bab97182c00..5c092d8880d 100644 --- a/packages/cli/src/encryption/key-manager.service.ts +++ b/packages/cli/src/encryption/key-manager.service.ts @@ -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; }