mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
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:
parent
829b4ac490
commit
954f6d8c8c
|
|
@ -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([]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user