Deprecate legacy encryption (#21831)
# Introduction Still preserving the cross-upgrade flow close https://github.com/twentyhq/core-team-issues/issues/2465 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21831?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+45
@@ -239,4 +239,49 @@ describe('SecretEncryptionService', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('decryptVersionedOrThrow', () => {
|
||||
it('round-trips a v2 envelope', () => {
|
||||
const secret = 'sk-strict-secret-value';
|
||||
const encrypted = service.encryptVersioned(secret as PlaintextString);
|
||||
|
||||
expect(service.decryptVersionedOrThrow(encrypted)).toBe(secret);
|
||||
});
|
||||
|
||||
it('throws on a legacy non-v2 value instead of falling back to CTR', () => {
|
||||
const legacyCiphertext = service.encrypt(testValue) as EncryptedString;
|
||||
|
||||
expect(() => service.decryptVersionedOrThrow(legacyCiphertext)).toThrow();
|
||||
});
|
||||
|
||||
it('returns null/undefined values as-is', () => {
|
||||
expect(
|
||||
service.decryptVersionedOrThrow(null as unknown as EncryptedString),
|
||||
).toBeNull();
|
||||
expect(
|
||||
service.decryptVersionedOrThrow(
|
||||
undefined as unknown as EncryptedString,
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacyDecryptVersionedWithFallback', () => {
|
||||
it('round-trips a v2 envelope', () => {
|
||||
const secret = 'sk-legacy-secret-value';
|
||||
const encrypted = service.encryptVersioned(secret as PlaintextString);
|
||||
|
||||
expect(service.legacyDecryptVersionedWithFallback(encrypted)).toBe(
|
||||
secret,
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to legacy CTR decryption for non-v2 values', () => {
|
||||
const legacyCiphertext = service.encrypt(testValue) as EncryptedString;
|
||||
|
||||
expect(service.legacyDecryptVersionedWithFallback(legacyCiphertext)).toBe(
|
||||
testValue,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+44
-2
@@ -4,6 +4,10 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type EncryptedString } from 'src/engine/core-modules/secret-encryption/branded-strings/encrypted-string.type';
|
||||
import { type PlaintextString } from 'src/engine/core-modules/secret-encryption/branded-strings/plaintext-string.type';
|
||||
import {
|
||||
SecretEncryptionException,
|
||||
SecretEncryptionExceptionCode,
|
||||
} from 'src/engine/core-modules/secret-encryption/exceptions/secret-encryption.exception';
|
||||
import { EnvironmentConfigDriver } from 'src/engine/core-modules/twenty-config/drivers/environment-config.driver';
|
||||
|
||||
import { computeEncryptionKeyId } from './utils/compute-encryption-key-id.util';
|
||||
@@ -87,7 +91,7 @@ export class SecretEncryptionService {
|
||||
}
|
||||
|
||||
return this.maskDecryptedValue(
|
||||
this.decryptVersioned(value, { workspaceId }),
|
||||
this.decryptVersionedOrThrow(value, { workspaceId }),
|
||||
mask,
|
||||
);
|
||||
}
|
||||
@@ -127,7 +131,45 @@ export class SecretEncryptionService {
|
||||
}) as EncryptedString;
|
||||
}
|
||||
|
||||
public decryptVersioned(
|
||||
public decryptVersionedOrThrow(
|
||||
value: EncryptedString,
|
||||
opts: VersionedOptions = {},
|
||||
): PlaintextString {
|
||||
if (!isDefined(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const parsed = parseSecretEncryptionEnvelopeOrThrow({ value });
|
||||
|
||||
if (parsed.version !== 2) {
|
||||
throw new SecretEncryptionException(
|
||||
'Expected an enc:v2 envelope but received a non-versioned value. The 2.5 encryption backfill instance commands must have run before this value can be decrypted.',
|
||||
SecretEncryptionExceptionCode.UNKNOWN_ENVELOPE_VERSION,
|
||||
);
|
||||
}
|
||||
|
||||
const keys = resolveEncryptionKeysOrThrow({
|
||||
environmentConfigDriver: this.environmentConfigDriver,
|
||||
});
|
||||
const rawKey = pickEncryptionKeyByKeyIdOrThrow({
|
||||
keyId: parsed.keyId,
|
||||
keys,
|
||||
});
|
||||
|
||||
return decryptAesGcmV2OrThrow({
|
||||
payloadBase64: parsed.payload,
|
||||
rawKey,
|
||||
workspaceId: opts.workspaceId,
|
||||
}) as PlaintextString;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Legacy variant kept only for the 2.5 encryption backfill
|
||||
* instance commands, which read pre-v2 rows (legacy AES-CTR ciphertext or
|
||||
* plaintext) and re-encrypt them into the enc:v2 envelope. Runtime and
|
||||
* rotation paths must use `decryptVersionedOrThrow` instead.
|
||||
*/
|
||||
public legacyDecryptVersionedWithFallback(
|
||||
value: EncryptedString,
|
||||
opts: VersionedOptions = {},
|
||||
): PlaintextString {
|
||||
|
||||
Reference in New Issue
Block a user